A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 663 lines 17 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 "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}