561 lines
15 KiB
Go
561 lines
15 KiB
Go
// 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 model
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/sha256"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/88250/gulu"
|
||
"github.com/88250/lute"
|
||
humanize "github.com/dustin/go-humanize"
|
||
"github.com/getsentry/sentry-go"
|
||
"github.com/siyuan-note/siyuan/kernel/conf"
|
||
"github.com/siyuan-note/siyuan/kernel/filesys"
|
||
"github.com/siyuan-note/siyuan/kernel/sql"
|
||
"github.com/siyuan-note/siyuan/kernel/treenode"
|
||
"github.com/siyuan-note/siyuan/kernel/util"
|
||
)
|
||
|
||
var Conf *AppConf
|
||
|
||
// AppConf 维护应用元数据,保存在 ~/.siyuan/conf.json。
|
||
type AppConf struct {
|
||
LogLevel string `json:"logLevel"` // 日志级别:Off, Trace, Debug, Info, Warn, Error, Fatal
|
||
Appearance *conf.Appearance `json:"appearance"` // 外观
|
||
Langs []*conf.Lang `json:"langs"` // 界面语言列表
|
||
Lang string `json:"lang"` // 选择的界面语言,同 Appearance.Lang
|
||
FileTree *conf.FileTree `json:"fileTree"` // 文档面板
|
||
Tag *conf.Tag `json:"tag"` // 标签面板
|
||
Editor *conf.Editor `json:"editor"` // 编辑器配置
|
||
Export *conf.Export `json:"export"` // 导出配置
|
||
Graph *conf.Graph `json:"graph"` // 关系图配置
|
||
UILayout *conf.UILayout `json:"uiLayout"` // 界面布局
|
||
UserData string `json:"userData"` // 社区用户信息,对 User 加密存储
|
||
User *conf.User `json:"-"` // 社区用户内存结构,不持久化
|
||
Account *conf.Account `json:"account"` // 帐号配置
|
||
ReadOnly bool `json:"readonly"` // 是否是只读
|
||
LocalIPs []string `json:"localIPs"` // 本地 IP 列表
|
||
AccessAuthCode string `json:"accessAuthCode"` // 访问授权码
|
||
E2EEPasswd string `json:"e2eePasswd"` // 端到端加密密码,用于备份和同步
|
||
E2EEPasswdMode int `json:"e2eePasswdMode"` // 端到端加密密码生成方式,0:自动,1:自定义
|
||
System *conf.System `json:"system"` // 系统
|
||
Keymap *conf.Keymap `json:"keymap"` // 快捷键
|
||
Backup *conf.Backup `json:"backup"` // 备份配置
|
||
Sync *conf.Sync `json:"sync"` // 同步配置
|
||
Search *conf.Search `json:"search"` // 搜索配置
|
||
Stat *conf.Stat `json:"stat"` // 统计
|
||
Api *conf.API `json:"api"` // API
|
||
Newbie bool `json:"newbie"` // 是否是安装后第一次启动
|
||
}
|
||
|
||
func InitConf() {
|
||
initLang()
|
||
|
||
windowStateConf := filepath.Join(util.ConfDir, "windowState.json")
|
||
if !gulu.File.IsExist(windowStateConf) {
|
||
if err := gulu.File.WriteFileSafer(windowStateConf, []byte("{}"), 0644); nil != err {
|
||
util.LogErrorf("create [windowState.json] failed: %s", err)
|
||
}
|
||
}
|
||
|
||
Conf = &AppConf{LogLevel: "debug", Lang: util.Lang}
|
||
confPath := filepath.Join(util.ConfDir, "conf.json")
|
||
if gulu.File.IsExist(confPath) {
|
||
data, err := os.ReadFile(confPath)
|
||
if nil != err {
|
||
util.LogErrorf("load conf [%s] failed: %s", confPath, err)
|
||
}
|
||
err = gulu.JSON.UnmarshalJSON(data, Conf)
|
||
if err != nil {
|
||
util.LogErrorf("parse conf [%s] failed: %s", confPath, err)
|
||
}
|
||
}
|
||
|
||
Conf.Langs = loadLangs()
|
||
if nil == Conf.Appearance {
|
||
Conf.Appearance = conf.NewAppearance()
|
||
}
|
||
var langOK bool
|
||
for _, l := range Conf.Langs {
|
||
if Conf.Lang == l.Name {
|
||
langOK = true
|
||
break
|
||
}
|
||
}
|
||
if !langOK {
|
||
Conf.Lang = "en_US"
|
||
}
|
||
Conf.Appearance.Lang = Conf.Lang
|
||
if nil == Conf.UILayout {
|
||
Conf.UILayout = &conf.UILayout{}
|
||
}
|
||
if nil == Conf.Keymap {
|
||
Conf.Keymap = &conf.Keymap{}
|
||
}
|
||
if "" == Conf.Appearance.CodeBlockThemeDark {
|
||
Conf.Appearance.CodeBlockThemeDark = "dracula"
|
||
}
|
||
if "" == Conf.Appearance.CodeBlockThemeLight {
|
||
Conf.Appearance.CodeBlockThemeLight = "github"
|
||
}
|
||
if nil == Conf.FileTree {
|
||
Conf.FileTree = conf.NewFileTree()
|
||
}
|
||
if 1 > Conf.FileTree.MaxListCount {
|
||
Conf.FileTree.MaxListCount = 512
|
||
}
|
||
if nil == Conf.Tag {
|
||
Conf.Tag = conf.NewTag()
|
||
}
|
||
if nil == Conf.Editor {
|
||
Conf.Editor = conf.NewEditor()
|
||
}
|
||
if 1 > len(Conf.Editor.Emoji) {
|
||
Conf.Editor.Emoji = []string{}
|
||
}
|
||
if 1 > Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
|
||
Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 64
|
||
}
|
||
if 5120 < Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
|
||
Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 5120
|
||
}
|
||
if nil == Conf.Export {
|
||
Conf.Export = conf.NewExport()
|
||
}
|
||
if 0 == Conf.Export.BlockRefMode || 1 == Conf.Export.BlockRefMode {
|
||
// 废弃导出选项引用块转换为原始块和引述块 https://github.com/siyuan-note/siyuan/issues/3155
|
||
Conf.Export.BlockRefMode = 4 // 改为脚注
|
||
}
|
||
if 9 > Conf.Editor.FontSize || 72 < Conf.Editor.FontSize {
|
||
Conf.Editor.FontSize = 16
|
||
}
|
||
if "" == Conf.Editor.PlantUMLServePath {
|
||
Conf.Editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1"
|
||
}
|
||
|
||
if nil == Conf.Graph || nil == Conf.Graph.Local || nil == Conf.Graph.Global {
|
||
Conf.Graph = conf.NewGraph()
|
||
}
|
||
if nil == Conf.System {
|
||
Conf.System = conf.NewSystem()
|
||
} else {
|
||
Conf.System.KernelVersion = util.Ver
|
||
Conf.System.IsInsider = util.IsInsider
|
||
}
|
||
if nil == Conf.System.NetworkProxy {
|
||
Conf.System.NetworkProxy = &conf.NetworkProxy{}
|
||
}
|
||
if "" != Conf.System.NetworkProxy.Scheme {
|
||
util.LogInfof("using network proxy [%s]", Conf.System.NetworkProxy.String())
|
||
}
|
||
if "" == Conf.System.ID {
|
||
Conf.System.ID = util.GetDeviceID()
|
||
}
|
||
if "std" == util.Container {
|
||
Conf.System.ID = util.GetDeviceID()
|
||
}
|
||
|
||
Conf.System.AppDir = util.WorkingDir
|
||
Conf.System.ConfDir = util.ConfDir
|
||
Conf.System.HomeDir = util.HomeDir
|
||
Conf.System.WorkspaceDir = util.WorkspaceDir
|
||
Conf.System.DataDir = util.DataDir
|
||
Conf.System.Container = util.Container
|
||
util.UserAgent = util.UserAgent + " " + util.Container
|
||
Conf.System.OS = runtime.GOOS
|
||
Conf.Newbie = util.IsNewbie
|
||
|
||
if "" != Conf.UserData {
|
||
Conf.User = loadUserFromConf()
|
||
}
|
||
if nil == Conf.Account {
|
||
Conf.Account = conf.NewAccount()
|
||
}
|
||
|
||
if nil == Conf.Backup {
|
||
Conf.Backup = conf.NewBackup()
|
||
}
|
||
if !gulu.File.IsExist(Conf.Backup.GetSaveDir()) {
|
||
if err := os.MkdirAll(Conf.Backup.GetSaveDir(), 0755); nil != err {
|
||
util.LogErrorf("create backup dir [%s] failed: %s", Conf.Backup.GetSaveDir(), err)
|
||
}
|
||
}
|
||
|
||
if nil == Conf.Sync {
|
||
Conf.Sync = conf.NewSync()
|
||
}
|
||
if !gulu.File.IsExist(Conf.Sync.GetSaveDir()) {
|
||
if err := os.MkdirAll(Conf.Sync.GetSaveDir(), 0755); nil != err {
|
||
util.LogErrorf("create sync dir [%s] failed: %s", Conf.Sync.GetSaveDir(), err)
|
||
}
|
||
}
|
||
|
||
if nil == Conf.Api {
|
||
Conf.Api = conf.NewAPI()
|
||
}
|
||
|
||
if 1440 < Conf.Editor.GenerateHistoryInterval {
|
||
Conf.Editor.GenerateHistoryInterval = 1440
|
||
}
|
||
if 1 > Conf.Editor.HistoryRetentionDays {
|
||
Conf.Editor.HistoryRetentionDays = 7
|
||
}
|
||
|
||
if nil == Conf.Search {
|
||
Conf.Search = conf.NewSearch()
|
||
}
|
||
|
||
if nil == Conf.Stat {
|
||
Conf.Stat = conf.NewStat()
|
||
}
|
||
|
||
Conf.ReadOnly = util.ReadOnly
|
||
if "" != util.AccessAuthCode {
|
||
Conf.AccessAuthCode = util.AccessAuthCode
|
||
}
|
||
|
||
Conf.E2EEPasswdMode = 0
|
||
if !isBuiltInE2EEPasswd() {
|
||
Conf.E2EEPasswdMode = 1
|
||
}
|
||
|
||
Conf.LocalIPs = util.GetLocalIPs()
|
||
|
||
Conf.Save()
|
||
util.SetLogLevel(Conf.LogLevel)
|
||
|
||
if Conf.System.UploadErrLog {
|
||
util.LogInfof("user has enabled [Automatically upload error messages and diagnostic data]")
|
||
sentry.Init(sentry.ClientOptions{
|
||
Dsn: "https://bdff135f14654ae58a054adeceb2c308@o1173696.ingest.sentry.io/6269178",
|
||
Release: util.Ver,
|
||
Environment: util.Mode,
|
||
})
|
||
}
|
||
}
|
||
|
||
var langs = map[string]map[int]string{}
|
||
var timeLangs = map[string]map[string]interface{}{}
|
||
|
||
func initLang() {
|
||
p := filepath.Join(util.WorkingDir, "appearance", "langs")
|
||
dir, err := os.Open(p)
|
||
if nil != err {
|
||
util.LogFatalf("open language configuration folder [%s] failed: %s", p, err)
|
||
}
|
||
defer dir.Close()
|
||
|
||
langNames, err := dir.Readdirnames(-1)
|
||
if nil != err {
|
||
util.LogFatalf("list language configuration folder [%s] failed: %s", p, err)
|
||
}
|
||
|
||
for _, langName := range langNames {
|
||
jsonPath := filepath.Join(p, langName)
|
||
data, err := os.ReadFile(jsonPath)
|
||
if nil != err {
|
||
util.LogErrorf("read language configuration [%s] failed: %s", jsonPath, err)
|
||
continue
|
||
}
|
||
langMap := map[string]interface{}{}
|
||
if err := gulu.JSON.UnmarshalJSON(data, &langMap); nil != err {
|
||
util.LogErrorf("parse language configuration failed [%s] failed: %s", jsonPath, err)
|
||
continue
|
||
}
|
||
|
||
kernelMap := map[int]string{}
|
||
label := langMap["_label"].(string)
|
||
kernelLangs := langMap["_kernel"].(map[string]interface{})
|
||
for k, v := range kernelLangs {
|
||
num, err := strconv.Atoi(k)
|
||
if nil != err {
|
||
util.LogErrorf("parse language configuration [%s] item [%d] failed [%s] failed: %s", p, num, err)
|
||
continue
|
||
}
|
||
kernelMap[num] = v.(string)
|
||
}
|
||
kernelMap[-1] = label
|
||
name := langName[:strings.LastIndex(langName, ".")]
|
||
langs[name] = kernelMap
|
||
|
||
timeLangs[name] = langMap["_time"].(map[string]interface{})
|
||
}
|
||
}
|
||
|
||
func loadLangs() (ret []*conf.Lang) {
|
||
for name, langMap := range langs {
|
||
lang := &conf.Lang{Label: langMap[-1], Name: name}
|
||
ret = append(ret, lang)
|
||
}
|
||
sort.Slice(ret, func(i, j int) bool {
|
||
return ret[i].Name < ret[j].Name
|
||
})
|
||
return
|
||
}
|
||
|
||
var exitLock = sync.Mutex{}
|
||
|
||
func Close(force bool) (err error) {
|
||
exitLock.Lock()
|
||
defer exitLock.Unlock()
|
||
|
||
treenode.CloseBlockTree()
|
||
util.PushMsg(Conf.Language(95), 10000*60)
|
||
WaitForWritingFiles()
|
||
if !force {
|
||
SyncData(false, true, false)
|
||
if 0 != ExitSyncSucc {
|
||
err = errors.New(Conf.Language(96))
|
||
return
|
||
}
|
||
}
|
||
|
||
//util.UIProcessIDs.Range(func(key, _ interface{}) bool {
|
||
// pid := key.(string)
|
||
// util.Kill(pid)
|
||
// return true
|
||
//})
|
||
|
||
Conf.Close()
|
||
sql.CloseDatabase()
|
||
util.WebSocketServer.Close()
|
||
clearWorkspaceTemp()
|
||
util.LogInfof("exited kernel")
|
||
go func() {
|
||
time.Sleep(500 * time.Millisecond)
|
||
os.Exit(util.ExitCodeOk)
|
||
}()
|
||
return
|
||
}
|
||
|
||
var CustomEmojis = sync.Map{}
|
||
|
||
func NewLute() (ret *lute.Lute) {
|
||
ret = util.NewLute()
|
||
ret.SetCodeSyntaxHighlightLineNum(Conf.Editor.CodeSyntaxHighlightLineNum)
|
||
ret.SetChineseParagraphBeginningSpace(Conf.Export.ParagraphBeginningSpace)
|
||
ret.SetProtyleMarkNetImg(Conf.Editor.DisplayNetImgMark)
|
||
|
||
customEmojiMap := map[string]string{}
|
||
CustomEmojis.Range(func(key, value interface{}) bool {
|
||
customEmojiMap[key.(string)] = value.(string)
|
||
return true
|
||
})
|
||
ret.PutEmojis(customEmojiMap)
|
||
return
|
||
}
|
||
|
||
var confSaveLock = sync.Mutex{}
|
||
|
||
func (conf *AppConf) Save() {
|
||
confSaveLock.Lock()
|
||
confSaveLock.Unlock()
|
||
|
||
newData, _ := gulu.JSON.MarshalIndentJSON(Conf, "", " ")
|
||
confPath := filepath.Join(util.ConfDir, "conf.json")
|
||
oldData, err := filesys.NoLockFileRead(confPath)
|
||
if nil != err {
|
||
conf.save0(newData)
|
||
return
|
||
}
|
||
|
||
if bytes.Equal(newData, oldData) {
|
||
return
|
||
}
|
||
|
||
conf.save0(newData)
|
||
}
|
||
|
||
func (conf *AppConf) save0(data []byte) {
|
||
confPath := filepath.Join(util.ConfDir, "conf.json")
|
||
if err := filesys.LockFileWrite(confPath, data); nil != err {
|
||
util.LogFatalf("write conf [%s] failed: %s", confPath, err)
|
||
}
|
||
}
|
||
|
||
func (conf *AppConf) Close() {
|
||
conf.Save()
|
||
}
|
||
|
||
func (conf *AppConf) Box(boxID string) *Box {
|
||
for _, box := range conf.GetOpenedBoxes() {
|
||
if box.ID == boxID {
|
||
return box
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (conf *AppConf) GetBoxes() (ret []*Box) {
|
||
ret = []*Box{}
|
||
notebooks, err := ListNotebooks()
|
||
if nil != err {
|
||
return
|
||
}
|
||
|
||
for _, notebook := range notebooks {
|
||
id := notebook.ID
|
||
name := notebook.Name
|
||
closed := notebook.Closed
|
||
box := &Box{ID: id, Name: name, Closed: closed}
|
||
ret = append(ret, box)
|
||
}
|
||
return
|
||
}
|
||
|
||
func (conf *AppConf) GetOpenedBoxes() (ret []*Box) {
|
||
ret = []*Box{}
|
||
notebooks, err := ListNotebooks()
|
||
if nil != err {
|
||
return
|
||
}
|
||
|
||
for _, notebook := range notebooks {
|
||
if !notebook.Closed {
|
||
ret = append(ret, notebook)
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
func (conf *AppConf) GetClosedBoxes() (ret []*Box) {
|
||
ret = []*Box{}
|
||
notebooks, err := ListNotebooks()
|
||
if nil != err {
|
||
return
|
||
}
|
||
|
||
for _, notebook := range notebooks {
|
||
if notebook.Closed {
|
||
ret = append(ret, notebook)
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
func (conf *AppConf) Language(num int) string {
|
||
return langs[conf.Lang][num]
|
||
}
|
||
|
||
func InitBoxes() {
|
||
initialized := false
|
||
blockCount := 0
|
||
if 1 > len(treenode.GetBlockTrees()) {
|
||
if gulu.File.IsExist(util.BlockTreePath) {
|
||
util.IncBootProgress(30, "Reading block trees...")
|
||
go func() {
|
||
for i := 0; i < 40; i++ {
|
||
util.RandomSleep(100, 200)
|
||
util.IncBootProgress(1, "Reading block trees...")
|
||
}
|
||
}()
|
||
if err := treenode.ReadBlockTree(); nil == err {
|
||
initialized = true
|
||
} else {
|
||
if err = os.RemoveAll(util.BlockTreePath); nil != err {
|
||
util.LogErrorf("remove block tree [%s] failed: %s", util.BlockTreePath, err)
|
||
}
|
||
}
|
||
}
|
||
} else { // 大于 1 的话说明在同步阶段已经加载过了
|
||
initialized = true
|
||
}
|
||
|
||
for _, box := range Conf.GetOpenedBoxes() {
|
||
box.UpdateHistoryGenerated() // 初始化历史生成时间为当前时间
|
||
if !initialized {
|
||
box.BootIndex()
|
||
}
|
||
|
||
ListDocTree(box.ID, "/", Conf.FileTree.Sort) // 缓存根一级的文档树展开
|
||
}
|
||
|
||
if !initialized {
|
||
treenode.SaveBlockTree()
|
||
}
|
||
|
||
blocktrees := treenode.GetBlockTrees()
|
||
blockCount = len(blocktrees)
|
||
|
||
var dbSize string
|
||
if dbFile, err := os.Stat(util.DBPath); nil == err {
|
||
dbSize = humanize.Bytes(uint64(dbFile.Size()))
|
||
}
|
||
util.LogInfof("database size [%s], block count [%d]", dbSize, blockCount)
|
||
}
|
||
|
||
func IsSubscriber() bool {
|
||
return nil != Conf.User && (-1 == Conf.User.UserSiYuanProExpireTime || 0 < Conf.User.UserSiYuanProExpireTime) && 0 == Conf.User.UserSiYuanSubscriptionStatus
|
||
}
|
||
|
||
func isBuiltInE2EEPasswd() bool {
|
||
if nil == Conf || nil == Conf.User || "" == Conf.E2EEPasswd {
|
||
return true
|
||
}
|
||
|
||
pwd := GetBuiltInE2EEPasswd()
|
||
return Conf.E2EEPasswd == util.AESEncrypt(pwd)
|
||
}
|
||
|
||
func GetBuiltInE2EEPasswd() (ret string) {
|
||
part1 := Conf.User.UserId[:7]
|
||
part2 := Conf.User.UserId[7:]
|
||
ret = part2 + part1
|
||
ret = fmt.Sprintf("%x", sha256.Sum256([]byte(ret)))[:7]
|
||
return
|
||
}
|
||
|
||
func clearWorkspaceTemp() {
|
||
os.RemoveAll(filepath.Join(util.TempDir, "bazaar"))
|
||
os.RemoveAll(filepath.Join(util.TempDir, "export"))
|
||
os.RemoveAll(filepath.Join(util.TempDir, "import"))
|
||
|
||
tmps, err := filepath.Glob(filepath.Join(util.TempDir, "*.tmp"))
|
||
if nil != err {
|
||
util.LogErrorf("glob temp files failed: %s", err)
|
||
}
|
||
for _, tmp := range tmps {
|
||
if err = os.RemoveAll(tmp); nil != err {
|
||
util.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
|
||
} else {
|
||
util.LogInfof("removed temp file [%s]", tmp)
|
||
}
|
||
}
|
||
|
||
tmps, err = filepath.Glob(filepath.Join(util.DataDir, ".siyuan", "*.tmp"))
|
||
if nil != err {
|
||
util.LogErrorf("glob temp files failed: %s", err)
|
||
}
|
||
for _, tmp := range tmps {
|
||
if err = os.RemoveAll(tmp); nil != err {
|
||
util.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
|
||
} else {
|
||
util.LogInfof("removed temp file [%s]", tmp)
|
||
}
|
||
}
|
||
}
|