conf.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. // SiYuan - Build Your Eternal Digital Garden
  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. "errors"
  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/Xuanwo/go-locale"
  31. humanize "github.com/dustin/go-humanize"
  32. "github.com/getsentry/sentry-go"
  33. "github.com/siyuan-note/filelock"
  34. "github.com/siyuan-note/logging"
  35. "github.com/siyuan-note/siyuan/kernel/conf"
  36. "github.com/siyuan-note/siyuan/kernel/sql"
  37. "github.com/siyuan-note/siyuan/kernel/treenode"
  38. "github.com/siyuan-note/siyuan/kernel/util"
  39. "golang.org/x/text/language"
  40. )
  41. var Conf *AppConf
  42. // AppConf 维护应用元数据,保存在 ~/.siyuan/conf.json。
  43. type AppConf struct {
  44. LogLevel string `json:"logLevel"` // 日志级别:Off, Trace, Debug, Info, Warn, Error, Fatal
  45. Appearance *conf.Appearance `json:"appearance"` // 外观
  46. Langs []*conf.Lang `json:"langs"` // 界面语言列表
  47. Lang string `json:"lang"` // 选择的界面语言,同 Appearance.Lang
  48. FileTree *conf.FileTree `json:"fileTree"` // 文档面板
  49. Tag *conf.Tag `json:"tag"` // 标签面板
  50. Editor *conf.Editor `json:"editor"` // 编辑器配置
  51. Export *conf.Export `json:"export"` // 导出配置
  52. Graph *conf.Graph `json:"graph"` // 关系图配置
  53. UILayout *conf.UILayout `json:"uiLayout"` // 界面布局
  54. UserData string `json:"userData"` // 社区用户信息,对 User 加密存储
  55. User *conf.User `json:"-"` // 社区用户内存结构,不持久化
  56. Account *conf.Account `json:"account"` // 帐号配置
  57. ReadOnly bool `json:"readonly"` // 是否是只读
  58. LocalIPs []string `json:"localIPs"` // 本地 IP 列表
  59. AccessAuthCode string `json:"accessAuthCode"` // 访问授权码
  60. System *conf.System `json:"system"` // 系统
  61. Keymap *conf.Keymap `json:"keymap"` // 快捷键
  62. Sync *conf.Sync `json:"sync"` // 同步配置
  63. Search *conf.Search `json:"search"` // 搜索配置
  64. Stat *conf.Stat `json:"stat"` // 统计
  65. Api *conf.API `json:"api"` // API
  66. Repo *conf.Repo `json:"repo"` // 数据仓库
  67. Newbie bool `json:"newbie"` // 是否是安装后第一次启动
  68. }
  69. func InitConf() {
  70. initLang()
  71. windowStateConf := filepath.Join(util.ConfDir, "windowState.json")
  72. if !gulu.File.IsExist(windowStateConf) {
  73. if err := gulu.File.WriteFileSafer(windowStateConf, []byte("{}"), 0644); nil != err {
  74. logging.LogErrorf("create [windowState.json] failed: %s", err)
  75. }
  76. }
  77. Conf = &AppConf{LogLevel: "debug"}
  78. confPath := filepath.Join(util.ConfDir, "conf.json")
  79. if gulu.File.IsExist(confPath) {
  80. data, err := os.ReadFile(confPath)
  81. if nil != err {
  82. logging.LogErrorf("load conf [%s] failed: %s", confPath, err)
  83. }
  84. err = gulu.JSON.UnmarshalJSON(data, Conf)
  85. if err != nil {
  86. logging.LogErrorf("parse conf [%s] failed: %s", confPath, err)
  87. }
  88. }
  89. if "" != util.Lang {
  90. Conf.Lang = util.Lang
  91. logging.LogInfof("initialized the specified language [%s]", util.Lang)
  92. } else {
  93. if "" == Conf.Lang {
  94. // 未指定外观语言时使用系统语言
  95. if userLang, err := locale.Detect(); nil == err {
  96. var supportLangs []language.Tag
  97. for lang := range langs {
  98. if tag, err := language.Parse(lang); nil == err {
  99. supportLangs = append(supportLangs, tag)
  100. } else {
  101. logging.LogErrorf("load language [%s] failed: %s", lang, err)
  102. }
  103. }
  104. matcher := language.NewMatcher(supportLangs)
  105. lang, _, _ := matcher.Match(userLang)
  106. base, _ := lang.Base()
  107. region, _ := lang.Region()
  108. util.Lang = base.String() + "_" + region.String()
  109. Conf.Lang = util.Lang
  110. logging.LogInfof("initialized language [%s] based on device locale", Conf.Lang)
  111. } else {
  112. logging.LogDebugf("check device locale failed [%s], using default language [en_US]", err)
  113. util.Lang = "en_US"
  114. Conf.Lang = util.Lang
  115. }
  116. }
  117. }
  118. Conf.Langs = loadLangs()
  119. if nil == Conf.Appearance {
  120. Conf.Appearance = conf.NewAppearance()
  121. }
  122. var langOK bool
  123. for _, l := range Conf.Langs {
  124. if Conf.Lang == l.Name {
  125. langOK = true
  126. break
  127. }
  128. }
  129. if !langOK {
  130. Conf.Lang = "en_US"
  131. }
  132. Conf.Appearance.Lang = Conf.Lang
  133. if nil == Conf.UILayout {
  134. Conf.UILayout = &conf.UILayout{}
  135. }
  136. if nil == Conf.Keymap {
  137. Conf.Keymap = &conf.Keymap{}
  138. }
  139. if "" == Conf.Appearance.CodeBlockThemeDark {
  140. Conf.Appearance.CodeBlockThemeDark = "dracula"
  141. }
  142. if "" == Conf.Appearance.CodeBlockThemeLight {
  143. Conf.Appearance.CodeBlockThemeLight = "github"
  144. }
  145. if nil == Conf.FileTree {
  146. Conf.FileTree = conf.NewFileTree()
  147. }
  148. if 1 > Conf.FileTree.MaxListCount {
  149. Conf.FileTree.MaxListCount = 512
  150. }
  151. if 1 > Conf.FileTree.MaxOpenTabCount {
  152. Conf.FileTree.MaxOpenTabCount = 8
  153. }
  154. if 32 < Conf.FileTree.MaxOpenTabCount {
  155. Conf.FileTree.MaxOpenTabCount = 32
  156. }
  157. if nil == Conf.Tag {
  158. Conf.Tag = conf.NewTag()
  159. }
  160. if nil == Conf.Editor {
  161. Conf.Editor = conf.NewEditor()
  162. }
  163. if 1 > len(Conf.Editor.Emoji) {
  164. Conf.Editor.Emoji = []string{}
  165. }
  166. if 1 > Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
  167. Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 64
  168. }
  169. if 5120 < Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
  170. Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 5120
  171. }
  172. if nil == Conf.Export {
  173. Conf.Export = conf.NewExport()
  174. }
  175. if 0 == Conf.Export.BlockRefMode || 1 == Conf.Export.BlockRefMode {
  176. // 废弃导出选项引用块转换为原始块和引述块 https://github.com/siyuan-note/siyuan/issues/3155
  177. Conf.Export.BlockRefMode = 4 // 改为脚注
  178. }
  179. if 9 > Conf.Editor.FontSize || 72 < Conf.Editor.FontSize {
  180. Conf.Editor.FontSize = 16
  181. }
  182. if "" == Conf.Editor.PlantUMLServePath {
  183. Conf.Editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1"
  184. }
  185. if nil == Conf.Graph || nil == Conf.Graph.Local || nil == Conf.Graph.Global {
  186. Conf.Graph = conf.NewGraph()
  187. }
  188. if nil == Conf.System {
  189. Conf.System = conf.NewSystem()
  190. } else {
  191. Conf.System.KernelVersion = util.Ver
  192. Conf.System.IsInsider = util.IsInsider
  193. }
  194. if nil == Conf.System.NetworkProxy {
  195. Conf.System.NetworkProxy = &conf.NetworkProxy{}
  196. }
  197. if "" == Conf.System.ID {
  198. Conf.System.ID = util.GetDeviceID()
  199. }
  200. if "std" == util.Container {
  201. Conf.System.ID = util.GetDeviceID()
  202. }
  203. Conf.System.AppDir = util.WorkingDir
  204. Conf.System.ConfDir = util.ConfDir
  205. Conf.System.HomeDir = util.HomeDir
  206. Conf.System.WorkspaceDir = util.WorkspaceDir
  207. Conf.System.DataDir = util.DataDir
  208. Conf.System.Container = util.Container
  209. Conf.System.OS = runtime.GOOS
  210. Conf.Newbie = util.IsNewbie
  211. if "" != Conf.UserData {
  212. Conf.User = loadUserFromConf()
  213. }
  214. if nil == Conf.Account {
  215. Conf.Account = conf.NewAccount()
  216. }
  217. if nil == Conf.Sync {
  218. Conf.Sync = conf.NewSync()
  219. }
  220. if 0 == Conf.Sync.Mode {
  221. Conf.Sync.Mode = 1
  222. }
  223. if nil == Conf.Api {
  224. Conf.Api = conf.NewAPI()
  225. }
  226. if nil == Conf.Repo {
  227. Conf.Repo = conf.NewRepo()
  228. }
  229. if 1440 < Conf.Editor.GenerateHistoryInterval {
  230. Conf.Editor.GenerateHistoryInterval = 1440
  231. }
  232. if 1 > Conf.Editor.HistoryRetentionDays {
  233. Conf.Editor.HistoryRetentionDays = 7
  234. }
  235. if nil == Conf.Search {
  236. Conf.Search = conf.NewSearch()
  237. }
  238. if nil == Conf.Stat {
  239. Conf.Stat = conf.NewStat()
  240. }
  241. Conf.ReadOnly = util.ReadOnly
  242. if "" != util.AccessAuthCode {
  243. Conf.AccessAuthCode = util.AccessAuthCode
  244. }
  245. Conf.LocalIPs = util.GetLocalIPs()
  246. Conf.Save()
  247. logging.SetLogLevel(Conf.LogLevel)
  248. if Conf.System.UploadErrLog {
  249. logging.LogInfof("user has enabled [Automatically upload error messages and diagnostic data]")
  250. sentry.Init(sentry.ClientOptions{
  251. Dsn: "https://bdff135f14654ae58a054adeceb2c308@o1173696.ingest.sentry.io/6269178",
  252. Release: util.Ver,
  253. Environment: util.Mode,
  254. })
  255. }
  256. util.SetNetworkProxy(Conf.System.NetworkProxy.String())
  257. }
  258. var langs = map[string]map[int]string{}
  259. var timeLangs = map[string]map[string]interface{}{}
  260. func initLang() {
  261. p := filepath.Join(util.WorkingDir, "appearance", "langs")
  262. dir, err := os.Open(p)
  263. if nil != err {
  264. logging.LogFatalf("open language configuration folder [%s] failed: %s", p, err)
  265. }
  266. defer dir.Close()
  267. langNames, err := dir.Readdirnames(-1)
  268. if nil != err {
  269. logging.LogFatalf("list language configuration folder [%s] failed: %s", p, err)
  270. }
  271. for _, langName := range langNames {
  272. jsonPath := filepath.Join(p, langName)
  273. data, err := os.ReadFile(jsonPath)
  274. if nil != err {
  275. logging.LogErrorf("read language configuration [%s] failed: %s", jsonPath, err)
  276. continue
  277. }
  278. langMap := map[string]interface{}{}
  279. if err := gulu.JSON.UnmarshalJSON(data, &langMap); nil != err {
  280. logging.LogErrorf("parse language configuration failed [%s] failed: %s", jsonPath, err)
  281. continue
  282. }
  283. kernelMap := map[int]string{}
  284. label := langMap["_label"].(string)
  285. kernelLangs := langMap["_kernel"].(map[string]interface{})
  286. for k, v := range kernelLangs {
  287. num, err := strconv.Atoi(k)
  288. if nil != err {
  289. logging.LogErrorf("parse language configuration [%s] item [%d] failed [%s] failed: %s", p, num, err)
  290. continue
  291. }
  292. kernelMap[num] = v.(string)
  293. }
  294. kernelMap[-1] = label
  295. name := langName[:strings.LastIndex(langName, ".")]
  296. langs[name] = kernelMap
  297. timeLangs[name] = langMap["_time"].(map[string]interface{})
  298. }
  299. }
  300. func loadLangs() (ret []*conf.Lang) {
  301. for name, langMap := range langs {
  302. lang := &conf.Lang{Label: langMap[-1], Name: name}
  303. ret = append(ret, lang)
  304. }
  305. sort.Slice(ret, func(i, j int) bool {
  306. return ret[i].Name < ret[j].Name
  307. })
  308. return
  309. }
  310. var exitLock = sync.Mutex{}
  311. func Close(force bool) (err error) {
  312. exitLock.Lock()
  313. defer exitLock.Unlock()
  314. treenode.CloseBlockTree()
  315. util.PushMsg(Conf.Language(95), 10000*60)
  316. WaitForWritingFiles()
  317. if !force {
  318. SyncData(false, true, false)
  319. if 0 != ExitSyncSucc {
  320. err = errors.New(Conf.Language(96))
  321. return
  322. }
  323. }
  324. //util.UIProcessIDs.Range(func(key, _ interface{}) bool {
  325. // pid := key.(string)
  326. // util.Kill(pid)
  327. // return true
  328. //})
  329. Conf.Close()
  330. sql.CloseDatabase()
  331. util.WebSocketServer.Close()
  332. clearWorkspaceTemp()
  333. logging.LogInfof("exited kernel")
  334. go func() {
  335. time.Sleep(500 * time.Millisecond)
  336. os.Exit(util.ExitCodeOk)
  337. }()
  338. return
  339. }
  340. var CustomEmojis = sync.Map{}
  341. func NewLute() (ret *lute.Lute) {
  342. ret = util.NewLute()
  343. ret.SetCodeSyntaxHighlightLineNum(Conf.Editor.CodeSyntaxHighlightLineNum)
  344. ret.SetChineseParagraphBeginningSpace(Conf.Export.ParagraphBeginningSpace)
  345. ret.SetProtyleMarkNetImg(Conf.Editor.DisplayNetImgMark)
  346. customEmojiMap := map[string]string{}
  347. CustomEmojis.Range(func(key, value interface{}) bool {
  348. customEmojiMap[key.(string)] = value.(string)
  349. return true
  350. })
  351. ret.PutEmojis(customEmojiMap)
  352. return
  353. }
  354. var confSaveLock = sync.Mutex{}
  355. func (conf *AppConf) Save() {
  356. confSaveLock.Lock()
  357. confSaveLock.Unlock()
  358. newData, _ := gulu.JSON.MarshalIndentJSON(Conf, "", " ")
  359. confPath := filepath.Join(util.ConfDir, "conf.json")
  360. oldData, err := filelock.NoLockFileRead(confPath)
  361. if nil != err {
  362. conf.save0(newData)
  363. return
  364. }
  365. if bytes.Equal(newData, oldData) {
  366. return
  367. }
  368. conf.save0(newData)
  369. }
  370. func (conf *AppConf) save0(data []byte) {
  371. confPath := filepath.Join(util.ConfDir, "conf.json")
  372. if err := filelock.NoLockFileWrite(confPath, data); nil != err {
  373. logging.LogFatalf("write conf [%s] failed: %s", confPath, err)
  374. }
  375. }
  376. func (conf *AppConf) Close() {
  377. conf.Save()
  378. }
  379. func (conf *AppConf) Box(boxID string) *Box {
  380. for _, box := range conf.GetOpenedBoxes() {
  381. if box.ID == boxID {
  382. return box
  383. }
  384. }
  385. return nil
  386. }
  387. func (conf *AppConf) GetBoxes() (ret []*Box) {
  388. ret = []*Box{}
  389. notebooks, err := ListNotebooks()
  390. if nil != err {
  391. return
  392. }
  393. for _, notebook := range notebooks {
  394. id := notebook.ID
  395. name := notebook.Name
  396. closed := notebook.Closed
  397. box := &Box{ID: id, Name: name, Closed: closed}
  398. ret = append(ret, box)
  399. }
  400. return
  401. }
  402. func (conf *AppConf) GetOpenedBoxes() (ret []*Box) {
  403. ret = []*Box{}
  404. notebooks, err := ListNotebooks()
  405. if nil != err {
  406. return
  407. }
  408. for _, notebook := range notebooks {
  409. if !notebook.Closed {
  410. ret = append(ret, notebook)
  411. }
  412. }
  413. return
  414. }
  415. func (conf *AppConf) GetClosedBoxes() (ret []*Box) {
  416. ret = []*Box{}
  417. notebooks, err := ListNotebooks()
  418. if nil != err {
  419. return
  420. }
  421. for _, notebook := range notebooks {
  422. if notebook.Closed {
  423. ret = append(ret, notebook)
  424. }
  425. }
  426. return
  427. }
  428. func (conf *AppConf) Language(num int) (ret string) {
  429. ret = langs[conf.Lang][num]
  430. if "" != ret {
  431. return
  432. }
  433. ret = langs["en_US"][num]
  434. return
  435. }
  436. func InitBoxes() {
  437. initialized := false
  438. if 1 > treenode.CountBlocks() {
  439. if gulu.File.IsExist(util.BlockTreePath) {
  440. util.IncBootProgress(20, "Reading block trees...")
  441. go func() {
  442. for i := 0; i < 40; i++ {
  443. util.RandomSleep(50, 100)
  444. util.IncBootProgress(1, "Reading block trees...")
  445. }
  446. }()
  447. treenode.InitBlockTree(false)
  448. initialized = true
  449. }
  450. } else { // 大于 1 的话说明在同步阶段已经加载过了
  451. initialized = true
  452. }
  453. for _, box := range Conf.GetOpenedBoxes() {
  454. box.UpdateHistoryGenerated() // 初始化历史生成时间为当前时间
  455. if !initialized {
  456. box.Index(true)
  457. }
  458. ListDocTree(box.ID, "/", Conf.FileTree.Sort) // 缓存根一级的文档树展开
  459. }
  460. if !initialized {
  461. treenode.SaveBlockTree()
  462. }
  463. var dbSize string
  464. if dbFile, err := os.Stat(util.DBPath); nil == err {
  465. dbSize = humanize.Bytes(uint64(dbFile.Size()))
  466. }
  467. logging.LogInfof("database size [%s], tree/block count [%d/%d]", dbSize, treenode.CountTrees(), treenode.CountBlocks())
  468. }
  469. func IsSubscriber() bool {
  470. return nil != Conf.User && (-1 == Conf.User.UserSiYuanProExpireTime || 0 < Conf.User.UserSiYuanProExpireTime) && 0 == Conf.User.UserSiYuanSubscriptionStatus
  471. }
  472. func clearWorkspaceTemp() {
  473. os.RemoveAll(filepath.Join(util.TempDir, "bazaar"))
  474. os.RemoveAll(filepath.Join(util.TempDir, "export"))
  475. os.RemoveAll(filepath.Join(util.TempDir, "import"))
  476. os.RemoveAll(filepath.Join(util.TempDir, "repo"))
  477. os.RemoveAll(filepath.Join(util.TempDir, "os"))
  478. tmps, err := filepath.Glob(filepath.Join(util.TempDir, "*.tmp"))
  479. if nil != err {
  480. logging.LogErrorf("glob temp files failed: %s", err)
  481. }
  482. for _, tmp := range tmps {
  483. if err = os.RemoveAll(tmp); nil != err {
  484. logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
  485. } else {
  486. logging.LogInfof("removed temp file [%s]", tmp)
  487. }
  488. }
  489. tmps, err = filepath.Glob(filepath.Join(util.DataDir, ".siyuan", "*.tmp"))
  490. if nil != err {
  491. logging.LogErrorf("glob temp files failed: %s", err)
  492. }
  493. for _, tmp := range tmps {
  494. if err = os.RemoveAll(tmp); nil != err {
  495. logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
  496. } else {
  497. logging.LogInfof("removed temp file [%s]", tmp)
  498. }
  499. }
  500. }