From 6c70144d7c747677e6e98ba19586b3e21fa9fbd8 Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Wed, 28 Jan 2026 18:42:50 +0800 Subject: [PATCH] :art: Supports cleaning up unreferenced databases https://github.com/siyuan-note/siyuan/issues/11569 Signed-off-by: Daniel <845765@qq.com> --- kernel/api/av.go | 10 ++++ kernel/model/attribute_view.go | 92 ++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/kernel/api/av.go b/kernel/api/av.go index 68ef65702..e4ebc6f0d 100644 --- a/kernel/api/av.go +++ b/kernel/api/av.go @@ -28,6 +28,16 @@ import ( "github.com/siyuan-note/siyuan/kernel/util" ) +func removeUnusedAttributeViews(c *gin.Context) { + ret := gulu.Ret.NewResult() + defer c.JSON(http.StatusOK, ret) + + paths := model.RemoveUnusedAttributeViews() + ret.Data = map[string]interface{}{ + "paths": paths, + } +} + func getUnusedAttributeViews(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret) diff --git a/kernel/model/attribute_view.go b/kernel/model/attribute_view.go index 1f9066a8e..b1ede8952 100644 --- a/kernel/model/attribute_view.go +++ b/kernel/model/attribute_view.go @@ -29,6 +29,7 @@ import ( "time" "unicode/utf8" + "github.com/88250/go-humanize" "github.com/88250/gulu" "github.com/88250/lute/ast" "github.com/88250/lute/parse" @@ -45,6 +46,58 @@ import ( "github.com/xrash/smetrics" ) +func RemoveUnusedAttributeViews() (ret []string) { + ret = []string{} + var size int64 + + msgId := util.PushMsg(Conf.Language(100), 30*1000) + defer func() { + msg := fmt.Sprintf(Conf.Language(91), len(ret), humanize.BytesCustomCeil(uint64(size), 2)) + util.PushUpdateMsg(msgId, msg, 7000) + }() + + unusedAttributeViews := UnusedAttributeViews() + + historyDir, err := GetHistoryDir(HistoryOpClean) + if err != nil { + logging.LogErrorf("get history dir failed: %s", err) + return + } + + for _, unusedAvID := range unusedAttributeViews { + srcPath := filepath.Join(util.DataDir, "storage", "av", unusedAvID+".json") + if filelock.IsExist(srcPath) { + historyPath := filepath.Join(historyDir, "storage", "av", unusedAvID+".json") + if err = filelock.Copy(srcPath, historyPath); err != nil { + return + } + } + } + + for _, unusedAvID := range unusedAttributeViews { + absPath := filepath.Join(util.DataDir, "storage", "av", unusedAvID+".json") + if filelock.IsExist(absPath) { + info, statErr := os.Stat(absPath) + if statErr == nil { + size += info.Size() + } + + if removeErr := filelock.RemoveWithoutFatal(absPath); removeErr != nil { + logging.LogErrorf("remove unused av [%s] failed: %s", absPath, removeErr) + util.PushErrMsg(fmt.Sprintf("%s", removeErr), 7000) + return + } + } + ret = append(ret, absPath) + } + if 0 < len(ret) { + IncSync() + } + + indexHistoryDir(filepath.Base(historyDir), util.NewLute()) + return +} + func UnusedAttributeViews() (ret []string) { defer logging.Recover() ret = []string{} @@ -54,7 +107,7 @@ func UnusedAttributeViews() (ret []string) { return } - referencedAvIDs := map[string]bool{} + docReferencedAvIDs := map[string]bool{} luteEngine := util.NewLute() boxes := Conf.GetBoxes() for _, box := range boxes { @@ -70,7 +123,7 @@ func UnusedAttributeViews() (ret []string) { } for _, tree := range trees { for _, id := range getAvIDs(tree, allAvIDs) { - referencedAvIDs[id] = true + docReferencedAvIDs[id] = true } } } @@ -78,11 +131,12 @@ func UnusedAttributeViews() (ret []string) { templateAvIDs := search.FindAllMatchedTargets(filepath.Join(util.DataDir, "templates"), allAvIDs) for _, id := range templateAvIDs { - referencedAvIDs[id] = true + docReferencedAvIDs[id] = true } + checkedAvIDs := map[string]bool{} for _, id := range allAvIDs { - if !referencedAvIDs[id] { + if !docReferencedAvIDs[id] && !isRelatedSrcAvDocReferenced(id, docReferencedAvIDs, checkedAvIDs) { ret = append(ret, id) } } @@ -91,6 +145,36 @@ func UnusedAttributeViews() (ret []string) { return } +func isRelatedSrcAvDocReferenced(destAvID string, docReferencedAvIDs, checkedAvIDs map[string]bool) bool { + if checkedAvIDs[destAvID] { + if docReferencedAvIDs[destAvID] { + return true + } + return false + } + checkedAvIDs[destAvID] = true + + srcAvIDs := av.GetSrcAvIDs(destAvID) + srcAvIDs = gulu.Str.RemoveElem(srcAvIDs, destAvID) // 忽略自身关联 + if 1 > len(srcAvIDs) { + return false + } + + for _, srcAvID := range srcAvIDs { + if docReferencedAvIDs[srcAvID] { + return true + } + } + + // 递归检查间接关联的 av + for _, srcAvID := range srcAvIDs { + if isRelatedSrcAvDocReferenced(srcAvID, docReferencedAvIDs, checkedAvIDs) { + return true + } + } + return false +} + func getAvIDs(tree *parse.Tree, allAvIDs []string) (ret []string) { ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { if !entering {