A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 339 lines 10 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 model 18 19import ( 20 "errors" 21 "io" 22 "os" 23 "path" 24 "path/filepath" 25 "strings" 26 27 "github.com/88250/gulu" 28 "github.com/88250/lute/ast" 29 "github.com/gin-gonic/gin" 30 "github.com/siyuan-note/filelock" 31 "github.com/siyuan-note/logging" 32 "github.com/siyuan-note/siyuan/kernel/cache" 33 "github.com/siyuan-note/siyuan/kernel/treenode" 34 "github.com/siyuan-note/siyuan/kernel/util" 35) 36 37func InsertLocalAssets(id string, assetAbsPaths []string, isUpload bool) (succMap map[string]interface{}, err error) { 38 succMap = map[string]interface{}{} 39 40 bt := treenode.GetBlockTree(id) 41 if nil == bt { 42 err = errors.New(Conf.Language(71)) 43 return 44 } 45 46 docDirLocalPath := filepath.Join(util.DataDir, bt.BoxID, path.Dir(bt.Path)) 47 assetsDirPath := getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), docDirLocalPath) 48 if !gulu.File.IsExist(assetsDirPath) { 49 if err = os.MkdirAll(assetsDirPath, 0755); err != nil { 50 return 51 } 52 } 53 54 for _, assetAbsPath := range assetAbsPaths { 55 baseName := filepath.Base(assetAbsPath) 56 fName := baseName 57 fName = util.FilterUploadFileName(fName) 58 ext := filepath.Ext(fName) 59 fName = strings.TrimSuffix(fName, ext) 60 ext = strings.ToLower(ext) 61 fName += ext 62 if gulu.File.IsDir(assetAbsPath) || !isUpload { 63 if !strings.HasPrefix(assetAbsPath, "\\\\") { 64 assetAbsPath = "file://" + assetAbsPath 65 } 66 succMap[baseName] = assetAbsPath 67 continue 68 } 69 70 if util.IsSubPath(assetsDirPath, assetAbsPath) { 71 // 已经位于 assets 目录下的资源文件不处理 72 // Dragging a file from the assets folder into the editor causes the kernel to exit https://github.com/siyuan-note/siyuan/issues/15355 73 succMap[baseName] = "assets/" + fName 74 continue 75 } 76 77 fi, statErr := os.Stat(assetAbsPath) 78 if nil != statErr { 79 err = statErr 80 return 81 } 82 f, openErr := os.Open(assetAbsPath) 83 if nil != openErr { 84 err = openErr 85 return 86 } 87 hash, hashErr := util.GetEtagByHandle(f, fi.Size()) 88 if nil != hashErr { 89 f.Close() 90 return 91 } 92 93 if existAssetPath := GetAssetPathByHash(hash); "" != existAssetPath { 94 // 已经存在同样数据的资源文件的话不重复保存 95 succMap[baseName] = existAssetPath 96 } else { 97 fName = util.AssetName(fName, ast.NewNodeID()) 98 writePath := filepath.Join(assetsDirPath, fName) 99 if _, err = f.Seek(0, io.SeekStart); err != nil { 100 f.Close() 101 return 102 } 103 if err = filelock.WriteFileByReader(writePath, f); err != nil { 104 f.Close() 105 return 106 } 107 f.Close() 108 109 p := "assets/" + fName 110 succMap[baseName] = p 111 cache.SetAssetHash(hash, p) 112 } 113 } 114 IncSync() 115 return 116} 117 118func Upload(c *gin.Context) { 119 ret := gulu.Ret.NewResult() 120 defer c.JSON(200, ret) 121 122 form, err := c.MultipartForm() 123 if err != nil { 124 logging.LogErrorf("insert asset failed: %s", err) 125 ret.Code = -1 126 ret.Msg = err.Error() 127 return 128 } 129 assetsDirPath := filepath.Join(util.DataDir, "assets") 130 if nil != form.Value["id"] { 131 id := form.Value["id"][0] 132 bt := treenode.GetBlockTree(id) 133 if nil == bt { 134 ret.Code = -1 135 ret.Msg = Conf.Language(71) 136 return 137 } 138 docDirLocalPath := filepath.Join(util.DataDir, bt.BoxID, path.Dir(bt.Path)) 139 assetsDirPath = getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), docDirLocalPath) 140 } 141 142 relAssetsDirPath := "assets" 143 if nil != form.Value["assetsDirPath"] { 144 relAssetsDirPath = form.Value["assetsDirPath"][0] 145 assetsDirPath = filepath.Join(util.DataDir, relAssetsDirPath) 146 if !util.IsAbsPathInWorkspace(assetsDirPath) { 147 ret.Code = -1 148 ret.Msg = "Path [" + assetsDirPath + "] is not in workspace" 149 return 150 } 151 } 152 if !gulu.File.IsExist(assetsDirPath) { 153 if err = os.MkdirAll(assetsDirPath, 0755); err != nil { 154 ret.Code = -1 155 ret.Msg = err.Error() 156 return 157 } 158 } 159 160 var errFiles []string 161 succMap := map[string]interface{}{} 162 files := form.File["file[]"] 163 skipIfDuplicated := false // 默认不跳过重复文件,但是有的场景需要跳过,比如上传 PDF 标注图片 https://github.com/siyuan-note/siyuan/issues/10666 164 if nil != form.Value["skipIfDuplicated"] { 165 skipIfDuplicated = "true" == form.Value["skipIfDuplicated"][0] 166 } 167 168 for _, file := range files { 169 baseName := file.Filename 170 _, lastID := util.LastID(baseName) 171 if !ast.IsNodeIDPattern(lastID) { 172 lastID = "" 173 } 174 175 needUnzip2Dir := false 176 if gulu.OS.IsDarwin() { 177 if strings.HasSuffix(baseName, ".rtfd.zip") { 178 needUnzip2Dir = true 179 } 180 } 181 182 fName := baseName 183 fName = util.FilterUploadFileName(fName) 184 ext := filepath.Ext(fName) 185 fName = strings.TrimSuffix(fName, ext) 186 ext = strings.ToLower(ext) 187 fName += ext 188 f, openErr := file.Open() 189 if nil != openErr { 190 errFiles = append(errFiles, fName) 191 ret.Msg = openErr.Error() 192 break 193 } 194 195 hash, hashErr := util.GetEtagByHandle(f, file.Size) 196 if nil != hashErr { 197 errFiles = append(errFiles, fName) 198 ret.Msg = err.Error() 199 f.Close() 200 break 201 } 202 203 if existAssetPath := GetAssetPathByHash(hash); "" != existAssetPath { 204 // 已经存在同样数据的资源文件的话不重复保存 205 succMap[baseName] = existAssetPath 206 } else { 207 if skipIfDuplicated { 208 // 复制 PDF 矩形注解时不再重复插入图片 No longer upload image repeatedly when copying PDF rectangle annotation https://github.com/siyuan-note/siyuan/issues/10666 209 pattern := assetsDirPath + string(os.PathSeparator) + strings.TrimSuffix(fName, ext) 210 _, patternLastID := util.LastID(fName) 211 if lastID != "" && lastID != patternLastID { 212 // 文件名太长被截断了,通过之前的 lastID 来匹配 PDF files with too long file names cannot generate annotated images https://github.com/siyuan-note/siyuan/issues/15739 213 pattern = assetsDirPath + string(os.PathSeparator) + "*" + lastID + ext 214 } else { 215 pattern += "*" + ext 216 } 217 218 matches, globErr := filepath.Glob(pattern) 219 if nil != globErr { 220 logging.LogErrorf("glob failed: %s", globErr) 221 } else { 222 if 0 < len(matches) { 223 fName = filepath.Base(matches[0]) 224 succMap[baseName] = strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/") 225 f.Close() 226 break 227 } 228 } 229 } 230 231 if "" == lastID { 232 lastID = ast.NewNodeID() 233 } 234 fName = util.AssetName(fName, lastID) 235 writePath := filepath.Join(assetsDirPath, fName) 236 tmpDir := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7)) 237 if needUnzip2Dir { 238 if err = os.MkdirAll(tmpDir, 0755); err != nil { 239 errFiles = append(errFiles, fName) 240 ret.Msg = err.Error() 241 f.Close() 242 break 243 } 244 writePath = filepath.Join(tmpDir, fName) 245 } 246 247 if _, err = f.Seek(0, io.SeekStart); err != nil { 248 logging.LogErrorf("seek failed: %s", err) 249 errFiles = append(errFiles, fName) 250 ret.Msg = err.Error() 251 f.Close() 252 break 253 } 254 if err = filelock.WriteFileByReader(writePath, f); err != nil { 255 logging.LogErrorf("write file failed: %s", err) 256 errFiles = append(errFiles, fName) 257 ret.Msg = err.Error() 258 f.Close() 259 break 260 } 261 f.Close() 262 263 if needUnzip2Dir { 264 baseName = strings.TrimSuffix(file.Filename, ".rtfd.zip") + ".rtfd" 265 fName = baseName 266 fName = util.FilterUploadFileName(fName) 267 ext = filepath.Ext(fName) 268 fName = strings.TrimSuffix(fName, ext) 269 ext = strings.ToLower(ext) 270 fName += ext 271 fName = util.AssetName(fName, ast.NewNodeID()) 272 tmpDir2 := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7)) 273 if err = gulu.Zip.Unzip(writePath, tmpDir2); err != nil { 274 errFiles = append(errFiles, fName) 275 ret.Msg = err.Error() 276 break 277 } 278 279 entries, readErr := os.ReadDir(tmpDir2) 280 if nil != readErr { 281 logging.LogErrorf("read dir [%s] failed: %s", tmpDir2, readErr) 282 errFiles = append(errFiles, fName) 283 ret.Msg = readErr.Error() 284 break 285 } 286 if 1 > len(entries) { 287 logging.LogErrorf("read dir [%s] failed: no entry", tmpDir2) 288 errFiles = append(errFiles, fName) 289 ret.Msg = "no entry" 290 break 291 } 292 dirName := entries[0].Name() 293 srcDir := filepath.Join(tmpDir2, dirName) 294 entries, readErr = os.ReadDir(srcDir) 295 if nil != readErr { 296 logging.LogErrorf("read dir [%s] failed: %s", filepath.Join(tmpDir2, entries[0].Name()), readErr) 297 errFiles = append(errFiles, fName) 298 ret.Msg = readErr.Error() 299 break 300 } 301 destDir := filepath.Join(assetsDirPath, fName) 302 for _, entry := range entries { 303 from := filepath.Join(srcDir, entry.Name()) 304 to := filepath.Join(destDir, entry.Name()) 305 if copyErr := gulu.File.Copy(from, to); nil != copyErr { 306 logging.LogErrorf("copy [%s] to [%s] failed: %s", from, to, copyErr) 307 errFiles = append(errFiles, fName) 308 ret.Msg = copyErr.Error() 309 break 310 } 311 } 312 os.RemoveAll(tmpDir) 313 os.RemoveAll(tmpDir2) 314 } 315 316 p := strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/") 317 succMap[baseName] = p 318 cache.SetAssetHash(hash, p) 319 } 320 } 321 322 ret.Data = map[string]interface{}{ 323 "errFiles": errFiles, 324 "succMap": succMap, 325 } 326 327 IncSync() 328} 329 330func getAssetsDir(boxLocalPath, docDirLocalPath string) (assets string) { 331 assets = filepath.Join(docDirLocalPath, "assets") 332 if !filelock.IsExist(assets) { 333 assets = filepath.Join(boxLocalPath, "assets") 334 if !filelock.IsExist(assets) { 335 assets = filepath.Join(util.DataDir, "assets") 336 } 337 } 338 return 339}