2023-06-24 20:39:55 +08:00
// SiYuan - Refactor your thinking
2022-05-26 15:18:53 +08:00
// 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 <https://www.gnu.org/licenses/>.
package model
import (
"bytes"
"errors"
"fmt"
"io/fs"
"mime"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
2023-08-31 21:15:31 +08:00
"time"
2022-05-26 15:18:53 +08:00
2024-04-24 19:51:15 +08:00
"github.com/88250/go-humanize"
2022-05-26 15:18:53 +08:00
"github.com/88250/gulu"
"github.com/88250/lute/ast"
2024-01-21 21:27:50 +08:00
"github.com/88250/lute/editor"
2023-05-15 14:56:12 +08:00
"github.com/88250/lute/html"
2022-05-26 15:18:53 +08:00
"github.com/88250/lute/parse"
2022-05-30 18:02:12 +08:00
"github.com/gabriel-vasile/mimetype"
2022-06-15 23:56:47 +08:00
"github.com/siyuan-note/filelock"
2022-06-23 01:22:28 +08:00
"github.com/siyuan-note/httpclient"
2022-07-17 12:22:32 +08:00
"github.com/siyuan-note/logging"
2024-07-11 10:25:58 +08:00
"github.com/siyuan-note/siyuan/kernel/av"
2022-07-15 09:25:02 +08:00
"github.com/siyuan-note/siyuan/kernel/cache"
2023-02-10 12:05:56 +08:00
"github.com/siyuan-note/siyuan/kernel/filesys"
2022-05-26 15:18:53 +08:00
"github.com/siyuan-note/siyuan/kernel/search"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func DocImageAssets ( rootID string ) ( ret [ ] string , err error ) {
2024-03-10 23:27:13 +08:00
tree , err := LoadTreeByBlockID ( rootID )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
return
}
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if ast . NodeImage == n . Type {
linkDest := n . ChildByType ( ast . NodeLinkDest )
dest := linkDest . Tokens
2022-09-14 10:25:15 +08:00
if 1 > len ( dest ) { // 双击打开图片不对 https://github.com/siyuan-note/siyuan/issues/5876
return ast . WalkContinue
}
2022-05-26 15:18:53 +08:00
ret = append ( ret , gulu . Str . FromBytes ( dest ) )
}
return ast . WalkContinue
} )
return
}
2025-01-30 15:06:57 +08:00
func DocAssets ( rootID string ) ( ret [ ] string , err error ) {
tree , err := LoadTreeByBlockID ( rootID )
if err != nil {
return
}
ret = assetsLinkDestsInTree ( tree )
return
}
2024-07-26 18:41:46 +08:00
func NetAssets2LocalAssets ( rootID string , onlyImg bool , originalURL string ) ( err error ) {
2024-03-10 23:27:13 +08:00
tree , err := LoadTreeByBlockID ( rootID )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-11-29 10:07:52 +08:00
return
}
var files int
msgId := gulu . Rand . String ( 7 )
docDirLocalPath := filepath . Join ( util . DataDir , tree . Box , path . Dir ( tree . Path ) )
assetsDirPath := getAssetsDir ( filepath . Join ( util . DataDir , tree . Box ) , docDirLocalPath )
if ! gulu . File . IsExist ( assetsDirPath ) {
2024-09-04 04:40:50 +03:00
if err = os . MkdirAll ( assetsDirPath , 0755 ) ; err != nil {
2023-11-29 10:07:52 +08:00
return
}
}
2025-05-29 18:04:50 +08:00
browserClient := util . CreateCustomReqClient ( ) .
EnableInsecureSkipVerify ( ) // 添加了自定义TLS指纹, 可以完成对于CDN的资源下载
2023-11-29 10:07:52 +08:00
2025-01-26 10:28:36 +08:00
forbiddenCount := 0
2024-07-26 18:41:46 +08:00
destNodes := getRemoteAssetsLinkDestsInTree ( tree , onlyImg )
for _ , destNode := range destNodes {
2024-07-26 18:55:01 +08:00
dests := getRemoteAssetsLinkDests ( destNode , onlyImg )
if 1 > len ( dests ) {
2024-07-26 18:41:46 +08:00
continue
2023-11-29 10:07:52 +08:00
}
2024-07-26 18:55:01 +08:00
for _ , dest := range dests {
if strings . HasPrefix ( strings . ToLower ( dest ) , "file://" ) { // 处理本地文件链接
u := dest [ 7 : ]
unescaped , _ := url . PathUnescape ( u )
if unescaped != u {
// `Convert network images/assets to local` supports URL-encoded local file names https://github.com/siyuan-note/siyuan/issues/9929
u = unescaped
}
if strings . Contains ( u , ":" ) {
u = strings . TrimPrefix ( u , "/" )
}
2024-12-24 16:33:24 +08:00
if strings . Contains ( u , "?" ) {
u = u [ : strings . Index ( u , "?" ) ]
}
2024-07-04 23:11:56 +08:00
2024-07-26 18:55:01 +08:00
if ! gulu . File . IsExist ( u ) || gulu . File . IsDir ( u ) {
continue
}
name := filepath . Base ( u )
name = util . FilterUploadFileName ( name )
name = "network-asset-" + name
name = util . AssetName ( name )
writePath := filepath . Join ( assetsDirPath , name )
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( u , writePath ) ; err != nil {
2024-07-26 18:55:01 +08:00
logging . LogErrorf ( "copy [%s] to [%s] failed: %s" , u , writePath , err )
continue
}
2023-11-29 10:07:52 +08:00
2024-07-26 18:55:01 +08:00
setAssetsLinkDest ( destNode , dest , "assets/" + name )
files ++
2024-07-26 18:41:46 +08:00
continue
2023-11-29 10:07:52 +08:00
}
2024-07-26 18:55:01 +08:00
if strings . HasPrefix ( strings . ToLower ( dest ) , "https://" ) || strings . HasPrefix ( strings . ToLower ( dest ) , "http://" ) || strings . HasPrefix ( dest , "//" ) {
if strings . HasPrefix ( dest , "//" ) {
// `Convert network images to local` supports `//` https://github.com/siyuan-note/siyuan/issues/10598
dest = "https:" + dest
}
2023-11-29 10:07:52 +08:00
2024-07-26 18:55:01 +08:00
u := dest
if strings . Contains ( u , "qpic.cn" ) {
// 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/5052
if strings . Contains ( u , "http://" ) {
u = strings . Replace ( u , "http://" , "https://" , 1 )
}
2024-03-14 11:15:21 +08:00
2024-07-26 18:55:01 +08:00
// 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/6431
// 下面这部分需要注释掉,否则会导致响应 400
//if strings.HasSuffix(u, "/0") {
// u = strings.Replace(u, "/0", "/640", 1)
//} else if strings.Contains(u, "/0?") {
// u = strings.Replace(u, "/0?", "/640?", 1)
//}
2023-11-29 10:07:52 +08:00
}
2024-07-26 18:55:01 +08:00
displayU := u
if 64 < len ( displayU ) {
displayU = displayU [ : 64 ] + "..."
}
util . PushUpdateMsg ( msgId , fmt . Sprintf ( Conf . Language ( 119 ) , displayU ) , 15000 )
request := browserClient . R ( )
request . SetRetryCount ( 1 ) . SetRetryFixedInterval ( 3 * time . Second )
if "" != originalURL {
request . SetHeader ( "Referer" , originalURL ) // 改进浏览器剪藏扩展转换本地图片成功率 https://github.com/siyuan-note/siyuan/issues/7464
}
resp , reqErr := request . Get ( u )
2025-02-20 10:09:24 +08:00
if nil != reqErr {
logging . LogErrorf ( "download network asset [%s] failed: %s" , u , reqErr )
continue
}
2025-01-26 10:28:36 +08:00
if http . StatusForbidden == resp . StatusCode || http . StatusUnauthorized == resp . StatusCode {
forbiddenCount ++
}
2024-07-26 18:55:01 +08:00
if strings . Contains ( strings . ToLower ( resp . GetContentType ( ) ) , "text/html" ) {
// 忽略超链接网页 `Convert network assets to local` no longer process webpage https://github.com/siyuan-note/siyuan/issues/9965
continue
}
if 200 != resp . StatusCode {
logging . LogErrorf ( "download network asset [%s] failed: %d" , u , resp . StatusCode )
continue
}
2023-11-29 10:07:52 +08:00
2024-07-26 18:55:01 +08:00
if 1024 * 1024 * 96 < resp . ContentLength {
logging . LogWarnf ( "network asset [%s]' size [%s] is large then [96 MB], ignore it" , u , humanize . IBytes ( uint64 ( resp . ContentLength ) ) )
continue
}
2023-11-29 10:07:52 +08:00
2024-07-26 18:55:01 +08:00
data , repErr := resp . ToBytes ( )
if nil != repErr {
logging . LogErrorf ( "download network asset [%s] failed: %s" , u , repErr )
continue
2023-11-29 10:07:52 +08:00
}
2024-07-26 18:55:01 +08:00
var name string
if strings . Contains ( u , "?" ) {
name = u [ : strings . Index ( u , "?" ) ]
name = path . Base ( name )
} else {
name = path . Base ( u )
2023-11-29 10:07:52 +08:00
}
2024-07-26 18:55:01 +08:00
if strings . Contains ( name , "#" ) {
name = name [ : strings . Index ( name , "#" ) ]
}
name , _ = url . PathUnescape ( name )
2024-12-04 22:37:22 +08:00
name = util . FilterUploadFileName ( name )
ext := util . Ext ( name )
2024-07-26 18:55:01 +08:00
if "" == ext {
if mtype := mimetype . Detect ( data ) ; nil != mtype {
ext = mtype . Extension ( )
2024-12-04 22:37:22 +08:00
name += ext
2024-07-26 18:55:01 +08:00
}
}
2024-12-04 22:37:22 +08:00
if "" == ext && bytes . HasPrefix ( data , [ ] byte ( "<svg " ) ) && bytes . HasSuffix ( data , [ ] byte ( "</svg>" ) ) {
ext = ".svg"
name += ext
}
2024-07-26 18:55:01 +08:00
if "" == ext {
contentType := resp . Header . Get ( "Content-Type" )
exts , _ := mime . ExtensionsByType ( contentType )
if 0 < len ( exts ) {
ext = exts [ 0 ]
2024-12-04 22:37:22 +08:00
name += ext
2024-07-26 18:55:01 +08:00
}
}
2024-12-04 22:37:22 +08:00
name = util . AssetName ( name )
name = "network-asset-" + name
2024-07-26 18:55:01 +08:00
writePath := filepath . Join ( assetsDirPath , name )
2024-09-04 04:40:50 +03:00
if err = filelock . WriteFile ( writePath , data ) ; err != nil {
2024-07-26 18:55:01 +08:00
logging . LogErrorf ( "write downloaded network asset [%s] to local asset [%s] failed: %s" , u , writePath , err )
continue
}
setAssetsLinkDest ( destNode , dest , "assets/" + name )
files ++
2024-07-26 18:41:46 +08:00
continue
2023-11-29 10:07:52 +08:00
}
}
2024-07-26 18:41:46 +08:00
}
2023-11-29 10:07:52 +08:00
2025-01-26 10:28:36 +08:00
util . PushClearMsg ( msgId )
2023-11-29 10:07:52 +08:00
if 0 < files {
2025-01-26 10:28:36 +08:00
msgId = util . PushMsg ( Conf . Language ( 113 ) , 7000 )
2024-09-04 04:40:50 +03:00
if err = writeTreeUpsertQueue ( tree ) ; err != nil {
2023-11-29 10:07:52 +08:00
return
}
util . PushUpdateMsg ( msgId , fmt . Sprintf ( Conf . Language ( 120 ) , files ) , 5000 )
2025-01-26 10:28:36 +08:00
if 0 < forbiddenCount {
util . PushErrMsg ( fmt . Sprintf ( Conf . Language ( 255 ) , forbiddenCount ) , 5000 )
}
2023-12-06 09:31:42 +08:00
} else {
2025-01-26 10:28:36 +08:00
if 0 < forbiddenCount {
util . PushErrMsg ( fmt . Sprintf ( Conf . Language ( 255 ) , forbiddenCount ) , 5000 )
} else {
util . PushMsg ( Conf . Language ( 121 ) , 3000 )
}
2023-11-29 10:07:52 +08:00
}
return
}
2023-10-30 15:25:46 +08:00
func SearchAssetsByName ( keyword string , exts [ ] string ) ( ret [ ] * cache . Asset ) {
2022-07-15 09:25:02 +08:00
ret = [ ] * cache . Asset { }
2024-11-25 21:51:40 +08:00
keywords := strings . Split ( keyword , " " )
2022-05-26 15:18:53 +08:00
2024-11-25 21:51:40 +08:00
pathHitCount := map [ string ] int { }
2022-07-15 09:25:02 +08:00
count := 0
2023-10-30 15:25:46 +08:00
filterByExt := 0 < len ( exts )
2022-11-29 10:59:05 +08:00
for _ , asset := range cache . GetAssets ( ) {
2023-10-30 15:25:46 +08:00
if filterByExt {
ext := filepath . Ext ( asset . HName )
includeExt := false
for _ , e := range exts {
if strings . ToLower ( ext ) == strings . ToLower ( e ) {
includeExt = true
break
}
}
if ! includeExt {
continue
}
}
2023-12-22 10:33:56 +08:00
lowerHName := strings . ToLower ( asset . HName )
lowerPath := strings . ToLower ( asset . Path )
2024-11-25 21:51:40 +08:00
var hitNameCount , hitPathCount int
for _ , k := range keywords {
lowerKeyword := strings . ToLower ( k )
hitNameCount += strings . Count ( lowerHName , lowerKeyword )
hitPathCount += strings . Count ( lowerPath , lowerKeyword )
if 1 > hitNameCount && 1 > hitPathCount {
continue
}
}
if 1 > hitNameCount + hitPathCount {
2022-08-04 18:08:15 +08:00
continue
2022-05-26 15:18:53 +08:00
}
2024-11-25 21:51:40 +08:00
pathHitCount [ asset . Path ] += hitNameCount + hitPathCount
2022-07-15 09:25:02 +08:00
2023-12-22 10:33:56 +08:00
hName := asset . HName
2024-11-25 21:51:40 +08:00
if 0 < hitNameCount {
_ , hName = search . MarkText ( asset . HName , strings . Join ( keywords , search . TermSep ) , 64 , Conf . Search . CaseSensitive )
2023-12-22 10:33:56 +08:00
}
2022-07-15 09:25:02 +08:00
ret = append ( ret , & cache . Asset {
HName : hName ,
Path : asset . Path ,
Updated : asset . Updated ,
} )
count ++
if Conf . Search . Limit <= count {
2022-08-04 18:08:15 +08:00
return
2022-07-15 09:25:02 +08:00
}
2022-08-04 18:08:15 +08:00
}
2022-07-15 09:25:02 +08:00
2024-11-25 21:51:40 +08:00
if 0 < len ( pathHitCount ) {
sort . Slice ( ret , func ( i , j int ) bool {
return pathHitCount [ ret [ i ] . Path ] > pathHitCount [ ret [ j ] . Path ]
} )
} else {
sort . Slice ( ret , func ( i , j int ) bool {
return ret [ i ] . Updated > ret [ j ] . Updated
} )
}
2022-05-26 15:18:53 +08:00
return
}
2023-11-30 12:37:33 +08:00
func GetAssetAbsPath ( relativePath string ) ( ret string , err error ) {
2022-05-26 15:18:53 +08:00
relativePath = strings . TrimSpace ( relativePath )
2022-09-16 22:59:24 +08:00
if strings . Contains ( relativePath , "?" ) {
relativePath = relativePath [ : strings . Index ( relativePath , "?" ) ]
}
2024-12-16 23:30:26 +08:00
// 在全局 assets 路径下搜索
p := filepath . Join ( util . DataDir , relativePath )
if gulu . File . IsExist ( p ) {
ret = p
if ! util . IsSubPath ( util . WorkspaceDir , ret ) {
err = fmt . Errorf ( "[%s] is not sub path of workspace" , ret )
return
}
return
}
// 在笔记本下搜索
2022-05-26 15:18:53 +08:00
notebooks , err := ListNotebooks ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
err = errors . New ( Conf . Language ( 0 ) )
return
}
for _ , notebook := range notebooks {
notebookAbsPath := filepath . Join ( util . DataDir , notebook . ID )
2024-11-21 10:59:29 +08:00
filelock . Walk ( notebookAbsPath , func ( path string , d fs . DirEntry , err error ) error {
if isSkipFile ( d . Name ( ) ) {
if d . IsDir ( ) {
2022-05-26 15:18:53 +08:00
return filepath . SkipDir
}
return nil
}
if p := filepath . ToSlash ( path ) ; strings . HasSuffix ( p , relativePath ) {
if gulu . File . IsExist ( path ) {
2023-11-30 12:37:33 +08:00
ret = path
2025-02-05 11:28:01 +08:00
return fs . SkipAll
2022-05-26 15:18:53 +08:00
}
}
return nil
} )
2023-11-30 12:37:33 +08:00
if "" != ret {
if ! util . IsSubPath ( util . WorkspaceDir , ret ) {
err = fmt . Errorf ( "[%s] is not sub path of workspace" , ret )
return
}
2022-05-26 15:18:53 +08:00
return
}
}
return "" , errors . New ( fmt . Sprintf ( Conf . Language ( 12 ) , relativePath ) )
}
2024-01-01 16:48:52 +08:00
func UploadAssets2Cloud ( rootID string ) ( count int , err error ) {
2022-05-26 15:18:53 +08:00
if ! IsSubscriber ( ) {
return
}
2024-03-10 23:27:13 +08:00
tree , err := LoadTreeByBlockID ( rootID )
2024-09-04 04:40:50 +03:00
if err != nil {
2024-01-01 16:54:00 +08:00
return
}
assets := assetsLinkDestsInTree ( tree )
2024-01-01 17:07:15 +08:00
embedAssets := assetsLinkDestsInQueryEmbedNodes ( tree )
assets = append ( assets , embedAssets ... )
assets = gulu . Str . RemoveDuplicatedElem ( assets )
2024-05-14 00:05:09 +08:00
count , err = uploadAssets2Cloud ( assets , bizTypeUploadAssets )
2024-09-04 04:40:50 +03:00
if err != nil {
2024-01-01 16:48:52 +08:00
return
}
2022-05-26 15:18:53 +08:00
return
}
2023-01-04 13:50:18 +08:00
const (
bizTypeUploadAssets = "upload-assets"
bizTypeExport2Liandi = "export-liandi"
)
2023-01-04 11:26:26 +08:00
// uploadAssets2Cloud 将资源文件上传到云端图床。
2024-05-14 00:05:09 +08:00
func uploadAssets2Cloud ( assetPaths [ ] string , bizType string ) ( count int , err error ) {
2022-05-26 15:18:53 +08:00
var uploadAbsAssets [ ] string
2024-01-01 16:54:00 +08:00
for _ , assetPath := range assetPaths {
2023-01-04 14:20:02 +08:00
var absPath string
2024-01-01 16:54:00 +08:00
absPath , err = GetAssetAbsPath ( assetPath )
2024-09-04 04:40:50 +03:00
if err != nil {
2024-01-01 16:54:00 +08:00
logging . LogWarnf ( "get asset [%s] abs path failed: %s" , assetPath , err )
2022-05-26 15:18:53 +08:00
return
}
2023-01-04 14:20:02 +08:00
if "" == absPath {
2024-01-01 16:54:00 +08:00
logging . LogErrorf ( "not found asset [%s]" , assetPath )
2023-01-04 14:20:02 +08:00
continue
2023-01-04 14:07:19 +08:00
}
2023-01-04 14:20:02 +08:00
uploadAbsAssets = append ( uploadAbsAssets , absPath )
2022-05-26 15:18:53 +08:00
}
2022-06-23 19:35:59 +08:00
uploadAbsAssets = gulu . Str . RemoveDuplicatedElem ( uploadAbsAssets )
2023-01-04 14:20:02 +08:00
if 1 > len ( uploadAbsAssets ) {
return
}
2022-05-26 15:18:53 +08:00
2022-07-17 12:22:32 +08:00
logging . LogInfof ( "uploading [%d] assets" , len ( uploadAbsAssets ) )
2022-07-18 22:04:17 +08:00
msgId := util . PushMsg ( fmt . Sprintf ( Conf . Language ( 27 ) , len ( uploadAbsAssets ) ) , 3000 )
2022-05-26 15:18:53 +08:00
if loadErr := LoadUploadToken ( ) ; nil != loadErr {
util . PushMsg ( loadErr . Error ( ) , 5000 )
return
}
2023-01-02 21:49:58 +08:00
limitSize := uint64 ( 3 * 1024 * 1024 ) // 3MB
if IsSubscriber ( ) {
limitSize = 10 * 1024 * 1024 // 10MB
}
2023-01-04 13:50:18 +08:00
// metaType 为服务端 Filemeta.FILEMETA_TYPE, 这里只有两个值:
//
// 5: SiYuan, 表示为 SiYuan 上传图床
// 4: Client, 表示作为客户端分享发布帖子时上传的文件
var metaType = "5"
if bizTypeUploadAssets == bizType {
metaType = "5"
} else if bizTypeExport2Liandi == bizType {
metaType = "4"
}
2024-05-14 08:04:40 +08:00
pushErrMsgCount := 0
2022-05-26 15:18:53 +08:00
var completedUploadAssets [ ] string
for _ , absAsset := range uploadAbsAssets {
2023-01-02 21:49:58 +08:00
fi , statErr := os . Stat ( absAsset )
if nil != statErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "stat file [%s] failed: %s" , absAsset , statErr )
2024-05-14 00:05:09 +08:00
return count , statErr
2023-01-02 21:49:58 +08:00
}
if limitSize < uint64 ( fi . Size ( ) ) {
2024-05-14 00:01:04 +08:00
logging . LogWarnf ( "file [%s] larger than limit size [%s], ignore uploading it" , absAsset , humanize . IBytes ( limitSize ) )
2024-05-14 08:04:40 +08:00
if 3 > pushErrMsgCount {
msg := fmt . Sprintf ( Conf . Language ( 247 ) , filepath . Base ( absAsset ) , humanize . IBytes ( limitSize ) )
util . PushErrMsg ( msg , 30000 )
}
pushErrMsgCount ++
2022-05-26 15:18:53 +08:00
continue
}
2023-05-15 14:56:12 +08:00
msg := fmt . Sprintf ( Conf . Language ( 27 ) , html . EscapeString ( absAsset ) )
2022-07-18 22:04:17 +08:00
util . PushStatusBar ( msg )
util . PushUpdateMsg ( msgId , msg , 3000 )
2022-05-26 15:18:53 +08:00
requestResult := gulu . Ret . NewResult ( )
2022-07-09 11:22:51 +08:00
request := httpclient . NewCloudFileRequest2m ( )
2022-05-26 15:18:53 +08:00
resp , reqErr := request .
2023-02-17 11:00:04 +08:00
SetSuccessResult ( requestResult ) .
2022-05-26 15:18:53 +08:00
SetFile ( "file[]" , absAsset ) .
SetCookies ( & http . Cookie { Name : "symphony" , Value : uploadToken } ) .
2023-01-04 11:26:26 +08:00
SetHeader ( "meta-type" , metaType ) .
2023-01-04 13:50:18 +08:00
SetHeader ( "biz-type" , bizType ) .
2023-06-20 11:48:44 +08:00
Post ( util . GetCloudServer ( ) + "/apis/siyuan/upload?ver=" + util . Ver )
2022-05-26 15:18:53 +08:00
if nil != reqErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "upload assets failed: %s" , reqErr )
2024-05-14 00:05:09 +08:00
return count , ErrFailedToConnectCloudServer
2022-05-26 15:18:53 +08:00
}
if 401 == resp . StatusCode {
err = errors . New ( Conf . Language ( 31 ) )
return
}
if 0 != requestResult . Code {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "upload assets failed: %s" , requestResult . Msg )
2022-05-26 15:18:53 +08:00
err = errors . New ( fmt . Sprintf ( Conf . Language ( 94 ) , requestResult . Msg ) )
return
}
absAsset = filepath . ToSlash ( absAsset )
relAsset := absAsset [ strings . Index ( absAsset , "assets/" ) : ]
completedUploadAssets = append ( completedUploadAssets , relAsset )
2022-07-17 12:22:32 +08:00
logging . LogInfof ( "uploaded asset [%s]" , relAsset )
2024-05-14 00:05:09 +08:00
count ++
2022-05-26 15:18:53 +08:00
}
2022-07-18 22:04:17 +08:00
util . PushClearMsg ( msgId )
2022-05-26 15:18:53 +08:00
2023-01-04 14:20:02 +08:00
if 0 < len ( completedUploadAssets ) {
2022-07-17 12:22:32 +08:00
logging . LogInfof ( "uploaded [%d] assets" , len ( completedUploadAssets ) )
2022-05-26 15:18:53 +08:00
}
return
}
func RemoveUnusedAssets ( ) ( ret [ ] string ) {
2024-09-04 11:02:29 +08:00
ret = [ ] string { }
var size int64
2022-06-05 15:16:46 +08:00
msgId := util . PushMsg ( Conf . Language ( 100 ) , 30 * 1000 )
defer func ( ) {
2024-09-04 11:02:29 +08:00
msg := fmt . Sprintf ( Conf . Language ( 91 ) , len ( ret ) , humanize . BytesCustomCeil ( uint64 ( size ) , 2 ) )
2024-09-04 11:04:20 +08:00
util . PushUpdateMsg ( msgId , msg , 7000 )
2022-06-05 15:16:46 +08:00
} ( )
2022-05-26 15:18:53 +08:00
unusedAssets := UnusedAssets ( )
2022-08-23 11:30:51 +08:00
historyDir , err := GetHistoryDir ( HistoryOpClean )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "get history dir failed: %s" , err )
2022-05-26 15:18:53 +08:00
return
}
2022-10-18 22:36:20 +08:00
var hashes [ ] string
2022-05-26 15:18:53 +08:00
for _ , p := range unusedAssets {
historyPath := filepath . Join ( historyDir , p )
2023-11-06 22:13:04 +08:00
if p = filepath . Join ( util . DataDir , p ) ; filelock . IsExist ( p ) {
2024-06-20 19:45:02 +08:00
if filelock . IsHidden ( p ) {
continue
}
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( p , historyPath ) ; err != nil {
2022-05-26 15:18:53 +08:00
return
}
2022-10-18 22:36:20 +08:00
hash , _ := util . GetEtag ( p )
hashes = append ( hashes , hash )
2022-05-26 15:18:53 +08:00
}
}
2023-01-31 20:24:44 +08:00
sql . BatchRemoveAssetsQueue ( hashes )
2022-10-18 22:36:20 +08:00
2022-05-26 15:18:53 +08:00
for _ , unusedAsset := range unusedAssets {
2024-10-31 00:12:21 +08:00
absPath := filepath . Join ( util . DataDir , unusedAsset )
if filelock . IsExist ( absPath ) {
info , statErr := os . Stat ( absPath )
2024-09-04 11:02:29 +08:00
if statErr == nil {
2024-11-10 11:32:17 +08:00
if info . IsDir ( ) {
dirSize , _ := util . SizeOfDirectory ( absPath )
size += dirSize
} else {
size += info . Size ( )
}
2024-09-04 11:02:29 +08:00
}
2024-10-31 00:12:21 +08:00
if err := filelock . Remove ( absPath ) ; err != nil {
logging . LogErrorf ( "remove unused asset [%s] failed: %s" , absPath , err )
2022-05-26 15:18:53 +08:00
}
2024-10-31 00:12:21 +08:00
util . RemoveAssetText ( unusedAsset )
2022-05-26 15:18:53 +08:00
}
2024-10-31 00:12:21 +08:00
ret = append ( ret , absPath )
2022-05-26 15:18:53 +08:00
}
if 0 < len ( ret ) {
2022-07-14 21:50:46 +08:00
IncSync ( )
2022-05-26 15:18:53 +08:00
}
2022-08-30 00:24:54 +08:00
2023-02-10 14:28:10 +08:00
indexHistoryDir ( filepath . Base ( historyDir ) , util . NewLute ( ) )
2022-11-29 10:59:05 +08:00
cache . LoadAssets ( )
2022-05-26 15:18:53 +08:00
return
}
func RemoveUnusedAsset ( p string ) ( ret string ) {
2022-11-29 10:59:05 +08:00
absPath := filepath . Join ( util . DataDir , p )
2023-11-06 22:13:04 +08:00
if ! filelock . IsExist ( absPath ) {
2022-11-29 10:59:05 +08:00
return absPath
2022-05-26 15:18:53 +08:00
}
2022-08-23 11:30:51 +08:00
historyDir , err := GetHistoryDir ( HistoryOpClean )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "get history dir failed: %s" , err )
2022-05-26 15:18:53 +08:00
return
}
2022-11-29 10:59:05 +08:00
newP := strings . TrimPrefix ( absPath , util . DataDir )
2022-05-26 15:18:53 +08:00
historyPath := filepath . Join ( historyDir , newP )
2023-11-06 22:13:04 +08:00
if filelock . IsExist ( absPath ) {
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( absPath , historyPath ) ; err != nil {
2022-10-18 22:36:20 +08:00
return
}
2022-11-29 10:59:05 +08:00
hash , _ := util . GetEtag ( absPath )
2023-01-31 20:24:44 +08:00
sql . BatchRemoveAssetsQueue ( [ ] string { hash } )
2022-05-26 15:18:53 +08:00
}
2024-09-04 04:40:50 +03:00
if err = filelock . Remove ( absPath ) ; err != nil {
2022-11-29 10:59:05 +08:00
logging . LogErrorf ( "remove unused asset [%s] failed: %s" , absPath , err )
2022-05-26 15:18:53 +08:00
}
2022-11-29 10:59:05 +08:00
ret = absPath
2024-10-31 00:12:21 +08:00
util . RemoveAssetText ( p )
2022-07-14 21:50:46 +08:00
IncSync ( )
2022-08-30 00:24:54 +08:00
2023-02-10 14:28:10 +08:00
indexHistoryDir ( filepath . Base ( historyDir ) , util . NewLute ( ) )
2022-11-29 10:59:05 +08:00
cache . RemoveAsset ( p )
2022-05-26 15:18:53 +08:00
return
}
2024-07-06 09:51:54 +08:00
func RenameAsset ( oldPath , newName string ) ( newPath string , err error ) {
2022-07-15 10:38:01 +08:00
util . PushEndlessProgress ( Conf . Language ( 110 ) )
defer util . PushClearProgress ( )
2022-07-15 11:11:54 +08:00
newName = strings . TrimSpace ( newName )
2024-12-04 22:37:22 +08:00
newName = util . FilterFileName ( newName )
2022-07-15 11:11:54 +08:00
if path . Base ( oldPath ) == newName {
return
}
if "" == newName {
return
}
2022-07-15 10:55:45 +08:00
2022-07-15 11:15:02 +08:00
if ! gulu . File . IsValidFilename ( newName ) {
err = errors . New ( Conf . Language ( 151 ) )
return
}
2023-03-17 17:01:50 +08:00
newName = util . AssetName ( newName + filepath . Ext ( oldPath ) )
2025-03-28 20:07:09 +08:00
parentDir := path . Dir ( oldPath )
newPath = path . Join ( parentDir , newName )
oldAbsPath , getErr := GetAssetAbsPath ( oldPath )
if getErr != nil {
logging . LogErrorf ( "get asset [%s] abs path failed: %s" , oldPath , getErr )
return
}
newAbsPath := filepath . Join ( filepath . Dir ( oldAbsPath ) , newName )
if err = filelock . Copy ( oldAbsPath , newAbsPath ) ; err != nil {
logging . LogErrorf ( "copy asset [%s] failed: %s" , oldAbsPath , err )
2022-07-15 11:18:35 +08:00
return
}
2023-10-10 22:31:46 +08:00
2023-11-06 22:13:04 +08:00
if filelock . IsExist ( filepath . Join ( util . DataDir , oldPath + ".sya" ) ) {
2023-10-10 22:31:46 +08:00
// Rename the .sya annotation file when renaming a PDF asset https://github.com/siyuan-note/siyuan/issues/9390
2024-09-04 04:40:50 +03:00
if err = filelock . Copy ( filepath . Join ( util . DataDir , oldPath + ".sya" ) , filepath . Join ( util . DataDir , newPath + ".sya" ) ) ; err != nil {
2023-10-10 22:31:46 +08:00
logging . LogErrorf ( "copy PDF annotation [%s] failed: %s" , oldPath + ".sya" , err )
return
}
}
2022-07-15 12:02:35 +08:00
oldName := path . Base ( oldPath )
2022-07-15 11:18:35 +08:00
2022-07-15 11:36:41 +08:00
notebooks , err := ListNotebooks ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-07-15 11:36:41 +08:00
return
}
2023-01-20 09:38:11 +08:00
2023-02-10 14:28:10 +08:00
luteEngine := util . NewLute ( )
2022-07-15 10:38:01 +08:00
for _ , notebook := range notebooks {
pages := pagedPaths ( filepath . Join ( util . DataDir , notebook . ID ) , 32 )
2023-01-20 09:38:11 +08:00
2022-07-15 10:38:01 +08:00
for _ , paths := range pages {
for _ , treeAbsPath := range paths {
2022-09-29 21:52:01 +08:00
data , readErr := filelock . ReadFile ( treeAbsPath )
2022-07-15 10:52:02 +08:00
if nil != readErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "get data [path=%s] failed: %s" , treeAbsPath , readErr )
2022-07-15 11:15:02 +08:00
err = readErr
2022-07-15 10:38:01 +08:00
return
}
2022-07-15 12:02:35 +08:00
if ! bytes . Contains ( data , [ ] byte ( oldName ) ) {
2022-07-15 11:40:56 +08:00
continue
2022-07-15 10:38:01 +08:00
}
2022-07-15 12:02:35 +08:00
data = bytes . Replace ( data , [ ] byte ( oldName ) , [ ] byte ( newName ) , - 1 )
2022-09-29 21:52:01 +08:00
if writeErr := filelock . WriteFile ( treeAbsPath , data ) ; nil != writeErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "write data [path=%s] failed: %s" , treeAbsPath , writeErr )
2022-07-15 11:15:02 +08:00
err = writeErr
2022-07-15 10:38:01 +08:00
return
}
2022-07-15 12:02:35 +08:00
p := filepath . ToSlash ( strings . TrimPrefix ( treeAbsPath , filepath . Join ( util . DataDir , notebook . ID ) ) )
2023-02-10 12:05:56 +08:00
tree , parseErr := filesys . LoadTreeByData ( data , notebook . ID , p , luteEngine )
2022-07-15 10:52:02 +08:00
if nil != parseErr {
2023-02-10 12:05:56 +08:00
logging . LogWarnf ( "parse json to tree [%s] failed: %s" , treeAbsPath , parseErr )
continue
2022-07-15 10:38:01 +08:00
}
2024-06-20 22:53:27 +08:00
treenode . UpsertBlockTree ( tree )
2022-07-15 10:38:01 +08:00
sql . UpsertTreeQueue ( tree )
2023-08-07 11:52:15 +08:00
util . PushEndlessProgress ( fmt . Sprintf ( Conf . Language ( 111 ) , util . EscapeHTML ( tree . Root . IALAttr ( "title" ) ) ) )
2022-07-15 10:38:01 +08:00
}
}
}
2024-10-30 23:41:27 +08:00
storageAvDir := filepath . Join ( util . DataDir , "storage" , "av" )
if gulu . File . IsDir ( storageAvDir ) {
entries , readErr := os . ReadDir ( storageAvDir )
if nil != readErr {
logging . LogErrorf ( "read dir [%s] failed: %s" , storageAvDir , readErr )
err = readErr
return
}
for _ , entry := range entries {
if ! strings . HasSuffix ( entry . Name ( ) , ".json" ) || ! ast . IsNodeIDPattern ( strings . TrimSuffix ( entry . Name ( ) , ".json" ) ) {
continue
}
data , readDataErr := filelock . ReadFile ( filepath . Join ( util . DataDir , "storage" , "av" , entry . Name ( ) ) )
if nil != readDataErr {
logging . LogErrorf ( "read file [%s] failed: %s" , entry . Name ( ) , readDataErr )
err = readDataErr
return
}
if bytes . Contains ( data , [ ] byte ( oldPath ) ) {
data = bytes . ReplaceAll ( data , [ ] byte ( oldPath ) , [ ] byte ( newPath ) )
if writeDataErr := filelock . WriteFile ( filepath . Join ( util . DataDir , "storage" , "av" , entry . Name ( ) ) , data ) ; nil != writeDataErr {
logging . LogErrorf ( "write file [%s] failed: %s" , entry . Name ( ) , writeDataErr )
err = writeDataErr
return
}
}
util . PushEndlessProgress ( fmt . Sprintf ( Conf . Language ( 111 ) , util . EscapeHTML ( entry . Name ( ) ) ) )
}
}
2024-10-30 23:17:29 +08:00
if ocrText := util . GetAssetText ( oldPath ) ; "" != ocrText {
// 图片重命名后 ocr-texts.json 需要更新 https://github.com/siyuan-note/siyuan/issues/12974
util . SetAssetText ( newPath , ocrText )
}
2022-07-15 10:38:01 +08:00
IncSync ( )
2022-07-15 11:15:02 +08:00
return
2022-07-15 10:38:01 +08:00
}
2022-05-26 15:18:53 +08:00
func UnusedAssets ( ) ( ret [ ] string ) {
2022-08-10 13:30:58 +08:00
defer logging . Recover ( )
2022-05-26 15:18:53 +08:00
ret = [ ] string { }
assetsPathMap , err := allAssetAbsPaths ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
return
}
linkDestMap := map [ string ] bool { }
notebooks , err := ListNotebooks ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
return
}
2023-02-10 14:28:10 +08:00
luteEngine := util . NewLute ( )
2022-05-26 15:18:53 +08:00
for _ , notebook := range notebooks {
dests := map [ string ] bool { }
2022-06-16 15:10:14 +08:00
// 分页加载,优化清理未引用资源内存占用 https://github.com/siyuan-note/siyuan/issues/5200
2022-06-16 15:10:55 +08:00
pages := pagedPaths ( filepath . Join ( util . DataDir , notebook . ID ) , 32 )
2022-06-16 15:08:11 +08:00
for _ , paths := range pages {
var trees [ ] * parse . Tree
for _ , localPath := range paths {
tree , loadTreeErr := loadTree ( localPath , luteEngine )
if nil != loadTreeErr {
continue
}
trees = append ( trees , tree )
2022-05-26 15:18:53 +08:00
}
2022-06-16 15:08:11 +08:00
for _ , tree := range trees {
for _ , d := range assetsLinkDestsInTree ( tree ) {
dests [ d ] = true
}
2022-05-26 15:18:53 +08:00
2022-06-16 15:08:11 +08:00
if titleImgPath := treenode . GetDocTitleImgPath ( tree . Root ) ; "" != titleImgPath {
// 题头图计入
2022-11-11 11:51:14 +08:00
if ! util . IsAssetLinkDest ( [ ] byte ( titleImgPath ) ) {
2022-06-16 15:08:11 +08:00
continue
}
dests [ titleImgPath ] = true
2022-05-26 15:18:53 +08:00
}
}
}
var linkDestFolderPaths , linkDestFilePaths [ ] string
2023-11-29 16:28:35 +08:00
for dest := range dests {
2022-05-26 15:18:53 +08:00
if ! strings . HasPrefix ( dest , "assets/" ) {
continue
}
2022-08-16 10:24:38 +08:00
if idx := strings . Index ( dest , "?" ) ; 0 < idx {
// `pdf?page` 资源文件链接会被判定为未引用资源 https://github.com/siyuan-note/siyuan/issues/5649
dest = dest [ : idx ]
}
2022-05-26 15:18:53 +08:00
if "" == assetsPathMap [ dest ] {
continue
}
if strings . HasSuffix ( dest , "/" ) {
linkDestFolderPaths = append ( linkDestFolderPaths , dest )
} else {
linkDestFilePaths = append ( linkDestFilePaths , dest )
}
}
// 排除文件夹链接
var toRemoves [ ] string
2023-11-29 16:28:35 +08:00
for asset := range assetsPathMap {
2022-05-26 15:18:53 +08:00
for _ , linkDestFolder := range linkDestFolderPaths {
if strings . HasPrefix ( asset , linkDestFolder ) {
toRemoves = append ( toRemoves , asset )
}
}
for _ , linkDestPath := range linkDestFilePaths {
if strings . HasPrefix ( linkDestPath , asset ) {
toRemoves = append ( toRemoves , asset )
}
}
}
for _ , toRemove := range toRemoves {
delete ( assetsPathMap , toRemove )
}
for _ , dest := range linkDestFilePaths {
linkDestMap [ dest ] = true
2023-04-12 12:02:56 +08:00
if strings . HasSuffix ( dest , ".pdf" ) {
linkDestMap [ dest + ".sya" ] = true
}
2022-05-26 15:18:53 +08:00
}
}
var toRemoves [ ] string
2023-11-29 16:28:35 +08:00
for asset := range assetsPathMap {
2023-01-16 16:16:34 +08:00
if strings . HasSuffix ( asset , "ocr-texts.json" ) {
// 排除 OCR 结果文本
toRemoves = append ( toRemoves , asset )
continue
2022-05-26 15:18:53 +08:00
}
2025-01-17 17:17:53 +08:00
if strings . HasSuffix ( asset , "android-notification-texts.txt" ) {
// 排除 Android 通知文本
toRemoves = append ( toRemoves , asset )
continue
}
2022-05-26 15:18:53 +08:00
}
2023-09-26 11:16:40 +08:00
// 排除数据库中引用的资源文件
storageAvDir := filepath . Join ( util . DataDir , "storage" , "av" )
if gulu . File . IsDir ( storageAvDir ) {
entries , readErr := os . ReadDir ( storageAvDir )
if nil != readErr {
logging . LogErrorf ( "read dir [%s] failed: %s" , storageAvDir , readErr )
err = readErr
return
}
for _ , entry := range entries {
if ! strings . HasSuffix ( entry . Name ( ) , ".json" ) || ! ast . IsNodeIDPattern ( strings . TrimSuffix ( entry . Name ( ) , ".json" ) ) {
continue
}
data , readDataErr := filelock . ReadFile ( filepath . Join ( util . DataDir , "storage" , "av" , entry . Name ( ) ) )
if nil != readDataErr {
logging . LogErrorf ( "read file [%s] failed: %s" , entry . Name ( ) , readDataErr )
err = readDataErr
return
}
2023-11-29 16:28:35 +08:00
for asset := range assetsPathMap {
2023-09-26 11:16:40 +08:00
if bytes . Contains ( data , [ ] byte ( asset ) ) {
toRemoves = append ( toRemoves , asset )
}
}
}
}
2022-05-26 15:18:53 +08:00
for _ , toRemove := range toRemoves {
delete ( assetsPathMap , toRemove )
}
2022-10-16 14:15:42 +08:00
dataAssetsAbsPath := util . GetDataAssetsAbsPath ( )
2023-04-12 12:02:56 +08:00
for dest , assetAbsPath := range assetsPathMap {
if _ , ok := linkDestMap [ dest ] ; ok {
2022-05-26 15:18:53 +08:00
continue
}
var p string
2022-07-23 17:41:47 +08:00
if strings . HasPrefix ( dataAssetsAbsPath , assetAbsPath ) {
2022-05-26 15:18:53 +08:00
p = assetAbsPath [ strings . Index ( assetAbsPath , "assets" ) : ]
} else {
2022-07-23 17:41:47 +08:00
p = strings . TrimPrefix ( assetAbsPath , filepath . Dir ( dataAssetsAbsPath ) )
2022-05-26 15:18:53 +08:00
}
p = filepath . ToSlash ( p )
2022-07-23 17:41:47 +08:00
if strings . HasPrefix ( p , "/" ) {
p = p [ 1 : ]
}
2022-05-26 15:18:53 +08:00
ret = append ( ret , p )
}
sort . Strings ( ret )
return
}
2023-06-04 10:30:19 +08:00
func MissingAssets ( ) ( ret [ ] string ) {
defer logging . Recover ( )
ret = [ ] string { }
assetsPathMap , err := allAssetAbsPaths ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-06-04 10:30:19 +08:00
return
}
notebooks , err := ListNotebooks ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2023-06-04 10:30:19 +08:00
return
}
luteEngine := util . NewLute ( )
for _ , notebook := range notebooks {
2023-06-04 11:46:35 +08:00
if notebook . Closed {
continue
}
2023-06-04 10:30:19 +08:00
2023-06-04 11:46:35 +08:00
dests := map [ string ] bool { }
2023-06-04 10:30:19 +08:00
pages := pagedPaths ( filepath . Join ( util . DataDir , notebook . ID ) , 32 )
for _ , paths := range pages {
var trees [ ] * parse . Tree
for _ , localPath := range paths {
tree , loadTreeErr := loadTree ( localPath , luteEngine )
if nil != loadTreeErr {
continue
}
trees = append ( trees , tree )
}
for _ , tree := range trees {
for _ , d := range assetsLinkDestsInTree ( tree ) {
dests [ d ] = true
}
if titleImgPath := treenode . GetDocTitleImgPath ( tree . Root ) ; "" != titleImgPath {
// 题头图计入
if ! util . IsAssetLinkDest ( [ ] byte ( titleImgPath ) ) {
continue
}
dests [ titleImgPath ] = true
}
}
}
2023-11-29 16:28:35 +08:00
for dest := range dests {
2023-06-04 10:30:19 +08:00
if ! strings . HasPrefix ( dest , "assets/" ) {
continue
}
if idx := strings . Index ( dest , "?" ) ; 0 < idx {
dest = dest [ : idx ]
}
if strings . HasSuffix ( dest , "/" ) {
continue
}
2025-01-30 14:14:33 +08:00
if strings . Contains ( strings . ToLower ( dest ) , ".pdf/" ) {
if idx := strings . LastIndex ( dest , "/" ) ; - 1 < idx {
if ast . IsNodeIDPattern ( dest [ idx + 1 : ] ) {
// PDF 标注不计入 https://github.com/siyuan-note/siyuan/issues/13891
continue
}
}
}
2023-06-04 10:30:19 +08:00
if "" == assetsPathMap [ dest ] {
2023-07-26 11:01:53 +08:00
if strings . HasPrefix ( dest , "assets/." ) {
// Assets starting with `.` should not be considered missing assets https://github.com/siyuan-note/siyuan/issues/8821
2023-11-06 22:13:04 +08:00
if ! filelock . IsExist ( filepath . Join ( util . DataDir , dest ) ) {
2023-07-26 11:01:53 +08:00
ret = append ( ret , dest )
}
2023-07-26 11:05:22 +08:00
} else {
ret = append ( ret , dest )
2023-07-26 11:01:53 +08:00
}
2023-06-04 10:30:19 +08:00
continue
}
}
}
sort . Strings ( ret )
return
}
2022-07-29 23:37:04 +08:00
func emojisInTree ( tree * parse . Tree ) ( ret [ ] string ) {
2024-12-19 23:51:12 +08:00
if icon := tree . Root . IALAttr ( "icon" ) ; "" != icon {
2024-12-21 17:28:50 +08:00
if ! strings . Contains ( icon , "://" ) && ! strings . HasPrefix ( icon , "api/icon/" ) && ! util . NativeEmojiChars [ icon ] {
2024-12-19 23:51:12 +08:00
ret = append ( ret , "/emojis/" + icon )
}
}
2022-07-29 23:37:04 +08:00
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
if ast . NodeEmojiImg == n . Type {
tokens := n . Tokens
idx := bytes . Index ( tokens , [ ] byte ( "src=\"" ) )
if - 1 == idx {
return ast . WalkContinue
}
src := tokens [ idx + len ( "src=\"" ) : ]
src = src [ : bytes . Index ( src , [ ] byte ( "\"" ) ) ]
ret = append ( ret , string ( src ) )
}
return ast . WalkContinue
} )
ret = gulu . Str . RemoveDuplicatedElem ( ret )
return
}
2024-01-01 17:07:15 +08:00
func assetsLinkDestsInQueryEmbedNodes ( tree * parse . Tree ) ( ret [ ] string ) {
2024-01-01 17:09:16 +08:00
// The images in the embed blocks are not uploaded to the community hosting https://github.com/siyuan-note/siyuan/issues/10042
2022-05-26 15:18:53 +08:00
ret = [ ] string { }
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
2024-01-01 17:07:15 +08:00
if ! entering || ast . NodeBlockQueryEmbedScript != n . Type {
return ast . WalkContinue
}
2024-01-01 17:08:04 +08:00
stmt := n . TokensStr ( )
2024-01-01 17:07:15 +08:00
stmt = html . UnescapeString ( stmt )
stmt = strings . ReplaceAll ( stmt , editor . IALValEscNewLine , "\n" )
sqlBlocks := sql . SelectBlocksRawStmt ( stmt , 1 , Conf . Search . Limit )
for _ , sqlBlock := range sqlBlocks {
2024-03-10 23:27:13 +08:00
subtree , _ := LoadTreeByBlockID ( sqlBlock . ID )
2024-01-01 17:07:15 +08:00
if nil == subtree {
continue
}
embedNode := treenode . GetNodeInTree ( subtree , sqlBlock . ID )
if nil == embedNode {
continue
}
ret = append ( ret , assetsLinkDestsInNode ( embedNode ) ... )
}
return ast . WalkContinue
} )
ret = gulu . Str . RemoveDuplicatedElem ( ret )
return
}
func assetsLinkDestsInTree ( tree * parse . Tree ) ( ret [ ] string ) {
ret = assetsLinkDestsInNode ( tree . Root )
return
}
func assetsLinkDestsInNode ( node * ast . Node ) ( ret [ ] string ) {
ret = [ ] string { }
ast . Walk ( node , func ( n * ast . Node , entering bool ) ast . WalkStatus {
2024-09-24 23:20:22 +08:00
if n . IsBlock ( ) {
// 以 custom-data-assets 开头的块属性值可能是多个资源文件链接,需要计入
// Ignore assets associated with the `custom-data-assets` block attribute when cleaning unreferenced assets https://github.com/siyuan-note/siyuan/issues/12574
for _ , kv := range n . KramdownIAL {
k := kv [ 0 ]
if strings . HasPrefix ( k , "custom-data-assets" ) {
dest := kv [ 1 ]
2024-11-04 11:40:03 +08:00
if "" == dest || ! util . IsAssetLinkDest ( [ ] byte ( dest ) ) {
2024-09-24 23:20:22 +08:00
continue
}
ret = append ( ret , dest )
}
}
}
2022-05-26 15:18:53 +08:00
// 修改以下代码时需要同时修改 database 构造行级元素实现,增加必要的类型
if ! entering || ( ast . NodeLinkDest != n . Type && ast . NodeHTMLBlock != n . Type && ast . NodeInlineHTML != n . Type &&
2022-09-19 09:22:47 +08:00
ast . NodeIFrame != n . Type && ast . NodeWidget != n . Type && ast . NodeAudio != n . Type && ast . NodeVideo != n . Type &&
2024-10-25 22:39:24 +08:00
ast . NodeAttributeView != n . Type && ! n . IsTextMarkType ( "a" ) && ! n . IsTextMarkType ( "file-annotation-ref" ) ) {
2022-05-26 15:18:53 +08:00
return ast . WalkContinue
}
if ast . NodeLinkDest == n . Type {
2024-11-04 11:40:03 +08:00
if ! util . IsAssetLinkDest ( n . Tokens ) {
2022-05-26 15:18:53 +08:00
return ast . WalkContinue
}
dest := strings . TrimSpace ( string ( n . Tokens ) )
ret = append ( ret , dest )
2022-09-19 09:22:47 +08:00
} else if n . IsTextMarkType ( "a" ) {
2024-11-04 11:40:03 +08:00
if ! util . IsAssetLinkDest ( gulu . Str . ToBytes ( n . TextMarkAHref ) ) {
2022-09-19 09:22:47 +08:00
return ast . WalkContinue
}
dest := strings . TrimSpace ( n . TextMarkAHref )
ret = append ( ret , dest )
2023-04-12 12:02:56 +08:00
} else if n . IsTextMarkType ( "file-annotation-ref" ) {
2024-11-04 11:40:03 +08:00
if ! util . IsAssetLinkDest ( gulu . Str . ToBytes ( n . TextMarkFileAnnotationRefID ) ) {
2023-04-12 12:02:56 +08:00
return ast . WalkContinue
}
2023-05-06 15:23:48 +08:00
if ! strings . Contains ( n . TextMarkFileAnnotationRefID , "/" ) {
2023-05-06 15:23:56 +08:00
return ast . WalkContinue
2023-05-06 15:23:48 +08:00
}
2023-04-12 12:02:56 +08:00
dest := n . TextMarkFileAnnotationRefID [ : strings . LastIndexByte ( n . TextMarkFileAnnotationRefID , '/' ) ]
dest = strings . TrimSpace ( dest )
ret = append ( ret , dest )
2024-10-25 22:39:24 +08:00
} else if ast . NodeAttributeView == n . Type {
attrView , _ := av . ParseAttributeView ( n . AttributeViewID )
if nil == attrView {
return ast . WalkContinue
}
for _ , keyValues := range attrView . KeyValues {
if av . KeyTypeMAsset == keyValues . Key . Type {
for _ , value := range keyValues . Values {
if 1 > len ( value . MAsset ) {
continue
}
for _ , asset := range value . MAsset {
dest := asset . Content
2024-11-04 11:40:03 +08:00
if ! util . IsAssetLinkDest ( [ ] byte ( dest ) ) {
2024-10-25 22:39:24 +08:00
continue
}
ret = append ( ret , strings . TrimSpace ( dest ) )
}
}
} else if av . KeyTypeURL == keyValues . Key . Type {
for _ , value := range keyValues . Values {
if nil != value . URL {
dest := value . URL . Content
2024-11-04 11:40:03 +08:00
if ! util . IsAssetLinkDest ( [ ] byte ( dest ) ) {
2024-10-25 22:39:24 +08:00
continue
}
ret = append ( ret , strings . TrimSpace ( dest ) )
}
}
}
}
2022-05-26 15:18:53 +08:00
} else {
if ast . NodeWidget == n . Type {
2022-06-14 15:06:41 +08:00
dataAssets := n . IALAttr ( "custom-data-assets" )
if "" == dataAssets {
// 兼容两种属性名 custom-data-assets 和 data-assets https://github.com/siyuan-note/siyuan/issues/4122#issuecomment-1154796568
dataAssets = n . IALAttr ( "data-assets" )
}
2025-01-05 09:34:57 +08:00
if ! util . IsAssetLinkDest ( [ ] byte ( dataAssets ) ) {
2022-05-26 15:18:53 +08:00
return ast . WalkContinue
}
ret = append ( ret , dataAssets )
} else { // HTMLBlock/InlineHTML/IFrame/Audio/Video
2024-02-28 21:08:46 +08:00
dest := treenode . GetNodeSrcTokens ( n )
2025-01-05 09:34:57 +08:00
if ! util . IsAssetLinkDest ( [ ] byte ( dest ) ) {
return ast . WalkContinue
2022-05-26 15:18:53 +08:00
}
2025-01-05 09:34:57 +08:00
ret = append ( ret , dest )
2022-05-26 15:18:53 +08:00
}
}
return ast . WalkContinue
} )
2023-04-12 12:02:56 +08:00
ret = gulu . Str . RemoveDuplicatedElem ( ret )
2023-11-04 10:30:05 +08:00
for i , dest := range ret {
// 对于 macOS 的 rtfd 文件夹格式需要特殊处理,为其加上结尾 /
if strings . HasSuffix ( dest , ".rtfd" ) {
ret [ i ] = dest + "/"
}
}
2022-05-26 15:18:53 +08:00
return
}
2024-07-26 18:41:46 +08:00
func setAssetsLinkDest ( node * ast . Node , oldDest , dest string ) {
if ast . NodeLinkDest == node . Type {
2024-09-23 23:44:32 +08:00
if bytes . HasPrefix ( node . Tokens , [ ] byte ( "//" ) ) {
node . Tokens = append ( [ ] byte ( "https:" ) , node . Tokens ... )
}
2024-07-26 18:41:46 +08:00
node . Tokens = bytes . ReplaceAll ( node . Tokens , [ ] byte ( oldDest ) , [ ] byte ( dest ) )
} else if node . IsTextMarkType ( "a" ) {
2024-09-23 23:44:32 +08:00
if strings . HasPrefix ( node . TextMarkAHref , "//" ) {
node . TextMarkAHref = "https:" + node . TextMarkAHref
}
2024-07-26 18:41:46 +08:00
node . TextMarkAHref = strings . ReplaceAll ( node . TextMarkAHref , oldDest , dest )
} else if ast . NodeAudio == node . Type || ast . NodeVideo == node . Type {
2024-09-23 23:44:32 +08:00
if strings . HasPrefix ( node . TextMarkAHref , "//" ) {
node . TextMarkAHref = "https:" + node . TextMarkAHref
}
2024-07-26 18:41:46 +08:00
node . Tokens = bytes . ReplaceAll ( node . Tokens , [ ] byte ( oldDest ) , [ ] byte ( dest ) )
2024-07-26 19:04:01 +08:00
} else if ast . NodeAttributeView == node . Type {
needWrite := false
attrView , _ := av . ParseAttributeView ( node . AttributeViewID )
if nil == attrView {
return
}
for _ , keyValues := range attrView . KeyValues {
if av . KeyTypeMAsset != keyValues . Key . Type {
continue
}
for _ , value := range keyValues . Values {
if 1 > len ( value . MAsset ) {
continue
}
for _ , asset := range value . MAsset {
if oldDest == asset . Content && oldDest != dest {
asset . Content = dest
needWrite = true
}
}
}
}
if needWrite {
av . SaveAttributeView ( attrView )
}
2024-07-26 18:41:46 +08:00
}
}
2024-07-26 18:55:01 +08:00
func getRemoteAssetsLinkDests ( node * ast . Node , onlyImg bool ) ( ret [ ] string ) {
2024-07-26 18:41:46 +08:00
if onlyImg {
2024-07-26 18:55:01 +08:00
if ast . NodeLinkDest == node . Type {
if node . ParentIs ( ast . NodeImage ) {
if ! util . IsAssetLinkDest ( node . Tokens ) {
ret = append ( ret , string ( node . Tokens ) )
}
}
} else if ast . NodeAttributeView == node . Type {
attrView , _ := av . ParseAttributeView ( node . AttributeViewID )
if nil == attrView {
return
}
for _ , keyValues := range attrView . KeyValues {
if av . KeyTypeMAsset != keyValues . Key . Type {
continue
}
for _ , value := range keyValues . Values {
if 1 > len ( value . MAsset ) {
continue
}
for _ , asset := range value . MAsset {
if av . AssetTypeImage != asset . Type {
continue
}
dest := asset . Content
if ! util . IsAssetLinkDest ( [ ] byte ( dest ) ) {
ret = append ( ret , strings . TrimSpace ( dest ) )
}
}
}
}
2024-07-26 18:41:46 +08:00
}
} else {
if ast . NodeLinkDest == node . Type {
2024-07-26 18:55:01 +08:00
if ! util . IsAssetLinkDest ( node . Tokens ) {
ret = append ( ret , string ( node . Tokens ) )
}
2024-07-26 18:41:46 +08:00
} else if node . IsTextMarkType ( "a" ) {
2024-07-26 18:55:01 +08:00
if ! util . IsAssetLinkDest ( [ ] byte ( node . TextMarkAHref ) ) {
ret = append ( ret , node . TextMarkAHref )
}
2024-07-26 18:41:46 +08:00
} else if ast . NodeAudio == node . Type || ast . NodeVideo == node . Type {
2024-07-26 18:55:01 +08:00
src := treenode . GetNodeSrcTokens ( node )
if ! util . IsAssetLinkDest ( [ ] byte ( src ) ) {
ret = append ( ret , src )
}
} else if ast . NodeAttributeView == node . Type {
attrView , _ := av . ParseAttributeView ( node . AttributeViewID )
if nil == attrView {
return
}
for _ , keyValues := range attrView . KeyValues {
if av . KeyTypeMAsset != keyValues . Key . Type {
continue
}
2024-07-26 18:41:46 +08:00
2024-07-26 18:55:01 +08:00
for _ , value := range keyValues . Values {
if 1 > len ( value . MAsset ) {
continue
}
for _ , asset := range value . MAsset {
dest := asset . Content
if ! util . IsAssetLinkDest ( [ ] byte ( dest ) ) {
ret = append ( ret , strings . TrimSpace ( dest ) )
}
}
}
}
}
2024-07-26 18:41:46 +08:00
}
return
}
func getRemoteAssetsLinkDestsInTree ( tree * parse . Tree , onlyImg bool ) ( nodes [ ] * ast . Node ) {
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
if ! entering {
return ast . WalkContinue
}
2024-07-26 18:55:01 +08:00
dests := getRemoteAssetsLinkDests ( n , onlyImg )
if 1 > len ( dests ) {
2024-07-26 18:41:46 +08:00
return ast . WalkContinue
}
nodes = append ( nodes , n )
return ast . WalkContinue
} )
return
}
2022-05-26 15:18:53 +08:00
// allAssetAbsPaths 返回 asset 相对路径( assets/xxx) 到绝对路径( F:\SiYuan\data\assets\xxx) 的映射。
func allAssetAbsPaths ( ) ( assetsAbsPathMap map [ string ] string , err error ) {
notebooks , err := ListNotebooks ( )
2024-09-04 04:40:50 +03:00
if err != nil {
2022-05-26 15:18:53 +08:00
return
}
assetsAbsPathMap = map [ string ] string { }
// 笔记本 assets
for _ , notebook := range notebooks {
notebookAbsPath := filepath . Join ( util . DataDir , notebook . ID )
2024-11-21 10:59:29 +08:00
filelock . Walk ( notebookAbsPath , func ( path string , d fs . DirEntry , err error ) error {
2022-05-26 15:18:53 +08:00
if notebookAbsPath == path {
return nil
}
2024-11-21 10:59:29 +08:00
if isSkipFile ( d . Name ( ) ) {
if d . IsDir ( ) {
2022-05-26 15:18:53 +08:00
return filepath . SkipDir
}
return nil
}
2024-08-04 12:18:20 +08:00
if filelock . IsHidden ( path ) {
// 清理资源文件时忽略隐藏文件 Ignore hidden files when cleaning unused assets https://github.com/siyuan-note/siyuan/issues/12172
return nil
}
2024-11-21 10:59:29 +08:00
if d . IsDir ( ) && "assets" == d . Name ( ) {
filelock . Walk ( path , func ( assetPath string , d fs . DirEntry , err error ) error {
2022-05-26 15:18:53 +08:00
if path == assetPath {
return nil
}
2024-11-21 10:59:29 +08:00
if isSkipFile ( d . Name ( ) ) {
if d . IsDir ( ) {
2022-05-26 15:18:53 +08:00
return filepath . SkipDir
}
return nil
}
relPath := filepath . ToSlash ( assetPath )
relPath = relPath [ strings . Index ( relPath , "assets/" ) : ]
2024-11-21 10:59:29 +08:00
if d . IsDir ( ) {
2022-05-26 15:18:53 +08:00
relPath += "/"
}
assetsAbsPathMap [ relPath ] = assetPath
return nil
} )
return filepath . SkipDir
}
return nil
} )
}
2022-07-23 17:41:47 +08:00
2022-05-26 15:18:53 +08:00
// 全局 assets
2022-10-16 14:15:42 +08:00
dataAssetsAbsPath := util . GetDataAssetsAbsPath ( )
2024-11-21 10:59:29 +08:00
filelock . Walk ( dataAssetsAbsPath , func ( assetPath string , d fs . DirEntry , err error ) error {
2022-07-23 17:41:47 +08:00
if dataAssetsAbsPath == assetPath {
2022-05-26 15:18:53 +08:00
return nil
}
2024-11-21 10:59:29 +08:00
if isSkipFile ( d . Name ( ) ) {
if d . IsDir ( ) {
2022-05-26 15:18:53 +08:00
return filepath . SkipDir
}
return nil
}
2024-08-04 12:18:20 +08:00
if filelock . IsHidden ( assetPath ) {
// 清理资源文件时忽略隐藏文件 Ignore hidden files when cleaning unused assets https://github.com/siyuan-note/siyuan/issues/12172
return nil
}
2022-05-26 15:18:53 +08:00
relPath := filepath . ToSlash ( assetPath )
relPath = relPath [ strings . Index ( relPath , "assets/" ) : ]
2024-11-21 10:59:29 +08:00
if d . IsDir ( ) {
2022-05-26 15:18:53 +08:00
relPath += "/"
}
assetsAbsPathMap [ relPath ] = assetPath
return nil
} )
return
}
// copyBoxAssetsToDataAssets 将笔记本路径下所有(包括子文档)的 assets 复制一份到 data/assets 中。
func copyBoxAssetsToDataAssets ( boxID string ) {
boxLocalPath := filepath . Join ( util . DataDir , boxID )
copyAssetsToDataAssets ( boxLocalPath )
}
// copyDocAssetsToDataAssets 将文档路径下所有(包括子文档)的 assets 复制一份到 data/assets 中。
func copyDocAssetsToDataAssets ( boxID , parentDocPath string ) {
boxLocalPath := filepath . Join ( util . DataDir , boxID )
parentDocDirAbsPath := filepath . Dir ( filepath . Join ( boxLocalPath , parentDocPath ) )
copyAssetsToDataAssets ( parentDocDirAbsPath )
}
func copyAssetsToDataAssets ( rootPath string ) {
var assetsDirPaths [ ] string
2024-11-21 10:59:29 +08:00
filelock . Walk ( rootPath , func ( path string , d fs . DirEntry , err error ) error {
if nil != err || rootPath == path || nil == d {
2022-05-26 15:18:53 +08:00
return nil
}
2024-11-21 10:59:29 +08:00
isDir , name := d . IsDir ( ) , d . Name ( )
2022-05-26 15:18:53 +08:00
if isSkipFile ( name ) {
if isDir {
return filepath . SkipDir
}
return nil
}
if "assets" == name && isDir {
assetsDirPaths = append ( assetsDirPaths , path )
}
return nil
} )
dataAssetsPath := filepath . Join ( util . DataDir , "assets" )
for _ , assetsDirPath := range assetsDirPaths {
2024-09-04 04:40:50 +03:00
if err := filelock . Copy ( assetsDirPath , dataAssetsPath ) ; err != nil {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "copy tree assets from [%s] to [%s] failed: %s" , assetsDirPaths , dataAssetsPath , err )
2022-05-26 15:18:53 +08:00
}
}
}