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