A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 794 lines 22 kB view raw
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}