A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1// SiYuan - Refactor your thinking
2// Copyright (c) 2020-present, b3log.org
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17package 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}