Something went wrong. Try again.
A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552// SiYuan - Refactor your thinking// Copyright (c) 2020-present, b3log.org//// This program is free software: you can redistribute it and/or modify// it under the terms of the GNU Affero General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// This program is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU Affero General Public License for more details.//// You should have received a copy of the GNU Affero General Public License// along with this program. If not, see <https://www.gnu.org/licenses/>.
package api
import ( "fmt" "io" "mime" "mime/multipart" "net/http" "os" "path/filepath" "runtime" "strconv" "strings" "time"
"github.com/88250/gulu" "github.com/gabriel-vasile/mimetype" "github.com/gin-gonic/gin" "github.com/siyuan-note/filelock" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/model" "github.com/siyuan-note/siyuan/kernel/util")
func getUniqueFilename(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret) if !ok { return }
filePath := arg["path"].(string) ret.Data = map[string]interface{}{ "path": util.GetUniqueFilename(filePath), }}
func globalCopyFiles(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret) if !ok { return }
var srcs []string srcsArg := arg["srcs"].([]interface{}) for _, s := range srcsArg { srcs = append(srcs, s.(string)) }
for i, src := range srcs { absSrc, _ := filepath.Abs(src)
if !filelock.IsExist(absSrc) { msg := fmt.Sprintf("file [%s] does not exist", src) logging.LogErrorf(msg) ret.Code = -1 ret.Msg = msg return }
if util.IsSensitivePath(absSrc) { msg := fmt.Sprintf("refuse to copy sensitive file [%s]", src) logging.LogErrorf(msg) ret.Code = -2 ret.Msg = msg return }
srcs[i] = absSrc }
destDir := arg["destDir"].(string) // 相对于工作空间的路径 destDir = filepath.Join(util.WorkspaceDir, destDir) for _, src := range srcs { dest := filepath.Join(destDir, filepath.Base(src)) if err := filelock.Copy(src, dest); err != nil { logging.LogErrorf("copy file [%s] to [%s] failed: %s", src, dest, err) ret.Code = -1 ret.Msg = err.Error() return } }
model.IncSync()}
func copyFile(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret) if !ok { return }
src := arg["src"].(string) src, err := model.GetAssetAbsPath(src) if err != nil { logging.LogErrorf("get asset [%s] abs path failed: %s", src, err) ret.Code = -1 ret.Msg = err.Error() ret.Data = map[string]interface{}{"closeTimeout": 5000} return }
info, err := os.Stat(src) if err != nil { logging.LogErrorf("stat [%s] failed: %s", src, err) ret.Code = -1 ret.Msg = err.Error() ret.Data = map[string]interface{}{"closeTimeout": 5000} return }
if info.IsDir() { ret.Code = -1 ret.Msg = "file is a directory" ret.Data = map[string]interface{}{"closeTimeout": 5000} return }
dest := arg["dest"].(string) if util.IsSensitivePath(dest) { msg := fmt.Sprintf("refuse to copy sensitive file [%s]", dest) logging.LogErrorf(msg) ret.Code = -2 ret.Msg = msg return }
if err = filelock.Copy(src, dest); err != nil { logging.LogErrorf("copy file [%s] to [%s] failed: %s", src, dest, err) ret.Code = -1 ret.Msg = err.Error() ret.Data = map[string]interface{}{"closeTimeout": 5000} return }
model.IncSync()}
func getFile(c *gin.Context) { ret := gulu.Ret.NewResult() arg, ok := util.JsonArg(c, ret) if !ok { ret.Code = -1 c.JSON(http.StatusAccepted, ret) return }
filePath := arg["path"].(string) fileAbsPath, err := util.GetAbsPathInWorkspace(filePath) if err != nil { ret.Code = http.StatusForbidden ret.Msg = err.Error() c.JSON(http.StatusAccepted, ret) return } if !filelock.IsExist(fileAbsPath) { ret.Code = http.StatusNotFound ret.Msg = "file does not exist" c.JSON(http.StatusAccepted, ret) return }
info, err := os.Stat(fileAbsPath) if os.IsNotExist(err) { ret.Code = http.StatusNotFound ret.Msg = err.Error() c.JSON(http.StatusAccepted, ret) return } if err != nil { logging.LogErrorf("stat [%s] failed: %s", fileAbsPath, err) ret.Code = http.StatusInternalServerError ret.Msg = err.Error() c.JSON(http.StatusAccepted, ret) return } if info.IsDir() { logging.LogErrorf("path [%s] is a directory path", fileAbsPath) ret.Code = http.StatusMethodNotAllowed ret.Msg = "This is a directory path" c.JSON(http.StatusAccepted, ret) return }
// REF: https://github.com/siyuan-note/siyuan/issues/11364 if !model.IsAdminRoleContext(c) { if refuseToAccess(c, fileAbsPath, ret) { return } }
data, err := filelock.ReadFile(fileAbsPath) if err != nil { logging.LogErrorf("read file [%s] failed: %s", fileAbsPath, err) ret.Code = http.StatusInternalServerError ret.Msg = err.Error() c.JSON(http.StatusAccepted, ret) return }
contentType := mime.TypeByExtension(filepath.Ext(fileAbsPath)) if "" == contentType { if m := mimetype.Detect(data); nil != m { contentType = m.String() } } if "" == contentType { contentType = "application/octet-stream" } c.Data(http.StatusOK, contentType, data)}
func refuseToAccess(c *gin.Context, fileAbsPath string, ret *gulu.Result) bool { // 规范化并解析符号链接,防止通过大小写或符号链接绕过 fileNorm := normalizeAndResolve(fileAbsPath)
// 禁止访问配置文件 conf/conf.json confPath := normalizeAndResolve(filepath.Join(util.ConfDir, "conf.json")) if fileNorm == confPath { ret.Code = http.StatusForbidden ret.Msg = http.StatusText(http.StatusForbidden) c.JSON(http.StatusAccepted, ret) return true }
// 禁止访问 data/snippets/conf.json snippetPath := normalizeAndResolve(filepath.Join(util.DataDir, "snippets", "conf.json")) if fileNorm == snippetPath { ret.Code = http.StatusForbidden ret.Msg = http.StatusText(http.StatusForbidden) c.JSON(http.StatusAccepted, ret) return true }
// 禁止访问 data/templates 目录 templatesBase := normalizeAndResolve(filepath.Join(util.DataDir, "templates")) if util.IsSubPath(templatesBase, fileNorm) { ret.Code = http.StatusForbidden ret.Msg = http.StatusText(http.StatusForbidden) c.JSON(http.StatusAccepted, ret) return true } return false}
// normalizeAndResolve 将路径转为绝对、解析符号链接并清理;在需要时转为小写以实现不区分大小写比较func normalizeAndResolve(p string) string { if abs, err := filepath.Abs(p); err == nil { p = abs } if eval, err := filepath.EvalSymlinks(p); err == nil { p = eval } p = filepath.Clean(p) // 在 Windows 和 macOS 上文件系统通常为不区分大小写,使用小写统一比较 if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { p = strings.ToLower(p) } return p}
func readDir(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret) if !ok { c.JSON(http.StatusOK, ret) return }
dirPath := arg["path"].(string) dirAbsPath, err := util.GetAbsPathInWorkspace(dirPath) if err != nil { ret.Code = http.StatusForbidden ret.Msg = err.Error() return } info, err := os.Stat(dirAbsPath) if os.IsNotExist(err) { ret.Code = http.StatusNotFound ret.Msg = err.Error() return } if err != nil { logging.LogErrorf("stat [%s] failed: %s", dirAbsPath, err) ret.Code = http.StatusInternalServerError ret.Msg = err.Error() return } if !info.IsDir() { logging.LogErrorf("file [%s] is not a directory", dirAbsPath) ret.Code = http.StatusMethodNotAllowed ret.Msg = "file is not a directory" return }
entries, err := os.ReadDir(dirAbsPath) if err != nil { logging.LogErrorf("read dir [%s] failed: %s", dirAbsPath, err) ret.Code = http.StatusInternalServerError ret.Msg = err.Error() return }
files := []map[string]interface{}{} for _, entry := range entries { path := filepath.Join(dirAbsPath, entry.Name()) info, err = os.Stat(path) if err != nil { logging.LogErrorf("stat [%s] failed: %s", path, err) ret.Code = http.StatusInternalServerError ret.Msg = err.Error() return } files = append(files, map[string]interface{}{ "name": entry.Name(), "isDir": info.IsDir(), "isSymlink": util.IsSymlink(entry), "updated": info.ModTime().Unix(), }) }
ret.Data = files}
func renameFile(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret) if !ok { c.JSON(http.StatusOK, ret) return }
srcPath := arg["path"].(string) srcAbsPath, err := util.GetAbsPathInWorkspace(srcPath) if err != nil { ret.Code = http.StatusForbidden ret.Msg = err.Error() return } if !filelock.IsExist(srcAbsPath) { ret.Code = http.StatusNotFound ret.Msg = "the [path] file or directory does not exist" return }
destPath := arg["newPath"].(string) destPath = strings.TrimSpace(destPath) destAbsPath, err := util.GetAbsPathInWorkspace(destPath) if err != nil { ret.Code = http.StatusForbidden ret.Msg = err.Error() return } if filelock.IsExist(destAbsPath) { ret.Code = http.StatusConflict ret.Msg = "the [newPath] file or directory already exists" return }
if err := filelock.Rename(srcAbsPath, destAbsPath); err != nil { logging.LogErrorf("rename file [%s] to [%s] failed: %s", srcAbsPath, destAbsPath, err) ret.Code = http.StatusInternalServerError ret.Msg = err.Error() return }
model.IncSync()}
func removeFile(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret) if !ok { c.JSON(http.StatusOK, ret) return }
filePath := arg["path"].(string) fileAbsPath, err := util.GetAbsPathInWorkspace(filePath) if err != nil { ret.Code = http.StatusForbidden ret.Msg = err.Error() return } _, err = os.Stat(fileAbsPath) if os.IsNotExist(err) { ret.Code = http.StatusNotFound return } if err != nil { logging.LogErrorf("stat [%s] failed: %s", fileAbsPath, err) ret.Code = http.StatusInternalServerError ret.Msg = err.Error() return }
if err = filelock.Remove(fileAbsPath); err != nil { logging.LogErrorf("remove [%s] failed: %s", fileAbsPath, err) ret.Code = http.StatusInternalServerError ret.Msg = err.Error() return }
model.IncSync()}
func putFile(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret)
isDirStr := c.PostForm("isDir") isDir, _ := strconv.ParseBool(isDirStr)
var err error filePath := c.PostForm("path") filePath = strings.TrimSpace(filePath) fileAbsPath, err := util.GetAbsPathInWorkspace(filePath) if err != nil { ret.Code = http.StatusForbidden ret.Msg = err.Error() return }
fileExists := filelock.IsExist(fileAbsPath) if !fileExists { if !util.IsValidUploadFileName(filepath.Base(fileAbsPath)) { // Improve kernel API `/api/file/putFile` parameter validation https://github.com/siyuan-note/siyuan/issues/14658 ret.Code = http.StatusBadRequest ret.Msg = "invalid file path, please check https://github.com/siyuan-note/siyuan/issues/14658 for more details" return } } else { info, statErr := os.Stat(fileAbsPath) if statErr != nil { logging.LogErrorf("stat file [%s] failed: %s", fileAbsPath, statErr) ret.Code = http.StatusInternalServerError ret.Msg = statErr.Error() return } if info.IsDir() && !isDir { ret.Code = http.StatusBadRequest ret.Msg = "the path is a directory" return } }
if isDir { err = os.MkdirAll(fileAbsPath, 0755) if err != nil { logging.LogErrorf("make dir [%s] failed: %s", fileAbsPath, err) } } else { fileHeader, _ := c.FormFile("file") if nil == fileHeader { logging.LogErrorf("form file is nil [path=%s]", fileAbsPath) ret.Code = http.StatusBadRequest ret.Msg = "form file is nil" return }
for { dir := filepath.Dir(fileAbsPath) if err = os.MkdirAll(dir, 0755); err != nil { logging.LogErrorf("put file [%s] make dir [%s] failed: %s", fileAbsPath, dir, err) break }
var f multipart.File f, err = fileHeader.Open() if err != nil { logging.LogErrorf("open file failed: %s", err) break }
var data []byte data, err = io.ReadAll(f) if err != nil { logging.LogErrorf("read file failed: %s", err) break }
err = filelock.WriteFile(fileAbsPath, data) if err != nil { logging.LogErrorf("write file [%s] failed: %s", fileAbsPath, err) break } break } } if err != nil { ret.Code = -1 ret.Msg = err.Error() return }
modTimeStr := c.PostForm("modTime") modTime := time.Now() if "" != modTimeStr { modTimeInt, parseErr := strconv.ParseInt(modTimeStr, 10, 64) if nil != parseErr { logging.LogErrorf("parse mod time [%s] failed: %s", modTimeStr, parseErr) ret.Code = http.StatusInternalServerError ret.Msg = parseErr.Error() return } modTime = millisecond2Time(modTimeInt) } if err = os.Chtimes(fileAbsPath, modTime, modTime); err != nil { logging.LogErrorf("change time failed: %s", err) ret.Code = http.StatusInternalServerError ret.Msg = err.Error() return }
model.IncSync()}
func millisecond2Time(t int64) time.Time { sec := t / 1000 msec := t % 1000 return time.Unix(sec, msec*int64(time.Millisecond))}