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 "os"
22 "path"
23 "path/filepath"
24 "sort"
25 "sync"
26 "time"
27
28 "github.com/88250/gulu"
29 "github.com/88250/lute/parse"
30 "github.com/siyuan-note/filelock"
31 "github.com/siyuan-note/logging"
32 "github.com/siyuan-note/siyuan/kernel/treenode"
33 "github.com/siyuan-note/siyuan/kernel/util"
34)
35
36type RecentDoc struct {
37 RootID string `json:"rootID"`
38 Icon string `json:"icon"`
39 Title string `json:"title"`
40 ViewedAt int64 `json:"viewedAt"` // 浏览时间字段
41 ClosedAt int64 `json:"closedAt"` // 关闭时间字段
42 OpenAt int64 `json:"openAt"` // 文档第一次从文档树加载到页签的时间
43}
44
45type OutlineDoc struct {
46 DocID string `json:"docID"`
47 Data map[string]interface{} `json:"data"`
48}
49
50var recentDocLock = sync.Mutex{}
51
52func RemoveRecentDoc(ids []string) {
53 recentDocLock.Lock()
54 defer recentDocLock.Unlock()
55
56 recentDocs, err := getRecentDocs("")
57 if err != nil {
58 return
59 }
60
61 ids = gulu.Str.RemoveDuplicatedElem(ids)
62 for i, doc := range recentDocs {
63 if gulu.Str.Contains(doc.RootID, ids) {
64 recentDocs = append(recentDocs[:i], recentDocs[i+1:]...)
65 break
66 }
67 }
68
69 err = setRecentDocs(recentDocs)
70 if err != nil {
71 return
72 }
73 return
74}
75
76func setRecentDocByTree(tree *parse.Tree) {
77 recentDoc := &RecentDoc{
78 RootID: tree.Root.ID,
79 Icon: tree.Root.IALAttr("icon"),
80 Title: tree.Root.IALAttr("title"),
81 ViewedAt: time.Now().Unix(), // 使用当前时间作为浏览时间
82 ClosedAt: 0, // 初始化关闭时间为0,表示未关闭
83 OpenAt: time.Now().Unix(), // 设置文档打开时间
84 }
85
86 recentDocLock.Lock()
87 defer recentDocLock.Unlock()
88
89 recentDocs, err := getRecentDocs("")
90 if err != nil {
91 return
92 }
93
94 for i, c := range recentDocs {
95 if c.RootID == recentDoc.RootID {
96 recentDocs = append(recentDocs[:i], recentDocs[i+1:]...)
97 break
98 }
99 }
100
101 recentDocs = append([]*RecentDoc{recentDoc}, recentDocs...)
102 if 32 < len(recentDocs) {
103 recentDocs = recentDocs[:32]
104 }
105
106 err = setRecentDocs(recentDocs)
107 return
108}
109
110// UpdateRecentDocOpenTime 更新文档打开时间(只在第一次从文档树加载到页签时调用)
111func UpdateRecentDocOpenTime(rootID string) (err error) {
112 recentDocLock.Lock()
113 defer recentDocLock.Unlock()
114
115 recentDocs, err := getRecentDocs("")
116 if err != nil {
117 return
118 }
119
120 // 查找文档并更新打开时间
121 found := false
122 for _, doc := range recentDocs {
123 if doc.RootID == rootID {
124 doc.OpenAt = time.Now().Unix()
125 found = true
126 break
127 }
128 }
129
130 if found {
131 err = setRecentDocs(recentDocs)
132 }
133 return
134}
135
136// UpdateRecentDocViewTime 更新文档浏览时间
137func UpdateRecentDocViewTime(rootID string) (err error) {
138 recentDocLock.Lock()
139 defer recentDocLock.Unlock()
140
141 recentDocs, err := getRecentDocs("")
142 if err != nil {
143 return
144 }
145
146 // 查找文档并更新浏览时间
147 found := false
148 for _, doc := range recentDocs {
149 if doc.RootID == rootID {
150 doc.ViewedAt = time.Now().Unix()
151 found = true
152 break
153 }
154 }
155
156 if found {
157 // 按浏览时间降序排序
158 sort.Slice(recentDocs, func(i, j int) bool {
159 return recentDocs[i].ViewedAt > recentDocs[j].ViewedAt
160 })
161 err = setRecentDocs(recentDocs)
162 }
163 return
164}
165
166// UpdateRecentDocCloseTime 更新文档关闭时间
167func UpdateRecentDocCloseTime(rootID string) (err error) {
168 recentDocLock.Lock()
169 defer recentDocLock.Unlock()
170
171 recentDocs, err := getRecentDocs("")
172 if err != nil {
173 return
174 }
175
176 // 查找文档并更新关闭时间
177 found := false
178 for _, doc := range recentDocs {
179 if doc.RootID == rootID {
180 doc.ClosedAt = time.Now().Unix()
181 found = true
182 break
183 }
184 }
185
186 if found {
187 err = setRecentDocs(recentDocs)
188 }
189 return
190}
191
192func GetRecentDocs(sortBy string) (ret []*RecentDoc, err error) {
193 recentDocLock.Lock()
194 defer recentDocLock.Unlock()
195 return getRecentDocs(sortBy)
196}
197
198func setRecentDocs(recentDocs []*RecentDoc) (err error) {
199 dirPath := filepath.Join(util.DataDir, "storage")
200 if err = os.MkdirAll(dirPath, 0755); err != nil {
201 logging.LogErrorf("create storage [recent-doc] dir failed: %s", err)
202 return
203 }
204
205 data, err := gulu.JSON.MarshalIndentJSON(recentDocs, "", " ")
206 if err != nil {
207 logging.LogErrorf("marshal storage [recent-doc] failed: %s", err)
208 return
209 }
210
211 lsPath := filepath.Join(dirPath, "recent-doc.json")
212 err = filelock.WriteFile(lsPath, data)
213 if err != nil {
214 logging.LogErrorf("write storage [recent-doc] failed: %s", err)
215 return
216 }
217 return
218}
219
220func getRecentDocs(sortBy string) (ret []*RecentDoc, err error) {
221 tmp := []*RecentDoc{}
222 dataPath := filepath.Join(util.DataDir, "storage/recent-doc.json")
223 if !filelock.IsExist(dataPath) {
224 return
225 }
226
227 data, err := filelock.ReadFile(dataPath)
228 if err != nil {
229 logging.LogErrorf("read storage [recent-doc] failed: %s", err)
230 return
231 }
232
233 if err = gulu.JSON.UnmarshalJSON(data, &tmp); err != nil {
234 logging.LogErrorf("unmarshal storage [recent-doc] failed: %s", err)
235 return
236 }
237
238 var rootIDs []string
239 for _, doc := range tmp {
240 rootIDs = append(rootIDs, doc.RootID)
241 }
242 bts := treenode.GetBlockTrees(rootIDs)
243 var notExists []string
244 for _, doc := range tmp {
245 if bt := bts[doc.RootID]; nil != bt {
246 doc.Title = path.Base(bt.HPath) // Recent docs not updated after renaming https://github.com/siyuan-note/siyuan/issues/7827
247 ret = append(ret, doc)
248 } else {
249 notExists = append(notExists, doc.RootID)
250 }
251 }
252
253 if 0 < len(notExists) {
254 setRecentDocs(ret)
255 }
256
257 // 根据排序参数进行排序
258 switch sortBy {
259 case "closedAt": // 按关闭时间排序
260 sort.Slice(ret, func(i, j int) bool {
261 if ret[i].ClosedAt == 0 && ret[j].ClosedAt == 0 {
262 // 如果都没有关闭时间,按浏览时间排序
263 return ret[i].ViewedAt > ret[j].ViewedAt
264 }
265 if ret[i].ClosedAt == 0 {
266 return false // 没有关闭时间的排在后面
267 }
268 if ret[j].ClosedAt == 0 {
269 return true // 有关闭时间的排在前面
270 }
271 return ret[i].ClosedAt > ret[j].ClosedAt
272 })
273 case "openAt": // 按打开时间排序
274 sort.Slice(ret, func(i, j int) bool {
275 if ret[i].OpenAt == 0 && ret[j].OpenAt == 0 {
276 // 如果都没有打开时间,按ID时间排序(ID包含时间信息)
277 return ret[i].RootID > ret[j].RootID
278 }
279 if ret[i].OpenAt == 0 {
280 return false // 没有打开时间的排在后面
281 }
282 if ret[j].OpenAt == 0 {
283 return true // 有打开时间的排在前面
284 }
285 return ret[i].OpenAt > ret[j].OpenAt
286 })
287 default: // 默认按浏览时间排序
288 sort.Slice(ret, func(i, j int) bool {
289 if ret[i].ViewedAt == 0 && ret[j].ViewedAt == 0 {
290 // 如果都没有浏览时间,按ID时间排序(ID包含时间信息)
291 return ret[i].RootID > ret[j].RootID
292 }
293 if ret[i].ViewedAt == 0 {
294 return false // 没有浏览时间的排在后面
295 }
296 if ret[j].ViewedAt == 0 {
297 return true // 有浏览时间的排在前面
298 }
299 return ret[i].ViewedAt > ret[j].ViewedAt
300 })
301 }
302 return
303}
304
305type Criterion struct {
306 Name string `json:"name"`
307 Sort int `json:"sort"` // 0:按块类型(默认),1:按创建时间升序,2:按创建时间降序,3:按更新时间升序,4:按更新时间降序,5:按内容顺序(仅在按文档分组时)
308 Group int `json:"group"` // 0:不分组,1:按文档分组
309 HasReplace bool `json:"hasReplace"` // 是否有替换
310 Method int `json:"method"` // 0:文本,1:查询语法,2:SQL,3:正则表达式
311 HPath string `json:"hPath"`
312 IDPath []string `json:"idPath"`
313 K string `json:"k"` // 搜索关键字
314 R string `json:"r"` // 替换关键字
315 Types *CriterionTypes `json:"types"` // 类型过滤选项
316 ReplaceTypes *CriterionReplaceTypes `json:"replaceTypes"` // 替换类型过滤选项
317}
318
319type CriterionTypes struct {
320 MathBlock bool `json:"mathBlock"`
321 Table bool `json:"table"`
322 Blockquote bool `json:"blockquote"`
323 SuperBlock bool `json:"superBlock"`
324 Paragraph bool `json:"paragraph"`
325 Document bool `json:"document"`
326 Heading bool `json:"heading"`
327 List bool `json:"list"`
328 ListItem bool `json:"listItem"`
329 CodeBlock bool `json:"codeBlock"`
330 HtmlBlock bool `json:"htmlBlock"`
331 EmbedBlock bool `json:"embedBlock"`
332 DatabaseBlock bool `json:"databaseBlock"`
333 AudioBlock bool `json:"audioBlock"`
334 VideoBlock bool `json:"videoBlock"`
335 IFrameBlock bool `json:"iframeBlock"`
336 WidgetBlock bool `json:"widgetBlock"`
337}
338
339type CriterionReplaceTypes struct {
340 Text bool `json:"text"`
341 ImgText bool `json:"imgText"`
342 ImgTitle bool `json:"imgTitle"`
343 ImgSrc bool `json:"imgSrc"`
344 AText bool `json:"aText"`
345 ATitle bool `json:"aTitle"`
346 AHref bool `json:"aHref"`
347 Code bool `json:"code"`
348 Em bool `json:"em"`
349 Strong bool `json:"strong"`
350 InlineMath bool `json:"inlineMath"`
351 InlineMemo bool `json:"inlineMemo"`
352 BlockRef bool `json:"blockRef"`
353 FileAnnotationRef bool `json:"fileAnnotationRef"`
354 Kbd bool `json:"kbd"`
355 Mark bool `json:"mark"`
356 S bool `json:"s"`
357 Sub bool `json:"sub"`
358 Sup bool `json:"sup"`
359 Tag bool `json:"tag"`
360 U bool `json:"u"`
361 DocTitle bool `json:"docTitle"`
362 CodeBlock bool `json:"codeBlock"`
363 MathBlock bool `json:"mathBlock"`
364 HtmlBlock bool `json:"htmlBlock"`
365}
366
367var criteriaLock = sync.Mutex{}
368
369func RemoveCriterion(name string) (err error) {
370 criteriaLock.Lock()
371 defer criteriaLock.Unlock()
372
373 criteria, err := getCriteria()
374 if err != nil {
375 return
376 }
377
378 for i, c := range criteria {
379 if c.Name == name {
380 criteria = append(criteria[:i], criteria[i+1:]...)
381 break
382 }
383 }
384
385 err = setCriteria(criteria)
386 return
387}
388
389func SetCriterion(criterion *Criterion) (err error) {
390 if "" == criterion.Name {
391 return errors.New(Conf.Language(142))
392 }
393
394 criteriaLock.Lock()
395 defer criteriaLock.Unlock()
396
397 criteria, err := getCriteria()
398 if err != nil {
399 return
400 }
401
402 update := false
403 for i, c := range criteria {
404 if c.Name == criterion.Name {
405 criteria[i] = criterion
406 update = true
407 break
408 }
409 }
410 if !update {
411 criteria = append(criteria, criterion)
412 }
413
414 err = setCriteria(criteria)
415 return
416}
417
418func GetCriteria() (ret []*Criterion) {
419 criteriaLock.Lock()
420 defer criteriaLock.Unlock()
421 ret, _ = getCriteria()
422 return
423}
424
425func setCriteria(criteria []*Criterion) (err error) {
426 dirPath := filepath.Join(util.DataDir, "storage")
427 if err = os.MkdirAll(dirPath, 0755); err != nil {
428 logging.LogErrorf("create storage [criteria] dir failed: %s", err)
429 return
430 }
431
432 data, err := gulu.JSON.MarshalIndentJSON(criteria, "", " ")
433 if err != nil {
434 logging.LogErrorf("marshal storage [criteria] failed: %s", err)
435 return
436 }
437
438 lsPath := filepath.Join(dirPath, "criteria.json")
439 err = filelock.WriteFile(lsPath, data)
440 if err != nil {
441 logging.LogErrorf("write storage [criteria] failed: %s", err)
442 return
443 }
444 return
445}
446
447func getCriteria() (ret []*Criterion, err error) {
448 ret = []*Criterion{}
449 dataPath := filepath.Join(util.DataDir, "storage/criteria.json")
450 if !filelock.IsExist(dataPath) {
451 return
452 }
453
454 data, err := filelock.ReadFile(dataPath)
455 if err != nil {
456 logging.LogErrorf("read storage [criteria] failed: %s", err)
457 return
458 }
459
460 if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
461 logging.LogErrorf("unmarshal storage [criteria] failed: %s", err)
462 return
463 }
464 return
465}
466
467var localStorageLock = sync.Mutex{}
468
469func RemoveLocalStorageVals(keys []string) (err error) {
470 localStorageLock.Lock()
471 defer localStorageLock.Unlock()
472
473 localStorage := getLocalStorage()
474 for _, key := range keys {
475 delete(localStorage, key)
476 }
477 return setLocalStorage(localStorage)
478}
479
480func SetLocalStorageVal(key string, val interface{}) (err error) {
481 localStorageLock.Lock()
482 defer localStorageLock.Unlock()
483
484 localStorage := getLocalStorage()
485 localStorage[key] = val
486 return setLocalStorage(localStorage)
487}
488
489func SetLocalStorage(val interface{}) (err error) {
490 localStorageLock.Lock()
491 defer localStorageLock.Unlock()
492 return setLocalStorage(val)
493}
494
495func GetLocalStorage() (ret map[string]interface{}) {
496 localStorageLock.Lock()
497 defer localStorageLock.Unlock()
498 return getLocalStorage()
499}
500
501func setLocalStorage(val interface{}) (err error) {
502 dirPath := filepath.Join(util.DataDir, "storage")
503 if err = os.MkdirAll(dirPath, 0755); err != nil {
504 logging.LogErrorf("create storage [local] dir failed: %s", err)
505 return
506 }
507
508 data, err := gulu.JSON.MarshalIndentJSON(val, "", " ")
509 if err != nil {
510 logging.LogErrorf("marshal storage [local] failed: %s", err)
511 return
512 }
513
514 lsPath := filepath.Join(dirPath, "local.json")
515 err = filelock.WriteFile(lsPath, data)
516 if err != nil {
517 logging.LogErrorf("write storage [local] failed: %s", err)
518 return
519 }
520 return
521}
522
523func getLocalStorage() (ret map[string]interface{}) {
524 // When local.json is corrupted, clear the file to avoid being unable to enter the main interface https://github.com/siyuan-note/siyuan/issues/7911
525 ret = map[string]interface{}{}
526 lsPath := filepath.Join(util.DataDir, "storage/local.json")
527 if !filelock.IsExist(lsPath) {
528 return
529 }
530
531 data, err := filelock.ReadFile(lsPath)
532 if err != nil {
533 logging.LogErrorf("read storage [local] failed: %s", err)
534 return
535 }
536
537 if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
538 logging.LogErrorf("unmarshal storage [local] failed: %s", err)
539 return
540 }
541 return
542}
543
544var outlineStorageLock = sync.Mutex{}
545
546func GetOutlineStorage(docID string) (ret map[string]interface{}, err error) {
547 outlineStorageLock.Lock()
548 defer outlineStorageLock.Unlock()
549
550 ret = map[string]interface{}{}
551 outlineDocs, err := getOutlineDocs()
552 if err != nil {
553 return
554 }
555
556 for _, doc := range outlineDocs {
557 if doc.DocID == docID {
558 ret = doc.Data
559 break
560 }
561 }
562 return
563}
564
565func SetOutlineStorage(docID string, val interface{}) (err error) {
566 outlineStorageLock.Lock()
567 defer outlineStorageLock.Unlock()
568
569 outlineDoc := &OutlineDoc{
570 DocID: docID,
571 Data: make(map[string]interface{}),
572 }
573
574 if valMap, ok := val.(map[string]interface{}); ok {
575 outlineDoc.Data = valMap
576 }
577
578 outlineDocs, err := getOutlineDocs()
579 if err != nil {
580 return
581 }
582
583 // 如果文档已存在,先移除旧的
584 for i, doc := range outlineDocs {
585 if doc.DocID == docID {
586 outlineDocs = append(outlineDocs[:i], outlineDocs[i+1:]...)
587 break
588 }
589 }
590
591 // 将新的文档信息添加到最前面
592 outlineDocs = append([]*OutlineDoc{outlineDoc}, outlineDocs...)
593
594 // 限制为2000个文档
595 if 2000 < len(outlineDocs) {
596 outlineDocs = outlineDocs[:2000]
597 }
598
599 err = setOutlineDocs(outlineDocs)
600 return
601}
602
603func RemoveOutlineStorage(docID string) (err error) {
604 outlineStorageLock.Lock()
605 defer outlineStorageLock.Unlock()
606
607 outlineDocs, err := getOutlineDocs()
608 if err != nil {
609 return
610 }
611
612 for i, doc := range outlineDocs {
613 if doc.DocID == docID {
614 outlineDocs = append(outlineDocs[:i], outlineDocs[i+1:]...)
615 break
616 }
617 }
618
619 err = setOutlineDocs(outlineDocs)
620 return
621}
622
623func setOutlineDocs(outlineDocs []*OutlineDoc) (err error) {
624 dirPath := filepath.Join(util.DataDir, "storage")
625 if err = os.MkdirAll(dirPath, 0755); err != nil {
626 logging.LogErrorf("create storage [outline] dir failed: %s", err)
627 return
628 }
629
630 data, err := gulu.JSON.MarshalJSON(outlineDocs)
631 if err != nil {
632 logging.LogErrorf("marshal storage [outline] failed: %s", err)
633 return
634 }
635
636 lsPath := filepath.Join(dirPath, "outline.json")
637 err = filelock.WriteFile(lsPath, data)
638 if err != nil {
639 logging.LogErrorf("write storage [outline] failed: %s", err)
640 return
641 }
642 return
643}
644
645func getOutlineDocs() (ret []*OutlineDoc, err error) {
646 ret = []*OutlineDoc{}
647 dataPath := filepath.Join(util.DataDir, "storage/outline.json")
648 if !filelock.IsExist(dataPath) {
649 return
650 }
651
652 data, err := filelock.ReadFile(dataPath)
653 if err != nil {
654 logging.LogErrorf("read storage [outline] failed: %s", err)
655 return
656 }
657
658 if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
659 logging.LogErrorf("unmarshal storage [outline] failed: %s", err)
660 return
661 }
662 return
663}