mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-02-27 01:14:07 +01:00
❤️ 完整开源界面和内核 https://github.com/siyuan-note/siyuan/issues/5013
This commit is contained in:
parent
e650b8100c
commit
f40ed985e1
1214 changed files with 345766 additions and 9 deletions
24
kernel/util/cmdattr.go
Normal file
24
kernel/util/cmdattr.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package util
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func CmdAttr(cmd *exec.Cmd) {
|
||||
}
|
||||
26
kernel/util/cmdattr_windows.go
Normal file
26
kernel/util/cmdattr_windows.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func CmdAttr(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
}
|
||||
76
kernel/util/crypt.go
Normal file
76
kernel/util/crypt.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
var SK = []byte("696D897C9AA0611B")
|
||||
|
||||
func AESEncrypt(str string) string {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.Grow(4096)
|
||||
_, err := hex.NewEncoder(buf).Write([]byte(str))
|
||||
if nil != err {
|
||||
LogErrorf("encrypt failed: %s", err)
|
||||
return ""
|
||||
}
|
||||
data := buf.Bytes()
|
||||
block, err := aes.NewCipher(SK)
|
||||
if nil != err {
|
||||
LogErrorf("encrypt failed: %s", err)
|
||||
return ""
|
||||
}
|
||||
cbc := cipher.NewCBCEncrypter(block, []byte("RandomInitVector"))
|
||||
content := data
|
||||
content = pkcs5Padding(content, block.BlockSize())
|
||||
crypted := make([]byte, len(content))
|
||||
cbc.CryptBlocks(crypted, content)
|
||||
return hex.EncodeToString(crypted)
|
||||
}
|
||||
|
||||
func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(ciphertext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(ciphertext, padtext...)
|
||||
}
|
||||
|
||||
func AESDecrypt(cryptStr string) []byte {
|
||||
crypt, err := hex.DecodeString(cryptStr)
|
||||
if nil != err {
|
||||
LogErrorf("decrypt failed: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(SK)
|
||||
if nil != err {
|
||||
return nil
|
||||
}
|
||||
cbc := cipher.NewCBCDecrypter(block, []byte("RandomInitVector"))
|
||||
decrypted := make([]byte, len(crypt))
|
||||
cbc.CryptBlocks(decrypted, crypt)
|
||||
return pkcs5Trimming(decrypted)
|
||||
}
|
||||
|
||||
func pkcs5Trimming(encrypt []byte) []byte {
|
||||
padding := encrypt[len(encrypt)-1]
|
||||
return encrypt[:len(encrypt)-int(padding)]
|
||||
}
|
||||
42
kernel/util/emoji.go
Normal file
42
kernel/util/emoji.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import "regexp"
|
||||
|
||||
var emojiRegex = regexp.MustCompile(`/([0-9#][\x{20E3}])|` +
|
||||
`[\x{00ae}\x{00a9}\x{203C}\x{2047}\x{2048}\x{2049}\x{3030}\x{303D}\x{2139}\x{2122}\x{3297}\x{3299}]|` +
|
||||
`[\x{2190}-\x{21FF}]|[\x{FE00}-\x{FEFF}]|` +
|
||||
`[\x{2300}-\x{23FF}]|[\x{FE00}-\x{FEFF}]|` +
|
||||
`[\x{2460}-\x{24FF}]|[\x{FE00}-\x{FEFF}]|` +
|
||||
`[\x{25A0}-\x{25FF}]|[\x{FE00}-\x{FEFF}]|` +
|
||||
`[\x{2600}-\x{27BF}]|[\x{FE00}-\x{FEFF}]|` +
|
||||
`[\x{2900}-\x{297F}]|[\x{FE00}-\x{FEFF}]|` +
|
||||
`[\x{2B00}-\x{2BF0}]|[\x{FE00}-\x{FEFF}]|` +
|
||||
`[\x{1F000}-\x{1F6FF}]|[\x{FE00}-\x{FEFF}]|` +
|
||||
`[\x{1F600}-\x{1F64F}]|` +
|
||||
`[\x{1F680}-\x{1F6FF}]|` +
|
||||
`[\x{1F900}-\x{1F9FF}]|` +
|
||||
`[\x{1F1E0}-\x{1F1FF}]|` +
|
||||
`[\x{1D100}-\x{1D1FF}]|` +
|
||||
`[\x{2600}-\x{26FF}]|` +
|
||||
`[\x{2700}-\x{27BF}]|` +
|
||||
`[\x{10000}-\x{E01EF}]`)
|
||||
|
||||
func RemoveEmoji(text string) string {
|
||||
return emojiRegex.ReplaceAllString(text, "")
|
||||
}
|
||||
218
kernel/util/file.go
Normal file
218
kernel/util/file.go
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
)
|
||||
|
||||
func IsEmptyDir(p string) bool {
|
||||
if !gulu.File.IsDir(p) {
|
||||
return false
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(p)
|
||||
if nil != err {
|
||||
return false
|
||||
}
|
||||
return 1 > len(files)
|
||||
}
|
||||
|
||||
func IsValidJSON(p string) bool {
|
||||
if !gulu.File.IsExist(p) {
|
||||
return false
|
||||
}
|
||||
data, err := os.ReadFile(p)
|
||||
if nil != err {
|
||||
LogErrorf("read json file [%s] failed: %s", p, err)
|
||||
return false
|
||||
}
|
||||
|
||||
json := map[string]interface{}{}
|
||||
if err = gulu.JSON.UnmarshalJSON(data, &json); nil != err {
|
||||
LogErrorf("parse json file [%s] failed: %s", p, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func RemoveID(name string) string {
|
||||
ext := path.Ext(name)
|
||||
name = strings.TrimSuffix(name, ext)
|
||||
if 23 < len(name) {
|
||||
name = name[:len(name)-23]
|
||||
}
|
||||
return name + ext
|
||||
}
|
||||
|
||||
func LastID(p string) (name, id string) {
|
||||
name = path.Base(p)
|
||||
ext := path.Ext(name)
|
||||
id = strings.TrimSuffix(name, ext)
|
||||
if 22 < len(id) {
|
||||
id = id[len(id)-22:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func LatestTmpFile(p string) string {
|
||||
dir, base := filepath.Split(p)
|
||||
files, err := os.ReadDir(dir)
|
||||
if nil != err {
|
||||
LogErrorf("read dir [%s] failed: %s", dir, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
var tmps []os.DirEntry
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(f.Name(), ".tmp") && strings.HasPrefix(f.Name(), base) && len(base)+7+len(".tmp") == len(f.Name()) {
|
||||
tmps = append(tmps, f)
|
||||
}
|
||||
}
|
||||
|
||||
if 1 > len(tmps) {
|
||||
return ""
|
||||
}
|
||||
|
||||
sort.Slice(tmps, func(i, j int) bool {
|
||||
info1, err := tmps[i].Info()
|
||||
if nil != err {
|
||||
LogErrorf("read file info [%s] failed: %s", tmps[i].Name(), err)
|
||||
return false
|
||||
}
|
||||
info2, err := tmps[j].Info()
|
||||
if nil != err {
|
||||
LogErrorf("read file info [%s] failed: %s", tmps[j].Name(), err)
|
||||
return false
|
||||
}
|
||||
return info1.ModTime().After(info2.ModTime())
|
||||
})
|
||||
return filepath.Join(dir, tmps[0].Name())
|
||||
}
|
||||
|
||||
func IsCorruptedSYData(data []byte) bool {
|
||||
if 64 > len(data) || '{' != data[0] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func FilterUploadFileName(name string) string {
|
||||
ret := FilterFileName(name)
|
||||
ret = strings.ReplaceAll(ret, "~", "")
|
||||
//ret = strings.ReplaceAll(ret, "_", "") // https://github.com/siyuan-note/siyuan/issues/3534
|
||||
ret = strings.ReplaceAll(ret, "[", "")
|
||||
ret = strings.ReplaceAll(ret, "]", "")
|
||||
ret = strings.ReplaceAll(ret, "(", "")
|
||||
ret = strings.ReplaceAll(ret, ")", "")
|
||||
ret = strings.ReplaceAll(ret, "!", "")
|
||||
ret = strings.ReplaceAll(ret, "`", "")
|
||||
ret = strings.ReplaceAll(ret, "&", "")
|
||||
ret = strings.ReplaceAll(ret, "{", "")
|
||||
ret = strings.ReplaceAll(ret, "}", "")
|
||||
ret = strings.ReplaceAll(ret, "=", "")
|
||||
ret = strings.ReplaceAll(ret, "#", "")
|
||||
ret = strings.ReplaceAll(ret, "%", "")
|
||||
ret = strings.ReplaceAll(ret, "$", "")
|
||||
return ret
|
||||
}
|
||||
|
||||
func FilterFilePath(p string) (ret string) {
|
||||
ret = strings.ReplaceAll(p, "/", "__@sep__")
|
||||
ret = FilterFileName(ret)
|
||||
ret = strings.ReplaceAll(ret, "__@sep__", "/")
|
||||
return
|
||||
}
|
||||
|
||||
func FilterFileName(name string) string {
|
||||
name = strings.ReplaceAll(name, "\\", "")
|
||||
name = strings.ReplaceAll(name, "/", "")
|
||||
name = strings.ReplaceAll(name, ":", "")
|
||||
name = strings.ReplaceAll(name, "*", "")
|
||||
name = strings.ReplaceAll(name, "?", "")
|
||||
name = strings.ReplaceAll(name, "\"", "")
|
||||
name = strings.ReplaceAll(name, "'", "")
|
||||
name = strings.ReplaceAll(name, "<", "")
|
||||
name = strings.ReplaceAll(name, ">", "")
|
||||
name = strings.ReplaceAll(name, "|", "")
|
||||
return name
|
||||
}
|
||||
|
||||
func IsSubFolder(parent, sub string) bool {
|
||||
if 1 > len(parent) || 1 > len(sub) {
|
||||
return false
|
||||
}
|
||||
if gulu.OS.IsWindows() {
|
||||
if filepath.IsAbs(parent) && filepath.IsAbs(sub) {
|
||||
if strings.ToLower(parent)[0] != strings.ToLower(sub)[0] {
|
||||
// 不在一个盘
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
up := ".." + string(os.PathSeparator)
|
||||
rel, err := filepath.Rel(parent, sub)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(rel, up) && rel != ".." {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const CloudSingleFileMaxSizeLimit = 96 * 1000 * 1000
|
||||
|
||||
func SizeOfDirectory(path string, includeBigFile bool) (int64, error) {
|
||||
var size int64
|
||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
s := info.Size()
|
||||
if CloudSingleFileMaxSizeLimit < s {
|
||||
if includeBigFile {
|
||||
size += s
|
||||
}
|
||||
} else {
|
||||
size += s
|
||||
}
|
||||
} else {
|
||||
size += 4096
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if nil != err {
|
||||
LogErrorf("size of dir [%s] failed: %s", path, err)
|
||||
}
|
||||
return size, err
|
||||
}
|
||||
|
||||
func IsReservedFilename(baseName string) bool {
|
||||
return "assets" == baseName || "templates" == baseName || "widgets" == baseName || "emojis" == baseName || ".siyuan" == baseName || strings.HasPrefix(baseName, ".")
|
||||
}
|
||||
154
kernel/util/font.go
Normal file
154
kernel/util/font.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ConradIrwin/font/sfnt"
|
||||
"github.com/flopp/go-findfont"
|
||||
ttc "golang.org/x/image/font/sfnt"
|
||||
textUnicode "golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
func GetSysFonts(currentLanguage string) (ret []string) {
|
||||
fonts := loadFonts(currentLanguage)
|
||||
ret = RemoveDuplicatedElem(fonts)
|
||||
ret = removeUnusedFonts(ret)
|
||||
sort.Strings(ret)
|
||||
return
|
||||
}
|
||||
|
||||
func removeUnusedFonts(fonts []string) (ret []string) {
|
||||
ret = []string{}
|
||||
for _, font := range fonts {
|
||||
if strings.HasPrefix(font, "Noto Sans") {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, font)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func loadFonts(currentLanguage string) (ret []string) {
|
||||
ret = []string{}
|
||||
for _, f := range findfont.List() {
|
||||
if strings.HasSuffix(strings.ToLower(f), ".ttc") {
|
||||
data, err := os.ReadFile(f)
|
||||
if nil != err {
|
||||
LogErrorf("read font file [%s] failed: %s", f, err)
|
||||
continue
|
||||
}
|
||||
collection, err := ttc.ParseCollection(data)
|
||||
if nil != err {
|
||||
//LogErrorf("parse font collection [%s] failed: %s", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < collection.NumFonts(); i++ {
|
||||
font, err := collection.Font(i)
|
||||
if nil != err {
|
||||
//LogErrorf("get font [%s] failed: %s", f, err)
|
||||
continue
|
||||
}
|
||||
if family := parseFontFamily(font); "" != family {
|
||||
ret = append(ret, family)
|
||||
//LogInfof("[%s] [%s]", f, family)
|
||||
}
|
||||
}
|
||||
} else if strings.HasSuffix(strings.ToLower(f), ".otf") || strings.HasSuffix(strings.ToLower(f), ".ttf") {
|
||||
fontFile, err := os.Open(f)
|
||||
if nil != err {
|
||||
//LogErrorf("open font file [%s] failed: %s", f, err)
|
||||
continue
|
||||
}
|
||||
font, err := sfnt.Parse(fontFile)
|
||||
if nil != err {
|
||||
//LogErrorf("parse font [%s] failed: %s", f, err)
|
||||
continue
|
||||
}
|
||||
|
||||
t, err := font.NameTable()
|
||||
if nil != err {
|
||||
//LogErrorf("parse font name table [%s] failed: %s", f, err)
|
||||
return
|
||||
}
|
||||
fontFile.Close()
|
||||
var family, familyChinese string
|
||||
for _, e := range t.List() {
|
||||
if sfnt.NameFontFamily != e.NameID && sfnt.NamePreferredFamily != e.NameID {
|
||||
continue
|
||||
}
|
||||
|
||||
if sfnt.PlatformLanguageID(1033) == e.LanguageID {
|
||||
v, _, err := transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.IgnoreBOM).NewDecoder(), e.Value)
|
||||
if nil != err {
|
||||
//LogErrorf("decode font family [%s] failed: %s", f, err)
|
||||
continue
|
||||
}
|
||||
val := string(v)
|
||||
if sfnt.NameFontFamily == e.NameID && "" != val {
|
||||
family = val
|
||||
}
|
||||
if sfnt.NamePreferredFamily == e.NameID && "" != val {
|
||||
family = val
|
||||
}
|
||||
} else if sfnt.PlatformLanguageID(2052) == e.LanguageID {
|
||||
if "zh_CN" != currentLanguage {
|
||||
continue
|
||||
}
|
||||
|
||||
v, _, err := transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.IgnoreBOM).NewDecoder(), e.Value)
|
||||
if nil != err {
|
||||
//LogErrorf("decode font family [%s] failed: %s", f, err)
|
||||
continue
|
||||
}
|
||||
val := string(v)
|
||||
if sfnt.NameFontFamily == e.NameID && "" != val {
|
||||
familyChinese = val
|
||||
}
|
||||
if sfnt.NamePreferredFamily == e.NameID && "" != val {
|
||||
familyChinese = val
|
||||
}
|
||||
}
|
||||
}
|
||||
if "" != family && !strings.HasPrefix(family, ".") {
|
||||
ret = append(ret, family)
|
||||
//LogInfof("[%s] [%s]", f, family)
|
||||
}
|
||||
if "" != familyChinese && !strings.HasPrefix(familyChinese, ".") {
|
||||
ret = append(ret, familyChinese)
|
||||
//LogInfof("[%s] [%s]", f, family)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseFontFamily(font *ttc.Font) string {
|
||||
family, _ := font.Name(nil, ttc.NameIDTypographicFamily)
|
||||
if "" == family {
|
||||
family, _ = font.Name(nil, ttc.NameIDFamily)
|
||||
}
|
||||
if strings.HasPrefix(family, ".") {
|
||||
return ""
|
||||
}
|
||||
return family
|
||||
}
|
||||
340
kernel/util/log.go
Normal file
340
kernel/util/log.go
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
func ShortStack() string {
|
||||
output := string(debug.Stack())
|
||||
lines := strings.Split(output, "\n")
|
||||
if 5 < len(lines) {
|
||||
lines = lines[5:]
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
for _, l := range lines {
|
||||
if strings.Contains(l, "gin-gonic") {
|
||||
break
|
||||
}
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(l)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
var (
|
||||
logger *Logger
|
||||
logFile *os.File
|
||||
)
|
||||
|
||||
func LogTracef(format string, v ...interface{}) {
|
||||
if !logger.IsTraceEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
defer closeLogger()
|
||||
openLogger()
|
||||
logger.Tracef(format, v...)
|
||||
}
|
||||
|
||||
func LogDebugf(format string, v ...interface{}) {
|
||||
if !logger.IsDebugEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
defer closeLogger()
|
||||
openLogger()
|
||||
logger.Debugf(format, v...)
|
||||
}
|
||||
|
||||
func LogInfof(format string, v ...interface{}) {
|
||||
defer closeLogger()
|
||||
openLogger()
|
||||
logger.Infof(format, v...)
|
||||
}
|
||||
|
||||
func LogErrorf(format string, v ...interface{}) {
|
||||
defer closeLogger()
|
||||
openLogger()
|
||||
logger.Errorf(format, v...)
|
||||
}
|
||||
|
||||
func LogWarnf(format string, v ...interface{}) {
|
||||
if !logger.IsWarnEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
defer closeLogger()
|
||||
openLogger()
|
||||
logger.Warnf(format, v...)
|
||||
}
|
||||
|
||||
func LogFatalf(format string, v ...interface{}) {
|
||||
openLogger()
|
||||
logger.Fatalf(format, v...)
|
||||
}
|
||||
|
||||
func openLogger() {
|
||||
if gulu.File.IsExist(LogPath) {
|
||||
if size := gulu.File.GetFileSize(LogPath); 1024*1024*2 <= size {
|
||||
// 日志文件大于 2M 的话删了重建
|
||||
os.Remove(LogPath)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
logFile, err = os.OpenFile(LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if nil != err {
|
||||
stdlog.Fatalf("create log file [%s] failed: %s", LogPath, err)
|
||||
}
|
||||
logger = NewLogger(io.MultiWriter(os.Stdout, logFile))
|
||||
}
|
||||
|
||||
func closeLogger() {
|
||||
logFile.Close()
|
||||
}
|
||||
|
||||
func Recover() {
|
||||
if e := recover(); nil != e {
|
||||
stack := stack()
|
||||
msg := fmt.Sprintf("PANIC RECOVERED: %v\n\t%s\n", e, stack)
|
||||
LogErrorf(msg)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
dunno = []byte("???")
|
||||
centerDot = []byte("·")
|
||||
dot = []byte(".")
|
||||
slash = []byte("/")
|
||||
)
|
||||
|
||||
// stack implements Stack, skipping 2 frames.
|
||||
func stack() []byte {
|
||||
buf := &bytes.Buffer{} // the returned data
|
||||
// As we loop, we open files and read them. These variables record the currently
|
||||
// loaded file.
|
||||
var lines [][]byte
|
||||
var lastFile string
|
||||
for i := 2; ; i++ { // Caller we care about is the user, 2 frames up
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
// Print this much at least. If we can't find the source, it won't show.
|
||||
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||
if file != lastFile {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
lastFile = file
|
||||
}
|
||||
line-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// source returns a space-trimmed slice of the n'th line.
|
||||
func source(lines [][]byte, n int) []byte {
|
||||
if n < 0 || n >= len(lines) {
|
||||
return dunno
|
||||
}
|
||||
return bytes.Trim(lines[n], " \t")
|
||||
}
|
||||
|
||||
// function returns, if possible, the name of the function containing the PC.
|
||||
func function(pc uintptr) []byte {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
return dunno
|
||||
}
|
||||
name := []byte(fn.Name())
|
||||
// The name includes the path name to the package, which is unnecessary
|
||||
// since the file name is already included. Plus, it has center dots.
|
||||
// That is, we see
|
||||
// runtime/debug.*T·ptrmethod
|
||||
// and want
|
||||
// *T.ptrmethod
|
||||
// Since the package path might contains dots (e.g. code.google.com/...),
|
||||
// we first remove the path prefix if there is one.
|
||||
if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
|
||||
name = name[lastslash+1:]
|
||||
}
|
||||
if period := bytes.Index(name, dot); period >= 0 {
|
||||
name = name[period+1:]
|
||||
}
|
||||
name = bytes.Replace(name, centerDot, dot, -1)
|
||||
return name
|
||||
}
|
||||
|
||||
// Logging level.
|
||||
const (
|
||||
Off = iota
|
||||
Trace
|
||||
Debug
|
||||
Info
|
||||
Warn
|
||||
Error
|
||||
Fatal
|
||||
)
|
||||
|
||||
// the global default logging level, it will be used for creating logger.
|
||||
var logLevel = Debug
|
||||
|
||||
// Logger represents a simple logger with level.
|
||||
// The underlying logger is the standard Go logging "log".
|
||||
type Logger struct {
|
||||
level int
|
||||
logger *stdlog.Logger
|
||||
}
|
||||
|
||||
// NewLogger creates a logger.
|
||||
func NewLogger(out io.Writer) *Logger {
|
||||
ret := &Logger{level: logLevel, logger: stdlog.New(out, "", stdlog.Ldate|stdlog.Ltime|stdlog.Lshortfile)}
|
||||
return ret
|
||||
}
|
||||
|
||||
// SetLogLevel sets the logging level of all loggers.
|
||||
func SetLogLevel(level string) {
|
||||
logLevel = getLevel(level)
|
||||
}
|
||||
|
||||
// getLevel gets logging level int value corresponding to the specified level.
|
||||
func getLevel(level string) int {
|
||||
level = strings.ToLower(level)
|
||||
|
||||
switch level {
|
||||
case "off":
|
||||
return Off
|
||||
case "trace":
|
||||
return Trace
|
||||
case "debug":
|
||||
return Debug
|
||||
case "info":
|
||||
return Info
|
||||
case "warn":
|
||||
return Warn
|
||||
case "error":
|
||||
return Error
|
||||
case "fatal":
|
||||
return Fatal
|
||||
default:
|
||||
return Info
|
||||
}
|
||||
}
|
||||
|
||||
// SetLevel sets the logging level of a logger.
|
||||
func (l *Logger) SetLevel(level string) {
|
||||
l.level = getLevel(level)
|
||||
}
|
||||
|
||||
// IsTraceEnabled determines whether the trace level is enabled.
|
||||
func (l *Logger) IsTraceEnabled() bool {
|
||||
return l.level <= Trace
|
||||
}
|
||||
|
||||
// IsDebugEnabled determines whether the debug level is enabled.
|
||||
func (l *Logger) IsDebugEnabled() bool {
|
||||
return l.level <= Debug
|
||||
}
|
||||
|
||||
// IsWarnEnabled determines whether the debug level is enabled.
|
||||
func (l *Logger) IsWarnEnabled() bool {
|
||||
return l.level <= Warn
|
||||
}
|
||||
|
||||
// Tracef prints trace level message with format.
|
||||
func (l *Logger) Tracef(format string, v ...interface{}) {
|
||||
if Trace < l.level {
|
||||
return
|
||||
}
|
||||
|
||||
l.logger.SetPrefix("T ")
|
||||
l.logger.Output(3, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Debugf prints debug level message with format.
|
||||
func (l *Logger) Debugf(format string, v ...interface{}) {
|
||||
if Debug < l.level {
|
||||
return
|
||||
}
|
||||
|
||||
l.logger.SetPrefix("D ")
|
||||
l.logger.Output(3, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Infof prints info level message with format.
|
||||
func (l *Logger) Infof(format string, v ...interface{}) {
|
||||
if Info < l.level {
|
||||
return
|
||||
}
|
||||
|
||||
l.logger.SetPrefix("I ")
|
||||
l.logger.Output(3, fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
// Warnf prints warning level message with format.
|
||||
func (l *Logger) Warnf(format string, v ...interface{}) {
|
||||
if Warn < l.level {
|
||||
return
|
||||
}
|
||||
|
||||
l.logger.SetPrefix("W ")
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
l.logger.Output(3, msg)
|
||||
}
|
||||
|
||||
// Errorf prints error level message with format.
|
||||
func (l *Logger) Errorf(format string, v ...interface{}) {
|
||||
if Error < l.level {
|
||||
return
|
||||
}
|
||||
|
||||
l.logger.SetPrefix("E ")
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
l.logger.Output(3, msg)
|
||||
sentry.CaptureMessage(msg)
|
||||
}
|
||||
|
||||
// Fatalf prints fatal level message with format and exit process with code 1.
|
||||
func (l *Logger) Fatalf(format string, v ...interface{}) {
|
||||
if Fatal < l.level {
|
||||
return
|
||||
}
|
||||
|
||||
l.logger.SetPrefix("F ")
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
l.logger.Output(3, msg)
|
||||
sentry.CaptureMessage(msg)
|
||||
closeLogger()
|
||||
os.Exit(ExitCodeFatal)
|
||||
}
|
||||
47
kernel/util/lute.go
Normal file
47
kernel/util/lute.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import "github.com/88250/lute"
|
||||
|
||||
func NewLute() (ret *lute.Lute) {
|
||||
ret = lute.New()
|
||||
ret.SetProtyleWYSIWYG(true)
|
||||
ret.SetBlockRef(true)
|
||||
ret.SetFileAnnotationRef(true)
|
||||
ret.SetKramdownIAL(true)
|
||||
ret.SetTag(true)
|
||||
ret.SetSuperBlock(true)
|
||||
ret.SetImgPathAllowSpace(true)
|
||||
ret.SetGitConflict(true)
|
||||
ret.SetMark(true)
|
||||
ret.SetSup(true)
|
||||
ret.SetSub(true)
|
||||
ret.SetInlineMathAllowDigitAfterOpenMarker(true)
|
||||
ret.SetFootnotes(false)
|
||||
ret.SetToC(false)
|
||||
ret.SetIndentCodeBlock(false)
|
||||
ret.SetParagraphBeginningSpace(true)
|
||||
ret.SetAutoSpace(false)
|
||||
ret.SetHeadingID(false)
|
||||
ret.SetSetext(false)
|
||||
ret.SetYamlFrontMatter(false)
|
||||
ret.SetLinkRef(false)
|
||||
ret.SetCodeSyntaxHighlight(false)
|
||||
ret.SetSanitize(true)
|
||||
return
|
||||
}
|
||||
141
kernel/util/path.go
Normal file
141
kernel/util/path.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
SSL = false
|
||||
UserAgent = "SiYuan/" + Ver
|
||||
)
|
||||
|
||||
const (
|
||||
ServerPort = "6806" // HTTP/WebSocket 端口
|
||||
AliyunServer = "https://siyuan-sync.b3logfile.com" // 云端服务地址,阿里云负载均衡,用于接口,数据同步文件上传、下载会走七牛云 OSS http://siyuan-data.b3logfile.com
|
||||
BazaarStatServer = "http://bazaar.b3logfile.com" // 集市包统计服务地址,直接对接 Bucket 没有 CDN 缓存
|
||||
BazaarOSSServer = "https://oss.b3logfile.com" // 云端对象存储地址,七牛云,仅用于读取小文件(比如配置 json),不用于读取包内容(如果是订阅会员则用于读取包内容)
|
||||
BazaarOSSFileServer = "https://oss0.b3logfile.com" // 云端对象存储文件服务地址,Cloudflare,用于读取包内容
|
||||
)
|
||||
|
||||
func ShortPathForBootingDisplay(p string) string {
|
||||
if 25 > len(p) {
|
||||
return p
|
||||
}
|
||||
p = strings.TrimSuffix(p, ".sy")
|
||||
p = path.Base(p)
|
||||
return p
|
||||
}
|
||||
|
||||
func IsIDPattern(str string) bool {
|
||||
if len("20060102150405-1a2b3c4") != len(str) {
|
||||
return false
|
||||
}
|
||||
|
||||
if 1 != strings.Count(str, "-") {
|
||||
return false
|
||||
}
|
||||
|
||||
parts := strings.Split(str, "-")
|
||||
idPart := parts[0]
|
||||
if 14 != len(idPart) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range idPart {
|
||||
if !('0' <= c && '9' >= c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
randPart := parts[1]
|
||||
if 7 != len(randPart) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range randPart {
|
||||
if !('a' <= c && 'z' >= c) && !('0' <= c && '9' >= c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var LocalIPs []string
|
||||
|
||||
func GetLocalIPs() (ret []string) {
|
||||
if 0 < len(LocalIPs) {
|
||||
return LocalIPs
|
||||
}
|
||||
|
||||
ret = []string{}
|
||||
addrs, err := net.InterfaceAddrs() // Android 上用不了 https://github.com/golang/go/issues/40569,所以前面使用启动内核传入的参数 localIPs
|
||||
if nil != err {
|
||||
LogWarnf("get interface addresses failed: %s", err)
|
||||
return
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if networkIp, ok := addr.(*net.IPNet); ok && !networkIp.IP.IsLoopback() && networkIp.IP.To4() != nil &&
|
||||
bytes.Equal([]byte{255, 255, 255, 0}, networkIp.Mask) {
|
||||
ret = append(ret, networkIp.IP.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isRunningInDockerContainer() bool {
|
||||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsRelativePath(dest string) bool {
|
||||
if 1 > len(dest) {
|
||||
return true
|
||||
}
|
||||
|
||||
if '/' == dest[0] {
|
||||
return false
|
||||
}
|
||||
return !strings.Contains(dest, ":/") && !strings.Contains(dest, ":\\")
|
||||
}
|
||||
|
||||
func TimeFromID(id string) (ret string) {
|
||||
ret = id[:14]
|
||||
return
|
||||
}
|
||||
|
||||
func IsValidPandocBin(binPath string) bool {
|
||||
if "" == binPath {
|
||||
return false
|
||||
}
|
||||
|
||||
cmd := exec.Command(binPath, "--version")
|
||||
CmdAttr(cmd)
|
||||
data, err := cmd.CombinedOutput()
|
||||
if nil == err && strings.HasPrefix(string(data), "pandoc") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
70
kernel/util/result.go
Normal file
70
kernel/util/result.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import "github.com/88250/gulu"
|
||||
|
||||
type PushMode int
|
||||
|
||||
const (
|
||||
PushModeBroadcast PushMode = 0 // 广播
|
||||
PushModeSingleSelf PushMode = 1 // 自我单播
|
||||
PushModeBroadcastExcludeSelf PushMode = 2 // 非自我广播
|
||||
PushModeBroadcastExcludeSelfApp PushMode = 4 // 非自我应用广播
|
||||
PushModeNone PushMode = 10 // 不进行 reload
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Cmd string `json:"cmd"`
|
||||
ReqId float64 `json:"reqId"`
|
||||
AppId string `json:"app"`
|
||||
SessionId string `json:"sid"`
|
||||
PushMode PushMode `json:"pushMode"`
|
||||
ReloadPushMode PushMode `json:"reloadPushMode"`
|
||||
Callback interface{} `json:"callback"`
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func NewResult() *Result {
|
||||
return &Result{Cmd: "",
|
||||
ReqId: 0,
|
||||
PushMode: 0,
|
||||
ReloadPushMode: 0,
|
||||
Callback: "",
|
||||
Code: 0,
|
||||
Msg: "",
|
||||
Data: nil}
|
||||
}
|
||||
|
||||
func NewCmdResult(cmdName string, cmdId float64, pushMode, reloadPushMode PushMode) *Result {
|
||||
ret := NewResult()
|
||||
ret.Cmd = cmdName
|
||||
ret.ReqId = cmdId
|
||||
ret.PushMode = pushMode
|
||||
ret.ReloadPushMode = reloadPushMode
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *Result) Bytes() []byte {
|
||||
ret, err := gulu.JSON.MarshalJSON(r)
|
||||
if nil != err {
|
||||
LogErrorf("marshal result [%+v] failed [%s]", r, err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
153
kernel/util/rhy.go
Normal file
153
kernel/util/rhy.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
var cachedRhyResult = map[string]interface{}{}
|
||||
var rhyResultCacheTime int64
|
||||
var rhyResultLock = sync.Mutex{}
|
||||
|
||||
func GetRhyResult(force bool, proxyURL string) (map[string]interface{}, error) {
|
||||
rhyResultLock.Lock()
|
||||
defer rhyResultLock.Unlock()
|
||||
|
||||
now := time.Now().Unix()
|
||||
if 3600 >= now-rhyResultCacheTime && !force {
|
||||
return cachedRhyResult, nil
|
||||
}
|
||||
|
||||
request := NewCloudRequest(proxyURL)
|
||||
_, err := request.SetResult(&cachedRhyResult).Get(AliyunServer + "/apis/siyuan/version?ver=" + Ver)
|
||||
if nil != err {
|
||||
LogErrorf("get version meta info failed: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
rhyResultCacheTime = now
|
||||
return cachedRhyResult, nil
|
||||
}
|
||||
|
||||
var (
|
||||
browserClient, browserDownloadClient, cloudAPIClient, cloudFileClientTimeout2Min, cloudFileClientTimeout15s *req.Client
|
||||
|
||||
browserUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36"
|
||||
)
|
||||
|
||||
func NewBrowserRequest(proxyURL string) (ret *req.Request) {
|
||||
if nil == browserClient {
|
||||
browserClient = req.C().
|
||||
SetUserAgent(browserUserAgent).
|
||||
SetTimeout(7 * time.Second).
|
||||
DisableInsecureSkipVerify()
|
||||
}
|
||||
if "" != proxyURL {
|
||||
browserClient.SetProxyURL(proxyURL)
|
||||
}
|
||||
ret = browserClient.R()
|
||||
ret.SetRetryCount(1).SetRetryFixedInterval(3 * time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
func NewBrowserDownloadRequest(proxyURL string) *req.Request {
|
||||
if nil == browserDownloadClient {
|
||||
browserDownloadClient = req.C().
|
||||
SetUserAgent(browserUserAgent).
|
||||
SetTimeout(2 * time.Minute).
|
||||
SetCommonRetryCount(1).
|
||||
SetCommonRetryFixedInterval(3 * time.Second).
|
||||
SetCommonRetryCondition(retryCondition).
|
||||
DisableInsecureSkipVerify()
|
||||
}
|
||||
if "" != proxyURL {
|
||||
browserDownloadClient.SetProxyURL(proxyURL)
|
||||
}
|
||||
return browserDownloadClient.R()
|
||||
}
|
||||
|
||||
func NewCloudRequest(proxyURL string) *req.Request {
|
||||
if nil == cloudAPIClient {
|
||||
cloudAPIClient = req.C().
|
||||
SetUserAgent(UserAgent).
|
||||
SetTimeout(7 * time.Second).
|
||||
SetCommonRetryCount(1).
|
||||
SetCommonRetryFixedInterval(3 * time.Second).
|
||||
SetCommonRetryCondition(retryCondition).
|
||||
DisableInsecureSkipVerify()
|
||||
}
|
||||
if "" != proxyURL {
|
||||
cloudAPIClient.SetProxyURL(proxyURL)
|
||||
}
|
||||
return cloudAPIClient.R()
|
||||
}
|
||||
|
||||
func NewCloudFileRequest2m(proxyURL string) *req.Request {
|
||||
if nil == cloudFileClientTimeout2Min {
|
||||
cloudFileClientTimeout2Min = req.C().
|
||||
SetUserAgent(UserAgent).
|
||||
SetTimeout(2 * time.Minute).
|
||||
SetCommonRetryCount(1).
|
||||
SetCommonRetryFixedInterval(3 * time.Second).
|
||||
SetCommonRetryCondition(retryCondition).
|
||||
DisableInsecureSkipVerify()
|
||||
setTransport(cloudFileClientTimeout2Min.GetClient())
|
||||
}
|
||||
if "" != proxyURL {
|
||||
cloudFileClientTimeout2Min.SetProxyURL(proxyURL)
|
||||
}
|
||||
return cloudFileClientTimeout2Min.R()
|
||||
}
|
||||
|
||||
func NewCloudFileRequest15s(proxyURL string) *req.Request {
|
||||
if nil == cloudFileClientTimeout15s {
|
||||
cloudFileClientTimeout15s = req.C().
|
||||
SetUserAgent(UserAgent).
|
||||
SetTimeout(15 * time.Second).
|
||||
SetCommonRetryCount(1).
|
||||
SetCommonRetryFixedInterval(3 * time.Second).
|
||||
SetCommonRetryCondition(retryCondition).
|
||||
DisableInsecureSkipVerify()
|
||||
setTransport(cloudFileClientTimeout15s.GetClient())
|
||||
}
|
||||
if "" != proxyURL {
|
||||
cloudFileClientTimeout15s.SetProxyURL(proxyURL)
|
||||
}
|
||||
return cloudFileClientTimeout15s.R()
|
||||
}
|
||||
|
||||
func retryCondition(resp *req.Response, err error) bool {
|
||||
if nil != err {
|
||||
return true
|
||||
}
|
||||
if 503 == resp.StatusCode { // 负载均衡会返回 503,需要重试
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setTransport(client *http.Client) {
|
||||
// 改进同步下载数据稳定性 https://github.com/siyuan-note/siyuan/issues/4994
|
||||
transport := client.Transport.(*req.Transport)
|
||||
transport.MaxIdleConns = 10
|
||||
transport.MaxIdleConnsPerHost = 2
|
||||
transport.MaxConnsPerHost = 2
|
||||
}
|
||||
76
kernel/util/runtime.go
Normal file
76
kernel/util/runtime.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
const DatabaseVer = "20220501" // 修改表结构的话需要修改这里
|
||||
|
||||
const (
|
||||
ExitCodeReadOnlyDatabase = 20 // 数据库文件被锁
|
||||
ExitCodeUnavailablePort = 21 // 端口不可用
|
||||
ExitCodeCreateConfDirErr = 22 // 创建配置目录失败
|
||||
ExitCodeBlockTreeErr = 23 // 无法读写 blocktree.msgpack 文件
|
||||
ExitCodeOk = 0 // 正常退出
|
||||
ExitCodeFatal = 1 // 致命错误
|
||||
)
|
||||
|
||||
func logBootInfo() {
|
||||
s, _ := SizeOfDirectory(DataDir, true)
|
||||
dataDirSize := humanize.Bytes(uint64(s))
|
||||
|
||||
LogInfof("kernel is booting:\n"+
|
||||
" * ver [%s]\n"+
|
||||
" * arch [%s]\n"+
|
||||
" * runtime mode [%s]\n"+
|
||||
" * working directory [%s]\n"+
|
||||
" * read only [%v]\n"+
|
||||
" * container [%s]\n"+
|
||||
" * database [ver=%s]\n"+
|
||||
" * workspace directory [%s, data %s]",
|
||||
Ver, runtime.GOARCH, Mode, WorkingDir, ReadOnly, Container, DatabaseVer, WorkspaceDir, dataDirSize)
|
||||
}
|
||||
|
||||
func IsMutexLocked(m *sync.Mutex) bool {
|
||||
state := reflect.ValueOf(m).Elem().FieldByName("state")
|
||||
return state.Int()&1 == 1
|
||||
}
|
||||
|
||||
func RandomSleep(minMills, maxMills int) {
|
||||
r := gulu.Rand.Int(minMills, maxMills)
|
||||
time.Sleep(time.Duration(r) * time.Millisecond)
|
||||
}
|
||||
|
||||
func GetDeviceID() string {
|
||||
if "std" == Container {
|
||||
machineID, err := machineid.ID()
|
||||
if nil != err {
|
||||
return gulu.Rand.String(12)
|
||||
}
|
||||
return machineID
|
||||
}
|
||||
return gulu.Rand.String(12)
|
||||
}
|
||||
59
kernel/util/session.go
Normal file
59
kernel/util/session.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"github.com/88250/gulu"
|
||||
ginSessions "github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SessionData represents the session.
|
||||
type SessionData struct {
|
||||
ID int
|
||||
AccessAuthCode string
|
||||
}
|
||||
|
||||
// Save saves the current session of the specified context.
|
||||
func (sd *SessionData) Save(c *gin.Context) error {
|
||||
session := ginSessions.Default(c)
|
||||
sessionDataBytes, err := gulu.JSON.MarshalJSON(sd)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
session.Set("data", string(sessionDataBytes))
|
||||
return session.Save()
|
||||
}
|
||||
|
||||
// GetSession returns session of the specified context.
|
||||
func GetSession(c *gin.Context) *SessionData {
|
||||
ret := &SessionData{}
|
||||
|
||||
session := ginSessions.Default(c)
|
||||
sessionDataStr := session.Get("data")
|
||||
if nil == sessionDataStr {
|
||||
return ret
|
||||
}
|
||||
|
||||
err := gulu.JSON.UnmarshalJSON([]byte(sessionDataStr.(string)), ret)
|
||||
if nil != err {
|
||||
return ret
|
||||
}
|
||||
|
||||
c.Set("session", ret)
|
||||
return ret
|
||||
}
|
||||
50
kernel/util/slice.go
Normal file
50
kernel/util/slice.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import "github.com/88250/gulu"
|
||||
|
||||
func RemoveElem(slice []string, elem string) (ret []string) {
|
||||
for _, e := range slice {
|
||||
if e != elem {
|
||||
ret = append(ret, e)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ExcludeElem(slice, excludes []string) (ret []string) {
|
||||
ret = []string{}
|
||||
for _, e := range slice {
|
||||
if !gulu.Str.Contains(e, excludes) {
|
||||
ret = append(ret, e)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RemoveDuplicatedElem(slice []string) (ret []string) {
|
||||
m := map[string]bool{}
|
||||
for _, str := range slice {
|
||||
m[str] = true
|
||||
}
|
||||
ret = []string{}
|
||||
for str, _ := range m {
|
||||
ret = append(ret, str)
|
||||
}
|
||||
return
|
||||
}
|
||||
67
kernel/util/sort.go
Normal file
67
kernel/util/sort.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
func PinYinCompare(str1, str2 string) bool {
|
||||
a, _ := UTF82GBK(str1)
|
||||
b, _ := UTF82GBK(str2)
|
||||
bLen := len(b)
|
||||
for idx, chr := range a {
|
||||
if idx > bLen-1 {
|
||||
return false
|
||||
}
|
||||
if chr != b[idx] {
|
||||
return chr < b[idx]
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
//UTF82GBK : transform UTF8 rune into GBK byte array
|
||||
func UTF82GBK(src string) ([]byte, error) {
|
||||
GB18030 := simplifiedchinese.All[0]
|
||||
return io.ReadAll(transform.NewReader(bytes.NewReader([]byte(src)), GB18030.NewEncoder()))
|
||||
}
|
||||
|
||||
//GBK2UTF8 : transform GBK byte array into UTF8 string
|
||||
func GBK2UTF8(src []byte) (string, error) {
|
||||
GB18030 := simplifiedchinese.All[0]
|
||||
bytes, err := io.ReadAll(transform.NewReader(bytes.NewReader(src), GB18030.NewDecoder()))
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
const (
|
||||
SortModeNameASC = iota // 0:文件名字母升序
|
||||
SortModeNameDESC // 1:文件名字母降序
|
||||
SortModeUpdatedASC // 2:文件更新时间升序
|
||||
SortModeUpdatedDESC // 3:文件更新时间降序
|
||||
SortModeAlphanumASC // 4:文件名自然数升序
|
||||
SortModeAlphanumDESC // 5:文件名自然数降序
|
||||
SortModeCustom // 6:自定义排序
|
||||
SortModeRefCountASC // 7:引用数升序
|
||||
SortModeRefCountDESC // 8:引用数降序
|
||||
SortModeCreatedASC // 9:文件创建时间升序
|
||||
SortModeCreatedDESC // 10:文件创建时间降序
|
||||
)
|
||||
113
kernel/util/string.go
Normal file
113
kernel/util/string.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func RemoveInvisible(str string) string {
|
||||
str = strings.ReplaceAll(str, "\u00A0", " ") // NBSP 转换为普通空格
|
||||
str = RemoveZeroWidthCharacters(str)
|
||||
str = stripCtlFromUTF8(str)
|
||||
return str
|
||||
}
|
||||
|
||||
func stripCtlFromUTF8(str string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r >= 32 && r != 127 {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, str)
|
||||
}
|
||||
|
||||
const (
|
||||
// ZWSP represents zero-width space.
|
||||
ZWSP = '\u200B'
|
||||
|
||||
// ZWNBSP represents zero-width no-break space.
|
||||
ZWNBSP = '\uFEFF'
|
||||
|
||||
// ZWJ represents zero-width joiner.
|
||||
ZWJ = '\u200D'
|
||||
|
||||
// ZWNJ represents zero-width non-joiner.
|
||||
ZWNJ = '\u200C'
|
||||
|
||||
empty = ""
|
||||
)
|
||||
|
||||
var replacer = strings.NewReplacer(string(ZWSP), empty,
|
||||
string(ZWNBSP), empty,
|
||||
string(ZWJ), empty,
|
||||
string(ZWNJ), empty)
|
||||
|
||||
// HasZeroWidthCharacters reports whether string s contains zero-width characters.
|
||||
func HasZeroWidthCharacters(s string) bool {
|
||||
return strings.ContainsRune(s, ZWSP) ||
|
||||
strings.ContainsRune(s, ZWNBSP) ||
|
||||
strings.ContainsRune(s, ZWJ) ||
|
||||
strings.ContainsRune(s, ZWNJ)
|
||||
}
|
||||
|
||||
// RemoveZeroWidthCharacters removes all zero-width characters from string s.
|
||||
func RemoveZeroWidthCharacters(s string) string {
|
||||
return replacer.Replace(s)
|
||||
}
|
||||
|
||||
// RemoveZeroWidthSpace removes zero-width space characters from string s.
|
||||
func RemoveZeroWidthSpace(s string) string {
|
||||
return strings.Replace(s, string(ZWSP), empty, -1)
|
||||
}
|
||||
|
||||
// RemoveZeroWidthNoBreakSpace removes zero-width no-break space characters from string s.
|
||||
func RemoveZeroWidthNoBreakSpace(s string) string {
|
||||
return strings.Replace(s, string(ZWNBSP), empty, -1)
|
||||
}
|
||||
|
||||
// RemoveZeroWidthJoiner removes zero-width joiner characters from string s.
|
||||
func RemoveZeroWidthJoiner(s string) string {
|
||||
return strings.Replace(s, string(ZWJ), empty, -1)
|
||||
}
|
||||
|
||||
// RemoveZeroWidthNonJoiner removes zero-width non-joiner characters from string s.
|
||||
func RemoveZeroWidthNonJoiner(s string) string {
|
||||
return strings.Replace(s, string(ZWNJ), empty, -1)
|
||||
}
|
||||
|
||||
func IsASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func SubstringsBetween(str, start, end string) (ret []string) {
|
||||
parts := strings.Split(str, start)
|
||||
for _, p := range parts {
|
||||
if !strings.Contains(p, end) {
|
||||
continue
|
||||
}
|
||||
parts2 := strings.Split(p, end)
|
||||
ret = append(ret, parts2[0])
|
||||
}
|
||||
return
|
||||
}
|
||||
35
kernel/util/time.go
Normal file
35
kernel/util/time.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func Millisecond2Time(t int64) time.Time {
|
||||
sec := t / 1000
|
||||
msec := t % 1000
|
||||
return time.Unix(sec, msec*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
func CurrentTimeMillis() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
func CurrentTimeSecondsStr() string {
|
||||
return time.Now().Format("20060102150405")
|
||||
}
|
||||
84
kernel/util/web.go
Normal file
84
kernel/util/web.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/88250/melody"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GetRemoteAddr(session *melody.Session) string {
|
||||
ret := session.Request.Header.Get("X-forwarded-for")
|
||||
ret = strings.TrimSpace(ret)
|
||||
if "" == ret {
|
||||
ret = session.Request.Header.Get("X-Real-IP")
|
||||
}
|
||||
ret = strings.TrimSpace(ret)
|
||||
if "" == ret {
|
||||
return session.Request.RemoteAddr
|
||||
}
|
||||
return strings.Split(ret, ",")[0]
|
||||
}
|
||||
|
||||
func JsonArg(c *gin.Context, result *gulu.Result) (arg map[string]interface{}, ok bool) {
|
||||
arg = map[string]interface{}{}
|
||||
if err := c.BindJSON(&arg); nil != err {
|
||||
result.Code = -1
|
||||
result.Msg = "parses request failed"
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func isPortOpen(port string) bool {
|
||||
timeout := time.Second
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", port), timeout)
|
||||
if nil != err {
|
||||
return false
|
||||
}
|
||||
if nil != conn {
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tryToListenPort() bool {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:"+ServerPort)
|
||||
if nil != err {
|
||||
time.Sleep(time.Second * 3)
|
||||
listener, err = net.Listen("tcp", "127.0.0.1:"+ServerPort)
|
||||
if nil != err {
|
||||
LogErrorf("try to listen port [%s] failed: %s", ServerPort, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if err = listener.Close(); nil != err {
|
||||
time.Sleep(time.Second * 1)
|
||||
if err = listener.Close(); nil != err {
|
||||
LogErrorf("close listen port [%s] failed: %s", ServerPort, err)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
269
kernel/util/websocket.go
Normal file
269
kernel/util/websocket.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/88250/melody"
|
||||
)
|
||||
|
||||
var (
|
||||
WebSocketServer = melody.New()
|
||||
|
||||
// map[string]map[string]*melody.Session{}
|
||||
sessions = sync.Map{} // {appId, {sessionId, session}}
|
||||
)
|
||||
|
||||
// BroadcastByType 广播所有实例上 typ 类型的会话。
|
||||
func BroadcastByType(typ, cmd string, code int, msg string, data interface{}) {
|
||||
typeSessions := SessionsByType(typ)
|
||||
for _, sess := range typeSessions {
|
||||
event := NewResult()
|
||||
event.Cmd = cmd
|
||||
event.Code = code
|
||||
event.Msg = msg
|
||||
event.Data = data
|
||||
sess.Write(event.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func SessionsByType(typ string) (ret []*melody.Session) {
|
||||
ret = []*melody.Session{}
|
||||
|
||||
sessions.Range(func(key, value interface{}) bool {
|
||||
appSessions := value.(*sync.Map)
|
||||
appSessions.Range(func(key, value interface{}) bool {
|
||||
session := value.(*melody.Session)
|
||||
if t, ok := session.Get("type"); ok && typ == t {
|
||||
ret = append(ret, session)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func AddPushChan(session *melody.Session) {
|
||||
appID := session.Request.URL.Query().Get("app")
|
||||
session.Set("app", appID)
|
||||
id := session.Request.URL.Query().Get("id")
|
||||
session.Set("id", id)
|
||||
typ := session.Request.URL.Query().Get("type")
|
||||
session.Set("type", typ)
|
||||
|
||||
if appSessions, ok := sessions.Load(appID); !ok {
|
||||
appSess := &sync.Map{}
|
||||
appSess.Store(id, session)
|
||||
sessions.Store(appID, appSess)
|
||||
} else {
|
||||
(appSessions.(*sync.Map)).Store(id, session)
|
||||
}
|
||||
}
|
||||
|
||||
func RemovePushChan(session *melody.Session) {
|
||||
app, _ := session.Get("app")
|
||||
id, _ := session.Get("id")
|
||||
|
||||
if nil == app || nil == id {
|
||||
return
|
||||
}
|
||||
|
||||
appSess, _ := sessions.Load(app)
|
||||
if nil != appSess {
|
||||
appSessions := appSess.(*sync.Map)
|
||||
appSessions.Delete(id)
|
||||
if 1 > lenOfSyncMap(appSessions) {
|
||||
sessions.Delete(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lenOfSyncMap(m *sync.Map) (ret int) {
|
||||
m.Range(func(key, value interface{}) bool {
|
||||
ret++
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func ClosePushChan(id string) {
|
||||
sessions.Range(func(key, value interface{}) bool {
|
||||
appSessions := value.(*sync.Map)
|
||||
appSessions.Range(func(key, value interface{}) bool {
|
||||
session := value.(*melody.Session)
|
||||
if sid, _ := session.Get("id"); sid == id {
|
||||
session.CloseWithMsg([]byte(" close websocket"))
|
||||
RemovePushChan(session)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func ReloadUI() {
|
||||
evt := NewCmdResult("reloadui", 0, PushModeBroadcast, 0)
|
||||
PushEvent(evt)
|
||||
}
|
||||
|
||||
func PushTxErr(msg string, code int, data interface{}) {
|
||||
BroadcastByType("main", "txerr", code, msg, data)
|
||||
}
|
||||
|
||||
func PushMsg(msg string, timeout int) {
|
||||
evt := NewCmdResult("msg", 0, PushModeBroadcast, 0)
|
||||
evt.Msg = msg
|
||||
evt.Data = map[string]interface{}{"closeTimeout": timeout}
|
||||
PushEvent(evt)
|
||||
}
|
||||
|
||||
func PushErrMsg(msg string, timeout int) {
|
||||
evt := NewCmdResult("msg", 0, PushModeBroadcast, 0)
|
||||
evt.Code = -1
|
||||
evt.Msg = msg
|
||||
evt.Data = map[string]interface{}{"closeTimeout": timeout}
|
||||
PushEvent(evt)
|
||||
}
|
||||
|
||||
const (
|
||||
PushProgressCodeProgressed = 0 // 有进度
|
||||
PushProgressCodeEndless = 1 // 无进度
|
||||
PushProgressCodeEnd = 2 // 关闭进度
|
||||
)
|
||||
|
||||
func ClearPushProgress(total int) {
|
||||
PushProgress(PushProgressCodeEnd, total, total, "")
|
||||
}
|
||||
|
||||
func PushEndlessProgress(msg string) {
|
||||
PushProgress(PushProgressCodeEndless, 1, 1, msg)
|
||||
}
|
||||
|
||||
func PushProgress(code, current, total int, msg string) {
|
||||
evt := NewCmdResult("progress", 0, PushModeBroadcast, 0)
|
||||
evt.Msg = msg
|
||||
evt.Code = code
|
||||
evt.Data = map[string]interface{}{
|
||||
"current": current,
|
||||
"total": total,
|
||||
}
|
||||
PushEvent(evt)
|
||||
}
|
||||
|
||||
// PushClearMsg 会清空消息提示以及进度遮罩。
|
||||
func PushClearMsg() {
|
||||
evt := NewCmdResult("cmsg", 0, PushModeBroadcast, 0)
|
||||
PushEvent(evt)
|
||||
}
|
||||
|
||||
func PushDownloadProgress(id string, percent float32) {
|
||||
evt := NewCmdResult("downloadProgress", 0, PushModeBroadcast, 0)
|
||||
evt.Data = map[string]interface{}{
|
||||
"id": id,
|
||||
"percent": percent,
|
||||
}
|
||||
PushEvent(evt)
|
||||
}
|
||||
|
||||
func PushEvent(event *Result) {
|
||||
msg := event.Bytes()
|
||||
mode := event.PushMode
|
||||
if "reload" == event.Cmd {
|
||||
mode = event.ReloadPushMode
|
||||
}
|
||||
switch mode {
|
||||
case PushModeBroadcast:
|
||||
Broadcast(msg)
|
||||
case PushModeSingleSelf:
|
||||
single(msg, event.AppId, event.SessionId)
|
||||
case PushModeBroadcastExcludeSelf:
|
||||
broadcastOthers(msg, event.SessionId)
|
||||
case PushModeBroadcastExcludeSelfApp:
|
||||
broadcastOtherApps(msg, event.AppId)
|
||||
case PushModeNone:
|
||||
}
|
||||
}
|
||||
|
||||
func single(msg []byte, appId, sid string) {
|
||||
sessions.Range(func(key, value interface{}) bool {
|
||||
appSessions := value.(*sync.Map)
|
||||
if key != appId {
|
||||
return true
|
||||
}
|
||||
|
||||
appSessions.Range(func(key, value interface{}) bool {
|
||||
session := value.(*melody.Session)
|
||||
if id, _ := session.Get("id"); id == sid {
|
||||
session.Write(msg)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func Broadcast(msg []byte) {
|
||||
sessions.Range(func(key, value interface{}) bool {
|
||||
appSessions := value.(*sync.Map)
|
||||
appSessions.Range(func(key, value interface{}) bool {
|
||||
session := value.(*melody.Session)
|
||||
session.Write(msg)
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func broadcastOtherApps(msg []byte, excludeApp string) {
|
||||
sessions.Range(func(key, value interface{}) bool {
|
||||
appSessions := value.(*sync.Map)
|
||||
appSessions.Range(func(key, value interface{}) bool {
|
||||
session := value.(*melody.Session)
|
||||
if app, _ := session.Get("app"); app == excludeApp {
|
||||
return true
|
||||
}
|
||||
session.Write(msg)
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func broadcastOthers(msg []byte, excludeSID string) {
|
||||
sessions.Range(func(key, value interface{}) bool {
|
||||
appSessions := value.(*sync.Map)
|
||||
appSessions.Range(func(key, value interface{}) bool {
|
||||
session := value.(*melody.Session)
|
||||
if id, _ := session.Get("id"); id == excludeSID {
|
||||
return true
|
||||
}
|
||||
session.Write(msg)
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func CountSessions() (ret int) {
|
||||
sessions.Range(func(key, value interface{}) bool {
|
||||
ret++
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
434
kernel/util/working.go
Normal file
434
kernel/util/working.go
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
figure "github.com/common-nighthawk/go-figure"
|
||||
goPS "github.com/mitchellh/go-ps"
|
||||
)
|
||||
|
||||
var Mode = "dev"
|
||||
|
||||
//var Mode = "prod"
|
||||
|
||||
const (
|
||||
Ver = "2.0.13"
|
||||
IsInsider = false
|
||||
)
|
||||
|
||||
var (
|
||||
bootProgress float64 // 启动进度,从 0 到 100
|
||||
bootDetails string // 启动细节描述
|
||||
HttpServing = false // 是否 HTTP 伺服已经可用
|
||||
)
|
||||
|
||||
func Boot() {
|
||||
IncBootProgress(3, "Booting...")
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
initMime()
|
||||
|
||||
workspacePath := flag.String("workspace", "", "dir path of the workspace, default to ~/Documents/SiYuan/")
|
||||
wdPath := flag.String("wd", WorkingDir, "working directory of SiYuan")
|
||||
servePath := flag.String("servePath", "", "obsoleted https://github.com/siyuan-note/siyuan/issues/4647")
|
||||
_ = servePath
|
||||
resident := flag.Bool("resident", true, "resident memory even if no active session")
|
||||
readOnly := flag.Bool("readonly", false, "read-only mode")
|
||||
accessAuthCode := flag.String("accessAuthCode", "", "access auth code")
|
||||
ssl := flag.Bool("ssl", false, "for https and wss")
|
||||
lang := flag.String("lang", "en_US", "zh_CN/zh_CHT/en_US/fr_FR")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if "" != *wdPath {
|
||||
WorkingDir = *wdPath
|
||||
}
|
||||
if "" != *lang {
|
||||
Lang = *lang
|
||||
}
|
||||
Resident = *resident
|
||||
ReadOnly = *readOnly
|
||||
AccessAuthCode = *accessAuthCode
|
||||
Container = "std"
|
||||
if isRunningInDockerContainer() {
|
||||
Container = "docker"
|
||||
}
|
||||
|
||||
initWorkspaceDir(*workspacePath)
|
||||
|
||||
SSL = *ssl
|
||||
LogPath = filepath.Join(TempDir, "siyuan.log")
|
||||
AppearancePath = filepath.Join(ConfDir, "appearance")
|
||||
if "dev" == Mode {
|
||||
ThemesPath = filepath.Join(WorkingDir, "appearance", "themes")
|
||||
IconsPath = filepath.Join(WorkingDir, "appearance", "icons")
|
||||
} else {
|
||||
ThemesPath = filepath.Join(AppearancePath, "themes")
|
||||
IconsPath = filepath.Join(AppearancePath, "icons")
|
||||
}
|
||||
|
||||
initPathDir()
|
||||
checkPort()
|
||||
|
||||
bootBanner := figure.NewColorFigure("SiYuan", "isometric3", "green", true)
|
||||
LogInfof("\n" + bootBanner.String())
|
||||
logBootInfo()
|
||||
|
||||
go cleanOld()
|
||||
}
|
||||
|
||||
func SetBootDetails(details string) {
|
||||
if 100 <= bootProgress {
|
||||
return
|
||||
}
|
||||
bootDetails = details
|
||||
}
|
||||
|
||||
func IncBootProgress(progress float64, details string) {
|
||||
if 100 <= bootProgress {
|
||||
return
|
||||
}
|
||||
bootProgress += progress
|
||||
bootDetails = details
|
||||
}
|
||||
|
||||
func IsBooted() bool {
|
||||
return 100 <= bootProgress
|
||||
}
|
||||
|
||||
func GetBootProgressDetails() (float64, string) {
|
||||
return bootProgress, bootDetails
|
||||
}
|
||||
|
||||
func GetBootProgress() float64 {
|
||||
return bootProgress
|
||||
}
|
||||
|
||||
func SetBooted() {
|
||||
bootDetails = "Finishing boot..."
|
||||
bootProgress = 100
|
||||
LogInfof("kernel booted")
|
||||
}
|
||||
|
||||
func GetHistoryDirNow(now, suffix string) (ret string, err error) {
|
||||
ret = filepath.Join(WorkspaceDir, "history", now+"-"+suffix)
|
||||
if err = os.MkdirAll(ret, 0755); nil != err {
|
||||
LogErrorf("make history dir failed: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetHistoryDir(suffix string) (ret string, err error) {
|
||||
ret = filepath.Join(WorkspaceDir, "history", time.Now().Format("2006-01-02-150405")+"-"+suffix)
|
||||
if err = os.MkdirAll(ret, 0755); nil != err {
|
||||
LogErrorf("make history dir failed: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
HomeDir, _ = gulu.OS.Home()
|
||||
WorkingDir, _ = os.Getwd()
|
||||
|
||||
WorkspaceDir string // 工作空间目录路径
|
||||
ConfDir string // 配置目录路径
|
||||
DataDir string // 数据目录路径
|
||||
TempDir string // 临时目录路径
|
||||
LogPath string // 配置目录下的日志文件 siyuan.log 路径
|
||||
DBName = "siyuan.db" // SQLite 数据库文件名
|
||||
DBPath string // SQLite 数据库文件路径
|
||||
BlockTreePath string // 区块树文件路径
|
||||
AppearancePath string // 配置目录下的外观目录 appearance/ 路径
|
||||
ThemesPath string // 配置目录下的外观目录下的 themes/ 路径
|
||||
IconsPath string // 配置目录下的外观目录下的 icons/ 路径
|
||||
|
||||
AndroidNativeLibDir string // Android 库路径
|
||||
AndroidPrivateDataDir string // Android 私有数据路径
|
||||
|
||||
UIProcessIDs = sync.Map{} // UI 进程 ID
|
||||
|
||||
IsNewbie bool // 是否是第一次安装
|
||||
)
|
||||
|
||||
func initWorkspaceDir(workspaceArg string) {
|
||||
userHomeConfDir := filepath.Join(HomeDir, ".config", "siyuan")
|
||||
workspaceConf := filepath.Join(userHomeConfDir, "workspace.json")
|
||||
if !gulu.File.IsExist(workspaceConf) {
|
||||
IsNewbie = "std" == Container // 只有桌面端需要设置新手标识,前端自动挂载帮助文档
|
||||
if err := os.MkdirAll(userHomeConfDir, 0755); nil != err && !os.IsExist(err) {
|
||||
log.Printf("create user home conf folder [%s] failed: %s", userHomeConfDir, err)
|
||||
os.Exit(ExitCodeCreateConfDirErr)
|
||||
}
|
||||
}
|
||||
|
||||
defaultWorkspaceDir := filepath.Join(HomeDir, "Documents", "SiYuan")
|
||||
var workspacePaths []string
|
||||
if !gulu.File.IsExist(workspaceConf) {
|
||||
WorkspaceDir = defaultWorkspaceDir
|
||||
if "" != workspaceArg {
|
||||
WorkspaceDir = workspaceArg
|
||||
}
|
||||
if !gulu.File.IsDir(WorkspaceDir) {
|
||||
log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
|
||||
WorkspaceDir = defaultWorkspaceDir
|
||||
}
|
||||
workspacePaths = append(workspacePaths, WorkspaceDir)
|
||||
} else {
|
||||
data, err := os.ReadFile(workspaceConf)
|
||||
if err = gulu.JSON.UnmarshalJSON(data, &workspacePaths); nil != err {
|
||||
log.Printf("unmarshal workspace conf [%s] failed: %s", workspaceConf, err)
|
||||
}
|
||||
|
||||
tmp := workspacePaths[:0]
|
||||
for _, d := range workspacePaths {
|
||||
if gulu.File.IsDir(d) {
|
||||
tmp = append(tmp, d)
|
||||
}
|
||||
}
|
||||
workspacePaths = tmp
|
||||
|
||||
if 0 < len(workspacePaths) {
|
||||
WorkspaceDir = workspacePaths[len(workspacePaths)-1]
|
||||
if "" != workspaceArg {
|
||||
WorkspaceDir = workspaceArg
|
||||
}
|
||||
if !gulu.File.IsDir(WorkspaceDir) {
|
||||
log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
|
||||
WorkspaceDir = defaultWorkspaceDir
|
||||
}
|
||||
workspacePaths[len(workspacePaths)-1] = WorkspaceDir
|
||||
} else {
|
||||
WorkspaceDir = defaultWorkspaceDir
|
||||
if "" != workspaceArg {
|
||||
WorkspaceDir = workspaceArg
|
||||
}
|
||||
if !gulu.File.IsDir(WorkspaceDir) {
|
||||
log.Printf("use the default workspace [%s] since the specified workspace [%s] is not a dir", WorkspaceDir, defaultWorkspaceDir)
|
||||
WorkspaceDir = defaultWorkspaceDir
|
||||
}
|
||||
workspacePaths = append(workspacePaths, WorkspaceDir)
|
||||
}
|
||||
}
|
||||
|
||||
if data, err := gulu.JSON.MarshalJSON(workspacePaths); nil == err {
|
||||
if err = os.WriteFile(workspaceConf, data, 0644); nil != err {
|
||||
log.Fatalf("write workspace conf [%s] failed: %s", workspaceConf, err)
|
||||
}
|
||||
} else {
|
||||
log.Fatalf("marshal workspace conf [%s] failed: %s", workspaceConf, err)
|
||||
}
|
||||
|
||||
ConfDir = filepath.Join(WorkspaceDir, "conf")
|
||||
DataDir = filepath.Join(WorkspaceDir, "data")
|
||||
TempDir = filepath.Join(WorkspaceDir, "temp")
|
||||
DBPath = filepath.Join(TempDir, DBName)
|
||||
BlockTreePath = filepath.Join(TempDir, "blocktree.msgpack")
|
||||
}
|
||||
|
||||
var (
|
||||
Resident bool
|
||||
ReadOnly bool
|
||||
AccessAuthCode string
|
||||
Lang = "en_US"
|
||||
|
||||
Container string // docker, android, ios, std
|
||||
)
|
||||
|
||||
func initPathDir() {
|
||||
if err := os.MkdirAll(ConfDir, 0755); nil != err && !os.IsExist(err) {
|
||||
log.Fatalf("create conf folder [%s] failed: %s", ConfDir, err)
|
||||
}
|
||||
if err := os.MkdirAll(DataDir, 0755); nil != err && !os.IsExist(err) {
|
||||
log.Fatalf("create data folder [%s] failed: %s", DataDir, err)
|
||||
}
|
||||
if err := os.MkdirAll(TempDir, 0755); nil != err && !os.IsExist(err) {
|
||||
log.Fatalf("create temp folder [%s] failed: %s", TempDir, err)
|
||||
}
|
||||
|
||||
assets := filepath.Join(DataDir, "assets")
|
||||
if err := os.MkdirAll(assets, 0755); nil != err && !os.IsExist(err) {
|
||||
log.Fatalf("create data assets folder [%s] failed: %s", assets, err)
|
||||
}
|
||||
|
||||
templates := filepath.Join(DataDir, "templates")
|
||||
if err := os.MkdirAll(templates, 0755); nil != err && !os.IsExist(err) {
|
||||
log.Fatalf("create data templates folder [%s] failed: %s", templates, err)
|
||||
}
|
||||
|
||||
widgets := filepath.Join(DataDir, "widgets")
|
||||
if err := os.MkdirAll(widgets, 0755); nil != err && !os.IsExist(err) {
|
||||
log.Fatalf("create data widgets folder [%s] failed: %s", widgets, err)
|
||||
}
|
||||
|
||||
emojis := filepath.Join(DataDir, "emojis")
|
||||
if err := os.MkdirAll(emojis, 0755); nil != err && !os.IsExist(err) {
|
||||
log.Fatalf("create data emojis folder [%s] failed: %s", widgets, err)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanOld() {
|
||||
dirs, _ := os.ReadDir(WorkingDir)
|
||||
for _, dir := range dirs {
|
||||
if strings.HasSuffix(dir.Name(), ".old") {
|
||||
old := filepath.Join(WorkingDir, dir.Name())
|
||||
os.RemoveAll(old)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkPort() {
|
||||
portOpened := isPortOpen(ServerPort)
|
||||
if !portOpened {
|
||||
return
|
||||
}
|
||||
|
||||
LogInfof("port [%s] is opened, try to check version of running kernel", ServerPort)
|
||||
result := NewResult()
|
||||
_, err := NewBrowserRequest("").
|
||||
SetResult(result).
|
||||
SetHeader("User-Agent", UserAgent).
|
||||
Get("http://127.0.0.1:" + ServerPort + "/api/system/version")
|
||||
if nil != err || 0 != result.Code {
|
||||
LogErrorf("connect to port [%s] for checking running kernel failed", ServerPort)
|
||||
KillByPort(ServerPort)
|
||||
return
|
||||
}
|
||||
|
||||
if nil == result.Data {
|
||||
LogErrorf("connect ot port [%s] for checking running kernel failed", ServerPort)
|
||||
os.Exit(ExitCodeUnavailablePort)
|
||||
}
|
||||
|
||||
runningVer := result.Data.(string)
|
||||
if runningVer == Ver {
|
||||
LogInfof("version of the running kernel is the same as this boot [%s], exit this boot", runningVer)
|
||||
os.Exit(ExitCodeOk)
|
||||
}
|
||||
|
||||
LogInfof("found kernel [%s] is running, try to exit it", runningVer)
|
||||
processes, err := goPS.Processes()
|
||||
if nil != err {
|
||||
LogErrorf("close kernel [%s] failed: %s", runningVer, err)
|
||||
os.Exit(ExitCodeUnavailablePort)
|
||||
}
|
||||
|
||||
currentPid := os.Getpid()
|
||||
for _, p := range processes {
|
||||
name := p.Executable()
|
||||
if strings.Contains(strings.ToLower(name), "siyuan-kernel") || strings.Contains(strings.ToLower(name), "siyuan kernel") {
|
||||
kernelPid := p.Pid()
|
||||
if currentPid != kernelPid {
|
||||
pid := strconv.Itoa(kernelPid)
|
||||
Kill(pid)
|
||||
LogInfof("killed kernel [name=%s, pid=%s, ver=%s], continue to boot", name, pid, runningVer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !tryToListenPort() {
|
||||
os.Exit(ExitCodeUnavailablePort)
|
||||
}
|
||||
}
|
||||
|
||||
func initMime() {
|
||||
// 在某版本的 Windows 10 操作系统上界面样式异常问题
|
||||
// https://github.com/siyuan-note/siyuan/issues/247
|
||||
// https://github.com/siyuan-note/siyuan/issues/3813
|
||||
mime.AddExtensionType(".css", "text/css")
|
||||
mime.AddExtensionType(".js", "application/x-javascript")
|
||||
mime.AddExtensionType(".json", "application/json")
|
||||
mime.AddExtensionType(".html", "text/html")
|
||||
}
|
||||
|
||||
func KillByPort(port string) {
|
||||
if pid := PidByPort(port); "" != pid {
|
||||
pidInt, _ := strconv.Atoi(pid)
|
||||
proc, _ := goPS.FindProcess(pidInt)
|
||||
var name string
|
||||
if nil != proc {
|
||||
name = proc.Executable()
|
||||
}
|
||||
Kill(pid)
|
||||
LogInfof("killed process [name=%s, pid=%s]", name, pid)
|
||||
}
|
||||
}
|
||||
|
||||
func Kill(pid string) {
|
||||
var kill *exec.Cmd
|
||||
if gulu.OS.IsWindows() {
|
||||
kill = exec.Command("cmd", "/c", "TASKKILL /F /PID "+pid)
|
||||
} else {
|
||||
kill = exec.Command("kill", "-9", pid)
|
||||
}
|
||||
CmdAttr(kill)
|
||||
kill.CombinedOutput()
|
||||
}
|
||||
|
||||
func PidByPort(port string) (ret string) {
|
||||
if gulu.OS.IsWindows() {
|
||||
cmd := exec.Command("cmd", "/c", "netstat -ano | findstr "+port)
|
||||
CmdAttr(cmd)
|
||||
data, err := cmd.CombinedOutput()
|
||||
if nil != err {
|
||||
LogErrorf("netstat failed: %s", err)
|
||||
return
|
||||
}
|
||||
output := string(data)
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, l := range lines {
|
||||
if strings.Contains(l, "LISTENING") {
|
||||
l = l[strings.Index(l, "LISTENING")+len("LISTENING"):]
|
||||
l = strings.TrimSpace(l)
|
||||
ret = l
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("lsof", "-Fp", "-i", ":"+port)
|
||||
CmdAttr(cmd)
|
||||
data, err := cmd.CombinedOutput()
|
||||
if nil != err {
|
||||
LogErrorf("lsof failed: %s", err)
|
||||
return
|
||||
}
|
||||
output := string(data)
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, l := range lines {
|
||||
if strings.HasPrefix(l, "p") {
|
||||
l = l[1:]
|
||||
ret = l
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
53
kernel/util/working_mobile.go
Normal file
53
kernel/util/working_mobile.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
figure "github.com/common-nighthawk/go-figure"
|
||||
)
|
||||
|
||||
func BootMobile(container, appDir, workspaceDir, nativeLibDir, privateDataDir, lang string) {
|
||||
IncBootProgress(3, "Booting...")
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
initMime()
|
||||
|
||||
HomeDir = filepath.Join(workspaceDir, "home")
|
||||
WorkingDir = filepath.Join(appDir, "app")
|
||||
WorkspaceDir = workspaceDir
|
||||
ConfDir = filepath.Join(workspaceDir, "conf")
|
||||
DataDir = filepath.Join(workspaceDir, "data")
|
||||
TempDir = filepath.Join(workspaceDir, "temp")
|
||||
DBPath = filepath.Join(TempDir, DBName)
|
||||
BlockTreePath = filepath.Join(TempDir, "blocktree.msgpack")
|
||||
AndroidNativeLibDir = nativeLibDir
|
||||
AndroidPrivateDataDir = privateDataDir
|
||||
LogPath = filepath.Join(TempDir, "siyuan.log")
|
||||
AppearancePath = filepath.Join(ConfDir, "appearance")
|
||||
ThemesPath = filepath.Join(AppearancePath, "themes")
|
||||
IconsPath = filepath.Join(AppearancePath, "icons")
|
||||
Resident = true
|
||||
Container = container
|
||||
Lang = lang
|
||||
initPathDir()
|
||||
bootBanner := figure.NewFigure("SiYuan", "", true)
|
||||
LogInfof("\n" + bootBanner.String())
|
||||
logBootInfo()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue