// 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 . 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/sql" "github.com/siyuan-note/siyuan/kernel/treenode" "github.com/siyuan-note/siyuan/kernel/util" ) func InsertLocalAssets(id string, assetPaths []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); nil != err { return } } for _, p := range assetPaths { baseName := filepath.Base(p) 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(p) || !isUpload { if !strings.HasPrefix(p, "\\\\") { p = "file://" + p } succMap[baseName] = p continue } fi, statErr := os.Stat(p) if nil != statErr { err = statErr return } f, openErr := os.Open(p) if nil != openErr { err = openErr return } hash, hashErr := util.GetEtagByHandle(f, fi.Size()) if nil != hashErr { f.Close() return } if existAsset := sql.QueryAssetByHash(hash); nil != existAsset { // 已经存在同样数据的资源文件的话不重复保存 succMap[baseName] = existAsset.Path } else { ext := path.Ext(fName) fName = fName[0 : len(fName)-len(ext)] fName = fName + "-" + ast.NewNodeID() + ext writePath := filepath.Join(assetsDirPath, fName) if _, err = f.Seek(0, io.SeekStart); nil != err { f.Close() return } if err = filelock.WriteFileByReader(writePath, f); nil != err { f.Close() return } f.Close() succMap[baseName] = "assets/" + fName } } IncSync() return } func Upload(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(200, ret) form, err := c.MultipartForm() if nil != err { 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 !gulu.File.IsExist(assetsDirPath) { if err = os.MkdirAll(assetsDirPath, 0755); nil != err { 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 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 existAsset := sql.QueryAssetByHash(hash); nil != existAsset { // 已经存在同样数据的资源文件的话不重复保存 succMap[baseName] = existAsset.Path } else { if skipIfDuplicated { // https://github.com/siyuan-note/siyuan/issues/10666 matches, globErr := filepath.Glob(assetsDirPath + string(os.PathSeparator) + strings.TrimSuffix(fName, ext) + "*") 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 } } } fName = util.AssetName(fName) writePath := filepath.Join(assetsDirPath, fName) tmpDir := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7)) if needUnzip2Dir { if err = os.MkdirAll(tmpDir, 0755); nil != err { errFiles = append(errFiles, fName) ret.Msg = err.Error() f.Close() break } writePath = filepath.Join(tmpDir, fName) } if _, err = f.Seek(0, io.SeekStart); nil != err { logging.LogErrorf("seek failed: %s", err) errFiles = append(errFiles, fName) ret.Msg = err.Error() f.Close() break } if err = filelock.WriteFileByReader(writePath, f); nil != err { 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) tmpDir2 := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7)) if err = gulu.Zip.Unzip(writePath, tmpDir2); nil != err { 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) } succMap[baseName] = strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/") } } 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 }