diff --git a/app/src/protyle/util/insertHTML.ts b/app/src/protyle/util/insertHTML.ts index fc64cf4b3..9a4a3a2a9 100644 --- a/app/src/protyle/util/insertHTML.ts +++ b/app/src/protyle/util/insertHTML.ts @@ -4,10 +4,12 @@ import {transaction, updateTransaction} from "../wysiwyg/transaction"; import {getContenteditableElement} from "../wysiwyg/getBlock"; import { fixTableRange, - focusBlock, focusByRange, + focusBlock, + focusByRange, focusByWbr, getEditorRange, - getSelectionOffset, setLastNodeRange, + getSelectionOffset, + setLastNodeRange, } from "./selection"; import {Constants} from "../../constants"; import {highlightRender} from "../render/highlightRender"; @@ -257,7 +259,9 @@ export const insertHTML = (html: string, protyle: IProtyle, isBlock = false, // 移动端插入嵌入块时,获取到的 range 为旧值 useProtyleRange = false, // 在开头粘贴块则插入上方 - insertByCursor = false) => { + insertByCursor = false, + // 是否需要再次 spin + spin = true) => { if (html === "") { 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 - 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 不再,需要使用原文 // 粘贴纯文本时会进行内部转义,这里需要进行反转义 https://github.com/siyuan-note/siyuan/issues/10620 innerHTML = innerHTML.replace(/;;;lt;;;/g, "<").replace(/;;;gt;;;/g, ">"); diff --git a/app/src/protyle/util/paste.ts b/app/src/protyle/util/paste.ts index 26fc192b5..10181aed6 100644 --- a/app/src/protyle/util/paste.ts +++ b/app/src/protyle/util/paste.ts @@ -591,7 +591,7 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven textPlain = textPlain.replace(/\u200D```/g, "```"); 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); processRender(protyle.wysiwyg.element); diff --git a/kernel/model/file.go b/kernel/model/file.go index 2cd5cf816..654f46ee2 100644 --- a/kernel/model/file.go +++ b/kernel/model/file.go @@ -764,6 +764,11 @@ func loadNodesByStartEnd(tree *parse.Tree, startID, endID string) (nodes []*ast. } break } + + if len(nodes) >= Conf.Editor.DynamicLoadBlocks { + // 如果加载到指定数量的块则停止加载 + break + } } return } diff --git a/kernel/model/transaction.go b/kernel/model/transaction.go index a9aee1db6..9f1867ca3 100644 --- a/kernel/model/transaction.go +++ b/kernel/model/transaction.go @@ -167,153 +167,156 @@ func performTx(tx *Transaction) (ret *TxErr) { } }() - for _, op := range tx.DoOperations { - switch op.Action { - case "create": - ret = tx.doCreate(op) - case "update": - ret = tx.doUpdate(op) - case "insert": - ret = tx.doInsert(op) - case "delete": - ret = tx.doDelete(op) - case "move": - ret = tx.doMove(op) - case "moveOutlineHeading": - ret = tx.doMoveOutlineHeading(op) - case "append": - ret = tx.doAppend(op) - case "appendInsert": - ret = tx.doAppendInsert(op) - case "prependInsert": - ret = tx.doPrependInsert(op) - case "foldHeading": - ret = tx.doFoldHeading(op) - case "unfoldHeading": - ret = tx.doUnfoldHeading(op) - case "setAttrs": - ret = tx.doSetAttrs(op) - case "doUpdateUpdated": - ret = tx.doUpdateUpdated(op) - case "addFlashcards": - ret = tx.doAddFlashcards(op) - case "removeFlashcards": - ret = tx.doRemoveFlashcards(op) - case "setAttrViewName": - ret = tx.doSetAttrViewName(op) - case "setAttrViewFilters": - ret = tx.doSetAttrViewFilters(op) - case "setAttrViewSorts": - ret = tx.doSetAttrViewSorts(op) - case "setAttrViewPageSize": - ret = tx.doSetAttrViewPageSize(op) - case "setAttrViewColWidth": - ret = tx.doSetAttrViewColumnWidth(op) - case "setAttrViewColWrap": - ret = tx.doSetAttrViewColumnWrap(op) - case "setAttrViewColHidden": - ret = tx.doSetAttrViewColumnHidden(op) - case "setAttrViewColPin": - ret = tx.doSetAttrViewColumnPin(op) - case "setAttrViewColIcon": - ret = tx.doSetAttrViewColumnIcon(op) - case "setAttrViewColDesc": - ret = tx.doSetAttrViewColumnDesc(op) - case "insertAttrViewBlock": - ret = tx.doInsertAttrViewBlock(op) - case "removeAttrViewBlock": - ret = tx.doRemoveAttrViewBlock(op) - case "addAttrViewCol": - ret = tx.doAddAttrViewColumn(op) - case "updateAttrViewCol": - ret = tx.doUpdateAttrViewColumn(op) - case "removeAttrViewCol": - ret = tx.doRemoveAttrViewColumn(op) - case "sortAttrViewRow": - ret = tx.doSortAttrViewRow(op) - case "sortAttrViewCol": - ret = tx.doSortAttrViewColumn(op) - case "sortAttrViewKey": - ret = tx.doSortAttrViewKey(op) - case "updateAttrViewCell": - ret = tx.doUpdateAttrViewCell(op) - case "updateAttrViewColOptions": - ret = tx.doUpdateAttrViewColOptions(op) - case "removeAttrViewColOption": - ret = tx.doRemoveAttrViewColOption(op) - case "updateAttrViewColOption": - ret = tx.doUpdateAttrViewColOption(op) - case "setAttrViewColOptionDesc": - ret = tx.doSetAttrViewColOptionDesc(op) - case "setAttrViewColCalc": - ret = tx.doSetAttrViewColCalc(op) - case "updateAttrViewColNumberFormat": - ret = tx.doUpdateAttrViewColNumberFormat(op) - case "replaceAttrViewBlock": - ret = tx.doReplaceAttrViewBlock(op) - case "updateAttrViewColTemplate": - ret = tx.doUpdateAttrViewColTemplate(op) - case "addAttrViewView": - ret = tx.doAddAttrViewView(op) - case "removeAttrViewView": - ret = tx.doRemoveAttrViewView(op) - case "setAttrViewViewName": - ret = tx.doSetAttrViewViewName(op) - case "setAttrViewViewIcon": - ret = tx.doSetAttrViewViewIcon(op) - case "setAttrViewViewDesc": - ret = tx.doSetAttrViewViewDesc(op) - case "duplicateAttrViewView": - ret = tx.doDuplicateAttrViewView(op) - case "sortAttrViewView": - ret = tx.doSortAttrViewView(op) - case "updateAttrViewColRelation": - ret = tx.doUpdateAttrViewColRelation(op) - case "updateAttrViewColRollup": - ret = tx.doUpdateAttrViewColRollup(op) - case "hideAttrViewName": - ret = tx.doHideAttrViewName(op) - case "setAttrViewColDate": - ret = tx.doSetAttrViewColDate(op) - case "unbindAttrViewBlock": - ret = tx.doUnbindAttrViewBlock(op) - case "duplicateAttrViewKey": - ret = tx.doDuplicateAttrViewKey(op) - case "setAttrViewCoverFrom": - ret = tx.doSetAttrViewCoverFrom(op) - case "setAttrViewCoverFromAssetKeyID": - ret = tx.doSetAttrViewCoverFromAssetKeyID(op) - case "setAttrViewCardSize": - ret = tx.doSetAttrViewCardSize(op) - case "setAttrViewFitImage": - ret = tx.doSetAttrViewFitImage(op) - case "setAttrViewShowIcon": - ret = tx.doSetAttrViewShowIcon(op) - case "setAttrViewWrapField": - ret = tx.doSetAttrViewWrapField(op) - case "changeAttrViewLayout": - ret = tx.doChangeAttrViewLayout(op) - case "setAttrViewBlockView": - ret = tx.doSetAttrViewBlockView(op) - case "setAttrViewCardAspectRatio": - ret = tx.doSetAttrViewCardAspectRatio(op) - case "setAttrViewGroup": - ret = tx.doSetAttrViewGroup(op) - case "hideAttrViewGroup": - ret = tx.doHideAttrViewGroup(op) - case "foldAttrViewGroup": - ret = tx.doFoldAttrViewGroup(op) - case "syncAttrViewTableColWidth": - ret = tx.doSyncAttrViewTableColWidth(op) - case "removeAttrViewGroup": - ret = tx.doRemoveAttrViewGroup(op) - case "sortAttrViewGroup": - ret = tx.doSortAttrViewGroup(op) - } + isLargeInsert := tx.processLargeInsert() + if !isLargeInsert { + for _, op := range tx.DoOperations { + switch op.Action { + case "create": + ret = tx.doCreate(op) + case "update": + ret = tx.doUpdate(op) + case "insert": + ret = tx.doInsert(op) + case "delete": + ret = tx.doDelete(op) + case "move": + ret = tx.doMove(op) + case "moveOutlineHeading": + ret = tx.doMoveOutlineHeading(op) + case "append": + ret = tx.doAppend(op) + case "appendInsert": + ret = tx.doAppendInsert(op) + case "prependInsert": + ret = tx.doPrependInsert(op) + case "foldHeading": + ret = tx.doFoldHeading(op) + case "unfoldHeading": + ret = tx.doUnfoldHeading(op) + case "setAttrs": + ret = tx.doSetAttrs(op) + case "doUpdateUpdated": + ret = tx.doUpdateUpdated(op) + case "addFlashcards": + ret = tx.doAddFlashcards(op) + case "removeFlashcards": + ret = tx.doRemoveFlashcards(op) + case "setAttrViewName": + ret = tx.doSetAttrViewName(op) + case "setAttrViewFilters": + ret = tx.doSetAttrViewFilters(op) + case "setAttrViewSorts": + ret = tx.doSetAttrViewSorts(op) + case "setAttrViewPageSize": + ret = tx.doSetAttrViewPageSize(op) + case "setAttrViewColWidth": + ret = tx.doSetAttrViewColumnWidth(op) + case "setAttrViewColWrap": + ret = tx.doSetAttrViewColumnWrap(op) + case "setAttrViewColHidden": + ret = tx.doSetAttrViewColumnHidden(op) + case "setAttrViewColPin": + ret = tx.doSetAttrViewColumnPin(op) + case "setAttrViewColIcon": + ret = tx.doSetAttrViewColumnIcon(op) + case "setAttrViewColDesc": + ret = tx.doSetAttrViewColumnDesc(op) + case "insertAttrViewBlock": + ret = tx.doInsertAttrViewBlock(op) + case "removeAttrViewBlock": + ret = tx.doRemoveAttrViewBlock(op) + case "addAttrViewCol": + ret = tx.doAddAttrViewColumn(op) + case "updateAttrViewCol": + ret = tx.doUpdateAttrViewColumn(op) + case "removeAttrViewCol": + ret = tx.doRemoveAttrViewColumn(op) + case "sortAttrViewRow": + ret = tx.doSortAttrViewRow(op) + case "sortAttrViewCol": + ret = tx.doSortAttrViewColumn(op) + case "sortAttrViewKey": + ret = tx.doSortAttrViewKey(op) + case "updateAttrViewCell": + ret = tx.doUpdateAttrViewCell(op) + case "updateAttrViewColOptions": + ret = tx.doUpdateAttrViewColOptions(op) + case "removeAttrViewColOption": + ret = tx.doRemoveAttrViewColOption(op) + case "updateAttrViewColOption": + ret = tx.doUpdateAttrViewColOption(op) + case "setAttrViewColOptionDesc": + ret = tx.doSetAttrViewColOptionDesc(op) + case "setAttrViewColCalc": + ret = tx.doSetAttrViewColCalc(op) + case "updateAttrViewColNumberFormat": + ret = tx.doUpdateAttrViewColNumberFormat(op) + case "replaceAttrViewBlock": + ret = tx.doReplaceAttrViewBlock(op) + case "updateAttrViewColTemplate": + ret = tx.doUpdateAttrViewColTemplate(op) + case "addAttrViewView": + ret = tx.doAddAttrViewView(op) + case "removeAttrViewView": + ret = tx.doRemoveAttrViewView(op) + case "setAttrViewViewName": + ret = tx.doSetAttrViewViewName(op) + case "setAttrViewViewIcon": + ret = tx.doSetAttrViewViewIcon(op) + case "setAttrViewViewDesc": + ret = tx.doSetAttrViewViewDesc(op) + case "duplicateAttrViewView": + ret = tx.doDuplicateAttrViewView(op) + case "sortAttrViewView": + ret = tx.doSortAttrViewView(op) + case "updateAttrViewColRelation": + ret = tx.doUpdateAttrViewColRelation(op) + case "updateAttrViewColRollup": + ret = tx.doUpdateAttrViewColRollup(op) + case "hideAttrViewName": + ret = tx.doHideAttrViewName(op) + case "setAttrViewColDate": + ret = tx.doSetAttrViewColDate(op) + case "unbindAttrViewBlock": + ret = tx.doUnbindAttrViewBlock(op) + case "duplicateAttrViewKey": + ret = tx.doDuplicateAttrViewKey(op) + case "setAttrViewCoverFrom": + ret = tx.doSetAttrViewCoverFrom(op) + case "setAttrViewCoverFromAssetKeyID": + ret = tx.doSetAttrViewCoverFromAssetKeyID(op) + case "setAttrViewCardSize": + ret = tx.doSetAttrViewCardSize(op) + case "setAttrViewFitImage": + ret = tx.doSetAttrViewFitImage(op) + case "setAttrViewShowIcon": + ret = tx.doSetAttrViewShowIcon(op) + case "setAttrViewWrapField": + ret = tx.doSetAttrViewWrapField(op) + case "changeAttrViewLayout": + ret = tx.doChangeAttrViewLayout(op) + case "setAttrViewBlockView": + ret = tx.doSetAttrViewBlockView(op) + case "setAttrViewCardAspectRatio": + ret = tx.doSetAttrViewCardAspectRatio(op) + case "setAttrViewGroup": + ret = tx.doSetAttrViewGroup(op) + case "hideAttrViewGroup": + ret = tx.doHideAttrViewGroup(op) + case "foldAttrViewGroup": + ret = tx.doFoldAttrViewGroup(op) + case "syncAttrViewTableColWidth": + ret = tx.doSyncAttrViewTableColWidth(op) + case "removeAttrViewGroup": + ret = tx.doRemoveAttrViewGroup(op) + case "sortAttrViewGroup": + ret = tx.doSortAttrViewGroup(op) + } - if nil != ret { - tx.rollback() - return + if nil != ret { + tx.rollback() + return + } } } @@ -324,6 +327,42 @@ func performTx(tx *Transaction) (ret *TxErr) { 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) { var err error id := operation.ID @@ -980,6 +1019,127 @@ func syncDelete2AttributeView(node *ast.Node) (changedAvIDs []string) { 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) { var bt *treenode.BlockTree 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, "") subTree := tx.luteEngine.BlockDOM2Tree(data) - - 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 - }) - } + tx.processGlobalAssets(subTree) insertedNode := subTree.Root.FirstChild if nil == insertedNode { @@ -1189,6 +1314,47 @@ func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) { 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) { id := operation.ID tree, err := tx.loadTree(id)