Improve pasting performance for large amounts of content https://github.com/siyuan-note/siyuan/issues/15306

This commit is contained in:
Daniel 2025-07-26 21:24:28 +08:00
parent 9d392bd663
commit 7c692e8273
No known key found for this signature in database
GPG key ID: 86211BA83DF03017
4 changed files with 362 additions and 187 deletions

View file

@ -4,10 +4,12 @@ import {transaction, updateTransaction} from "../wysiwyg/transaction";
import {getContenteditableElement} from "../wysiwyg/getBlock"; import {getContenteditableElement} from "../wysiwyg/getBlock";
import { import {
fixTableRange, fixTableRange,
focusBlock, focusByRange, focusBlock,
focusByRange,
focusByWbr, focusByWbr,
getEditorRange, getEditorRange,
getSelectionOffset, setLastNodeRange, getSelectionOffset,
setLastNodeRange,
} from "./selection"; } from "./selection";
import {Constants} from "../../constants"; import {Constants} from "../../constants";
import {highlightRender} from "../render/highlightRender"; import {highlightRender} from "../render/highlightRender";
@ -257,7 +259,9 @@ export const insertHTML = (html: string, protyle: IProtyle, isBlock = false,
// 移动端插入嵌入块时,获取到的 range 为旧值 // 移动端插入嵌入块时,获取到的 range 为旧值
useProtyleRange = false, useProtyleRange = false,
// 在开头粘贴块则插入上方 // 在开头粘贴块则插入上方
insertByCursor = false) => { insertByCursor = false,
// 是否需要再次 spin
spin = true) => {
if (html === "") { if (html === "") {
return; return;
} }
@ -364,7 +368,7 @@ export const insertHTML = (html: string, protyle: IProtyle, isBlock = false,
} }
let innerHTML = unSpinHTML || // 在 table 中插入需要使用转换好的行内元素 https://github.com/siyuan-note/siyuan/issues/9358 let innerHTML = unSpinHTML || // 在 table 中插入需要使用转换好的行内元素 https://github.com/siyuan-note/siyuan/issues/9358
protyle.lute.SpinBlockDOM(html) || // 需要再 spin 一次 https://github.com/siyuan-note/siyuan/issues/7118 (spin && protyle.lute.SpinBlockDOM(html)) || // 需要再 spin 一次 https://github.com/siyuan-note/siyuan/issues/7118
html; // 空格会被 Spin 不再,需要使用原文 html; // 空格会被 Spin 不再,需要使用原文
// 粘贴纯文本时会进行内部转义,这里需要进行反转义 https://github.com/siyuan-note/siyuan/issues/10620 // 粘贴纯文本时会进行内部转义,这里需要进行反转义 https://github.com/siyuan-note/siyuan/issues/10620
innerHTML = innerHTML.replace(/;;;lt;;;/g, "<").replace(/;;;gt;;;/g, ">"); innerHTML = innerHTML.replace(/;;;lt;;;/g, "<").replace(/;;;gt;;;/g, ">");

View file

@ -591,7 +591,7 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven
textPlain = textPlain.replace(/\u200D```/g, "```"); textPlain = textPlain.replace(/\u200D```/g, "```");
const textPlainDom = protyle.lute.Md2BlockDOM(textPlain); const textPlainDom = protyle.lute.Md2BlockDOM(textPlain);
insertHTML(textPlainDom, protyle, false, false, true); insertHTML(textPlainDom, protyle, false, false, true, false /* 不再次 spin 以提升性能 https://github.com/siyuan-note/siyuan/issues/15306 */);
} }
blockRender(protyle, protyle.wysiwyg.element); blockRender(protyle, protyle.wysiwyg.element);
processRender(protyle.wysiwyg.element); processRender(protyle.wysiwyg.element);

View file

@ -764,6 +764,11 @@ func loadNodesByStartEnd(tree *parse.Tree, startID, endID string) (nodes []*ast.
} }
break break
} }
if len(nodes) >= Conf.Editor.DynamicLoadBlocks {
// 如果加载到指定数量的块则停止加载
break
}
} }
return return
} }

View file

@ -167,153 +167,156 @@ func performTx(tx *Transaction) (ret *TxErr) {
} }
}() }()
for _, op := range tx.DoOperations { isLargeInsert := tx.processLargeInsert()
switch op.Action { if !isLargeInsert {
case "create": for _, op := range tx.DoOperations {
ret = tx.doCreate(op) switch op.Action {
case "update": case "create":
ret = tx.doUpdate(op) ret = tx.doCreate(op)
case "insert": case "update":
ret = tx.doInsert(op) ret = tx.doUpdate(op)
case "delete": case "insert":
ret = tx.doDelete(op) ret = tx.doInsert(op)
case "move": case "delete":
ret = tx.doMove(op) ret = tx.doDelete(op)
case "moveOutlineHeading": case "move":
ret = tx.doMoveOutlineHeading(op) ret = tx.doMove(op)
case "append": case "moveOutlineHeading":
ret = tx.doAppend(op) ret = tx.doMoveOutlineHeading(op)
case "appendInsert": case "append":
ret = tx.doAppendInsert(op) ret = tx.doAppend(op)
case "prependInsert": case "appendInsert":
ret = tx.doPrependInsert(op) ret = tx.doAppendInsert(op)
case "foldHeading": case "prependInsert":
ret = tx.doFoldHeading(op) ret = tx.doPrependInsert(op)
case "unfoldHeading": case "foldHeading":
ret = tx.doUnfoldHeading(op) ret = tx.doFoldHeading(op)
case "setAttrs": case "unfoldHeading":
ret = tx.doSetAttrs(op) ret = tx.doUnfoldHeading(op)
case "doUpdateUpdated": case "setAttrs":
ret = tx.doUpdateUpdated(op) ret = tx.doSetAttrs(op)
case "addFlashcards": case "doUpdateUpdated":
ret = tx.doAddFlashcards(op) ret = tx.doUpdateUpdated(op)
case "removeFlashcards": case "addFlashcards":
ret = tx.doRemoveFlashcards(op) ret = tx.doAddFlashcards(op)
case "setAttrViewName": case "removeFlashcards":
ret = tx.doSetAttrViewName(op) ret = tx.doRemoveFlashcards(op)
case "setAttrViewFilters": case "setAttrViewName":
ret = tx.doSetAttrViewFilters(op) ret = tx.doSetAttrViewName(op)
case "setAttrViewSorts": case "setAttrViewFilters":
ret = tx.doSetAttrViewSorts(op) ret = tx.doSetAttrViewFilters(op)
case "setAttrViewPageSize": case "setAttrViewSorts":
ret = tx.doSetAttrViewPageSize(op) ret = tx.doSetAttrViewSorts(op)
case "setAttrViewColWidth": case "setAttrViewPageSize":
ret = tx.doSetAttrViewColumnWidth(op) ret = tx.doSetAttrViewPageSize(op)
case "setAttrViewColWrap": case "setAttrViewColWidth":
ret = tx.doSetAttrViewColumnWrap(op) ret = tx.doSetAttrViewColumnWidth(op)
case "setAttrViewColHidden": case "setAttrViewColWrap":
ret = tx.doSetAttrViewColumnHidden(op) ret = tx.doSetAttrViewColumnWrap(op)
case "setAttrViewColPin": case "setAttrViewColHidden":
ret = tx.doSetAttrViewColumnPin(op) ret = tx.doSetAttrViewColumnHidden(op)
case "setAttrViewColIcon": case "setAttrViewColPin":
ret = tx.doSetAttrViewColumnIcon(op) ret = tx.doSetAttrViewColumnPin(op)
case "setAttrViewColDesc": case "setAttrViewColIcon":
ret = tx.doSetAttrViewColumnDesc(op) ret = tx.doSetAttrViewColumnIcon(op)
case "insertAttrViewBlock": case "setAttrViewColDesc":
ret = tx.doInsertAttrViewBlock(op) ret = tx.doSetAttrViewColumnDesc(op)
case "removeAttrViewBlock": case "insertAttrViewBlock":
ret = tx.doRemoveAttrViewBlock(op) ret = tx.doInsertAttrViewBlock(op)
case "addAttrViewCol": case "removeAttrViewBlock":
ret = tx.doAddAttrViewColumn(op) ret = tx.doRemoveAttrViewBlock(op)
case "updateAttrViewCol": case "addAttrViewCol":
ret = tx.doUpdateAttrViewColumn(op) ret = tx.doAddAttrViewColumn(op)
case "removeAttrViewCol": case "updateAttrViewCol":
ret = tx.doRemoveAttrViewColumn(op) ret = tx.doUpdateAttrViewColumn(op)
case "sortAttrViewRow": case "removeAttrViewCol":
ret = tx.doSortAttrViewRow(op) ret = tx.doRemoveAttrViewColumn(op)
case "sortAttrViewCol": case "sortAttrViewRow":
ret = tx.doSortAttrViewColumn(op) ret = tx.doSortAttrViewRow(op)
case "sortAttrViewKey": case "sortAttrViewCol":
ret = tx.doSortAttrViewKey(op) ret = tx.doSortAttrViewColumn(op)
case "updateAttrViewCell": case "sortAttrViewKey":
ret = tx.doUpdateAttrViewCell(op) ret = tx.doSortAttrViewKey(op)
case "updateAttrViewColOptions": case "updateAttrViewCell":
ret = tx.doUpdateAttrViewColOptions(op) ret = tx.doUpdateAttrViewCell(op)
case "removeAttrViewColOption": case "updateAttrViewColOptions":
ret = tx.doRemoveAttrViewColOption(op) ret = tx.doUpdateAttrViewColOptions(op)
case "updateAttrViewColOption": case "removeAttrViewColOption":
ret = tx.doUpdateAttrViewColOption(op) ret = tx.doRemoveAttrViewColOption(op)
case "setAttrViewColOptionDesc": case "updateAttrViewColOption":
ret = tx.doSetAttrViewColOptionDesc(op) ret = tx.doUpdateAttrViewColOption(op)
case "setAttrViewColCalc": case "setAttrViewColOptionDesc":
ret = tx.doSetAttrViewColCalc(op) ret = tx.doSetAttrViewColOptionDesc(op)
case "updateAttrViewColNumberFormat": case "setAttrViewColCalc":
ret = tx.doUpdateAttrViewColNumberFormat(op) ret = tx.doSetAttrViewColCalc(op)
case "replaceAttrViewBlock": case "updateAttrViewColNumberFormat":
ret = tx.doReplaceAttrViewBlock(op) ret = tx.doUpdateAttrViewColNumberFormat(op)
case "updateAttrViewColTemplate": case "replaceAttrViewBlock":
ret = tx.doUpdateAttrViewColTemplate(op) ret = tx.doReplaceAttrViewBlock(op)
case "addAttrViewView": case "updateAttrViewColTemplate":
ret = tx.doAddAttrViewView(op) ret = tx.doUpdateAttrViewColTemplate(op)
case "removeAttrViewView": case "addAttrViewView":
ret = tx.doRemoveAttrViewView(op) ret = tx.doAddAttrViewView(op)
case "setAttrViewViewName": case "removeAttrViewView":
ret = tx.doSetAttrViewViewName(op) ret = tx.doRemoveAttrViewView(op)
case "setAttrViewViewIcon": case "setAttrViewViewName":
ret = tx.doSetAttrViewViewIcon(op) ret = tx.doSetAttrViewViewName(op)
case "setAttrViewViewDesc": case "setAttrViewViewIcon":
ret = tx.doSetAttrViewViewDesc(op) ret = tx.doSetAttrViewViewIcon(op)
case "duplicateAttrViewView": case "setAttrViewViewDesc":
ret = tx.doDuplicateAttrViewView(op) ret = tx.doSetAttrViewViewDesc(op)
case "sortAttrViewView": case "duplicateAttrViewView":
ret = tx.doSortAttrViewView(op) ret = tx.doDuplicateAttrViewView(op)
case "updateAttrViewColRelation": case "sortAttrViewView":
ret = tx.doUpdateAttrViewColRelation(op) ret = tx.doSortAttrViewView(op)
case "updateAttrViewColRollup": case "updateAttrViewColRelation":
ret = tx.doUpdateAttrViewColRollup(op) ret = tx.doUpdateAttrViewColRelation(op)
case "hideAttrViewName": case "updateAttrViewColRollup":
ret = tx.doHideAttrViewName(op) ret = tx.doUpdateAttrViewColRollup(op)
case "setAttrViewColDate": case "hideAttrViewName":
ret = tx.doSetAttrViewColDate(op) ret = tx.doHideAttrViewName(op)
case "unbindAttrViewBlock": case "setAttrViewColDate":
ret = tx.doUnbindAttrViewBlock(op) ret = tx.doSetAttrViewColDate(op)
case "duplicateAttrViewKey": case "unbindAttrViewBlock":
ret = tx.doDuplicateAttrViewKey(op) ret = tx.doUnbindAttrViewBlock(op)
case "setAttrViewCoverFrom": case "duplicateAttrViewKey":
ret = tx.doSetAttrViewCoverFrom(op) ret = tx.doDuplicateAttrViewKey(op)
case "setAttrViewCoverFromAssetKeyID": case "setAttrViewCoverFrom":
ret = tx.doSetAttrViewCoverFromAssetKeyID(op) ret = tx.doSetAttrViewCoverFrom(op)
case "setAttrViewCardSize": case "setAttrViewCoverFromAssetKeyID":
ret = tx.doSetAttrViewCardSize(op) ret = tx.doSetAttrViewCoverFromAssetKeyID(op)
case "setAttrViewFitImage": case "setAttrViewCardSize":
ret = tx.doSetAttrViewFitImage(op) ret = tx.doSetAttrViewCardSize(op)
case "setAttrViewShowIcon": case "setAttrViewFitImage":
ret = tx.doSetAttrViewShowIcon(op) ret = tx.doSetAttrViewFitImage(op)
case "setAttrViewWrapField": case "setAttrViewShowIcon":
ret = tx.doSetAttrViewWrapField(op) ret = tx.doSetAttrViewShowIcon(op)
case "changeAttrViewLayout": case "setAttrViewWrapField":
ret = tx.doChangeAttrViewLayout(op) ret = tx.doSetAttrViewWrapField(op)
case "setAttrViewBlockView": case "changeAttrViewLayout":
ret = tx.doSetAttrViewBlockView(op) ret = tx.doChangeAttrViewLayout(op)
case "setAttrViewCardAspectRatio": case "setAttrViewBlockView":
ret = tx.doSetAttrViewCardAspectRatio(op) ret = tx.doSetAttrViewBlockView(op)
case "setAttrViewGroup": case "setAttrViewCardAspectRatio":
ret = tx.doSetAttrViewGroup(op) ret = tx.doSetAttrViewCardAspectRatio(op)
case "hideAttrViewGroup": case "setAttrViewGroup":
ret = tx.doHideAttrViewGroup(op) ret = tx.doSetAttrViewGroup(op)
case "foldAttrViewGroup": case "hideAttrViewGroup":
ret = tx.doFoldAttrViewGroup(op) ret = tx.doHideAttrViewGroup(op)
case "syncAttrViewTableColWidth": case "foldAttrViewGroup":
ret = tx.doSyncAttrViewTableColWidth(op) ret = tx.doFoldAttrViewGroup(op)
case "removeAttrViewGroup": case "syncAttrViewTableColWidth":
ret = tx.doRemoveAttrViewGroup(op) ret = tx.doSyncAttrViewTableColWidth(op)
case "sortAttrViewGroup": case "removeAttrViewGroup":
ret = tx.doSortAttrViewGroup(op) ret = tx.doRemoveAttrViewGroup(op)
} case "sortAttrViewGroup":
ret = tx.doSortAttrViewGroup(op)
}
if nil != ret { if nil != ret {
tx.rollback() tx.rollback()
return return
}
} }
} }
@ -324,6 +327,42 @@ func performTx(tx *Transaction) (ret *TxErr) {
return return
} }
func (tx *Transaction) processLargeInsert() bool {
opSize := len(tx.DoOperations)
isLargeInsert := 128 < opSize
if isLargeInsert {
var previousID string
for i, op := range tx.DoOperations {
if i == opSize-1 {
if "delete" != op.Action {
// 最后一个是 delete
isLargeInsert = false
}
break
}
if "insert" != op.Action {
isLargeInsert = false
break
}
if "" == op.PreviousID {
isLargeInsert = false
break
}
if "" == previousID {
previousID = op.PreviousID
} else if previousID != op.PreviousID {
isLargeInsert = false
break
}
}
if isLargeInsert {
tx.doLargeInsert(previousID)
}
}
return isLargeInsert
}
func (tx *Transaction) doMove(operation *Operation) (ret *TxErr) { func (tx *Transaction) doMove(operation *Operation) (ret *TxErr) {
var err error var err error
id := operation.ID id := operation.ID
@ -980,6 +1019,127 @@ func syncDelete2AttributeView(node *ast.Node) (changedAvIDs []string) {
return return
} }
func (tx *Transaction) doLargeInsert(previousID string) (ret *TxErr) {
tree, err := tx.loadTree(previousID)
if nil != err {
logging.LogErrorf("load tree [%s] failed: %s", previousID, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
}
for _, operation := range tx.DoOperations {
if "insert" != operation.Action {
break
}
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
subTree := tx.luteEngine.BlockDOM2Tree(data)
tx.processGlobalAssets(subTree)
insertedNode := subTree.Root.FirstChild
if nil == insertedNode {
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: tree.ID}
}
var remains []*ast.Node
for remain := insertedNode.Next; nil != remain; remain = remain.Next {
if ast.NodeKramdownBlockIAL != remain.Type {
if "" == remain.ID {
remain.ID = ast.NewNodeID()
remain.SetIALAttr("id", remain.ID)
}
remains = append(remains, remain)
}
}
if "" == insertedNode.ID {
insertedNode.ID = ast.NewNodeID()
insertedNode.SetIALAttr("id", insertedNode.ID)
}
node := treenode.GetNodeInTree(tree, previousID)
if nil == node {
logging.LogErrorf("get node [%s] in tree [%s] failed", previousID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
}
if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") {
children := treenode.HeadingChildren(node)
if l := len(children); 0 < l {
node = children[l-1]
}
}
if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
insertedNode = insertedNode.FirstChild
}
for i := len(remains) - 1; 0 <= i; i-- {
remain := remains[i]
node.InsertAfter(remain)
}
node.InsertAfter(insertedNode)
createdUpdated(insertedNode)
tx.nodes[insertedNode.ID] = insertedNode
tx.trees[tree.ID] = tree
// 收集引用的定义块 ID
refDefIDs := getRefDefIDs(insertedNode)
// 推送定义节点引用计数
for _, defID := range refDefIDs {
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, util.SQLFlushInterval, refreshRefCount, defID)
}
upsertAvBlockRel(insertedNode)
// 复制为副本时将该副本块插入到数据库中 https://github.com/siyuan-note/siyuan/issues/11959
avs := insertedNode.IALAttr(av.NodeAttrNameAvs)
for _, avID := range strings.Split(avs, ",") {
if !ast.IsNodeIDPattern(avID) {
continue
}
AddAttributeViewBlock(tx, []map[string]interface{}{{
"id": insertedNode.ID,
"isDetached": false,
}}, avID, "", previousID, false)
ReloadAttrView(avID)
}
if ast.NodeAttributeView == insertedNode.Type {
// 插入数据库块时需要重新绑定其中已经存在的块
// 比如剪切操作时,会先进行 delete 数据库解绑块,这里需要重新绑定 https://github.com/siyuan-note/siyuan/issues/13031
attrView, parseErr := av.ParseAttributeView(insertedNode.AttributeViewID)
if nil == parseErr {
trees, toBindNodes := tx.getAttrViewBoundNodes(attrView)
for _, toBindNode := range toBindNodes {
t := trees[toBindNode.ID]
bindBlockAv0(tx, insertedNode.AttributeViewID, toBindNode, t)
}
// 设置视图 https://github.com/siyuan-note/siyuan/issues/15279
v := attrView.GetView(attrView.ViewID)
if nil != v {
insertedNode.AttributeViewType = string(v.LayoutType)
attrs := parse.IAL2Map(insertedNode.KramdownIAL)
if "" == attrs[av.NodeAttrView] {
attrs[av.NodeAttrView] = v.ID
err = setNodeAttrs(insertedNode, tree, attrs)
if err != nil {
logging.LogWarnf("set node [%s] attrs failed: %s", operation.BlockID, err)
return
}
}
}
}
}
operation.ID = insertedNode.ID
operation.ParentID = insertedNode.Parent.ID
}
if err = tx.writeTree(tree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: tree.ID}
}
return tx.doDelete(tx.DoOperations[len(tx.DoOperations)-1])
}
func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) { func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
var bt *treenode.BlockTree var bt *treenode.BlockTree
bts := treenode.GetBlockTrees([]string{operation.ParentID, operation.PreviousID, operation.NextID}) bts := treenode.GetBlockTrees([]string{operation.ParentID, operation.PreviousID, operation.NextID})
@ -1005,42 +1165,7 @@ func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "") data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
subTree := tx.luteEngine.BlockDOM2Tree(data) subTree := tx.luteEngine.BlockDOM2Tree(data)
tx.processGlobalAssets(subTree)
if !tx.isGlobalAssetsInit {
tx.assetsDir = getAssetsDir(filepath.Join(util.DataDir, bt.BoxID), filepath.Dir(filepath.Join(util.DataDir, bt.BoxID, bt.Path)))
tx.isGlobalAssets = strings.HasPrefix(tx.assetsDir, filepath.Join(util.DataDir, "assets"))
tx.isGlobalAssetsInit = true
}
if !tx.isGlobalAssets {
// 本地资源文件需要移动到用户手动建立的 assets 下 https://github.com/siyuan-note/siyuan/issues/2410
ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeLinkDest == n.Type && bytes.HasPrefix(n.Tokens, []byte("assets/")) {
assetP := gulu.Str.FromBytes(n.Tokens)
assetPath, e := GetAssetAbsPath(assetP)
if nil != e {
logging.LogErrorf("get path of asset [%s] failed: %s", assetP, err)
return ast.WalkContinue
}
if !strings.HasPrefix(assetPath, filepath.Join(util.DataDir, "assets")) {
// 非全局 assets 则跳过
return ast.WalkContinue
}
// 只有全局 assets 才移动到相对 assets
targetP := filepath.Join(tx.assetsDir, filepath.Base(assetPath))
if e = filelock.Rename(assetPath, targetP); err != nil {
logging.LogErrorf("copy path of asset from [%s] to [%s] failed: %s", assetPath, targetP, err)
return ast.WalkContinue
}
}
return ast.WalkContinue
})
}
insertedNode := subTree.Root.FirstChild insertedNode := subTree.Root.FirstChild
if nil == insertedNode { if nil == insertedNode {
@ -1189,6 +1314,47 @@ func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
return return
} }
func (tx *Transaction) processGlobalAssets(tree *parse.Tree) {
if !tx.isGlobalAssetsInit {
tx.assetsDir = getAssetsDir(filepath.Join(util.DataDir, tree.Box), filepath.Dir(filepath.Join(util.DataDir, tree.Box, tree.Path)))
tx.isGlobalAssets = strings.HasPrefix(tx.assetsDir, filepath.Join(util.DataDir, "assets"))
tx.isGlobalAssetsInit = true
}
if tx.isGlobalAssets {
return
}
// 本地资源文件需要移动到用户手动建立的 assets 下 https://github.com/siyuan-note/siyuan/issues/2410
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeLinkDest == n.Type && bytes.HasPrefix(n.Tokens, []byte("assets/")) {
assetP := gulu.Str.FromBytes(n.Tokens)
assetPath, e := GetAssetAbsPath(assetP)
if nil != e {
logging.LogErrorf("get path of asset [%s] failed: %s", assetP, e)
return ast.WalkContinue
}
if !strings.HasPrefix(assetPath, filepath.Join(util.DataDir, "assets")) {
// 非全局 assets 则跳过
return ast.WalkContinue
}
// 只有全局 assets 才移动到相对 assets
targetP := filepath.Join(tx.assetsDir, filepath.Base(assetPath))
if e = filelock.Rename(assetPath, targetP); e != nil {
logging.LogErrorf("copy path of asset from [%s] to [%s] failed: %s", assetPath, targetP, e)
return ast.WalkContinue
}
}
return ast.WalkContinue
})
}
func (tx *Transaction) doUpdate(operation *Operation) (ret *TxErr) { func (tx *Transaction) doUpdate(operation *Operation) (ret *TxErr) {
id := operation.ID id := operation.ID
tree, err := tx.loadTree(id) tree, err := tx.loadTree(id)