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