A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 2117 lines 57 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 "errors" 21 "fmt" 22 "io/fs" 23 "os" 24 "path" 25 "path/filepath" 26 "sort" 27 "strconv" 28 "strings" 29 "sync" 30 "time" 31 "unicode/utf8" 32 33 "github.com/88250/go-humanize" 34 "github.com/88250/gulu" 35 "github.com/88250/lute" 36 "github.com/88250/lute/ast" 37 "github.com/88250/lute/html" 38 "github.com/88250/lute/parse" 39 util2 "github.com/88250/lute/util" 40 "github.com/siyuan-note/filelock" 41 "github.com/siyuan-note/logging" 42 "github.com/siyuan-note/riff" 43 "github.com/siyuan-note/siyuan/kernel/av" 44 "github.com/siyuan-note/siyuan/kernel/cache" 45 "github.com/siyuan-note/siyuan/kernel/filesys" 46 "github.com/siyuan-note/siyuan/kernel/search" 47 "github.com/siyuan-note/siyuan/kernel/sql" 48 "github.com/siyuan-note/siyuan/kernel/task" 49 "github.com/siyuan-note/siyuan/kernel/treenode" 50 "github.com/siyuan-note/siyuan/kernel/util" 51) 52 53type File struct { 54 Path string `json:"path"` 55 Name string `json:"name"` // 标题,即 ial["title"] 56 Icon string `json:"icon"` 57 Name1 string `json:"name1"` // 命名,即 ial["name"] 58 Alias string `json:"alias"` 59 Memo string `json:"memo"` 60 Bookmark string `json:"bookmark"` 61 ID string `json:"id"` 62 Count int `json:"count"` 63 Size uint64 `json:"size"` 64 HSize string `json:"hSize"` 65 Mtime int64 `json:"mtime"` 66 CTime int64 `json:"ctime"` 67 HMtime string `json:"hMtime"` 68 HCtime string `json:"hCtime"` 69 Sort int `json:"sort"` 70 SubFileCount int `json:"subFileCount"` 71 Hidden bool `json:"hidden"` 72 73 NewFlashcardCount int `json:"newFlashcardCount"` 74 DueFlashcardCount int `json:"dueFlashcardCount"` 75 FlashcardCount int `json:"flashcardCount"` 76} 77 78func (box *Box) docFromFileInfo(fileInfo *FileInfo, ial map[string]string) (ret *File) { 79 ret = &File{} 80 ret.Path = fileInfo.path 81 ret.Size = uint64(fileInfo.size) 82 ret.Name = ial["title"] + ".sy" 83 ret.Icon = ial["icon"] 84 ret.ID = ial["id"] 85 ret.Name1 = ial["name"] 86 ret.Alias = ial["alias"] 87 ret.Memo = ial["memo"] 88 ret.Bookmark = ial["bookmark"] 89 t, _ := time.ParseInLocation("20060102150405", ret.ID[:14], time.Local) 90 ret.CTime = t.Unix() 91 ret.HCtime = t.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(t, Conf.Lang) 92 ret.HSize = humanize.BytesCustomCeil(ret.Size, 2) 93 94 mTime := t 95 if updated := ial["updated"]; "" != updated { 96 if updatedTime, err := time.ParseInLocation("20060102150405", updated, time.Local); err == nil { 97 mTime = updatedTime 98 } 99 } 100 101 ret.Mtime = mTime.Unix() 102 ret.HMtime = mTime.Format("2006-01-02 15:04:05") + ", " + util.HumanizeTime(mTime, Conf.Lang) 103 return 104} 105 106func (box *Box) docIAL(p string) (ret map[string]string) { 107 name := strings.ToLower(filepath.Base(p)) 108 if !strings.HasSuffix(name, ".sy") { 109 return nil 110 } 111 112 ret = cache.GetDocIAL(p) 113 if nil != ret { 114 return ret 115 } 116 117 filePath := filepath.Join(util.DataDir, box.ID, p) 118 ret = filesys.DocIAL(filePath) 119 if 1 > len(ret) { 120 logging.LogWarnf("properties not found in file [%s]", filePath) 121 box.moveCorruptedData(filePath) 122 return nil 123 } 124 cache.PutDocIAL(p, ret) 125 return ret 126} 127 128func (box *Box) moveCorruptedData(filePath string) { 129 base := filepath.Base(filePath) 130 to := filepath.Join(util.WorkspaceDir, "corrupted", time.Now().Format("2006-01-02-150405"), box.ID, base) 131 if copyErr := filelock.Copy(filePath, to); nil != copyErr { 132 logging.LogErrorf("copy corrupted data file [%s] failed: %s", filePath, copyErr) 133 return 134 } 135 if removeErr := filelock.Remove(filePath); nil != removeErr { 136 logging.LogErrorf("remove corrupted data file [%s] failed: %s", filePath, removeErr) 137 return 138 } 139 logging.LogWarnf("moved corrupted data file [%s] to [%s]", filePath, to) 140} 141 142func SearchDocsByKeyword(keyword string, flashcard bool) (ret []map[string]string) { 143 ret = []map[string]string{} 144 145 var deck *riff.Deck 146 var deckBlockIDs []string 147 if flashcard { 148 deck = Decks[builtinDeckID] 149 if nil == deck { 150 return 151 } 152 153 deckBlockIDs = deck.GetBlockIDs() 154 } 155 156 openedBoxes := Conf.GetOpenedBoxes() 157 boxes := map[string]*Box{} 158 for _, box := range openedBoxes { 159 boxes[box.ID] = box 160 } 161 162 keywords := strings.Fields(keyword) 163 var rootBlocks []*sql.Block 164 if 0 < len(keywords) { 165 for _, box := range boxes { 166 if gulu.Str.Contains(box.Name, keywords) { 167 if flashcard { 168 newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs) 169 if 0 < flashcardCount { 170 ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)}) 171 } 172 } else { 173 ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon}) 174 } 175 } 176 } 177 178 var condition string 179 for i, k := range keywords { 180 condition += "(hpath LIKE '%" + k + "%'" 181 namCondition := Conf.Search.NAMFilter(k) 182 condition += " " + namCondition 183 condition += ")" 184 185 if i < len(keywords)-1 { 186 condition += " AND " 187 } 188 } 189 190 rootBlocks = sql.QueryRootBlockByCondition(condition, Conf.Search.Limit) 191 } else { 192 for _, box := range boxes { 193 if flashcard { 194 newFlashcardCount, dueFlashcardCount, flashcardCount := countBoxFlashcard(box.ID, deck, deckBlockIDs) 195 if 0 < flashcardCount { 196 ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)}) 197 } 198 } else { 199 ret = append(ret, map[string]string{"path": "/", "hPath": box.Name + "/", "box": box.ID, "boxIcon": box.Icon}) 200 } 201 } 202 } 203 204 for _, rootBlock := range rootBlocks { 205 b := boxes[rootBlock.Box] 206 if nil == b { 207 continue 208 } 209 hPath := b.Name + rootBlock.HPath 210 if flashcard { 211 newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootBlock.ID, deck, deckBlockIDs) 212 if 0 < flashcardCount { 213 ret = append(ret, map[string]string{"path": rootBlock.Path, "hPath": hPath, "box": rootBlock.Box, "boxIcon": b.Icon, "newFlashcardCount": strconv.Itoa(newFlashcardCount), "dueFlashcardCount": strconv.Itoa(dueFlashcardCount), "flashcardCount": strconv.Itoa(flashcardCount)}) 214 } 215 } else { 216 ret = append(ret, map[string]string{"path": rootBlock.Path, "hPath": hPath, "box": rootBlock.Box, "boxIcon": b.Icon}) 217 } 218 } 219 220 sort.Slice(ret, func(i, j int) bool { 221 return ret[i]["hPath"] < ret[j]["hPath"] 222 }) 223 return 224} 225 226type FileInfo struct { 227 path string 228 name string 229 size int64 230 isdir bool 231} 232 233func ListDocTree(boxID, listPath string, sortMode int, flashcard, showHidden bool, maxListCount int) (ret []*File, totals int, err error) { 234 //os.MkdirAll("pprof", 0755) 235 //cpuProfile, _ := os.Create("pprof/cpu_profile_list_doc_tree") 236 //pprof.StartCPUProfile(cpuProfile) 237 //defer pprof.StopCPUProfile() 238 239 ret = []*File{} 240 241 var deck *riff.Deck 242 var deckBlockIDs []string 243 if flashcard { 244 deck = Decks[builtinDeckID] 245 if nil == deck { 246 return 247 } 248 249 deckBlockIDs = deck.GetBlockIDs() 250 } 251 252 box := Conf.Box(boxID) 253 if nil == box { 254 return nil, 0, errors.New(Conf.Language(0)) 255 } 256 257 boxConf := box.GetConf() 258 259 if util.SortModeUnassigned == sortMode { 260 sortMode = Conf.FileTree.Sort 261 if util.SortModeFileTree != boxConf.SortMode { 262 sortMode = boxConf.SortMode 263 } 264 } 265 266 var files []*FileInfo 267 start := time.Now() 268 files, totals, err = box.Ls(listPath) 269 if err != nil { 270 return 271 } 272 elapsed := time.Now().Sub(start).Milliseconds() 273 if 100 < elapsed { 274 logging.LogWarnf("ls elapsed [%dms]", elapsed) 275 } 276 277 start = time.Now() 278 boxLocalPath := filepath.Join(util.DataDir, box.ID) 279 var docs []*File 280 for _, file := range files { 281 if file.isdir { 282 if !ast.IsNodeIDPattern(file.name) { 283 continue 284 } 285 286 parentDocPath := strings.TrimSuffix(file.path, "/") + ".sy" 287 parentDocFile := box.Stat(parentDocPath) 288 if nil == parentDocFile { 289 continue 290 } 291 if ial := box.docIAL(parentDocPath); nil != ial { 292 if !showHidden && "true" == ial["custom-hidden"] { 293 continue 294 } 295 296 doc := box.docFromFileInfo(parentDocFile, ial) 297 subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, file.path)) 298 if err == nil { 299 for _, subFile := range subFiles { 300 subDocFilePath := path.Join(file.path, subFile.Name()) 301 if subIAL := box.docIAL(subDocFilePath); "true" == subIAL["custom-hidden"] { 302 continue 303 } 304 305 if strings.HasSuffix(subFile.Name(), ".sy") { 306 doc.SubFileCount++ 307 } 308 } 309 } 310 311 if flashcard { 312 rootID := util.GetTreeID(parentDocPath) 313 newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs) 314 if 0 < flashcardCount { 315 doc.NewFlashcardCount = newFlashcardCount 316 doc.DueFlashcardCount = dueFlashcardCount 317 doc.FlashcardCount = flashcardCount 318 docs = append(docs, doc) 319 } 320 } else { 321 docs = append(docs, doc) 322 } 323 } 324 325 continue 326 } else { 327 if strings.HasSuffix(file.name, ".sy") && !ast.IsNodeIDPattern(strings.TrimSuffix(file.name, ".sy")) { 328 // 不以块 ID 命名的 .sy 文件不应该被加载到思源中 https://github.com/siyuan-note/siyuan/issues/16089 329 continue 330 } 331 } 332 333 subFolder := filepath.Join(boxLocalPath, strings.TrimSuffix(file.path, ".sy")) 334 if gulu.File.IsDir(subFolder) { 335 continue 336 } 337 338 if ial := box.docIAL(file.path); nil != ial { 339 if !showHidden && "true" == ial["custom-hidden"] { 340 continue 341 } 342 343 doc := box.docFromFileInfo(file, ial) 344 345 if flashcard { 346 rootID := util.GetTreeID(file.path) 347 newFlashcardCount, dueFlashcardCount, flashcardCount := countTreeFlashcard(rootID, deck, deckBlockIDs) 348 if 0 < flashcardCount { 349 doc.NewFlashcardCount = newFlashcardCount 350 doc.DueFlashcardCount = dueFlashcardCount 351 doc.FlashcardCount = flashcardCount 352 docs = append(docs, doc) 353 } 354 } else { 355 docs = append(docs, doc) 356 } 357 } 358 } 359 elapsed = time.Now().Sub(start).Milliseconds() 360 if 500 < elapsed { 361 logging.LogWarnf("list doc tree [%s] build docs [%d] elapsed [%dms]", listPath, len(docs), elapsed) 362 } 363 364 start = time.Now() 365 refCount := sql.QueryRootBlockRefCount() 366 for _, doc := range docs { 367 if count := refCount[doc.ID]; 0 < count { 368 doc.Count = count 369 } 370 } 371 elapsed = time.Now().Sub(start).Milliseconds() 372 if 500 < elapsed { 373 logging.LogWarnf("query root block ref count elapsed [%dms]", elapsed) 374 } 375 376 start = time.Now() 377 switch sortMode { 378 case util.SortModeNameASC: 379 sort.Slice(docs, func(i, j int) bool { 380 return util.PinYinCompare4FileTree(docs[i].Name, docs[j].Name) 381 }) 382 case util.SortModeNameDESC: 383 sort.Slice(docs, func(i, j int) bool { 384 return util.PinYinCompare4FileTree(docs[j].Name, docs[i].Name) 385 }) 386 case util.SortModeUpdatedASC: 387 sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime < docs[j].Mtime }) 388 case util.SortModeUpdatedDESC: 389 sort.Slice(docs, func(i, j int) bool { return docs[i].Mtime > docs[j].Mtime }) 390 case util.SortModeAlphanumASC: 391 sort.Slice(docs, func(i, j int) bool { 392 return util.NaturalCompare(docs[i].Name, docs[j].Name) 393 }) 394 case util.SortModeAlphanumDESC: 395 sort.Slice(docs, func(i, j int) bool { 396 return util.NaturalCompare(docs[j].Name, docs[i].Name) 397 }) 398 case util.SortModeCustom: 399 fileTreeFiles := docs 400 box.fillSort(&fileTreeFiles) 401 sort.Slice(fileTreeFiles, func(i, j int) bool { 402 if fileTreeFiles[i].Sort == fileTreeFiles[j].Sort { 403 return util.TimeFromID(fileTreeFiles[i].ID) > util.TimeFromID(fileTreeFiles[j].ID) 404 } 405 return fileTreeFiles[i].Sort < fileTreeFiles[j].Sort 406 }) 407 ret = append(ret, fileTreeFiles...) 408 totals = len(ret) 409 if maxListCount < len(ret) { 410 ret = ret[:maxListCount] 411 } 412 ret = ret[:] 413 return 414 case util.SortModeRefCountASC: 415 sort.Slice(docs, func(i, j int) bool { return docs[i].Count < docs[j].Count }) 416 case util.SortModeRefCountDESC: 417 sort.Slice(docs, func(i, j int) bool { return docs[i].Count > docs[j].Count }) 418 case util.SortModeCreatedASC: 419 sort.Slice(docs, func(i, j int) bool { return docs[i].CTime < docs[j].CTime }) 420 case util.SortModeCreatedDESC: 421 sort.Slice(docs, func(i, j int) bool { return docs[i].CTime > docs[j].CTime }) 422 case util.SortModeSizeASC: 423 sort.Slice(docs, func(i, j int) bool { return docs[i].Size < docs[j].Size }) 424 case util.SortModeSizeDESC: 425 sort.Slice(docs, func(i, j int) bool { return docs[i].Size > docs[j].Size }) 426 case util.SortModeSubDocCountASC: 427 sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount < docs[j].SubFileCount }) 428 case util.SortModeSubDocCountDESC: 429 sort.Slice(docs, func(i, j int) bool { return docs[i].SubFileCount > docs[j].SubFileCount }) 430 } 431 432 if util.SortModeCustom != sortMode { 433 ret = append(ret, docs...) 434 } 435 436 totals = len(ret) 437 if maxListCount < len(ret) { 438 ret = ret[:maxListCount] 439 } 440 ret = ret[:] 441 442 elapsed = time.Now().Sub(start).Milliseconds() 443 if 200 < elapsed { 444 logging.LogInfof("sort docs elapsed [%dms]", elapsed) 445 } 446 return 447} 448 449func GetDoc(startID, endID, id string, index int, query string, queryTypes map[string]bool, queryMethod, mode int, size int, isBacklink bool, originalRefBlockIDs map[string]string, highlight bool) ( 450 blockCount int, dom, parentID, parent2ID, rootID, typ string, eof, scroll bool, boxID, docPath string, isBacklinkExpand bool, keywords []string, err error) { 451 //os.MkdirAll("pprof", 0755) 452 //cpuProfile, _ := os.Create("pprof/GetDoc") 453 //pprof.StartCPUProfile(cpuProfile) 454 //defer pprof.StopCPUProfile() 455 456 FlushTxQueue() // 写入数据时阻塞,避免获取到的数据不一致 457 458 inputIndex := index 459 tree, err := LoadTreeByBlockID(id) 460 if err != nil { 461 if ErrBlockNotFound == err { 462 if 0 == mode { 463 err = ErrTreeNotFound // 初始化打开文档时如果找不到则关闭编辑器 464 } 465 } 466 return 467 } 468 if nil == tree { 469 err = ErrBlockNotFound 470 return 471 } 472 473 luteEngine := NewLute() 474 node := treenode.GetNodeInTree(tree, id) 475 if nil == node { 476 // Unable to open the doc when the block pointed by the scroll position does not exist https://github.com/siyuan-note/siyuan/issues/9030 477 node = treenode.GetNodeInTree(tree, tree.Root.ID) 478 if nil == node { 479 err = ErrBlockNotFound 480 return 481 } 482 } 483 484 located := false 485 isDoc := ast.NodeDocument == node.Type 486 isHeading := ast.NodeHeading == node.Type 487 488 boxID = node.Box 489 docPath = node.Path 490 if isDoc { 491 if 4 == mode { // 加载文档末尾 492 node = node.LastChild 493 located = true 494 // 重新计算 index 495 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 496 if !entering { 497 return ast.WalkContinue 498 } 499 500 index++ 501 return ast.WalkContinue 502 }) 503 } else { 504 node = node.FirstChild 505 } 506 typ = ast.NodeDocument.String() 507 idx := 0 508 if 0 < index { 509 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 510 if !entering || !n.IsChildBlockOf(tree.Root, 1) { 511 return ast.WalkContinue 512 } 513 514 idx++ 515 if index == idx { 516 node = n.DocChild() 517 if "1" == node.IALAttr("heading-fold") { 518 // 加载到折叠标题下方块的话需要回溯到上方标题块 519 for h := node.Previous; nil != h; h = h.Previous { 520 if "1" == h.IALAttr("fold") { 521 node = h 522 break 523 } 524 } 525 } 526 located = true 527 return ast.WalkStop 528 } 529 return ast.WalkContinue 530 }) 531 } 532 } else { 533 if 0 == index && 0 != mode { 534 // 非文档且没有指定 index 时需要计算 index 535 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 536 if !entering { 537 return ast.WalkContinue 538 } 539 540 index++ 541 if id == n.ID { 542 node = n.DocChild() 543 located = true 544 return ast.WalkStop 545 } 546 return ast.WalkContinue 547 }) 548 } 549 } 550 551 if 1 < index && !located { 552 count := 0 553 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 554 if !entering { 555 return ast.WalkContinue 556 } 557 558 count++ 559 if index == count { 560 node = n.DocChild() 561 return ast.WalkStop 562 } 563 return ast.WalkContinue 564 }) 565 } 566 567 blockCount = tree.DocBlockCount() 568 if ast.NodeDocument == node.Type { 569 parentID = node.ID 570 parent2ID = parentID 571 } else { 572 parentID = node.Parent.ID 573 parent2ID = parentID 574 tmp := node 575 if ast.NodeListItem == node.Type { 576 // 列表项聚焦返回和面包屑保持一致 https://github.com/siyuan-note/siyuan/issues/4914 577 tmp = node.Parent 578 } 579 if headingParent := treenode.HeadingParent(tmp); nil != headingParent { 580 parent2ID = headingParent.ID 581 } 582 } 583 rootID = tree.Root.ID 584 if !isDoc { 585 typ = node.Type.String() 586 } 587 588 // 判断是否需要显示动态加载滚动条 https://github.com/siyuan-note/siyuan/issues/7693 589 childCount := 0 590 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 591 if !entering { 592 return ast.WalkContinue 593 } 594 595 if 1 > childCount { 596 childCount = 1 597 } else { 598 childCount += treenode.CountBlockNodes(n) 599 } 600 601 if childCount > Conf.Editor.DynamicLoadBlocks { 602 scroll = true 603 return ast.WalkStop 604 } 605 return ast.WalkContinue 606 }) 607 608 var nodes []*ast.Node 609 if isBacklink { 610 // 引用计数浮窗请求,需要按照反链逻辑组装 https://github.com/siyuan-note/siyuan/issues/6853 611 nodes, isBacklinkExpand = getBacklinkRenderNodes(node, originalRefBlockIDs) 612 } else { 613 // 如果同时存在 startID 和 endID,并且是动态加载的情况,则只加载 startID 和 endID 之间的块 [startID, endID] 614 if "" != startID && "" != endID && scroll { 615 nodes, eof = loadNodesByStartEnd(tree, startID, endID) 616 if 1 > len(nodes) { 617 // 按 mode 加载兜底 618 nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading) 619 } else { 620 // 文档块没有指定 index 时需要计算 index,否则初次打开文档时 node-index 会为 0,导致首次 Ctrl+Home 无法回到顶部 621 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 622 if !entering { 623 return ast.WalkContinue 624 } 625 626 index++ 627 if nodes[0].ID == n.ID { 628 return ast.WalkStop 629 } 630 return ast.WalkContinue 631 }) 632 } 633 } else { 634 nodes, eof = loadNodesByMode(node, inputIndex, mode, size, isDoc, isHeading) 635 } 636 } 637 638 refCount := sql.QueryRootChildrenRefCount(rootID) 639 virtualBlockRefKeywords := getBlockVirtualRefKeywords(tree.Root) 640 641 subTree := &parse.Tree{ID: rootID, Root: &ast.Node{Type: ast.NodeDocument}, Marks: tree.Marks} 642 643 query = filterQueryInvisibleChars(query) 644 if "" != query && (0 == queryMethod || 1 == queryMethod || 3 == queryMethod) { // 只有关键字、查询语法和正则表达式搜索支持高亮 645 typeFilter := buildTypeFilter(queryTypes) 646 switch queryMethod { 647 case 0: 648 query = stringQuery(query) 649 keywords = highlightByFTS(query, typeFilter, rootID) 650 case 1: 651 keywords = highlightByFTS(query, typeFilter, rootID) 652 case 3: 653 keywords = highlightByRegexp(query, typeFilter, rootID) 654 } 655 } 656 657 existKeywords := 0 < len(keywords) 658 for _, n := range nodes { 659 var unlinks []*ast.Node 660 ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus { 661 if !entering { 662 return ast.WalkContinue 663 } 664 665 if "1" == n.IALAttr("heading-fold") { 666 // 折叠标题下被引用的块无法悬浮查看 667 // The referenced block under the folded heading cannot be hovered to view https://github.com/siyuan-note/siyuan/issues/9582 668 if (0 != mode && id != n.ID) || isDoc { 669 unlinks = append(unlinks, n) 670 return ast.WalkContinue 671 } 672 } 673 674 if avs := n.IALAttr(av.NodeAttrNameAvs); "" != avs { 675 // 填充属性视图角标 Display the database title on the block superscript https://github.com/siyuan-note/siyuan/issues/10545 676 avNames := getAvNames(n.IALAttr(av.NodeAttrNameAvs)) 677 if "" != avNames { 678 n.SetIALAttr(av.NodeAttrViewNames, avNames) 679 } 680 } 681 682 if "" != n.ID { 683 // 填充块引计数 684 if cnt := refCount[n.ID]; 0 < cnt { 685 n.SetIALAttr("refcount", strconv.Itoa(cnt)) 686 } 687 } 688 689 if highlight && existKeywords { 690 hitBlock := false 691 for p := n.Parent; nil != p; p = p.Parent { 692 if p.ID == id { 693 hitBlock = true 694 break 695 } 696 } 697 if hitBlock { 698 if ast.NodeCodeBlockCode == n.Type && !treenode.IsChartCodeBlockCode(n) { 699 // 支持代码块搜索定位 https://github.com/siyuan-note/siyuan/issues/5520 700 code := string(n.Tokens) 701 markedCode := search.EncloseHighlighting(code, keywords, search.SearchMarkLeft, search.SearchMarkRight, Conf.Search.CaseSensitive, false) 702 if code != markedCode { 703 n.Tokens = gulu.Str.ToBytes(markedCode) 704 return ast.WalkContinue 705 } 706 } else if markReplaceSpan(n, &unlinks, keywords, search.MarkDataType, luteEngine) { 707 return ast.WalkContinue 708 } 709 } 710 } 711 712 if existKeywords && id == n.ID { 713 inlines := n.ChildrenByType(ast.NodeTextMark) 714 for _, inline := range inlines { 715 if inline.IsTextMarkType("inline-memo") && util.ContainsSubStr(inline.TextMarkInlineMemoContent, keywords) { 716 // 支持行级备注搜索定位 https://github.com/siyuan-note/siyuan/issues/13465 717 keywords = append(keywords, inline.TextMarkTextContent) 718 } 719 } 720 } 721 722 if processVirtualRef(n, &unlinks, virtualBlockRefKeywords, refCount, luteEngine) { 723 return ast.WalkContinue 724 } 725 return ast.WalkContinue 726 }) 727 728 for _, unlink := range unlinks { 729 unlink.Unlink() 730 } 731 732 subTree.Root.AppendChild(n) 733 } 734 735 luteEngine.RenderOptions.NodeIndexStart = index 736 dom = luteEngine.Tree2BlockDOM(subTree, luteEngine.RenderOptions) 737 738 if 1 > len(keywords) { 739 keywords = []string{} 740 } 741 for i, keyword := range keywords { 742 keyword = strings.TrimPrefix(keyword, "#") 743 keyword = strings.TrimSuffix(keyword, "#") 744 keywords[i] = keyword 745 } 746 keywords = gulu.Str.RemoveDuplicatedElem(keywords) 747 748 go setRecentDocByTree(tree) 749 return 750} 751 752func loadNodesByStartEnd(tree *parse.Tree, startID, endID string) (nodes []*ast.Node, eof bool) { 753 node := treenode.GetNodeInTree(tree, startID) 754 if nil == node { 755 return 756 } 757 nodes = append(nodes, node) 758 for n := node.Next; nil != n; n = n.Next { 759 if treenode.IsInFoldedHeading(n, nil) { 760 continue 761 } 762 nodes = append(nodes, n) 763 764 if n.ID == endID { 765 if next := n.Next; nil == next { 766 eof = true 767 } else { 768 eof = util2.IsDocIAL(n.Tokens) || util2.IsDocIAL(next.Tokens) 769 } 770 break 771 } 772 773 if len(nodes) >= Conf.Editor.DynamicLoadBlocks { 774 // 如果加载到指定数量的块则停止加载 775 break 776 } 777 } 778 return 779} 780 781func loadNodesByMode(node *ast.Node, inputIndex, mode, size int, isDoc, isHeading bool) (nodes []*ast.Node, eof bool) { 782 if 2 == mode /* 向下 */ { 783 next := node.Next 784 if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") { 785 // 标题展开时进行动态加载导致重复内容 https://github.com/siyuan-note/siyuan/issues/4671 786 // 这里要考虑折叠标题是最后一个块的情况 787 if children := treenode.HeadingChildren(node); 0 < len(children) { 788 next = children[len(children)-1].Next 789 } 790 } 791 if nil == next { 792 eof = true 793 } else { 794 eof = util2.IsDocIAL(node.Tokens) || util2.IsDocIAL(next.Tokens) 795 } 796 } 797 798 count := 0 799 switch mode { 800 case 0: // 仅加载当前 ID 801 nodes = append(nodes, node) 802 if isDoc { 803 for n := node.Next; nil != n; n = n.Next { 804 if treenode.IsInFoldedHeading(n, nil) { 805 continue 806 } 807 nodes = append(nodes, n) 808 if 1 > count { 809 count++ 810 } else { 811 count += treenode.CountBlockNodes(n) 812 } 813 if size < count { 814 break 815 } 816 } 817 } else if isHeading { 818 level := node.HeadingLevel 819 for n := node.Next; nil != n; n = n.Next { 820 if treenode.IsInFoldedHeading(n, node) { 821 // 大纲点击折叠标题跳转聚焦 https://github.com/siyuan-note/siyuan/issues/4920 822 // 多级标题折叠后上级块引浮窗中未折叠 https://github.com/siyuan-note/siyuan/issues/4997 823 continue 824 } 825 if ast.NodeHeading == n.Type { 826 if n.HeadingLevel <= level { 827 break 828 } 829 } 830 nodes = append(nodes, n) 831 count++ 832 if size < count { 833 break 834 } 835 } 836 } 837 case 4: // Ctrl+End 跳转到末尾后向上加载 838 for n := node; nil != n; n = n.Previous { 839 if treenode.IsInFoldedHeading(n, nil) { 840 continue 841 } 842 nodes = append([]*ast.Node{n}, nodes...) 843 if 1 > count { 844 count++ 845 } else { 846 count += treenode.CountBlockNodes(n) 847 } 848 if size < count { 849 break 850 } 851 } 852 eof = true 853 case 1: // 向上加载 854 for n := node.Previous; /* 从上一个节点开始加载 */ nil != n; n = n.Previous { 855 if treenode.IsInFoldedHeading(n, nil) { 856 continue 857 } 858 nodes = append([]*ast.Node{n}, nodes...) 859 if 1 > count { 860 count++ 861 } else { 862 count += treenode.CountBlockNodes(n) 863 } 864 if size < count { 865 break 866 } 867 } 868 eof = nil == node.Previous 869 case 2: // 向下加载 870 for n := node.Next; /* 从下一个节点开始加载 */ nil != n; n = n.Next { 871 if treenode.IsInFoldedHeading(n, node) { 872 continue 873 } 874 nodes = append(nodes, n) 875 if 1 > count { 876 count++ 877 } else { 878 count += treenode.CountBlockNodes(n) 879 } 880 if size < count { 881 break 882 } 883 } 884 case 3: // 上下都加载 885 for n := node; nil != n; n = n.Previous { 886 if treenode.IsInFoldedHeading(n, nil) { 887 continue 888 } 889 nodes = append([]*ast.Node{n}, nodes...) 890 if 1 > count { 891 count++ 892 } else { 893 count += treenode.CountBlockNodes(n) 894 } 895 if 0 < inputIndex { 896 if 1 < count { 897 break // 滑块指示器加载 898 } 899 } else { 900 if size < count { 901 break 902 } 903 } 904 } 905 if size/2 < count { 906 size = size / 2 907 } else { 908 size = size - count 909 } 910 count = 0 911 for n := node.Next; nil != n; n = n.Next { 912 if treenode.IsInFoldedHeading(n, nil) { 913 continue 914 } 915 nodes = append(nodes, n) 916 if 1 > count { 917 count++ 918 } else { 919 count += treenode.CountBlockNodes(n) 920 } 921 if 0 < inputIndex { 922 if size < count { 923 break 924 } 925 } else { 926 if size < count { 927 break 928 } 929 } 930 } 931 } 932 return 933} 934 935func writeTreeUpsertQueue(tree *parse.Tree) (err error) { 936 size, err := filesys.WriteTree(tree) 937 if err != nil { 938 return 939 } 940 sql.UpsertTreeQueue(tree) 941 refreshDocInfoWithSize(tree, size) 942 return 943} 944 945func indexWriteTreeIndexQueue(tree *parse.Tree) (err error) { 946 treenode.IndexBlockTree(tree) 947 _, err = filesys.WriteTree(tree) 948 if err != nil { 949 return 950 } 951 sql.IndexTreeQueue(tree) 952 return 953} 954 955func indexWriteTreeUpsertQueue(tree *parse.Tree) (err error) { 956 treenode.UpsertBlockTree(tree) 957 return writeTreeUpsertQueue(tree) 958} 959 960func renameWriteJSONQueue(tree *parse.Tree) (err error) { 961 size, err := filesys.WriteTree(tree) 962 if err != nil { 963 return 964 } 965 sql.RenameTreeQueue(tree) 966 treenode.UpsertBlockTree(tree) 967 refreshDocInfoWithSize(tree, size) 968 return 969} 970 971func DuplicateDoc(tree *parse.Tree) { 972 msgId := util.PushMsg(Conf.Language(116), 30000) 973 defer util.PushClearMsg(msgId) 974 975 previousPath := tree.Path 976 resetTree(tree, "Duplicated", false) 977 createTreeTx(tree) 978 box := Conf.Box(tree.Box) 979 if nil != box { 980 box.addSort(previousPath, tree.ID) 981 } 982 FlushTxQueue() 983 984 // 复制为副本时将该副本块插入到数据库中 https://github.com/siyuan-note/siyuan/issues/11959 985 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 986 if !entering || !n.IsBlock() { 987 return ast.WalkContinue 988 } 989 990 avs := n.IALAttr(av.NodeAttrNameAvs) 991 for _, avID := range strings.Split(avs, ",") { 992 if !ast.IsNodeIDPattern(avID) { 993 continue 994 } 995 996 AddAttributeViewBlock(nil, []map[string]interface{}{{ 997 "id": n.ID, 998 "isDetached": false, 999 }}, avID, "", "", "", "", false, map[string]interface{}{}) 1000 ReloadAttrView(avID) 1001 } 1002 return ast.WalkContinue 1003 }) 1004 return 1005} 1006 1007func createTreeTx(tree *parse.Tree) { 1008 transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}} 1009 PerformTransactions(&[]*Transaction{transaction}) 1010} 1011 1012var createDocLock = sync.Mutex{} 1013 1014func CreateDocByMd(boxID, p, title, md string, sorts []string) (tree *parse.Tree, err error) { 1015 createDocLock.Lock() 1016 defer createDocLock.Unlock() 1017 1018 box := Conf.Box(boxID) 1019 if nil == box { 1020 err = errors.New(Conf.Language(0)) 1021 return 1022 } 1023 1024 luteEngine := util.NewLute() 1025 dom := luteEngine.Md2BlockDOM(md, false) 1026 tree, err = createDoc(box.ID, p, title, dom) 1027 if err != nil { 1028 return 1029 } 1030 1031 FlushTxQueue() 1032 if 0 < len(sorts) { 1033 ChangeFileTreeSort(box.ID, sorts) 1034 } else { 1035 box.addMinSort(path.Dir(tree.Path), tree.ID) 1036 } 1037 return 1038} 1039 1040func CreateWithMarkdown(tags, boxID, hPath, md, parentID, id string, withMath bool, clippingHref string) (retID string, err error) { 1041 createDocLock.Lock() 1042 defer createDocLock.Unlock() 1043 1044 box := Conf.Box(boxID) 1045 if nil == box { 1046 err = errors.New(Conf.Language(0)) 1047 return 1048 } 1049 1050 FlushTxQueue() 1051 luteEngine := util.NewLute() 1052 if withMath { 1053 luteEngine.SetInlineMath(true) 1054 } 1055 luteEngine.SetHTMLTag2TextMark(true) 1056 if strings.HasPrefix(clippingHref, "https://ld246.com/article/") || strings.HasPrefix(clippingHref, "https://liuyun.io/article/") { 1057 // 改进链滴剪藏 https://github.com/siyuan-note/siyuan/issues/13117 1058 enableLuteInlineSyntax(luteEngine) 1059 } 1060 dom := luteEngine.Md2BlockDOM(md, false) 1061 retID, err = createDocsByHPath(box.ID, hPath, dom, parentID, id) 1062 1063 nameValues := map[string]string{} 1064 tags = strings.TrimSpace(tags) 1065 tags = strings.ReplaceAll(tags, ",", ",") 1066 tagArray := strings.Split(tags, ",") 1067 var tmp []string 1068 for _, tag := range tagArray { 1069 tmp = append(tmp, strings.TrimSpace(tag)) 1070 } 1071 tags = strings.Join(tmp, ",") 1072 nameValues["tags"] = tags 1073 SetBlockAttrs(retID, nameValues) 1074 1075 FlushTxQueue() 1076 box.addMinSort(path.Dir(hPath), retID) 1077 return 1078} 1079 1080func CreateDailyNote(boxID string) (p string, existed bool, err error) { 1081 createDocLock.Lock() 1082 defer createDocLock.Unlock() 1083 1084 box := Conf.Box(boxID) 1085 if nil == box { 1086 err = ErrBoxNotFound 1087 return 1088 } 1089 1090 boxConf := box.GetConf() 1091 if "" == boxConf.DailyNoteSavePath || "/" == boxConf.DailyNoteSavePath { 1092 err = errors.New(Conf.Language(49)) 1093 return 1094 } 1095 1096 hPath, err := RenderGoTemplate(boxConf.DailyNoteSavePath) 1097 if err != nil { 1098 return 1099 } 1100 1101 FlushTxQueue() 1102 1103 hPath = util.TrimSpaceInPath(hPath) 1104 existRoot := treenode.GetBlockTreeRootByHPath(box.ID, hPath) 1105 if nil != existRoot { 1106 existed = true 1107 p = existRoot.Path 1108 1109 tree, loadErr := LoadTreeByBlockID(existRoot.RootID) 1110 if nil != loadErr { 1111 logging.LogWarnf("load tree by block id [%s] failed: %v", existRoot.RootID, loadErr) 1112 return 1113 } 1114 p = tree.Path 1115 date := time.Now().Format("20060102") 1116 if tree.Root.IALAttr("custom-dailynote-"+date) == "" { 1117 tree.Root.SetIALAttr("custom-dailynote-"+date, date) 1118 if err = indexWriteTreeUpsertQueue(tree); err != nil { 1119 return 1120 } 1121 } 1122 return 1123 } 1124 1125 id, err := createDocsByHPath(box.ID, hPath, "", "", "") 1126 if err != nil { 1127 return 1128 } 1129 1130 var templateTree *parse.Tree 1131 var templateDom string 1132 if "" != boxConf.DailyNoteTemplatePath { 1133 tplPath := filepath.Join(util.DataDir, "templates", boxConf.DailyNoteTemplatePath) 1134 if !filelock.IsExist(tplPath) { 1135 logging.LogWarnf("not found daily note template [%s]", tplPath) 1136 } else { 1137 var renderErr error 1138 templateTree, templateDom, renderErr = RenderTemplate(tplPath, id, false) 1139 if nil != renderErr { 1140 logging.LogWarnf("render daily note template [%s] failed: %s", boxConf.DailyNoteTemplatePath, err) 1141 } 1142 } 1143 } 1144 if "" != templateDom { 1145 var tree *parse.Tree 1146 tree, err = LoadTreeByBlockID(id) 1147 if err == nil { 1148 tree.Root.FirstChild.Unlink() 1149 1150 luteEngine := util.NewLute() 1151 newTree := luteEngine.BlockDOM2Tree(templateDom) 1152 var children []*ast.Node 1153 for c := newTree.Root.FirstChild; nil != c; c = c.Next { 1154 children = append(children, c) 1155 } 1156 for _, c := range children { 1157 tree.Root.AppendChild(c) 1158 } 1159 1160 // Creating a dailynote template supports doc attributes https://github.com/siyuan-note/siyuan/issues/10698 1161 templateIALs := parse.IAL2Map(templateTree.Root.KramdownIAL) 1162 for k, v := range templateIALs { 1163 if "name" == k || "alias" == k || "bookmark" == k || "memo" == k || "icon" == k || strings.HasPrefix(k, "custom-") { 1164 tree.Root.SetIALAttr(k, v) 1165 } 1166 } 1167 1168 tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr()) 1169 if err = indexWriteTreeUpsertQueue(tree); err != nil { 1170 return 1171 } 1172 } 1173 } 1174 IncSync() 1175 1176 FlushTxQueue() 1177 1178 tree, err := LoadTreeByBlockID(id) 1179 if err != nil { 1180 logging.LogErrorf("load tree by block id [%s] failed: %v", id, err) 1181 return 1182 } 1183 p = tree.Path 1184 date := time.Now().Format("20060102") 1185 tree.Root.SetIALAttr("custom-dailynote-"+date, date) 1186 if err = indexWriteTreeUpsertQueue(tree); err != nil { 1187 return 1188 } 1189 1190 return 1191} 1192 1193func GetHPathByPath(boxID, p string) (hPath string, err error) { 1194 if "/" == p { 1195 hPath = "/" 1196 return 1197 } 1198 1199 luteEngine := util.NewLute() 1200 tree, err := filesys.LoadTree(boxID, p, luteEngine) 1201 if err != nil { 1202 return 1203 } 1204 hPath = tree.HPath 1205 return 1206} 1207 1208func GetHPathsByPaths(paths []string) (hPaths []string, err error) { 1209 pathsBoxes := getBoxesByPaths(paths) 1210 for p, box := range pathsBoxes { 1211 if nil == box { 1212 logging.LogWarnf("box not found by path [%s]", p) 1213 continue 1214 } 1215 1216 bt := treenode.GetBlockTreeByPath(p) 1217 if nil == bt { 1218 logging.LogWarnf("block tree not found by path [%s]", p) 1219 continue 1220 } 1221 1222 hpath := html.UnescapeString(bt.HPath) 1223 hPaths = append(hPaths, box.Name+hpath) 1224 } 1225 return 1226} 1227 1228func GetHPathByID(id string) (hPath string, err error) { 1229 tree, err := LoadTreeByBlockID(id) 1230 if err != nil { 1231 return 1232 } 1233 hPath = tree.HPath 1234 return 1235} 1236 1237func GetPathByID(id string) (path, boxID string, err error) { 1238 tree, err := LoadTreeByBlockID(id) 1239 if err != nil { 1240 return 1241 } 1242 1243 path = tree.Path 1244 boxID = tree.Box 1245 return 1246} 1247 1248func GetFullHPathByID(id string) (hPath string, err error) { 1249 tree, err := LoadTreeByBlockID(id) 1250 if err != nil { 1251 return 1252 } 1253 1254 box := Conf.Box(tree.Box) 1255 if nil == box { 1256 err = ErrBoxNotFound 1257 return 1258 } 1259 hPath = box.Name + tree.HPath 1260 return 1261} 1262 1263func GetIDsByHPath(hpath, boxID string) (ret []string, err error) { 1264 ret = []string{} 1265 roots := treenode.GetBlockTreeRootsByHPath(boxID, hpath) 1266 if 1 > len(roots) { 1267 return 1268 } 1269 1270 for _, root := range roots { 1271 ret = append(ret, root.ID) 1272 } 1273 ret = gulu.Str.RemoveDuplicatedElem(ret) 1274 if 1 > len(ret) { 1275 ret = []string{} 1276 } 1277 return 1278} 1279 1280func MoveDocs(fromPaths []string, toBoxID, toPath string, callback interface{}) (err error) { 1281 toBox := Conf.Box(toBoxID) 1282 if nil == toBox { 1283 err = errors.New(Conf.Language(0)) 1284 return 1285 } 1286 1287 fromPaths = util.FilterMoveDocFromPaths(fromPaths, toPath) 1288 if 1 > len(fromPaths) { 1289 return 1290 } 1291 1292 pathsBoxes := getBoxesByPaths(fromPaths) 1293 1294 if 1 == len(fromPaths) { 1295 // 移动到自己的父文档下的情况相当于不移动,直接返回 1296 if fromBox := pathsBoxes[fromPaths[0]]; nil != fromBox && fromBox.ID == toBoxID { 1297 parentDir := path.Dir(fromPaths[0]) 1298 if ("/" == toPath && "/" == parentDir) || (parentDir+".sy" == toPath) { 1299 return 1300 } 1301 } 1302 } 1303 1304 // 检查路径深度是否超过限制 1305 for fromPath, fromBox := range pathsBoxes { 1306 childDepth := util.GetChildDocDepth(filepath.Join(util.DataDir, fromBox.ID, fromPath)) 1307 if depth := strings.Count(toPath, "/") + childDepth; 6 < depth && !Conf.FileTree.AllowCreateDeeper { 1308 err = errors.New(Conf.Language(118)) 1309 return 1310 } 1311 } 1312 1313 // A progress layer appears when moving more than 64 documents at once https://github.com/siyuan-note/siyuan/issues/9356 1314 subDocsCount := 0 1315 for fromPath, fromBox := range pathsBoxes { 1316 subDocsCount += countSubDocs(fromBox.ID, fromPath) 1317 } 1318 needShowProgress := 64 < subDocsCount 1319 if needShowProgress { 1320 defer util.PushClearProgress() 1321 } 1322 1323 FlushTxQueue() 1324 luteEngine := util.NewLute() 1325 count := 0 1326 for fromPath, fromBox := range pathsBoxes { 1327 count++ 1328 if needShowProgress { 1329 util.PushEndlessProgress(fmt.Sprintf(Conf.Language(70), fmt.Sprintf("%d/%d", count, len(fromPaths)))) 1330 } 1331 1332 _, err = moveDoc(fromBox, fromPath, toBox, toPath, luteEngine, callback) 1333 if err != nil { 1334 return 1335 } 1336 } 1337 cache.ClearDocsIAL() 1338 IncSync() 1339 return 1340} 1341 1342func countSubDocs(box, p string) (ret int) { 1343 p = strings.TrimSuffix(p, ".sy") 1344 _ = filelock.Walk(filepath.Join(util.DataDir, box, p), func(path string, d fs.DirEntry, err error) error { 1345 if err != nil { 1346 return err 1347 } 1348 if d.IsDir() { 1349 return nil 1350 } 1351 if strings.HasSuffix(path, ".sy") { 1352 ret++ 1353 } 1354 return nil 1355 }) 1356 return 1357} 1358 1359func moveDoc(fromBox *Box, fromPath string, toBox *Box, toPath string, luteEngine *lute.Lute, callback interface{}) (newPath string, err error) { 1360 isSameBox := fromBox.ID == toBox.ID 1361 1362 if isSameBox { 1363 if !fromBox.Exist(toPath) { 1364 err = ErrBlockNotFound 1365 return 1366 } 1367 } else { 1368 if !toBox.Exist(toPath) { 1369 err = ErrBlockNotFound 1370 return 1371 } 1372 } 1373 1374 tree, err := filesys.LoadTree(fromBox.ID, fromPath, luteEngine) 1375 if err != nil { 1376 err = ErrBlockNotFound 1377 return 1378 } 1379 1380 fromParentTree := loadParentTree(tree) 1381 1382 moveToRoot := "/" == toPath 1383 toBlockID := tree.ID 1384 fromFolder := path.Join(path.Dir(fromPath), tree.ID) 1385 toFolder := "/" 1386 if !moveToRoot { 1387 var toTree *parse.Tree 1388 if isSameBox { 1389 toTree, err = filesys.LoadTree(fromBox.ID, toPath, luteEngine) 1390 } else { 1391 toTree, err = filesys.LoadTree(toBox.ID, toPath, luteEngine) 1392 } 1393 if err != nil { 1394 err = ErrBlockNotFound 1395 return 1396 } 1397 1398 toBlockID = toTree.ID 1399 toFolder = path.Join(path.Dir(toPath), toBlockID) 1400 } 1401 1402 if isSameBox { 1403 if err = fromBox.MkdirAll(toFolder); err != nil { 1404 return 1405 } 1406 } else { 1407 if err = toBox.MkdirAll(toFolder); err != nil { 1408 return 1409 } 1410 } 1411 1412 needMoveSubDocs := fromBox.Exist(fromFolder) 1413 if needMoveSubDocs { 1414 // 移动子文档文件夹 1415 1416 newFolder := path.Join(toFolder, tree.ID) 1417 if isSameBox { 1418 if err = fromBox.Move(fromFolder, newFolder); err != nil { 1419 return 1420 } 1421 } else { 1422 absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromFolder) 1423 absToPath := filepath.Join(util.DataDir, toBox.ID, newFolder) 1424 if filelock.IsExist(absToPath) { 1425 filelock.Remove(absToPath) 1426 } 1427 if err = filelock.Rename(absFromPath, absToPath); err != nil { 1428 msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err) 1429 logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err) 1430 err = errors.New(msg) 1431 return 1432 } 1433 } 1434 } 1435 1436 newPath = path.Join(toFolder, tree.ID+".sy") 1437 1438 if isSameBox { 1439 if err = fromBox.Move(fromPath, newPath); err != nil { 1440 return 1441 } 1442 1443 tree, err = filesys.LoadTree(fromBox.ID, newPath, luteEngine) 1444 if err != nil { 1445 return 1446 } 1447 1448 moveTree(tree) 1449 } else { 1450 absFromPath := filepath.Join(util.DataDir, fromBox.ID, fromPath) 1451 absToPath := filepath.Join(util.DataDir, toBox.ID, newPath) 1452 if err = filelock.Rename(absFromPath, absToPath); err != nil { 1453 msg := fmt.Sprintf(Conf.Language(5), fromBox.Name, fromPath, err) 1454 logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, fromBox.ID, err) 1455 err = errors.New(msg) 1456 return 1457 } 1458 1459 tree, err = filesys.LoadTree(toBox.ID, newPath, luteEngine) 1460 if err != nil { 1461 return 1462 } 1463 1464 moveTree(tree) 1465 moveSorts(tree.ID, fromBox.ID, toBox.ID) 1466 } 1467 1468 if needMoveSubDocs { 1469 // 将其所有子文档的移动事件推送到前端 https://github.com/siyuan-note/siyuan/issues/11661 1470 subDocsFolder := path.Join(toFolder, tree.ID) 1471 syFiles := listSyFiles(path.Join(toBox.ID, subDocsFolder)) 1472 for _, syFile := range syFiles { 1473 relPath := strings.TrimPrefix(syFile, "/"+path.Join(toBox.ID, toFolder)) 1474 subFromPath := path.Join(path.Dir(fromPath), relPath) 1475 subToPath := path.Join(toFolder, relPath) 1476 1477 evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast) 1478 evt.Data = map[string]interface{}{ 1479 "fromNotebook": fromBox.ID, 1480 "fromPath": subFromPath, 1481 "toNotebook": toBox.ID, 1482 "toPath": path.Dir(subToPath) + ".sy", 1483 "newPath": subToPath, 1484 } 1485 evt.Callback = callback 1486 util.PushEvent(evt) 1487 } 1488 } 1489 1490 evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast) 1491 evt.Data = map[string]interface{}{ 1492 "fromNotebook": fromBox.ID, 1493 "fromPath": fromPath, 1494 "toNotebook": toBox.ID, 1495 "toPath": toPath, 1496 "newPath": newPath, 1497 } 1498 evt.Callback = callback 1499 util.PushEvent(evt) 1500 1501 refreshDocInfo(fromParentTree) 1502 return 1503} 1504 1505func RemoveDoc(boxID, p string) { 1506 box := Conf.Box(boxID) 1507 if nil == box { 1508 return 1509 } 1510 1511 FlushTxQueue() 1512 luteEngine := util.NewLute() 1513 removeDoc(box, p, luteEngine) 1514 IncSync() 1515 return 1516} 1517 1518func RemoveDocs(paths []string) { 1519 util.PushEndlessProgress(Conf.Language(116)) 1520 defer util.PushClearProgress() 1521 1522 paths = util.FilterSelfChildDocs(paths) 1523 pathsBoxes := getBoxesByPaths(paths) 1524 FlushTxQueue() 1525 luteEngine := util.NewLute() 1526 for p, box := range pathsBoxes { 1527 removeDoc(box, p, luteEngine) 1528 } 1529 return 1530} 1531 1532func removeDoc(box *Box, p string, luteEngine *lute.Lute) { 1533 tree, _ := filesys.LoadTree(box.ID, p, luteEngine) 1534 if nil == tree { 1535 return 1536 } 1537 1538 historyDir, err := GetHistoryDir(HistoryOpDelete) 1539 if err != nil { 1540 logging.LogErrorf("get history dir failed: %s", err) 1541 return 1542 } 1543 1544 historyPath := filepath.Join(historyDir, box.ID, p) 1545 absPath := filepath.Join(util.DataDir, box.ID, p) 1546 if err = filelock.Copy(absPath, historyPath); err != nil { 1547 logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absPath, historyPath, err) 1548 return 1549 } 1550 1551 generateAvHistory(tree, historyDir) 1552 copyDocAssetsToDataAssets(box.ID, p) 1553 1554 removeIDs := treenode.RootChildIDs(tree.ID) 1555 dir := path.Dir(p) 1556 childrenDir := path.Join(dir, tree.ID) 1557 existChildren := box.Exist(childrenDir) 1558 if existChildren { 1559 absChildrenDir := filepath.Join(util.DataDir, tree.Box, childrenDir) 1560 historyPath = filepath.Join(historyDir, tree.Box, childrenDir) 1561 if err = filelock.Copy(absChildrenDir, historyPath); err != nil { 1562 logging.LogErrorf("backup [path=%s] to history [%s] failed: %s", absChildrenDir, historyPath, err) 1563 return 1564 } 1565 } 1566 indexHistoryDir(filepath.Base(historyDir), util.NewLute()) 1567 1568 allRemoveRootIDs := []string{tree.ID} 1569 allRemoveRootIDs = append(allRemoveRootIDs, removeIDs...) 1570 allRemoveRootIDs = gulu.Str.RemoveDuplicatedElem(allRemoveRootIDs) 1571 for _, rootID := range allRemoveRootIDs { 1572 removeTree, _ := LoadTreeByBlockID(rootID) 1573 if nil == removeTree { 1574 continue 1575 } 1576 1577 syncDelete2AvBlock(removeTree.Root, removeTree, nil) 1578 } 1579 1580 if existChildren { 1581 if err = box.Remove(childrenDir); err != nil { 1582 logging.LogErrorf("remove children dir [%s%s] failed: %s", box.ID, childrenDir, err) 1583 return 1584 } 1585 logging.LogInfof("removed children dir [%s%s]", box.ID, childrenDir) 1586 } 1587 if err = box.Remove(p); err != nil { 1588 logging.LogErrorf("remove [%s%s] failed: %s", box.ID, p, err) 1589 return 1590 } 1591 logging.LogInfof("removed doc [%s%s]", box.ID, p) 1592 1593 box.removeSort(removeIDs) 1594 RemoveRecentDoc(removeIDs) 1595 if "/" != dir { 1596 others, err := os.ReadDir(filepath.Join(util.DataDir, box.ID, dir)) 1597 if err == nil && 1 > len(others) { 1598 box.Remove(dir) 1599 } 1600 } 1601 1602 evt := util.NewCmdResult("removeDoc", 0, util.PushModeBroadcast) 1603 evt.Data = map[string]interface{}{ 1604 "ids": removeIDs, 1605 } 1606 util.PushEvent(evt) 1607 1608 refreshParentDocInfo(tree) 1609 task.AppendTask(task.DatabaseIndex, removeDoc0, tree, childrenDir) 1610} 1611 1612func removeDoc0(tree *parse.Tree, childrenDir string) { 1613 // 收集引用的定义块 ID 1614 refDefIDs := getRefDefIDs(tree.Root) 1615 // 推送定义节点引用计数 1616 for _, defID := range refDefIDs { 1617 task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID) 1618 } 1619 1620 treenode.RemoveBlockTreesByPathPrefix(childrenDir) 1621 sql.RemoveTreePathQueue(tree.Box, childrenDir) 1622 cache.RemoveDocIAL(tree.Path) 1623 return 1624} 1625 1626func RenameDoc(boxID, p, title string) (err error) { 1627 box := Conf.Box(boxID) 1628 if nil == box { 1629 err = errors.New(Conf.Language(0)) 1630 return 1631 } 1632 1633 FlushTxQueue() 1634 luteEngine := util.NewLute() 1635 tree, err := filesys.LoadTree(box.ID, p, luteEngine) 1636 if err != nil { 1637 return 1638 } 1639 1640 title = removeInvisibleCharsInTitle(title) 1641 if 512 < utf8.RuneCountInString(title) { 1642 // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299 1643 return errors.New(Conf.Language(106)) 1644 } 1645 1646 oldTitle := tree.Root.IALAttr("title") 1647 if oldTitle == title { 1648 return 1649 } 1650 if "" == title { 1651 title = Conf.language(16) 1652 } 1653 title = strings.ReplaceAll(title, "/", "") 1654 1655 tree.HPath = path.Join(path.Dir(tree.HPath), title) 1656 tree.Root.SetIALAttr("title", title) 1657 tree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr()) 1658 if err = renameWriteJSONQueue(tree); err != nil { 1659 return 1660 } 1661 1662 refText := getNodeRefText(tree.Root) 1663 evt := util.NewCmdResult("rename", 0, util.PushModeBroadcast) 1664 evt.Data = map[string]interface{}{ 1665 "box": boxID, 1666 "id": tree.Root.ID, 1667 "path": p, 1668 "title": title, 1669 "refText": refText, 1670 } 1671 util.PushEvent(evt) 1672 1673 box.renameSubTrees(tree) 1674 updateRefTextRenameDoc(tree) 1675 IncSync() 1676 return 1677} 1678 1679func createDoc(boxID, p, title, dom string) (tree *parse.Tree, err error) { 1680 title = removeInvisibleCharsInTitle(title) 1681 if 512 < utf8.RuneCountInString(title) { 1682 // 限制笔记本名和文档名最大长度为 `512` https://github.com/siyuan-note/siyuan/issues/6299 1683 err = errors.New(Conf.Language(106)) 1684 return 1685 } 1686 title = strings.ReplaceAll(title, "/", "") 1687 title = strings.TrimSpace(title) 1688 if "" == title { 1689 title = Conf.Language(16) 1690 } 1691 1692 baseName := strings.TrimSpace(path.Base(p)) 1693 if "" == util.GetTreeID(baseName) { 1694 err = errors.New(Conf.Language(16)) 1695 return 1696 } 1697 1698 if strings.HasPrefix(baseName, ".") { 1699 err = errors.New(Conf.Language(13)) 1700 return 1701 } 1702 1703 box := Conf.Box(boxID) 1704 if nil == box { 1705 err = errors.New(Conf.Language(0)) 1706 return 1707 } 1708 1709 id := util.GetTreeID(p) 1710 var hPath string 1711 folder := path.Dir(p) 1712 if "/" != folder { 1713 parentID := path.Base(folder) 1714 parentTree, loadErr := LoadTreeByBlockID(parentID) 1715 if nil != loadErr { 1716 logging.LogErrorf("get parent tree [%s] failed", parentID) 1717 err = ErrBlockNotFound 1718 return 1719 } 1720 hPath = path.Join(parentTree.HPath, title) 1721 } else { 1722 hPath = "/" + title 1723 } 1724 1725 if depth := strings.Count(p, "/"); 7 < depth && !Conf.FileTree.AllowCreateDeeper { 1726 err = errors.New(Conf.Language(118)) 1727 return 1728 } 1729 1730 if !box.Exist(folder) { 1731 if err = box.MkdirAll(folder); err != nil { 1732 return 1733 } 1734 } 1735 1736 if box.Exist(p) { 1737 err = errors.New(Conf.Language(1)) 1738 return 1739 } 1740 1741 luteEngine := util.NewLute() 1742 tree = luteEngine.BlockDOM2Tree(dom) 1743 tree.Box = boxID 1744 tree.Path = p 1745 tree.HPath = hPath 1746 tree.ID = id 1747 tree.Root.ID = id 1748 tree.Root.Spec = "1" 1749 updated := util.TimeFromID(id) 1750 tree.Root.KramdownIAL = [][]string{{"id", id}, {"title", html.EscapeAttrVal(title)}, {"updated", updated}} 1751 if nil == tree.Root.FirstChild { 1752 tree.Root.AppendChild(treenode.NewParagraph("")) 1753 } 1754 1755 // 如果段落块中仅包含一个 mp3/mp4 超链接,则将其转换为音视频块 1756 // Convert mp3 and mp4 hyperlinks to audio and video when moving cloud inbox to docs https://github.com/siyuan-note/siyuan/issues/9778 1757 var unlinks []*ast.Node 1758 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 1759 if !entering { 1760 return ast.WalkContinue 1761 } 1762 1763 if ast.NodeParagraph == n.Type { 1764 link := n.FirstChild 1765 if nil != link && link.IsTextMarkType("a") { 1766 if strings.HasSuffix(link.TextMarkAHref, ".mp3") { 1767 unlinks = append(unlinks, n) 1768 audio := &ast.Node{ID: n.ID, Type: ast.NodeAudio, Tokens: []byte("<audio controls=\"controls\" src=\"" + link.TextMarkAHref + "\" data-src=\"" + link.TextMarkAHref + "\"></audio>")} 1769 audio.SetIALAttr("id", n.ID) 1770 audio.SetIALAttr("updated", util.TimeFromID(n.ID)) 1771 n.InsertBefore(audio) 1772 } else if strings.HasSuffix(link.TextMarkAHref, ".mp4") { 1773 unlinks = append(unlinks, n) 1774 video := &ast.Node{ID: n.ID, Type: ast.NodeVideo, Tokens: []byte("<video controls=\"controls\" src=\"" + link.TextMarkAHref + "\" data-src=\"" + link.TextMarkAHref + "\"></video>")} 1775 video.SetIALAttr("id", n.ID) 1776 video.SetIALAttr("updated", util.TimeFromID(n.ID)) 1777 n.InsertBefore(video) 1778 } 1779 } 1780 } 1781 return ast.WalkContinue 1782 }) 1783 for _, unlink := range unlinks { 1784 unlink.Unlink() 1785 } 1786 1787 transaction := &Transaction{DoOperations: []*Operation{{Action: "create", Data: tree}}} 1788 PerformTransactions(&[]*Transaction{transaction}) 1789 FlushTxQueue() 1790 return 1791} 1792 1793func removeInvisibleCharsInTitle(title string) string { 1794 // 不要踢掉 零宽连字符,否则有的 Emoji 会变形 https://github.com/siyuan-note/siyuan/issues/11480 1795 title = strings.ReplaceAll(title, string(gulu.ZWJ), "__@ZWJ@__") 1796 title = util.RemoveInvalid(title) 1797 title = strings.ReplaceAll(title, "__@ZWJ@__", string(gulu.ZWJ)) 1798 title = strings.TrimSpace(title) 1799 return title 1800} 1801 1802func moveSorts(rootID, fromBox, toBox string) { 1803 root := treenode.GetBlockTree(rootID) 1804 if nil == root { 1805 return 1806 } 1807 1808 fromRootSorts := map[string]int{} 1809 ids := treenode.RootChildIDs(rootID) 1810 fromConfPath := filepath.Join(util.DataDir, fromBox, ".siyuan", "sort.json") 1811 fromFullSortIDs := map[string]int{} 1812 if filelock.IsExist(fromConfPath) { 1813 data, err := filelock.ReadFile(fromConfPath) 1814 if err != nil { 1815 logging.LogErrorf("read sort conf failed: %s", err) 1816 return 1817 } 1818 1819 if err = gulu.JSON.UnmarshalJSON(data, &fromFullSortIDs); err != nil { 1820 logging.LogErrorf("unmarshal sort conf failed: %s", err) 1821 } 1822 } 1823 for _, id := range ids { 1824 fromRootSorts[id] = fromFullSortIDs[id] 1825 } 1826 1827 toConfPath := filepath.Join(util.DataDir, toBox, ".siyuan", "sort.json") 1828 toFullSortIDs := map[string]int{} 1829 if filelock.IsExist(toConfPath) { 1830 data, err := filelock.ReadFile(toConfPath) 1831 if err != nil { 1832 logging.LogErrorf("read sort conf failed: %s", err) 1833 return 1834 } 1835 1836 if err = gulu.JSON.UnmarshalJSON(data, &toFullSortIDs); err != nil { 1837 logging.LogErrorf("unmarshal sort conf failed: %s", err) 1838 return 1839 } 1840 } 1841 1842 for id, sortVal := range fromRootSorts { 1843 toFullSortIDs[id] = sortVal 1844 } 1845 1846 data, err := gulu.JSON.MarshalJSON(toFullSortIDs) 1847 if err != nil { 1848 logging.LogErrorf("marshal sort conf failed: %s", err) 1849 return 1850 } 1851 if err = filelock.WriteFile(toConfPath, data); err != nil { 1852 logging.LogErrorf("write sort conf failed: %s", err) 1853 return 1854 } 1855} 1856 1857func ChangeFileTreeSort(boxID string, paths []string) { 1858 if 1 > len(paths) { 1859 return 1860 } 1861 1862 FlushTxQueue() 1863 box := Conf.Box(boxID) 1864 sortIDs := map[string]int{} 1865 max := 0 1866 for i, p := range paths { 1867 id := util.GetTreeID(p) 1868 sortIDs[id] = i + 1 1869 if i == len(paths)-1 { 1870 max = i + 2 1871 } 1872 } 1873 1874 p := paths[0] 1875 parentPath := path.Dir(p) 1876 absParentPath := filepath.Join(util.DataDir, boxID, parentPath) 1877 files, err := os.ReadDir(absParentPath) 1878 if err != nil { 1879 logging.LogErrorf("read dir [%s] failed: %s", absParentPath, err) 1880 } 1881 1882 sortFolderIDs := map[string]int{} 1883 for _, f := range files { 1884 if !strings.HasSuffix(f.Name(), ".sy") { 1885 continue 1886 } 1887 1888 id := strings.TrimSuffix(f.Name(), ".sy") 1889 val := sortIDs[id] 1890 if 0 == val { 1891 val = max 1892 max++ 1893 } 1894 sortFolderIDs[id] = val 1895 } 1896 1897 confDir := filepath.Join(util.DataDir, box.ID, ".siyuan") 1898 if err = os.MkdirAll(confDir, 0755); err != nil { 1899 logging.LogErrorf("create conf dir failed: %s", err) 1900 return 1901 } 1902 confPath := filepath.Join(confDir, "sort.json") 1903 fullSortIDs := map[string]int{} 1904 var data []byte 1905 if filelock.IsExist(confPath) { 1906 data, err = filelock.ReadFile(confPath) 1907 if err != nil { 1908 logging.LogErrorf("read sort conf failed: %s", err) 1909 return 1910 } 1911 1912 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil { 1913 logging.LogErrorf("unmarshal sort conf failed: %s", err) 1914 } 1915 } 1916 1917 for sortID, sortVal := range sortFolderIDs { 1918 fullSortIDs[sortID] = sortVal 1919 } 1920 1921 data, err = gulu.JSON.MarshalJSON(fullSortIDs) 1922 if err != nil { 1923 logging.LogErrorf("marshal sort conf failed: %s", err) 1924 return 1925 } 1926 if err = filelock.WriteFile(confPath, data); err != nil { 1927 logging.LogErrorf("write sort conf failed: %s", err) 1928 return 1929 } 1930 1931 IncSync() 1932} 1933 1934func (box *Box) fillSort(files *[]*File) { 1935 confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json") 1936 if !filelock.IsExist(confPath) { 1937 return 1938 } 1939 1940 data, err := filelock.ReadFile(confPath) 1941 if err != nil { 1942 logging.LogErrorf("read sort conf failed: %s", err) 1943 return 1944 } 1945 1946 fullSortIDs := map[string]int{} 1947 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil { 1948 logging.LogErrorf("unmarshal sort conf failed: %s", err) 1949 return 1950 } 1951 1952 for _, f := range *files { 1953 id := strings.TrimSuffix(f.ID, ".sy") 1954 f.Sort = fullSortIDs[id] 1955 } 1956} 1957 1958func (box *Box) removeSort(ids []string) { 1959 confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json") 1960 if !filelock.IsExist(confPath) { 1961 return 1962 } 1963 1964 data, err := filelock.ReadFile(confPath) 1965 if err != nil { 1966 logging.LogErrorf("read sort conf failed: %s", err) 1967 return 1968 } 1969 1970 fullSortIDs := map[string]int{} 1971 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil { 1972 logging.LogErrorf("unmarshal sort conf failed: %s", err) 1973 return 1974 } 1975 1976 for _, toRemove := range ids { 1977 delete(fullSortIDs, toRemove) 1978 } 1979 1980 data, err = gulu.JSON.MarshalJSON(fullSortIDs) 1981 if err != nil { 1982 logging.LogErrorf("marshal sort conf failed: %s", err) 1983 return 1984 } 1985 if err = filelock.WriteFile(confPath, data); err != nil { 1986 logging.LogErrorf("write sort conf failed: %s", err) 1987 return 1988 } 1989} 1990 1991func (box *Box) addMinSort(parentPath, id string) { 1992 docs, _, err := ListDocTree(box.ID, parentPath, util.SortModeUnassigned, false, false, 1) 1993 if err != nil { 1994 logging.LogErrorf("list doc tree failed: %s", err) 1995 return 1996 } 1997 1998 sortVal := 0 1999 if 0 < len(docs) { 2000 sortVal = docs[0].Sort - 1 2001 } 2002 2003 confDir := filepath.Join(util.DataDir, box.ID, ".siyuan") 2004 if err = os.MkdirAll(confDir, 0755); err != nil { 2005 logging.LogErrorf("create conf dir failed: %s", err) 2006 return 2007 } 2008 confPath := filepath.Join(confDir, "sort.json") 2009 fullSortIDs := map[string]int{} 2010 var data []byte 2011 if filelock.IsExist(confPath) { 2012 data, err = filelock.ReadFile(confPath) 2013 if err != nil { 2014 logging.LogErrorf("read sort conf failed: %s", err) 2015 return 2016 } 2017 2018 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil { 2019 logging.LogErrorf("unmarshal sort conf failed: %s", err) 2020 } 2021 } 2022 2023 fullSortIDs[id] = sortVal 2024 2025 data, err = gulu.JSON.MarshalJSON(fullSortIDs) 2026 if err != nil { 2027 logging.LogErrorf("marshal sort conf failed: %s", err) 2028 return 2029 } 2030 if err = filelock.WriteFile(confPath, data); err != nil { 2031 logging.LogErrorf("write sort conf failed: %s", err) 2032 return 2033 } 2034} 2035 2036func (box *Box) addSort(previousPath, id string) { 2037 confDir := filepath.Join(util.DataDir, box.ID, ".siyuan") 2038 if err := os.MkdirAll(confDir, 0755); err != nil { 2039 logging.LogErrorf("create conf dir failed: %s", err) 2040 return 2041 } 2042 confPath := filepath.Join(confDir, "sort.json") 2043 fullSortIDs := map[string]int{} 2044 var data []byte 2045 if filelock.IsExist(confPath) { 2046 data, err := filelock.ReadFile(confPath) 2047 if err != nil { 2048 logging.LogErrorf("read sort conf failed: %s", err) 2049 return 2050 } 2051 2052 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil { 2053 logging.LogErrorf("unmarshal sort conf failed: %s", err) 2054 } 2055 } 2056 2057 parentPath := path.Dir(previousPath) 2058 docs, _, err := ListDocTree(box.ID, parentPath, util.SortModeUnassigned, false, false, Conf.FileTree.MaxListCount) 2059 if err != nil { 2060 logging.LogErrorf("list doc tree failed: %s", err) 2061 return 2062 } 2063 2064 previousID := util.GetTreeID(previousPath) 2065 sortVal := 0 2066 for _, doc := range docs { 2067 fullSortIDs[doc.ID] = sortVal 2068 if doc.ID == previousID { 2069 sortVal++ 2070 fullSortIDs[id] = sortVal 2071 } 2072 sortVal++ 2073 } 2074 2075 data, err = gulu.JSON.MarshalJSON(fullSortIDs) 2076 if err != nil { 2077 logging.LogErrorf("marshal sort conf failed: %s", err) 2078 return 2079 } 2080 if err = filelock.WriteFile(confPath, data); err != nil { 2081 logging.LogErrorf("write sort conf failed: %s", err) 2082 return 2083 } 2084} 2085 2086func (box *Box) setSort(sortIDVals map[string]int) { 2087 confPath := filepath.Join(util.DataDir, box.ID, ".siyuan", "sort.json") 2088 if !filelock.IsExist(confPath) { 2089 return 2090 } 2091 2092 data, err := filelock.ReadFile(confPath) 2093 if err != nil { 2094 logging.LogErrorf("read sort conf failed: %s", err) 2095 return 2096 } 2097 2098 fullSortIDs := map[string]int{} 2099 if err = gulu.JSON.UnmarshalJSON(data, &fullSortIDs); err != nil { 2100 logging.LogErrorf("unmarshal sort conf failed: %s", err) 2101 return 2102 } 2103 2104 for sortID := range sortIDVals { 2105 fullSortIDs[sortID] = sortIDVals[sortID] 2106 } 2107 2108 data, err = gulu.JSON.MarshalJSON(fullSortIDs) 2109 if err != nil { 2110 logging.LogErrorf("marshal sort conf failed: %s", err) 2111 return 2112 } 2113 if err = filelock.WriteFile(confPath, data); err != nil { 2114 logging.LogErrorf("write sort conf failed: %s", err) 2115 return 2116 } 2117}