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 "io"
21 "mime"
22 "net/http"
23 "net/url"
24 "os"
25 "path"
26 "path/filepath"
27 "strings"
28 "time"
29
30 "github.com/88250/gulu"
31 "github.com/88250/lute/parse"
32 "github.com/gin-gonic/gin"
33 "github.com/mssola/useragent"
34 "github.com/siyuan-note/filelock"
35 "github.com/siyuan-note/logging"
36 "github.com/siyuan-note/siyuan/kernel/model"
37 "github.com/siyuan-note/siyuan/kernel/util"
38)
39
40func exportAttributeView(c *gin.Context) {
41 ret := gulu.Ret.NewResult()
42 defer c.JSON(http.StatusOK, ret)
43
44 arg, ok := util.JsonArg(c, ret)
45 if !ok {
46 return
47 }
48
49 avID := arg["id"].(string)
50 blockID := arg["blockID"].(string)
51 zipPath, err := model.ExportAv2CSV(avID, blockID)
52 if err != nil {
53 ret.Code = 1
54 ret.Msg = err.Error()
55 ret.Data = map[string]interface{}{"closeTimeout": 7000}
56 return
57 }
58
59 ret.Data = map[string]interface{}{
60 "zip": zipPath,
61 }
62}
63
64func exportEPUB(c *gin.Context) {
65 ret := gulu.Ret.NewResult()
66 defer c.JSON(http.StatusOK, ret)
67
68 arg, ok := util.JsonArg(c, ret)
69 if !ok {
70 return
71 }
72
73 id := arg["id"].(string)
74 name, zipPath := model.ExportPandocConvertZip([]string{id}, "epub", ".epub")
75 ret.Data = map[string]interface{}{
76 "name": name,
77 "zip": zipPath,
78 }
79}
80
81func exportRTF(c *gin.Context) {
82 ret := gulu.Ret.NewResult()
83 defer c.JSON(http.StatusOK, ret)
84
85 arg, ok := util.JsonArg(c, ret)
86 if !ok {
87 return
88 }
89
90 id := arg["id"].(string)
91 name, zipPath := model.ExportPandocConvertZip([]string{id}, "rtf", ".rtf")
92 ret.Data = map[string]interface{}{
93 "name": name,
94 "zip": zipPath,
95 }
96}
97
98func exportODT(c *gin.Context) {
99 ret := gulu.Ret.NewResult()
100 defer c.JSON(http.StatusOK, ret)
101
102 arg, ok := util.JsonArg(c, ret)
103 if !ok {
104 return
105 }
106
107 id := arg["id"].(string)
108 name, zipPath := model.ExportPandocConvertZip([]string{id}, "odt", ".odt")
109 ret.Data = map[string]interface{}{
110 "name": name,
111 "zip": zipPath,
112 }
113}
114
115func exportMediaWiki(c *gin.Context) {
116 ret := gulu.Ret.NewResult()
117 defer c.JSON(http.StatusOK, ret)
118
119 arg, ok := util.JsonArg(c, ret)
120 if !ok {
121 return
122 }
123
124 id := arg["id"].(string)
125 name, zipPath := model.ExportPandocConvertZip([]string{id}, "mediawiki", ".wiki")
126 ret.Data = map[string]interface{}{
127 "name": name,
128 "zip": zipPath,
129 }
130}
131
132func exportOrgMode(c *gin.Context) {
133 ret := gulu.Ret.NewResult()
134 defer c.JSON(http.StatusOK, ret)
135
136 arg, ok := util.JsonArg(c, ret)
137 if !ok {
138 return
139 }
140
141 id := arg["id"].(string)
142 name, zipPath := model.ExportPandocConvertZip([]string{id}, "org", ".org")
143 ret.Data = map[string]interface{}{
144 "name": name,
145 "zip": zipPath,
146 }
147}
148
149func exportOPML(c *gin.Context) {
150 ret := gulu.Ret.NewResult()
151 defer c.JSON(http.StatusOK, ret)
152
153 arg, ok := util.JsonArg(c, ret)
154 if !ok {
155 return
156 }
157
158 id := arg["id"].(string)
159 name, zipPath := model.ExportPandocConvertZip([]string{id}, "opml", ".opml")
160 ret.Data = map[string]interface{}{
161 "name": name,
162 "zip": zipPath,
163 }
164}
165
166func exportTextile(c *gin.Context) {
167 ret := gulu.Ret.NewResult()
168 defer c.JSON(http.StatusOK, ret)
169
170 arg, ok := util.JsonArg(c, ret)
171 if !ok {
172 return
173 }
174
175 id := arg["id"].(string)
176 name, zipPath := model.ExportPandocConvertZip([]string{id}, "textile", ".textile")
177 ret.Data = map[string]interface{}{
178 "name": name,
179 "zip": zipPath,
180 }
181}
182
183func exportAsciiDoc(c *gin.Context) {
184 ret := gulu.Ret.NewResult()
185 defer c.JSON(http.StatusOK, ret)
186
187 arg, ok := util.JsonArg(c, ret)
188 if !ok {
189 return
190 }
191
192 id := arg["id"].(string)
193 name, zipPath := model.ExportPandocConvertZip([]string{id}, "asciidoc", ".adoc")
194 ret.Data = map[string]interface{}{
195 "name": name,
196 "zip": zipPath,
197 }
198}
199
200func exportReStructuredText(c *gin.Context) {
201 ret := gulu.Ret.NewResult()
202 defer c.JSON(http.StatusOK, ret)
203
204 arg, ok := util.JsonArg(c, ret)
205 if !ok {
206 return
207 }
208
209 id := arg["id"].(string)
210 name, zipPath := model.ExportPandocConvertZip([]string{id}, "rst", ".rst")
211 ret.Data = map[string]interface{}{
212 "name": name,
213 "zip": zipPath,
214 }
215}
216
217func export2Liandi(c *gin.Context) {
218 ret := gulu.Ret.NewResult()
219 defer c.JSON(http.StatusOK, ret)
220
221 arg, ok := util.JsonArg(c, ret)
222 if !ok {
223 return
224 }
225
226 id := arg["id"].(string)
227 err := model.Export2Liandi(id)
228 if err != nil {
229 ret.Code = -1
230 ret.Msg = err.Error()
231 return
232 }
233}
234
235func exportDataInFolder(c *gin.Context) {
236 ret := gulu.Ret.NewResult()
237 defer c.JSON(http.StatusOK, ret)
238
239 arg, ok := util.JsonArg(c, ret)
240 if !ok {
241 return
242 }
243
244 exportFolder := arg["folder"].(string)
245 name, err := model.ExportDataInFolder(exportFolder)
246 if err != nil {
247 ret.Code = -1
248 ret.Msg = err.Error()
249 ret.Data = map[string]interface{}{"closeTimeout": 7000}
250 return
251 }
252 ret.Data = map[string]interface{}{
253 "name": name,
254 }
255}
256
257func exportData(c *gin.Context) {
258 ret := gulu.Ret.NewResult()
259 defer c.JSON(http.StatusOK, ret)
260
261 zipPath, err := model.ExportData()
262 if err != nil {
263 ret.Code = 1
264 ret.Msg = err.Error()
265 ret.Data = map[string]interface{}{"closeTimeout": 7000}
266 return
267 }
268 ret.Data = map[string]interface{}{
269 "zip": zipPath,
270 }
271}
272
273func exportResources(c *gin.Context) {
274 ret := gulu.Ret.NewResult()
275 defer c.JSON(http.StatusOK, ret)
276
277 arg, ok := util.JsonArg(c, ret)
278 if !ok {
279 return
280 }
281
282 var name string
283 if nil != arg["name"] {
284 name = util.TruncateLenFileName(arg["name"].(string))
285 }
286 if name == "" {
287 name = time.Now().Format("export-2006-01-02_15-04-05") // 生成的 *.zip 文件主文件名
288 }
289
290 if nil == arg["paths"] {
291 ret.Code = 1
292 ret.Data = ""
293 ret.Msg = "paths is required"
294 return
295 }
296
297 var resourcePaths []string // 文件/文件夹在工作空间中的路径
298 for _, resourcePath := range arg["paths"].([]interface{}) {
299 resourcePaths = append(resourcePaths, resourcePath.(string))
300 }
301
302 zipFilePath, err := model.ExportResources(resourcePaths, name)
303 if err != nil {
304 ret.Code = 1
305 ret.Msg = err.Error()
306 ret.Data = map[string]interface{}{"closeTimeout": 7000}
307 return
308 }
309 ret.Data = map[string]interface{}{
310 "path": zipFilePath, // 相对于工作空间目录的路径
311 }
312}
313
314func exportNotebookMd(c *gin.Context) {
315 ret := gulu.Ret.NewResult()
316 defer c.JSON(http.StatusOK, ret)
317
318 arg, ok := util.JsonArg(c, ret)
319 if !ok {
320 return
321 }
322
323 notebook := arg["notebook"].(string)
324 zipPath := model.ExportNotebookMarkdown(notebook)
325 ret.Data = map[string]interface{}{
326 "name": path.Base(zipPath),
327 "zip": zipPath,
328 }
329}
330
331func exportMds(c *gin.Context) {
332 ret := gulu.Ret.NewResult()
333 defer c.JSON(http.StatusOK, ret)
334
335 arg, ok := util.JsonArg(c, ret)
336 if !ok {
337 return
338 }
339
340 idsArg := arg["ids"].([]interface{})
341 var ids []string
342 for _, id := range idsArg {
343 ids = append(ids, id.(string))
344 }
345
346 name, zipPath := model.ExportPandocConvertZip(ids, "", ".md")
347 ret.Data = map[string]interface{}{
348 "name": name,
349 "zip": zipPath,
350 }
351}
352
353func exportMd(c *gin.Context) {
354 ret := gulu.Ret.NewResult()
355 defer c.JSON(http.StatusOK, ret)
356
357 arg, ok := util.JsonArg(c, ret)
358 if !ok {
359 return
360 }
361
362 id := arg["id"].(string)
363 name, zipPath := model.ExportPandocConvertZip([]string{id}, "", ".md")
364 ret.Data = map[string]interface{}{
365 "name": name,
366 "zip": zipPath,
367 }
368}
369
370func exportNotebookSY(c *gin.Context) {
371 ret := gulu.Ret.NewResult()
372 defer c.JSON(http.StatusOK, ret)
373
374 arg, ok := util.JsonArg(c, ret)
375 if !ok {
376 return
377 }
378
379 id := arg["id"].(string)
380 zipPath := model.ExportNotebookSY(id)
381 ret.Data = map[string]interface{}{
382 "zip": zipPath,
383 }
384}
385
386func exportSY(c *gin.Context) {
387 ret := gulu.Ret.NewResult()
388 defer c.JSON(http.StatusOK, ret)
389
390 arg, ok := util.JsonArg(c, ret)
391 if !ok {
392 return
393 }
394
395 id := arg["id"].(string)
396 name, zipPath := model.ExportSY(id)
397 ret.Data = map[string]interface{}{
398 "name": name,
399 "zip": zipPath,
400 }
401}
402
403func exportMdContent(c *gin.Context) {
404 ret := gulu.Ret.NewResult()
405 defer c.JSON(http.StatusOK, ret)
406
407 arg, ok := util.JsonArg(c, ret)
408 if !ok {
409 return
410 }
411
412 id := arg["id"].(string)
413 if util.InvalidIDPattern(id, ret) {
414 return
415 }
416
417 refMode := model.Conf.Export.BlockRefMode
418 if nil != arg["refMode"] {
419 refMode = int(arg["refMode"].(float64))
420 }
421
422 embedMode := model.Conf.Export.BlockEmbedMode
423 if nil != arg["embedMode"] {
424 embedMode = int(arg["embedMode"].(float64))
425 }
426
427 yfm := true
428 if nil != arg["yfm"] {
429 yfm = arg["yfm"].(bool)
430 }
431
432 fillCSSVar := false
433 if nil != arg["fillCSSVar"] {
434 fillCSSVar = arg["fillCSSVar"].(bool)
435 }
436
437 adjustHeadingLevel := false
438 if nil != arg["adjustHeadingLevel"] {
439 adjustHeadingLevel = arg["adjustHeadingLevel"].(bool)
440 }
441
442 imgTag := false
443 if nil != arg["imgTag"] {
444 imgTag = arg["imgTag"].(bool)
445 }
446
447 hPath, content := model.ExportMarkdownContent(id, refMode, embedMode, yfm, fillCSSVar, adjustHeadingLevel, imgTag)
448 ret.Data = map[string]interface{}{
449 "hPath": hPath,
450 "content": content,
451 }
452}
453
454func exportDocx(c *gin.Context) {
455 ret := gulu.Ret.NewResult()
456 defer c.JSON(http.StatusOK, ret)
457
458 arg, ok := util.JsonArg(c, ret)
459 if !ok {
460 return
461 }
462
463 id := arg["id"].(string)
464 savePath := arg["savePath"].(string)
465 removeAssets := arg["removeAssets"].(bool)
466 merge := false
467 if nil != arg["merge"] {
468 merge = arg["merge"].(bool)
469 }
470
471 fullPath, err := model.ExportDocx(id, savePath, removeAssets, merge)
472 if err != nil {
473 ret.Code = -1
474 ret.Msg = err.Error()
475 ret.Data = map[string]interface{}{"closeTimeout": 7000}
476 return
477 }
478 ret.Data = map[string]interface{}{
479 "path": fullPath,
480 }
481}
482
483func exportMdHTML(c *gin.Context) {
484 ret := gulu.Ret.NewResult()
485 defer c.JSON(http.StatusOK, ret)
486
487 arg, ok := util.JsonArg(c, ret)
488 if !ok {
489 return
490 }
491
492 id := arg["id"].(string)
493 savePath := arg["savePath"].(string)
494
495 savePath = strings.TrimSpace(savePath)
496 if savePath == "" {
497 folderName := "htmlmd-" + id + "-" + util.CurrentTimeSecondsStr()
498 tmpDir := filepath.Join(util.TempDir, "export", folderName)
499 name, content := model.ExportMarkdownHTML(id, tmpDir, false, false)
500 ret.Data = map[string]interface{}{
501 "id": id,
502 "name": name,
503 "content": content,
504 "folder": folderName,
505 }
506 return
507 }
508
509 name, content := model.ExportMarkdownHTML(id, savePath, false, false)
510 ret.Data = map[string]interface{}{
511 "id": id,
512 "name": name,
513 "content": content,
514 }
515}
516
517func exportTempContent(c *gin.Context) {
518 ret := gulu.Ret.NewResult()
519 defer c.JSON(http.StatusOK, ret)
520
521 arg, ok := util.JsonArg(c, ret)
522 if !ok {
523 return
524 }
525
526 content := arg["content"].(string)
527 tmpExport := filepath.Join(util.TempDir, "export", "temp")
528 if err := os.MkdirAll(tmpExport, 0755); err != nil {
529 ret.Code = 1
530 ret.Msg = err.Error()
531 ret.Data = map[string]interface{}{"closeTimeout": 7000}
532 return
533 }
534 p := filepath.Join(tmpExport, gulu.Rand.String(7))
535 if err := os.WriteFile(p, []byte(content), 0644); err != nil {
536 ret.Code = 1
537 ret.Msg = err.Error()
538 ret.Data = map[string]interface{}{"closeTimeout": 7000}
539 return
540 }
541 url := path.Join("/export/temp/", filepath.Base(p))
542 ret.Data = map[string]interface{}{
543 "url": "http://" + util.LocalHost + ":" + util.ServerPort + url,
544 }
545}
546
547func exportBrowserHTML(c *gin.Context) {
548 ret := gulu.Ret.NewResult()
549 defer c.JSON(http.StatusOK, ret)
550
551 arg, ok := util.JsonArg(c, ret)
552 if !ok {
553 return
554 }
555
556 folder := arg["folder"].(string)
557 htmlContent := arg["html"].(string)
558 name := arg["name"].(string)
559
560 tmpDir := filepath.Join(util.TempDir, "export", folder)
561
562 htmlPath := filepath.Join(tmpDir, "index.html")
563 if err := filelock.WriteFile(htmlPath, []byte(htmlContent)); err != nil {
564 ret.Code = -1
565 ret.Msg = err.Error()
566 ret.Data = nil
567 return
568 }
569
570 zipFileName := util.FilterFileName(name) + ".zip"
571 zipPath := filepath.Join(util.TempDir, "export", zipFileName)
572 zip, err := gulu.Zip.Create(zipPath)
573 if err != nil {
574 ret.Code = -1
575 ret.Msg = err.Error()
576 ret.Data = nil
577 return
578 }
579
580 err = zip.AddDirectory("", tmpDir, func(string) {})
581 if err != nil {
582 ret.Code = -1
583 ret.Msg = err.Error()
584 ret.Data = nil
585 return
586 }
587
588 if err = zip.Close(); err != nil {
589 ret.Code = -1
590 ret.Msg = err.Error()
591 ret.Data = nil
592 return
593 }
594
595 os.RemoveAll(tmpDir)
596
597 zipURL := "/export/" + url.PathEscape(filepath.Base(zipPath))
598 ret.Data = map[string]interface{}{
599 "zip": zipURL,
600 }
601}
602
603func exportPreviewHTML(c *gin.Context) {
604 ret := gulu.Ret.NewResult()
605 defer c.JSON(http.StatusOK, ret)
606
607 arg, ok := util.JsonArg(c, ret)
608 if !ok {
609 return
610 }
611
612 id := arg["id"].(string)
613 keepFold := false
614 if nil != arg["keepFold"] {
615 keepFold = arg["keepFold"].(bool)
616 }
617 merge := false
618 if nil != arg["merge"] {
619 merge = arg["merge"].(bool)
620 }
621 image := false
622 if nil != arg["image"] {
623 image = arg["image"].(bool)
624 }
625 name, content, node := model.ExportHTML(id, "", true, image, keepFold, merge)
626 // 导出 PDF 预览时点击块引转换后的脚注跳转不正确 https://github.com/siyuan-note/siyuan/issues/5894
627 content = strings.ReplaceAll(content, "http://"+util.LocalHost+":"+util.ServerPort+"/#", "#")
628
629 // Add `data-doc-type` and attribute when exporting image and PDF https://github.com/siyuan-note/siyuan/issues/9497
630 attrs := map[string]string{}
631 var typ string
632 if nil != node {
633 attrs = parse.IAL2Map(node.KramdownIAL)
634 typ = node.Type.String()
635 }
636
637 ret.Data = map[string]interface{}{
638 "id": id,
639 "name": name,
640 "content": content,
641 "attrs": attrs,
642 "type": typ,
643 }
644}
645
646func exportHTML(c *gin.Context) {
647 ret := gulu.Ret.NewResult()
648 defer c.JSON(http.StatusOK, ret)
649
650 arg, ok := util.JsonArg(c, ret)
651 if !ok {
652 return
653 }
654
655 id := arg["id"].(string)
656 pdf := arg["pdf"].(bool)
657 savePath := arg["savePath"].(string)
658 keepFold := false
659 if nil != arg["keepFold"] {
660 keepFold = arg["keepFold"].(bool)
661 }
662 merge := false
663 if nil != arg["merge"] {
664 merge = arg["merge"].(bool)
665 }
666
667 savePath = strings.TrimSpace(savePath)
668 if savePath == "" {
669 folderName := "html-" + id + "-" + util.CurrentTimeSecondsStr()
670 tmpDir := filepath.Join(util.TempDir, "export", folderName)
671 name, content, _ := model.ExportHTML(id, tmpDir, pdf, false, keepFold, merge)
672 ret.Data = map[string]interface{}{
673 "id": id,
674 "name": name,
675 "content": content,
676 "folder": folderName,
677 }
678 return
679 }
680
681 name, content, _ := model.ExportHTML(id, savePath, pdf, false, keepFold, merge)
682 ret.Data = map[string]interface{}{
683 "id": id,
684 "name": name,
685 "content": content,
686 }
687}
688
689func processPDF(c *gin.Context) {
690 ret := gulu.Ret.NewResult()
691 defer c.JSON(http.StatusOK, ret)
692
693 arg, ok := util.JsonArg(c, ret)
694 if !ok {
695 return
696 }
697
698 id := arg["id"].(string)
699 path := arg["path"].(string)
700 merge := false
701 if nil != arg["merge"] {
702 merge = arg["merge"].(bool)
703 }
704 removeAssets := arg["removeAssets"].(bool)
705 watermark := arg["watermark"].(bool)
706 err := model.ProcessPDF(id, path, merge, removeAssets, watermark)
707 if err != nil {
708 ret.Code = -1
709 ret.Msg = err.Error()
710 return
711 }
712}
713
714func exportPreview(c *gin.Context) {
715 ret := gulu.Ret.NewResult()
716 defer c.JSON(http.StatusOK, ret)
717
718 arg, ok := util.JsonArg(c, ret)
719 if !ok {
720 return
721 }
722
723 id := arg["id"].(string)
724
725 userAgentStr := c.GetHeader("User-Agent")
726 fillCSSVar := true
727 if userAgentStr != "" {
728 ua := useragent.New(userAgentStr)
729 name, _ := ua.Browser()
730 // Chrome、Edge、SiYuan 桌面端不需要替换 CSS 变量
731 if !ua.Mobile() && (name == "Chrome" || name == "Edge" || strings.Contains(userAgentStr, "Electron") || strings.Contains(userAgentStr, "SiYuan/")) {
732 fillCSSVar = false
733 }
734 }
735
736 stdHTML := model.Preview(id, fillCSSVar)
737 ret.Data = map[string]interface{}{
738 "html": stdHTML,
739 "fillCSSVar": fillCSSVar,
740 }
741}
742
743func exportAsFile(c *gin.Context) {
744 ret := gulu.Ret.NewResult()
745 defer c.JSON(http.StatusOK, ret)
746
747 form, err := c.MultipartForm()
748 if err != nil {
749 logging.LogErrorf("export as file failed: %s", err)
750 ret.Code = -1
751 ret.Msg = err.Error()
752 return
753 }
754
755 file := form.File["file"][0]
756 reader, err := file.Open()
757 if err != nil {
758 logging.LogErrorf("export as file failed: %s", err)
759 ret.Code = -1
760 ret.Msg = err.Error()
761 return
762 }
763 defer reader.Close()
764
765 data, err := io.ReadAll(reader)
766 if err != nil {
767 logging.LogErrorf("export as file failed: %s", err)
768 ret.Code = -1
769 ret.Msg = err.Error()
770 return
771 }
772
773 name := "file-" + file.Filename
774 typ := form.Value["type"][0]
775 exts, _ := mime.ExtensionsByType(typ)
776 if 0 < len(exts) && filepath.Ext(name) != exts[0] {
777 name += exts[0]
778 }
779 name = util.FilterFileName(name)
780 name = strings.ReplaceAll(name, "#", "_")
781 tmpDir := filepath.Join(util.TempDir, "export")
782 if err = os.MkdirAll(tmpDir, 0755); err != nil {
783 logging.LogErrorf("export as file failed: %s", err)
784 ret.Code = -1
785 ret.Msg = err.Error()
786 return
787 }
788
789 tmp := filepath.Join(tmpDir, name)
790 err = os.WriteFile(tmp, data, 0644)
791 if err != nil {
792 logging.LogErrorf("export as file failed: %s", err)
793 ret.Code = -1
794 ret.Msg = err.Error()
795 return
796 }
797
798 ret.Data = map[string]interface{}{
799 "file": path.Join("/export/", name),
800 }
801}