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 "io"
22 "mime"
23 "mime/multipart"
24 "net/http"
25 "os"
26 "path/filepath"
27 "strconv"
28 "time"
29
30 "github.com/88250/gulu"
31 "github.com/gabriel-vasile/mimetype"
32 "github.com/gin-gonic/gin"
33 "github.com/siyuan-note/filelock"
34 "github.com/siyuan-note/logging"
35 "github.com/siyuan-note/siyuan/kernel/model"
36 "github.com/siyuan-note/siyuan/kernel/util"
37)
38
39func getUniqueFilename(c *gin.Context) {
40 ret := gulu.Ret.NewResult()
41 defer c.JSON(http.StatusOK, ret)
42
43 arg, ok := util.JsonArg(c, ret)
44 if !ok {
45 return
46 }
47
48 filePath := arg["path"].(string)
49 ret.Data = map[string]interface{}{
50 "path": util.GetUniqueFilename(filePath),
51 }
52}
53
54func globalCopyFiles(c *gin.Context) {
55 ret := gulu.Ret.NewResult()
56 defer c.JSON(http.StatusOK, ret)
57
58 arg, ok := util.JsonArg(c, ret)
59 if !ok {
60 return
61 }
62
63 var srcs []string
64 srcsArg := arg["srcs"].([]interface{})
65 for _, s := range srcsArg {
66 srcs = append(srcs, s.(string))
67 }
68
69 for _, src := range srcs {
70 if !filelock.IsExist(src) {
71 msg := fmt.Sprintf("file [%s] does not exist", src)
72 logging.LogErrorf(msg)
73 ret.Code = -1
74 ret.Msg = msg
75 return
76 }
77 }
78
79 destDir := arg["destDir"].(string) // 相对于工作空间的路径
80 destDir = filepath.Join(util.WorkspaceDir, destDir)
81 for _, src := range srcs {
82 dest := filepath.Join(destDir, filepath.Base(src))
83 if err := filelock.Copy(src, dest); err != nil {
84 logging.LogErrorf("copy file [%s] to [%s] failed: %s", src, dest, err)
85 ret.Code = -1
86 ret.Msg = err.Error()
87 return
88 }
89 }
90
91 model.IncSync()
92}
93
94func copyFile(c *gin.Context) {
95 ret := gulu.Ret.NewResult()
96 defer c.JSON(http.StatusOK, ret)
97
98 arg, ok := util.JsonArg(c, ret)
99 if !ok {
100 return
101 }
102
103 src := arg["src"].(string)
104 src, err := model.GetAssetAbsPath(src)
105 if err != nil {
106 logging.LogErrorf("get asset [%s] abs path failed: %s", src, err)
107 ret.Code = -1
108 ret.Msg = err.Error()
109 ret.Data = map[string]interface{}{"closeTimeout": 5000}
110 return
111 }
112
113 info, err := os.Stat(src)
114 if err != nil {
115 logging.LogErrorf("stat [%s] failed: %s", src, err)
116 ret.Code = -1
117 ret.Msg = err.Error()
118 ret.Data = map[string]interface{}{"closeTimeout": 5000}
119 return
120 }
121
122 if info.IsDir() {
123 ret.Code = -1
124 ret.Msg = "file is a directory"
125 ret.Data = map[string]interface{}{"closeTimeout": 5000}
126 return
127 }
128
129 dest := arg["dest"].(string)
130 if err = filelock.Copy(src, dest); err != nil {
131 logging.LogErrorf("copy file [%s] to [%s] failed: %s", src, dest, err)
132 ret.Code = -1
133 ret.Msg = err.Error()
134 ret.Data = map[string]interface{}{"closeTimeout": 5000}
135 return
136 }
137
138 model.IncSync()
139}
140
141func getFile(c *gin.Context) {
142 ret := gulu.Ret.NewResult()
143 arg, ok := util.JsonArg(c, ret)
144 if !ok {
145 ret.Code = -1
146 c.JSON(http.StatusAccepted, ret)
147 return
148 }
149
150 filePath := arg["path"].(string)
151 fileAbsPath, err := util.GetAbsPathInWorkspace(filePath)
152 if err != nil {
153 ret.Code = http.StatusForbidden
154 ret.Msg = err.Error()
155 c.JSON(http.StatusAccepted, ret)
156 return
157 }
158 info, err := os.Stat(fileAbsPath)
159 if os.IsNotExist(err) {
160 ret.Code = http.StatusNotFound
161 ret.Msg = err.Error()
162 c.JSON(http.StatusAccepted, ret)
163 return
164 }
165 if err != nil {
166 logging.LogErrorf("stat [%s] failed: %s", fileAbsPath, err)
167 ret.Code = http.StatusInternalServerError
168 ret.Msg = err.Error()
169 c.JSON(http.StatusAccepted, ret)
170 return
171 }
172 if info.IsDir() {
173 logging.LogErrorf("path [%s] is a directory path", fileAbsPath)
174 ret.Code = http.StatusMethodNotAllowed
175 ret.Msg = "This is a directory path"
176 c.JSON(http.StatusAccepted, ret)
177 return
178 }
179
180 // REF: https://github.com/siyuan-note/siyuan/issues/11364
181 if role := model.GetGinContextRole(c); !model.IsValidRole(role, []model.Role{
182 model.RoleAdministrator,
183 }) {
184 if relPath, err := filepath.Rel(util.ConfDir, fileAbsPath); err != nil {
185 logging.LogErrorf("Get a relative path from [%s] to [%s] failed: %s", util.ConfDir, fileAbsPath, err)
186 ret.Code = http.StatusInternalServerError
187 ret.Msg = err.Error()
188 c.JSON(http.StatusAccepted, ret)
189 return
190 } else if relPath == "conf.json" {
191 ret.Code = http.StatusForbidden
192 ret.Msg = http.StatusText(http.StatusForbidden)
193 c.JSON(http.StatusAccepted, ret)
194 return
195 }
196 }
197
198 data, err := filelock.ReadFile(fileAbsPath)
199 if err != nil {
200 logging.LogErrorf("read file [%s] failed: %s", fileAbsPath, err)
201 ret.Code = http.StatusInternalServerError
202 ret.Msg = err.Error()
203 c.JSON(http.StatusAccepted, ret)
204 return
205 }
206
207 contentType := mime.TypeByExtension(filepath.Ext(fileAbsPath))
208 if "" == contentType {
209 if m := mimetype.Detect(data); nil != m {
210 contentType = m.String()
211 }
212 }
213 if "" == contentType {
214 contentType = "application/octet-stream"
215 }
216 c.Data(http.StatusOK, contentType, data)
217}
218
219func readDir(c *gin.Context) {
220 ret := gulu.Ret.NewResult()
221 defer c.JSON(http.StatusOK, ret)
222
223 arg, ok := util.JsonArg(c, ret)
224 if !ok {
225 c.JSON(http.StatusOK, ret)
226 return
227 }
228
229 dirPath := arg["path"].(string)
230 dirAbsPath, err := util.GetAbsPathInWorkspace(dirPath)
231 if err != nil {
232 ret.Code = http.StatusForbidden
233 ret.Msg = err.Error()
234 return
235 }
236 info, err := os.Stat(dirAbsPath)
237 if os.IsNotExist(err) {
238 ret.Code = http.StatusNotFound
239 ret.Msg = err.Error()
240 return
241 }
242 if err != nil {
243 logging.LogErrorf("stat [%s] failed: %s", dirAbsPath, err)
244 ret.Code = http.StatusInternalServerError
245 ret.Msg = err.Error()
246 return
247 }
248 if !info.IsDir() {
249 logging.LogErrorf("file [%s] is not a directory", dirAbsPath)
250 ret.Code = http.StatusMethodNotAllowed
251 ret.Msg = "file is not a directory"
252 return
253 }
254
255 entries, err := os.ReadDir(dirAbsPath)
256 if err != nil {
257 logging.LogErrorf("read dir [%s] failed: %s", dirAbsPath, err)
258 ret.Code = http.StatusInternalServerError
259 ret.Msg = err.Error()
260 return
261 }
262
263 files := []map[string]interface{}{}
264 for _, entry := range entries {
265 path := filepath.Join(dirAbsPath, entry.Name())
266 info, err = os.Stat(path)
267 if err != nil {
268 logging.LogErrorf("stat [%s] failed: %s", path, err)
269 ret.Code = http.StatusInternalServerError
270 ret.Msg = err.Error()
271 return
272 }
273 files = append(files, map[string]interface{}{
274 "name": entry.Name(),
275 "isDir": info.IsDir(),
276 "isSymlink": util.IsSymlink(entry),
277 "updated": info.ModTime().Unix(),
278 })
279 }
280
281 ret.Data = files
282}
283
284func renameFile(c *gin.Context) {
285 ret := gulu.Ret.NewResult()
286 defer c.JSON(http.StatusOK, ret)
287
288 arg, ok := util.JsonArg(c, ret)
289 if !ok {
290 c.JSON(http.StatusOK, ret)
291 return
292 }
293
294 srcPath := arg["path"].(string)
295 srcAbsPath, err := util.GetAbsPathInWorkspace(srcPath)
296 if err != nil {
297 ret.Code = http.StatusForbidden
298 ret.Msg = err.Error()
299 return
300 }
301 if !filelock.IsExist(srcAbsPath) {
302 ret.Code = http.StatusNotFound
303 ret.Msg = "the [path] file or directory does not exist"
304 return
305 }
306
307 destPath := arg["newPath"].(string)
308 destAbsPath, err := util.GetAbsPathInWorkspace(destPath)
309 if err != nil {
310 ret.Code = http.StatusForbidden
311 ret.Msg = err.Error()
312 c.JSON(http.StatusAccepted, ret)
313 return
314 }
315 if filelock.IsExist(destAbsPath) {
316 ret.Code = http.StatusConflict
317 ret.Msg = "the [newPath] file or directory already exists"
318 return
319 }
320
321 if err := filelock.Rename(srcAbsPath, destAbsPath); err != nil {
322 logging.LogErrorf("rename file [%s] to [%s] failed: %s", srcAbsPath, destAbsPath, err)
323 ret.Code = http.StatusInternalServerError
324 ret.Msg = err.Error()
325 return
326 }
327
328 model.IncSync()
329}
330
331func removeFile(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 c.JSON(http.StatusOK, ret)
338 return
339 }
340
341 filePath := arg["path"].(string)
342 fileAbsPath, err := util.GetAbsPathInWorkspace(filePath)
343 if err != nil {
344 ret.Code = http.StatusForbidden
345 ret.Msg = err.Error()
346 return
347 }
348 _, err = os.Stat(fileAbsPath)
349 if os.IsNotExist(err) {
350 ret.Code = http.StatusNotFound
351 return
352 }
353 if err != nil {
354 logging.LogErrorf("stat [%s] failed: %s", fileAbsPath, err)
355 ret.Code = http.StatusInternalServerError
356 ret.Msg = err.Error()
357 return
358 }
359
360 if err = filelock.Remove(fileAbsPath); err != nil {
361 logging.LogErrorf("remove [%s] failed: %s", fileAbsPath, err)
362 ret.Code = http.StatusInternalServerError
363 ret.Msg = err.Error()
364 return
365 }
366
367 model.IncSync()
368}
369
370func putFile(c *gin.Context) {
371 ret := gulu.Ret.NewResult()
372 defer c.JSON(http.StatusOK, ret)
373
374 var err error
375 filePath := c.PostForm("path")
376 fileAbsPath, err := util.GetAbsPathInWorkspace(filePath)
377 if err != nil {
378 ret.Code = http.StatusForbidden
379 ret.Msg = err.Error()
380 return
381 }
382
383 if !util.IsValidUploadFileName(filepath.Base(fileAbsPath)) { // Improve kernel API `/api/file/putFile` parameter validation https://github.com/siyuan-note/siyuan/issues/14658
384 ret.Code = http.StatusBadRequest
385 ret.Msg = "invalid file path, please check https://github.com/siyuan-note/siyuan/issues/14658 for more details"
386 return
387 }
388
389 isDirStr := c.PostForm("isDir")
390 isDir, _ := strconv.ParseBool(isDirStr)
391
392 if isDir {
393 err = os.MkdirAll(fileAbsPath, 0755)
394 if err != nil {
395 logging.LogErrorf("make dir [%s] failed: %s", fileAbsPath, err)
396 }
397 } else {
398 fileHeader, _ := c.FormFile("file")
399 if nil == fileHeader {
400 logging.LogErrorf("form file is nil [path=%s]", fileAbsPath)
401 ret.Code = http.StatusBadRequest
402 ret.Msg = "form file is nil"
403 return
404 }
405
406 for {
407 dir := filepath.Dir(fileAbsPath)
408 if err = os.MkdirAll(dir, 0755); err != nil {
409 logging.LogErrorf("put file [%s] make dir [%s] failed: %s", fileAbsPath, dir, err)
410 break
411 }
412
413 var f multipart.File
414 f, err = fileHeader.Open()
415 if err != nil {
416 logging.LogErrorf("open file failed: %s", err)
417 break
418 }
419
420 var data []byte
421 data, err = io.ReadAll(f)
422 if err != nil {
423 logging.LogErrorf("read file failed: %s", err)
424 break
425 }
426
427 err = filelock.WriteFile(fileAbsPath, data)
428 if err != nil {
429 logging.LogErrorf("write file [%s] failed: %s", fileAbsPath, err)
430 break
431 }
432 break
433 }
434 }
435 if err != nil {
436 ret.Code = -1
437 ret.Msg = err.Error()
438 return
439 }
440
441 modTimeStr := c.PostForm("modTime")
442 modTime := time.Now()
443 if "" != modTimeStr {
444 modTimeInt, parseErr := strconv.ParseInt(modTimeStr, 10, 64)
445 if nil != parseErr {
446 logging.LogErrorf("parse mod time [%s] failed: %s", modTimeStr, parseErr)
447 ret.Code = http.StatusInternalServerError
448 ret.Msg = parseErr.Error()
449 return
450 }
451 modTime = millisecond2Time(modTimeInt)
452 }
453 if err = os.Chtimes(fileAbsPath, modTime, modTime); err != nil {
454 logging.LogErrorf("change time failed: %s", err)
455 ret.Code = http.StatusInternalServerError
456 ret.Msg = err.Error()
457 return
458 }
459
460 model.IncSync()
461}
462
463func millisecond2Time(t int64) time.Time {
464 sec := t / 1000
465 msec := t % 1000
466 return time.Unix(sec, msec*int64(time.Millisecond))
467}