A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 320 lines 8.4 kB view raw
1// SiYuan - Refactor your thinking 2// Copyright (c) 2020-present, b3log.org 3// 4// This program is free software: you can redistribute it and/or modify 5// it under the terms of the GNU Affero General Public License as published by 6// the Free Software Foundation, either version 3 of the License, or 7// (at your option) any later version. 8// 9// This program is distributed in the hope that it will be useful, 10// but WITHOUT ANY WARRANTY; without even the implied warranty of 11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12// GNU Affero General Public License for more details. 13// 14// You should have received a copy of the GNU Affero General Public License 15// along with this program. If not, see <https://www.gnu.org/licenses/>. 16 17package model 18 19import ( 20 "errors" 21 "io/fs" 22 "os" 23 "path" 24 "path/filepath" 25 "strings" 26 "time" 27 28 "github.com/88250/lute" 29 "github.com/88250/lute/ast" 30 "github.com/88250/lute/parse" 31 "github.com/siyuan-note/dataparser" 32 "github.com/siyuan-note/filelock" 33 "github.com/siyuan-note/logging" 34 "github.com/siyuan-note/siyuan/kernel/av" 35 "github.com/siyuan-note/siyuan/kernel/filesys" 36 "github.com/siyuan-note/siyuan/kernel/sql" 37 "github.com/siyuan-note/siyuan/kernel/task" 38 "github.com/siyuan-note/siyuan/kernel/treenode" 39 "github.com/siyuan-note/siyuan/kernel/util" 40 "golang.org/x/time/rate" 41) 42 43func resetTree(tree *parse.Tree, titleSuffix string, removeAvBinding bool) { 44 tree.ID = ast.NewNodeID() 45 tree.Root.ID = tree.ID 46 title := tree.Root.IALAttr("title") 47 if "" != titleSuffix { 48 if t, parseErr := time.Parse("20060102150405", util.TimeFromID(tree.ID)); nil == parseErr { 49 titleSuffix += " " + t.Format("2006-01-02 15:04:05") 50 } else { 51 titleSuffix = "Duplicated " + time.Now().Format("2006-01-02 15:04:05") 52 } 53 titleSuffix = "(" + titleSuffix + ")" 54 titleSuffix = " " + titleSuffix 55 if Conf.language(16) == title { 56 titleSuffix = "" 57 } 58 } 59 tree.Root.SetIALAttr("id", tree.ID) 60 tree.Root.SetIALAttr("title", title+titleSuffix) 61 tree.Root.RemoveIALAttr("scroll") 62 p := path.Join(path.Dir(tree.Path), tree.ID) + ".sy" 63 tree.Path = p 64 tree.HPath = tree.HPath + " " + titleSuffix 65 66 // 收集所有引用 67 refIDs := map[string]string{} 68 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 69 if !entering || !treenode.IsBlockRef(n) { 70 return ast.WalkContinue 71 } 72 defID, _, _ := treenode.GetBlockRef(n) 73 if "" == defID { 74 return ast.WalkContinue 75 } 76 refIDs[defID] = "1" 77 return ast.WalkContinue 78 }) 79 80 // 重置块 ID 81 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 82 if !entering || ast.NodeDocument == n.Type { 83 return ast.WalkContinue 84 } 85 if n.IsBlock() && "" != n.ID { 86 newID := ast.NewNodeID() 87 if "1" == refIDs[n.ID] { 88 // 如果是文档自身的内部引用 89 refIDs[n.ID] = newID 90 } 91 n.ID = newID 92 n.SetIALAttr("id", n.ID) 93 } 94 return ast.WalkContinue 95 }) 96 97 // 重置内部引用 98 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 99 if !entering || !treenode.IsBlockRef(n) { 100 return ast.WalkContinue 101 } 102 defID, _, _ := treenode.GetBlockRef(n) 103 if "" == defID { 104 return ast.WalkContinue 105 } 106 if "1" != refIDs[defID] { 107 if ast.NodeTextMark == n.Type { 108 n.TextMarkBlockRefID = refIDs[defID] 109 } 110 } 111 return ast.WalkContinue 112 }) 113 114 var attrViewIDs []string 115 // 绑定镜像数据库 116 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 117 if !entering { 118 return ast.WalkContinue 119 } 120 121 if ast.NodeAttributeView == n.Type { 122 av.UpsertBlockRel(n.AttributeViewID, n.ID) 123 attrViewIDs = append(attrViewIDs, n.AttributeViewID) 124 } 125 return ast.WalkContinue 126 }) 127 128 if removeAvBinding { 129 // 清空文档绑定的数据库 130 tree.Root.RemoveIALAttr(av.NodeAttrNameAvs) 131 } 132} 133 134func pagedPaths(localPath string, pageSize int) (ret map[int][]string) { 135 ret = map[int][]string{} 136 page := 1 137 filelock.Walk(localPath, func(path string, d fs.DirEntry, err error) error { 138 if nil != err || nil == d { 139 return nil 140 } 141 142 if d.IsDir() { 143 if strings.HasPrefix(d.Name(), ".") { 144 return filepath.SkipDir 145 } 146 return nil 147 } 148 149 if !strings.HasSuffix(d.Name(), ".sy") { 150 return nil 151 } 152 153 ret[page] = append(ret[page], path) 154 if pageSize <= len(ret[page]) { 155 page++ 156 } 157 return nil 158 }) 159 return 160} 161 162func loadTree(localPath string, luteEngine *lute.Lute) (ret *parse.Tree, err error) { 163 data, err := filelock.ReadFile(localPath) 164 if err != nil { 165 logging.LogErrorf("get data [path=%s] failed: %s", localPath, err) 166 return 167 } 168 169 ret, err = dataparser.ParseJSONWithoutFix(data, luteEngine.ParseOptions) 170 if err != nil { 171 logging.LogErrorf("parse json to tree [%s] failed: %s", localPath, err) 172 return 173 } 174 return 175} 176 177var ( 178 ErrBoxNotFound = errors.New("notebook not found") 179 ErrBlockNotFound = errors.New("block not found") 180 ErrTreeNotFound = errors.New("tree not found") 181 ErrIndexing = errors.New("indexing") 182) 183 184func LoadTreeByBlockIDWithReindex(id string) (ret *parse.Tree, err error) { 185 if "" == id { 186 logging.LogWarnf("block id is empty") 187 return nil, ErrTreeNotFound 188 } 189 190 bt := treenode.GetBlockTree(id) 191 if nil == bt { 192 if task.ContainIndexTask() { 193 err = ErrIndexing 194 return 195 } 196 197 // 尝试从文件系统加载并建立索引 198 indexTreeInFilesystem(id) 199 200 bt = treenode.GetBlockTree(id) 201 if nil == bt { 202 if "dev" == util.Mode { 203 logging.LogWarnf("block tree not found [id=%s], stack: [%s]", id, logging.ShortStack()) 204 } 205 return nil, ErrTreeNotFound 206 } 207 } 208 209 luteEngine := util.NewLute() 210 ret, err = filesys.LoadTree(bt.BoxID, bt.Path, luteEngine) 211 return 212} 213 214func LoadTreeByBlockID(id string) (ret *parse.Tree, err error) { 215 if !ast.IsNodeIDPattern(id) { 216 stack := logging.ShortStack() 217 logging.LogErrorf("block id is invalid [id=%s], stack: [%s]", id, stack) 218 return nil, ErrTreeNotFound 219 } 220 221 bt := treenode.GetBlockTree(id) 222 if nil == bt { 223 if task.ContainIndexTask() { 224 err = ErrIndexing 225 return 226 } 227 228 stack := logging.ShortStack() 229 if !strings.Contains(stack, "BuildBlockBreadcrumb") { 230 if "dev" == util.Mode { 231 logging.LogWarnf("block tree not found [id=%s], stack: [%s]", id, stack) 232 } 233 } 234 return nil, ErrTreeNotFound 235 } 236 237 ret, err = loadTreeByBlockTree(bt) 238 return 239} 240 241func loadTreeByBlockTree(bt *treenode.BlockTree) (ret *parse.Tree, err error) { 242 luteEngine := util.NewLute() 243 ret, err = filesys.LoadTree(bt.BoxID, bt.Path, luteEngine) 244 return 245} 246 247var searchTreeLimiter = rate.NewLimiter(rate.Every(3*time.Second), 1) 248 249func indexTreeInFilesystem(rootID string) { 250 if !searchTreeLimiter.Allow() { 251 return 252 } 253 254 msdID := util.PushMsg(Conf.language(45), 7000) 255 defer util.PushClearMsg(msdID) 256 257 logging.LogWarnf("searching tree on filesystem [rootID=%s]", rootID) 258 var treePath string 259 filelock.Walk(util.DataDir, func(path string, d fs.DirEntry, err error) error { 260 if d.IsDir() { 261 if strings.HasPrefix(d.Name(), ".") { 262 return filepath.SkipDir 263 } 264 return nil 265 } 266 267 if !strings.HasSuffix(d.Name(), ".sy") { 268 return nil 269 } 270 271 baseName := filepath.Base(path) 272 if rootID+".sy" != baseName { 273 return nil 274 } 275 276 treePath = path 277 return filepath.SkipAll 278 }) 279 280 if "" == treePath { 281 logging.LogErrorf("tree not found on filesystem [rootID=%s]", rootID) 282 return 283 } 284 285 boxID := strings.TrimPrefix(treePath, util.DataDir) 286 boxID = boxID[1:] 287 boxID = boxID[:strings.Index(boxID, string(os.PathSeparator))] 288 treePath = strings.TrimPrefix(treePath, util.DataDir) 289 treePath = strings.TrimPrefix(treePath, string(os.PathSeparator)) 290 treePath = strings.TrimPrefix(treePath, boxID) 291 treePath = filepath.ToSlash(treePath) 292 if nil == Conf.Box(boxID) { 293 logging.LogInfof("box [%s] not found", boxID) 294 // 如果笔记本不存在或者已经关闭,则不处理 https://github.com/siyuan-note/siyuan/issues/11149 295 return 296 } 297 298 tree, err := filesys.LoadTree(boxID, treePath, util.NewLute()) 299 if err != nil { 300 logging.LogErrorf("load tree [%s] failed: %s", treePath, err) 301 return 302 } 303 304 treenode.UpsertBlockTree(tree) 305 sql.IndexTreeQueue(tree) 306 logging.LogInfof("reindexed tree by filesystem [rootID=%s]", rootID) 307} 308 309func loadParentTree(tree *parse.Tree) (ret *parse.Tree) { 310 boxDir := filepath.Join(util.DataDir, tree.Box) 311 parentDir := path.Dir(tree.Path) 312 if parentDir == boxDir || parentDir == "/" { 313 return 314 } 315 316 luteEngine := lute.New() 317 parentPath := parentDir + ".sy" 318 ret, _ = filesys.LoadTree(tree.Box, parentPath, luteEngine) 319 return 320}