assets.go 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304
  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. "bytes"
  19. "errors"
  20. "fmt"
  21. "io"
  22. "io/fs"
  23. "mime"
  24. "net/http"
  25. "net/url"
  26. "os"
  27. "path"
  28. "path/filepath"
  29. "sort"
  30. "strings"
  31. "time"
  32. "github.com/88250/go-humanize"
  33. "github.com/88250/gulu"
  34. "github.com/88250/lute/ast"
  35. "github.com/88250/lute/editor"
  36. "github.com/88250/lute/html"
  37. "github.com/88250/lute/parse"
  38. "github.com/gabriel-vasile/mimetype"
  39. "github.com/imroc/req/v3"
  40. "github.com/siyuan-note/filelock"
  41. "github.com/siyuan-note/httpclient"
  42. "github.com/siyuan-note/logging"
  43. "github.com/siyuan-note/siyuan/kernel/cache"
  44. "github.com/siyuan-note/siyuan/kernel/filesys"
  45. "github.com/siyuan-note/siyuan/kernel/search"
  46. "github.com/siyuan-note/siyuan/kernel/sql"
  47. "github.com/siyuan-note/siyuan/kernel/treenode"
  48. "github.com/siyuan-note/siyuan/kernel/util"
  49. )
  50. func DocImageAssets(rootID string) (ret []string, err error) {
  51. tree, err := LoadTreeByBlockID(rootID)
  52. if nil != err {
  53. return
  54. }
  55. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  56. if !entering {
  57. return ast.WalkContinue
  58. }
  59. if ast.NodeImage == n.Type {
  60. linkDest := n.ChildByType(ast.NodeLinkDest)
  61. dest := linkDest.Tokens
  62. if 1 > len(dest) { // 双击打开图片不对 https://github.com/siyuan-note/siyuan/issues/5876
  63. return ast.WalkContinue
  64. }
  65. ret = append(ret, gulu.Str.FromBytes(dest))
  66. }
  67. return ast.WalkContinue
  68. })
  69. return
  70. }
  71. func NetImg2LocalAssets(rootID, originalURL string) (err error) {
  72. tree, err := LoadTreeByBlockID(rootID)
  73. if nil != err {
  74. return
  75. }
  76. var files int
  77. msgId := gulu.Rand.String(7)
  78. docDirLocalPath := filepath.Join(util.DataDir, tree.Box, path.Dir(tree.Path))
  79. assetsDirPath := getAssetsDir(filepath.Join(util.DataDir, tree.Box), docDirLocalPath)
  80. if !gulu.File.IsExist(assetsDirPath) {
  81. if err = os.MkdirAll(assetsDirPath, 0755); nil != err {
  82. return
  83. }
  84. }
  85. browserClient := req.C().
  86. SetUserAgent(util.UserAgent).
  87. SetTimeout(30 * time.Second).
  88. EnableInsecureSkipVerify(). // HTTPS certificate is no longer verified when `Convert network images to local images` https://github.com/siyuan-note/siyuan/issues/9080
  89. SetProxy(httpclient.ProxyFromEnvironment)
  90. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  91. if !entering {
  92. return ast.WalkContinue
  93. }
  94. if ast.NodeImage == n.Type {
  95. linkDest := n.ChildByType(ast.NodeLinkDest)
  96. linkText := n.ChildByType(ast.NodeLinkText)
  97. if nil == linkText {
  98. linkText = &ast.Node{Type: ast.NodeLinkText, Tokens: []byte("image")}
  99. if openBracket := n.ChildByType(ast.NodeOpenBracket); nil != openBracket {
  100. openBracket.InsertAfter(linkText)
  101. }
  102. }
  103. dest := linkDest.Tokens
  104. if util.IsAssetLinkDest(dest) {
  105. return ast.WalkSkipChildren
  106. }
  107. if bytes.HasPrefix(bytes.ToLower(dest), []byte("file://")) {
  108. // `网络图片转换为本地图片` 支持处理 `file://` 本地路径图片 https://github.com/siyuan-note/siyuan/issues/6546
  109. u := string(dest)[7:]
  110. unescaped, _ := url.PathUnescape(u)
  111. if unescaped != u {
  112. // `Convert network images/assets to local` supports URL-encoded local file names https://github.com/siyuan-note/siyuan/issues/9929
  113. u = unescaped
  114. }
  115. if !gulu.File.IsExist(u) || gulu.File.IsDir(u) {
  116. return ast.WalkSkipChildren
  117. }
  118. name := filepath.Base(u)
  119. name = util.FilterUploadFileName(name)
  120. name = util.TruncateLenFileName(name)
  121. if 1 > len(bytes.TrimSpace(linkText.Tokens)) {
  122. linkText.Tokens = []byte(name)
  123. }
  124. name = "net-img-" + name
  125. name = util.AssetName(name)
  126. writePath := filepath.Join(assetsDirPath, name)
  127. if err = filelock.Copy(u, writePath); nil != err {
  128. logging.LogErrorf("copy [%s] to [%s] failed: %s", u, writePath, err)
  129. return ast.WalkSkipChildren
  130. }
  131. linkDest.Tokens = []byte("assets/" + name)
  132. files++
  133. return ast.WalkSkipChildren
  134. }
  135. if bytes.HasPrefix(bytes.ToLower(dest), []byte("https://")) || bytes.HasPrefix(bytes.ToLower(dest), []byte("http://")) || bytes.HasPrefix(dest, []byte("//")) {
  136. if bytes.HasPrefix(dest, []byte("//")) {
  137. // `Convert network images to local` supports `//` https://github.com/siyuan-note/siyuan/issues/10598
  138. dest = append([]byte("https:"), dest...)
  139. }
  140. u := string(dest)
  141. if strings.Contains(u, "qpic.cn") {
  142. // 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/5052
  143. if strings.Contains(u, "http://") {
  144. u = strings.Replace(u, "http://", "https://", 1)
  145. }
  146. // 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/6431
  147. // 下面这部分需要注释掉,否则会导致响应 400
  148. //if strings.HasSuffix(u, "/0") {
  149. // u = strings.Replace(u, "/0", "/640", 1)
  150. //} else if strings.Contains(u, "/0?") {
  151. // u = strings.Replace(u, "/0?", "/640?", 1)
  152. //}
  153. }
  154. util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(119), u), 15000)
  155. request := browserClient.R()
  156. request.SetRetryCount(1).SetRetryFixedInterval(3 * time.Second)
  157. if "" != originalURL {
  158. request.SetHeader("Referer", originalURL) // 改进浏览器剪藏扩展转换本地图片成功率 https://github.com/siyuan-note/siyuan/issues/7464
  159. }
  160. resp, reqErr := request.Get(u)
  161. if nil != reqErr {
  162. logging.LogErrorf("download net img [%s] failed: %s", u, reqErr)
  163. return ast.WalkSkipChildren
  164. }
  165. if 200 != resp.StatusCode {
  166. logging.LogErrorf("download net img [%s] failed: %d", u, resp.StatusCode)
  167. return ast.WalkSkipChildren
  168. }
  169. data, repErr := resp.ToBytes()
  170. if nil != repErr {
  171. logging.LogErrorf("download net img [%s] failed: %s", u, repErr)
  172. return ast.WalkSkipChildren
  173. }
  174. var name string
  175. if strings.Contains(u, "?") {
  176. name = u[:strings.Index(u, "?")]
  177. name = path.Base(name)
  178. } else {
  179. name = path.Base(u)
  180. }
  181. if strings.Contains(name, "#") {
  182. name = name[:strings.Index(name, "#")]
  183. }
  184. name, _ = url.PathUnescape(name)
  185. ext := path.Ext(name)
  186. if "" == ext {
  187. if mtype := mimetype.Detect(data); nil != mtype {
  188. ext = mtype.Extension()
  189. }
  190. }
  191. if "" == ext {
  192. contentType := resp.Header.Get("Content-Type")
  193. exts, _ := mime.ExtensionsByType(contentType)
  194. if 0 < len(exts) {
  195. ext = exts[0]
  196. }
  197. }
  198. name = strings.TrimSuffix(name, ext)
  199. name = util.FilterUploadFileName(name)
  200. name = util.TruncateLenFileName(name)
  201. if 1 > len(bytes.TrimSpace(linkText.Tokens)) {
  202. linkText.Tokens = []byte(name)
  203. }
  204. name = "net-img-" + name + "-" + ast.NewNodeID() + ext
  205. writePath := filepath.Join(assetsDirPath, name)
  206. if err = filelock.WriteFile(writePath, data); nil != err {
  207. logging.LogErrorf("write downloaded net img [%s] to local assets [%s] failed: %s", u, writePath, err)
  208. return ast.WalkSkipChildren
  209. }
  210. linkDest.Tokens = []byte("assets/" + name)
  211. files++
  212. }
  213. return ast.WalkSkipChildren
  214. }
  215. return ast.WalkContinue
  216. })
  217. if 0 < files {
  218. util.PushUpdateMsg(msgId, Conf.Language(113), 7000)
  219. if err = writeTreeUpsertQueue(tree); nil != err {
  220. return
  221. }
  222. util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(120), files), 5000)
  223. } else {
  224. util.PushUpdateMsg(msgId, Conf.Language(121), 3000)
  225. }
  226. return
  227. }
  228. func NetAssets2LocalAssets(rootID string) (err error) {
  229. tree, err := LoadTreeByBlockID(rootID)
  230. if nil != err {
  231. return
  232. }
  233. var files int
  234. msgId := gulu.Rand.String(7)
  235. docDirLocalPath := filepath.Join(util.DataDir, tree.Box, path.Dir(tree.Path))
  236. assetsDirPath := getAssetsDir(filepath.Join(util.DataDir, tree.Box), docDirLocalPath)
  237. if !gulu.File.IsExist(assetsDirPath) {
  238. if err = os.MkdirAll(assetsDirPath, 0755); nil != err {
  239. return
  240. }
  241. }
  242. browserClient := req.C().
  243. SetUserAgent(util.UserAgent).
  244. SetTimeout(30 * time.Second).
  245. EnableInsecureSkipVerify().
  246. SetProxy(httpclient.ProxyFromEnvironment)
  247. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  248. if !entering || (ast.NodeLinkDest != n.Type && !n.IsTextMarkType("a") && ast.NodeAudio != n.Type && ast.NodeVideo != n.Type) {
  249. return ast.WalkContinue
  250. }
  251. var dest []byte
  252. if ast.NodeLinkDest == n.Type {
  253. dest = n.Tokens
  254. } else if n.IsTextMarkType("a") {
  255. dest = []byte(n.TextMarkAHref)
  256. } else if ast.NodeAudio == n.Type || ast.NodeVideo == n.Type {
  257. if srcIndex := bytes.Index(n.Tokens, []byte("src=\"")); 0 < srcIndex {
  258. src := n.Tokens[srcIndex+len("src=\""):]
  259. if srcIndex = bytes.Index(src, []byte("\"")); 0 < srcIndex {
  260. src = src[:bytes.Index(src, []byte("\""))]
  261. dest = bytes.TrimSpace(src)
  262. }
  263. }
  264. }
  265. if util.IsAssetLinkDest(dest) {
  266. return ast.WalkContinue
  267. }
  268. if bytes.HasPrefix(bytes.ToLower(dest), []byte("file://")) { // 处理本地文件链接
  269. u := string(dest)[7:]
  270. unescaped, _ := url.PathUnescape(u)
  271. if unescaped != u {
  272. // `Convert network images/assets to local` supports URL-encoded local file names https://github.com/siyuan-note/siyuan/issues/9929
  273. u = unescaped
  274. }
  275. if !gulu.File.IsExist(u) || gulu.File.IsDir(u) {
  276. return ast.WalkContinue
  277. }
  278. name := filepath.Base(u)
  279. name = util.FilterUploadFileName(name)
  280. name = util.TruncateLenFileName(name)
  281. name = "network-asset-" + name
  282. name = util.AssetName(name)
  283. writePath := filepath.Join(assetsDirPath, name)
  284. if err = filelock.Copy(u, writePath); nil != err {
  285. logging.LogErrorf("copy [%s] to [%s] failed: %s", u, writePath, err)
  286. return ast.WalkContinue
  287. }
  288. if ast.NodeLinkDest == n.Type {
  289. n.Tokens = []byte("assets/" + name)
  290. } else if n.IsTextMarkType("a") {
  291. n.TextMarkAHref = "assets/" + name
  292. } else if ast.NodeAudio == n.Type || ast.NodeVideo == n.Type {
  293. n.Tokens = bytes.ReplaceAll(n.Tokens, dest, []byte("assets/"+name))
  294. }
  295. files++
  296. return ast.WalkContinue
  297. }
  298. if bytes.HasPrefix(bytes.ToLower(dest), []byte("https://")) || bytes.HasPrefix(bytes.ToLower(dest), []byte("http://")) || bytes.HasPrefix(dest, []byte("//")) {
  299. if bytes.HasPrefix(dest, []byte("//")) {
  300. // `Convert network images to local` supports `//` https://github.com/siyuan-note/siyuan/issues/10598
  301. dest = append([]byte("https:"), dest...)
  302. }
  303. u := string(dest)
  304. if strings.Contains(u, "qpic.cn") {
  305. // 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/5052
  306. if strings.Contains(u, "http://") {
  307. u = strings.Replace(u, "http://", "https://", 1)
  308. }
  309. // 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/6431
  310. // 下面这部分需要注释掉,否则会导致响应 400
  311. //if strings.HasSuffix(u, "/0") {
  312. // u = strings.Replace(u, "/0", "/640", 1)
  313. //} else if strings.Contains(u, "/0?") {
  314. // u = strings.Replace(u, "/0?", "/640?", 1)
  315. //}
  316. }
  317. util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(119), u), 15000)
  318. request := browserClient.R()
  319. request.SetRetryCount(1).SetRetryFixedInterval(3 * time.Second)
  320. resp, reqErr := request.Get(u)
  321. if strings.Contains(strings.ToLower(resp.GetContentType()), "text/html") {
  322. // 忽略超链接网页 `Convert network assets to local` no longer process webpage https://github.com/siyuan-note/siyuan/issues/9965
  323. return ast.WalkContinue
  324. }
  325. if nil != reqErr {
  326. logging.LogErrorf("download network asset [%s] failed: %s", u, reqErr)
  327. return ast.WalkContinue
  328. }
  329. if 200 != resp.StatusCode {
  330. logging.LogErrorf("download network asset [%s] failed: %d", u, resp.StatusCode)
  331. return ast.WalkContinue
  332. }
  333. if 1024*1024*96 < resp.ContentLength {
  334. logging.LogWarnf("network asset [%s]' size [%s] is large then [96 MB], ignore it", u, humanize.IBytes(uint64(resp.ContentLength)))
  335. return ast.WalkContinue
  336. }
  337. data, repErr := resp.ToBytes()
  338. if nil != repErr {
  339. logging.LogErrorf("download network asset [%s] failed: %s", u, repErr)
  340. return ast.WalkContinue
  341. }
  342. var name string
  343. if strings.Contains(u, "?") {
  344. name = u[:strings.Index(u, "?")]
  345. name = path.Base(name)
  346. } else {
  347. name = path.Base(u)
  348. }
  349. if strings.Contains(name, "#") {
  350. name = name[:strings.Index(name, "#")]
  351. }
  352. name, _ = url.PathUnescape(name)
  353. ext := path.Ext(name)
  354. if "" == ext {
  355. if mtype := mimetype.Detect(data); nil != mtype {
  356. ext = mtype.Extension()
  357. }
  358. }
  359. if "" == ext {
  360. contentType := resp.Header.Get("Content-Type")
  361. exts, _ := mime.ExtensionsByType(contentType)
  362. if 0 < len(exts) {
  363. ext = exts[0]
  364. }
  365. }
  366. name = strings.TrimSuffix(name, ext)
  367. name = util.FilterUploadFileName(name)
  368. name = util.TruncateLenFileName(name)
  369. name = "network-asset-" + name + "-" + ast.NewNodeID() + ext
  370. writePath := filepath.Join(assetsDirPath, name)
  371. if err = filelock.WriteFile(writePath, data); nil != err {
  372. logging.LogErrorf("write downloaded network asset [%s] to local asset [%s] failed: %s", u, writePath, err)
  373. return ast.WalkContinue
  374. }
  375. if ast.NodeLinkDest == n.Type {
  376. n.Tokens = []byte("assets/" + name)
  377. } else if n.IsTextMarkType("a") {
  378. n.TextMarkAHref = "assets/" + name
  379. } else if ast.NodeAudio == n.Type || ast.NodeVideo == n.Type {
  380. n.Tokens = bytes.ReplaceAll(n.Tokens, dest, []byte("assets/"+name))
  381. }
  382. files++
  383. }
  384. return ast.WalkContinue
  385. })
  386. if 0 < files {
  387. util.PushUpdateMsg(msgId, Conf.Language(113), 7000)
  388. if err = writeTreeUpsertQueue(tree); nil != err {
  389. return
  390. }
  391. util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(120), files), 5000)
  392. } else {
  393. util.PushUpdateMsg(msgId, Conf.Language(121), 3000)
  394. }
  395. return
  396. }
  397. func SearchAssetsByName(keyword string, exts []string) (ret []*cache.Asset) {
  398. ret = []*cache.Asset{}
  399. count := 0
  400. filterByExt := 0 < len(exts)
  401. for _, asset := range cache.GetAssets() {
  402. if filterByExt {
  403. ext := filepath.Ext(asset.HName)
  404. includeExt := false
  405. for _, e := range exts {
  406. if strings.ToLower(ext) == strings.ToLower(e) {
  407. includeExt = true
  408. break
  409. }
  410. }
  411. if !includeExt {
  412. continue
  413. }
  414. }
  415. lowerHName := strings.ToLower(asset.HName)
  416. lowerPath := strings.ToLower(asset.Path)
  417. lowerKeyword := strings.ToLower(keyword)
  418. hitName := strings.Contains(lowerHName, lowerKeyword)
  419. hitPath := strings.Contains(lowerPath, lowerKeyword)
  420. if !hitName && !hitPath {
  421. continue
  422. }
  423. hName := asset.HName
  424. if hitName {
  425. _, hName = search.MarkText(asset.HName, keyword, 64, Conf.Search.CaseSensitive)
  426. }
  427. ret = append(ret, &cache.Asset{
  428. HName: hName,
  429. Path: asset.Path,
  430. Updated: asset.Updated,
  431. })
  432. count++
  433. if Conf.Search.Limit <= count {
  434. return
  435. }
  436. }
  437. sort.Slice(ret, func(i, j int) bool {
  438. return ret[i].Updated > ret[j].Updated
  439. })
  440. return
  441. }
  442. func GetAssetAbsPath(relativePath string) (ret string, err error) {
  443. relativePath = strings.TrimSpace(relativePath)
  444. if strings.Contains(relativePath, "?") {
  445. relativePath = relativePath[:strings.Index(relativePath, "?")]
  446. }
  447. notebooks, err := ListNotebooks()
  448. if nil != err {
  449. err = errors.New(Conf.Language(0))
  450. return
  451. }
  452. // 在笔记本下搜索
  453. for _, notebook := range notebooks {
  454. notebookAbsPath := filepath.Join(util.DataDir, notebook.ID)
  455. filelock.Walk(notebookAbsPath, func(path string, info fs.FileInfo, _ error) error {
  456. if isSkipFile(info.Name()) {
  457. if info.IsDir() {
  458. return filepath.SkipDir
  459. }
  460. return nil
  461. }
  462. if p := filepath.ToSlash(path); strings.HasSuffix(p, relativePath) {
  463. if gulu.File.IsExist(path) {
  464. ret = path
  465. return io.EOF
  466. }
  467. }
  468. return nil
  469. })
  470. if "" != ret {
  471. if !util.IsSubPath(util.WorkspaceDir, ret) {
  472. err = fmt.Errorf("[%s] is not sub path of workspace", ret)
  473. return
  474. }
  475. return
  476. }
  477. }
  478. // 在全局 assets 路径下搜索
  479. p := filepath.Join(util.DataDir, relativePath)
  480. if gulu.File.IsExist(p) {
  481. ret = p
  482. if !util.IsSubPath(util.WorkspaceDir, ret) {
  483. err = fmt.Errorf("[%s] is not sub path of workspace", ret)
  484. return
  485. }
  486. return
  487. }
  488. return "", errors.New(fmt.Sprintf(Conf.Language(12), relativePath))
  489. }
  490. func UploadAssets2Cloud(rootID string) (count int, err error) {
  491. if !IsSubscriber() {
  492. return
  493. }
  494. tree, err := LoadTreeByBlockID(rootID)
  495. if nil != err {
  496. return
  497. }
  498. assets := assetsLinkDestsInTree(tree)
  499. embedAssets := assetsLinkDestsInQueryEmbedNodes(tree)
  500. assets = append(assets, embedAssets...)
  501. assets = gulu.Str.RemoveDuplicatedElem(assets)
  502. count, err = uploadAssets2Cloud(assets, bizTypeUploadAssets)
  503. if nil != err {
  504. return
  505. }
  506. return
  507. }
  508. const (
  509. bizTypeUploadAssets = "upload-assets"
  510. bizTypeExport2Liandi = "export-liandi"
  511. )
  512. // uploadAssets2Cloud 将资源文件上传到云端图床。
  513. func uploadAssets2Cloud(assetPaths []string, bizType string) (count int, err error) {
  514. var uploadAbsAssets []string
  515. for _, assetPath := range assetPaths {
  516. var absPath string
  517. absPath, err = GetAssetAbsPath(assetPath)
  518. if nil != err {
  519. logging.LogWarnf("get asset [%s] abs path failed: %s", assetPath, err)
  520. return
  521. }
  522. if "" == absPath {
  523. logging.LogErrorf("not found asset [%s]", assetPath)
  524. continue
  525. }
  526. uploadAbsAssets = append(uploadAbsAssets, absPath)
  527. }
  528. uploadAbsAssets = gulu.Str.RemoveDuplicatedElem(uploadAbsAssets)
  529. if 1 > len(uploadAbsAssets) {
  530. return
  531. }
  532. logging.LogInfof("uploading [%d] assets", len(uploadAbsAssets))
  533. msgId := util.PushMsg(fmt.Sprintf(Conf.Language(27), len(uploadAbsAssets)), 3000)
  534. if loadErr := LoadUploadToken(); nil != loadErr {
  535. util.PushMsg(loadErr.Error(), 5000)
  536. return
  537. }
  538. limitSize := uint64(3 * 1024 * 1024) // 3MB
  539. if IsSubscriber() {
  540. limitSize = 10 * 1024 * 1024 // 10MB
  541. }
  542. // metaType 为服务端 Filemeta.FILEMETA_TYPE,这里只有两个值:
  543. //
  544. // 5: SiYuan,表示为 SiYuan 上传图床
  545. // 4: Client,表示作为客户端分享发布帖子时上传的文件
  546. var metaType = "5"
  547. if bizTypeUploadAssets == bizType {
  548. metaType = "5"
  549. } else if bizTypeExport2Liandi == bizType {
  550. metaType = "4"
  551. }
  552. pushErrMsgCount := 0
  553. var completedUploadAssets []string
  554. for _, absAsset := range uploadAbsAssets {
  555. fi, statErr := os.Stat(absAsset)
  556. if nil != statErr {
  557. logging.LogErrorf("stat file [%s] failed: %s", absAsset, statErr)
  558. return count, statErr
  559. }
  560. if limitSize < uint64(fi.Size()) {
  561. logging.LogWarnf("file [%s] larger than limit size [%s], ignore uploading it", absAsset, humanize.IBytes(limitSize))
  562. if 3 > pushErrMsgCount {
  563. msg := fmt.Sprintf(Conf.Language(247), filepath.Base(absAsset), humanize.IBytes(limitSize))
  564. util.PushErrMsg(msg, 30000)
  565. }
  566. pushErrMsgCount++
  567. continue
  568. }
  569. msg := fmt.Sprintf(Conf.Language(27), html.EscapeString(absAsset))
  570. util.PushStatusBar(msg)
  571. util.PushUpdateMsg(msgId, msg, 3000)
  572. requestResult := gulu.Ret.NewResult()
  573. request := httpclient.NewCloudFileRequest2m()
  574. resp, reqErr := request.
  575. SetSuccessResult(requestResult).
  576. SetFile("file[]", absAsset).
  577. SetCookies(&http.Cookie{Name: "symphony", Value: uploadToken}).
  578. SetHeader("meta-type", metaType).
  579. SetHeader("biz-type", bizType).
  580. Post(util.GetCloudServer() + "/apis/siyuan/upload?ver=" + util.Ver)
  581. if nil != reqErr {
  582. logging.LogErrorf("upload assets failed: %s", reqErr)
  583. return count, ErrFailedToConnectCloudServer
  584. }
  585. if 401 == resp.StatusCode {
  586. err = errors.New(Conf.Language(31))
  587. return
  588. }
  589. if 0 != requestResult.Code {
  590. logging.LogErrorf("upload assets failed: %s", requestResult.Msg)
  591. err = errors.New(fmt.Sprintf(Conf.Language(94), requestResult.Msg))
  592. return
  593. }
  594. absAsset = filepath.ToSlash(absAsset)
  595. relAsset := absAsset[strings.Index(absAsset, "assets/"):]
  596. completedUploadAssets = append(completedUploadAssets, relAsset)
  597. logging.LogInfof("uploaded asset [%s]", relAsset)
  598. count++
  599. }
  600. util.PushClearMsg(msgId)
  601. if 0 < len(completedUploadAssets) {
  602. logging.LogInfof("uploaded [%d] assets", len(completedUploadAssets))
  603. }
  604. return
  605. }
  606. func RemoveUnusedAssets() (ret []string) {
  607. msgId := util.PushMsg(Conf.Language(100), 30*1000)
  608. defer func() {
  609. util.PushClearMsg(msgId)
  610. util.PushMsg(Conf.Language(99), 3000)
  611. }()
  612. ret = []string{}
  613. unusedAssets := UnusedAssets()
  614. historyDir, err := GetHistoryDir(HistoryOpClean)
  615. if nil != err {
  616. logging.LogErrorf("get history dir failed: %s", err)
  617. return
  618. }
  619. var hashes []string
  620. for _, p := range unusedAssets {
  621. historyPath := filepath.Join(historyDir, p)
  622. if p = filepath.Join(util.DataDir, p); filelock.IsExist(p) {
  623. if err = filelock.Copy(p, historyPath); nil != err {
  624. return
  625. }
  626. hash, _ := util.GetEtag(p)
  627. hashes = append(hashes, hash)
  628. }
  629. }
  630. sql.BatchRemoveAssetsQueue(hashes)
  631. for _, unusedAsset := range unusedAssets {
  632. if unusedAsset = filepath.Join(util.DataDir, unusedAsset); filelock.IsExist(unusedAsset) {
  633. if err := filelock.Remove(unusedAsset); nil != err {
  634. logging.LogErrorf("remove unused asset [%s] failed: %s", unusedAsset, err)
  635. }
  636. }
  637. ret = append(ret, unusedAsset)
  638. }
  639. if 0 < len(ret) {
  640. IncSync()
  641. }
  642. indexHistoryDir(filepath.Base(historyDir), util.NewLute())
  643. cache.LoadAssets()
  644. return
  645. }
  646. func RemoveUnusedAsset(p string) (ret string) {
  647. absPath := filepath.Join(util.DataDir, p)
  648. if !filelock.IsExist(absPath) {
  649. return absPath
  650. }
  651. historyDir, err := GetHistoryDir(HistoryOpClean)
  652. if nil != err {
  653. logging.LogErrorf("get history dir failed: %s", err)
  654. return
  655. }
  656. newP := strings.TrimPrefix(absPath, util.DataDir)
  657. historyPath := filepath.Join(historyDir, newP)
  658. if filelock.IsExist(absPath) {
  659. if err = filelock.Copy(absPath, historyPath); nil != err {
  660. return
  661. }
  662. hash, _ := util.GetEtag(absPath)
  663. sql.BatchRemoveAssetsQueue([]string{hash})
  664. }
  665. if err = filelock.Remove(absPath); nil != err {
  666. logging.LogErrorf("remove unused asset [%s] failed: %s", absPath, err)
  667. }
  668. ret = absPath
  669. IncSync()
  670. indexHistoryDir(filepath.Base(historyDir), util.NewLute())
  671. cache.RemoveAsset(p)
  672. return
  673. }
  674. func RenameAsset(oldPath, newName string) (err error) {
  675. util.PushEndlessProgress(Conf.Language(110))
  676. defer util.PushClearProgress()
  677. newName = strings.TrimSpace(newName)
  678. newName = gulu.Str.RemoveInvisible(newName)
  679. if path.Base(oldPath) == newName {
  680. return
  681. }
  682. if "" == newName {
  683. return
  684. }
  685. if !gulu.File.IsValidFilename(newName) {
  686. err = errors.New(Conf.Language(151))
  687. return
  688. }
  689. newName = util.AssetName(newName + filepath.Ext(oldPath))
  690. newPath := "assets/" + newName
  691. if err = filelock.Copy(filepath.Join(util.DataDir, oldPath), filepath.Join(util.DataDir, newPath)); nil != err {
  692. logging.LogErrorf("copy asset [%s] failed: %s", oldPath, err)
  693. return
  694. }
  695. if filelock.IsExist(filepath.Join(util.DataDir, oldPath+".sya")) {
  696. // Rename the .sya annotation file when renaming a PDF asset https://github.com/siyuan-note/siyuan/issues/9390
  697. if err = filelock.Copy(filepath.Join(util.DataDir, oldPath+".sya"), filepath.Join(util.DataDir, newPath+".sya")); nil != err {
  698. logging.LogErrorf("copy PDF annotation [%s] failed: %s", oldPath+".sya", err)
  699. return
  700. }
  701. }
  702. oldName := path.Base(oldPath)
  703. notebooks, err := ListNotebooks()
  704. if nil != err {
  705. return
  706. }
  707. luteEngine := util.NewLute()
  708. for _, notebook := range notebooks {
  709. pages := pagedPaths(filepath.Join(util.DataDir, notebook.ID), 32)
  710. for _, paths := range pages {
  711. for _, treeAbsPath := range paths {
  712. data, readErr := filelock.ReadFile(treeAbsPath)
  713. if nil != readErr {
  714. logging.LogErrorf("get data [path=%s] failed: %s", treeAbsPath, readErr)
  715. err = readErr
  716. return
  717. }
  718. if !bytes.Contains(data, []byte(oldName)) {
  719. continue
  720. }
  721. data = bytes.Replace(data, []byte(oldName), []byte(newName), -1)
  722. if writeErr := filelock.WriteFile(treeAbsPath, data); nil != writeErr {
  723. logging.LogErrorf("write data [path=%s] failed: %s", treeAbsPath, writeErr)
  724. err = writeErr
  725. return
  726. }
  727. p := filepath.ToSlash(strings.TrimPrefix(treeAbsPath, filepath.Join(util.DataDir, notebook.ID)))
  728. tree, parseErr := filesys.LoadTreeByData(data, notebook.ID, p, luteEngine)
  729. if nil != parseErr {
  730. logging.LogWarnf("parse json to tree [%s] failed: %s", treeAbsPath, parseErr)
  731. continue
  732. }
  733. treenode.IndexBlockTree(tree)
  734. sql.UpsertTreeQueue(tree)
  735. util.PushEndlessProgress(fmt.Sprintf(Conf.Language(111), util.EscapeHTML(tree.Root.IALAttr("title"))))
  736. }
  737. }
  738. }
  739. IncSync()
  740. util.ReloadUI()
  741. return
  742. }
  743. func UnusedAssets() (ret []string) {
  744. defer logging.Recover()
  745. ret = []string{}
  746. assetsPathMap, err := allAssetAbsPaths()
  747. if nil != err {
  748. return
  749. }
  750. linkDestMap := map[string]bool{}
  751. notebooks, err := ListNotebooks()
  752. if nil != err {
  753. return
  754. }
  755. luteEngine := util.NewLute()
  756. for _, notebook := range notebooks {
  757. dests := map[string]bool{}
  758. // 分页加载,优化清理未引用资源内存占用 https://github.com/siyuan-note/siyuan/issues/5200
  759. pages := pagedPaths(filepath.Join(util.DataDir, notebook.ID), 32)
  760. for _, paths := range pages {
  761. var trees []*parse.Tree
  762. for _, localPath := range paths {
  763. tree, loadTreeErr := loadTree(localPath, luteEngine)
  764. if nil != loadTreeErr {
  765. continue
  766. }
  767. trees = append(trees, tree)
  768. }
  769. for _, tree := range trees {
  770. for _, d := range assetsLinkDestsInTree(tree) {
  771. dests[d] = true
  772. }
  773. if titleImgPath := treenode.GetDocTitleImgPath(tree.Root); "" != titleImgPath {
  774. // 题头图计入
  775. if !util.IsAssetLinkDest([]byte(titleImgPath)) {
  776. continue
  777. }
  778. dests[titleImgPath] = true
  779. }
  780. }
  781. }
  782. var linkDestFolderPaths, linkDestFilePaths []string
  783. for dest := range dests {
  784. if !strings.HasPrefix(dest, "assets/") {
  785. continue
  786. }
  787. if idx := strings.Index(dest, "?"); 0 < idx {
  788. // `pdf?page` 资源文件链接会被判定为未引用资源 https://github.com/siyuan-note/siyuan/issues/5649
  789. dest = dest[:idx]
  790. }
  791. if "" == assetsPathMap[dest] {
  792. continue
  793. }
  794. if strings.HasSuffix(dest, "/") {
  795. linkDestFolderPaths = append(linkDestFolderPaths, dest)
  796. } else {
  797. linkDestFilePaths = append(linkDestFilePaths, dest)
  798. }
  799. }
  800. // 排除文件夹链接
  801. var toRemoves []string
  802. for asset := range assetsPathMap {
  803. for _, linkDestFolder := range linkDestFolderPaths {
  804. if strings.HasPrefix(asset, linkDestFolder) {
  805. toRemoves = append(toRemoves, asset)
  806. }
  807. }
  808. for _, linkDestPath := range linkDestFilePaths {
  809. if strings.HasPrefix(linkDestPath, asset) {
  810. toRemoves = append(toRemoves, asset)
  811. }
  812. }
  813. }
  814. for _, toRemove := range toRemoves {
  815. delete(assetsPathMap, toRemove)
  816. }
  817. for _, dest := range linkDestFilePaths {
  818. linkDestMap[dest] = true
  819. if strings.HasSuffix(dest, ".pdf") {
  820. linkDestMap[dest+".sya"] = true
  821. }
  822. }
  823. }
  824. var toRemoves []string
  825. for asset := range assetsPathMap {
  826. if strings.HasSuffix(asset, "ocr-texts.json") {
  827. // 排除 OCR 结果文本
  828. toRemoves = append(toRemoves, asset)
  829. continue
  830. }
  831. }
  832. // 排除数据库中引用的资源文件
  833. storageAvDir := filepath.Join(util.DataDir, "storage", "av")
  834. if gulu.File.IsDir(storageAvDir) {
  835. entries, readErr := os.ReadDir(storageAvDir)
  836. if nil != readErr {
  837. logging.LogErrorf("read dir [%s] failed: %s", storageAvDir, readErr)
  838. err = readErr
  839. return
  840. }
  841. for _, entry := range entries {
  842. if !strings.HasSuffix(entry.Name(), ".json") || !ast.IsNodeIDPattern(strings.TrimSuffix(entry.Name(), ".json")) {
  843. continue
  844. }
  845. data, readDataErr := filelock.ReadFile(filepath.Join(util.DataDir, "storage", "av", entry.Name()))
  846. if nil != readDataErr {
  847. logging.LogErrorf("read file [%s] failed: %s", entry.Name(), readDataErr)
  848. err = readDataErr
  849. return
  850. }
  851. for asset := range assetsPathMap {
  852. if bytes.Contains(data, []byte(asset)) {
  853. toRemoves = append(toRemoves, asset)
  854. }
  855. }
  856. }
  857. }
  858. for _, toRemove := range toRemoves {
  859. delete(assetsPathMap, toRemove)
  860. }
  861. dataAssetsAbsPath := util.GetDataAssetsAbsPath()
  862. for dest, assetAbsPath := range assetsPathMap {
  863. if _, ok := linkDestMap[dest]; ok {
  864. continue
  865. }
  866. var p string
  867. if strings.HasPrefix(dataAssetsAbsPath, assetAbsPath) {
  868. p = assetAbsPath[strings.Index(assetAbsPath, "assets"):]
  869. } else {
  870. p = strings.TrimPrefix(assetAbsPath, filepath.Dir(dataAssetsAbsPath))
  871. }
  872. p = filepath.ToSlash(p)
  873. if strings.HasPrefix(p, "/") {
  874. p = p[1:]
  875. }
  876. ret = append(ret, p)
  877. }
  878. sort.Strings(ret)
  879. return
  880. }
  881. func MissingAssets() (ret []string) {
  882. defer logging.Recover()
  883. ret = []string{}
  884. assetsPathMap, err := allAssetAbsPaths()
  885. if nil != err {
  886. return
  887. }
  888. notebooks, err := ListNotebooks()
  889. if nil != err {
  890. return
  891. }
  892. luteEngine := util.NewLute()
  893. for _, notebook := range notebooks {
  894. if notebook.Closed {
  895. continue
  896. }
  897. dests := map[string]bool{}
  898. pages := pagedPaths(filepath.Join(util.DataDir, notebook.ID), 32)
  899. for _, paths := range pages {
  900. var trees []*parse.Tree
  901. for _, localPath := range paths {
  902. tree, loadTreeErr := loadTree(localPath, luteEngine)
  903. if nil != loadTreeErr {
  904. continue
  905. }
  906. trees = append(trees, tree)
  907. }
  908. for _, tree := range trees {
  909. for _, d := range assetsLinkDestsInTree(tree) {
  910. dests[d] = true
  911. }
  912. if titleImgPath := treenode.GetDocTitleImgPath(tree.Root); "" != titleImgPath {
  913. // 题头图计入
  914. if !util.IsAssetLinkDest([]byte(titleImgPath)) {
  915. continue
  916. }
  917. dests[titleImgPath] = true
  918. }
  919. }
  920. }
  921. for dest := range dests {
  922. if !strings.HasPrefix(dest, "assets/") {
  923. continue
  924. }
  925. if idx := strings.Index(dest, "?"); 0 < idx {
  926. dest = dest[:idx]
  927. }
  928. if strings.HasSuffix(dest, "/") {
  929. continue
  930. }
  931. if "" == assetsPathMap[dest] {
  932. if strings.HasPrefix(dest, "assets/.") {
  933. // Assets starting with `.` should not be considered missing assets https://github.com/siyuan-note/siyuan/issues/8821
  934. if !filelock.IsExist(filepath.Join(util.DataDir, dest)) {
  935. ret = append(ret, dest)
  936. }
  937. } else {
  938. ret = append(ret, dest)
  939. }
  940. continue
  941. }
  942. }
  943. }
  944. sort.Strings(ret)
  945. return
  946. }
  947. func emojisInTree(tree *parse.Tree) (ret []string) {
  948. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  949. if !entering {
  950. return ast.WalkContinue
  951. }
  952. if ast.NodeEmojiImg == n.Type {
  953. tokens := n.Tokens
  954. idx := bytes.Index(tokens, []byte("src=\""))
  955. if -1 == idx {
  956. return ast.WalkContinue
  957. }
  958. src := tokens[idx+len("src=\""):]
  959. src = src[:bytes.Index(src, []byte("\""))]
  960. ret = append(ret, string(src))
  961. }
  962. return ast.WalkContinue
  963. })
  964. ret = gulu.Str.RemoveDuplicatedElem(ret)
  965. return
  966. }
  967. func assetsLinkDestsInQueryEmbedNodes(tree *parse.Tree) (ret []string) {
  968. // The images in the embed blocks are not uploaded to the community hosting https://github.com/siyuan-note/siyuan/issues/10042
  969. ret = []string{}
  970. ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  971. if !entering || ast.NodeBlockQueryEmbedScript != n.Type {
  972. return ast.WalkContinue
  973. }
  974. stmt := n.TokensStr()
  975. stmt = html.UnescapeString(stmt)
  976. stmt = strings.ReplaceAll(stmt, editor.IALValEscNewLine, "\n")
  977. sqlBlocks := sql.SelectBlocksRawStmt(stmt, 1, Conf.Search.Limit)
  978. for _, sqlBlock := range sqlBlocks {
  979. subtree, _ := LoadTreeByBlockID(sqlBlock.ID)
  980. if nil == subtree {
  981. continue
  982. }
  983. embedNode := treenode.GetNodeInTree(subtree, sqlBlock.ID)
  984. if nil == embedNode {
  985. continue
  986. }
  987. ret = append(ret, assetsLinkDestsInNode(embedNode)...)
  988. }
  989. return ast.WalkContinue
  990. })
  991. ret = gulu.Str.RemoveDuplicatedElem(ret)
  992. return
  993. }
  994. func assetsLinkDestsInTree(tree *parse.Tree) (ret []string) {
  995. ret = assetsLinkDestsInNode(tree.Root)
  996. return
  997. }
  998. func assetsLinkDestsInNode(node *ast.Node) (ret []string) {
  999. ret = []string{}
  1000. ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
  1001. // 修改以下代码时需要同时修改 database 构造行级元素实现,增加必要的类型
  1002. if !entering || (ast.NodeLinkDest != n.Type && ast.NodeHTMLBlock != n.Type && ast.NodeInlineHTML != n.Type &&
  1003. ast.NodeIFrame != n.Type && ast.NodeWidget != n.Type && ast.NodeAudio != n.Type && ast.NodeVideo != n.Type &&
  1004. !n.IsTextMarkType("a") && !n.IsTextMarkType("file-annotation-ref")) {
  1005. return ast.WalkContinue
  1006. }
  1007. if ast.NodeLinkDest == n.Type {
  1008. if !treenode.IsRelativePath(n.Tokens) {
  1009. return ast.WalkContinue
  1010. }
  1011. dest := strings.TrimSpace(string(n.Tokens))
  1012. ret = append(ret, dest)
  1013. } else if n.IsTextMarkType("a") {
  1014. if !treenode.IsRelativePath(gulu.Str.ToBytes(n.TextMarkAHref)) {
  1015. return ast.WalkContinue
  1016. }
  1017. dest := strings.TrimSpace(n.TextMarkAHref)
  1018. ret = append(ret, dest)
  1019. } else if n.IsTextMarkType("file-annotation-ref") {
  1020. if !treenode.IsRelativePath(gulu.Str.ToBytes(n.TextMarkFileAnnotationRefID)) {
  1021. return ast.WalkContinue
  1022. }
  1023. if !strings.Contains(n.TextMarkFileAnnotationRefID, "/") {
  1024. return ast.WalkContinue
  1025. }
  1026. dest := n.TextMarkFileAnnotationRefID[:strings.LastIndexByte(n.TextMarkFileAnnotationRefID, '/')]
  1027. dest = strings.TrimSpace(dest)
  1028. ret = append(ret, dest)
  1029. } else {
  1030. if ast.NodeWidget == n.Type {
  1031. dataAssets := n.IALAttr("custom-data-assets")
  1032. if "" == dataAssets {
  1033. // 兼容两种属性名 custom-data-assets 和 data-assets https://github.com/siyuan-note/siyuan/issues/4122#issuecomment-1154796568
  1034. dataAssets = n.IALAttr("data-assets")
  1035. }
  1036. if "" == dataAssets || !treenode.IsRelativePath([]byte(dataAssets)) {
  1037. return ast.WalkContinue
  1038. }
  1039. ret = append(ret, dataAssets)
  1040. } else { // HTMLBlock/InlineHTML/IFrame/Audio/Video
  1041. dest := treenode.GetNodeSrcTokens(n)
  1042. if "" != dest {
  1043. ret = append(ret, dest)
  1044. }
  1045. }
  1046. }
  1047. return ast.WalkContinue
  1048. })
  1049. ret = gulu.Str.RemoveDuplicatedElem(ret)
  1050. for i, dest := range ret {
  1051. // 对于 macOS 的 rtfd 文件夹格式需要特殊处理,为其加上结尾 /
  1052. if strings.HasSuffix(dest, ".rtfd") {
  1053. ret[i] = dest + "/"
  1054. }
  1055. }
  1056. return
  1057. }
  1058. // allAssetAbsPaths 返回 asset 相对路径(assets/xxx)到绝对路径(F:\SiYuan\data\assets\xxx)的映射。
  1059. func allAssetAbsPaths() (assetsAbsPathMap map[string]string, err error) {
  1060. notebooks, err := ListNotebooks()
  1061. if nil != err {
  1062. return
  1063. }
  1064. assetsAbsPathMap = map[string]string{}
  1065. // 笔记本 assets
  1066. for _, notebook := range notebooks {
  1067. notebookAbsPath := filepath.Join(util.DataDir, notebook.ID)
  1068. filelock.Walk(notebookAbsPath, func(path string, info fs.FileInfo, err error) error {
  1069. if notebookAbsPath == path {
  1070. return nil
  1071. }
  1072. if isSkipFile(info.Name()) {
  1073. if info.IsDir() {
  1074. return filepath.SkipDir
  1075. }
  1076. return nil
  1077. }
  1078. if info.IsDir() && "assets" == info.Name() {
  1079. filelock.Walk(path, func(assetPath string, info fs.FileInfo, err error) error {
  1080. if path == assetPath {
  1081. return nil
  1082. }
  1083. if isSkipFile(info.Name()) {
  1084. if info.IsDir() {
  1085. return filepath.SkipDir
  1086. }
  1087. return nil
  1088. }
  1089. relPath := filepath.ToSlash(assetPath)
  1090. relPath = relPath[strings.Index(relPath, "assets/"):]
  1091. if info.IsDir() {
  1092. relPath += "/"
  1093. }
  1094. assetsAbsPathMap[relPath] = assetPath
  1095. return nil
  1096. })
  1097. return filepath.SkipDir
  1098. }
  1099. return nil
  1100. })
  1101. }
  1102. // 全局 assets
  1103. dataAssetsAbsPath := util.GetDataAssetsAbsPath()
  1104. filelock.Walk(dataAssetsAbsPath, func(assetPath string, info fs.FileInfo, err error) error {
  1105. if dataAssetsAbsPath == assetPath {
  1106. return nil
  1107. }
  1108. if isSkipFile(info.Name()) {
  1109. if info.IsDir() {
  1110. return filepath.SkipDir
  1111. }
  1112. return nil
  1113. }
  1114. relPath := filepath.ToSlash(assetPath)
  1115. relPath = relPath[strings.Index(relPath, "assets/"):]
  1116. if info.IsDir() {
  1117. relPath += "/"
  1118. }
  1119. assetsAbsPathMap[relPath] = assetPath
  1120. return nil
  1121. })
  1122. return
  1123. }
  1124. // copyBoxAssetsToDataAssets 将笔记本路径下所有(包括子文档)的 assets 复制一份到 data/assets 中。
  1125. func copyBoxAssetsToDataAssets(boxID string) {
  1126. boxLocalPath := filepath.Join(util.DataDir, boxID)
  1127. copyAssetsToDataAssets(boxLocalPath)
  1128. }
  1129. // copyDocAssetsToDataAssets 将文档路径下所有(包括子文档)的 assets 复制一份到 data/assets 中。
  1130. func copyDocAssetsToDataAssets(boxID, parentDocPath string) {
  1131. boxLocalPath := filepath.Join(util.DataDir, boxID)
  1132. parentDocDirAbsPath := filepath.Dir(filepath.Join(boxLocalPath, parentDocPath))
  1133. copyAssetsToDataAssets(parentDocDirAbsPath)
  1134. }
  1135. func copyAssetsToDataAssets(rootPath string) {
  1136. var assetsDirPaths []string
  1137. filelock.Walk(rootPath, func(path string, info fs.FileInfo, err error) error {
  1138. if rootPath == path || nil == info {
  1139. return nil
  1140. }
  1141. isDir := info.IsDir()
  1142. name := info.Name()
  1143. if isSkipFile(name) {
  1144. if isDir {
  1145. return filepath.SkipDir
  1146. }
  1147. return nil
  1148. }
  1149. if "assets" == name && isDir {
  1150. assetsDirPaths = append(assetsDirPaths, path)
  1151. }
  1152. return nil
  1153. })
  1154. dataAssetsPath := filepath.Join(util.DataDir, "assets")
  1155. for _, assetsDirPath := range assetsDirPaths {
  1156. if err := filelock.Copy(assetsDirPath, dataAssetsPath); nil != err {
  1157. logging.LogErrorf("copy tree assets from [%s] to [%s] failed: %s", assetsDirPaths, dataAssetsPath, err)
  1158. }
  1159. }
  1160. }