A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
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}