// 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 . 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) } } }