siyuan/kernel/model/conf.go

561 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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