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"
"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
"github.com/88250/gulu"
"github.com/88250/lute/ast"
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"
2023-01-02 21:49:58 +08:00
"github.com/dustin/go-humanize"
2022-05-30 18:02:12 +08:00
"github.com/gabriel-vasile/mimetype"
2023-08-31 21:15:31 +08:00
"github.com/imroc/req/v3"
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"
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 ) {
tree , err := loadTreeByBlockID ( rootID )
if nil != err {
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
}
2023-02-24 10:44:14 +08:00
func NetImg2LocalAssets ( rootID , originalURL string ) ( err error ) {
2022-05-26 15:18:53 +08:00
tree , err := loadTreeByBlockID ( rootID )
if nil != err {
return
}
var files int
2022-06-09 19:17:15 +08:00
msgId := gulu . Rand . String ( 7 )
2022-10-31 00:04:48 +08:00
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 ) {
if err = os . MkdirAll ( assetsDirPath , 0755 ) ; nil != err {
return
}
}
2023-08-31 21:15:31 +08:00
browserClient := req . C ( ) .
SetUserAgent ( util . UserAgent ) .
SetTimeout ( 30 * time . Second ) .
EnableInsecureSkipVerify ( ) . // HTTPS certificate is no longer verified when `Convert network images to local images` https://github.com/siyuan-note/siyuan/issues/9080
SetProxy ( httpclient . ProxyFromEnvironment )
2022-05-26 15:18:53 +08:00
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-11-11 11:51:14 +08:00
if util . IsAssetLinkDest ( dest ) {
return ast . WalkSkipChildren
}
if bytes . HasPrefix ( bytes . ToLower ( dest ) , [ ] byte ( "file://" ) ) {
// `网络图片转换为本地图片` 支持处理 `file://` 本地路径图片 https://github.com/siyuan-note/siyuan/issues/6546
u := string ( dest ) [ 7 : ]
2023-06-17 15:26:55 +08:00
if ! gulu . File . IsExist ( u ) || gulu . File . IsDir ( u ) {
2022-11-11 11:51:14 +08:00
return ast . WalkSkipChildren
}
name := filepath . Base ( u )
2023-04-14 11:38:35 +08:00
name = util . FilterFileName ( name )
name = util . TruncateLenFileName ( name )
2022-11-11 11:51:14 +08:00
name = "net-img-" + name
name = util . AssetName ( name )
writePath := filepath . Join ( assetsDirPath , name )
2023-03-31 10:34:30 +08:00
if err = filelock . Copy ( u , writePath ) ; nil != err {
2022-11-11 11:51:14 +08:00
logging . LogErrorf ( "copy [%s] to [%s] failed: %s" , u , writePath , err )
return ast . WalkSkipChildren
}
linkDest . Tokens = [ ] byte ( "assets/" + name )
files ++
return ast . WalkSkipChildren
}
if bytes . HasPrefix ( bytes . ToLower ( dest ) , [ ] byte ( "https://" ) ) || bytes . HasPrefix ( bytes . ToLower ( dest ) , [ ] byte ( "http://" ) ) {
2022-05-26 15:18:53 +08:00
u := string ( dest )
2022-05-30 17:53:56 +08:00
if strings . Contains ( u , "qpic.cn" ) {
2022-10-31 21:26:11 +08:00
// 改进 `网络图片转换为本地图片` 微信图片拉取 https://github.com/siyuan-note/siyuan/issues/5052
2022-05-30 17:53:56 +08:00
if strings . Contains ( u , "http://" ) {
u = strings . Replace ( u , "http://" , "https://" , 1 )
}
2022-10-31 21:26:11 +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)
//}
2022-05-30 17:53:56 +08:00
}
2022-06-09 19:17:15 +08:00
util . PushUpdateMsg ( msgId , fmt . Sprintf ( Conf . Language ( 119 ) , u ) , 15000 )
2023-08-31 21:15:31 +08:00
request := browserClient . R ( )
request . SetRetryCount ( 1 ) . SetRetryFixedInterval ( 3 * time . Second )
2023-04-19 10:10:55 +08:00
if "" != originalURL {
request . SetHeader ( "Referer" , originalURL ) // 改进浏览器剪藏扩展转换本地图片成功率 https://github.com/siyuan-note/siyuan/issues/7464
}
2022-05-26 15:18:53 +08:00
resp , reqErr := request . Get ( u )
if nil != reqErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "download net img [%s] failed: %s" , u , reqErr )
2022-05-26 15:18:53 +08:00
return ast . WalkSkipChildren
}
if 200 != resp . StatusCode {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "download net img [%s] failed: %d" , u , resp . StatusCode )
2022-05-26 15:18:53 +08:00
return ast . WalkSkipChildren
}
data , repErr := resp . ToBytes ( )
if nil != repErr {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "download net img [%s] failed: %s" , u , repErr )
2022-05-26 15:18:53 +08:00
return ast . WalkSkipChildren
}
var name string
if strings . Contains ( u , "?" ) {
name = u [ : strings . Index ( u , "?" ) ]
name = path . Base ( name )
} else {
name = path . Base ( u )
}
2022-05-26 20:33:45 +08:00
if strings . Contains ( name , "#" ) {
name = name [ : strings . Index ( name , "#" ) ]
}
2022-05-26 15:18:53 +08:00
name , _ = url . PathUnescape ( name )
ext := path . Ext ( name )
2022-05-30 18:02:12 +08:00
if "" == ext {
if mtype := mimetype . Detect ( data ) ; nil != mtype {
ext = mtype . Extension ( )
}
}
2022-05-26 15:18:53 +08:00
if "" == ext {
contentType := resp . Header . Get ( "Content-Type" )
exts , _ := mime . ExtensionsByType ( contentType )
if 0 < len ( exts ) {
ext = exts [ 0 ]
}
}
name = strings . TrimSuffix ( name , ext )
name = util . FilterFileName ( name )
2023-01-20 11:27:28 +08:00
name = util . TruncateLenFileName ( name )
2022-05-26 15:18:53 +08:00
name = "net-img-" + name + "-" + ast . NewNodeID ( ) + ext
2022-10-31 00:04:48 +08:00
writePath := filepath . Join ( assetsDirPath , name )
2022-09-29 21:52:01 +08:00
if err = filelock . WriteFile ( writePath , data ) ; nil != err {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "write downloaded net img [%s] to local assets [%s] failed: %s" , u , writePath , err )
2022-05-26 15:18:53 +08:00
return ast . WalkSkipChildren
}
linkDest . Tokens = [ ] byte ( "assets/" + name )
files ++
}
return ast . WalkSkipChildren
}
return ast . WalkContinue
} )
if 0 < files {
2022-06-09 19:17:15 +08:00
util . PushUpdateMsg ( msgId , Conf . Language ( 113 ) , 7000 )
2022-05-26 15:18:53 +08:00
if err = writeJSONQueue ( tree ) ; nil != err {
return
}
2022-06-09 19:17:15 +08:00
util . PushUpdateMsg ( msgId , fmt . Sprintf ( Conf . Language ( 120 ) , files ) , 5000 )
2022-05-26 15:18:53 +08:00
} else {
2022-06-09 19:17:15 +08:00
util . PushUpdateMsg ( msgId , Conf . Language ( 121 ) , 3000 )
2022-05-26 15:18:53 +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 { }
2022-05-26 15:18:53 +08:00
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
}
}
2022-07-15 09:25:02 +08:00
if ! strings . Contains ( strings . ToLower ( asset . HName ) , strings . ToLower ( keyword ) ) {
2022-08-04 18:08:15 +08:00
continue
2022-05-26 15:18:53 +08:00
}
2022-07-15 09:25:02 +08:00
_ , hName := search . MarkText ( asset . HName , keyword , 64 , Conf . Search . CaseSensitive )
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
sort . Slice ( ret , func ( i , j int ) bool {
return ret [ i ] . Updated > ret [ j ] . Updated
} )
2022-05-26 15:18:53 +08:00
return
}
func GetAssetAbsPath ( relativePath string ) ( absPath string , err error ) {
relativePath = strings . TrimSpace ( relativePath )
2022-09-16 22:59:24 +08:00
if strings . Contains ( relativePath , "?" ) {
relativePath = relativePath [ : strings . Index ( relativePath , "?" ) ]
}
2022-05-26 15:18:53 +08:00
notebooks , err := ListNotebooks ( )
if nil != err {
err = errors . New ( Conf . Language ( 0 ) )
return
}
// 在笔记本下搜索
for _ , notebook := range notebooks {
notebookAbsPath := filepath . Join ( util . DataDir , notebook . ID )
filepath . Walk ( notebookAbsPath , func ( path string , info fs . FileInfo , _ error ) error {
if isSkipFile ( info . Name ( ) ) {
if info . IsDir ( ) {
return filepath . SkipDir
}
return nil
}
if p := filepath . ToSlash ( path ) ; strings . HasSuffix ( p , relativePath ) {
if gulu . File . IsExist ( path ) {
absPath = path
return io . EOF
}
}
return nil
} )
if "" != absPath {
return
}
}
// 在全局 assets 路径下搜索
p := filepath . Join ( util . DataDir , relativePath )
if gulu . File . IsExist ( p ) {
absPath = p
return
}
return "" , errors . New ( fmt . Sprintf ( Conf . Language ( 12 ) , relativePath ) )
}
func UploadAssets2Cloud ( rootID string ) ( err error ) {
if ! IsSubscriber ( ) {
return
}
sqlAssets := sql . QueryRootBlockAssets ( rootID )
2023-01-04 13:50:18 +08:00
err = uploadAssets2Cloud ( sqlAssets , bizTypeUploadAssets )
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 将资源文件上传到云端图床。
2023-01-04 13:50:18 +08:00
func uploadAssets2Cloud ( sqlAssets [ ] * sql . Asset , bizType string ) ( err error ) {
2022-05-26 15:18:53 +08:00
var uploadAbsAssets [ ] string
2023-01-04 14:20:02 +08:00
for _ , asset := range sqlAssets {
var absPath string
absPath , err = GetAssetAbsPath ( asset . Path )
if nil != err {
logging . LogWarnf ( "get asset [%s] abs path failed: %s" , asset , err )
2022-05-26 15:18:53 +08:00
return
}
2023-01-04 14:20:02 +08:00
if "" == absPath {
logging . LogErrorf ( "not found asset [%s]" , asset )
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"
}
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 )
2022-05-26 15:18:53 +08:00
return statErr
2023-01-02 21:49:58 +08:00
}
if limitSize < uint64 ( fi . Size ( ) ) {
logging . LogWarnf ( "file [%s] larger than limit size [%s], ignore uploading it" , humanize . IBytes ( limitSize ) , absAsset )
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 )
2022-05-26 15:18:53 +08:00
return ErrFailedToConnectCloudServer
}
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 )
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 ) {
2022-06-05 15:16:46 +08:00
msgId := util . PushMsg ( Conf . Language ( 100 ) , 30 * 1000 )
defer func ( ) {
util . PushClearMsg ( msgId )
util . PushMsg ( Conf . Language ( 99 ) , 3000 )
} ( )
2022-05-26 15:18:53 +08:00
ret = [ ] string { }
unusedAssets := UnusedAssets ( )
2022-08-23 11:30:51 +08:00
historyDir , err := GetHistoryDir ( HistoryOpClean )
2022-05-26 15:18:53 +08:00
if nil != err {
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 ) {
2023-03-31 10:34:30 +08:00
if err = filelock . Copy ( p , historyPath ) ; nil != err {
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 {
2023-11-06 22:13:04 +08:00
if unusedAsset = filepath . Join ( util . DataDir , unusedAsset ) ; filelock . IsExist ( unusedAsset ) {
2022-05-26 15:18:53 +08:00
if err := os . RemoveAll ( unusedAsset ) ; nil != err {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "remove unused asset [%s] failed: %s" , unusedAsset , err )
2022-05-26 15:18:53 +08:00
}
}
ret = append ( ret , unusedAsset )
}
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 )
2022-05-26 15:18:53 +08:00
if nil != err {
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 ) {
2023-03-31 10:34:30 +08:00
if err = filelock . Copy ( absPath , historyPath ) ; nil != err {
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
}
2022-11-29 10:59:05 +08:00
if err = os . RemoveAll ( absPath ) ; nil != err {
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
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
}
2022-07-15 11:15:02 +08:00
func RenameAsset ( oldPath , newName 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 )
newName = gulu . Str . RemoveInvisible ( newName )
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 ) )
2022-07-15 12:02:35 +08:00
newPath := "assets/" + newName
2022-09-29 21:52:01 +08:00
if err = filelock . Copy ( filepath . Join ( util . DataDir , oldPath ) , filepath . Join ( util . DataDir , newPath ) ) ; nil != err {
2022-07-17 12:22:32 +08:00
logging . LogErrorf ( "copy asset [%s] failed: %s" , oldPath , 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
if err = filelock . Copy ( filepath . Join ( util . DataDir , oldPath + ".sya" ) , filepath . Join ( util . DataDir , newPath + ".sya" ) ) ; nil != err {
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 ( )
if nil != err {
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
}
2023-01-20 09:38:11 +08:00
util . PushEndlessProgress ( fmt . Sprintf ( Conf . Language ( 70 ) , filepath . Base ( treeAbsPath ) ) )
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
}
2023-01-25 20:46:17 +08:00
treenode . IndexBlockTree ( 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
}
}
}
IncSync ( )
2022-07-15 11:08:48 +08:00
util . ReloadUI ( )
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 ( )
if nil != err {
return
}
linkDestMap := map [ string ] bool { }
notebooks , err := ListNotebooks ( )
if nil != err {
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
for dest , _ := range dests {
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
for asset , _ := range assetsPathMap {
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
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
}
}
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
}
for asset , _ := range assetsPathMap {
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 ( )
if nil != err {
return
}
notebooks , err := ListNotebooks ( )
if nil != err {
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
}
}
}
for dest , _ := range dests {
if ! strings . HasPrefix ( dest , "assets/" ) {
continue
}
if idx := strings . Index ( dest , "?" ) ; 0 < idx {
dest = dest [ : idx ]
}
if strings . HasSuffix ( dest , "/" ) {
continue
}
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 ) {
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
}
2022-05-26 15:18:53 +08:00
func assetsLinkDestsInTree ( tree * parse . Tree ) ( ret [ ] string ) {
ret = [ ] string { }
ast . Walk ( tree . Root , func ( n * ast . Node , entering bool ) ast . WalkStatus {
// 修改以下代码时需要同时修改 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 &&
2023-04-12 12:02:56 +08:00
! n . IsTextMarkType ( "a" ) && ! n . IsTextMarkType ( "file-annotation-ref" ) ) {
2022-05-26 15:18:53 +08:00
return ast . WalkContinue
}
if ast . NodeLinkDest == n . Type {
if ! isRelativePath ( n . Tokens ) {
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" ) {
if ! isRelativePath ( gulu . Str . ToBytes ( n . TextMarkAHref ) ) {
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" ) {
if ! isRelativePath ( gulu . Str . ToBytes ( n . TextMarkFileAnnotationRefID ) ) {
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 )
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" )
}
2022-05-26 15:18:53 +08:00
if "" == dataAssets || ! isRelativePath ( [ ] byte ( dataAssets ) ) {
return ast . WalkContinue
}
ret = append ( ret , dataAssets )
} else { // HTMLBlock/InlineHTML/IFrame/Audio/Video
if index := bytes . Index ( n . Tokens , [ ] byte ( "src=\"" ) ) ; 0 < index {
src := n . Tokens [ index + len ( "src=\"" ) : ]
2022-08-15 16:49:49 +08:00
if index = bytes . Index ( src , [ ] byte ( "\"" ) ) ; 0 < index {
src = src [ : bytes . Index ( src , [ ] byte ( "\"" ) ) ]
if ! isRelativePath ( src ) {
return ast . WalkContinue
}
dest := strings . TrimSpace ( string ( src ) )
ret = append ( ret , dest )
} else {
logging . LogWarnf ( "src is missing the closing double quote in tree [%s] " , tree . Box + tree . Path )
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
}
func isRelativePath ( dest [ ] byte ) bool {
if 1 > len ( dest ) {
return false
}
if '/' == dest [ 0 ] {
return false
}
return ! bytes . Contains ( dest , [ ] byte ( ":" ) )
}
// allAssetAbsPaths 返回 asset 相对路径( assets/xxx) 到绝对路径( F:\SiYuan\data\assets\xxx) 的映射。
func allAssetAbsPaths ( ) ( assetsAbsPathMap map [ string ] string , err error ) {
notebooks , err := ListNotebooks ( )
if nil != err {
return
}
assetsAbsPathMap = map [ string ] string { }
// 笔记本 assets
for _ , notebook := range notebooks {
notebookAbsPath := filepath . Join ( util . DataDir , notebook . ID )
filepath . Walk ( notebookAbsPath , func ( path string , info fs . FileInfo , err error ) error {
if notebookAbsPath == path {
return nil
}
if isSkipFile ( info . Name ( ) ) {
if info . IsDir ( ) {
return filepath . SkipDir
}
return nil
}
if info . IsDir ( ) && "assets" == info . Name ( ) {
filepath . Walk ( path , func ( assetPath string , info fs . FileInfo , err error ) error {
if path == assetPath {
return nil
}
if isSkipFile ( info . Name ( ) ) {
if info . IsDir ( ) {
return filepath . SkipDir
}
return nil
}
relPath := filepath . ToSlash ( assetPath )
relPath = relPath [ strings . Index ( relPath , "assets/" ) : ]
if info . IsDir ( ) {
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 ( )
2022-07-23 17:41:47 +08:00
filepath . Walk ( dataAssetsAbsPath , func ( assetPath string , info fs . FileInfo , err error ) error {
if dataAssetsAbsPath == assetPath {
2022-05-26 15:18:53 +08:00
return nil
}
if isSkipFile ( info . Name ( ) ) {
if info . IsDir ( ) {
return filepath . SkipDir
}
return nil
}
relPath := filepath . ToSlash ( assetPath )
relPath = relPath [ strings . Index ( relPath , "assets/" ) : ]
if info . IsDir ( ) {
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
filepath . Walk ( rootPath , func ( path string , info fs . FileInfo , err error ) error {
if rootPath == path || nil == info {
return nil
}
isDir := info . IsDir ( )
name := info . Name ( )
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 {
2022-09-29 21:52:01 +08:00
if err := filelock . Copy ( assetsDirPath , dataAssetsPath ) ; nil != err {
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
}
}
}