upload.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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. package model
  17. import (
  18. "errors"
  19. "io"
  20. "os"
  21. "path"
  22. "path/filepath"
  23. "strings"
  24. "github.com/88250/gulu"
  25. "github.com/88250/lute/ast"
  26. "github.com/gin-gonic/gin"
  27. "github.com/siyuan-note/filelock"
  28. "github.com/siyuan-note/logging"
  29. "github.com/siyuan-note/siyuan/kernel/sql"
  30. "github.com/siyuan-note/siyuan/kernel/treenode"
  31. "github.com/siyuan-note/siyuan/kernel/util"
  32. )
  33. func InsertLocalAssets(id string, assetPaths []string, isUpload bool) (succMap map[string]interface{}, err error) {
  34. succMap = map[string]interface{}{}
  35. bt := treenode.GetBlockTree(id)
  36. if nil == bt {
  37. err = errors.New(Conf.Language(71))
  38. return
  39. }
  40. docDirLocalPath := filepath.Join(util.DataDir, bt.BoxID, path.Dir(bt.Path))
  41. assetsDirPath := getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), docDirLocalPath)
  42. if !gulu.File.IsExist(assetsDirPath) {
  43. if err = os.MkdirAll(assetsDirPath, 0755); nil != err {
  44. return
  45. }
  46. }
  47. for _, p := range assetPaths {
  48. baseName := filepath.Base(p)
  49. fName := baseName
  50. fName = util.FilterUploadFileName(fName)
  51. ext := filepath.Ext(fName)
  52. fName = strings.TrimSuffix(fName, ext)
  53. ext = strings.ToLower(ext)
  54. fName += ext
  55. if gulu.File.IsDir(p) || !isUpload {
  56. if !strings.HasPrefix(p, "\\\\") {
  57. p = "file://" + p
  58. }
  59. succMap[baseName] = p
  60. continue
  61. }
  62. fi, statErr := os.Stat(p)
  63. if nil != statErr {
  64. err = statErr
  65. return
  66. }
  67. f, openErr := os.Open(p)
  68. if nil != openErr {
  69. err = openErr
  70. return
  71. }
  72. hash, hashErr := util.GetEtagByHandle(f, fi.Size())
  73. if nil != hashErr {
  74. f.Close()
  75. return
  76. }
  77. if existAsset := sql.QueryAssetByHash(hash); nil != existAsset {
  78. // 已经存在同样数据的资源文件的话不重复保存
  79. succMap[baseName] = existAsset.Path
  80. } else {
  81. ext := path.Ext(fName)
  82. fName = fName[0 : len(fName)-len(ext)]
  83. fName = fName + "-" + ast.NewNodeID() + ext
  84. writePath := filepath.Join(assetsDirPath, fName)
  85. if _, err = f.Seek(0, io.SeekStart); nil != err {
  86. f.Close()
  87. return
  88. }
  89. if err = filelock.WriteFileByReader(writePath, f); nil != err {
  90. f.Close()
  91. return
  92. }
  93. f.Close()
  94. succMap[baseName] = "assets/" + fName
  95. }
  96. }
  97. IncSync()
  98. return
  99. }
  100. func Upload(c *gin.Context) {
  101. ret := gulu.Ret.NewResult()
  102. defer c.JSON(200, ret)
  103. form, err := c.MultipartForm()
  104. if nil != err {
  105. logging.LogErrorf("insert asset failed: %s", err)
  106. ret.Code = -1
  107. ret.Msg = err.Error()
  108. return
  109. }
  110. assetsDirPath := filepath.Join(util.DataDir, "assets")
  111. if nil != form.Value["id"] {
  112. id := form.Value["id"][0]
  113. bt := treenode.GetBlockTree(id)
  114. if nil == bt {
  115. ret.Code = -1
  116. ret.Msg = Conf.Language(71)
  117. return
  118. }
  119. docDirLocalPath := filepath.Join(util.DataDir, bt.BoxID, path.Dir(bt.Path))
  120. assetsDirPath = getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), docDirLocalPath)
  121. }
  122. relAssetsDirPath := "assets"
  123. if nil != form.Value["assetsDirPath"] {
  124. relAssetsDirPath = form.Value["assetsDirPath"][0]
  125. assetsDirPath = filepath.Join(util.DataDir, relAssetsDirPath)
  126. }
  127. if !gulu.File.IsExist(assetsDirPath) {
  128. if err = os.MkdirAll(assetsDirPath, 0755); nil != err {
  129. ret.Code = -1
  130. ret.Msg = err.Error()
  131. return
  132. }
  133. }
  134. var errFiles []string
  135. succMap := map[string]interface{}{}
  136. files := form.File["file[]"]
  137. skipIfDuplicated := false // 默认不跳过重复文件,但是有的场景需要跳过,比如上传 PDF 标注图片 https://github.com/siyuan-note/siyuan/issues/10666
  138. if nil != form.Value["skipIfDuplicated"] {
  139. skipIfDuplicated = "true" == form.Value["skipIfDuplicated"][0]
  140. }
  141. for _, file := range files {
  142. baseName := file.Filename
  143. needUnzip2Dir := false
  144. if gulu.OS.IsDarwin() {
  145. if strings.HasSuffix(baseName, ".rtfd.zip") {
  146. needUnzip2Dir = true
  147. }
  148. }
  149. fName := baseName
  150. fName = util.FilterUploadFileName(fName)
  151. ext := filepath.Ext(fName)
  152. fName = strings.TrimSuffix(fName, ext)
  153. ext = strings.ToLower(ext)
  154. fName += ext
  155. f, openErr := file.Open()
  156. if nil != openErr {
  157. errFiles = append(errFiles, fName)
  158. ret.Msg = openErr.Error()
  159. break
  160. }
  161. hash, hashErr := util.GetEtagByHandle(f, file.Size)
  162. if nil != hashErr {
  163. errFiles = append(errFiles, fName)
  164. ret.Msg = err.Error()
  165. f.Close()
  166. break
  167. }
  168. if existAsset := sql.QueryAssetByHash(hash); nil != existAsset {
  169. // 已经存在同样数据的资源文件的话不重复保存
  170. succMap[baseName] = existAsset.Path
  171. } else {
  172. if skipIfDuplicated {
  173. // https://github.com/siyuan-note/siyuan/issues/10666
  174. matches, globErr := filepath.Glob(assetsDirPath + string(os.PathSeparator) + strings.TrimSuffix(fName, ext) + "*")
  175. if nil != globErr {
  176. logging.LogErrorf("glob failed: %s", globErr)
  177. } else {
  178. if 0 < len(matches) {
  179. fName = filepath.Base(matches[0])
  180. succMap[baseName] = strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/")
  181. f.Close()
  182. break
  183. }
  184. }
  185. }
  186. fName = util.AssetName(fName)
  187. writePath := filepath.Join(assetsDirPath, fName)
  188. tmpDir := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7))
  189. if needUnzip2Dir {
  190. if err = os.MkdirAll(tmpDir, 0755); nil != err {
  191. errFiles = append(errFiles, fName)
  192. ret.Msg = err.Error()
  193. f.Close()
  194. break
  195. }
  196. writePath = filepath.Join(tmpDir, fName)
  197. }
  198. if _, err = f.Seek(0, io.SeekStart); nil != err {
  199. logging.LogErrorf("seek failed: %s", err)
  200. errFiles = append(errFiles, fName)
  201. ret.Msg = err.Error()
  202. f.Close()
  203. break
  204. }
  205. if err = filelock.WriteFileByReader(writePath, f); nil != err {
  206. logging.LogErrorf("write file failed: %s", err)
  207. errFiles = append(errFiles, fName)
  208. ret.Msg = err.Error()
  209. f.Close()
  210. break
  211. }
  212. f.Close()
  213. if needUnzip2Dir {
  214. baseName = strings.TrimSuffix(file.Filename, ".rtfd.zip") + ".rtfd"
  215. fName = baseName
  216. fName = util.FilterUploadFileName(fName)
  217. ext = filepath.Ext(fName)
  218. fName = strings.TrimSuffix(fName, ext)
  219. ext = strings.ToLower(ext)
  220. fName += ext
  221. fName = util.AssetName(fName)
  222. tmpDir2 := filepath.Join(util.TempDir, "convert", "zip", gulu.Rand.String(7))
  223. if err = gulu.Zip.Unzip(writePath, tmpDir2); nil != err {
  224. errFiles = append(errFiles, fName)
  225. ret.Msg = err.Error()
  226. break
  227. }
  228. entries, readErr := os.ReadDir(tmpDir2)
  229. if nil != readErr {
  230. logging.LogErrorf("read dir [%s] failed: %s", tmpDir2, readErr)
  231. errFiles = append(errFiles, fName)
  232. ret.Msg = readErr.Error()
  233. break
  234. }
  235. if 1 > len(entries) {
  236. logging.LogErrorf("read dir [%s] failed: no entry", tmpDir2)
  237. errFiles = append(errFiles, fName)
  238. ret.Msg = "no entry"
  239. break
  240. }
  241. dirName := entries[0].Name()
  242. srcDir := filepath.Join(tmpDir2, dirName)
  243. entries, readErr = os.ReadDir(srcDir)
  244. if nil != readErr {
  245. logging.LogErrorf("read dir [%s] failed: %s", filepath.Join(tmpDir2, entries[0].Name()), readErr)
  246. errFiles = append(errFiles, fName)
  247. ret.Msg = readErr.Error()
  248. break
  249. }
  250. destDir := filepath.Join(assetsDirPath, fName)
  251. for _, entry := range entries {
  252. from := filepath.Join(srcDir, entry.Name())
  253. to := filepath.Join(destDir, entry.Name())
  254. if copyErr := gulu.File.Copy(from, to); nil != copyErr {
  255. logging.LogErrorf("copy [%s] to [%s] failed: %s", from, to, copyErr)
  256. errFiles = append(errFiles, fName)
  257. ret.Msg = copyErr.Error()
  258. break
  259. }
  260. }
  261. os.RemoveAll(tmpDir)
  262. os.RemoveAll(tmpDir2)
  263. }
  264. succMap[baseName] = strings.TrimPrefix(path.Join(relAssetsDirPath, fName), "/")
  265. }
  266. }
  267. ret.Data = map[string]interface{}{
  268. "errFiles": errFiles,
  269. "succMap": succMap,
  270. }
  271. IncSync()
  272. }
  273. func getAssetsDir(boxLocalPath, docDirLocalPath string) (assets string) {
  274. assets = filepath.Join(docDirLocalPath, "assets")
  275. if !filelock.IsExist(assets) {
  276. assets = filepath.Join(boxLocalPath, "assets")
  277. if !filelock.IsExist(assets) {
  278. assets = filepath.Join(util.DataDir, "assets")
  279. }
  280. }
  281. return
  282. }