A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1// SiYuan - Refactor your thinking
2// Copyright (c) 2020-present, b3log.org
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17package model
18
19import (
20 "bytes"
21 "crypto/sha1"
22 "fmt"
23 "os"
24 "path/filepath"
25 "runtime"
26 "sort"
27 "strconv"
28 "strings"
29 "sync"
30 "time"
31
32 "github.com/88250/gulu"
33 "github.com/88250/lute"
34 "github.com/88250/lute/ast"
35 "github.com/Xuanwo/go-locale"
36 "github.com/sashabaranov/go-openai"
37 "github.com/siyuan-note/eventbus"
38 "github.com/siyuan-note/filelock"
39 "github.com/siyuan-note/logging"
40 "github.com/siyuan-note/siyuan/kernel/conf"
41 "github.com/siyuan-note/siyuan/kernel/sql"
42 "github.com/siyuan-note/siyuan/kernel/task"
43 "github.com/siyuan-note/siyuan/kernel/treenode"
44 "github.com/siyuan-note/siyuan/kernel/util"
45 "golang.org/x/mod/semver"
46 "golang.org/x/text/language"
47)
48
49var Conf *AppConf
50
51// AppConf 维护应用元数据,保存在 ~/.siyuan/conf.json。
52type AppConf struct {
53 LogLevel string `json:"logLevel"` // 日志级别:off, trace, debug, info, warn, error, fatal
54 Appearance *conf.Appearance `json:"appearance"` // 外观
55 Langs []*conf.Lang `json:"langs"` // 界面语言列表
56 Lang string `json:"lang"` // 选择的界面语言,同 Appearance.Lang
57 FileTree *conf.FileTree `json:"fileTree"` // 文档面板
58 Tag *conf.Tag `json:"tag"` // 标签面板
59 Editor *conf.Editor `json:"editor"` // 编辑器配置
60 Export *conf.Export `json:"export"` // 导出配置
61 Graph *conf.Graph `json:"graph"` // 关系图配置
62 UILayout *conf.UILayout `json:"uiLayout"` // 界面布局。不要直接使用,使用 GetUILayout() 和 SetUILayout() 方法
63 UserData string `json:"userData"` // 社区用户信息,对 User 加密存储
64 User *conf.User `json:"-"` // 社区用户内存结构,不持久化。不要直接使用,使用 GetUser() 和 SetUser() 方法
65 ReadOnly bool `json:"readonly"` // 是否是以只读模式运行
66 LocalIPs []string `json:"localIPs"` // 本地 IP 列表
67 AccessAuthCode string `json:"accessAuthCode"` // 访问授权码
68 System *conf.System `json:"system"` // 系统配置
69 Keymap *conf.Keymap `json:"keymap"` // 快捷键配置
70 Sync *conf.Sync `json:"sync"` // 同步配置
71 Search *conf.Search `json:"search"` // 搜索配置
72 Flashcard *conf.Flashcard `json:"flashcard"` // 闪卡配置
73 AI *conf.AI `json:"ai"` // 人工智能配置
74 Bazaar *conf.Bazaar `json:"bazaar"` // 集市配置
75 Stat *conf.Stat `json:"stat"` // 统计
76 Api *conf.API `json:"api"` // API
77 Repo *conf.Repo `json:"repo"` // 数据仓库
78 Publish *conf.Publish `json:"publish"` // 发布服务
79 OpenHelp bool `json:"openHelp"` // 启动后是否需要打开用户指南
80 ShowChangelog bool `json:"showChangelog"` // 是否显示版本更新日志
81 CloudRegion int `json:"cloudRegion"` // 云端区域,0:中国大陆,1:北美
82 Snippet *conf.Snpt `json:"snippet"` // 代码片段
83 DataIndexState int `json:"dataIndexState"` // 数据索引状态,0:已索引,1:未索引
84
85 m *sync.Mutex
86}
87
88func NewAppConf() *AppConf {
89 return &AppConf{LogLevel: "debug", m: &sync.Mutex{}}
90}
91
92func (conf *AppConf) GetUILayout() *conf.UILayout {
93 conf.m.Lock()
94 defer conf.m.Unlock()
95 return conf.UILayout
96}
97
98func (conf *AppConf) SetUILayout(uiLayout *conf.UILayout) {
99 conf.m.Lock()
100 defer conf.m.Unlock()
101 conf.UILayout = uiLayout
102}
103
104func (conf *AppConf) GetUser() *conf.User {
105 conf.m.Lock()
106 defer conf.m.Unlock()
107 return conf.User
108}
109
110func (conf *AppConf) SetUser(user *conf.User) {
111 conf.m.Lock()
112 defer conf.m.Unlock()
113 conf.User = user
114}
115
116func InitConf() {
117 initLang()
118
119 Conf = NewAppConf()
120 confPath := filepath.Join(util.ConfDir, "conf.json")
121 if gulu.File.IsExist(confPath) {
122 if data, err := os.ReadFile(confPath); err != nil {
123 logging.LogErrorf("load conf [%s] failed: %s", confPath, err)
124 } else {
125 if err = gulu.JSON.UnmarshalJSON(data, Conf); err != nil {
126 logging.LogErrorf("parse conf [%s] failed: %s", confPath, err)
127 } else {
128 logging.LogInfof("loaded conf [%s]", confPath)
129 }
130 }
131 }
132
133 if "" != util.Lang {
134 initialized := false
135 if util.ContainerAndroid == util.Container || util.ContainerIOS == util.Container || util.ContainerHarmony == util.Container {
136 // 移动端以上次设置的外观语言为准
137 if "" != Conf.Lang && util.Lang != Conf.Lang {
138 util.Lang = Conf.Lang
139 logging.LogInfof("use the last specified language [%s]", util.Lang)
140 initialized = true
141 }
142 }
143
144 if !initialized {
145 Conf.Lang = util.Lang
146 logging.LogInfof("initialized the specified language [%s]", util.Lang)
147 }
148 } else {
149 if "" == Conf.Lang {
150 // 未指定外观语言时使用系统语言
151
152 if userLang, err := locale.Detect(); err == nil {
153 var supportLangs []language.Tag
154 for lang := range util.Langs {
155 if tag, err := language.Parse(lang); err == nil {
156 supportLangs = append(supportLangs, tag)
157 } else {
158 logging.LogErrorf("load language [%s] failed: %s", lang, err)
159 }
160 }
161 matcher := language.NewMatcher(supportLangs)
162 lang, _, _ := matcher.Match(userLang)
163 base, _ := lang.Base()
164 region, _ := lang.Region()
165 util.Lang = base.String() + "_" + region.String()
166 Conf.Lang = util.Lang
167 logging.LogInfof("initialized language [%s] based on device locale", Conf.Lang)
168 } else {
169 logging.LogDebugf("check device locale failed [%s], using default language [en_US]", err)
170 util.Lang = "en_US"
171 Conf.Lang = util.Lang
172 }
173 }
174 util.Lang = Conf.Lang
175 }
176
177 Conf.Langs = loadLangs()
178 if nil == Conf.Appearance {
179 Conf.Appearance = conf.NewAppearance()
180 }
181 var langOK bool
182 for _, l := range Conf.Langs {
183 if Conf.Lang == l.Name {
184 langOK = true
185 break
186 }
187 }
188 if !langOK {
189 Conf.Lang = "en_US"
190 util.Lang = Conf.Lang
191 }
192 Conf.Appearance.Lang = Conf.Lang
193 if nil == Conf.UILayout {
194 Conf.UILayout = &conf.UILayout{}
195 }
196 if nil == Conf.Keymap {
197 Conf.Keymap = &conf.Keymap{}
198 }
199 if "" == Conf.Appearance.CodeBlockThemeDark {
200 Conf.Appearance.CodeBlockThemeDark = "dracula"
201 }
202 if "" == Conf.Appearance.CodeBlockThemeLight {
203 Conf.Appearance.CodeBlockThemeLight = "github"
204 }
205 if nil == Conf.FileTree {
206 Conf.FileTree = conf.NewFileTree()
207 }
208 if 1 > Conf.FileTree.MaxListCount {
209 Conf.FileTree.MaxListCount = 512
210 }
211 if 1 > Conf.FileTree.MaxOpenTabCount {
212 Conf.FileTree.MaxOpenTabCount = 8
213 }
214 if 32 < Conf.FileTree.MaxOpenTabCount {
215 Conf.FileTree.MaxOpenTabCount = 32
216 }
217 Conf.FileTree.DocCreateSavePath = util.TrimSpaceInPath(Conf.FileTree.DocCreateSavePath)
218 Conf.FileTree.RefCreateSavePath = util.TrimSpaceInPath(Conf.FileTree.RefCreateSavePath)
219 util.UseSingleLineSave = Conf.FileTree.UseSingleLineSave
220 if 2 > Conf.FileTree.LargeFileWarningSize {
221 Conf.FileTree.LargeFileWarningSize = 8
222 }
223 util.LargeFileWarningSize = Conf.FileTree.LargeFileWarningSize
224
225 util.CurrentCloudRegion = Conf.CloudRegion
226
227 if nil == Conf.Tag {
228 Conf.Tag = conf.NewTag()
229 }
230
231 if nil == Conf.Editor {
232 Conf.Editor = conf.NewEditor()
233 }
234 if 1 > len(Conf.Editor.Emoji) {
235 Conf.Editor.Emoji = []string{}
236 }
237 for i, emoji := range Conf.Editor.Emoji {
238 if strings.Contains(emoji, ".") {
239 // XSS through emoji name https://github.com/siyuan-note/siyuan/issues/15034
240 emoji = util.FilterUploadEmojiFileName(emoji)
241 Conf.Editor.Emoji[i] = emoji
242 }
243 }
244 if 9 > Conf.Editor.FontSize || 72 < Conf.Editor.FontSize {
245 Conf.Editor.FontSize = 16
246 }
247 if "" == Conf.Editor.PlantUMLServePath {
248 Conf.Editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1"
249 }
250 if 1 > Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
251 Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 64
252 }
253 if 5120 < Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
254 Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 5120
255 }
256 if 1440 < Conf.Editor.GenerateHistoryInterval {
257 Conf.Editor.GenerateHistoryInterval = 1440
258 }
259 if 1 > Conf.Editor.HistoryRetentionDays {
260 Conf.Editor.HistoryRetentionDays = 30
261 }
262 if 3650 < Conf.Editor.HistoryRetentionDays {
263 Conf.Editor.HistoryRetentionDays = 3650
264 }
265 if conf.MinDynamicLoadBlocks > Conf.Editor.DynamicLoadBlocks {
266 Conf.Editor.DynamicLoadBlocks = conf.MinDynamicLoadBlocks
267 }
268 if 0 > Conf.Editor.BacklinkExpandCount {
269 Conf.Editor.BacklinkExpandCount = 0
270 }
271 if -1 > Conf.Editor.BackmentionExpandCount {
272 Conf.Editor.BackmentionExpandCount = -1
273 }
274 if nil == Conf.Editor.Markdown {
275 Conf.Editor.Markdown = &util.Markdown{}
276 }
277 util.MarkdownSettings = Conf.Editor.Markdown
278
279 if nil == Conf.Export {
280 Conf.Export = conf.NewExport()
281 }
282 if 0 == Conf.Export.BlockRefMode || 1 == Conf.Export.BlockRefMode || 5 == Conf.Export.BlockRefMode {
283 // 废弃导出选项引用块转换为原始块和引述块 https://github.com/siyuan-note/siyuan/issues/3155
284 // 锚点哈希模式和脚注模式合并 https://github.com/siyuan-note/siyuan/issues/13331
285 Conf.Export.BlockRefMode = 4 // 改为脚注+锚点哈希
286 }
287 if "" == Conf.Export.PandocBin {
288 Conf.Export.PandocBin = util.PandocBinPath
289 }
290
291 if nil == Conf.Graph || nil == Conf.Graph.Local || nil == Conf.Graph.Global {
292 Conf.Graph = conf.NewGraph()
293 }
294
295 if nil == Conf.System {
296 Conf.System = conf.NewSystem()
297 if util.ContainerIOS != util.Container {
298 Conf.OpenHelp = true
299 }
300 } else {
301 if 0 < semver.Compare("v"+util.Ver, "v"+Conf.System.KernelVersion) {
302 logging.LogInfof("upgraded from version [%s] to [%s]", Conf.System.KernelVersion, util.Ver)
303 Conf.ShowChangelog = true
304 } else if 0 > semver.Compare("v"+util.Ver, "v"+Conf.System.KernelVersion) {
305 logging.LogInfof("downgraded from version [%s] to [%s]", Conf.System.KernelVersion, util.Ver)
306 }
307
308 Conf.System.KernelVersion = util.Ver
309 Conf.System.IsInsider = util.IsInsider
310 }
311 if nil == Conf.System.NetworkProxy {
312 Conf.System.NetworkProxy = &conf.NetworkProxy{}
313 }
314 if "" == Conf.System.ID {
315 Conf.System.ID = util.GetDeviceID()
316 }
317 if "" == Conf.System.Name {
318 Conf.System.Name = util.GetDeviceName()
319 }
320 if util.ContainerStd == util.Container {
321 Conf.System.ID = util.GetDeviceID()
322 Conf.System.Name = util.GetDeviceName()
323 }
324 Conf.System.DisabledFeatures = util.DisabledFeatures
325 if 1 > len(Conf.System.DisabledFeatures) {
326 Conf.System.DisabledFeatures = []string{}
327 }
328
329 if nil == Conf.Snippet {
330 Conf.Snippet = conf.NewSnpt()
331 }
332
333 Conf.System.AppDir = util.WorkingDir
334 Conf.System.ConfDir = util.ConfDir
335 Conf.System.HomeDir = util.HomeDir
336 Conf.System.WorkspaceDir = util.WorkspaceDir
337 Conf.System.DataDir = util.DataDir
338 Conf.System.Container = util.Container
339 Conf.System.IsMicrosoftStore = util.ISMicrosoftStore
340 if util.ISMicrosoftStore {
341 logging.LogInfof("using Microsoft Store edition")
342 }
343 Conf.System.OS = runtime.GOOS
344 Conf.System.OSPlatform = util.GetOSPlatform()
345
346 if "" != Conf.UserData {
347 Conf.SetUser(loadUserFromConf())
348 }
349
350 if nil == Conf.Sync {
351 Conf.Sync = conf.NewSync()
352 }
353 if 0 == Conf.Sync.Mode {
354 Conf.Sync.Mode = 1
355 }
356 if 30 > Conf.Sync.Interval {
357 Conf.Sync.Interval = 30
358 }
359 if 60*60*12 < Conf.Sync.Interval {
360 Conf.Sync.Interval = 60 * 60 * 12
361 }
362 if nil == Conf.Sync.S3 {
363 Conf.Sync.S3 = &conf.S3{PathStyle: true, SkipTlsVerify: true}
364 }
365 Conf.Sync.S3.Endpoint = util.NormalizeEndpoint(Conf.Sync.S3.Endpoint)
366 Conf.Sync.S3.Timeout = util.NormalizeTimeout(Conf.Sync.S3.Timeout)
367 Conf.Sync.S3.ConcurrentReqs = util.NormalizeConcurrentReqs(Conf.Sync.S3.ConcurrentReqs, conf.ProviderS3)
368 if nil == Conf.Sync.WebDAV {
369 Conf.Sync.WebDAV = &conf.WebDAV{SkipTlsVerify: true}
370 }
371 Conf.Sync.WebDAV.Endpoint = util.NormalizeEndpoint(Conf.Sync.WebDAV.Endpoint)
372 Conf.Sync.WebDAV.Timeout = util.NormalizeTimeout(Conf.Sync.WebDAV.Timeout)
373 Conf.Sync.WebDAV.ConcurrentReqs = util.NormalizeConcurrentReqs(Conf.Sync.WebDAV.ConcurrentReqs, conf.ProviderWebDAV)
374 if nil == Conf.Sync.Local {
375 Conf.Sync.Local = &conf.Local{}
376 }
377 Conf.Sync.Local.Endpoint = util.NormalizeLocalPath(Conf.Sync.Local.Endpoint)
378 Conf.Sync.Local.Timeout = util.NormalizeTimeout(Conf.Sync.Local.Timeout)
379 Conf.Sync.Local.ConcurrentReqs = util.NormalizeConcurrentReqs(Conf.Sync.Local.ConcurrentReqs, conf.ProviderLocal)
380
381 if util.ContainerDocker == util.Container {
382 Conf.Sync.Perception = false
383 }
384
385 if nil == Conf.Api {
386 Conf.Api = conf.NewAPI()
387 }
388
389 if nil == Conf.Bazaar {
390 Conf.Bazaar = conf.NewBazaar()
391 }
392
393 if nil == Conf.Publish {
394 Conf.Publish = conf.NewPublish()
395 }
396 if Conf.OpenHelp && Conf.Publish.Enable {
397 Conf.OpenHelp = false
398 }
399
400 if nil == Conf.Repo {
401 Conf.Repo = conf.NewRepo()
402 }
403 if timingEnv := os.Getenv("SIYUAN_SYNC_INDEX_TIMING"); "" != timingEnv {
404 val, err := strconv.Atoi(timingEnv)
405 if err == nil {
406 Conf.Repo.SyncIndexTiming = int64(val)
407 }
408 }
409 if 12000 > Conf.Repo.SyncIndexTiming {
410 Conf.Repo.SyncIndexTiming = 12 * 1000
411 }
412 if 1 > Conf.Repo.IndexRetentionDays {
413 Conf.Repo.IndexRetentionDays = 180
414 }
415 if 1 > Conf.Repo.RetentionIndexesDaily {
416 Conf.Repo.RetentionIndexesDaily = 2
417 }
418 if 0 < len(Conf.Repo.Key) {
419 logging.LogInfof("repo key [%x]", sha1.Sum(Conf.Repo.Key))
420 }
421
422 if nil == Conf.Search {
423 Conf.Search = conf.NewSearch()
424 }
425 if 1 > Conf.Search.Limit {
426 Conf.Search.Limit = 64
427 }
428 if 32 > Conf.Search.Limit {
429 Conf.Search.Limit = 32
430 }
431 if 1 > Conf.Search.BacklinkMentionKeywordsLimit {
432 Conf.Search.BacklinkMentionKeywordsLimit = 512
433 }
434
435 if nil == Conf.Stat {
436 Conf.Stat = conf.NewStat()
437 }
438
439 if nil == Conf.Flashcard {
440 Conf.Flashcard = conf.NewFlashcard()
441 }
442 if 0 > Conf.Flashcard.NewCardLimit {
443 Conf.Flashcard.NewCardLimit = 20
444 }
445 if 0 > Conf.Flashcard.ReviewCardLimit {
446 Conf.Flashcard.ReviewCardLimit = 200
447 }
448 if 0 >= Conf.Flashcard.RequestRetention || 1 <= Conf.Flashcard.RequestRetention {
449 Conf.Flashcard.RequestRetention = conf.NewFlashcard().RequestRetention
450 }
451 if 0 >= Conf.Flashcard.MaximumInterval || 36500 <= Conf.Flashcard.MaximumInterval {
452 Conf.Flashcard.MaximumInterval = conf.NewFlashcard().MaximumInterval
453 }
454 if "" == Conf.Flashcard.Weights {
455 Conf.Flashcard.Weights = conf.NewFlashcard().Weights
456 }
457 if 19 != len(strings.Split(Conf.Flashcard.Weights, ",")) {
458 defaultWeights := conf.DefaultFSRSWeights()
459 msg := "fsrs store weights length must be [19]"
460 logging.LogWarnf("%s , given [%s], reset to default weights [%s]", msg, Conf.Flashcard.Weights, defaultWeights)
461 Conf.Flashcard.Weights = defaultWeights
462 go func() {
463 util.WaitForUILoaded()
464 task.AppendAsyncTaskWithDelay(task.PushMsg, 2*time.Second, util.PushErrMsg, msg, 15000)
465 }()
466 }
467 isInvalidFlashcardWeights := false
468 for _, w := range strings.Split(Conf.Flashcard.Weights, ",") {
469 if _, err := strconv.ParseFloat(strings.TrimSpace(w), 64); err != nil {
470 isInvalidFlashcardWeights = true
471 break
472 }
473 }
474 if isInvalidFlashcardWeights {
475 defaultWeights := conf.DefaultFSRSWeights()
476 msg := "fsrs store weights contain invalid number"
477 logging.LogWarnf("%s, given [%s], reset to default weights [%s]", msg, Conf.Flashcard.Weights, defaultWeights)
478 Conf.Flashcard.Weights = defaultWeights
479 go func() {
480 util.WaitForUILoaded()
481 task.AppendAsyncTaskWithDelay(task.PushMsg, 2*time.Second, util.PushErrMsg, msg, 15000)
482 }()
483 }
484
485 if nil == Conf.AI {
486 Conf.AI = conf.NewAI()
487 }
488 if "" == Conf.AI.OpenAI.APIModel {
489 Conf.AI.OpenAI.APIModel = openai.GPT3Dot5Turbo
490 }
491 if "" == Conf.AI.OpenAI.APIUserAgent {
492 Conf.AI.OpenAI.APIUserAgent = util.UserAgent
493 }
494 if strings.HasPrefix(Conf.AI.OpenAI.APIUserAgent, "SiYuan/") {
495 Conf.AI.OpenAI.APIUserAgent = util.UserAgent
496 }
497 if "" == Conf.AI.OpenAI.APIProvider {
498 Conf.AI.OpenAI.APIProvider = "OpenAI"
499 }
500 if 0 > Conf.AI.OpenAI.APIMaxTokens {
501 Conf.AI.OpenAI.APIMaxTokens = 0
502 }
503 if 0 >= Conf.AI.OpenAI.APITemperature || 2 < Conf.AI.OpenAI.APITemperature {
504 Conf.AI.OpenAI.APITemperature = 1.0
505 }
506 if 1 > Conf.AI.OpenAI.APIMaxContexts || 64 < Conf.AI.OpenAI.APIMaxContexts {
507 Conf.AI.OpenAI.APIMaxContexts = 7
508 }
509
510 if "" != Conf.AI.OpenAI.APIKey {
511 logging.LogInfof("OpenAI API enabled\n"+
512 " userAgent=%s\n"+
513 " baseURL=%s\n"+
514 " timeout=%ds\n"+
515 " proxy=%s\n"+
516 " model=%s\n"+
517 " maxTokens=%d\n"+
518 " temperature=%.1f\n"+
519 " maxContexts=%d",
520 Conf.AI.OpenAI.APIUserAgent,
521 Conf.AI.OpenAI.APIBaseURL,
522 Conf.AI.OpenAI.APITimeout,
523 Conf.AI.OpenAI.APIProxy,
524 Conf.AI.OpenAI.APIModel,
525 Conf.AI.OpenAI.APIMaxTokens,
526 Conf.AI.OpenAI.APITemperature,
527 Conf.AI.OpenAI.APIMaxContexts)
528 }
529
530 Conf.ReadOnly = util.ReadOnly
531
532 if "" != util.AccessAuthCode {
533 Conf.AccessAuthCode = util.AccessAuthCode
534 }
535 Conf.AccessAuthCode = strings.TrimSpace(Conf.AccessAuthCode)
536 Conf.AccessAuthCode = util.RemoveInvalid(Conf.AccessAuthCode)
537
538 Conf.LocalIPs = util.GetLocalIPs()
539
540 if 1 == Conf.DataIndexState {
541 // 上次未正常完成数据索引
542 go func() {
543 util.WaitForUILoaded()
544 if util.ContainerIOS == util.Container || util.ContainerAndroid == util.Container || util.ContainerHarmony == util.Container {
545 task.AppendAsyncTaskWithDelay(task.PushMsg, 2*time.Second, util.PushMsg, Conf.language(245), 15000)
546 } else {
547 task.AppendAsyncTaskWithDelay(task.PushMsg, 2*time.Second, util.PushMsg, Conf.language(244), 15000)
548 }
549 }()
550 }
551
552 Conf.DataIndexState = 0
553
554 Conf.Save()
555 logging.SetLogLevel(Conf.LogLevel)
556
557 util.SetNetworkProxy(Conf.System.NetworkProxy.String())
558
559 go util.InitPandoc()
560 go util.InitTesseract()
561}
562
563func initLang() {
564 p := filepath.Join(util.WorkingDir, "appearance", "langs")
565 dir, err := os.Open(p)
566 if err != nil {
567 logging.LogErrorf("open language configuration folder [%s] failed: %s", p, err)
568 util.ReportFileSysFatalError(err)
569 return
570 }
571 defer dir.Close()
572
573 langNames, err := dir.Readdirnames(-1)
574 if err != nil {
575 logging.LogErrorf("list language configuration folder [%s] failed: %s", p, err)
576 util.ReportFileSysFatalError(err)
577 return
578 }
579
580 for _, langName := range langNames {
581 jsonPath := filepath.Join(p, langName)
582 data, err := os.ReadFile(jsonPath)
583 if err != nil {
584 logging.LogErrorf("read language configuration [%s] failed: %s", jsonPath, err)
585 continue
586 }
587 langMap := map[string]interface{}{}
588 if err := gulu.JSON.UnmarshalJSON(data, &langMap); err != nil {
589 logging.LogErrorf("parse language configuration failed [%s] failed: %s", jsonPath, err)
590 continue
591 }
592
593 kernelMap := map[int]string{}
594 label := langMap["_label"].(string)
595 kernelLangs := langMap["_kernel"].(map[string]interface{})
596 for k, v := range kernelLangs {
597 num, convErr := strconv.Atoi(k)
598 if nil != convErr {
599 logging.LogErrorf("parse language configuration [%s] item [%d] failed: %s", p, num, convErr)
600 continue
601 }
602 kernelMap[num] = v.(string)
603 }
604 kernelMap[-1] = label
605 name := langName[:strings.LastIndex(langName, ".")]
606 util.Langs[name] = kernelMap
607
608 util.TimeLangs[name] = langMap["_time"].(map[string]interface{})
609 util.TaskActionLangs[name] = langMap["_taskAction"].(map[string]interface{})
610 util.TrayMenuLangs[name] = langMap["_trayMenu"].(map[string]interface{})
611 util.AttrViewLangs[name] = langMap["_attrView"].(map[string]interface{})
612 }
613}
614
615func loadLangs() (ret []*conf.Lang) {
616 for name, langMap := range util.Langs {
617 lang := &conf.Lang{Label: langMap[-1], Name: name}
618 ret = append(ret, lang)
619 }
620 sort.Slice(ret, func(i, j int) bool {
621 return ret[i].Name < ret[j].Name
622 })
623 return
624}
625
626var exitLock = sync.Mutex{}
627
628// Close 退出内核进程.
629//
630// force:是否不执行同步过程而直接退出
631//
632// setCurrentWorkspace:是否将当前工作空间放到工作空间列表的最后一个
633//
634// execInstallPkg:是否执行新版本安装包
635//
636// 0:默认按照设置项 System.DownloadInstallPkg 检查并推送提示
637// 1:不执行新版本安装
638// 2:执行新版本安装
639//
640// 返回值 exitCode:
641//
642// 0:正常退出
643// 1:同步执行失败
644// 2:提示新安装包
645//
646// 当 force 为 true(强制退出)并且 execInstallPkg 为 0(默认检查更新)并且同步失败并且新版本安装版已经准备就绪时,执行新版本安装 https://github.com/siyuan-note/siyuan/issues/10288
647func Close(force, setCurrentWorkspace bool, execInstallPkg int) (exitCode int) {
648 exitLock.Lock()
649 defer exitLock.Unlock()
650
651 logging.LogInfof("exiting kernel [force=%v, setCurrentWorkspace=%v, execInstallPkg=%d]", force, setCurrentWorkspace, execInstallPkg)
652 util.PushMsg(Conf.Language(95), 10000*60)
653 FlushTxQueue()
654
655 if !force {
656 if Conf.Sync.Enabled && 3 != Conf.Sync.Mode &&
657 ((IsSubscriber() && conf.ProviderSiYuan == Conf.Sync.Provider) || conf.ProviderSiYuan != Conf.Sync.Provider) {
658 syncData(true, false)
659 if 0 != ExitSyncSucc {
660 exitCode = 1
661 return
662 }
663 }
664 }
665
666 // Close the user guide when exiting https://github.com/siyuan-note/siyuan/issues/10322
667 closeUserGuide()
668
669 // Improve indexing completeness when exiting https://github.com/siyuan-note/siyuan/issues/12039
670 sql.FlushQueue()
671
672 util.IsExiting.Store(true)
673 waitSecondForExecInstallPkg := false
674 if !skipNewVerInstallPkg() && "" != newVerInstallPkgPath {
675 if 2 == execInstallPkg || (force && 0 == execInstallPkg) { // 执行新版本安装
676 waitSecondForExecInstallPkg = true
677 if gulu.OS.IsWindows() {
678 util.PushMsg(Conf.Language(130), 1000*30)
679 }
680 go execNewVerInstallPkg(newVerInstallPkgPath)
681 } else if 0 == execInstallPkg { // 新版本安装包已经准备就绪
682 exitCode = 2
683 logging.LogInfof("the new version install pkg is ready [%s], waiting for the user's next instruction", newVerInstallPkgPath)
684 return
685 }
686 }
687
688 Conf.Close()
689 sql.CloseDatabase()
690 util.SaveAssetsTexts()
691 clearWorkspaceTemp()
692 clearCorruptedNotebooks()
693 clearPortJSON()
694
695 if setCurrentWorkspace {
696 // 将当前工作空间放到工作空间列表的最后一个
697 // Open the last workspace by default https://github.com/siyuan-note/siyuan/issues/10570
698 workspacePaths, err := util.ReadWorkspacePaths()
699 if err != nil {
700 logging.LogErrorf("read workspace paths failed: %s", err)
701 } else {
702 workspacePaths = gulu.Str.RemoveElem(workspacePaths, util.WorkspaceDir)
703 workspacePaths = append(workspacePaths, util.WorkspaceDir)
704 util.WriteWorkspacePaths(workspacePaths)
705 }
706 }
707
708 util.UnlockWorkspace()
709
710 time.Sleep(500 * time.Millisecond)
711 if waitSecondForExecInstallPkg {
712 // 桌面端退出拉起更新安装时有时需要重启两次 https://github.com/siyuan-note/siyuan/issues/6544
713 // 这里多等待一段时间,等待安装程序启动
714 if gulu.OS.IsWindows() {
715 time.Sleep(30 * time.Second)
716 }
717 }
718 closeSyncWebSocket()
719 go func() {
720 time.Sleep(500 * time.Millisecond)
721 logging.LogInfof("exited kernel")
722 if nil != util.WebSocketServer {
723 util.WebSocketServer.Close()
724 }
725 util.HttpServing = false
726 os.Exit(logging.ExitCodeOk)
727 }()
728 return
729}
730
731var customEmojis = sync.Map{}
732
733func AddCustomEmoji(emojiName, imgSrc string) {
734 customEmojis.Store(emojiName, imgSrc)
735}
736
737func ClearCustomEmojis() {
738 customEmojis.Clear()
739}
740
741func NewLute() (ret *lute.Lute) {
742 ret = util.NewLute()
743 ret.SetCodeSyntaxHighlightLineNum(Conf.Editor.CodeSyntaxHighlightLineNum)
744 ret.SetChineseParagraphBeginningSpace(Conf.Export.ParagraphBeginningSpace)
745 ret.SetProtyleMarkNetImg(Conf.Editor.DisplayNetImgMark)
746 ret.SetSpellcheck(Conf.Editor.Spellcheck)
747
748 customEmojiMap := map[string]string{}
749 customEmojis.Range(func(key, value interface{}) bool {
750 customEmojiMap[key.(string)] = value.(string)
751 return true
752 })
753 ret.PutEmojis(customEmojiMap)
754 return
755}
756
757func enableLuteInlineSyntax(luteEngine *lute.Lute) {
758 luteEngine.SetInlineAsterisk(true)
759 luteEngine.SetInlineUnderscore(true)
760 luteEngine.SetSup(true)
761 luteEngine.SetSub(true)
762 luteEngine.SetTag(true)
763 luteEngine.SetInlineMath(true)
764 luteEngine.SetGFMStrikethrough(true)
765}
766
767func (conf *AppConf) Save() {
768 if util.ReadOnly {
769 return
770 }
771
772 Conf.m.Lock()
773 defer Conf.m.Unlock()
774
775 newData, _ := gulu.JSON.MarshalIndentJSON(Conf, "", " ")
776 confPath := filepath.Join(util.ConfDir, "conf.json")
777 oldData, err := filelock.ReadFile(confPath)
778 if err != nil {
779 conf.save0(newData)
780 return
781 }
782
783 if bytes.Equal(newData, oldData) {
784 return
785 }
786
787 conf.save0(newData)
788}
789
790func (conf *AppConf) save0(data []byte) {
791 confPath := filepath.Join(util.ConfDir, "conf.json")
792 if err := filelock.WriteFile(confPath, data); err != nil {
793 logging.LogErrorf("write conf [%s] failed: %s", confPath, err)
794 util.ReportFileSysFatalError(err)
795 return
796 }
797}
798
799func (conf *AppConf) Close() {
800 conf.Save()
801}
802
803func (conf *AppConf) Box(boxID string) *Box {
804 for _, box := range conf.GetOpenedBoxes() {
805 if box.ID == boxID {
806 return box
807 }
808 }
809 return nil
810}
811
812func (conf *AppConf) GetBox(boxID string) *Box {
813 for _, box := range conf.GetBoxes() {
814 if box.ID == boxID {
815 return box
816 }
817 }
818 return nil
819}
820
821func (conf *AppConf) BoxNames(boxIDs []string) (ret map[string]string) {
822 ret = map[string]string{}
823
824 boxes := conf.GetOpenedBoxes()
825 for _, boxID := range boxIDs {
826 for _, box := range boxes {
827 if box.ID == boxID {
828 ret[boxID] = box.Name
829 break
830 }
831 }
832 }
833 return
834}
835
836func (conf *AppConf) GetBoxes() (ret []*Box) {
837 ret = []*Box{}
838 notebooks, err := ListNotebooks()
839 if err != nil {
840 return
841 }
842
843 for _, notebook := range notebooks {
844 id := notebook.ID
845 name := notebook.Name
846 closed := notebook.Closed
847 box := &Box{ID: id, Name: name, Closed: closed}
848 ret = append(ret, box)
849 }
850 return
851}
852
853func (conf *AppConf) GetOpenedBoxes() (ret []*Box) {
854 ret = []*Box{}
855 notebooks, err := ListNotebooks()
856 if err != nil {
857 return
858 }
859
860 for _, notebook := range notebooks {
861 if !notebook.Closed {
862 ret = append(ret, notebook)
863 }
864 }
865 return
866}
867
868func (conf *AppConf) GetClosedBoxes() (ret []*Box) {
869 ret = []*Box{}
870 notebooks, err := ListNotebooks()
871 if err != nil {
872 return
873 }
874
875 for _, notebook := range notebooks {
876 if notebook.Closed {
877 ret = append(ret, notebook)
878 }
879 }
880 return
881}
882
883func (conf *AppConf) Language(num int) (ret string) {
884 ret = conf.language(num)
885 ret = strings.ReplaceAll(ret, "${accountServer}", util.GetCloudAccountServer())
886 return
887}
888
889func (conf *AppConf) language(num int) (ret string) {
890 ret = util.Langs[conf.Lang][num]
891 if "" != ret {
892 return
893 }
894 ret = util.Langs["en_US"][num]
895 return
896}
897
898func InitBoxes() {
899 blockCount := treenode.CountBlocks()
900 initialized := 0 < blockCount
901 for _, box := range Conf.GetOpenedBoxes() {
902 box.UpdateHistoryGenerated() // 初始化历史生成时间为当前时间
903
904 if !initialized {
905 indexBox(box.ID)
906 }
907 }
908
909 logging.LogInfof("tree/block count [%d/%d]", treenode.CountTrees(), blockCount)
910}
911
912func IsSubscriber() bool {
913 u := Conf.GetUser()
914 return nil != u && (-1 == u.UserSiYuanProExpireTime || 0 < u.UserSiYuanProExpireTime) && 0 == u.UserSiYuanSubscriptionStatus
915}
916
917func IsPaidUser() bool {
918 // S3/WebDAV data sync and backup are available for a fee https://github.com/siyuan-note/siyuan/issues/8780
919
920 if IsSubscriber() {
921 return true
922 }
923
924 u := Conf.GetUser()
925 if nil == u {
926 return false
927 }
928 return 1 == u.UserSiYuanOneTimePayStatus
929}
930
931const (
932 MaskedUserData = ""
933 MaskedAccessAuthCode = "*******"
934)
935
936func GetMaskedConf() (ret *AppConf, err error) {
937 // 脱敏处理
938 data, err := gulu.JSON.MarshalIndentJSON(Conf, "", " ")
939 if err != nil {
940 logging.LogErrorf("marshal conf failed: %s", err)
941 return
942 }
943 ret = &AppConf{}
944 if err = gulu.JSON.UnmarshalJSON(data, ret); err != nil {
945 logging.LogErrorf("unmarshal conf failed: %s", err)
946 return
947 }
948
949 ret.UserData = MaskedUserData
950 if "" != ret.AccessAuthCode {
951 ret.AccessAuthCode = MaskedAccessAuthCode
952 }
953 return
954}
955
956// REF: https://github.com/siyuan-note/siyuan/issues/11364
957// HideConfSecret 隐藏设置中的秘密信息
958func HideConfSecret(c *AppConf) {
959 c.AI = &conf.AI{}
960 c.Api = &conf.API{}
961 c.Flashcard = &conf.Flashcard{}
962 c.LocalIPs = []string{}
963 c.Publish = &conf.Publish{}
964 c.Repo = &conf.Repo{}
965 c.Sync = &conf.Sync{}
966 c.System.AppDir = ""
967 c.System.ConfDir = ""
968 c.System.DataDir = ""
969 c.System.HomeDir = ""
970 c.System.Name = ""
971 c.System.NetworkProxy = &conf.NetworkProxy{}
972}
973
974func clearPortJSON() {
975 pid := fmt.Sprintf("%d", os.Getpid())
976 portJSON := filepath.Join(util.HomeDir, ".config", "siyuan", "port.json")
977 pidPorts := map[string]string{}
978 var data []byte
979 var err error
980
981 if gulu.File.IsExist(portJSON) {
982 data, err = os.ReadFile(portJSON)
983 if err != nil {
984 logging.LogWarnf("read port.json failed: %s", err)
985 } else {
986 if err = gulu.JSON.UnmarshalJSON(data, &pidPorts); err != nil {
987 logging.LogWarnf("unmarshal port.json failed: %s", err)
988 }
989 }
990 }
991
992 delete(pidPorts, pid)
993 if data, err = gulu.JSON.MarshalIndentJSON(pidPorts, "", " "); err != nil {
994 logging.LogWarnf("marshal port.json failed: %s", err)
995 } else {
996 if err = os.WriteFile(portJSON, data, 0644); err != nil {
997 logging.LogWarnf("write port.json failed: %s", err)
998 }
999 }
1000}
1001
1002func clearCorruptedNotebooks() {
1003 // 数据同步时展开文档树操作可能导致数据丢失 https://github.com/siyuan-note/siyuan/issues/7129
1004
1005 dirs, err := os.ReadDir(util.DataDir)
1006 if err != nil {
1007 logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
1008 return
1009 }
1010 for _, dir := range dirs {
1011 if util.IsReservedFilename(dir.Name()) {
1012 continue
1013 }
1014
1015 if !dir.IsDir() {
1016 continue
1017 }
1018
1019 if !ast.IsNodeIDPattern(dir.Name()) {
1020 continue
1021 }
1022
1023 boxDirPath := filepath.Join(util.DataDir, dir.Name())
1024 boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json")
1025 if !filelock.IsExist(boxConfPath) {
1026 logging.LogWarnf("found a corrupted box [%s]", boxDirPath)
1027 continue
1028 }
1029 }
1030}
1031
1032func clearWorkspaceTemp() {
1033 os.RemoveAll(filepath.Join(util.TempDir, "bazaar"))
1034 os.RemoveAll(filepath.Join(util.TempDir, "export"))
1035 os.RemoveAll(filepath.Join(util.TempDir, "convert"))
1036 os.RemoveAll(filepath.Join(util.TempDir, "import"))
1037 os.RemoveAll(filepath.Join(util.TempDir, "repo"))
1038 os.RemoveAll(filepath.Join(util.TempDir, "os"))
1039 os.RemoveAll(filepath.Join(util.TempDir, "base64"))
1040 os.RemoveAll(filepath.Join(util.TempDir, "blocktree.msgpack")) // v2.7.2 前旧版的块树数据
1041 os.RemoveAll(filepath.Join(util.TempDir, "blocktree")) // v3.1.0 前旧版的块树数据
1042
1043 // 退出时自动删除超过 7 天的安装包 https://github.com/siyuan-note/siyuan/issues/6128
1044 install := filepath.Join(util.TempDir, "install")
1045 if gulu.File.IsDir(install) {
1046 monthAgo := time.Now().Add(-time.Hour * 24 * 7)
1047 entries, err := os.ReadDir(install)
1048 if err != nil {
1049 logging.LogErrorf("read dir [%s] failed: %s", install, err)
1050 } else {
1051 for _, entry := range entries {
1052 info, _ := entry.Info()
1053 if nil != info && !info.IsDir() && info.ModTime().Before(monthAgo) {
1054 if err = os.RemoveAll(filepath.Join(install, entry.Name())); err != nil {
1055 logging.LogErrorf("remove old install pkg [%s] failed: %s", filepath.Join(install, entry.Name()), err)
1056 }
1057 }
1058 }
1059 }
1060 }
1061
1062 tmps, err := filepath.Glob(filepath.Join(util.TempDir, "*.tmp"))
1063 if err != nil {
1064 logging.LogErrorf("glob temp files failed: %s", err)
1065 }
1066 for _, tmp := range tmps {
1067 if err = os.RemoveAll(tmp); err != nil {
1068 logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
1069 } else {
1070 logging.LogInfof("removed temp file [%s]", tmp)
1071 }
1072 }
1073
1074 tmps, err = filepath.Glob(filepath.Join(util.DataDir, ".siyuan", "*.tmp"))
1075 if err != nil {
1076 logging.LogErrorf("glob temp files failed: %s", err)
1077 }
1078 for _, tmp := range tmps {
1079 if err = os.RemoveAll(tmp); err != nil {
1080 logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
1081 } else {
1082 logging.LogInfof("removed temp file [%s]", tmp)
1083 }
1084 }
1085
1086 // 老版本遗留文件清理
1087 os.RemoveAll(filepath.Join(util.DataDir, "assets", ".siyuan", "assets.json"))
1088 os.RemoveAll(filepath.Join(util.DataDir, ".siyuan", "history"))
1089 os.RemoveAll(filepath.Join(util.WorkspaceDir, "backup"))
1090 os.RemoveAll(filepath.Join(util.WorkspaceDir, "sync"))
1091 os.RemoveAll(filepath.Join(util.DataDir, "%")) // v3.0.6 生成的错误历史文件夹
1092
1093 logging.LogInfof("cleared workspace temp")
1094}
1095
1096func closeUserGuide() {
1097 defer logging.Recover()
1098
1099 dirs, err := os.ReadDir(util.DataDir)
1100 if err != nil {
1101 logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
1102 return
1103 }
1104
1105 for _, dir := range dirs {
1106 if !IsUserGuide(dir.Name()) {
1107 continue
1108 }
1109
1110 boxID := dir.Name()
1111 boxDirPath := filepath.Join(util.DataDir, boxID)
1112 boxConf := conf.NewBoxConf()
1113 boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json")
1114 if !filelock.IsExist(boxConfPath) {
1115 logging.LogWarnf("found a corrupted user guide box [%s]", boxDirPath)
1116 if removeErr := filelock.Remove(boxDirPath); nil != removeErr {
1117 logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr)
1118 } else {
1119 logging.LogInfof("removed corrupted user guide box [%s]", boxDirPath)
1120 }
1121 continue
1122 }
1123
1124 data, readErr := filelock.ReadFile(boxConfPath)
1125 if nil != readErr {
1126 logging.LogErrorf("read box conf [%s] failed: %s", boxConfPath, readErr)
1127 if removeErr := filelock.Remove(boxDirPath); nil != removeErr {
1128 logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr)
1129 } else {
1130 logging.LogInfof("removed corrupted user guide box [%s]", boxDirPath)
1131 }
1132 continue
1133 }
1134 if readErr = gulu.JSON.UnmarshalJSON(data, boxConf); nil != readErr {
1135 logging.LogErrorf("parse box conf [%s] failed: %s", boxConfPath, readErr)
1136 if removeErr := filelock.Remove(boxDirPath); nil != removeErr {
1137 logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr)
1138 } else {
1139 logging.LogInfof("removed corrupted user guide box [%s]", boxDirPath)
1140 }
1141 continue
1142 }
1143
1144 if boxConf.Closed {
1145 continue
1146 }
1147
1148 msgId := util.PushMsg(Conf.language(233), 30000)
1149 evt := util.NewCmdResult("unmount", 0, util.PushModeBroadcast)
1150 evt.Data = map[string]interface{}{
1151 "box": boxID,
1152 }
1153 util.PushEvent(evt)
1154
1155 unindex(boxID)
1156
1157 sql.FlushQueue()
1158 if removeErr := filelock.Remove(boxDirPath); nil != removeErr {
1159 logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr)
1160 }
1161
1162 util.PushClearMsg(msgId)
1163 logging.LogInfof("closed user guide box [%s]", boxID)
1164 }
1165}
1166
1167func init() {
1168 subscribeConfEvents()
1169}
1170
1171func subscribeConfEvents() {
1172 eventbus.Subscribe(util.EvtConfPandocInitialized, func() {
1173 logging.LogInfof("pandoc initialized, set pandoc bin to [%s]", util.PandocBinPath)
1174 Conf.Export.PandocBin = util.PandocBinPath
1175 Conf.Save()
1176 })
1177}