siyuan/kernel/model/transaction.go

1254 lines
35 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SiYuan - Build Your Eternal Digital Garden
// 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"
"os"
"path/filepath"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
util2 "github.com/88250/lute/util"
"github.com/emirpasic/gods/sets/hashset"
"github.com/siyuan-note/siyuan/kernel/cache"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
var (
ErrNotFullyBoot = errors.New("the kernel has not been fully booted, please try again later")
)
var writingDataLock = sync.Mutex{}
func IsFoldHeading(transactions *[]*Transaction) bool {
if 1 == len(*transactions) && 1 == len((*transactions)[0].DoOperations) {
if op := (*transactions)[0].DoOperations[0]; "foldHeading" == op.Action {
return true
}
}
return false
}
func IsUnfoldHeading(transactions *[]*Transaction) bool {
if 1 == len(*transactions) && 1 == len((*transactions)[0].DoOperations) {
if op := (*transactions)[0].DoOperations[0]; "unfoldHeading" == op.Action {
return true
}
}
return false
}
func IsSetAttrs(transactions *[]*Transaction) *Operation {
if 1 == len(*transactions) && 1 == len((*transactions)[0].DoOperations) {
if op := (*transactions)[0].DoOperations[0]; "setAttrs" == op.Action {
return op
}
}
return nil
}
const txFixDelay = 10
var (
txQueue []*Transaction
txQueueLock = sync.Mutex{}
txDelay = txFixDelay
currentTx *Transaction
)
func WaitForWritingFiles() {
var printLog bool
var lastPrintLog bool
for i := 0; isWritingFiles(); i++ {
time.Sleep(5 * time.Millisecond)
if 2000 < i && !printLog { // 10s 后打日志
util.LogWarnf("file is writing: \n%s", util.ShortStack())
printLog = true
}
if 12000 < i && !lastPrintLog { // 60s 后打日志
util.LogWarnf("file is still writing")
lastPrintLog = true
}
}
}
func isWritingFiles() bool {
time.Sleep(time.Duration(txDelay+5) * time.Millisecond)
if 0 < len(txQueue) || util.IsMutexLocked(&txQueueLock) {
return true
}
return nil != currentTx
}
func AutoFlushTx() {
for {
flushTx()
time.Sleep(time.Duration(txDelay) * time.Millisecond)
}
}
func flushTx() {
writingDataLock.Lock()
defer writingDataLock.Unlock()
defer util.Recover()
currentTx = mergeTx()
start := time.Now()
if txErr := performTx(currentTx); nil != txErr {
switch txErr.code {
case TxErrCodeBlockNotFound:
util.PushTxErr("Transaction failed", txErr.code, nil)
return
case TxErrCodeUnableLockFile:
util.PushTxErr(Conf.Language(76), txErr.code, txErr.id)
return
default:
util.LogFatalf("transaction failed: %s", txErr.msg)
}
}
elapsed := time.Now().Sub(start).Milliseconds()
if 0 < len(currentTx.DoOperations) {
if 2000 < elapsed {
util.LogWarnf("tx [%dms]", elapsed)
}
}
currentTx = nil
}
func mergeTx() (ret *Transaction) {
txQueueLock.Lock()
defer txQueueLock.Unlock()
ret = &Transaction{}
var doOps []*Operation
for _, tx := range txQueue {
for _, op := range tx.DoOperations {
if l := len(doOps); 0 < l {
lastOp := doOps[l-1]
if "update" == lastOp.Action && "update" == op.Action && lastOp.ID == op.ID { // 连续相同的更新操作
lastOp.discard = true
}
}
doOps = append(doOps, op)
}
}
for _, op := range doOps {
if !op.discard {
ret.DoOperations = append(ret.DoOperations, op)
}
}
txQueue = nil
return
}
func PerformTransactions(transactions *[]*Transaction) (err error) {
if !util.IsBooted() {
err = ErrNotFullyBoot
return
}
txQueueLock.Lock()
txQueue = append(txQueue, *transactions...)
txQueueLock.Unlock()
return
}
const (
TxErrCodeBlockNotFound = 0
TxErrCodeUnableLockFile = 1
TxErrCodeWriteTree = 2
)
type TxErr struct {
code int
msg string
id string
}
func performTx(tx *Transaction) (ret *TxErr) {
if 1 > len(tx.DoOperations) {
txDelay -= 1000
if 100*txFixDelay < txDelay {
txDelay = txDelay / 2
} else if 0 > txDelay {
txDelay = txFixDelay
}
return
}
//os.MkdirAll("pprof", 0755)
//cpuProfile, _ := os.Create("pprof/cpu_profile_tx")
//pprof.StartCPUProfile(cpuProfile)
//defer pprof.StopCPUProfile()
var err error
if err = tx.begin(); nil != err {
if strings.Contains(err.Error(), "database is closed") {
return
}
util.LogErrorf("begin tx failed: %s", err)
ret = &TxErr{msg: err.Error()}
return
}
if isLargePaste(tx) {
if ret = tx.doLargeInsert(); nil != ret {
tx.rollback()
return
}
if cr := tx.commit(); nil != cr {
util.LogErrorf("commit tx failed: %s", cr)
return &TxErr{msg: cr.Error()}
}
return
}
start := time.Now()
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 "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)
}
if nil != ret {
tx.rollback()
return
}
}
if cr := tx.commit(); nil != cr {
util.LogErrorf("commit tx failed: %s", cr)
return &TxErr{msg: cr.Error()}
}
elapsed := int(time.Now().Sub(start).Milliseconds())
txDelay = 10 + elapsed
if 1000*10 < txDelay {
txDelay = 1000 * 10
}
return
}
func (tx *Transaction) doMove(operation *Operation) (ret *TxErr) {
var err error
id := operation.ID
srcTree, err := tx.loadTree(id)
if nil != err {
util.LogErrorf("load tree [id=%s] failed: %s", id, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
srcNode := treenode.GetNodeInTree(srcTree, id)
if nil == srcNode {
util.LogErrorf("get node [%s] in tree [%s] failed", id, srcTree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
var headingChildren []*ast.Node
if isMovingFoldHeading := ast.NodeHeading == srcNode.Type && "1" == srcNode.IALAttr("fold"); isMovingFoldHeading {
headingChildren = treenode.FoldedHeadingChildren(srcNode)
}
var srcEmptyList *ast.Node
if ast.NodeListItem == srcNode.Type && srcNode.Parent.FirstChild == srcNode && srcNode.Parent.LastChild == srcNode {
// 列表中唯一的列表项被移除后,该列表就为空了
srcEmptyList = srcNode.Parent
}
targetPreviousID := operation.PreviousID
targetParentID := operation.ParentID
if "" != targetPreviousID {
if id == targetPreviousID {
return
}
var targetTree *parse.Tree
targetTree, err = tx.loadTree(targetPreviousID)
if nil != err {
util.LogErrorf("load tree [id=%s] failed: %s", targetPreviousID, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetPreviousID}
}
isSameTree := srcTree.ID == targetTree.ID
if isSameTree {
targetTree = srcTree
}
targetNode := treenode.GetNodeInTree(targetTree, targetPreviousID)
if nil == targetNode {
util.LogErrorf("get node [%s] in tree [%s] failed", targetPreviousID, targetTree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetPreviousID}
}
if ast.NodeHeading == targetNode.Type && "1" == targetNode.IALAttr("fold") {
targetChildren := treenode.FoldedHeadingChildren(targetNode)
if l := len(targetChildren); 0 < l {
targetNode = targetChildren[l-1]
}
}
for i := len(headingChildren) - 1; -1 < i; i-- {
c := headingChildren[i]
targetNode.InsertAfter(c)
}
targetNode.InsertAfter(srcNode)
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
refreshUpdated(srcNode)
refreshUpdated(srcTree.Root)
if err = tx.writeTree(srcTree); nil != err {
return
}
if !isSameTree {
if err = tx.writeTree(targetTree); nil != err {
return
}
}
return
}
if id == targetParentID {
return
}
targetTree, err := tx.loadTree(targetParentID)
if nil != err {
util.LogErrorf("load tree [id=%s] failed: %s", targetParentID, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetParentID}
}
isSameTree := srcTree.ID == targetTree.ID
if isSameTree {
targetTree = srcTree
}
targetNode := treenode.GetNodeInTree(targetTree, targetParentID)
if nil == targetNode {
util.LogErrorf("get node [%s] in tree [%s] failed", targetParentID, targetTree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetParentID}
}
processed := false
if ast.NodeSuperBlock == targetNode.Type {
// 在布局节点后插入
targetNode = targetNode.FirstChild.Next
for i := len(headingChildren) - 1; -1 < i; i-- {
c := headingChildren[i]
targetNode.InsertAfter(c)
}
targetNode.InsertAfter(srcNode)
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
processed = true
} else if ast.NodeListItem == targetNode.Type {
if 3 == targetNode.ListData.Typ {
// 在任务列表标记节点后插入
targetNode = targetNode.FirstChild
for i := len(headingChildren) - 1; -1 < i; i-- {
c := headingChildren[i]
targetNode.InsertAfter(c)
}
targetNode.InsertAfter(srcNode)
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
processed = true
}
}
if !processed {
for i := len(headingChildren) - 1; -1 < i; i-- {
c := headingChildren[i]
targetNode.PrependChild(c)
}
targetNode.PrependChild(srcNode)
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
}
refreshUpdated(srcNode)
refreshUpdated(srcTree.Root)
if err = tx.writeTree(srcTree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
if !isSameTree {
if err = tx.writeTree(targetTree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
}
return
}
func (tx *Transaction) doPrependInsert(operation *Operation) (ret *TxErr) {
var err error
block := treenode.GetBlockTree(operation.ParentID)
if nil == block {
msg := fmt.Sprintf("not found parent block [id=%s]", operation.ParentID)
util.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
}
tree, err := tx.loadTree(block.ID)
if filesys.ErrUnableLockFile == err {
return &TxErr{code: TxErrCodeUnableLockFile, msg: err.Error(), id: block.ID}
}
if nil != err {
msg := fmt.Sprintf("load tree [id=%s] failed: %s", block.ID, err)
util.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
}
data := strings.ReplaceAll(operation.Data.(string), util2.FrontEndCaret, "")
luteEngine := NewLute()
subTree := luteEngine.BlockDOM2Tree(data)
insertedNode := subTree.Root.FirstChild
if nil == insertedNode {
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.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, operation.ParentID)
if nil == node {
util.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
}
isContainer := node.IsContainerBlock()
for i := len(remains) - 1; 0 <= i; i-- {
remain := remains[i]
if isContainer {
if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
node.FirstChild.InsertAfter(remain)
} else if ast.NodeSuperBlock == node.Type {
node.FirstChild.Next.InsertAfter(remain)
} else {
node.PrependChild(remain)
}
} else {
node.InsertAfter(remain)
}
}
if isContainer {
if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
node.FirstChild.InsertAfter(insertedNode)
} else if ast.NodeSuperBlock == node.Type {
node.FirstChild.Next.InsertAfter(insertedNode)
} else {
node.PrependChild(insertedNode)
}
} else {
node.InsertAfter(insertedNode)
}
createdUpdated(insertedNode)
tx.nodes[insertedNode.ID] = insertedNode
if err = tx.writeTree(tree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: block.ID}
}
operation.ID = insertedNode.ID
operation.ParentID = insertedNode.Parent.ID
// 将 prependInsert 转换为 insert 推送
operation.Action = "insert"
if nil != insertedNode.Previous {
operation.PreviousID = insertedNode.Previous.ID
}
return
}
func (tx *Transaction) doAppendInsert(operation *Operation) (ret *TxErr) {
var err error
block := treenode.GetBlockTree(operation.ParentID)
if nil == block {
msg := fmt.Sprintf("not found parent block [id=%s]", operation.ParentID)
util.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
}
tree, err := tx.loadTree(block.ID)
if filesys.ErrUnableLockFile == err {
return &TxErr{code: TxErrCodeUnableLockFile, msg: err.Error(), id: block.ID}
}
if nil != err {
msg := fmt.Sprintf("load tree [id=%s] failed: %s", block.ID, err)
util.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
}
data := strings.ReplaceAll(operation.Data.(string), util2.FrontEndCaret, "")
luteEngine := NewLute()
subTree := luteEngine.BlockDOM2Tree(data)
insertedNode := subTree.Root.FirstChild
if nil == insertedNode {
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.ID}
}
if "" == insertedNode.ID {
insertedNode.ID = ast.NewNodeID()
insertedNode.SetIALAttr("id", insertedNode.ID)
}
var toInserts []*ast.Node
for toInsert := insertedNode; nil != toInsert; toInsert = toInsert.Next {
if ast.NodeKramdownBlockIAL != toInsert.Type {
if "" == toInsert.ID {
toInsert.ID = ast.NewNodeID()
toInsert.SetIALAttr("id", toInsert.ID)
}
toInserts = append(toInserts, toInsert)
}
}
node := treenode.GetNodeInTree(tree, operation.ParentID)
if nil == node {
util.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
}
isContainer := node.IsContainerBlock()
for i := 0; i < len(toInserts); i++ {
toInsert := toInserts[i]
if isContainer {
if ast.NodeSuperBlock == node.Type {
node.LastChild.InsertBefore(toInsert)
} else {
node.AppendChild(toInsert)
}
} else {
node.InsertAfter(toInsert)
}
}
createdUpdated(insertedNode)
tx.nodes[insertedNode.ID] = insertedNode
if err = tx.writeTree(tree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: block.ID}
}
operation.ID = insertedNode.ID
operation.ParentID = insertedNode.Parent.ID
// 将 appendInsert 转换为 insert 推送
operation.Action = "insert"
if nil != insertedNode.Previous {
operation.PreviousID = insertedNode.Previous.ID
}
return
}
func (tx *Transaction) doAppend(operation *Operation) (ret *TxErr) {
var err error
id := operation.ID
srcTree, err := tx.loadTree(id)
if nil != err {
util.LogErrorf("load tree [id=%s] failed: %s", id, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
srcNode := treenode.GetNodeInTree(srcTree, id)
if nil == srcNode {
util.LogErrorf("get node [%s] in tree [%s] failed", id, srcTree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
if ast.NodeDocument == srcNode.Type {
util.LogWarnf("can't append a root to another root")
return
}
var headingChildren []*ast.Node
if isMovingFoldHeading := ast.NodeHeading == srcNode.Type && "1" == srcNode.IALAttr("fold"); isMovingFoldHeading {
headingChildren = treenode.FoldedHeadingChildren(srcNode)
}
var srcEmptyList, targetNewList *ast.Node
if ast.NodeListItem == srcNode.Type {
targetNewListID := ast.NewNodeID()
targetNewList = &ast.Node{ID: targetNewListID, Type: ast.NodeList, ListData: &ast.ListData{Typ: srcNode.ListData.Typ}}
targetNewList.SetIALAttr("id", targetNewListID)
if srcNode.Parent.FirstChild == srcNode && srcNode.Parent.LastChild == srcNode {
// 列表中唯一的列表项被移除后,该列表就为空了
srcEmptyList = srcNode.Parent
}
}
targetRootID := operation.ParentID
if id == targetRootID {
util.LogWarnf("target root id is nil")
return
}
targetTree, err := tx.loadTree(targetRootID)
if nil != err {
util.LogErrorf("load tree [id=%s] failed: %s", targetRootID, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetRootID}
}
isSameTree := srcTree.ID == targetTree.ID
if isSameTree {
targetTree = srcTree
}
targetRoot := targetTree.Root
if nil != targetNewList {
if nil != targetRoot.LastChild {
if ast.NodeList != targetRoot.LastChild.Type {
targetNewList.AppendChild(srcNode)
targetRoot.AppendChild(targetNewList)
} else {
targetRoot.LastChild.AppendChild(srcNode)
}
} else {
targetRoot.AppendChild(srcNode)
}
} else {
targetRoot.AppendChild(srcNode)
}
for _, c := range headingChildren {
targetRoot.AppendChild(c)
}
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
if err = tx.writeTree(srcTree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
if !isSameTree {
if err = tx.writeTree(targetTree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
}
return
}
// isLargePaste 用于判断 transaction 是否是粘贴大量数据。
// 粘贴大量数据时:
// 1. 最后一个 op 是 delete 或者 insert且 id 是之前 ops 的 previous id
// 2. 除了最后一个 op之前的所有 op 都是 insert且 previous id 一样
func isLargePaste(transaction *Transaction) bool {
length := len(transaction.DoOperations)
if 16 > length {
return false
}
lastOp := transaction.DoOperations[length-1]
if "delete" != lastOp.Action && "insert" != lastOp.Action {
return false
}
ops := transaction.DoOperations[:length-1]
previousID := ops[0].PreviousID
if "insert" == lastOp.Action {
ops = transaction.DoOperations
} else {
if previousID != lastOp.ID {
return false
}
}
for _, op := range ops {
if "insert" != op.Action {
return false
}
if previousID != op.PreviousID {
return false
}
}
return true
}
func (tx *Transaction) doLargeInsert() (ret *TxErr) {
var err error
operations := tx.DoOperations
previousID := operations[0].PreviousID
parentBlock := treenode.GetBlockTree(previousID)
if nil == parentBlock {
parentID := operations[0].ParentID
parentBlock = treenode.GetBlockTree(parentID)
if nil == parentBlock {
util.LogErrorf("not found previous block [id=%s]", parentID)
return &TxErr{code: TxErrCodeBlockNotFound, id: parentID}
}
}
id := parentBlock.ID
tree, err := tx.loadTree(id)
if filesys.ErrUnableLockFile == err {
return &TxErr{code: TxErrCodeUnableLockFile, msg: err.Error(), id: id}
}
if nil != err {
util.LogErrorf("load tree [id=%s] failed: %s", id, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
luteEngine := NewLute()
length := len(operations)
var inserts []*Operation
if "insert" == operations[length-1].Action {
inserts = operations
} else {
inserts = operations[:length-1]
}
for _, op := range inserts {
data := strings.ReplaceAll(op.Data.(string), util2.FrontEndCaret, "")
subTree := luteEngine.BlockDOM2Tree(data)
insertedNode := subTree.Root.FirstChild
if "" == insertedNode.ID {
insertedNode.ID = ast.NewNodeID()
insertedNode.SetIALAttr("id", insertedNode.ID)
}
node := treenode.GetNodeInTree(tree, previousID)
if nil == node {
util.LogErrorf("get node [%s] in tree [%s] failed", previousID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
}
if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
insertedNode = insertedNode.FirstChild
}
node.InsertAfter(insertedNode)
createdUpdated(insertedNode)
tx.nodes[insertedNode.ID] = insertedNode
}
if "delete" == operations[length-1].Action {
node := treenode.GetNodeInTree(tree, previousID)
if nil == node {
util.LogErrorf("get node [%s] in tree [%s] failed", previousID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
}
node.Unlink()
delete(tx.nodes, node.ID)
}
if err = tx.writeTree(tree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
return
}
func (tx *Transaction) doDelete(operation *Operation) (ret *TxErr) {
// util.LogInfof("commit delete [%+v]", operation)
var err error
id := operation.ID
tree, err := tx.loadTree(id)
if filesys.ErrUnableLockFile == err {
return &TxErr{code: TxErrCodeUnableLockFile, msg: err.Error(), id: id}
}
if ErrBlockNotFound == err {
return nil // move 以后这里会空,算作正常情况
}
if nil != err {
msg := fmt.Sprintf("load tree [id=%s] failed: %s", id, err)
util.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return nil // move 以后的情况,列表项移动导致的状态异常 https://github.com/siyuan-note/insider/issues/961
}
parent := node.Parent
if nil != node.Next && ast.NodeKramdownBlockIAL == node.Next.Type && bytes.Contains(node.Next.Tokens, []byte(node.ID)) {
// 列表块撤销状态异常 https://github.com/siyuan-note/siyuan/issues/3985
node.Next.Unlink()
}
node.Unlink()
if nil != parent && ast.NodeListItem == parent.Type && nil == parent.FirstChild {
// 保持空列表项
node.FirstChild = nil
parent.AppendChild(node)
}
treenode.RemoveBlockTree(node.ID)
delete(tx.nodes, node.ID)
if err = tx.writeTree(tree); nil != err {
return
}
return
}
func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
var err error
opParentID := operation.ParentID
block := treenode.GetBlockTree(opParentID)
if nil == block {
block = treenode.GetBlockTree(operation.PreviousID)
if nil == block {
msg := fmt.Sprintf("not found previous block [id=%s]", operation.PreviousID)
util.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.PreviousID}
}
}
tree, err := tx.loadTree(block.ID)
if filesys.ErrUnableLockFile == err {
return &TxErr{code: TxErrCodeUnableLockFile, msg: err.Error(), id: block.ID}
}
if nil != err {
msg := fmt.Sprintf("load tree [id=%s] failed: %s", block.ID, err)
util.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
}
data := strings.ReplaceAll(operation.Data.(string), util2.FrontEndCaret, "")
luteEngine := NewLute()
subTree := luteEngine.BlockDOM2Tree(data)
p := block.Path
assets := getAssetsDir(filepath.Join(util.DataDir, block.BoxID), filepath.Dir(filepath.Join(util.DataDir, block.BoxID, p)))
isGlobalAssets := strings.HasPrefix(assets, filepath.Join(util.DataDir, "assets"))
if !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 {
util.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(assets, filepath.Base(assetPath))
if e = os.Rename(assetPath, targetP); nil != err {
util.LogErrorf("copy path of asset from [%s] to [%s] failed: %s", assetPath, targetP, err)
return ast.WalkContinue
}
}
return ast.WalkContinue
})
}
insertedNode := subTree.Root.FirstChild
if nil == insertedNode {
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.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)
}
var node *ast.Node
previousID := operation.PreviousID
if "" != previousID {
node = treenode.GetNodeInTree(tree, previousID)
if nil == node {
util.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.FoldedHeadingChildren(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)
} else {
node = treenode.GetNodeInTree(tree, operation.ParentID)
if nil == node {
util.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
}
if ast.NodeSuperBlock == node.Type {
// 在布局节点后插入
node.FirstChild.Next.InsertAfter(insertedNode)
} else {
if ast.NodeList == insertedNode.Type && nil != insertedNode.FirstChild && operation.ID == insertedNode.FirstChild.ID && operation.ID != insertedNode.ID {
// 将一个列表项移动到另一个列表的第一项时 https://github.com/siyuan-note/siyuan/issues/2341
insertedNode = insertedNode.FirstChild
}
if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
// 在任务列表标记节点后插入
node.FirstChild.InsertAfter(insertedNode)
for _, remain := range remains {
node.FirstChild.InsertAfter(remain)
}
} else {
for i := len(remains) - 1; 0 <= i; i-- {
remain := remains[i]
node.PrependChild(remain)
}
node.PrependChild(insertedNode)
}
}
}
createdUpdated(insertedNode)
tx.nodes[insertedNode.ID] = insertedNode
if err = tx.writeTree(tree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: block.ID}
}
operation.ID = insertedNode.ID
operation.ParentID = insertedNode.Parent.ID
return
}
func (tx *Transaction) doUpdate(operation *Operation) (ret *TxErr) {
id := operation.ID
tree, err := tx.loadTree(id)
if filesys.ErrUnableLockFile == err {
return &TxErr{code: TxErrCodeUnableLockFile, msg: err.Error(), id: id}
}
if nil != err {
util.LogErrorf("load tree [id=%s] failed: %s", id, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
data := strings.ReplaceAll(operation.Data.(string), util2.FrontEndCaret, "")
if "" == data {
util.LogErrorf("update data is nil")
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
luteEngine := NewLute()
subTree := luteEngine.BlockDOM2Tree(data)
subTree.ID, subTree.Box, subTree.Path = tree.ID, tree.Box, tree.Path
oldNode := treenode.GetNodeInTree(tree, id)
if nil == oldNode {
util.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
}
var unlinks []*ast.Node
ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeInlineMath == n.Type {
content := n.ChildByType(ast.NodeInlineMathContent)
if nil == content || 1 > len(content.Tokens) {
// 剔除空白的行级公式
unlinks = append(unlinks, n)
}
} else if ast.NodeBlockRefID == n.Type {
sql.CacheRef(subTree, n)
}
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
updatedNode := subTree.Root.FirstChild
if nil == updatedNode {
util.LogErrorf("get fist node in sub tree [%s] failed", subTree.Root.ID)
return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
}
if ast.NodeList == updatedNode.Type && ast.NodeList == oldNode.Parent.Type {
updatedNode = updatedNode.FirstChild
}
if oldNode.IsContainerBlock() {
// 更新容器块的话需要考虑其子块中可能存在的折叠标题,需要把这些折叠标题的下方块移动到新节点下面
treenode.MoveFoldHeading(updatedNode, oldNode)
}
cache.PutBlockIAL(updatedNode.ID, parse.IAL2Map(updatedNode.KramdownIAL))
// 替换为新节点
oldNode.InsertAfter(updatedNode)
oldNode.Unlink()
createdUpdated(updatedNode)
tx.nodes[updatedNode.ID] = updatedNode
if err = tx.writeTree(tree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
return
}
func (tx *Transaction) doCreate(operation *Operation) (ret *TxErr) {
tree := operation.Data.(*parse.Tree)
tx.writeTree(tree)
return
}
func refreshUpdated(n *ast.Node) {
updated := util.CurrentTimeSecondsStr()
n.SetIALAttr("updated", updated)
parents := treenode.ParentNodes(n)
for _, parent := range parents { // 更新所有父节点的更新时间字段
parent.SetIALAttr("updated", updated)
}
}
func createdUpdated(n *ast.Node) {
ast.Walk(n, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || "" == n.ID {
return ast.WalkContinue
}
created := util.TimeFromID(n.ID)
updated := n.IALAttr("updated")
if "" == updated {
updated = created
}
if updated < created {
updated = created // 复制粘贴块后创建时间小于更新时间 https://github.com/siyuan-note/siyuan/issues/3624
}
n.SetIALAttr("updated", updated)
parents := treenode.ParentNodes(n)
for _, parent := range parents { // 更新所有父节点的更新时间字段
parent.SetIALAttr("updated", updated)
}
return ast.WalkContinue
})
}
type Operation struct {
Action string `json:"action"`
Data interface{} `json:"data"`
ID string `json:"id"`
ParentID string `json:"parentID"`
PreviousID string `json:"previousID"`
RetData interface{} `json:"retData"`
discard bool // 用于标识是否在事务合并中丢弃
}
type Transaction struct {
DoOperations []*Operation `json:"doOperations"`
UndoOperations []*Operation `json:"undoOperations"`
trees map[string]*parse.Tree
nodes map[string]*ast.Node
}
func (tx *Transaction) begin() (err error) {
if nil != err {
return
}
tx.trees = map[string]*parse.Tree{}
tx.nodes = map[string]*ast.Node{}
return
}
func (tx *Transaction) commit() (err error) {
for _, tree := range tx.trees {
if err = writeJSONQueue(tree); nil != err {
return
}
}
refreshDynamicRefText(tx.nodes, tx.trees)
IncWorkspaceDataVer()
tx.trees = nil
return
}
func (tx *Transaction) rollback() {
tx.trees, tx.nodes = nil, nil
return
}
func (tx *Transaction) loadTree(id string) (ret *parse.Tree, err error) {
var rootID, box, p string
bt := treenode.GetBlockTree(id)
if nil == bt {
return nil, ErrBlockNotFound
}
rootID = bt.RootID
box = bt.BoxID
p = bt.Path
ret = tx.trees[rootID]
if nil != ret {
return
}
ret, err = LoadTree(box, p)
if nil != err {
return
}
tx.trees[rootID] = ret
return
}
func (tx *Transaction) writeTree(tree *parse.Tree) (err error) {
tx.trees[tree.ID] = tree
treenode.ReindexBlockTree(tree)
return
}
func refreshDynamicRefText(updatedDefNodes map[string]*ast.Node, updatedTrees map[string]*parse.Tree) {
// 这个实现依赖了数据库缓存,导致外部调用时可能需要阻塞等待数据库写入后才能获取到 refs
// 比如通过块引创建文档后立即重命名文档,这时引用关系还没有入库,所以重命名查询不到引用关系,最终导致动态锚文本设置失败
// 引用文档时锚文本没有跟随文档重命名 https://github.com/siyuan-note/siyuan/issues/4193
// 解决方案是将重命名通过协程异步调用,详见 RenameDoc 函数
treeRefNodeIDs := map[string]*hashset.Set{}
for _, updateNode := range updatedDefNodes {
refs := sql.GetRefsCacheByDefID(updateNode.ID)
if nil != updateNode.Parent && ast.NodeDocument != updateNode.Parent.Type &&
updateNode.Parent.IsContainerBlock() && (updateNode == treenode.FirstLeafBlock(updateNode.Parent)) { // 容器块下第一个子块
var parentRefs []*sql.Ref
if ast.NodeListItem == updateNode.Parent.Type { // 引用列表块时动态锚文本未跟随定义块内容变动 https://github.com/siyuan-note/siyuan/issues/4393
parentRefs = sql.GetRefsCacheByDefID(updateNode.Parent.Parent.ID)
updatedDefNodes[updateNode.Parent.ID] = updateNode.Parent
updatedDefNodes[updateNode.Parent.Parent.ID] = updateNode.Parent.Parent
} else {
parentRefs = sql.GetRefsCacheByDefID(updateNode.Parent.ID)
updatedDefNodes[updateNode.Parent.ID] = updateNode.Parent
}
if 0 < len(parentRefs) {
refs = append(refs, parentRefs...)
}
}
for _, ref := range refs {
if refIDs, ok := treeRefNodeIDs[ref.RootID]; !ok {
refIDs = hashset.New()
refIDs.Add(ref.BlockID)
treeRefNodeIDs[ref.RootID] = refIDs
} else {
refIDs.Add(ref.BlockID)
}
}
}
for refTreeID, refNodeIDs := range treeRefNodeIDs {
refTree, ok := updatedTrees[refTreeID]
if !ok {
var err error
refTree, err = loadTreeByBlockID(refTreeID)
if nil != err {
continue
}
}
var refTreeChanged bool
ast.Walk(refTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if n.IsBlock() && refNodeIDs.Contains(n.ID) {
changed := updateRefText(n, updatedDefNodes)
if !refTreeChanged && changed {
refTreeChanged = true
}
return ast.WalkContinue
}
return ast.WalkContinue
})
if refTreeChanged {
indexWriteJSONQueue(refTree)
}
}
}
func updateRefText(refNode *ast.Node, changedDefNodes map[string]*ast.Node) (changed bool) {
ast.Walk(refNode, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeBlockRef == n.Type && nil != n.ChildByType(ast.NodeBlockRefDynamicText) {
defIDNode := n.ChildByType(ast.NodeBlockRefID)
if nil == defIDNode {
return ast.WalkSkipChildren
}
defID := defIDNode.TokensStr()
defNode := changedDefNodes[defID]
if nil == defNode {
return ast.WalkSkipChildren
}
if ast.NodeDocument != defNode.Type && defNode.IsContainerBlock() {
defNode = treenode.FirstLeafBlock(defNode)
}
defContent := renderBlockText(defNode)
if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(defContent) {
defContent = gulu.Str.SubStr(defContent, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..."
}
treenode.SetDynamicBlockRefText(n, defContent)
changed = true
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
return
}