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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363// 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 model
import ( "errors" "io" "os" "path" "path/filepath" "strings"
"github.com/88250/gulu" "github.com/88250/lute/ast" "github.com/gin-gonic/gin" "github.com/siyuan-note/filelock" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/cache" "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util")
func InsertLocalAssets(id string, assetAbsPaths []string, isUpload bool) (succMap map[string]interface{}, err error) { succMap = map[string]interface{}{}
bt := treenode.GetBlockTree(id) if nil == bt { err = errors.New(Conf.Language(71)) return }
docDirLocalPath := filepath.Join(util.DataDir, bt.BoxID, path.Dir(bt.Path)) assetsDirPath := getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), docDirLocalPath) if !gulu.File.IsExist(assetsDirPath) { if err = os.MkdirAll(assetsDirPath, 0755); err != nil { return } }
for _, assetAbsPath := range assetAbsPaths { baseName := filepath.Base(assetAbsPath) fName := baseName fName = util.FilterUploadFileName(fName) ext := filepath.Ext(fName) fName = strings.TrimSuffix(fName, ext) ext = strings.ToLower(ext) fName += ext if gulu.File.IsDir(assetAbsPath) || !isUpload { if !strings.HasPrefix(assetAbsPath, "\\\\") { assetAbsPath = "file://" + assetAbsPath } succMap[baseName] = assetAbsPath continue }
if util.IsSubPath(assetsDirPath, assetAbsPath) { // 已经位于 assets 目录下的资源文件不处理 // Dragging a file from the assets folder into the editor causes the kernel to exit https://github.com/siyuan-note/siyuan/issues/15355 succMap[baseName] = "assets/" + fName continue }
fi, statErr := os.Stat(assetAbsPath) if nil != statErr { err = statErr return } f, openErr := os.Open(assetAbsPath) if nil != openErr { err = openErr return }
hash, hashErr := util.GetEtagByHandle(f, fi.Size()) if nil != hashErr { f.Close() return }
if 1 > fi.Size() { hash = "random_1_" + gulu.Rand.String(12) }
existAssetPath := GetAssetPathByHash(hash) if "" != existAssetPath { originalName := util.RemoveID(filepath.Base(existAssetPath)) if strings.ToLower(fName) != strings.ToLower(originalName) { hash = "random_2_" + gulu.Rand.String(12) } }
if "" != existAssetPath && !strings.HasPrefix(hash, "random_") { succMap[baseName] = strings.TrimPrefix(existAssetPath, "/") } else { fName = util.AssetName(fName, ast.NewNodeID()) writePath := filepath.Join(assetsDirPath, fName) if _, err = f.Seek(0, io.SeekStart); err != nil { f.Close() return } if err = filelock.WriteFileByReader(writePath, f); err != nil { f.Close() return } f.Close()
p := "assets/" + fName succMap[baseName] = p cache.SetAssetHash(hash, p) } } IncSync() return}
func Upload(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(200, ret)
form, err := c.MultipartForm() if err != nil { logging.LogErrorf("insert asset failed: %s", err) ret.Code = -1 ret.Msg = err.Error() return } assetsDirPath := filepath.Join(util.DataDir, "assets") if nil != form.Value["id"] { id := form.Value["id"][0] bt := treenode.GetBlockTree(id) if nil == bt { ret.Code = -1 ret.Msg = Conf.Language(71) return } docDirLocalPath := filepath.Join(util.DataDir, bt.BoxID, path.Dir(bt.Path)) assetsDirPath = getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), docDirLocalPath) }
relAssetsDirPath := "assets" if nil != form.Value["assetsDirPath"] { relAssetsDirPath = form.Value["assetsDirPath"][0] assetsDirPath = filepath.Join(util.DataDir, relAssetsDirPath) if !util.IsAbsPathInWorkspace(assetsDirPath) { ret.Code = -1 ret.Msg = "Path [" + assetsDirPath + "] is not in workspace" return } } if !gulu.File.IsExist(assetsDirPath) { if err = os.MkdirAll(assetsDirPath, 0755); err != nil { ret.Code = -1 ret.Msg = err.Error() return } }
var errFiles []string succMap := map[string]interface{}{} files := form.File["file[]"] skipIfDuplicated := false // 默认不跳过重复文件,但是有的场景需要跳过,比如上传 PDF 标注图片 https://github.com/siyuan-note/siyuan/issues/10666 if nil != form.Value["skipIfDuplicated"] { skipIfDuplicated = "true" == form.Value["skipIfDuplicated"][0] }
for _, file := range files { baseName := file.Filename _, lastID := util.LastID(baseName) if !ast.IsNodeIDPattern(lastID) { lastID = "" }
needUnzip2Dir := false if gulu.OS.IsDarwin() { if strings.HasSuffix(baseName, ".rtfd.zip") { needUnzip2Dir = true } }
fName := baseName fName = util.FilterUploadFileName(fName) ext := filepath.Ext(fName) fName = strings.TrimSuffix(fName, ext) ext = strings.ToLower(ext) fName += ext f, openErr := file.Open() if nil != openErr { errFiles = append(errFiles, fName) ret.Msg = openErr.Error() break }
hash, hashErr := util.GetEtagByHandle(f, file.Size) if nil != hashErr { errFiles = append(errFiles, fName) ret.Msg = err.Error() f.Close() break }
if 1 > file.Size { hash = "random_1_" + gulu.Rand.String(12) }
existAssetPath := GetAssetPathByHash(hash) if "" != existAssetPath { originalName := util.RemoveID(filepath.Base(existAssetPath)) if strings.ToLower(fName) != strings.ToLower(originalName) { hash = "random_2_" + gulu.Rand.String(12) } }
if "" != existAssetPath && !strings.HasPrefix(hash, "random_") { succMap[baseName] = strings.TrimPrefix(existAssetPath, "/") } else { if skipIfDuplicated { // 复制 PDF 矩形注解时不再重复插入图片 No longer upload image repeatedly when copying PDF rectangle annotation https://github.com/siyuan-note/siyuan/issues/10666 pattern := assetsDirPath + string(os.PathSeparator) + strings.TrimSuffix(fName, ext) _, patternLastID := util.LastID(fName) if lastID != "" && lastID != patternLastID { // 文件名太长被截断了,通过之前的 lastID 来匹配 PDF files with too long file names cannot generate annotated images https://github.com/siyuan-note/siyuan/issues/15739 pattern = assetsDirPath + string(os.PathSeparator) + "*" + lastID + ext } else { pattern += "*" + ext }
matches, globErr := filepath.Glob(pattern) if nil != globErr { logging.LogErrorf("glob failed: %s", globErr) } else { if 0 < len(matches) { fName = filepath.Base(matches[0]) succMap[baseName] = strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/") f.Close() break } } }
if "" == lastID { lastID = ast.NewNodeID() } fName = util.AssetName(fName, lastID) writePath := filepath.Join(assetsDirPath, fName) tmpDir := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7)) if needUnzip2Dir { if err = os.MkdirAll(tmpDir, 0755); err != nil { errFiles = append(errFiles, fName) ret.Msg = err.Error() f.Close() break } writePath = filepath.Join(tmpDir, fName) }
if _, err = f.Seek(0, io.SeekStart); err != nil { logging.LogErrorf("seek failed: %s", err) errFiles = append(errFiles, fName) ret.Msg = err.Error() f.Close() break } if err = filelock.WriteFileByReader(writePath, f); err != nil { logging.LogErrorf("write file failed: %s", err) errFiles = append(errFiles, fName) ret.Msg = err.Error() f.Close() break } f.Close()
if needUnzip2Dir { baseName = strings.TrimSuffix(file.Filename, ".rtfd.zip") + ".rtfd" fName = baseName fName = util.FilterUploadFileName(fName) ext = filepath.Ext(fName) fName = strings.TrimSuffix(fName, ext) ext = strings.ToLower(ext) fName += ext fName = util.AssetName(fName, ast.NewNodeID()) tmpDir2 := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7)) if err = gulu.Zip.Unzip(writePath, tmpDir2); err != nil { errFiles = append(errFiles, fName) ret.Msg = err.Error() break }
entries, readErr := os.ReadDir(tmpDir2) if nil != readErr { logging.LogErrorf("read dir [%s] failed: %s", tmpDir2, readErr) errFiles = append(errFiles, fName) ret.Msg = readErr.Error() break } if 1 > len(entries) { logging.LogErrorf("read dir [%s] failed: no entry", tmpDir2) errFiles = append(errFiles, fName) ret.Msg = "no entry" break } dirName := entries[0].Name() srcDir := filepath.Join(tmpDir2, dirName) entries, readErr = os.ReadDir(srcDir) if nil != readErr { logging.LogErrorf("read dir [%s] failed: %s", filepath.Join(tmpDir2, entries[0].Name()), readErr) errFiles = append(errFiles, fName) ret.Msg = readErr.Error() break } destDir := filepath.Join(assetsDirPath, fName) for _, entry := range entries { from := filepath.Join(srcDir, entry.Name()) to := filepath.Join(destDir, entry.Name()) if copyErr := gulu.File.Copy(from, to); nil != copyErr { logging.LogErrorf("copy [%s] to [%s] failed: %s", from, to, copyErr) errFiles = append(errFiles, fName) ret.Msg = copyErr.Error() break } } os.RemoveAll(tmpDir) os.RemoveAll(tmpDir2) }
p := strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/") succMap[baseName] = p cache.SetAssetHash(hash, p) } }
ret.Data = map[string]interface{}{ "errFiles": errFiles, "succMap": succMap, }
IncSync()}
func getAssetsDir(boxLocalPath, docDirLocalPath string) (assets string) { assets = filepath.Join(docDirLocalPath, "assets") if !filelock.IsExist(assets) { assets = filepath.Join(boxLocalPath, "assets") if !filelock.IsExist(assets) { assets = filepath.Join(util.DataDir, "assets") } } return}