A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at upstream/main 801 lines 17 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 "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}