conf.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  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. package model
  17. import (
  18. "bytes"
  19. "fmt"
  20. "os"
  21. "path/filepath"
  22. "runtime"
  23. "sort"
  24. "strconv"
  25. "strings"
  26. "sync"
  27. "time"
  28. "github.com/88250/gulu"
  29. "github.com/88250/lute"
  30. "github.com/88250/lute/ast"
  31. "github.com/Xuanwo/go-locale"
  32. "github.com/dustin/go-humanize"
  33. "github.com/getsentry/sentry-go"
  34. "github.com/sashabaranov/go-openai"
  35. "github.com/siyuan-note/eventbus"
  36. "github.com/siyuan-note/filelock"
  37. "github.com/siyuan-note/logging"
  38. "github.com/siyuan-note/siyuan/kernel/conf"
  39. "github.com/siyuan-note/siyuan/kernel/sql"
  40. "github.com/siyuan-note/siyuan/kernel/task"
  41. "github.com/siyuan-note/siyuan/kernel/treenode"
  42. "github.com/siyuan-note/siyuan/kernel/util"
  43. "golang.org/x/mod/semver"
  44. "golang.org/x/text/language"
  45. )
  46. var Conf *AppConf
  47. // AppConf 维护应用元数据,保存在 ~/.siyuan/conf.json。
  48. type AppConf struct {
  49. LogLevel string `json:"logLevel"` // 日志级别:Off, Trace, Debug, Info, Warn, Error, Fatal
  50. Appearance *conf.Appearance `json:"appearance"` // 外观
  51. Langs []*conf.Lang `json:"langs"` // 界面语言列表
  52. Lang string `json:"lang"` // 选择的界面语言,同 Appearance.Lang
  53. FileTree *conf.FileTree `json:"fileTree"` // 文档面板
  54. Tag *conf.Tag `json:"tag"` // 标签面板
  55. Editor *conf.Editor `json:"editor"` // 编辑器配置
  56. Export *conf.Export `json:"export"` // 导出配置
  57. Graph *conf.Graph `json:"graph"` // 关系图配置
  58. UILayout *conf.UILayout `json:"uiLayout"` // 界面布局,v2.8.0 后这个字段不再使用
  59. UserData string `json:"userData"` // 社区用户信息,对 User 加密存储
  60. User *conf.User `json:"-"` // 社区用户内存结构,不持久化
  61. Account *conf.Account `json:"account"` // 帐号配置
  62. ReadOnly bool `json:"readonly"` // 是否是以只读模式运行
  63. LocalIPs []string `json:"localIPs"` // 本地 IP 列表
  64. AccessAuthCode string `json:"accessAuthCode"` // 访问授权码
  65. System *conf.System `json:"system"` // 系统配置
  66. Keymap *conf.Keymap `json:"keymap"` // 快捷键配置
  67. Sync *conf.Sync `json:"sync"` // 同步配置
  68. Search *conf.Search `json:"search"` // 搜索配置
  69. Flashcard *conf.Flashcard `json:"flashcard"` // 闪卡配置
  70. AI *conf.AI `json:"ai"` // 人工智能配置
  71. Bazaar *conf.Bazaar `json:"bazaar"` // 集市配置
  72. Stat *conf.Stat `json:"stat"` // 统计
  73. Api *conf.API `json:"api"` // API
  74. Repo *conf.Repo `json:"repo"` // 数据仓库
  75. OpenHelp bool `json:"openHelp"` // 启动后是否需要打开用户指南
  76. ShowChangelog bool `json:"showChangelog"` // 是否显示版本更新日志
  77. CloudRegion int `json:"cloudRegion"` // 云端区域,0:中国大陆,1:北美
  78. }
  79. func InitConf() {
  80. initLang()
  81. windowStateConf := filepath.Join(util.ConfDir, "windowState.json")
  82. if !gulu.File.IsExist(windowStateConf) {
  83. if err := gulu.File.WriteFileSafer(windowStateConf, []byte("{}"), 0644); nil != err {
  84. logging.LogErrorf("create [windowState.json] failed: %s", err)
  85. }
  86. }
  87. Conf = &AppConf{LogLevel: "debug"}
  88. confPath := filepath.Join(util.ConfDir, "conf.json")
  89. if gulu.File.IsExist(confPath) {
  90. data, err := os.ReadFile(confPath)
  91. if nil != err {
  92. logging.LogErrorf("load conf [%s] failed: %s", confPath, err)
  93. }
  94. err = gulu.JSON.UnmarshalJSON(data, Conf)
  95. if err != nil {
  96. logging.LogErrorf("parse conf [%s] failed: %s", confPath, err)
  97. }
  98. }
  99. if "" != util.Lang {
  100. Conf.Lang = util.Lang
  101. logging.LogInfof("initialized the specified language [%s]", util.Lang)
  102. } else {
  103. if "" == Conf.Lang {
  104. // 未指定外观语言时使用系统语言
  105. if userLang, err := locale.Detect(); nil == err {
  106. var supportLangs []language.Tag
  107. for lang := range util.Langs {
  108. if tag, err := language.Parse(lang); nil == err {
  109. supportLangs = append(supportLangs, tag)
  110. } else {
  111. logging.LogErrorf("load language [%s] failed: %s", lang, err)
  112. }
  113. }
  114. matcher := language.NewMatcher(supportLangs)
  115. lang, _, _ := matcher.Match(userLang)
  116. base, _ := lang.Base()
  117. region, _ := lang.Region()
  118. util.Lang = base.String() + "_" + region.String()
  119. Conf.Lang = util.Lang
  120. logging.LogInfof("initialized language [%s] based on device locale", Conf.Lang)
  121. } else {
  122. logging.LogDebugf("check device locale failed [%s], using default language [en_US]", err)
  123. util.Lang = "en_US"
  124. Conf.Lang = util.Lang
  125. }
  126. }
  127. util.Lang = Conf.Lang
  128. }
  129. Conf.Langs = loadLangs()
  130. if nil == Conf.Appearance {
  131. Conf.Appearance = conf.NewAppearance()
  132. }
  133. var langOK bool
  134. for _, l := range Conf.Langs {
  135. if Conf.Lang == l.Name {
  136. langOK = true
  137. break
  138. }
  139. }
  140. if !langOK {
  141. Conf.Lang = "en_US"
  142. util.Lang = Conf.Lang
  143. }
  144. Conf.Appearance.Lang = Conf.Lang
  145. if nil == Conf.UILayout {
  146. Conf.UILayout = &conf.UILayout{}
  147. }
  148. if nil == Conf.Keymap {
  149. Conf.Keymap = &conf.Keymap{}
  150. }
  151. if "" == Conf.Appearance.CodeBlockThemeDark {
  152. Conf.Appearance.CodeBlockThemeDark = "dracula"
  153. }
  154. if "" == Conf.Appearance.CodeBlockThemeLight {
  155. Conf.Appearance.CodeBlockThemeLight = "github"
  156. }
  157. if nil == Conf.FileTree {
  158. Conf.FileTree = conf.NewFileTree()
  159. }
  160. if 1 > Conf.FileTree.MaxListCount {
  161. Conf.FileTree.MaxListCount = 512
  162. }
  163. if 1 > Conf.FileTree.MaxOpenTabCount {
  164. Conf.FileTree.MaxOpenTabCount = 8
  165. }
  166. if 32 < Conf.FileTree.MaxOpenTabCount {
  167. Conf.FileTree.MaxOpenTabCount = 32
  168. }
  169. Conf.FileTree.DocCreateSavePath = strings.TrimSpace(Conf.FileTree.DocCreateSavePath)
  170. if "../" == Conf.FileTree.DocCreateSavePath {
  171. Conf.FileTree.DocCreateSavePath = "../Untitled"
  172. }
  173. for strings.HasSuffix(Conf.FileTree.DocCreateSavePath, "/") {
  174. Conf.FileTree.DocCreateSavePath = strings.TrimSuffix(Conf.FileTree.DocCreateSavePath, "/")
  175. Conf.FileTree.DocCreateSavePath = strings.TrimSpace(Conf.FileTree.DocCreateSavePath)
  176. }
  177. util.UseSingleLineSave = Conf.FileTree.UseSingleLineSave
  178. util.CurrentCloudRegion = Conf.CloudRegion
  179. if nil == Conf.Tag {
  180. Conf.Tag = conf.NewTag()
  181. }
  182. if nil == Conf.Editor {
  183. Conf.Editor = conf.NewEditor()
  184. }
  185. if 1 > len(Conf.Editor.Emoji) {
  186. Conf.Editor.Emoji = []string{}
  187. }
  188. if 1 > Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
  189. Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 64
  190. }
  191. if 5120 < Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
  192. Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 5120
  193. }
  194. if nil == Conf.Export {
  195. Conf.Export = conf.NewExport()
  196. }
  197. if 0 == Conf.Export.BlockRefMode || 1 == Conf.Export.BlockRefMode {
  198. // 废弃导出选项引用块转换为原始块和引述块 https://github.com/siyuan-note/siyuan/issues/3155
  199. Conf.Export.BlockRefMode = 4 // 改为脚注
  200. }
  201. if "" == Conf.Export.PandocBin {
  202. Conf.Export.PandocBin = util.PandocBinPath
  203. }
  204. if 9 > Conf.Editor.FontSize || 72 < Conf.Editor.FontSize {
  205. Conf.Editor.FontSize = 16
  206. }
  207. if "" == Conf.Editor.PlantUMLServePath {
  208. Conf.Editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1"
  209. }
  210. if nil == Conf.Graph || nil == Conf.Graph.Local || nil == Conf.Graph.Global {
  211. Conf.Graph = conf.NewGraph()
  212. }
  213. if nil == Conf.System {
  214. Conf.System = conf.NewSystem()
  215. Conf.OpenHelp = true
  216. } else {
  217. if 0 < semver.Compare("v"+util.Ver, "v"+Conf.System.KernelVersion) {
  218. logging.LogInfof("upgraded from version [%s] to [%s]", Conf.System.KernelVersion, util.Ver)
  219. Conf.ShowChangelog = true
  220. } else if 0 > semver.Compare("v"+util.Ver, "v"+Conf.System.KernelVersion) {
  221. logging.LogInfof("downgraded from version [%s] to [%s]", Conf.System.KernelVersion, util.Ver)
  222. }
  223. Conf.System.KernelVersion = util.Ver
  224. Conf.System.IsInsider = util.IsInsider
  225. task.AppendTask(task.UpgradeUserGuide, upgradeUserGuide)
  226. }
  227. if nil == Conf.System.NetworkProxy {
  228. Conf.System.NetworkProxy = &conf.NetworkProxy{}
  229. }
  230. if "" == Conf.System.ID {
  231. Conf.System.ID = util.GetDeviceID()
  232. }
  233. if "" == Conf.System.Name {
  234. Conf.System.Name = util.GetDeviceName()
  235. }
  236. if util.ContainerStd == util.Container {
  237. Conf.System.ID = util.GetDeviceID()
  238. Conf.System.Name = util.GetDeviceName()
  239. }
  240. Conf.System.AppDir = util.WorkingDir
  241. Conf.System.ConfDir = util.ConfDir
  242. Conf.System.HomeDir = util.HomeDir
  243. Conf.System.WorkspaceDir = util.WorkspaceDir
  244. Conf.System.DataDir = util.DataDir
  245. Conf.System.Container = util.Container
  246. Conf.System.IsMicrosoftStore = util.ISMicrosoftStore
  247. if util.ISMicrosoftStore {
  248. logging.LogInfof("using Microsoft Store edition")
  249. }
  250. Conf.System.OS = runtime.GOOS
  251. Conf.System.OSPlatform = util.GetOSPlatform()
  252. if "" != Conf.UserData {
  253. Conf.User = loadUserFromConf()
  254. }
  255. if nil == Conf.Account {
  256. Conf.Account = conf.NewAccount()
  257. }
  258. if nil == Conf.Sync {
  259. Conf.Sync = conf.NewSync()
  260. }
  261. if 0 == Conf.Sync.Mode {
  262. Conf.Sync.Mode = 1
  263. }
  264. if nil == Conf.Sync.S3 {
  265. Conf.Sync.S3 = &conf.S3{}
  266. }
  267. Conf.Sync.S3.Endpoint = util.NormalizeEndpoint(Conf.Sync.S3.Endpoint)
  268. Conf.Sync.S3.Timeout = util.NormalizeTimeout(Conf.Sync.S3.Timeout)
  269. if nil == Conf.Sync.WebDAV {
  270. Conf.Sync.WebDAV = &conf.WebDAV{}
  271. }
  272. Conf.Sync.WebDAV.Endpoint = util.NormalizeEndpoint(Conf.Sync.WebDAV.Endpoint)
  273. Conf.Sync.WebDAV.Timeout = util.NormalizeTimeout(Conf.Sync.WebDAV.Timeout)
  274. if util.ContainerDocker == util.Container {
  275. Conf.Sync.Perception = false
  276. }
  277. if nil == Conf.Api {
  278. Conf.Api = conf.NewAPI()
  279. }
  280. if nil == Conf.Bazaar {
  281. Conf.Bazaar = conf.NewBazaar()
  282. }
  283. if nil == Conf.Repo {
  284. Conf.Repo = conf.NewRepo()
  285. }
  286. if 1440 < Conf.Editor.GenerateHistoryInterval {
  287. Conf.Editor.GenerateHistoryInterval = 1440
  288. }
  289. if 1 > Conf.Editor.HistoryRetentionDays {
  290. Conf.Editor.HistoryRetentionDays = 7
  291. }
  292. if 48 > Conf.Editor.DynamicLoadBlocks {
  293. Conf.Editor.DynamicLoadBlocks = 48
  294. }
  295. if 1024 < Conf.Editor.DynamicLoadBlocks {
  296. Conf.Editor.DynamicLoadBlocks = 1024
  297. }
  298. if 0 > Conf.Editor.BacklinkExpandCount {
  299. Conf.Editor.BacklinkExpandCount = 0
  300. }
  301. if 0 > Conf.Editor.BackmentionExpandCount {
  302. Conf.Editor.BackmentionExpandCount = 0
  303. }
  304. if nil == Conf.Search {
  305. Conf.Search = conf.NewSearch()
  306. }
  307. if 1 > Conf.Search.Limit {
  308. Conf.Search.Limit = 64
  309. }
  310. if 32 > Conf.Search.Limit {
  311. Conf.Search.Limit = 32
  312. }
  313. if 1 > Conf.Search.BacklinkMentionKeywordsLimit {
  314. Conf.Search.BacklinkMentionKeywordsLimit = 512
  315. }
  316. if nil == Conf.Stat {
  317. Conf.Stat = conf.NewStat()
  318. }
  319. if nil == Conf.Flashcard {
  320. Conf.Flashcard = conf.NewFlashcard()
  321. }
  322. if 0 > Conf.Flashcard.NewCardLimit {
  323. Conf.Flashcard.NewCardLimit = 20
  324. }
  325. if 0 > Conf.Flashcard.ReviewCardLimit {
  326. Conf.Flashcard.ReviewCardLimit = 200
  327. }
  328. if nil == Conf.AI {
  329. Conf.AI = conf.NewAI()
  330. }
  331. if "" == Conf.AI.OpenAI.APIModel {
  332. Conf.AI.OpenAI.APIModel = openai.GPT3Dot5Turbo
  333. }
  334. if "" != Conf.AI.OpenAI.APIKey {
  335. logging.LogInfof("OpenAI API enabled\n"+
  336. " baseURL=%s\n"+
  337. " timeout=%ds\n"+
  338. " proxy=%s\n"+
  339. " model=%s\n"+
  340. " maxTokens=%d",
  341. Conf.AI.OpenAI.APIBaseURL, Conf.AI.OpenAI.APITimeout, Conf.AI.OpenAI.APIProxy, Conf.AI.OpenAI.APIModel, Conf.AI.OpenAI.APIMaxTokens)
  342. }
  343. Conf.ReadOnly = util.ReadOnly
  344. if "" != util.AccessAuthCode {
  345. Conf.AccessAuthCode = util.AccessAuthCode
  346. }
  347. Conf.LocalIPs = util.GetLocalIPs()
  348. Conf.Save()
  349. logging.SetLogLevel(Conf.LogLevel)
  350. if Conf.System.UploadErrLog {
  351. logging.LogInfof("user has enabled [Automatically upload error messages and diagnostic data]")
  352. sentry.Init(sentry.ClientOptions{
  353. Dsn: "https://bdff135f14654ae58a054adeceb2c308@o1173696.ingest.sentry.io/6269178",
  354. Release: util.Ver,
  355. Environment: util.Mode,
  356. })
  357. }
  358. if Conf.System.DisableGoogleAnalytics {
  359. logging.LogInfof("user has disabled [Google Analytics]")
  360. }
  361. util.SetNetworkProxy(Conf.System.NetworkProxy.String())
  362. go util.InitPandoc()
  363. go util.InitTesseract()
  364. }
  365. func initLang() {
  366. p := filepath.Join(util.WorkingDir, "appearance", "langs")
  367. dir, err := os.Open(p)
  368. if nil != err {
  369. logging.LogErrorf("open language configuration folder [%s] failed: %s", p, err)
  370. util.ReportFileSysFatalError(err)
  371. return
  372. }
  373. defer dir.Close()
  374. langNames, err := dir.Readdirnames(-1)
  375. if nil != err {
  376. logging.LogErrorf("list language configuration folder [%s] failed: %s", p, err)
  377. util.ReportFileSysFatalError(err)
  378. return
  379. }
  380. for _, langName := range langNames {
  381. jsonPath := filepath.Join(p, langName)
  382. data, err := os.ReadFile(jsonPath)
  383. if nil != err {
  384. logging.LogErrorf("read language configuration [%s] failed: %s", jsonPath, err)
  385. continue
  386. }
  387. langMap := map[string]interface{}{}
  388. if err := gulu.JSON.UnmarshalJSON(data, &langMap); nil != err {
  389. logging.LogErrorf("parse language configuration failed [%s] failed: %s", jsonPath, err)
  390. continue
  391. }
  392. kernelMap := map[int]string{}
  393. label := langMap["_label"].(string)
  394. kernelLangs := langMap["_kernel"].(map[string]interface{})
  395. for k, v := range kernelLangs {
  396. num, convErr := strconv.Atoi(k)
  397. if nil != convErr {
  398. logging.LogErrorf("parse language configuration [%s] item [%d] failed: %s", p, num, convErr)
  399. continue
  400. }
  401. kernelMap[num] = v.(string)
  402. }
  403. kernelMap[-1] = label
  404. name := langName[:strings.LastIndex(langName, ".")]
  405. util.Langs[name] = kernelMap
  406. util.TimeLangs[name] = langMap["_time"].(map[string]interface{})
  407. util.TaskActionLangs[name] = langMap["_taskAction"].(map[string]interface{})
  408. }
  409. }
  410. func loadLangs() (ret []*conf.Lang) {
  411. for name, langMap := range util.Langs {
  412. lang := &conf.Lang{Label: langMap[-1], Name: name}
  413. ret = append(ret, lang)
  414. }
  415. sort.Slice(ret, func(i, j int) bool {
  416. return ret[i].Name < ret[j].Name
  417. })
  418. return
  419. }
  420. var exitLock = sync.Mutex{}
  421. // Close 退出内核进程.
  422. //
  423. // force:是否不执行同步过程而直接退出
  424. // execInstallPkg:是否执行新版本安装包
  425. // 0:默认按照设置项 System.DownloadInstallPkg 检查并推送提示
  426. // 1:不执行新版本安装
  427. // 2:执行新版本安装
  428. func Close(force bool, execInstallPkg int) (exitCode int) {
  429. exitLock.Lock()
  430. defer exitLock.Unlock()
  431. util.IsExiting = true
  432. logging.LogInfof("exiting kernel [force=%v, execInstallPkg=%d]", force, execInstallPkg)
  433. util.PushMsg(Conf.Language(95), 10000*60)
  434. WaitForWritingFiles()
  435. if !force {
  436. if Conf.Sync.Enabled && 3 != Conf.Sync.Mode &&
  437. ((IsSubscriber() && conf.ProviderSiYuan == Conf.Sync.Provider) || conf.ProviderSiYuan != Conf.Sync.Provider) {
  438. syncData(true, false, false)
  439. if 0 != ExitSyncSucc {
  440. exitCode = 1
  441. return
  442. }
  443. }
  444. }
  445. waitSecondForExecInstallPkg := false
  446. if !skipNewVerInstallPkg() {
  447. newVerInstallPkgPath := getNewVerInstallPkgPath()
  448. if "" != newVerInstallPkgPath {
  449. if 0 == execInstallPkg { // 新版本安装包已经准备就绪
  450. exitCode = 2
  451. logging.LogInfof("the new version install pkg is ready [%s], waiting for the user's next instruction", newVerInstallPkgPath)
  452. return
  453. } else if 2 == execInstallPkg { // 执行新版本安装
  454. waitSecondForExecInstallPkg = true
  455. go execNewVerInstallPkg(newVerInstallPkgPath)
  456. }
  457. }
  458. }
  459. Conf.Close()
  460. sql.CloseDatabase()
  461. treenode.SaveBlockTree(false)
  462. SaveAssetsTexts()
  463. clearWorkspaceTemp()
  464. clearCorruptedNotebooks()
  465. clearPortJSON()
  466. util.UnlockWorkspace()
  467. time.Sleep(500 * time.Millisecond)
  468. if waitSecondForExecInstallPkg {
  469. util.PushMsg(Conf.Language(130), 1000*5)
  470. // 桌面端退出拉起更新安装时有时需要重启两次 https://github.com/siyuan-note/siyuan/issues/6544
  471. // 这里多等待一段时间,等待安装程序启动
  472. time.Sleep(4 * time.Second)
  473. }
  474. logging.LogInfof("exited kernel")
  475. closeSyncWebSocket()
  476. util.WebSocketServer.Close()
  477. go func() {
  478. time.Sleep(500 * time.Millisecond)
  479. os.Exit(logging.ExitCodeOk)
  480. }()
  481. return
  482. }
  483. var CustomEmojis = sync.Map{}
  484. func NewLute() (ret *lute.Lute) {
  485. ret = util.NewLute()
  486. ret.SetCodeSyntaxHighlightLineNum(Conf.Editor.CodeSyntaxHighlightLineNum)
  487. ret.SetChineseParagraphBeginningSpace(Conf.Export.ParagraphBeginningSpace)
  488. ret.SetProtyleMarkNetImg(Conf.Editor.DisplayNetImgMark)
  489. ret.SetSpellcheck(Conf.Editor.Spellcheck)
  490. customEmojiMap := map[string]string{}
  491. CustomEmojis.Range(func(key, value interface{}) bool {
  492. customEmojiMap[key.(string)] = value.(string)
  493. return true
  494. })
  495. ret.PutEmojis(customEmojiMap)
  496. return
  497. }
  498. var confSaveLock = sync.Mutex{}
  499. func (conf *AppConf) Save() {
  500. if util.ReadOnly {
  501. return
  502. }
  503. confSaveLock.Lock()
  504. defer confSaveLock.Unlock()
  505. newData, _ := gulu.JSON.MarshalIndentJSON(Conf, "", " ")
  506. confPath := filepath.Join(util.ConfDir, "conf.json")
  507. oldData, err := filelock.ReadFile(confPath)
  508. if nil != err {
  509. conf.save0(newData)
  510. return
  511. }
  512. if bytes.Equal(newData, oldData) {
  513. return
  514. }
  515. conf.save0(newData)
  516. }
  517. func (conf *AppConf) save0(data []byte) {
  518. confPath := filepath.Join(util.ConfDir, "conf.json")
  519. if err := filelock.WriteFile(confPath, data); nil != err {
  520. logging.LogErrorf("write conf [%s] failed: %s", confPath, err)
  521. util.ReportFileSysFatalError(err)
  522. return
  523. }
  524. }
  525. func (conf *AppConf) Close() {
  526. conf.Save()
  527. }
  528. func (conf *AppConf) Box(boxID string) *Box {
  529. for _, box := range conf.GetOpenedBoxes() {
  530. if box.ID == boxID {
  531. return box
  532. }
  533. }
  534. return nil
  535. }
  536. func (conf *AppConf) BoxNames(boxIDs []string) (ret map[string]string) {
  537. ret = map[string]string{}
  538. boxes := conf.GetOpenedBoxes()
  539. for _, boxID := range boxIDs {
  540. for _, box := range boxes {
  541. if box.ID == boxID {
  542. ret[boxID] = box.Name
  543. break
  544. }
  545. }
  546. }
  547. return
  548. }
  549. func (conf *AppConf) GetBoxes() (ret []*Box) {
  550. ret = []*Box{}
  551. notebooks, err := ListNotebooks()
  552. if nil != err {
  553. return
  554. }
  555. for _, notebook := range notebooks {
  556. id := notebook.ID
  557. name := notebook.Name
  558. closed := notebook.Closed
  559. box := &Box{ID: id, Name: name, Closed: closed}
  560. ret = append(ret, box)
  561. }
  562. return
  563. }
  564. func (conf *AppConf) GetOpenedBoxes() (ret []*Box) {
  565. ret = []*Box{}
  566. notebooks, err := ListNotebooks()
  567. if nil != err {
  568. return
  569. }
  570. for _, notebook := range notebooks {
  571. if !notebook.Closed {
  572. ret = append(ret, notebook)
  573. }
  574. }
  575. return
  576. }
  577. func (conf *AppConf) GetClosedBoxes() (ret []*Box) {
  578. ret = []*Box{}
  579. notebooks, err := ListNotebooks()
  580. if nil != err {
  581. return
  582. }
  583. for _, notebook := range notebooks {
  584. if notebook.Closed {
  585. ret = append(ret, notebook)
  586. }
  587. }
  588. return
  589. }
  590. func (conf *AppConf) Language(num int) (ret string) {
  591. ret = conf.language(num)
  592. subscribeURL := util.GetCloudAccountServer() + "/subscribe/siyuan"
  593. ret = strings.ReplaceAll(ret, "${url}", subscribeURL)
  594. return
  595. }
  596. func (conf *AppConf) language(num int) (ret string) {
  597. ret = util.Langs[conf.Lang][num]
  598. if "" != ret {
  599. return
  600. }
  601. ret = util.Langs["en_US"][num]
  602. return
  603. }
  604. func InitBoxes() {
  605. initialized := false
  606. if 1 > treenode.CountBlocks() {
  607. if gulu.File.IsExist(util.BlockTreePath) {
  608. util.IncBootProgress(20, Conf.Language(91))
  609. go func() {
  610. for i := 0; i < 40; i++ {
  611. util.RandomSleep(50, 100)
  612. util.IncBootProgress(1, Conf.Language(91))
  613. }
  614. }()
  615. treenode.InitBlockTree(false)
  616. initialized = true
  617. }
  618. } else { // 大于 1 的话说明在同步阶段已经加载过了
  619. initialized = true
  620. }
  621. for _, box := range Conf.GetOpenedBoxes() {
  622. box.UpdateHistoryGenerated() // 初始化历史生成时间为当前时间
  623. if !initialized {
  624. index(box.ID)
  625. }
  626. }
  627. if !initialized {
  628. treenode.SaveBlockTree(true)
  629. }
  630. var dbSize string
  631. if dbFile, err := os.Stat(util.DBPath); nil == err {
  632. dbSize = humanize.Bytes(uint64(dbFile.Size()))
  633. }
  634. logging.LogInfof("database size [%s], tree/block count [%d/%d]", dbSize, treenode.CountTrees(), treenode.CountBlocks())
  635. }
  636. func IsSubscriber() bool {
  637. return nil != Conf.User && (-1 == Conf.User.UserSiYuanProExpireTime || 0 < Conf.User.UserSiYuanProExpireTime) && 0 == Conf.User.UserSiYuanSubscriptionStatus
  638. }
  639. func IsOneTimePaid() bool {
  640. if IsSubscriber() {
  641. return true
  642. }
  643. return nil != Conf.User // Sign in to use S3/WebDAV data sync https://github.com/siyuan-note/siyuan/issues/8779
  644. // TODO https://github.com/siyuan-note/siyuan/issues/8780
  645. // return nil != Conf.User && 1 == Conf.User.UserSiYuanOneTimePayStatus
  646. }
  647. const (
  648. MaskedUserData = ""
  649. MaskedAccessAuthCode = "*******"
  650. )
  651. func GetMaskedConf() (ret *AppConf, err error) {
  652. // 脱敏处理
  653. data, err := gulu.JSON.MarshalIndentJSON(Conf, "", " ")
  654. if nil != err {
  655. logging.LogErrorf("marshal conf failed: %s", err)
  656. return
  657. }
  658. ret = &AppConf{}
  659. if err = gulu.JSON.UnmarshalJSON(data, ret); nil != err {
  660. logging.LogErrorf("unmarshal conf failed: %s", err)
  661. return
  662. }
  663. ret.UserData = MaskedUserData
  664. if "" != ret.AccessAuthCode {
  665. ret.AccessAuthCode = MaskedAccessAuthCode
  666. }
  667. return
  668. }
  669. func clearPortJSON() {
  670. pid := fmt.Sprintf("%d", os.Getpid())
  671. portJSON := filepath.Join(util.HomeDir, ".config", "siyuan", "port.json")
  672. pidPorts := map[string]string{}
  673. var data []byte
  674. var err error
  675. if gulu.File.IsExist(portJSON) {
  676. data, err = os.ReadFile(portJSON)
  677. if nil != err {
  678. logging.LogWarnf("read port.json failed: %s", err)
  679. } else {
  680. if err = gulu.JSON.UnmarshalJSON(data, &pidPorts); nil != err {
  681. logging.LogWarnf("unmarshal port.json failed: %s", err)
  682. }
  683. }
  684. }
  685. delete(pidPorts, pid)
  686. if data, err = gulu.JSON.MarshalIndentJSON(pidPorts, "", " "); nil != err {
  687. logging.LogWarnf("marshal port.json failed: %s", err)
  688. } else {
  689. if err = os.WriteFile(portJSON, data, 0644); nil != err {
  690. logging.LogWarnf("write port.json failed: %s", err)
  691. }
  692. }
  693. }
  694. func clearCorruptedNotebooks() {
  695. // 数据同步时展开文档树操作可能导致数据丢失 https://github.com/siyuan-note/siyuan/issues/7129
  696. dirs, err := os.ReadDir(util.DataDir)
  697. if nil != err {
  698. logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
  699. return
  700. }
  701. for _, dir := range dirs {
  702. if util.IsReservedFilename(dir.Name()) {
  703. continue
  704. }
  705. if !dir.IsDir() {
  706. continue
  707. }
  708. if !ast.IsNodeIDPattern(dir.Name()) {
  709. continue
  710. }
  711. boxDirPath := filepath.Join(util.DataDir, dir.Name())
  712. boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json")
  713. if !gulu.File.IsExist(boxConfPath) {
  714. logging.LogWarnf("found a corrupted box [%s]", boxDirPath)
  715. continue
  716. }
  717. }
  718. }
  719. func clearWorkspaceTemp() {
  720. os.RemoveAll(filepath.Join(util.TempDir, "bazaar"))
  721. os.RemoveAll(filepath.Join(util.TempDir, "export"))
  722. os.RemoveAll(filepath.Join(util.TempDir, "convert"))
  723. os.RemoveAll(filepath.Join(util.TempDir, "import"))
  724. os.RemoveAll(filepath.Join(util.TempDir, "repo"))
  725. os.RemoveAll(filepath.Join(util.TempDir, "os"))
  726. os.RemoveAll(filepath.Join(util.TempDir, "blocktree.msgpack")) // v2.7.2 前旧版的块数数据
  727. // 退出时自动删除超过 7 天的安装包 https://github.com/siyuan-note/siyuan/issues/6128
  728. install := filepath.Join(util.TempDir, "install")
  729. if gulu.File.IsDir(install) {
  730. monthAgo := time.Now().Add(-time.Hour * 24 * 7)
  731. entries, err := os.ReadDir(install)
  732. if nil != err {
  733. logging.LogErrorf("read dir [%s] failed: %s", install, err)
  734. } else {
  735. for _, entry := range entries {
  736. info, _ := entry.Info()
  737. if nil != info && !info.IsDir() && info.ModTime().Before(monthAgo) {
  738. if err = os.RemoveAll(filepath.Join(install, entry.Name())); nil != err {
  739. logging.LogErrorf("remove old install pkg [%s] failed: %s", filepath.Join(install, entry.Name()), err)
  740. }
  741. }
  742. }
  743. }
  744. }
  745. tmps, err := filepath.Glob(filepath.Join(util.TempDir, "*.tmp"))
  746. if nil != err {
  747. logging.LogErrorf("glob temp files failed: %s", err)
  748. }
  749. for _, tmp := range tmps {
  750. if err = os.RemoveAll(tmp); nil != err {
  751. logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
  752. } else {
  753. logging.LogInfof("removed temp file [%s]", tmp)
  754. }
  755. }
  756. tmps, err = filepath.Glob(filepath.Join(util.DataDir, ".siyuan", "*.tmp"))
  757. if nil != err {
  758. logging.LogErrorf("glob temp files failed: %s", err)
  759. }
  760. for _, tmp := range tmps {
  761. if err = os.RemoveAll(tmp); nil != err {
  762. logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
  763. } else {
  764. logging.LogInfof("removed temp file [%s]", tmp)
  765. }
  766. }
  767. // 老版本遗留文件清理
  768. os.RemoveAll(filepath.Join(util.DataDir, "assets", ".siyuan", "assets.json"))
  769. os.RemoveAll(filepath.Join(util.DataDir, ".siyuan", "history"))
  770. os.RemoveAll(filepath.Join(util.WorkspaceDir, "backup"))
  771. os.RemoveAll(filepath.Join(util.WorkspaceDir, "sync"))
  772. logging.LogInfof("cleared workspace temp")
  773. }
  774. func upgradeUserGuide() {
  775. defer logging.Recover()
  776. dirs, err := os.ReadDir(util.DataDir)
  777. if nil != err {
  778. logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
  779. return
  780. }
  781. for _, dir := range dirs {
  782. if !IsUserGuide(dir.Name()) {
  783. continue
  784. }
  785. boxID := dir.Name()
  786. boxDirPath := filepath.Join(util.DataDir, boxID)
  787. boxConf := conf.NewBoxConf()
  788. boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json")
  789. if !gulu.File.IsExist(boxConfPath) {
  790. logging.LogWarnf("found a corrupted box [%s]", boxDirPath)
  791. continue
  792. }
  793. data, readErr := filelock.ReadFile(boxConfPath)
  794. if nil != readErr {
  795. logging.LogErrorf("read box conf [%s] failed: %s", boxConfPath, readErr)
  796. continue
  797. }
  798. if readErr = gulu.JSON.UnmarshalJSON(data, boxConf); nil != readErr {
  799. logging.LogErrorf("parse box conf [%s] failed: %s", boxConfPath, readErr)
  800. continue
  801. }
  802. if boxConf.Closed {
  803. continue
  804. }
  805. unindex(boxID)
  806. if err = filelock.Remove(boxDirPath); nil != err {
  807. return
  808. }
  809. p := filepath.Join(util.WorkingDir, "guide", boxID)
  810. if err = filelock.Copy(p, boxDirPath); nil != err {
  811. return
  812. }
  813. index(boxID)
  814. }
  815. }
  816. func init() {
  817. subscribeConfEvents()
  818. }
  819. func subscribeConfEvents() {
  820. eventbus.Subscribe(util.EvtConfPandocInitialized, func() {
  821. logging.LogInfof("pandoc initialized, set pandoc bin to [%s]", util.PandocBinPath)
  822. Conf.Export.PandocBin = util.PandocBinPath
  823. Conf.Save()
  824. })
  825. }