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 "encoding/hex"
21 "io"
22 "net/http"
23 "os"
24 "path/filepath"
25 "strings"
26 "time"
27
28 "github.com/siyuan-note/logging"
29
30 "github.com/88250/gulu"
31 "github.com/gin-gonic/gin"
32 "github.com/siyuan-note/siyuan/kernel/conf"
33 "github.com/siyuan-note/siyuan/kernel/model"
34 "github.com/siyuan-note/siyuan/kernel/util"
35)
36
37func importSyncProviderWebDAV(c *gin.Context) {
38 ret := gulu.Ret.NewResult()
39 defer c.JSON(200, ret)
40
41 form, err := c.MultipartForm()
42 if err != nil {
43 logging.LogErrorf("read upload file failed: %s", err)
44 ret.Code = -1
45 ret.Msg = err.Error()
46 return
47 }
48
49 files := form.File["file"]
50 if 1 != len(files) {
51 ret.Code = -1
52 ret.Msg = "invalid upload file"
53 return
54 }
55
56 f := files[0]
57 fh, err := f.Open()
58 if err != nil {
59 logging.LogErrorf("read upload file failed: %s", err)
60 ret.Code = -1
61 ret.Msg = err.Error()
62 return
63 }
64
65 data, err := io.ReadAll(fh)
66 fh.Close()
67 if err != nil {
68 logging.LogErrorf("read upload file failed: %s", err)
69 ret.Code = -1
70 ret.Msg = err.Error()
71 return
72 }
73
74 importDir := filepath.Join(util.TempDir, "import")
75 if err = os.MkdirAll(importDir, 0755); err != nil {
76 logging.LogErrorf("import WebDAV provider failed: %s", err)
77 ret.Code = -1
78 ret.Msg = err.Error()
79 return
80 }
81
82 tmp := filepath.Join(importDir, f.Filename)
83 if err = os.WriteFile(tmp, data, 0644); err != nil {
84 logging.LogErrorf("import WebDAV provider failed: %s", err)
85 ret.Code = -1
86 ret.Msg = err.Error()
87 return
88 }
89
90 tmpDir := filepath.Join(importDir, "webdav")
91 os.RemoveAll(tmpDir)
92 if strings.HasSuffix(strings.ToLower(tmp), ".zip") {
93 if err = gulu.Zip.Unzip(tmp, tmpDir); err != nil {
94 logging.LogErrorf("import WebDAV provider failed: %s", err)
95 ret.Code = -1
96 ret.Msg = err.Error()
97 return
98 }
99 } else if strings.HasSuffix(strings.ToLower(tmp), ".json") {
100 if err = gulu.File.CopyFile(tmp, filepath.Join(tmpDir, f.Filename)); err != nil {
101 logging.LogErrorf("import WebDAV provider failed: %s", err)
102 ret.Code = -1
103 ret.Msg = err.Error()
104 }
105 } else {
106 logging.LogErrorf("invalid WebDAV provider package")
107 ret.Code = -1
108 ret.Msg = "invalid WebDAV provider package"
109 return
110 }
111
112 entries, err := os.ReadDir(tmpDir)
113 if err != nil {
114 logging.LogErrorf("import WebDAV provider failed: %s", err)
115 ret.Code = -1
116 ret.Msg = err.Error()
117 return
118 }
119
120 if 1 != len(entries) {
121 logging.LogErrorf("invalid WebDAV provider package")
122 ret.Code = -1
123 ret.Msg = "invalid WebDAV provider package"
124 return
125 }
126
127 tmp = filepath.Join(tmpDir, entries[0].Name())
128 data, err = os.ReadFile(tmp)
129 if err != nil {
130 logging.LogErrorf("import WebDAV provider failed: %s", err)
131 ret.Code = -1
132 ret.Msg = err.Error()
133 return
134 }
135
136 data = util.AESDecrypt(string(data))
137 data, _ = hex.DecodeString(string(data))
138 webdav := &conf.WebDAV{}
139 if err = gulu.JSON.UnmarshalJSON(data, webdav); err != nil {
140 logging.LogErrorf("import WebDAV provider failed: %s", err)
141 ret.Code = -1
142 ret.Msg = err.Error()
143 return
144 }
145
146 err = model.SetSyncProviderWebDAV(webdav)
147 if err != nil {
148 logging.LogErrorf("import WebDAV provider failed: %s", err)
149 ret.Code = -1
150 ret.Msg = err.Error()
151 return
152 }
153
154 ret.Data = map[string]interface{}{
155 "webdav": model.Conf.Sync.WebDAV,
156 }
157}
158
159func exportSyncProviderWebDAV(c *gin.Context) {
160 ret := gulu.Ret.NewResult()
161 defer c.JSON(http.StatusOK, ret)
162
163 name := "siyuan-webdav-" + time.Now().Format("20060102150405") + ".json"
164 tmpDir := filepath.Join(util.TempDir, "export")
165 if err := os.MkdirAll(tmpDir, 0755); err != nil {
166 logging.LogErrorf("export WebDAV provider failed: %s", err)
167 ret.Code = -1
168 ret.Msg = err.Error()
169 return
170 }
171
172 webdav := model.Conf.Sync.WebDAV
173 if nil == webdav {
174 webdav = &conf.WebDAV{}
175 }
176
177 data, err := gulu.JSON.MarshalJSON(model.Conf.Sync.WebDAV)
178 if err != nil {
179 logging.LogErrorf("export WebDAV provider failed: %s", err)
180 ret.Code = -1
181 ret.Msg = err.Error()
182 return
183 }
184
185 dataStr := util.AESEncrypt(string(data))
186 tmp := filepath.Join(tmpDir, name)
187 if err = os.WriteFile(tmp, []byte(dataStr), 0644); err != nil {
188 logging.LogErrorf("export WebDAV provider failed: %s", err)
189 ret.Code = -1
190 ret.Msg = err.Error()
191 return
192 }
193
194 zipFile, err := gulu.Zip.Create(tmp + ".zip")
195 if err != nil {
196 logging.LogErrorf("export WebDAV provider failed: %s", err)
197 ret.Code = -1
198 ret.Msg = err.Error()
199 return
200 }
201
202 if err = zipFile.AddEntry(name, tmp); err != nil {
203 logging.LogErrorf("export WebDAV provider failed: %s", err)
204 ret.Code = -1
205 ret.Msg = err.Error()
206 return
207 }
208
209 if err = zipFile.Close(); err != nil {
210 logging.LogErrorf("export WebDAV provider failed: %s", err)
211 ret.Code = -1
212 ret.Msg = err.Error()
213 return
214 }
215
216 zipPath := "/export/" + name + ".zip"
217 ret.Data = map[string]interface{}{
218 "name": name,
219 "zip": zipPath,
220 }
221}
222
223func importSyncProviderS3(c *gin.Context) {
224 ret := gulu.Ret.NewResult()
225 defer c.JSON(200, ret)
226
227 form, err := c.MultipartForm()
228 if err != nil {
229 logging.LogErrorf("read upload file failed: %s", err)
230 ret.Code = -1
231 ret.Msg = err.Error()
232 return
233 }
234
235 files := form.File["file"]
236 if 1 != len(files) {
237 ret.Code = -1
238 ret.Msg = "invalid upload file"
239 return
240 }
241
242 f := files[0]
243 fh, err := f.Open()
244 if err != nil {
245 logging.LogErrorf("read upload file failed: %s", err)
246 ret.Code = -1
247 ret.Msg = err.Error()
248 return
249 }
250
251 data, err := io.ReadAll(fh)
252 fh.Close()
253 if err != nil {
254 logging.LogErrorf("read upload file failed: %s", err)
255 ret.Code = -1
256 ret.Msg = err.Error()
257 return
258 }
259
260 importDir := filepath.Join(util.TempDir, "import")
261 if err = os.MkdirAll(importDir, 0755); err != nil {
262 logging.LogErrorf("import S3 provider failed: %s", err)
263 ret.Code = -1
264 ret.Msg = err.Error()
265 return
266 }
267
268 tmp := filepath.Join(importDir, f.Filename)
269 if err = os.WriteFile(tmp, data, 0644); err != nil {
270 logging.LogErrorf("import S3 provider failed: %s", err)
271 ret.Code = -1
272 ret.Msg = err.Error()
273 return
274 }
275
276 tmpDir := filepath.Join(importDir, "s3")
277 os.RemoveAll(tmpDir)
278 if strings.HasSuffix(strings.ToLower(tmp), ".zip") {
279 if err = gulu.Zip.Unzip(tmp, tmpDir); err != nil {
280 logging.LogErrorf("import S3 provider failed: %s", err)
281 ret.Code = -1
282 ret.Msg = err.Error()
283 return
284 }
285 } else if strings.HasSuffix(strings.ToLower(tmp), ".json") {
286 if err = gulu.File.CopyFile(tmp, filepath.Join(tmpDir, f.Filename)); err != nil {
287 logging.LogErrorf("import S3 provider failed: %s", err)
288 ret.Code = -1
289 ret.Msg = err.Error()
290 }
291 } else {
292 logging.LogErrorf("invalid S3 provider package")
293 ret.Code = -1
294 ret.Msg = "invalid S3 provider package"
295 return
296 }
297
298 entries, err := os.ReadDir(tmpDir)
299 if err != nil {
300 logging.LogErrorf("import S3 provider failed: %s", err)
301 ret.Code = -1
302 ret.Msg = err.Error()
303 return
304 }
305
306 if 1 != len(entries) {
307 logging.LogErrorf("invalid S3 provider package")
308 ret.Code = -1
309 ret.Msg = "invalid S3 provider package"
310 return
311 }
312
313 tmp = filepath.Join(tmpDir, entries[0].Name())
314 data, err = os.ReadFile(tmp)
315 if err != nil {
316 logging.LogErrorf("import S3 provider failed: %s", err)
317 ret.Code = -1
318 ret.Msg = err.Error()
319 return
320 }
321
322 data = util.AESDecrypt(string(data))
323 data, _ = hex.DecodeString(string(data))
324 s3 := &conf.S3{}
325 if err = gulu.JSON.UnmarshalJSON(data, s3); err != nil {
326 logging.LogErrorf("import S3 provider failed: %s", err)
327 ret.Code = -1
328 ret.Msg = err.Error()
329 return
330 }
331
332 err = model.SetSyncProviderS3(s3)
333 if err != nil {
334 logging.LogErrorf("import S3 provider failed: %s", err)
335 ret.Code = -1
336 ret.Msg = err.Error()
337 return
338 }
339
340 ret.Data = map[string]interface{}{
341 "s3": model.Conf.Sync.S3,
342 }
343}
344
345func exportSyncProviderS3(c *gin.Context) {
346 ret := gulu.Ret.NewResult()
347 defer c.JSON(http.StatusOK, ret)
348
349 name := "siyuan-s3-" + time.Now().Format("20060102150405") + ".json"
350 tmpDir := filepath.Join(util.TempDir, "export")
351 if err := os.MkdirAll(tmpDir, 0755); err != nil {
352 logging.LogErrorf("export S3 provider failed: %s", err)
353 ret.Code = -1
354 ret.Msg = err.Error()
355 return
356 }
357
358 s3 := model.Conf.Sync.S3
359 if nil == s3 {
360 s3 = &conf.S3{}
361 }
362
363 data, err := gulu.JSON.MarshalJSON(model.Conf.Sync.S3)
364 if err != nil {
365 logging.LogErrorf("export S3 provider failed: %s", err)
366 ret.Code = -1
367 ret.Msg = err.Error()
368 return
369 }
370
371 dataStr := util.AESEncrypt(string(data))
372 tmp := filepath.Join(tmpDir, name)
373 if err = os.WriteFile(tmp, []byte(dataStr), 0644); err != nil {
374 logging.LogErrorf("export S3 provider failed: %s", err)
375 ret.Code = -1
376 ret.Msg = err.Error()
377 return
378 }
379
380 zipFile, err := gulu.Zip.Create(tmp + ".zip")
381 if err != nil {
382 logging.LogErrorf("export S3 provider failed: %s", err)
383 ret.Code = -1
384 ret.Msg = err.Error()
385 return
386 }
387
388 if err = zipFile.AddEntry(name, tmp); err != nil {
389 logging.LogErrorf("export S3 provider failed: %s", err)
390 ret.Code = -1
391 ret.Msg = err.Error()
392 return
393 }
394
395 if err = zipFile.Close(); err != nil {
396 logging.LogErrorf("export S3 provider failed: %s", err)
397 ret.Code = -1
398 ret.Msg = err.Error()
399 return
400 }
401
402 zipPath := "/export/" + name + ".zip"
403 ret.Data = map[string]interface{}{
404 "name": name,
405 "zip": zipPath,
406 }
407}
408
409func getSyncInfo(c *gin.Context) {
410 ret := gulu.Ret.NewResult()
411 defer c.JSON(http.StatusOK, ret)
412
413 stat := model.Conf.Sync.Stat
414 if !model.Conf.Sync.Enabled {
415 stat = model.Conf.Language(53)
416 }
417
418 ret.Data = map[string]interface{}{
419 "synced": model.Conf.Sync.Synced,
420 "stat": stat,
421 "kernels": model.GetOnlineKernels(),
422 "kernel": model.KernelID,
423 }
424}
425
426func getBootSync(c *gin.Context) {
427 ret := gulu.Ret.NewResult()
428 defer c.JSON(http.StatusOK, ret)
429
430 if !model.IsAdminRoleContext(c) {
431 return
432 }
433
434 if model.Conf.Sync.Enabled && 1 == model.BootSyncSucc {
435 ret.Code = 1
436 ret.Msg = model.Conf.Language(17)
437 return
438 }
439}
440
441func performSync(c *gin.Context) {
442 ret := gulu.Ret.NewResult()
443 defer c.JSON(http.StatusOK, ret)
444
445 arg, ok := util.JsonArg(c, ret)
446 if !ok {
447 return
448 }
449
450 // Android 端前后台切换时自动触发同步 https://github.com/siyuan-note/siyuan/issues/7122
451 var mobileSwitch bool
452 if mobileSwitchArg := arg["mobileSwitch"]; nil != mobileSwitchArg {
453 mobileSwitch = mobileSwitchArg.(bool)
454 }
455 if mobileSwitch {
456 if nil == model.Conf.GetUser() || !model.Conf.Sync.Enabled {
457 return
458 }
459 }
460
461 if 3 != model.Conf.Sync.Mode {
462 model.SyncData(true)
463 return
464 }
465
466 // 云端同步模式支持 `完全手动同步` 模式 https://github.com/siyuan-note/siyuan/issues/7295
467 uploadArg := arg["upload"]
468 if nil == uploadArg {
469 // 必须传入同步方向,未传的话不执行同步
470 return
471 }
472
473 upload := uploadArg.(bool)
474 if upload {
475 model.SyncDataUpload()
476 } else {
477 model.SyncDataDownload()
478 }
479}
480
481func performBootSync(c *gin.Context) {
482 ret := gulu.Ret.NewResult()
483 defer c.JSON(http.StatusOK, ret)
484 model.BootSyncData()
485 ret.Code = model.BootSyncSucc
486}
487
488func listCloudSyncDir(c *gin.Context) {
489 ret := gulu.Ret.NewResult()
490 defer c.JSON(http.StatusOK, ret)
491
492 syncDirs, hSize, err := model.ListCloudSyncDir()
493 if err != nil {
494 ret.Code = 1
495 ret.Msg = err.Error()
496 ret.Data = map[string]interface{}{"closeTimeout": 5000}
497 return
498 }
499
500 ret.Data = map[string]interface{}{
501 "syncDirs": syncDirs,
502 "hSize": hSize,
503 "checkedSyncDir": model.Conf.Sync.CloudName,
504 }
505}
506
507func removeCloudSyncDir(c *gin.Context) {
508 ret := gulu.Ret.NewResult()
509 defer c.JSON(http.StatusOK, ret)
510
511 arg, ok := util.JsonArg(c, ret)
512 if !ok {
513 return
514 }
515
516 name := arg["name"].(string)
517 err := model.RemoveCloudSyncDir(name)
518 if err != nil {
519 ret.Code = -1
520 ret.Msg = err.Error()
521 ret.Data = map[string]interface{}{"closeTimeout": 5000}
522 return
523 }
524
525 ret.Data = model.Conf.Sync.CloudName
526}
527
528func createCloudSyncDir(c *gin.Context) {
529 ret := gulu.Ret.NewResult()
530 defer c.JSON(http.StatusOK, ret)
531
532 arg, ok := util.JsonArg(c, ret)
533 if !ok {
534 return
535 }
536
537 name := arg["name"].(string)
538 err := model.CreateCloudSyncDir(name)
539 if err != nil {
540 ret.Code = -1
541 ret.Msg = err.Error()
542 ret.Data = map[string]interface{}{"closeTimeout": 5000}
543 return
544 }
545}
546
547func setSyncGenerateConflictDoc(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 enabled := arg["enabled"].(bool)
557 model.SetSyncGenerateConflictDoc(enabled)
558}
559
560func setSyncEnable(c *gin.Context) {
561 ret := gulu.Ret.NewResult()
562 defer c.JSON(http.StatusOK, ret)
563
564 arg, ok := util.JsonArg(c, ret)
565 if !ok {
566 return
567 }
568
569 enabled := arg["enabled"].(bool)
570 model.SetSyncEnable(enabled)
571}
572
573func setSyncInterval(c *gin.Context) {
574 ret := gulu.Ret.NewResult()
575 defer c.JSON(http.StatusOK, ret)
576 arg, ok := util.JsonArg(c, ret)
577 if !ok {
578 return
579 }
580 interval := int(arg["interval"].(float64))
581 model.SetSyncInterval(interval)
582}
583
584func setSyncPerception(c *gin.Context) {
585 ret := gulu.Ret.NewResult()
586 defer c.JSON(http.StatusOK, ret)
587
588 arg, ok := util.JsonArg(c, ret)
589 if !ok {
590 return
591 }
592
593 enabled := arg["enabled"].(bool)
594 model.SetSyncPerception(enabled)
595}
596
597func setSyncMode(c *gin.Context) {
598 ret := gulu.Ret.NewResult()
599 defer c.JSON(http.StatusOK, ret)
600
601 arg, ok := util.JsonArg(c, ret)
602 if !ok {
603 return
604 }
605
606 mode := int(arg["mode"].(float64))
607 model.SetSyncMode(mode)
608}
609
610func setSyncProvider(c *gin.Context) {
611 ret := gulu.Ret.NewResult()
612 defer c.JSON(http.StatusOK, ret)
613
614 arg, ok := util.JsonArg(c, ret)
615 if !ok {
616 return
617 }
618
619 provider := int(arg["provider"].(float64))
620 err := model.SetSyncProvider(provider)
621 if err != nil {
622 ret.Code = -1
623 ret.Msg = err.Error()
624 ret.Data = map[string]interface{}{"closeTimeout": 5000}
625 return
626 }
627}
628
629func setSyncProviderS3(c *gin.Context) {
630 ret := gulu.Ret.NewResult()
631 defer c.JSON(http.StatusOK, ret)
632
633 arg, ok := util.JsonArg(c, ret)
634 if !ok {
635 return
636 }
637
638 s3Arg := arg["s3"].(interface{})
639 data, err := gulu.JSON.MarshalJSON(s3Arg)
640 if err != nil {
641 ret.Code = -1
642 ret.Msg = err.Error()
643 ret.Data = map[string]interface{}{"closeTimeout": 5000}
644 return
645 }
646
647 s3 := &conf.S3{}
648 if err = gulu.JSON.UnmarshalJSON(data, s3); err != nil {
649 ret.Code = -1
650 ret.Msg = err.Error()
651 ret.Data = map[string]interface{}{"closeTimeout": 5000}
652 return
653 }
654
655 err = model.SetSyncProviderS3(s3)
656 if err != nil {
657 ret.Code = -1
658 ret.Msg = err.Error()
659 ret.Data = map[string]interface{}{"closeTimeout": 5000}
660 return
661 }
662}
663
664func setSyncProviderWebDAV(c *gin.Context) {
665 ret := gulu.Ret.NewResult()
666 defer c.JSON(http.StatusOK, ret)
667
668 arg, ok := util.JsonArg(c, ret)
669 if !ok {
670 return
671 }
672
673 webdavArg := arg["webdav"].(interface{})
674 data, err := gulu.JSON.MarshalJSON(webdavArg)
675 if err != nil {
676 ret.Code = -1
677 ret.Msg = err.Error()
678 ret.Data = map[string]interface{}{"closeTimeout": 5000}
679 return
680 }
681
682 webdav := &conf.WebDAV{}
683 if err = gulu.JSON.UnmarshalJSON(data, webdav); err != nil {
684 ret.Code = -1
685 ret.Msg = err.Error()
686 ret.Data = map[string]interface{}{"closeTimeout": 5000}
687 return
688 }
689
690 err = model.SetSyncProviderWebDAV(webdav)
691 if err != nil {
692 ret.Code = -1
693 ret.Msg = err.Error()
694 ret.Data = map[string]interface{}{"closeTimeout": 5000}
695 return
696 }
697}
698
699func setSyncProviderLocal(c *gin.Context) {
700 ret := gulu.Ret.NewResult()
701 defer c.JSON(http.StatusOK, ret)
702
703 arg, ok := util.JsonArg(c, ret)
704 if !ok {
705 return
706 }
707
708 localArg := arg["local"].(interface{})
709 data, err := gulu.JSON.MarshalJSON(localArg)
710 if err != nil {
711 ret.Code = -1
712 ret.Msg = err.Error()
713 ret.Data = map[string]interface{}{"closeTimeout": 5000}
714 return
715 }
716
717 local := &conf.Local{}
718 if err = gulu.JSON.UnmarshalJSON(data, local); err != nil {
719 ret.Code = -1
720 ret.Msg = err.Error()
721 ret.Data = map[string]interface{}{"closeTimeout": 5000}
722 return
723 }
724
725 err = model.SetSyncProviderLocal(local)
726 if err != nil {
727 ret.Code = -1
728 ret.Msg = err.Error()
729 ret.Data = map[string]interface{}{"closeTimeout": 5000}
730 return
731 }
732
733 ret.Data = map[string]interface{}{
734 "local": local,
735 }
736}
737
738func setCloudSyncDir(c *gin.Context) {
739 ret := gulu.Ret.NewResult()
740 defer c.JSON(http.StatusOK, ret)
741
742 arg, ok := util.JsonArg(c, ret)
743 if !ok {
744 return
745 }
746
747 name := arg["name"].(string)
748 model.SetCloudSyncDir(name)
749}