A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at upstream/main 1221 lines 27 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 api 18 19import ( 20 "fmt" 21 "math" 22 "net/http" 23 "os" 24 "path" 25 "path/filepath" 26 "regexp" 27 "strings" 28 "unicode/utf8" 29 30 "github.com/88250/gulu" 31 "github.com/88250/lute/ast" 32 "github.com/gin-gonic/gin" 33 "github.com/siyuan-note/siyuan/kernel/filesys" 34 "github.com/siyuan-note/siyuan/kernel/model" 35 "github.com/siyuan-note/siyuan/kernel/util" 36) 37 38func moveLocalShorthands(c *gin.Context) { 39 ret := gulu.Ret.NewResult() 40 defer c.JSON(http.StatusOK, ret) 41 42 arg, ok := util.JsonArg(c, ret) 43 if !ok { 44 return 45 } 46 47 notebook := arg["notebook"].(string) 48 if util.InvalidIDPattern(notebook, ret) { 49 return 50 } 51 52 var parentID string 53 parentIDArg := arg["parentID"] 54 if nil != parentIDArg { 55 parentID = parentIDArg.(string) 56 } 57 58 var hPath string 59 hPathArg := arg["path"] 60 if nil != hPathArg { 61 hPath = arg["path"].(string) 62 baseName := path.Base(hPath) 63 dir := path.Dir(hPath) 64 r, _ := regexp.Compile("\r\n|\r|\n|\u2028|\u2029|\t|/") 65 baseName = r.ReplaceAllString(baseName, "") 66 if 512 < utf8.RuneCountInString(baseName) { 67 baseName = gulu.Str.SubStr(baseName, 512) 68 } 69 hPath = path.Join(dir, baseName) 70 } 71 72 // TODO: 改造旧方案,去掉 hPath, parentID,改为使用文档树配置项 闪念速记存放位置,参考创建日记实现 73 // https://github.com/siyuan-note/siyuan/issues/14414 74 ids, err := model.MoveLocalShorthands(notebook, hPath, parentID) 75 if err != nil { 76 ret.Code = -1 77 ret.Msg = err.Error() 78 return 79 } 80 81 model.FlushTxQueue() 82 box := model.Conf.Box(notebook) 83 for _, id := range ids { 84 b, _ := model.GetBlock(id, nil) 85 pushCreate(box, b.Path, arg) 86 } 87} 88 89func listDocTree(c *gin.Context) { 90 // Add kernel API `/api/filetree/listDocTree` https://github.com/siyuan-note/siyuan/issues/10482 91 92 ret := gulu.Ret.NewResult() 93 defer c.JSON(http.StatusOK, ret) 94 95 arg, ok := util.JsonArg(c, ret) 96 if !ok { 97 return 98 } 99 100 notebook := arg["notebook"].(string) 101 if util.InvalidIDPattern(notebook, ret) { 102 return 103 } 104 105 p := arg["path"].(string) 106 p = strings.TrimSuffix(p, ".sy") 107 var doctree []*DocFile 108 root := filepath.Join(util.WorkspaceDir, "data", notebook, p) 109 dir, err := os.ReadDir(root) 110 if err != nil { 111 ret.Code = -1 112 ret.Msg = err.Error() 113 return 114 } 115 116 ids := map[string]bool{} 117 for _, entry := range dir { 118 if strings.HasPrefix(entry.Name(), ".") { 119 continue 120 } 121 122 if entry.IsDir() { 123 if !ast.IsNodeIDPattern(entry.Name()) { 124 continue 125 } 126 127 parent := &DocFile{ID: entry.Name()} 128 ids[parent.ID] = true 129 doctree = append(doctree, parent) 130 131 subPath := filepath.Join(root, entry.Name()) 132 if err = walkDocTree(subPath, parent, ids); err != nil { 133 ret.Code = -1 134 ret.Msg = err.Error() 135 return 136 } 137 } else { 138 id := strings.TrimSuffix(entry.Name(), ".sy") 139 if !ast.IsNodeIDPattern(id) { 140 continue 141 } 142 143 doc := &DocFile{ID: id} 144 if !ids[doc.ID] { 145 doctree = append(doctree, doc) 146 } 147 ids[doc.ID] = true 148 } 149 } 150 151 ret.Data = map[string]interface{}{ 152 "tree": doctree, 153 } 154} 155 156type DocFile struct { 157 ID string `json:"id"` 158 Children []*DocFile `json:"children,omitempty"` 159} 160 161func walkDocTree(p string, docFile *DocFile, ids map[string]bool) (err error) { 162 dir, err := os.ReadDir(p) 163 if err != nil { 164 return 165 } 166 167 for _, entry := range dir { 168 if entry.IsDir() { 169 if strings.HasPrefix(entry.Name(), ".") { 170 continue 171 } 172 173 if !ast.IsNodeIDPattern(entry.Name()) { 174 continue 175 } 176 177 parent := &DocFile{ID: entry.Name()} 178 ids[parent.ID] = true 179 docFile.Children = append(docFile.Children, parent) 180 181 subPath := filepath.Join(p, entry.Name()) 182 if err = walkDocTree(subPath, parent, ids); err != nil { 183 return 184 } 185 } else { 186 doc := &DocFile{ID: strings.TrimSuffix(entry.Name(), ".sy")} 187 if !ids[doc.ID] { 188 docFile.Children = append(docFile.Children, doc) 189 } 190 ids[doc.ID] = true 191 } 192 } 193 return 194} 195 196func upsertIndexes(c *gin.Context) { 197 ret := gulu.Ret.NewResult() 198 defer c.JSON(http.StatusOK, ret) 199 200 arg, ok := util.JsonArg(c, ret) 201 if !ok { 202 return 203 } 204 205 pathsArg := arg["paths"].([]interface{}) 206 var paths []string 207 for _, p := range pathsArg { 208 paths = append(paths, p.(string)) 209 } 210 model.UpsertIndexes(paths) 211} 212 213func removeIndexes(c *gin.Context) { 214 ret := gulu.Ret.NewResult() 215 defer c.JSON(http.StatusOK, ret) 216 217 arg, ok := util.JsonArg(c, ret) 218 if !ok { 219 return 220 } 221 222 pathsArg := arg["paths"].([]interface{}) 223 var paths []string 224 for _, p := range pathsArg { 225 paths = append(paths, p.(string)) 226 } 227 model.RemoveIndexes(paths) 228} 229 230func doc2Heading(c *gin.Context) { 231 ret := gulu.Ret.NewResult() 232 defer c.JSON(http.StatusOK, ret) 233 234 arg, ok := util.JsonArg(c, ret) 235 if !ok { 236 return 237 } 238 239 srcID := arg["srcID"].(string) 240 targetID := arg["targetID"].(string) 241 after := arg["after"].(bool) 242 srcTreeBox, srcTreePath, err := model.Doc2Heading(srcID, targetID, after) 243 if err != nil { 244 ret.Code = -1 245 ret.Msg = err.Error() 246 ret.Data = map[string]interface{}{"closeTimeout": 5000} 247 return 248 } 249 250 ret.Data = map[string]interface{}{ 251 "srcTreeBox": srcTreeBox, 252 "srcTreePath": srcTreePath, 253 } 254} 255 256func heading2Doc(c *gin.Context) { 257 ret := gulu.Ret.NewResult() 258 defer c.JSON(http.StatusOK, ret) 259 260 arg, ok := util.JsonArg(c, ret) 261 if !ok { 262 return 263 } 264 265 srcHeadingID := arg["srcHeadingID"].(string) 266 targetNotebook := arg["targetNoteBook"].(string) 267 var targetPath string 268 if arg["targetPath"] != nil { 269 targetPath = arg["targetPath"].(string) 270 } 271 var previousPath string 272 if arg["previousPath"] != nil { 273 previousPath = arg["previousPath"].(string) 274 } 275 srcRootBlockID, targetPath, err := model.Heading2Doc(srcHeadingID, targetNotebook, targetPath, previousPath) 276 if err != nil { 277 ret.Code = -1 278 ret.Msg = err.Error() 279 ret.Data = map[string]interface{}{"closeTimeout": 5000} 280 return 281 } 282 283 model.FlushTxQueue() 284 285 box := model.Conf.Box(targetNotebook) 286 evt := util.NewCmdResult("heading2doc", 0, util.PushModeBroadcast) 287 evt.Data = map[string]interface{}{ 288 "box": box, 289 "path": targetPath, 290 "srcRootBlockID": srcRootBlockID, 291 } 292 evt.Callback = arg["callback"] 293 util.PushEvent(evt) 294} 295 296func li2Doc(c *gin.Context) { 297 ret := gulu.Ret.NewResult() 298 defer c.JSON(http.StatusOK, ret) 299 300 arg, ok := util.JsonArg(c, ret) 301 if !ok { 302 return 303 } 304 305 srcListItemID := arg["srcListItemID"].(string) 306 targetNotebook := arg["targetNoteBook"].(string) 307 var targetPath string 308 if arg["targetPath"] != nil { 309 targetPath = arg["targetPath"].(string) 310 } 311 var previousPath string 312 if arg["previousPath"] != nil { 313 previousPath = arg["previousPath"].(string) 314 } 315 srcRootBlockID, targetPath, err := model.ListItem2Doc(srcListItemID, targetNotebook, targetPath, previousPath) 316 if err != nil { 317 ret.Code = -1 318 ret.Msg = err.Error() 319 ret.Data = map[string]interface{}{"closeTimeout": 5000} 320 return 321 } 322 323 model.FlushTxQueue() 324 325 box := model.Conf.Box(targetNotebook) 326 evt := util.NewCmdResult("li2doc", 0, util.PushModeBroadcast) 327 evt.Data = map[string]interface{}{ 328 "box": box, 329 "path": targetPath, 330 "srcRootBlockID": srcRootBlockID, 331 } 332 evt.Callback = arg["callback"] 333 util.PushEvent(evt) 334} 335 336func getHPathByPath(c *gin.Context) { 337 ret := gulu.Ret.NewResult() 338 defer c.JSON(http.StatusOK, ret) 339 340 arg, ok := util.JsonArg(c, ret) 341 if !ok { 342 return 343 } 344 345 notebook := arg["notebook"].(string) 346 if util.InvalidIDPattern(notebook, ret) { 347 return 348 } 349 350 p := arg["path"].(string) 351 352 hPath, err := model.GetHPathByPath(notebook, p) 353 if err != nil { 354 ret.Code = -1 355 ret.Msg = err.Error() 356 return 357 } 358 ret.Data = hPath 359} 360 361func getHPathsByPaths(c *gin.Context) { 362 ret := gulu.Ret.NewResult() 363 defer c.JSON(http.StatusOK, ret) 364 365 arg, ok := util.JsonArg(c, ret) 366 if !ok { 367 return 368 } 369 370 pathsArg := arg["paths"].([]interface{}) 371 var paths []string 372 for _, p := range pathsArg { 373 paths = append(paths, p.(string)) 374 } 375 hPath, err := model.GetHPathsByPaths(paths) 376 if err != nil { 377 ret.Code = -1 378 ret.Msg = err.Error() 379 return 380 } 381 ret.Data = hPath 382} 383 384func getHPathByID(c *gin.Context) { 385 ret := gulu.Ret.NewResult() 386 defer c.JSON(http.StatusOK, ret) 387 388 arg, ok := util.JsonArg(c, ret) 389 if !ok { 390 return 391 } 392 393 id := arg["id"].(string) 394 if util.InvalidIDPattern(id, ret) { 395 return 396 } 397 398 hPath, err := model.GetHPathByID(id) 399 if err != nil { 400 ret.Code = -1 401 ret.Msg = err.Error() 402 return 403 } 404 ret.Data = hPath 405} 406 407func getPathByID(c *gin.Context) { 408 ret := gulu.Ret.NewResult() 409 defer c.JSON(http.StatusOK, ret) 410 411 arg, ok := util.JsonArg(c, ret) 412 if !ok { 413 return 414 } 415 416 id := arg["id"].(string) 417 if util.InvalidIDPattern(id, ret) { 418 return 419 } 420 421 p, notebook, err := model.GetPathByID(id) 422 if err != nil { 423 ret.Code = -1 424 ret.Msg = err.Error() 425 return 426 } 427 ret.Data = map[string]interface{}{ 428 "path": p, 429 "notebook": notebook, 430 } 431} 432 433func getFullHPathByID(c *gin.Context) { 434 ret := gulu.Ret.NewResult() 435 defer c.JSON(http.StatusOK, ret) 436 437 arg, ok := util.JsonArg(c, ret) 438 if !ok { 439 return 440 } 441 if nil == arg["id"] { 442 return 443 } 444 445 id := arg["id"].(string) 446 hPath, err := model.GetFullHPathByID(id) 447 if err != nil { 448 ret.Code = -1 449 ret.Msg = err.Error() 450 return 451 } 452 ret.Data = hPath 453} 454 455func getIDsByHPath(c *gin.Context) { 456 ret := gulu.Ret.NewResult() 457 defer c.JSON(http.StatusOK, ret) 458 459 arg, ok := util.JsonArg(c, ret) 460 if !ok { 461 return 462 } 463 if nil == arg["path"] { 464 return 465 } 466 if nil == arg["notebook"] { 467 return 468 } 469 470 notebook := arg["notebook"].(string) 471 if util.InvalidIDPattern(notebook, ret) { 472 return 473 } 474 475 p := arg["path"].(string) 476 ids, err := model.GetIDsByHPath(p, notebook) 477 if err != nil { 478 ret.Code = -1 479 ret.Msg = err.Error() 480 return 481 } 482 ret.Data = ids 483} 484 485func moveDocs(c *gin.Context) { 486 ret := gulu.Ret.NewResult() 487 defer c.JSON(http.StatusOK, ret) 488 489 arg, ok := util.JsonArg(c, ret) 490 if !ok { 491 return 492 } 493 494 var fromPaths []string 495 fromPathsArg := arg["fromPaths"].([]interface{}) 496 for _, fromPath := range fromPathsArg { 497 fromPaths = append(fromPaths, fromPath.(string)) 498 } 499 toPath := arg["toPath"].(string) 500 toNotebook := arg["toNotebook"].(string) 501 if util.InvalidIDPattern(toNotebook, ret) { 502 return 503 } 504 callback := arg["callback"] 505 err := model.MoveDocs(fromPaths, toNotebook, toPath, callback) 506 if err != nil { 507 ret.Code = -1 508 ret.Msg = err.Error() 509 ret.Data = map[string]interface{}{"closeTimeout": 7000} 510 return 511 } 512} 513 514func moveDocsByID(c *gin.Context) { 515 ret := gulu.Ret.NewResult() 516 defer c.JSON(http.StatusOK, ret) 517 518 arg, ok := util.JsonArg(c, ret) 519 if !ok { 520 return 521 } 522 523 fromIDsArg := arg["fromIDs"].([]any) 524 var fromIDs []string 525 for _, fromIDArg := range fromIDsArg { 526 fromID := fromIDArg.(string) 527 if util.InvalidIDPattern(fromID, ret) { 528 return 529 } 530 fromIDs = append(fromIDs, fromID) 531 } 532 toID := arg["toID"].(string) 533 if util.InvalidIDPattern(toID, ret) { 534 return 535 } 536 537 var fromPaths []string 538 for _, fromID := range fromIDs { 539 tree, err := model.LoadTreeByBlockID(fromID) 540 if err != nil { 541 ret.Code = -1 542 ret.Msg = err.Error() 543 ret.Data = map[string]interface{}{"closeTimeout": 7000} 544 return 545 } 546 fromPaths = append(fromPaths, tree.Path) 547 } 548 fromPaths = gulu.Str.RemoveDuplicatedElem(fromPaths) 549 550 var box *model.Box 551 toTree, err := model.LoadTreeByBlockID(toID) 552 if err != nil { 553 box = model.Conf.Box(toID) 554 if nil == box { 555 ret.Code = -1 556 ret.Msg = "can't found box or tree by id [" + toID + "]" 557 ret.Data = map[string]interface{}{"closeTimeout": 7000} 558 return 559 } 560 } 561 562 var toNotebook, toPath string 563 if nil != toTree { 564 toNotebook = toTree.Box 565 toPath = toTree.Path 566 } else if nil != box { 567 toNotebook = box.ID 568 toPath = "/" 569 } 570 callback := arg["callback"] 571 err = model.MoveDocs(fromPaths, toNotebook, toPath, callback) 572 if err != nil { 573 ret.Code = -1 574 ret.Msg = err.Error() 575 ret.Data = map[string]interface{}{"closeTimeout": 7000} 576 return 577 } 578} 579 580func removeDoc(c *gin.Context) { 581 ret := gulu.Ret.NewResult() 582 defer c.JSON(http.StatusOK, ret) 583 584 arg, ok := util.JsonArg(c, ret) 585 if !ok { 586 return 587 } 588 589 notebook := arg["notebook"].(string) 590 if util.InvalidIDPattern(notebook, ret) { 591 return 592 } 593 594 p := arg["path"].(string) 595 model.RemoveDoc(notebook, p) 596} 597 598func removeDocByID(c *gin.Context) { 599 ret := gulu.Ret.NewResult() 600 defer c.JSON(http.StatusOK, ret) 601 602 arg, ok := util.JsonArg(c, ret) 603 if !ok { 604 return 605 } 606 607 id := arg["id"].(string) 608 if util.InvalidIDPattern(id, ret) { 609 return 610 } 611 612 tree, err := model.LoadTreeByBlockID(id) 613 if err != nil { 614 ret.Code = -1 615 ret.Msg = err.Error() 616 ret.Data = map[string]interface{}{"closeTimeout": 7000} 617 return 618 } 619 620 model.RemoveDoc(tree.Box, tree.Path) 621} 622 623func removeDocs(c *gin.Context) { 624 ret := gulu.Ret.NewResult() 625 defer c.JSON(http.StatusOK, ret) 626 627 arg, ok := util.JsonArg(c, ret) 628 if !ok { 629 return 630 } 631 632 pathsArg := arg["paths"].([]interface{}) 633 var paths []string 634 for _, path := range pathsArg { 635 paths = append(paths, path.(string)) 636 } 637 model.RemoveDocs(paths) 638} 639 640func renameDoc(c *gin.Context) { 641 ret := gulu.Ret.NewResult() 642 defer c.JSON(http.StatusOK, ret) 643 644 arg, ok := util.JsonArg(c, ret) 645 if !ok { 646 return 647 } 648 649 notebook := arg["notebook"].(string) 650 if util.InvalidIDPattern(notebook, ret) { 651 return 652 } 653 654 p := arg["path"].(string) 655 title := arg["title"].(string) 656 657 err := model.RenameDoc(notebook, p, title) 658 if err != nil { 659 ret.Code = -1 660 ret.Msg = err.Error() 661 return 662 } 663 return 664} 665 666func renameDocByID(c *gin.Context) { 667 ret := gulu.Ret.NewResult() 668 defer c.JSON(http.StatusOK, ret) 669 670 arg, ok := util.JsonArg(c, ret) 671 if !ok { 672 return 673 } 674 if nil == arg["id"] { 675 return 676 } 677 678 id := arg["id"].(string) 679 if util.InvalidIDPattern(id, ret) { 680 return 681 } 682 683 title := arg["title"].(string) 684 685 tree, err := model.LoadTreeByBlockID(id) 686 if err != nil { 687 ret.Code = -1 688 ret.Msg = err.Error() 689 ret.Data = map[string]interface{}{"closeTimeout": 7000} 690 return 691 } 692 693 err = model.RenameDoc(tree.Box, tree.Path, title) 694 if err != nil { 695 ret.Code = -1 696 ret.Msg = err.Error() 697 return 698 } 699} 700 701func duplicateDoc(c *gin.Context) { 702 ret := gulu.Ret.NewResult() 703 defer c.JSON(http.StatusOK, ret) 704 705 arg, ok := util.JsonArg(c, ret) 706 if !ok { 707 return 708 } 709 710 id := arg["id"].(string) 711 tree, err := model.LoadTreeByBlockID(id) 712 if err != nil { 713 ret.Code = -1 714 ret.Msg = err.Error() 715 ret.Data = map[string]interface{}{"closeTimeout": 7000} 716 return 717 } 718 719 notebook := tree.Box 720 box := model.Conf.Box(notebook) 721 model.DuplicateDoc(tree) 722 arg["listDocTree"] = true 723 pushCreate(box, tree.Path, arg) 724 725 ret.Data = map[string]interface{}{ 726 "id": tree.Root.ID, 727 "notebook": notebook, 728 "path": tree.Path, 729 "hPath": tree.HPath, 730 } 731} 732 733func createDoc(c *gin.Context) { 734 ret := gulu.Ret.NewResult() 735 defer c.JSON(http.StatusOK, ret) 736 737 arg, ok := util.JsonArg(c, ret) 738 if !ok { 739 return 740 } 741 742 notebook := arg["notebook"].(string) 743 p := arg["path"].(string) 744 title := arg["title"].(string) 745 md := arg["md"].(string) 746 sortsArg := arg["sorts"] 747 var sorts []string 748 if nil != sortsArg { 749 for _, sort := range sortsArg.([]interface{}) { 750 sorts = append(sorts, sort.(string)) 751 } 752 } 753 754 tree, err := model.CreateDocByMd(notebook, p, title, md, sorts) 755 if err != nil { 756 ret.Code = -1 757 ret.Msg = err.Error() 758 ret.Data = map[string]interface{}{"closeTimeout": 7000} 759 return 760 } 761 762 model.FlushTxQueue() 763 box := model.Conf.Box(notebook) 764 pushCreate(box, p, arg) 765 766 ret.Data = map[string]interface{}{ 767 "id": tree.Root.ID, 768 } 769} 770 771func createDailyNote(c *gin.Context) { 772 ret := gulu.Ret.NewResult() 773 defer c.JSON(http.StatusOK, ret) 774 775 arg, ok := util.JsonArg(c, ret) 776 if !ok { 777 return 778 } 779 780 notebook := arg["notebook"].(string) 781 p, existed, err := model.CreateDailyNote(notebook) 782 if err != nil { 783 if model.ErrBoxNotFound == err { 784 ret.Code = 1 785 } else { 786 ret.Code = -1 787 } 788 ret.Msg = err.Error() 789 return 790 } 791 792 model.FlushTxQueue() 793 box := model.Conf.Box(notebook) 794 luteEngine := util.NewLute() 795 tree, err := filesys.LoadTree(box.ID, p, luteEngine) 796 if err != nil { 797 ret.Code = -1 798 ret.Msg = err.Error() 799 return 800 } 801 802 if !existed { 803 // 只有创建的情况才推送,已经存在的情况不推送 804 // Creating a dailynote existed no longer expands the doc tree https://github.com/siyuan-note/siyuan/issues/9959 805 appArg := arg["app"] 806 app := "" 807 if nil != appArg { 808 app = appArg.(string) 809 } 810 evt := util.NewCmdResult("createdailynote", 0, util.PushModeBroadcast) 811 evt.AppId = app 812 evt.Data = map[string]interface{}{ 813 "box": box, 814 "path": p, 815 } 816 evt.Callback = arg["callback"] 817 util.PushEvent(evt) 818 } 819 820 ret.Data = map[string]interface{}{ 821 "id": tree.Root.ID, 822 } 823} 824 825func createDocWithMd(c *gin.Context) { 826 ret := gulu.Ret.NewResult() 827 defer c.JSON(http.StatusOK, ret) 828 829 arg, ok := util.JsonArg(c, ret) 830 if !ok { 831 return 832 } 833 834 notebook := arg["notebook"].(string) 835 if util.InvalidIDPattern(notebook, ret) { 836 return 837 } 838 839 tagsArg := arg["tags"] 840 var tags string 841 if nil != tagsArg { 842 tags = tagsArg.(string) 843 } 844 845 var parentID string 846 parentIDArg := arg["parentID"] 847 if nil != parentIDArg { 848 parentID = parentIDArg.(string) 849 } 850 851 id := ast.NewNodeID() 852 idArg := arg["id"] 853 if nil != idArg { 854 id = idArg.(string) 855 } 856 857 hPath := arg["path"].(string) 858 markdown := arg["markdown"].(string) 859 860 baseName := path.Base(hPath) 861 dir := path.Dir(hPath) 862 r, _ := regexp.Compile("\r\n|\r|\n|\u2028|\u2029|\t|/") 863 baseName = r.ReplaceAllString(baseName, "") 864 if 512 < utf8.RuneCountInString(baseName) { 865 baseName = gulu.Str.SubStr(baseName, 512) 866 } 867 hPath = path.Join(dir, baseName) 868 if !strings.HasPrefix(hPath, "/") { 869 hPath = "/" + hPath 870 } 871 872 withMath := false 873 withMathArg := arg["withMath"] 874 if nil != withMathArg { 875 withMath = withMathArg.(bool) 876 } 877 clippingHref := "" 878 clippingHrefArg := arg["clippingHref"] 879 if nil != clippingHrefArg { 880 clippingHref = clippingHrefArg.(string) 881 } 882 883 id, err := model.CreateWithMarkdown(tags, notebook, hPath, markdown, parentID, id, withMath, clippingHref) 884 if err != nil { 885 ret.Code = -1 886 ret.Msg = err.Error() 887 return 888 } 889 ret.Data = id 890 891 model.FlushTxQueue() 892 box := model.Conf.Box(notebook) 893 b, _ := model.GetBlock(id, nil) 894 pushCreate(box, b.Path, arg) 895} 896 897func getDocCreateSavePath(c *gin.Context) { 898 ret := gulu.Ret.NewResult() 899 defer c.JSON(http.StatusOK, ret) 900 901 arg, ok := util.JsonArg(c, ret) 902 if !ok { 903 return 904 } 905 906 notebook := arg["notebook"].(string) 907 box := model.Conf.Box(notebook) 908 var docCreateSaveBox string 909 docCreateSavePathTpl := model.Conf.FileTree.DocCreateSavePath 910 if nil != box { 911 boxConf := box.GetConf() 912 docCreateSaveBox = boxConf.DocCreateSaveBox 913 docCreateSavePathTpl = boxConf.DocCreateSavePath 914 } 915 if "" == docCreateSaveBox && "" == docCreateSavePathTpl { 916 docCreateSaveBox = model.Conf.FileTree.DocCreateSaveBox 917 } 918 if "" != docCreateSaveBox { 919 if nil == model.Conf.Box(docCreateSaveBox) { 920 // 如果配置的笔记本未打开或者不存在,则使用当前笔记本 921 docCreateSaveBox = notebook 922 } 923 } 924 if "" == docCreateSaveBox { 925 docCreateSaveBox = notebook 926 } 927 if "" == docCreateSavePathTpl { 928 docCreateSavePathTpl = model.Conf.FileTree.DocCreateSavePath 929 } 930 docCreateSavePathTpl = strings.TrimSpace(docCreateSavePathTpl) 931 932 if docCreateSaveBox != notebook { 933 if "" != docCreateSavePathTpl && !strings.HasPrefix(docCreateSavePathTpl, "/") { 934 // 如果配置的笔记本不是当前笔记本,则将相对路径转换为绝对路径 935 docCreateSavePathTpl = "/" + docCreateSavePathTpl 936 } 937 } 938 939 docCreateSavePath, err := model.RenderGoTemplate(docCreateSavePathTpl) 940 if err != nil { 941 ret.Code = -1 942 ret.Msg = err.Error() 943 return 944 } 945 946 ret.Data = map[string]interface{}{ 947 "box": docCreateSaveBox, 948 "path": docCreateSavePath, 949 } 950} 951 952func getRefCreateSavePath(c *gin.Context) { 953 ret := gulu.Ret.NewResult() 954 defer c.JSON(http.StatusOK, ret) 955 956 arg, ok := util.JsonArg(c, ret) 957 if !ok { 958 return 959 } 960 961 notebook := arg["notebook"].(string) 962 box := model.Conf.Box(notebook) 963 var refCreateSaveBox string 964 refCreateSavePathTpl := model.Conf.FileTree.RefCreateSavePath 965 if nil != box { 966 boxConf := box.GetConf() 967 refCreateSaveBox = boxConf.RefCreateSaveBox 968 refCreateSavePathTpl = boxConf.RefCreateSavePath 969 } 970 if "" == refCreateSaveBox && "" == refCreateSavePathTpl { 971 refCreateSaveBox = model.Conf.FileTree.RefCreateSaveBox 972 } 973 if "" != refCreateSaveBox { 974 if nil == model.Conf.Box(refCreateSaveBox) { 975 // 如果配置的笔记本未打开或者不存在,则使用当前笔记本 976 refCreateSaveBox = notebook 977 } 978 } 979 if "" == refCreateSaveBox { 980 refCreateSaveBox = notebook 981 } 982 if "" == refCreateSavePathTpl { 983 refCreateSavePathTpl = model.Conf.FileTree.RefCreateSavePath 984 } 985 986 if refCreateSaveBox != notebook { 987 if "" != refCreateSavePathTpl && !strings.HasPrefix(refCreateSavePathTpl, "/") { 988 // 如果配置的笔记本不是当前笔记本,则将相对路径转换为绝对路径 989 refCreateSavePathTpl = "/" + refCreateSavePathTpl 990 } 991 } 992 993 refCreateSavePath, err := model.RenderGoTemplate(refCreateSavePathTpl) 994 if err != nil { 995 ret.Code = -1 996 ret.Msg = err.Error() 997 return 998 } 999 ret.Data = map[string]interface{}{ 1000 "box": refCreateSaveBox, 1001 "path": refCreateSavePath, 1002 } 1003} 1004 1005func changeSort(c *gin.Context) { 1006 ret := gulu.Ret.NewResult() 1007 defer c.JSON(http.StatusOK, ret) 1008 1009 arg, ok := util.JsonArg(c, ret) 1010 if !ok { 1011 return 1012 } 1013 1014 notebook := arg["notebook"].(string) 1015 pathsArg := arg["paths"].([]interface{}) 1016 var paths []string 1017 for _, p := range pathsArg { 1018 paths = append(paths, p.(string)) 1019 } 1020 model.ChangeFileTreeSort(notebook, paths) 1021} 1022 1023func searchDocs(c *gin.Context) { 1024 ret := gulu.Ret.NewResult() 1025 defer c.JSON(http.StatusOK, ret) 1026 1027 arg, ok := util.JsonArg(c, ret) 1028 if !ok { 1029 return 1030 } 1031 1032 flashcard := false 1033 if arg["flashcard"] != nil { 1034 flashcard = arg["flashcard"].(bool) 1035 } 1036 1037 k := arg["k"].(string) 1038 ret.Data = model.SearchDocsByKeyword(k, flashcard) 1039} 1040 1041func listDocsByPath(c *gin.Context) { 1042 ret := gulu.Ret.NewResult() 1043 defer c.JSON(http.StatusOK, ret) 1044 1045 arg, ok := util.JsonArg(c, ret) 1046 if !ok { 1047 return 1048 } 1049 1050 notebook := arg["notebook"].(string) 1051 p := arg["path"].(string) 1052 sortParam := arg["sort"] 1053 sortMode := util.SortModeUnassigned 1054 if nil != sortParam { 1055 sortMode = int(sortParam.(float64)) 1056 } 1057 flashcard := false 1058 if arg["flashcard"] != nil { 1059 flashcard = arg["flashcard"].(bool) 1060 } 1061 maxListCount := model.Conf.FileTree.MaxListCount 1062 if arg["maxListCount"] != nil { 1063 // API `listDocsByPath` add an optional parameter `maxListCount` https://github.com/siyuan-note/siyuan/issues/7993 1064 maxListCount = int(arg["maxListCount"].(float64)) 1065 if 0 >= maxListCount { 1066 maxListCount = math.MaxInt 1067 } 1068 } 1069 showHidden := false 1070 if arg["showHidden"] != nil { 1071 showHidden = arg["showHidden"].(bool) 1072 } 1073 1074 files, totals, err := model.ListDocTree(notebook, p, sortMode, flashcard, showHidden, maxListCount) 1075 if err != nil { 1076 ret.Code = -1 1077 ret.Msg = err.Error() 1078 return 1079 } 1080 if maxListCount < totals { 1081 // API `listDocsByPath` add an optional parameter `ignoreMaxListHint` https://github.com/siyuan-note/siyuan/issues/10290 1082 ignoreMaxListHintArg := arg["ignoreMaxListHint"] 1083 if nil == ignoreMaxListHintArg || !ignoreMaxListHintArg.(bool) { 1084 var app string 1085 if nil != arg["app"] { 1086 app = arg["app"].(string) 1087 } 1088 util.PushMsgWithApp(app, fmt.Sprintf(model.Conf.Language(48), len(files)), 7000) 1089 } 1090 } 1091 1092 ret.Data = map[string]interface{}{ 1093 "box": notebook, 1094 "path": p, 1095 "files": files, 1096 } 1097} 1098 1099func getDoc(c *gin.Context) { 1100 ret := gulu.Ret.NewResult() 1101 defer c.JSON(http.StatusOK, ret) 1102 1103 arg, ok := util.JsonArg(c, ret) 1104 if !ok { 1105 return 1106 } 1107 1108 id := arg["id"].(string) 1109 idx := arg["index"] 1110 index := 0 1111 if nil != idx { 1112 index = int(idx.(float64)) 1113 } 1114 1115 var query string 1116 if queryArg := arg["query"]; nil != queryArg { 1117 query = queryArg.(string) 1118 } 1119 var queryMethod int 1120 if queryMethodArg := arg["queryMethod"]; nil != queryMethodArg { 1121 queryMethod = int(queryMethodArg.(float64)) 1122 } 1123 var queryTypes map[string]bool 1124 if queryTypesArg := arg["queryTypes"]; nil != queryTypesArg { 1125 typesArg := queryTypesArg.(map[string]interface{}) 1126 queryTypes = map[string]bool{} 1127 for t, b := range typesArg { 1128 queryTypes[t] = b.(bool) 1129 } 1130 } 1131 1132 m := arg["mode"] // 0: 仅当前 ID,1:向上 2:向下,3:上下都加载,4:加载末尾 1133 mode := 0 1134 if nil != m { 1135 mode = int(m.(float64)) 1136 } 1137 s := arg["size"] 1138 size := 102400 // 默认最大加载块数 1139 if nil != s { 1140 size = int(s.(float64)) 1141 } 1142 startID := "" 1143 endID := "" 1144 startIDArg := arg["startID"] 1145 endIDArg := arg["endID"] 1146 if nil != startIDArg && nil != endIDArg { 1147 startID = startIDArg.(string) 1148 endID = endIDArg.(string) 1149 size = model.Conf.Editor.DynamicLoadBlocks 1150 } 1151 isBacklinkArg := arg["isBacklink"] 1152 isBacklink := false 1153 if nil != isBacklinkArg { 1154 isBacklink = isBacklinkArg.(bool) 1155 } 1156 originalRefBlockIDsArg := arg["originalRefBlockIDs"] 1157 originalRefBlockIDs := map[string]string{} 1158 if nil != originalRefBlockIDsArg { 1159 m := originalRefBlockIDsArg.(map[string]interface{}) 1160 for k, v := range m { 1161 originalRefBlockIDs[k] = v.(string) 1162 } 1163 } 1164 highlightArg := arg["highlight"] 1165 highlight := true 1166 if nil != highlightArg { 1167 highlight = highlightArg.(bool) 1168 } 1169 1170 blockCount, content, parentID, parent2ID, rootID, typ, eof, scroll, boxID, docPath, isBacklinkExpand, keywords, err := 1171 model.GetDoc(startID, endID, id, index, query, queryTypes, queryMethod, mode, size, isBacklink, originalRefBlockIDs, highlight) 1172 if model.ErrBlockNotFound == err { 1173 ret.Code = 3 1174 return 1175 } 1176 1177 if err != nil { 1178 ret.Code = 1 1179 ret.Msg = err.Error() 1180 return 1181 } 1182 1183 // 判断是否正在同步中 https://github.com/siyuan-note/siyuan/issues/6290 1184 isSyncing := model.IsSyncingFile(rootID) 1185 1186 ret.Data = map[string]interface{}{ 1187 "id": id, 1188 "mode": mode, 1189 "parentID": parentID, 1190 "parent2ID": parent2ID, 1191 "rootID": rootID, 1192 "type": typ, 1193 "content": content, 1194 "blockCount": blockCount, 1195 "eof": eof, 1196 "scroll": scroll, 1197 "box": boxID, 1198 "path": docPath, 1199 "isSyncing": isSyncing, 1200 "isBacklinkExpand": isBacklinkExpand, 1201 "keywords": keywords, 1202 "reqId": arg["reqId"], 1203 } 1204} 1205 1206func pushCreate(box *model.Box, p string, arg map[string]interface{}) { 1207 evt := util.NewCmdResult("create", 0, util.PushModeBroadcast) 1208 listDocTree := false 1209 listDocTreeArg := arg["listDocTree"] 1210 if nil != listDocTreeArg { 1211 listDocTree = listDocTreeArg.(bool) 1212 } 1213 1214 evt.Data = map[string]interface{}{ 1215 "box": box, 1216 "path": p, 1217 "listDocTree": listDocTree, 1218 } 1219 evt.Callback = arg["callback"] 1220 util.PushEvent(evt) 1221}