box.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  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. "errors"
  20. "fmt"
  21. "os"
  22. "path"
  23. "path/filepath"
  24. "runtime/debug"
  25. "sort"
  26. "strings"
  27. "time"
  28. "github.com/88250/go-humanize"
  29. "github.com/88250/gulu"
  30. "github.com/88250/lute/ast"
  31. "github.com/88250/lute/html"
  32. "github.com/88250/lute/parse"
  33. "github.com/facette/natsort"
  34. "github.com/siyuan-note/filelock"
  35. "github.com/siyuan-note/logging"
  36. "github.com/siyuan-note/siyuan/kernel/cache"
  37. "github.com/siyuan-note/siyuan/kernel/conf"
  38. "github.com/siyuan-note/siyuan/kernel/filesys"
  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. )
  44. // Box 笔记本。
  45. type Box struct {
  46. ID string `json:"id"`
  47. Name string `json:"name"`
  48. Icon string `json:"icon"`
  49. Sort int `json:"sort"`
  50. SortMode int `json:"sortMode"`
  51. Closed bool `json:"closed"`
  52. NewFlashcardCount int `json:"newFlashcardCount"`
  53. DueFlashcardCount int `json:"dueFlashcardCount"`
  54. FlashcardCount int `json:"flashcardCount"`
  55. historyGenerated int64 // 最近一次历史生成时间
  56. }
  57. func StatJob() {
  58. Conf.m.Lock()
  59. Conf.Stat.TreeCount = treenode.CountTrees()
  60. Conf.Stat.CTreeCount = treenode.CeilTreeCount(Conf.Stat.TreeCount)
  61. Conf.Stat.BlockCount = treenode.CountBlocks()
  62. Conf.Stat.CBlockCount = treenode.CeilBlockCount(Conf.Stat.BlockCount)
  63. Conf.Stat.DataSize, Conf.Stat.AssetsSize = util.DataSize()
  64. Conf.Stat.CDataSize = util.CeilSize(Conf.Stat.DataSize)
  65. Conf.Stat.CAssetsSize = util.CeilSize(Conf.Stat.AssetsSize)
  66. Conf.m.Unlock()
  67. Conf.Save()
  68. logging.LogInfof("auto stat [trees=%d, blocks=%d, dataSize=%s, assetsSize=%s]", Conf.Stat.TreeCount, Conf.Stat.BlockCount, humanize.BytesCustomCeil(uint64(Conf.Stat.DataSize), 2), humanize.BytesCustomCeil(uint64(Conf.Stat.AssetsSize), 2))
  69. // 桌面端检查磁盘可用空间 https://github.com/siyuan-note/siyuan/issues/6873
  70. if util.ContainerStd != util.Container {
  71. return
  72. }
  73. if util.NeedWarnDiskUsage(Conf.Stat.DataSize) {
  74. util.PushMsg(Conf.Language(179), 7000)
  75. }
  76. }
  77. func ListNotebooks() (ret []*Box, err error) {
  78. ret = []*Box{}
  79. dirs, err := os.ReadDir(util.DataDir)
  80. if nil != err {
  81. logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
  82. return ret, err
  83. }
  84. for _, dir := range dirs {
  85. if util.IsReservedFilename(dir.Name()) {
  86. continue
  87. }
  88. if !dir.IsDir() {
  89. continue
  90. }
  91. if !ast.IsNodeIDPattern(dir.Name()) {
  92. continue
  93. }
  94. boxConf := conf.NewBoxConf()
  95. boxDirPath := filepath.Join(util.DataDir, dir.Name())
  96. boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json")
  97. isExistConf := filelock.IsExist(boxConfPath)
  98. if !isExistConf {
  99. // 数据同步时展开文档树操作可能导致数据丢失 https://github.com/siyuan-note/siyuan/issues/7129
  100. logging.LogWarnf("found a corrupted box [%s]", boxDirPath)
  101. } else {
  102. data, readErr := filelock.ReadFile(boxConfPath)
  103. if nil != readErr {
  104. logging.LogErrorf("read box conf [%s] failed: %s", boxConfPath, readErr)
  105. continue
  106. }
  107. if readErr = gulu.JSON.UnmarshalJSON(data, boxConf); nil != readErr {
  108. logging.LogErrorf("parse box conf [%s] failed: %s", boxConfPath, readErr)
  109. filelock.Remove(boxConfPath)
  110. continue
  111. }
  112. }
  113. id := dir.Name()
  114. box := &Box{
  115. ID: id,
  116. Name: boxConf.Name,
  117. Icon: boxConf.Icon,
  118. Sort: boxConf.Sort,
  119. SortMode: boxConf.SortMode,
  120. Closed: boxConf.Closed,
  121. }
  122. if !isExistConf {
  123. // Automatically create notebook conf.json if not found it https://github.com/siyuan-note/siyuan/issues/9647
  124. box.SaveConf(boxConf)
  125. box.Unindex()
  126. logging.LogWarnf("fixed a corrupted box [%s]", boxDirPath)
  127. }
  128. ret = append(ret, box)
  129. }
  130. switch Conf.FileTree.Sort {
  131. case util.SortModeNameASC:
  132. sort.Slice(ret, func(i, j int) bool {
  133. return util.PinYinCompare(util.RemoveEmojiInvisible(ret[i].Name), util.RemoveEmojiInvisible(ret[j].Name))
  134. })
  135. case util.SortModeNameDESC:
  136. sort.Slice(ret, func(i, j int) bool {
  137. return util.PinYinCompare(util.RemoveEmojiInvisible(ret[j].Name), util.RemoveEmojiInvisible(ret[i].Name))
  138. })
  139. case util.SortModeUpdatedASC:
  140. case util.SortModeUpdatedDESC:
  141. case util.SortModeAlphanumASC:
  142. sort.Slice(ret, func(i, j int) bool {
  143. return natsort.Compare(util.RemoveEmojiInvisible(ret[i].Name), util.RemoveEmojiInvisible(ret[j].Name))
  144. })
  145. case util.SortModeAlphanumDESC:
  146. sort.Slice(ret, func(i, j int) bool {
  147. return natsort.Compare(util.RemoveEmojiInvisible(ret[j].Name), util.RemoveEmojiInvisible(ret[i].Name))
  148. })
  149. case util.SortModeCustom:
  150. sort.Slice(ret, func(i, j int) bool { return ret[i].Sort < ret[j].Sort })
  151. case util.SortModeRefCountASC:
  152. case util.SortModeRefCountDESC:
  153. case util.SortModeCreatedASC:
  154. sort.Slice(ret, func(i, j int) bool { return natsort.Compare(ret[j].ID, ret[i].ID) })
  155. case util.SortModeCreatedDESC:
  156. sort.Slice(ret, func(i, j int) bool { return natsort.Compare(ret[j].ID, ret[i].ID) })
  157. }
  158. return
  159. }
  160. func (box *Box) GetConf() (ret *conf.BoxConf) {
  161. ret = conf.NewBoxConf()
  162. confPath := filepath.Join(util.DataDir, box.ID, ".siyuan/conf.json")
  163. if !filelock.IsExist(confPath) {
  164. return
  165. }
  166. data, err := filelock.ReadFile(confPath)
  167. if nil != err {
  168. logging.LogErrorf("read box conf [%s] failed: %s", confPath, err)
  169. return
  170. }
  171. if err = gulu.JSON.UnmarshalJSON(data, ret); nil != err {
  172. logging.LogErrorf("parse box conf [%s] failed: %s", confPath, err)
  173. return
  174. }
  175. return
  176. }
  177. func (box *Box) SaveConf(conf *conf.BoxConf) {
  178. confPath := filepath.Join(util.DataDir, box.ID, ".siyuan/conf.json")
  179. newData, err := gulu.JSON.MarshalIndentJSON(conf, "", " ")
  180. if nil != err {
  181. logging.LogErrorf("marshal box conf [%s] failed: %s", confPath, err)
  182. return
  183. }
  184. oldData, err := filelock.ReadFile(confPath)
  185. if nil != err {
  186. box.saveConf0(newData)
  187. return
  188. }
  189. if bytes.Equal(newData, oldData) {
  190. return
  191. }
  192. box.saveConf0(newData)
  193. }
  194. func (box *Box) saveConf0(data []byte) {
  195. confPath := filepath.Join(util.DataDir, box.ID, ".siyuan/conf.json")
  196. if err := os.MkdirAll(filepath.Join(util.DataDir, box.ID, ".siyuan"), 0755); nil != err {
  197. logging.LogErrorf("save box conf [%s] failed: %s", confPath, err)
  198. }
  199. if err := filelock.WriteFile(confPath, data); nil != err {
  200. logging.LogErrorf("write box conf [%s] failed: %s", confPath, err)
  201. util.ReportFileSysFatalError(err)
  202. return
  203. }
  204. }
  205. func (box *Box) Ls(p string) (ret []*FileInfo, totals int, err error) {
  206. boxLocalPath := filepath.Join(util.DataDir, box.ID)
  207. if strings.HasSuffix(p, ".sy") {
  208. dir := strings.TrimSuffix(p, ".sy")
  209. absDir := filepath.Join(boxLocalPath, dir)
  210. if gulu.File.IsDir(absDir) {
  211. p = dir
  212. } else {
  213. return
  214. }
  215. }
  216. entries, err := os.ReadDir(filepath.Join(util.DataDir, box.ID, p))
  217. if nil != err {
  218. return
  219. }
  220. for _, f := range entries {
  221. info, infoErr := f.Info()
  222. if nil != infoErr {
  223. logging.LogErrorf("read file info failed: %s", infoErr)
  224. continue
  225. }
  226. name := f.Name()
  227. if util.IsReservedFilename(name) {
  228. continue
  229. }
  230. if strings.HasSuffix(name, ".tmp") {
  231. // 移除写入失败时产生的并且早于 30 分钟前的临时文件,近期创建的临时文件可能正在写入中
  232. removePath := filepath.Join(util.DataDir, box.ID, p, name)
  233. if info.ModTime().Before(time.Now().Add(-30 * time.Minute)) {
  234. if removeErr := os.Remove(removePath); nil != removeErr {
  235. logging.LogWarnf("remove tmp file [%s] failed: %s", removePath, removeErr)
  236. }
  237. }
  238. continue
  239. }
  240. totals += 1
  241. fi := &FileInfo{}
  242. fi.name = name
  243. fi.isdir = f.IsDir()
  244. fi.size = info.Size()
  245. fPath := path.Join(p, name)
  246. if f.IsDir() {
  247. fPath += "/"
  248. }
  249. fi.path = fPath
  250. ret = append(ret, fi)
  251. }
  252. return
  253. }
  254. func (box *Box) Stat(p string) (ret *FileInfo) {
  255. absPath := filepath.Join(util.DataDir, box.ID, p)
  256. info, err := os.Stat(absPath)
  257. if nil != err {
  258. if !os.IsNotExist(err) {
  259. logging.LogErrorf("stat [%s] failed: %s", absPath, err)
  260. }
  261. return
  262. }
  263. ret = &FileInfo{
  264. path: p,
  265. name: info.Name(),
  266. size: info.Size(),
  267. isdir: info.IsDir(),
  268. }
  269. return
  270. }
  271. func (box *Box) Exist(p string) bool {
  272. return filelock.IsExist(filepath.Join(util.DataDir, box.ID, p))
  273. }
  274. func (box *Box) Mkdir(path string) error {
  275. if err := os.Mkdir(filepath.Join(util.DataDir, box.ID, path), 0755); nil != err {
  276. msg := fmt.Sprintf(Conf.Language(6), box.Name, path, err)
  277. logging.LogErrorf("mkdir [path=%s] in box [%s] failed: %s", path, box.ID, err)
  278. return errors.New(msg)
  279. }
  280. IncSync()
  281. return nil
  282. }
  283. func (box *Box) MkdirAll(path string) error {
  284. if err := os.MkdirAll(filepath.Join(util.DataDir, box.ID, path), 0755); nil != err {
  285. msg := fmt.Sprintf(Conf.Language(6), box.Name, path, err)
  286. logging.LogErrorf("mkdir all [path=%s] in box [%s] failed: %s", path, box.ID, err)
  287. return errors.New(msg)
  288. }
  289. IncSync()
  290. return nil
  291. }
  292. func (box *Box) Move(oldPath, newPath string) error {
  293. boxLocalPath := filepath.Join(util.DataDir, box.ID)
  294. fromPath := filepath.Join(boxLocalPath, oldPath)
  295. toPath := filepath.Join(boxLocalPath, newPath)
  296. if err := filelock.Rename(fromPath, toPath); nil != err {
  297. msg := fmt.Sprintf(Conf.Language(5), box.Name, fromPath, err)
  298. logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, box.Name, err)
  299. return errors.New(msg)
  300. }
  301. if oldDir := path.Dir(oldPath); ast.IsNodeIDPattern(path.Base(oldDir)) {
  302. fromDir := filepath.Join(boxLocalPath, oldDir)
  303. if util.IsEmptyDir(fromDir) {
  304. filelock.Remove(fromDir)
  305. }
  306. }
  307. IncSync()
  308. return nil
  309. }
  310. func (box *Box) Remove(path string) error {
  311. boxLocalPath := filepath.Join(util.DataDir, box.ID)
  312. filePath := filepath.Join(boxLocalPath, path)
  313. if err := filelock.Remove(filePath); nil != err {
  314. msg := fmt.Sprintf(Conf.Language(7), box.Name, path, err)
  315. logging.LogErrorf("remove [path=%s] in box [%s] failed: %s", path, box.ID, err)
  316. return errors.New(msg)
  317. }
  318. IncSync()
  319. return nil
  320. }
  321. func (box *Box) ListFiles(path string) (ret []*FileInfo) {
  322. fis, _, err := box.Ls(path)
  323. if nil != err {
  324. return
  325. }
  326. box.listFiles(&fis, &ret)
  327. return
  328. }
  329. func (box *Box) listFiles(files, ret *[]*FileInfo) {
  330. for _, file := range *files {
  331. if file.isdir {
  332. fis, _, err := box.Ls(file.path)
  333. if nil == err {
  334. box.listFiles(&fis, ret)
  335. }
  336. *ret = append(*ret, file)
  337. } else {
  338. *ret = append(*ret, file)
  339. }
  340. }
  341. return
  342. }
  343. func isSkipFile(filename string) bool {
  344. return strings.HasPrefix(filename, ".") || "node_modules" == filename || "dist" == filename || "target" == filename
  345. }
  346. func moveTree(tree *parse.Tree) {
  347. treenode.SetBlockTreePath(tree)
  348. if hidden := tree.Root.IALAttr("custom-hidden"); "true" == hidden {
  349. tree.Root.RemoveIALAttr("custom-hidden")
  350. filesys.WriteTree(tree)
  351. }
  352. sql.RemoveTreeQueue(tree.ID)
  353. sql.IndexTreeQueue(tree)
  354. box := Conf.Box(tree.Box)
  355. box.renameSubTrees(tree)
  356. }
  357. func (box *Box) renameSubTrees(tree *parse.Tree) {
  358. subFiles := box.ListFiles(tree.Path)
  359. luteEngine := util.NewLute()
  360. for _, subFile := range subFiles {
  361. if !strings.HasSuffix(subFile.path, ".sy") {
  362. continue
  363. }
  364. subTree, err := filesys.LoadTree(box.ID, subFile.path, luteEngine) // LoadTree 会重新构造 HPath
  365. if nil != err {
  366. continue
  367. }
  368. treenode.SetBlockTreePath(subTree)
  369. sql.RenameSubTreeQueue(subTree)
  370. msg := fmt.Sprintf(Conf.Language(107), html.EscapeString(subTree.HPath))
  371. util.PushStatusBar(msg)
  372. }
  373. }
  374. func parseKTree(kramdown []byte) (ret *parse.Tree) {
  375. luteEngine := NewLute()
  376. ret = parse.Parse("", kramdown, luteEngine.ParseOptions)
  377. normalizeTree(ret)
  378. return
  379. }
  380. func normalizeTree(tree *parse.Tree) {
  381. if nil == tree.Root.FirstChild {
  382. tree.Root.AppendChild(treenode.NewParagraph())
  383. }
  384. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  385. if !entering {
  386. return ast.WalkContinue
  387. }
  388. if n.IsEmptyBlockIAL() {
  389. // 空段落保留
  390. p := &ast.Node{Type: ast.NodeParagraph}
  391. p.KramdownIAL = parse.Tokens2IAL(n.Tokens)
  392. p.ID = p.IALAttr("id")
  393. n.InsertBefore(p)
  394. return ast.WalkContinue
  395. }
  396. id := n.IALAttr("id")
  397. if "" == id {
  398. n.SetIALAttr("id", n.ID)
  399. }
  400. if "" == n.IALAttr("id") && (ast.NodeParagraph == n.Type || ast.NodeList == n.Type || ast.NodeListItem == n.Type || ast.NodeBlockquote == n.Type ||
  401. ast.NodeMathBlock == n.Type || ast.NodeCodeBlock == n.Type || ast.NodeHeading == n.Type || ast.NodeTable == n.Type || ast.NodeThematicBreak == n.Type ||
  402. ast.NodeYamlFrontMatter == n.Type || ast.NodeBlockQueryEmbed == n.Type || ast.NodeSuperBlock == n.Type || ast.NodeAttributeView == n.Type ||
  403. ast.NodeHTMLBlock == n.Type || ast.NodeIFrame == n.Type || ast.NodeWidget == n.Type || ast.NodeAudio == n.Type || ast.NodeVideo == n.Type) {
  404. n.ID = ast.NewNodeID()
  405. n.KramdownIAL = [][]string{{"id", n.ID}}
  406. n.InsertAfter(&ast.Node{Type: ast.NodeKramdownBlockIAL, Tokens: []byte("{: id=\"" + n.ID + "\"}")})
  407. n.SetIALAttr("updated", util.TimeFromID(n.ID))
  408. }
  409. if "" == n.ID && 0 < len(n.KramdownIAL) && ast.NodeDocument != n.Type {
  410. n.ID = n.IALAttr("id")
  411. }
  412. if ast.NodeHTMLBlock == n.Type {
  413. tokens := bytes.TrimSpace(n.Tokens)
  414. if !bytes.HasPrefix(tokens, []byte("<div>")) {
  415. tokens = []byte("<div>\n" + string(tokens))
  416. }
  417. if !bytes.HasSuffix(tokens, []byte("</div>")) {
  418. tokens = append(tokens, []byte("\n</div>")...)
  419. }
  420. n.Tokens = tokens
  421. return ast.WalkContinue
  422. }
  423. if ast.NodeInlineHTML == n.Type {
  424. n.Type = ast.NodeText
  425. return ast.WalkContinue
  426. }
  427. if ast.NodeParagraph == n.Type && nil != n.FirstChild && ast.NodeTaskListItemMarker == n.FirstChild.Type {
  428. // 踢掉任务列表的第一个子节点左侧空格
  429. n.FirstChild.Next.Tokens = bytes.TrimLeft(n.FirstChild.Next.Tokens, " ")
  430. // 调整 li.p.tlim 为 li.tlim.p
  431. n.InsertBefore(n.FirstChild)
  432. }
  433. if ast.NodeLinkTitle == n.Type {
  434. // 避免重复转义图片标题内容 Repeat the escaped content of the image title https://github.com/siyuan-note/siyuan/issues/11681
  435. n.Tokens = html.UnescapeBytes(n.Tokens)
  436. }
  437. return ast.WalkContinue
  438. })
  439. tree.Root.KramdownIAL = parse.Tokens2IAL(tree.Root.LastChild.Tokens)
  440. return
  441. }
  442. func FullReindex() {
  443. task.AppendTask(task.DatabaseIndexFull, fullReindex)
  444. task.AppendTask(task.DatabaseIndexRef, IndexRefs)
  445. go func() {
  446. sql.WaitForWritingDatabase()
  447. ResetVirtualBlockRefCache()
  448. }()
  449. task.AppendTaskWithTimeout(task.DatabaseIndexEmbedBlock, 30*time.Second, autoIndexEmbedBlock)
  450. cache.ClearDocsIAL()
  451. cache.ClearBlocksIAL()
  452. task.AppendTask(task.ReloadUI, util.ReloadUI)
  453. }
  454. func fullReindex() {
  455. util.PushEndlessProgress(Conf.language(35))
  456. defer util.PushClearProgress()
  457. WaitForWritingFiles()
  458. if err := sql.InitDatabase(true); nil != err {
  459. os.Exit(logging.ExitCodeReadOnlyDatabase)
  460. return
  461. }
  462. sql.IndexIgnoreCached = false
  463. openedBoxes := Conf.GetOpenedBoxes()
  464. for _, openedBox := range openedBoxes {
  465. index(openedBox.ID)
  466. }
  467. treenode.SaveBlockTree(true)
  468. LoadFlashcards()
  469. debug.FreeOSMemory()
  470. }
  471. func ChangeBoxSort(boxIDs []string) {
  472. for i, boxID := range boxIDs {
  473. box := &Box{ID: boxID}
  474. boxConf := box.GetConf()
  475. boxConf.Sort = i + 1
  476. box.SaveConf(boxConf)
  477. }
  478. }
  479. func SetBoxIcon(boxID, icon string) {
  480. box := &Box{ID: boxID}
  481. boxConf := box.GetConf()
  482. boxConf.Icon = icon
  483. box.SaveConf(boxConf)
  484. }
  485. func (box *Box) UpdateHistoryGenerated() {
  486. boxLatestHistoryTime[box.ID] = time.Now()
  487. }
  488. func getBoxesByPaths(paths []string) (ret map[string]*Box) {
  489. ret = map[string]*Box{}
  490. for _, p := range paths {
  491. id := strings.TrimSuffix(path.Base(p), ".sy")
  492. bt := treenode.GetBlockTree(id)
  493. if nil != bt {
  494. ret[p] = Conf.Box(bt.BoxID)
  495. }
  496. }
  497. return
  498. }