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 "errors"
22 "fmt"
23 "os"
24 "path"
25 "path/filepath"
26 "runtime/debug"
27 "sort"
28 "strings"
29 "time"
30
31 "github.com/88250/go-humanize"
32 "github.com/88250/gulu"
33 "github.com/88250/lute/ast"
34 "github.com/88250/lute/html"
35 "github.com/88250/lute/lex"
36 "github.com/88250/lute/parse"
37 "github.com/araddon/dateparse"
38 "github.com/siyuan-note/filelock"
39 "github.com/siyuan-note/logging"
40 "github.com/siyuan-note/siyuan/kernel/cache"
41 "github.com/siyuan-note/siyuan/kernel/conf"
42 "github.com/siyuan-note/siyuan/kernel/filesys"
43 "github.com/siyuan-note/siyuan/kernel/sql"
44 "github.com/siyuan-note/siyuan/kernel/task"
45 "github.com/siyuan-note/siyuan/kernel/treenode"
46 "github.com/siyuan-note/siyuan/kernel/util"
47 "gopkg.in/yaml.v3"
48)
49
50// Box 笔记本。
51type Box struct {
52 ID string `json:"id"`
53 Name string `json:"name"`
54 Icon string `json:"icon"`
55 Sort int `json:"sort"`
56 SortMode int `json:"sortMode"`
57 Closed bool `json:"closed"`
58
59 NewFlashcardCount int `json:"newFlashcardCount"`
60 DueFlashcardCount int `json:"dueFlashcardCount"`
61 FlashcardCount int `json:"flashcardCount"`
62}
63
64func StatJob() {
65
66 Conf.m.Lock()
67 Conf.Stat.TreeCount = treenode.CountTrees()
68 Conf.Stat.CTreeCount = treenode.CeilTreeCount(Conf.Stat.TreeCount)
69 Conf.Stat.BlockCount = treenode.CountBlocks()
70 Conf.Stat.CBlockCount = treenode.CeilBlockCount(Conf.Stat.BlockCount)
71 Conf.Stat.DataSize, Conf.Stat.AssetsSize = util.DataSize()
72 Conf.Stat.CDataSize = util.CeilSize(Conf.Stat.DataSize)
73 Conf.Stat.CAssetsSize = util.CeilSize(Conf.Stat.AssetsSize)
74 Conf.m.Unlock()
75 Conf.Save()
76
77 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))
78
79 // 桌面端检查磁盘可用空间 https://github.com/siyuan-note/siyuan/issues/6873
80 if util.ContainerStd != util.Container {
81 return
82 }
83
84 if util.NeedWarnDiskUsage(Conf.Stat.DataSize) {
85 util.PushMsg(Conf.Language(179), 7000)
86 }
87}
88
89func ListNotebooks() (ret []*Box, err error) {
90 ret = []*Box{}
91 dirs, err := os.ReadDir(util.DataDir)
92 if err != nil {
93 logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
94 return ret, err
95 }
96 for _, dir := range dirs {
97 if util.IsReservedFilename(dir.Name()) {
98 continue
99 }
100
101 if !dir.IsDir() {
102 continue
103 }
104
105 id := dir.Name()
106 if !ast.IsNodeIDPattern(id) {
107 continue
108 }
109
110 boxConf := conf.NewBoxConf()
111 boxDirPath := filepath.Join(util.DataDir, id)
112 boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json")
113 isExistConf := filelock.IsExist(boxConfPath)
114 if !isExistConf {
115 if !IsUserGuide(id) {
116 // 数据同步时展开文档树操作可能导致数据丢失 https://github.com/siyuan-note/siyuan/issues/7129
117 logging.LogWarnf("found a corrupted box [%s]", boxDirPath)
118 } else {
119 continue
120 }
121 } else {
122 data, readErr := filelock.ReadFile(boxConfPath)
123 if nil != readErr {
124 logging.LogErrorf("read box conf [%s] failed: %s", boxConfPath, readErr)
125 continue
126 }
127 if readErr = gulu.JSON.UnmarshalJSON(data, boxConf); nil != readErr {
128 logging.LogErrorf("parse box conf [%s] failed: %s", boxConfPath, readErr)
129 filelock.Remove(boxConfPath)
130 continue
131 }
132 }
133
134 icon := boxConf.Icon
135 if strings.Contains(icon, ".") { // 说明是自定义图标
136 // XSS through emoji name https://github.com/siyuan-note/siyuan/issues/15034
137 icon = util.FilterUploadEmojiFileName(icon)
138 }
139
140 box := &Box{
141 ID: id,
142 Name: boxConf.Name,
143 Icon: icon,
144 Sort: boxConf.Sort,
145 SortMode: boxConf.SortMode,
146 Closed: boxConf.Closed,
147 }
148
149 if !isExistConf {
150 // Automatically create notebook conf.json if not found it https://github.com/siyuan-note/siyuan/issues/9647
151 box.SaveConf(boxConf)
152 box.Unindex()
153 logging.LogWarnf("fixed a corrupted box [%s]", boxDirPath)
154 }
155 ret = append(ret, box)
156 }
157
158 switch Conf.FileTree.Sort {
159 case util.SortModeNameASC:
160 sort.Slice(ret, func(i, j int) bool {
161 return util.PinYinCompare4FileTree(ret[i].Name, ret[j].Name)
162 })
163 case util.SortModeNameDESC:
164 sort.Slice(ret, func(i, j int) bool {
165 return util.PinYinCompare4FileTree(ret[j].Name, ret[i].Name)
166 })
167 case util.SortModeAlphanumASC:
168 sort.Slice(ret, func(i, j int) bool {
169 return util.NaturalCompare(ret[i].Name, ret[j].Name)
170 })
171 case util.SortModeAlphanumDESC:
172 sort.Slice(ret, func(i, j int) bool {
173 return util.NaturalCompare(ret[j].Name, ret[i].Name)
174 })
175 case util.SortModeCustom:
176 sort.Slice(ret, func(i, j int) bool { return ret[i].Sort < ret[j].Sort })
177 case util.SortModeCreatedASC:
178 sort.Slice(ret, func(i, j int) bool { return ret[i].ID < ret[j].ID })
179 case util.SortModeCreatedDESC:
180 sort.Slice(ret, func(i, j int) bool { return ret[i].ID > ret[j].ID })
181 }
182 return
183}
184
185func (box *Box) GetConf() (ret *conf.BoxConf) {
186 ret = conf.NewBoxConf()
187
188 confPath := filepath.Join(util.DataDir, box.ID, ".siyuan/conf.json")
189 if !filelock.IsExist(confPath) {
190 return
191 }
192
193 data, err := filelock.ReadFile(confPath)
194 if err != nil {
195 logging.LogErrorf("read box conf [%s] failed: %s", confPath, err)
196 return
197 }
198
199 if err = gulu.JSON.UnmarshalJSON(data, ret); err != nil {
200 logging.LogErrorf("parse box conf [%s] failed: %s", confPath, err)
201 return
202 }
203
204 icon := ret.Icon
205 if strings.Contains(icon, ".") {
206 // XSS through emoji name https://github.com/siyuan-note/siyuan/issues/15034
207 icon = util.FilterUploadEmojiFileName(icon)
208 ret.Icon = icon
209 }
210 return
211}
212
213func (box *Box) SaveConf(conf *conf.BoxConf) {
214 confPath := filepath.Join(util.DataDir, box.ID, ".siyuan/conf.json")
215 newData, err := gulu.JSON.MarshalIndentJSON(conf, "", " ")
216 if err != nil {
217 logging.LogErrorf("marshal box conf [%s] failed: %s", confPath, err)
218 return
219 }
220
221 oldData, err := filelock.ReadFile(confPath)
222 if err != nil {
223 box.saveConf0(newData)
224 return
225 }
226
227 if bytes.Equal(newData, oldData) {
228 return
229 }
230
231 box.saveConf0(newData)
232}
233
234func (box *Box) saveConf0(data []byte) {
235 confPath := filepath.Join(util.DataDir, box.ID, ".siyuan/conf.json")
236 if err := os.MkdirAll(filepath.Join(util.DataDir, box.ID, ".siyuan"), 0755); err != nil {
237 logging.LogErrorf("save box conf [%s] failed: %s", confPath, err)
238 }
239 if err := filelock.WriteFile(confPath, data); err != nil {
240 logging.LogErrorf("write box conf [%s] failed: %s", confPath, err)
241 util.ReportFileSysFatalError(err)
242 return
243 }
244}
245
246func (box *Box) Ls(p string) (ret []*FileInfo, totals int, err error) {
247 boxLocalPath := filepath.Join(util.DataDir, box.ID)
248 if strings.HasSuffix(p, ".sy") {
249 dir := strings.TrimSuffix(p, ".sy")
250 absDir := filepath.Join(boxLocalPath, dir)
251 if gulu.File.IsDir(absDir) {
252 p = dir
253 } else {
254 return
255 }
256 }
257
258 entries, err := os.ReadDir(filepath.Join(util.DataDir, box.ID, p))
259 if err != nil {
260 return
261 }
262
263 for _, f := range entries {
264 info, infoErr := f.Info()
265 if nil != infoErr {
266 logging.LogErrorf("read file info failed: %s", infoErr)
267 continue
268 }
269
270 name := f.Name()
271 if util.IsReservedFilename(name) {
272 continue
273 }
274 if strings.HasSuffix(name, ".tmp") {
275 // 移除写入失败时产生的并且早于 30 分钟前的临时文件,近期创建的临时文件可能正在写入中
276 removePath := filepath.Join(util.DataDir, box.ID, p, name)
277 if info.ModTime().Before(time.Now().Add(-30 * time.Minute)) {
278 if removeErr := os.Remove(removePath); nil != removeErr {
279 logging.LogWarnf("remove tmp file [%s] failed: %s", removePath, removeErr)
280 }
281 }
282 continue
283 }
284
285 totals += 1
286 fi := &FileInfo{}
287 fi.name = name
288 fi.isdir = f.IsDir()
289 fi.size = info.Size()
290 fPath := path.Join(p, name)
291 if f.IsDir() {
292 fPath += "/"
293 }
294 fi.path = fPath
295 ret = append(ret, fi)
296 }
297 return
298}
299
300func (box *Box) Stat(p string) (ret *FileInfo) {
301 absPath := filepath.Join(util.DataDir, box.ID, p)
302 info, err := os.Stat(absPath)
303 if err != nil {
304 if !os.IsNotExist(err) {
305 logging.LogErrorf("stat [%s] failed: %s", absPath, err)
306 }
307 return
308 }
309 ret = &FileInfo{
310 path: p,
311 name: info.Name(),
312 size: info.Size(),
313 isdir: info.IsDir(),
314 }
315 return
316}
317
318func (box *Box) Exist(p string) bool {
319 return filelock.IsExist(filepath.Join(util.DataDir, box.ID, p))
320}
321
322func (box *Box) Mkdir(path string) error {
323 if err := os.Mkdir(filepath.Join(util.DataDir, box.ID, path), 0755); err != nil {
324 msg := fmt.Sprintf(Conf.Language(6), box.Name, path, err)
325 logging.LogErrorf("mkdir [path=%s] in box [%s] failed: %s", path, box.ID, err)
326 return errors.New(msg)
327 }
328 IncSync()
329 return nil
330}
331
332func (box *Box) MkdirAll(path string) error {
333 if err := os.MkdirAll(filepath.Join(util.DataDir, box.ID, path), 0755); err != nil {
334 msg := fmt.Sprintf(Conf.Language(6), box.Name, path, err)
335 logging.LogErrorf("mkdir all [path=%s] in box [%s] failed: %s", path, box.ID, err)
336 return errors.New(msg)
337 }
338 IncSync()
339 return nil
340}
341
342func (box *Box) Move(oldPath, newPath string) error {
343 boxLocalPath := filepath.Join(util.DataDir, box.ID)
344 fromPath := filepath.Join(boxLocalPath, oldPath)
345 toPath := filepath.Join(boxLocalPath, newPath)
346
347 if err := filelock.Rename(fromPath, toPath); err != nil {
348 msg := fmt.Sprintf(Conf.Language(5), box.Name, fromPath, err)
349 logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, box.Name, err)
350 return errors.New(msg)
351 }
352
353 if oldDir := path.Dir(oldPath); ast.IsNodeIDPattern(path.Base(oldDir)) {
354 fromDir := filepath.Join(boxLocalPath, oldDir)
355 if util.IsEmptyDir(fromDir) {
356 filelock.Remove(fromDir)
357 }
358 }
359 IncSync()
360 return nil
361}
362
363func (box *Box) Remove(path string) error {
364 boxLocalPath := filepath.Join(util.DataDir, box.ID)
365 filePath := filepath.Join(boxLocalPath, path)
366 if err := filelock.Remove(filePath); err != nil {
367 msg := fmt.Sprintf(Conf.Language(7), box.Name, path, err)
368 logging.LogErrorf("remove [path=%s] in box [%s] failed: %s", path, box.ID, err)
369 return errors.New(msg)
370 }
371 IncSync()
372 return nil
373}
374
375func (box *Box) ListFiles(path string) (ret []*FileInfo) {
376 fis, _, err := box.Ls(path)
377 if err != nil {
378 return
379 }
380 box.listFiles(&fis, &ret)
381 return
382}
383
384func (box *Box) listFiles(files, ret *[]*FileInfo) {
385 for _, file := range *files {
386 if file.isdir {
387 fis, _, err := box.Ls(file.path)
388 if err == nil {
389 box.listFiles(&fis, ret)
390 }
391 *ret = append(*ret, file)
392 } else {
393 *ret = append(*ret, file)
394 }
395 }
396 return
397}
398
399type BoxInfo struct {
400 ID string `json:"id"`
401 Name string `json:"name"`
402 DocCount int `json:"docCount"`
403 Size uint64 `json:"size"`
404 HSize string `json:"hSize"`
405 Mtime int64 `json:"mtime"`
406 CTime int64 `json:"ctime"`
407 HMtime string `json:"hMtime"`
408 HCtime string `json:"hCtime"`
409}
410
411func (box *Box) GetInfo() (ret *BoxInfo) {
412 ret = &BoxInfo{
413 ID: box.ID,
414 Name: util.EscapeHTML(box.Name),
415 }
416
417 fileInfos := box.ListFiles("/")
418
419 t, _ := time.ParseInLocation("20060102150405", box.ID[:14], time.Local)
420 ret.CTime = t.Unix()
421 ret.HCtime = t.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(t, Conf.Lang)
422
423 docLatestModTime := t
424 for _, fileInfo := range fileInfos {
425 if fileInfo.isdir {
426 continue
427 }
428
429 if strings.HasPrefix(fileInfo.name, ".") {
430 continue
431 }
432
433 if !strings.HasSuffix(fileInfo.path, ".sy") {
434 continue
435 }
436
437 id := strings.TrimSuffix(fileInfo.name, ".sy")
438 if !ast.IsNodeIDPattern(id) {
439 continue
440 }
441
442 absPath := filepath.Join(util.DataDir, box.ID, fileInfo.path)
443 info, err := os.Stat(absPath)
444 if err != nil {
445 logging.LogErrorf("stat [%s] failed: %s", absPath, err)
446 continue
447 }
448
449 ret.DocCount++
450 ret.Size += uint64(info.Size())
451 docModT := info.ModTime()
452 if docModT.After(docLatestModTime) {
453 docLatestModTime = docModT
454 }
455 }
456
457 ret.HSize = humanize.BytesCustomCeil(ret.Size, 2)
458 ret.Mtime = docLatestModTime.Unix()
459 ret.HMtime = docLatestModTime.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(docLatestModTime, Conf.Lang)
460 return
461}
462
463func isSkipFile(filename string) bool {
464 return strings.HasPrefix(filename, ".") || "node_modules" == filename || "dist" == filename || "target" == filename
465}
466
467func moveTree(tree *parse.Tree) {
468 treenode.SetBlockTreePath(tree)
469
470 if hidden := tree.Root.IALAttr("custom-hidden"); "true" == hidden {
471 tree.Root.RemoveIALAttr("custom-hidden")
472 filesys.WriteTree(tree)
473 }
474
475 sql.RemoveTreeQueue(tree.ID)
476 sql.IndexTreeQueue(tree)
477
478 box := Conf.Box(tree.Box)
479 box.renameSubTrees(tree)
480
481 refreshDocInfo(tree)
482}
483
484func (box *Box) renameSubTrees(tree *parse.Tree) {
485 subFiles := box.ListFiles(tree.Path)
486
487 luteEngine := util.NewLute()
488 for _, subFile := range subFiles {
489 if !strings.HasSuffix(subFile.path, ".sy") {
490 continue
491 }
492
493 subTree, err := filesys.LoadTree(box.ID, subFile.path, luteEngine) // LoadTree 会重新构造 HPath
494 if err != nil {
495 continue
496 }
497
498 treenode.SetBlockTreePath(subTree)
499 sql.RenameSubTreeQueue(subTree)
500 msg := fmt.Sprintf(Conf.Language(107), html.EscapeString(subTree.HPath))
501 util.PushStatusBar(msg)
502 }
503}
504
505func parseKTree(kramdown []byte) (ret *parse.Tree) {
506 luteEngine := NewLute()
507 ret = parse.Parse("", kramdown, luteEngine.ParseOptions)
508 normalizeTree(ret)
509 return
510}
511
512func normalizeTree(tree *parse.Tree) (yfmRootID, yfmTitle, yfmUpdated string) {
513 if nil == tree.Root.FirstChild {
514 tree.Root.AppendChild(treenode.NewParagraph(""))
515 } else if !tree.Root.FirstChild.IsBlock() || ast.NodeKramdownBlockIAL == tree.Root.FirstChild.Type {
516 tree.Root.PrependChild(treenode.NewParagraph(""))
517 }
518
519 var unlinks []*ast.Node
520 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
521 if !entering {
522 return ast.WalkContinue
523 }
524
525 if n.IsEmptyBlockIAL() {
526 // 空段落保留
527 p := &ast.Node{Type: ast.NodeParagraph}
528 p.KramdownIAL = parse.Tokens2IAL(n.Tokens)
529 p.ID = p.IALAttr("id")
530 n.InsertBefore(p)
531 return ast.WalkContinue
532 }
533
534 id := n.IALAttr("id")
535 if "" == id && n.IsBlock() {
536 n.SetIALAttr("id", n.ID)
537 }
538
539 if "" == n.IALAttr("id") && (ast.NodeParagraph == n.Type || ast.NodeList == n.Type || ast.NodeListItem == n.Type || ast.NodeBlockquote == n.Type ||
540 ast.NodeMathBlock == n.Type || ast.NodeCodeBlock == n.Type || ast.NodeHeading == n.Type || ast.NodeTable == n.Type || ast.NodeThematicBreak == n.Type ||
541 ast.NodeYamlFrontMatter == n.Type || ast.NodeBlockQueryEmbed == n.Type || ast.NodeSuperBlock == n.Type || ast.NodeAttributeView == n.Type ||
542 ast.NodeHTMLBlock == n.Type || ast.NodeIFrame == n.Type || ast.NodeWidget == n.Type || ast.NodeAudio == n.Type || ast.NodeVideo == n.Type) {
543 n.ID = ast.NewNodeID()
544 n.KramdownIAL = [][]string{{"id", n.ID}}
545 n.InsertAfter(&ast.Node{Type: ast.NodeKramdownBlockIAL, Tokens: []byte("{: id=\"" + n.ID + "\"}")})
546 n.SetIALAttr("updated", util.TimeFromID(n.ID))
547 }
548 if "" == n.ID && 0 < len(n.KramdownIAL) && ast.NodeDocument != n.Type {
549 n.ID = n.IALAttr("id")
550 }
551
552 if ast.NodeHTMLBlock == n.Type {
553 tokens := bytes.TrimSpace(n.Tokens)
554 if !bytes.HasPrefix(tokens, []byte("<div>")) {
555 tokens = []byte("<div>\n" + string(tokens))
556 }
557 if !bytes.HasSuffix(tokens, []byte("</div>")) {
558 tokens = append(tokens, []byte("\n</div>")...)
559 }
560 n.Tokens = tokens
561 return ast.WalkContinue
562 }
563
564 if ast.NodeInlineHTML == n.Type {
565 n.Type = ast.NodeText
566 return ast.WalkContinue
567 }
568
569 if ast.NodeParagraph == n.Type && nil != n.FirstChild && ast.NodeTaskListItemMarker == n.FirstChild.Type {
570 // 踢掉任务列表的第一个子节点左侧空格
571 n.FirstChild.Next.Tokens = bytes.TrimLeft(n.FirstChild.Next.Tokens, " ")
572 // 调整 li.p.tlim 为 li.tlim.p
573 n.InsertBefore(n.FirstChild)
574 }
575
576 if ast.NodeLinkTitle == n.Type {
577 // 避免重复转义图片标题内容 Repeat the escaped content of the image title https://github.com/siyuan-note/siyuan/issues/11681
578 n.Tokens = html.UnescapeBytes(n.Tokens)
579 }
580
581 if ast.NodeYamlFrontMatterContent == n.Type {
582 // Parsing YAML Front Matter as document custom attributes when importing Markdown files https://github.com/siyuan-note/siyuan/issues/10878
583 attrs := map[string]interface{}{}
584 parseErr := yaml.Unmarshal(n.Tokens, &attrs)
585 if parseErr != nil {
586 logging.LogWarnf("parse YAML front matter [%s] failed: %s", n.Tokens, parseErr)
587 return ast.WalkContinue
588 }
589
590 for attrK, attrV := range attrs {
591 // Improve parsing of YAML Front Matter when importing Markdown https://github.com/siyuan-note/siyuan/issues/12962
592 if "title" == attrK {
593 yfmTitle = fmt.Sprint(attrV)
594 tree.Root.SetIALAttr("title", yfmTitle)
595 continue
596 }
597 if "date" == attrK {
598 created, parseTimeErr := dateparse.ParseIn(fmt.Sprint(attrV), time.Local)
599 if nil == parseTimeErr {
600 yfmRootID = created.Format("20060102150405") + "-" + gulu.Rand.String(7)
601 tree.Root.ID = yfmRootID
602 tree.Root.SetIALAttr("id", yfmRootID)
603 }
604 continue
605 }
606 if "lastmod" == attrK {
607 updated, parseTimeErr := dateparse.ParseIn(fmt.Sprint(attrV), time.Local)
608 if nil == parseTimeErr {
609 yfmUpdated = updated.Format("20060102150405")
610 tree.Root.SetIALAttr("updated", yfmUpdated)
611 }
612 continue
613 }
614 if "tags" == attrK && nil != attrV {
615 var tags string
616 if str, ok := attrV.(string); ok {
617 tags = strings.TrimSpace(str)
618 tree.Root.SetIALAttr("tags", tags)
619 continue
620 }
621
622 for _, tag := range attrV.([]any) {
623 tagStr := fmt.Sprintf("%v", tag)
624 if "" == tag {
625 continue
626 }
627 tagStr = strings.TrimLeft(tagStr, "#,'\"")
628 tagStr = strings.TrimRight(tagStr, "#,'\"")
629 tags += tagStr + ","
630 }
631 tags = strings.TrimRight(tags, ",")
632 tags = strings.TrimSpace(tags)
633 if "" != tags {
634 tree.Root.SetIALAttr("tags", tags)
635 }
636 continue
637 }
638
639 validKeyName := true
640 for i := 0; i < len(attrK); i++ {
641 if !lex.IsASCIILetterNumHyphen(attrK[i]) {
642 validKeyName = false
643 break
644 }
645 }
646 if !validKeyName {
647 logging.LogWarnf("invalid YAML key [%s] in [%s]", attrK, n.ID)
648 continue
649 }
650
651 tree.Root.SetIALAttr("custom-"+attrK, fmt.Sprint(attrV))
652 }
653 }
654
655 if ast.NodeYamlFrontMatter == n.Type {
656 unlinks = append(unlinks, n)
657 }
658
659 return ast.WalkContinue
660 })
661 for _, n := range unlinks {
662 n.Unlink()
663 }
664
665 rootIAL := parse.Tokens2IAL(tree.Root.LastChild.Tokens)
666 for _, kv := range rootIAL {
667 tree.Root.SetIALAttr(kv[0], kv[1])
668 }
669 return
670}
671
672func VacuumDataIndex() {
673 util.PushEndlessProgress(Conf.language(270))
674 defer util.PushClearProgress()
675
676 var oldsyDbSize, newSyDbSize, oldHistoryDbSize, newHistoryDbSize, oldAssetContentDbSize, newAssetContentDbSize int64
677 info, _ := os.Stat(util.DBPath)
678 if nil != info {
679 oldsyDbSize = info.Size()
680 }
681 info, _ = os.Stat(util.HistoryDBPath)
682 if nil != info {
683 oldHistoryDbSize = info.Size()
684 }
685 info, _ = os.Stat(util.AssetContentDBPath)
686 if nil != info {
687 oldAssetContentDbSize = info.Size()
688 }
689
690 sql.Vacuum()
691
692 info, _ = os.Stat(util.DBPath)
693 if nil != info {
694 newSyDbSize = info.Size()
695 }
696 info, _ = os.Stat(util.HistoryDBPath)
697 if nil != info {
698 newHistoryDbSize = info.Size()
699 }
700 info, _ = os.Stat(util.AssetContentDBPath)
701 if nil != info {
702 newAssetContentDbSize = info.Size()
703 }
704
705 logging.LogInfof("vacuum database [siyuan.db: %s -> %s, history.db: %s -> %s, asset_content.db: %s -> %s]",
706 humanize.BytesCustomCeil(uint64(oldsyDbSize), 2), humanize.BytesCustomCeil(uint64(newSyDbSize), 2),
707 humanize.BytesCustomCeil(uint64(oldHistoryDbSize), 2), humanize.BytesCustomCeil(uint64(newHistoryDbSize), 2),
708 humanize.BytesCustomCeil(uint64(oldAssetContentDbSize), 2), humanize.BytesCustomCeil(uint64(newAssetContentDbSize), 2))
709
710 releaseSize := (oldsyDbSize - newSyDbSize) + (oldHistoryDbSize - newHistoryDbSize) + (oldAssetContentDbSize - newAssetContentDbSize)
711 msg := fmt.Sprintf(Conf.language(271), humanize.BytesCustomCeil(uint64(releaseSize), 2))
712 util.PushMsg(msg, 7000)
713}
714
715func FullReindex() {
716 task.AppendTask(task.DatabaseIndexFull, fullReindex)
717 task.AppendTask(task.DatabaseIndexRef, IndexRefs)
718 go func() {
719 sql.FlushQueue()
720 ResetVirtualBlockRefCache()
721 }()
722 task.AppendTaskWithTimeout(task.DatabaseIndexEmbedBlock, 30*time.Second, autoIndexEmbedBlock)
723 cache.ClearDocsIAL()
724 cache.ClearBlocksIAL()
725 task.AppendTask(task.ReloadUI, util.ReloadUI)
726}
727
728func fullReindex() {
729 pushSQLInsertBlocksFTSMsg, pushSQLDeleteBlocksMsg = true, true
730 defer func() {
731 sql.FlushQueue()
732 pushSQLInsertBlocksFTSMsg, pushSQLDeleteBlocksMsg = false, false
733 }()
734
735 util.PushEndlessProgress(Conf.language(35))
736 defer util.PushClearProgress()
737
738 FlushTxQueue()
739
740 if err := sql.InitDatabase(true); err != nil {
741 os.Exit(logging.ExitCodeReadOnlyDatabase)
742 return
743 }
744
745 sql.IndexIgnoreCached = false
746 openedBoxes := Conf.GetOpenedBoxes()
747 for _, openedBox := range openedBoxes {
748 indexBox(openedBox.ID)
749 }
750 LoadFlashcards()
751 debug.FreeOSMemory()
752}
753
754func ChangeBoxSort(boxIDs []string) {
755 for i, boxID := range boxIDs {
756 box := &Box{ID: boxID}
757 boxConf := box.GetConf()
758 boxConf.Sort = i + 1
759 box.SaveConf(boxConf)
760 }
761}
762
763func SetBoxIcon(boxID, icon string) {
764 if strings.Contains(icon, ".") {
765 // XSS through emoji name https://github.com/siyuan-note/siyuan/issues/15034
766 icon = util.FilterUploadEmojiFileName(icon)
767 }
768
769 box := &Box{ID: boxID}
770 boxConf := box.GetConf()
771 boxConf.Icon = icon
772 box.SaveConf(boxConf)
773}
774
775func (box *Box) UpdateHistoryGenerated() {
776 boxLatestHistoryTime[box.ID] = time.Now()
777}
778
779func getBoxesByPaths(paths []string) (ret map[string]*Box) {
780 ret = map[string]*Box{}
781 var ids []string
782 for _, p := range paths {
783 ids = append(ids, util.GetTreeID(p))
784 }
785
786 bts := treenode.GetBlockTrees(ids)
787 for _, id := range ids {
788 bt := bts[id]
789 if nil != bt {
790 ret[bt.Path] = Conf.Box(bt.BoxID)
791 }
792 }
793 return
794}