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 sql
18
19import (
20 "bytes"
21 "crypto/sha256"
22 "database/sql"
23 "errors"
24 "fmt"
25 "os"
26 "path/filepath"
27 "regexp"
28 "runtime"
29 "runtime/debug"
30 "strconv"
31 "strings"
32 "sync"
33 "text/template"
34 "time"
35 "unicode/utf8"
36
37 "github.com/88250/gulu"
38 "github.com/88250/lute/ast"
39 "github.com/88250/lute/editor"
40 "github.com/88250/lute/html"
41 "github.com/88250/lute/parse"
42 "github.com/mattn/go-sqlite3"
43 _ "github.com/mattn/go-sqlite3"
44 "github.com/siyuan-note/eventbus"
45 "github.com/siyuan-note/logging"
46 "github.com/siyuan-note/siyuan/kernel/treenode"
47 "github.com/siyuan-note/siyuan/kernel/util"
48)
49
50var (
51 db *sql.DB
52 historyDB *sql.DB
53 assetContentDB *sql.DB
54)
55
56func init() {
57 regex := func(re, s string) (bool, error) {
58 re = strings.ReplaceAll(re, "\\\\", "\\")
59 return regexp.MatchString(re, s)
60 }
61
62 sql.Register("sqlite3_extended", &sqlite3.SQLiteDriver{
63 ConnectHook: func(conn *sqlite3.SQLiteConn) error {
64 return conn.RegisterFunc("regexp", regex, true)
65 },
66 })
67}
68
69var initDatabaseLock = sync.Mutex{}
70
71func InitDatabase(forceRebuild bool) (err error) {
72 initDatabaseLock.Lock()
73 defer initDatabaseLock.Unlock()
74
75 ClearCache()
76 disableCache()
77 defer enableCache()
78
79 util.IncBootProgress(2, "Initializing database...")
80
81 if forceRebuild {
82 ClearQueue()
83 }
84
85 initDBConnection()
86 treenode.InitBlockTree(forceRebuild)
87
88 if !forceRebuild {
89 // 检查数据库结构版本,如果版本不一致的话说明改过表结构,需要重建
90 if util.DatabaseVer == getDatabaseVer() {
91 return
92 }
93 logging.LogInfof("the database structure is changed, rebuilding database...")
94 }
95
96 // 不存在库或者版本不一致都会走到这里
97
98 closeDatabase()
99 if gulu.File.IsExist(util.DBPath) {
100 if err = removeDatabaseFile(); err != nil {
101 logging.LogErrorf("remove database file [%s] failed: %s", util.DBPath, err)
102 util.PushClearProgress()
103 err = nil
104 }
105 }
106
107 initDBConnection()
108 initDBTables()
109
110 logging.LogInfof("reinitialized database [%s]", util.DBPath)
111 return
112}
113
114func initDBTables() {
115 _, err := db.Exec("DROP TABLE IF EXISTS stat")
116 if err != nil {
117 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "drop table [stat] failed: %s", err)
118 }
119 _, err = db.Exec("CREATE TABLE stat (key, value)")
120 if err != nil {
121 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [stat] failed: %s", err)
122 }
123 setDatabaseVer()
124
125 _, err = db.Exec("DROP TABLE IF EXISTS blocks")
126 if err != nil {
127 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "drop table [blocks] failed: %s", err)
128 }
129 _, err = db.Exec("CREATE TABLE blocks (id, parent_id, root_id, hash, box, path, hpath, name, alias, memo, tag, content, fcontent, markdown, length, type, subtype, ial, sort, created, updated)")
130 if err != nil {
131 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [blocks] failed: %s", err)
132 }
133
134 _, err = db.Exec("CREATE INDEX idx_blocks_id ON blocks(id)")
135 if err != nil {
136 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create index [idx_blocks_id] failed: %s", err)
137 }
138
139 _, err = db.Exec("CREATE INDEX idx_blocks_parent_id ON blocks(parent_id)")
140 if err != nil {
141 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create index [idx_blocks_parent_id] failed: %s", err)
142 }
143
144 _, err = db.Exec("CREATE INDEX idx_blocks_root_id ON blocks(root_id)")
145 if err != nil {
146 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create index [idx_blocks_root_id] failed: %s", err)
147 }
148
149 _, err = db.Exec("DROP TABLE IF EXISTS blocks_fts")
150 if err != nil {
151 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "drop table [blocks_fts] failed: %s", err)
152 }
153 _, err = db.Exec("CREATE VIRTUAL TABLE blocks_fts USING fts5(id UNINDEXED, parent_id UNINDEXED, root_id UNINDEXED, hash UNINDEXED, box UNINDEXED, path UNINDEXED, hpath, name, alias, memo, tag, content, fcontent, markdown UNINDEXED, length UNINDEXED, type UNINDEXED, subtype UNINDEXED, ial, sort UNINDEXED, created UNINDEXED, updated UNINDEXED, tokenize=\"siyuan\")")
154 if err != nil {
155 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [blocks_fts] failed: %s", err)
156 }
157
158 _, err = db.Exec("DROP TABLE IF EXISTS blocks_fts_case_insensitive")
159 if err != nil {
160 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "drop table [blocks_fts_case_insensitive] failed: %s", err)
161 }
162 _, err = db.Exec("CREATE VIRTUAL TABLE blocks_fts_case_insensitive USING fts5(id UNINDEXED, parent_id UNINDEXED, root_id UNINDEXED, hash UNINDEXED, box UNINDEXED, path UNINDEXED, hpath, name, alias, memo, tag, content, fcontent, markdown UNINDEXED, length UNINDEXED, type UNINDEXED, subtype UNINDEXED, ial, sort UNINDEXED, created UNINDEXED, updated UNINDEXED, tokenize=\"siyuan case_insensitive\")")
163 if err != nil {
164 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [blocks_fts_case_insensitive] failed: %s", err)
165 }
166
167 _, err = db.Exec("DROP TABLE IF EXISTS spans")
168 if err != nil {
169 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "drop table [spans] failed: %s", err)
170 }
171 _, err = db.Exec("CREATE TABLE spans (id, block_id, root_id, box, path, content, markdown, type, ial)")
172 if err != nil {
173 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [spans] failed: %s", err)
174 }
175 _, err = db.Exec("CREATE INDEX idx_spans_root_id ON spans(root_id)")
176 if err != nil {
177 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create index [idx_spans_root_id] failed: %s", err)
178 }
179
180 _, err = db.Exec("DROP TABLE IF EXISTS assets")
181 if err != nil {
182 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "drop table [assets] failed: %s", err)
183 }
184 _, err = db.Exec("CREATE TABLE assets (id, block_id, root_id, box, docpath, path, name, title, hash)")
185 if err != nil {
186 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [assets] failed: %s", err)
187 }
188 _, err = db.Exec("CREATE INDEX idx_assets_root_id ON assets(root_id)")
189 if err != nil {
190 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create index [idx_assets_root_id] failed: %s", err)
191 }
192
193 _, err = db.Exec("DROP TABLE IF EXISTS attributes")
194 if err != nil {
195 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "drop table [attributes] failed: %s", err)
196 }
197 _, err = db.Exec("CREATE TABLE attributes (id, name, value, type, block_id, root_id, box, path)")
198 if err != nil {
199 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [attributes] failed: %s", err)
200 }
201 _, err = db.Exec("CREATE INDEX idx_attributes_block_id ON attributes(block_id)")
202 if err != nil {
203 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create index [idx_attributes_block_id] failed: %s", err)
204 }
205 _, err = db.Exec("CREATE INDEX idx_attributes_root_id ON attributes(root_id)")
206 if err != nil {
207 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create index [idx_attributes_root_id] failed: %s", err)
208 }
209
210 _, err = db.Exec("DROP TABLE IF EXISTS refs")
211 if err != nil {
212 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "drop table [refs] failed: %s", err)
213 }
214 _, err = db.Exec("CREATE TABLE refs (id, def_block_id, def_block_parent_id, def_block_root_id, def_block_path, block_id, root_id, box, path, content, markdown, type)")
215 if err != nil {
216 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [refs] failed: %s", err)
217 }
218
219 _, err = db.Exec("DROP TABLE IF EXISTS file_annotation_refs")
220 if err != nil {
221 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "drop table [refs] failed: %s", err)
222 }
223 _, err = db.Exec("CREATE TABLE file_annotation_refs (id, file_path, annotation_id, block_id, root_id, box, path, content, type)")
224 if err != nil {
225 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [refs] failed: %s", err)
226 }
227}
228
229func initDBConnection() {
230 if nil != db {
231 closeDatabase()
232 }
233
234 util.LogDatabaseSize(util.DBPath)
235 dsn := util.DBPath + "?_journal_mode=WAL" +
236 "&_synchronous=OFF" +
237 "&_mmap_size=2684354560" +
238 "&_secure_delete=OFF" +
239 "&_cache_size=-20480" +
240 "&_page_size=32768" +
241 "&_busy_timeout=7000" +
242 "&_ignore_check_constraints=ON" +
243 "&_temp_store=MEMORY" +
244 "&_case_sensitive_like=OFF"
245 var err error
246 db, err = sql.Open("sqlite3_extended", dsn)
247 if err != nil {
248 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create database failed: %s", err)
249 }
250 db.SetMaxIdleConns(20)
251 db.SetMaxOpenConns(20)
252 db.SetConnMaxLifetime(365 * 24 * time.Hour)
253}
254
255var initHistoryDatabaseLock = sync.Mutex{}
256
257func InitHistoryDatabase(forceRebuild bool) {
258 initHistoryDatabaseLock.Lock()
259 defer initHistoryDatabaseLock.Unlock()
260
261 initHistoryDBConnection()
262
263 if !forceRebuild && gulu.File.IsExist(util.HistoryDBPath) {
264 return
265 }
266
267 historyDB.Close()
268 if err := os.RemoveAll(util.HistoryDBPath); err != nil {
269 logging.LogErrorf("remove history database file [%s] failed: %s", util.HistoryDBPath, err)
270 return
271 }
272
273 initHistoryDBConnection()
274 initHistoryDBTables()
275}
276
277func initHistoryDBConnection() {
278 if nil != historyDB {
279 historyDB.Close()
280 }
281
282 util.LogDatabaseSize(util.HistoryDBPath)
283 dsn := util.HistoryDBPath + "?_journal_mode=WAL" +
284 "&_synchronous=OFF" +
285 "&_mmap_size=2684354560" +
286 "&_secure_delete=OFF" +
287 "&_cache_size=-20480" +
288 "&_page_size=32768" +
289 "&_busy_timeout=7000" +
290 "&_ignore_check_constraints=ON" +
291 "&_temp_store=MEMORY" +
292 "&_case_sensitive_like=OFF"
293 var err error
294 historyDB, err = sql.Open("sqlite3_extended", dsn)
295 if err != nil {
296 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create history database failed: %s", err)
297 }
298 historyDB.SetMaxIdleConns(3)
299 historyDB.SetMaxOpenConns(3)
300 historyDB.SetConnMaxLifetime(365 * 24 * time.Hour)
301}
302
303func initHistoryDBTables() {
304 historyDB.Exec("DROP TABLE histories_fts_case_insensitive")
305 _, err := historyDB.Exec("CREATE VIRTUAL TABLE histories_fts_case_insensitive USING fts5(id UNINDEXED, type UNINDEXED, op UNINDEXED, title, content, path UNINDEXED, created UNINDEXED, tokenize=\"siyuan case_insensitive\")")
306 if err != nil {
307 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [histories_fts_case_insensitive] failed: %s", err)
308 }
309}
310
311var initAssetContentDatabaseLock = sync.Mutex{}
312
313func InitAssetContentDatabase(forceRebuild bool) {
314 initAssetContentDatabaseLock.Lock()
315 defer initAssetContentDatabaseLock.Unlock()
316
317 initAssetContentDBConnection()
318
319 if !forceRebuild && gulu.File.IsExist(util.AssetContentDBPath) {
320 return
321 }
322
323 assetContentDB.Close()
324 if err := os.RemoveAll(util.AssetContentDBPath); err != nil {
325 logging.LogErrorf("remove assets database file [%s] failed: %s", util.AssetContentDBPath, err)
326 return
327 }
328
329 initAssetContentDBConnection()
330 initAssetContentDBTables()
331}
332
333func initAssetContentDBConnection() {
334 if nil != assetContentDB {
335 assetContentDB.Close()
336 }
337
338 util.LogDatabaseSize(util.AssetContentDBPath)
339 dsn := util.AssetContentDBPath + "?_journal_mode=WAL" +
340 "&_synchronous=OFF" +
341 "&_mmap_size=2684354560" +
342 "&_secure_delete=OFF" +
343 "&_cache_size=-20480" +
344 "&_page_size=32768" +
345 "&_busy_timeout=7000" +
346 "&_ignore_check_constraints=ON" +
347 "&_temp_store=MEMORY" +
348 "&_case_sensitive_like=OFF"
349 var err error
350 assetContentDB, err = sql.Open("sqlite3_extended", dsn)
351 if err != nil {
352 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create assets database failed: %s", err)
353 }
354 assetContentDB.SetMaxIdleConns(3)
355 assetContentDB.SetMaxOpenConns(3)
356 assetContentDB.SetConnMaxLifetime(365 * 24 * time.Hour)
357}
358
359func initAssetContentDBTables() {
360 assetContentDB.Exec("DROP TABLE asset_contents_fts_case_insensitive")
361 _, err := assetContentDB.Exec("CREATE VIRTUAL TABLE asset_contents_fts_case_insensitive USING fts5(id UNINDEXED, name, ext, path, size UNINDEXED, updated UNINDEXED, content, tokenize=\"siyuan case_insensitive\")")
362 if err != nil {
363 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [asset_contents_fts_case_insensitive] failed: %s", err)
364 }
365}
366
367var (
368 caseSensitive bool
369 indexAssetPath bool
370)
371
372func SetCaseSensitive(b bool) {
373 caseSensitive = b
374 if b {
375 db.Exec("PRAGMA case_sensitive_like = ON;")
376 } else {
377 db.Exec("PRAGMA case_sensitive_like = OFF;")
378 }
379}
380
381func SetIndexAssetPath(b bool) {
382 indexAssetPath = b
383}
384
385func refsFromTree(tree *parse.Tree) (refs []*Ref, fileAnnotationRefs []*FileAnnotationRef) {
386 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
387 if entering {
388 return ast.WalkContinue
389 }
390
391 if treenode.IsBlockRef(n) {
392 ref := buildRef(tree, n)
393 if !isRepeatedRef(refs, ref) {
394 refs = append(refs, ref)
395 }
396 } else if treenode.IsFileAnnotationRef(n) {
397 pathID := n.TextMarkFileAnnotationRefID
398 idx := strings.LastIndex(pathID, "/")
399 if -1 == idx {
400 return ast.WalkContinue
401 }
402
403 filePath := pathID[:idx]
404 annotationID := pathID[idx+1:]
405
406 anchor := n.TextMarkTextContent
407 text := filePath
408 if "" != anchor {
409 text = anchor
410 }
411 parentBlock := treenode.ParentBlock(n)
412 ref := &FileAnnotationRef{
413 ID: ast.NewNodeID(),
414 FilePath: filePath,
415 AnnotationID: annotationID,
416 BlockID: parentBlock.ID,
417 RootID: tree.ID,
418 Box: tree.Box,
419 Path: tree.Path,
420 Content: text,
421 Type: treenode.TypeAbbr(n.Type.String()),
422 }
423 fileAnnotationRefs = append(fileAnnotationRefs, ref)
424 } else if treenode.IsEmbedBlockRef(n) {
425 ref := buildEmbedRef(tree, n)
426 if !isRepeatedRef(refs, ref) {
427 refs = append(refs, ref)
428 }
429 }
430 return ast.WalkContinue
431 })
432 return
433}
434
435func isRepeatedRef(refs []*Ref, ref *Ref) bool {
436 // Repeated references to the same block within a block only count as one reference https://github.com/siyuan-note/siyuan/issues/9670
437 for _, r := range refs {
438 if r.DefBlockID == ref.DefBlockID && r.BlockID == ref.BlockID {
439 return true
440 }
441 }
442 return false
443}
444
445func buildRef(tree *parse.Tree, refNode *ast.Node) *Ref {
446 // 多个类型可能会导致渲染的 Markdown 不正确,所以这里只保留 block-ref 类型
447 tmpTyp := refNode.TextMarkType
448 refNode.TextMarkType = "block-ref"
449 markdown := treenode.ExportNodeStdMd(refNode, luteEngine)
450 refNode.TextMarkType = tmpTyp
451
452 defBlockID, text, _ := treenode.GetBlockRef(refNode)
453 var defBlockParentID, defBlockRootID, defBlockPath string
454 defBlock := treenode.GetBlockTree(defBlockID)
455 if nil != defBlock {
456 defBlockParentID = defBlock.ParentID
457 defBlockRootID = defBlock.RootID
458 defBlockPath = defBlock.Path
459 }
460 parentBlock := treenode.ParentBlock(refNode)
461 return &Ref{
462 ID: ast.NewNodeID(),
463 DefBlockID: defBlockID,
464 DefBlockParentID: defBlockParentID,
465 DefBlockRootID: defBlockRootID,
466 DefBlockPath: defBlockPath,
467 BlockID: parentBlock.ID,
468 RootID: tree.ID,
469 Box: tree.Box,
470 Path: tree.Path,
471 Content: text,
472 Markdown: markdown,
473 Type: treenode.TypeAbbr(refNode.Type.String()),
474 }
475}
476
477func buildEmbedRef(tree *parse.Tree, embedNode *ast.Node) *Ref {
478 defBlockID := getEmbedRef(embedNode)
479 var defBlockParentID, defBlockRootID, defBlockPath string
480 defBlock := treenode.GetBlockTree(defBlockID)
481 if nil != defBlock {
482 defBlockParentID = defBlock.ParentID
483 defBlockRootID = defBlock.RootID
484 defBlockPath = defBlock.Path
485 }
486
487 return &Ref{
488 ID: ast.NewNodeID(),
489 DefBlockID: defBlockID,
490 DefBlockParentID: defBlockParentID,
491 DefBlockRootID: defBlockRootID,
492 DefBlockPath: defBlockPath,
493 BlockID: embedNode.ID,
494 RootID: tree.ID,
495 Box: tree.Box,
496 Path: tree.Path,
497 Content: "", // 通过嵌入块构建引用时定义块可能还没有入库,所以这里统一不填充内容
498 Markdown: "",
499 Type: treenode.TypeAbbr(embedNode.Type.String()),
500 }
501}
502
503func getEmbedRef(embedNode *ast.Node) (queryBlockID string) {
504 queryBlockID = treenode.GetEmbedBlockRef(embedNode)
505 return
506}
507
508func fromTree(node *ast.Node, tree *parse.Tree) (blocks []*Block, spans []*Span, assets []*Asset, attributes []*Attribute) {
509 rootID := tree.Root.ID
510 boxID := tree.Box
511 p := tree.Path
512 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
513 if !entering {
514 return ast.WalkContinue
515 }
516
517 // 构造行级元素
518 spanBlocks, spanSpans, spanAssets, spanAttrs, walkStatus := buildSpanFromNode(n, tree, rootID, boxID, p)
519 if 0 < len(spanBlocks) {
520 blocks = append(blocks, spanBlocks...)
521 }
522 if 0 < len(spanSpans) {
523 spans = append(spans, spanSpans...)
524 }
525 if 0 < len(spanAssets) {
526 assets = append(assets, spanAssets...)
527 }
528 if 0 < len(spanAttrs) {
529 attributes = append(attributes, spanAttrs...)
530 }
531
532 // 构造属性
533 attrs := buildAttributeFromNode(n, rootID, boxID, p)
534 if 0 < len(attrs) {
535 attributes = append(attributes, attrs...)
536 }
537 if -1 != walkStatus {
538 return walkStatus
539 }
540
541 // 构造块级元素
542 if "" == n.ID || !n.IsBlock() {
543 return ast.WalkContinue
544 }
545
546 b, attrs := buildBlockFromNode(n, tree)
547 blocks = append(blocks, b)
548 if 0 < len(attrs) {
549 attributes = append(attributes, attrs...)
550 }
551 return ast.WalkContinue
552 })
553 return
554}
555
556func buildAttributeFromNode(n *ast.Node, rootID, boxID, p string) (attributes []*Attribute) {
557 switch n.Type {
558 case ast.NodeKramdownSpanIAL:
559 parentBlock := treenode.ParentBlock(n)
560 attrs := parse.IALValMap(n)
561 for name, val := range attrs {
562 if !isAttr(name) {
563 continue
564 }
565
566 attr := &Attribute{
567 ID: ast.NewNodeID(),
568 Name: name,
569 Value: val,
570 Type: "s",
571 BlockID: parentBlock.ID,
572 RootID: rootID,
573 Box: boxID,
574 Path: p,
575 }
576 attributes = append(attributes, attr)
577 }
578 case ast.NodeKramdownBlockIAL:
579 attrs := parse.IALValMap(n)
580 for name, val := range attrs {
581 if !isAttr(name) {
582 continue
583 }
584
585 attr := &Attribute{
586 ID: ast.NewNodeID(),
587 Name: name,
588 Value: val,
589 Type: "b",
590 BlockID: n.ID,
591 RootID: rootID,
592 Box: boxID,
593 Path: p,
594 }
595 attributes = append(attributes, attr)
596 }
597 }
598 return
599}
600
601func isAttr(name string) bool {
602 return strings.HasPrefix(name, "custom-") || "name" == name || "alias" == name || "memo" == name || "bookmark" == name || "fold" == name || "heading-fold" == name || "style" == name
603}
604
605func buildSpanFromNode(n *ast.Node, tree *parse.Tree, rootID, boxID, p string) (blocks []*Block, spans []*Span, assets []*Asset, attributes []*Attribute, walkStatus ast.WalkStatus) {
606 boxLocalPath := filepath.Join(util.DataDir, boxID)
607 docDirLocalPath := filepath.Join(boxLocalPath, p)
608 switch n.Type {
609 case ast.NodeImage:
610 text := n.Text()
611 markdown := treenode.ExportNodeStdMd(n, luteEngine)
612 parentBlock := treenode.ParentBlock(n)
613 span := &Span{
614 ID: ast.NewNodeID(),
615 BlockID: parentBlock.ID,
616 RootID: rootID,
617 Box: boxID,
618 Path: p,
619 Content: text,
620 Markdown: markdown,
621 Type: treenode.TypeAbbr(n.Type.String()),
622 IAL: treenode.IALStr(n),
623 }
624 spans = append(spans, span)
625 walkStatus = ast.WalkSkipChildren
626
627 destNode := n.ChildByType(ast.NodeLinkDest)
628 if nil == destNode {
629 return
630 }
631
632 // assetsLinkDestsInTree
633
634 if !util.IsAssetLinkDest(destNode.Tokens) {
635 return
636 }
637
638 dest := gulu.Str.FromBytes(destNode.Tokens)
639 var title string
640 if titleNode := n.ChildByType(ast.NodeLinkTitle); nil != titleNode {
641 title = gulu.Str.FromBytes(titleNode.Tokens)
642 }
643
644 var hash string
645 var hashErr error
646 if lp := assetLocalPath(dest, boxLocalPath, docDirLocalPath); "" != lp {
647 if !gulu.File.IsDir(lp) {
648 hash, hashErr = util.GetEtag(lp)
649 if nil != hashErr {
650 logging.LogErrorf("calc asset [%s] hash failed: %s", lp, hashErr)
651 }
652 }
653 }
654 name, _ := util.LastID(dest)
655 asset := &Asset{
656 ID: ast.NewNodeID(),
657 BlockID: parentBlock.ID,
658 RootID: rootID,
659 Box: boxID,
660 DocPath: p,
661 Path: dest,
662 Name: name,
663 Title: title,
664 Hash: hash,
665 }
666 assets = append(assets, asset)
667 return
668 case ast.NodeTextMark:
669 typ := treenode.TypeAbbr(n.Type.String()) + " " + n.TextMarkType
670 text := strings.TrimSuffix(n.Content(), string(gulu.ZWJ))
671 markdown := treenode.ExportNodeStdMd(n, luteEngine)
672 markdown = strings.ReplaceAll(markdown, string(gulu.ZWJ)+"#", "#")
673 parentBlock := treenode.ParentBlock(n)
674 span := &Span{
675 ID: ast.NewNodeID(),
676 BlockID: parentBlock.ID,
677 RootID: rootID,
678 Box: boxID,
679 Path: p,
680 Content: text,
681 Markdown: markdown,
682 Type: typ,
683 IAL: treenode.IALStr(n),
684 }
685 spans = append(spans, span)
686
687 if n.IsTextMarkType("a") {
688 dest := n.TextMarkAHref
689 if util.IsAssetLinkDest([]byte(dest)) {
690 var title string
691 if titleNode := n.ChildByType(ast.NodeLinkTitle); nil != titleNode {
692 title = gulu.Str.FromBytes(titleNode.Tokens)
693 }
694
695 var hash string
696 var hashErr error
697 if lp := assetLocalPath(dest, boxLocalPath, docDirLocalPath); "" != lp {
698 if !gulu.File.IsDir(lp) {
699 hash, hashErr = util.GetEtag(lp)
700 if nil != hashErr {
701 logging.LogErrorf("calc asset [%s] hash failed: %s", lp, hashErr)
702 }
703 }
704 }
705 name, _ := util.LastID(dest)
706 asset := &Asset{
707 ID: ast.NewNodeID(),
708 BlockID: parentBlock.ID,
709 RootID: rootID,
710 Box: boxID,
711 DocPath: p,
712 Path: dest,
713 Name: name,
714 Title: title,
715 Hash: hash,
716 }
717 assets = append(assets, asset)
718 }
719 }
720 walkStatus = ast.WalkSkipChildren
721 return
722 case ast.NodeDocument:
723 if asset := docTitleImgAsset(n, boxLocalPath, docDirLocalPath); nil != asset {
724 assets = append(assets, asset)
725 }
726 if tags := docTagSpans(n); 0 < len(tags) {
727 spans = append(spans, tags...)
728 }
729 case ast.NodeInlineHTML, ast.NodeHTMLBlock, ast.NodeIFrame, ast.NodeWidget, ast.NodeAudio, ast.NodeVideo:
730 nodes, err := html.ParseFragment(bytes.NewReader(n.Tokens), &html.Node{Type: html.ElementNode})
731 if err != nil {
732 logging.LogErrorf("parse HTML failed: %s", err)
733 walkStatus = ast.WalkContinue
734 return
735 }
736 if 1 > len(nodes) &&
737 ast.NodeHTMLBlock != n.Type { // HTML 块若内容为空时无法在数据库中查询到 https://github.com/siyuan-note/siyuan/issues/4691
738 walkStatus = ast.WalkContinue
739 return
740 }
741
742 if ast.NodeHTMLBlock == n.Type || ast.NodeIFrame == n.Type || ast.NodeWidget == n.Type || ast.NodeAudio == n.Type || ast.NodeVideo == n.Type {
743 b, attrs := buildBlockFromNode(n, tree)
744 blocks = append(blocks, b)
745 attributes = append(attributes, attrs...)
746 }
747
748 if ast.NodeInlineHTML == n.Type {
749 // 没有行级 HTML,只有块级 HTML,这里转换为块
750 n.ID = ast.NewNodeID()
751 n.SetIALAttr("id", n.ID)
752 n.SetIALAttr("updated", n.ID[:14])
753 b, attrs := buildBlockFromNode(n, tree)
754 b.Type = ast.NodeHTMLBlock.String()
755 blocks = append(blocks, b)
756 attributes = append(attributes, attrs...)
757 walkStatus = ast.WalkContinue
758 logging.LogWarnf("inline HTML [%s] is converted to HTML block ", n.Tokens)
759 return
760 }
761
762 if 1 > len(nodes) {
763 walkStatus = ast.WalkContinue
764 return
765 }
766
767 var src []byte
768 for _, attr := range nodes[0].Attr {
769 if "src" == attr.Key || strings.HasPrefix(attr.Key, "data-assets") || strings.HasPrefix(attr.Key, "custom-data-assets") {
770 src = gulu.Str.ToBytes(attr.Val)
771 break
772 }
773 }
774 if 1 > len(src) {
775 walkStatus = ast.WalkContinue
776 return
777 }
778
779 if !util.IsAssetLinkDest(src) {
780 walkStatus = ast.WalkContinue
781 return
782 }
783
784 dest := string(src)
785 var hash string
786 var hashErr error
787 if lp := assetLocalPath(dest, boxLocalPath, docDirLocalPath); "" != lp {
788 hash, hashErr = util.GetEtag(lp)
789 if nil != hashErr {
790 logging.LogErrorf("calc asset [%s] hash failed: %s", lp, hashErr)
791 }
792 }
793
794 parentBlock := treenode.ParentBlock(n)
795 if ast.NodeInlineHTML != n.Type {
796 parentBlock = n
797 }
798 name, _ := util.LastID(dest)
799 asset := &Asset{
800 ID: ast.NewNodeID(),
801 BlockID: parentBlock.ID,
802 RootID: rootID,
803 Box: boxID,
804 DocPath: p,
805 Path: dest,
806 Name: name,
807 Title: "",
808 Hash: hash,
809 }
810 assets = append(assets, asset)
811 walkStatus = ast.WalkSkipChildren
812 return
813 }
814 walkStatus = -1
815 return
816}
817
818func BuildBlockFromNode(n *ast.Node, tree *parse.Tree) (block *Block) {
819 block, _ = buildBlockFromNode(n, tree)
820 return
821}
822
823func buildBlockFromNode(n *ast.Node, tree *parse.Tree) (block *Block, attributes []*Attribute) {
824 boxID := tree.Box
825 p := tree.Path
826 rootID := tree.Root.ID
827 name := html.UnescapeString(n.IALAttr("name"))
828 alias := html.UnescapeString(n.IALAttr("alias"))
829 memo := html.UnescapeString(n.IALAttr("memo"))
830 tag := tagFromNode(n)
831
832 var content, fcontent, markdown, parentID string
833 ialContent := treenode.IALStr(n)
834 hash := treenode.NodeHash(n, tree, luteEngine)
835 var length int
836 if ast.NodeDocument == n.Type {
837 content = n.IALAttr("title")
838 fcontent = content
839 length = utf8.RuneCountInString(fcontent)
840 } else if n.IsContainerBlock() {
841 markdown = treenode.ExportNodeStdMd(n, luteEngine)
842 if !treenode.IsNodeOCRed(n) {
843 util.PushNodeOCRQueue(n)
844 }
845 content = NodeStaticContent(n, nil, true, indexAssetPath, true)
846
847 fc := treenode.FirstLeafBlock(n)
848 if !treenode.IsNodeOCRed(fc) {
849 util.PushNodeOCRQueue(fc)
850 }
851 fcontent = NodeStaticContent(fc, nil, true, false, true)
852
853 parentID = n.Parent.ID
854 if h := treenode.HeadingParent(n); nil != h { // 如果在标题块下方,则将标题块作为父节点
855 parentID = h.ID
856 }
857 length = utf8.RuneCountInString(fcontent)
858 } else {
859 markdown = treenode.ExportNodeStdMd(n, luteEngine)
860 if !treenode.IsNodeOCRed(n) {
861 util.PushNodeOCRQueue(n)
862 }
863 content = NodeStaticContent(n, nil, true, indexAssetPath, true)
864
865 parentID = n.Parent.ID
866 if h := treenode.HeadingParent(n); nil != h {
867 parentID = h.ID
868 }
869 length = utf8.RuneCountInString(content)
870 }
871
872 // 剔除零宽空格 Database index content/markdown values no longer contain zero-width spaces https://github.com/siyuan-note/siyuan/issues/15204
873 fcontent = strings.ReplaceAll(fcontent, editor.Zwsp, "")
874 content = strings.ReplaceAll(content, editor.Zwsp, "")
875 markdown = strings.ReplaceAll(markdown, editor.Zwsp, "")
876
877 // 剔除标签结尾处的零宽连字符 Improve search for emojis in tags https://github.com/siyuan-note/siyuan/issues/15391
878 fcontent = strings.ReplaceAll(fcontent, string(gulu.ZWJ)+"#", "#")
879 content = strings.ReplaceAll(content, string(gulu.ZWJ)+"#", "#")
880 markdown = strings.ReplaceAll(markdown, string(gulu.ZWJ)+"#", "#")
881
882 block = &Block{
883 ID: n.ID,
884 ParentID: parentID,
885 RootID: rootID,
886 Hash: hash,
887 Box: boxID,
888 Path: p,
889 HPath: tree.HPath,
890 Name: name,
891 Alias: alias,
892 Memo: memo,
893 Tag: tag,
894 Content: content,
895 FContent: fcontent,
896 Markdown: markdown,
897 Length: length,
898 Type: treenode.TypeAbbr(n.Type.String()),
899 SubType: treenode.SubTypeAbbr(n),
900 IAL: ialContent,
901 Sort: nSort(n),
902 Created: util.TimeFromID(n.ID),
903 Updated: n.IALAttr("updated"),
904 }
905
906 attrs := parse.IAL2Map(n.KramdownIAL)
907 for attrName, attrVal := range attrs {
908 if !isAttr(attrName) {
909 continue
910 }
911
912 attr := &Attribute{
913 ID: ast.NewNodeID(),
914 Name: attrName,
915 Value: attrVal,
916 Type: "b",
917 BlockID: n.ID,
918 RootID: rootID,
919 Box: boxID,
920 Path: p,
921 }
922 attributes = append(attributes, attr)
923 }
924 return
925}
926
927func tagFromNode(node *ast.Node) (ret string) {
928 tagBuilder := bytes.Buffer{}
929
930 if ast.NodeDocument == node.Type {
931 tagIAL := html.UnescapeString(node.IALAttr("tags"))
932 tags := strings.Split(tagIAL, ",")
933 for _, t := range tags {
934 t = strings.TrimSpace(t)
935 if "" == t {
936 continue
937 }
938 tagBuilder.WriteString("#")
939 tagBuilder.WriteString(t)
940 tagBuilder.WriteString("# ")
941 }
942 return strings.TrimSpace(tagBuilder.String())
943 }
944
945 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
946 if !entering {
947 return ast.WalkContinue
948 }
949
950 if n.IsTextMarkType("tag") {
951 tagBuilder.WriteString("#")
952 tagBuilder.WriteString(n.Content())
953 tagBuilder.WriteString("# ")
954 }
955 return ast.WalkContinue
956 })
957 return strings.TrimSpace(tagBuilder.String())
958}
959
960func deleteByBoxTx(tx *sql.Tx, box string) (err error) {
961 if err = deleteBlocksByBoxTx(tx, box); err != nil {
962 return
963 }
964 if err = deleteSpansByBoxTx(tx, box); err != nil {
965 return
966 }
967 if err = deleteAssetsByBoxTx(tx, box); err != nil {
968 return
969 }
970 if err = deleteAttributesByBoxTx(tx, box); err != nil {
971 return
972 }
973 if err = deleteBlockRefsByBoxTx(tx, box); err != nil {
974 return
975 }
976 if err = deleteFileAnnotationRefsByBoxTx(tx, box); err != nil {
977 return
978 }
979 return
980}
981
982func deleteBlocksByIDs(tx *sql.Tx, ids []string) (err error) {
983 if 1 > len(ids) {
984 return
985 }
986
987 var ftsIDs []string
988 for _, id := range ids {
989 removeBlockCache(id)
990 ftsIDs = append(ftsIDs, "\""+id+"\"")
991 }
992
993 var rowIDs []string
994 stmt := "SELECT ROWID FROM blocks WHERE id IN (" + strings.Join(ftsIDs, ",") + ")"
995 rows, err := tx.Query(stmt)
996 if err != nil {
997 logging.LogErrorf("query block rowIDs failed: %s", err)
998 return
999 }
1000 for rows.Next() {
1001 var rowID int64
1002 if err = rows.Scan(&rowID); err != nil {
1003 logging.LogErrorf("scan block rowID failed: %s", err)
1004 rows.Close()
1005 return
1006 }
1007 rowIDs = append(rowIDs, strconv.FormatInt(rowID, 10))
1008 }
1009 rows.Close()
1010
1011 if 1 > len(rowIDs) {
1012 return
1013 }
1014
1015 stmt = "DELETE FROM blocks WHERE ROWID IN (" + strings.Join(rowIDs, ",") + ")"
1016 if err = execStmtTx(tx, stmt); err != nil {
1017 return
1018 }
1019
1020 stmt = "DELETE FROM blocks_fts WHERE ROWID IN (" + strings.Join(rowIDs, ",") + ")"
1021 if err = execStmtTx(tx, stmt); err != nil {
1022 return
1023 }
1024
1025 if !caseSensitive {
1026 stmt = "DELETE FROM blocks_fts_case_insensitive WHERE ROWID IN (" + strings.Join(rowIDs, ",") + ")"
1027 if err = execStmtTx(tx, stmt); err != nil {
1028 return
1029 }
1030 }
1031 return
1032}
1033
1034func deleteBlocksByBoxTx(tx *sql.Tx, box string) (err error) {
1035 stmt := "DELETE FROM blocks WHERE box = ?"
1036 if err = execStmtTx(tx, stmt, box); err != nil {
1037 return
1038 }
1039 stmt = "DELETE FROM blocks_fts WHERE box = ?"
1040 if err = execStmtTx(tx, stmt, box); err != nil {
1041 return
1042 }
1043 if !caseSensitive {
1044 stmt = "DELETE FROM blocks_fts_case_insensitive WHERE box = ?"
1045 if err = execStmtTx(tx, stmt, box); err != nil {
1046 return
1047 }
1048 }
1049 ClearCache()
1050 return
1051}
1052
1053func deleteSpansByRootID(tx *sql.Tx, rootID string) (err error) {
1054 stmt := "DELETE FROM spans WHERE root_id =?"
1055 err = execStmtTx(tx, stmt, rootID)
1056 return
1057}
1058
1059func deleteSpansByBoxTx(tx *sql.Tx, box string) (err error) {
1060 stmt := "DELETE FROM spans WHERE box = ?"
1061 err = execStmtTx(tx, stmt, box)
1062 return
1063}
1064
1065func deleteAssetsByRootID(tx *sql.Tx, rootID string) (err error) {
1066 stmt := "DELETE FROM assets WHERE root_id = ?"
1067 err = execStmtTx(tx, stmt, rootID)
1068 return
1069}
1070
1071func deleteAssetsByBoxTx(tx *sql.Tx, box string) (err error) {
1072 stmt := "DELETE FROM assets WHERE box = ?"
1073 err = execStmtTx(tx, stmt, box)
1074 return
1075}
1076
1077func deleteAttributesByRootID(tx *sql.Tx, rootID string) (err error) {
1078 stmt := "DELETE FROM attributes WHERE root_id = ?"
1079 err = execStmtTx(tx, stmt, rootID)
1080 return
1081
1082}
1083
1084func deleteAttributesByBoxTx(tx *sql.Tx, box string) (err error) {
1085 stmt := "DELETE FROM attributes WHERE box = ?"
1086 err = execStmtTx(tx, stmt, box)
1087 return
1088}
1089
1090func deleteRefsByPath(tx *sql.Tx, box, path string) (err error) {
1091 stmt := "DELETE FROM refs WHERE box = ? AND path = ?"
1092 err = execStmtTx(tx, stmt, box, path)
1093 return
1094}
1095
1096func deleteRefsByPathTx(tx *sql.Tx, box, path string) (err error) {
1097 stmt := "DELETE FROM refs WHERE box = ? AND path = ?"
1098 err = execStmtTx(tx, stmt, box, path)
1099 return
1100}
1101
1102func deleteRefsByBoxTx(tx *sql.Tx, box string) (err error) {
1103 if err = deleteFileAnnotationRefsByBoxTx(tx, box); err != nil {
1104 return
1105 }
1106 return deleteBlockRefsByBoxTx(tx, box)
1107}
1108
1109func deleteBlockRefsByBoxTx(tx *sql.Tx, box string) (err error) {
1110 stmt := "DELETE FROM refs WHERE box = ?"
1111 err = execStmtTx(tx, stmt, box)
1112 return
1113}
1114
1115func deleteFileAnnotationRefsByPath(tx *sql.Tx, box, path string) (err error) {
1116 stmt := "DELETE FROM file_annotation_refs WHERE box = ? AND path = ?"
1117 err = execStmtTx(tx, stmt, box, path)
1118 return
1119}
1120
1121func deleteFileAnnotationRefsByPathTx(tx *sql.Tx, box, path string) (err error) {
1122 stmt := "DELETE FROM file_annotation_refs WHERE box = ? AND path = ?"
1123 err = execStmtTx(tx, stmt, box, path)
1124 return
1125}
1126
1127func deleteFileAnnotationRefsByBoxTx(tx *sql.Tx, box string) (err error) {
1128 stmt := "DELETE FROM file_annotation_refs WHERE box = ?"
1129 err = execStmtTx(tx, stmt, box)
1130 return
1131}
1132
1133func deleteByRootID(tx *sql.Tx, rootID string, context map[string]interface{}) (err error) {
1134 stmt := "DELETE FROM blocks WHERE root_id = ?"
1135 if err = execStmtTx(tx, stmt, rootID); err != nil {
1136 return
1137 }
1138 stmt = "DELETE FROM blocks_fts WHERE root_id = ?"
1139 if err = execStmtTx(tx, stmt, rootID); err != nil {
1140 return
1141 }
1142 if !caseSensitive {
1143 stmt = "DELETE FROM blocks_fts_case_insensitive WHERE root_id = ?"
1144 if err = execStmtTx(tx, stmt, rootID); err != nil {
1145 return
1146 }
1147 }
1148 stmt = "DELETE FROM spans WHERE root_id = ?"
1149 if err = execStmtTx(tx, stmt, rootID); err != nil {
1150 return
1151 }
1152 stmt = "DELETE FROM assets WHERE root_id = ?"
1153 if err = execStmtTx(tx, stmt, rootID); err != nil {
1154 return
1155 }
1156 stmt = "DELETE FROM refs WHERE root_id = ?"
1157 if err = execStmtTx(tx, stmt, rootID); err != nil {
1158 return
1159 }
1160 stmt = "DELETE FROM file_annotation_refs WHERE root_id = ?"
1161 if err = execStmtTx(tx, stmt, rootID); err != nil {
1162 return
1163 }
1164 stmt = "DELETE FROM attributes WHERE root_id = ?"
1165 if err = execStmtTx(tx, stmt, rootID); err != nil {
1166 return
1167 }
1168 ClearCache()
1169 eventbus.Publish(eventbus.EvtSQLDeleteBlocks, context, rootID)
1170 return
1171}
1172
1173func batchDeleteByRootIDs(tx *sql.Tx, rootIDs []string, context map[string]interface{}) (err error) {
1174 if 1 > len(rootIDs) {
1175 return
1176 }
1177
1178 ids := strings.Join(rootIDs, "','")
1179 ids = "('" + ids + "')"
1180 stmt := "DELETE FROM blocks WHERE root_id IN " + ids
1181 if err = execStmtTx(tx, stmt); err != nil {
1182 return
1183 }
1184 stmt = "DELETE FROM blocks_fts WHERE root_id IN " + ids
1185 if err = execStmtTx(tx, stmt); err != nil {
1186 return
1187 }
1188 if !caseSensitive {
1189 stmt = "DELETE FROM blocks_fts_case_insensitive WHERE root_id IN " + ids
1190 if err = execStmtTx(tx, stmt); err != nil {
1191 return
1192 }
1193 }
1194 stmt = "DELETE FROM spans WHERE root_id IN " + ids
1195 if err = execStmtTx(tx, stmt); err != nil {
1196 return
1197 }
1198 stmt = "DELETE FROM assets WHERE root_id IN " + ids
1199 if err = execStmtTx(tx, stmt); err != nil {
1200 return
1201 }
1202 stmt = "DELETE FROM refs WHERE root_id IN " + ids
1203 if err = execStmtTx(tx, stmt); err != nil {
1204 return
1205 }
1206 stmt = "DELETE FROM file_annotation_refs WHERE root_id IN " + ids
1207 if err = execStmtTx(tx, stmt); err != nil {
1208 return
1209 }
1210 stmt = "DELETE FROM attributes WHERE root_id IN " + ids
1211 if err = execStmtTx(tx, stmt); err != nil {
1212 return
1213 }
1214 ClearCache()
1215 eventbus.Publish(eventbus.EvtSQLDeleteBlocks, context, fmt.Sprintf("%d", len(rootIDs)))
1216 return
1217}
1218
1219func batchDeleteByPathPrefix(tx *sql.Tx, boxID, pathPrefix string) (err error) {
1220 stmt := "DELETE FROM blocks WHERE box = ? AND path LIKE ?"
1221 if err = execStmtTx(tx, stmt, boxID, pathPrefix+"%"); err != nil {
1222 return
1223 }
1224 stmt = "DELETE FROM blocks_fts WHERE box = ? AND path LIKE ?"
1225 if err = execStmtTx(tx, stmt, boxID, pathPrefix+"%"); err != nil {
1226 return
1227 }
1228 if !caseSensitive {
1229 stmt = "DELETE FROM blocks_fts_case_insensitive WHERE box = ? AND path LIKE ?"
1230 if err = execStmtTx(tx, stmt, boxID, pathPrefix+"%"); err != nil {
1231 return
1232 }
1233 }
1234 stmt = "DELETE FROM spans WHERE box = ? AND path LIKE ?"
1235 if err = execStmtTx(tx, stmt, boxID, pathPrefix+"%"); err != nil {
1236 return
1237 }
1238 stmt = "DELETE FROM assets WHERE box = ? AND docpath LIKE ?"
1239 if err = execStmtTx(tx, stmt, boxID, pathPrefix+"%"); err != nil {
1240 return
1241 }
1242 stmt = "DELETE FROM refs WHERE box = ? AND path LIKE ?"
1243 if err = execStmtTx(tx, stmt, boxID, pathPrefix+"%"); err != nil {
1244 return
1245 }
1246 stmt = "DELETE FROM file_annotation_refs WHERE box = ? AND path LIKE ?"
1247 if err = execStmtTx(tx, stmt, boxID, pathPrefix+"%"); err != nil {
1248 return
1249 }
1250 stmt = "DELETE FROM attributes WHERE box = ? AND path LIKE ?"
1251 if err = execStmtTx(tx, stmt, boxID, pathPrefix+"%"); err != nil {
1252 return
1253 }
1254 ClearCache()
1255 return
1256}
1257
1258func batchUpdatePath(tx *sql.Tx, tree *parse.Tree, context map[string]interface{}) (err error) {
1259 ialContent := treenode.IALStr(tree.Root)
1260 stmt := "UPDATE blocks SET box = ?, path = ?, hpath = ?, ial = ? WHERE root_id = ?"
1261 if err = execStmtTx(tx, stmt, tree.Box, tree.Path, tree.HPath, ialContent, tree.ID); err != nil {
1262 return
1263 }
1264 stmt = "UPDATE blocks_fts SET box = ?, path = ?, hpath = ?, ial = ? WHERE root_id = ?"
1265 if err = execStmtTx(tx, stmt, tree.Box, tree.Path, tree.HPath, ialContent, tree.ID); err != nil {
1266 return
1267 }
1268 if !caseSensitive {
1269 stmt = "UPDATE blocks_fts_case_insensitive SET box = ?, path = ?, hpath = ?, ial = ? WHERE root_id = ?"
1270 if err = execStmtTx(tx, stmt, tree.Box, tree.Path, tree.HPath, ialContent, tree.ID); err != nil {
1271 return
1272 }
1273 }
1274 ClearCache()
1275 evtHash := fmt.Sprintf("%x", sha256.Sum256([]byte(tree.ID)))[:7]
1276 eventbus.Publish(eventbus.EvtSQLUpdateBlocksHPaths, context, 1, evtHash)
1277 return
1278}
1279
1280func batchUpdateHPath(tx *sql.Tx, tree *parse.Tree, context map[string]interface{}) (err error) {
1281 ialContent := treenode.IALStr(tree.Root)
1282 stmt := "UPDATE blocks SET hpath = ?, ial = ? WHERE root_id = ?"
1283 if err = execStmtTx(tx, stmt, tree.HPath, ialContent, tree.ID); err != nil {
1284 return
1285 }
1286 stmt = "UPDATE blocks_fts SET hpath = ?, ial = ? WHERE root_id = ?"
1287 if err = execStmtTx(tx, stmt, tree.HPath, ialContent, tree.ID); err != nil {
1288 return
1289 }
1290 if !caseSensitive {
1291 stmt = "UPDATE blocks_fts_case_insensitive SET hpath = ?, ial = ? WHERE root_id = ?"
1292 if err = execStmtTx(tx, stmt, tree.HPath, ialContent, tree.ID); err != nil {
1293 return
1294 }
1295 }
1296 ClearCache()
1297 evtHash := fmt.Sprintf("%x", sha256.Sum256([]byte(tree.ID)))[:7]
1298 eventbus.Publish(eventbus.EvtSQLUpdateBlocksHPaths, context, 1, evtHash)
1299 return
1300}
1301
1302func CloseDatabase() {
1303 if err := closeDatabase(); err != nil {
1304 logging.LogErrorf("close database failed: %s", err)
1305 return
1306 }
1307 if err := historyDB.Close(); err != nil {
1308 logging.LogErrorf("close history database failed: %s", err)
1309 return
1310 }
1311 if err := assetContentDB.Close(); err != nil {
1312 logging.LogErrorf("close asset content database failed: %s", err)
1313 return
1314 }
1315 treenode.CloseDatabase()
1316 logging.LogInfof("closed database")
1317}
1318
1319func queryRow(query string, args ...interface{}) *sql.Row {
1320 query = strings.TrimSpace(query)
1321 if "" == query {
1322 logging.LogErrorf("statement is empty")
1323 return nil
1324 }
1325 if nil == db {
1326 return nil
1327 }
1328 return db.QueryRow(query, args...)
1329}
1330
1331func query(query string, args ...interface{}) (*sql.Rows, error) {
1332 query = strings.TrimSpace(query)
1333 if "" == query {
1334 return nil, errors.New("statement is empty")
1335 }
1336 if nil == db {
1337 return nil, errors.New("database is nil")
1338 }
1339 return db.Query(query, args...)
1340}
1341
1342func beginTx() (tx *sql.Tx, err error) {
1343 if tx, err = db.Begin(); err != nil {
1344 logging.LogErrorf("begin tx failed: %s\n %s", err, logging.ShortStack())
1345 if strings.Contains(err.Error(), "database is locked") {
1346 os.Exit(logging.ExitCodeReadOnlyDatabase)
1347 }
1348 }
1349 return
1350}
1351
1352func commitTx(tx *sql.Tx) (err error) {
1353 if nil == tx {
1354 logging.LogErrorf("tx is nil")
1355 return errors.New("tx is nil")
1356 }
1357
1358 if err = tx.Commit(); err != nil {
1359 logging.LogErrorf("commit tx failed: %s\n %s", err, logging.ShortStack())
1360 }
1361 return
1362}
1363
1364func beginHistoryTx() (tx *sql.Tx, err error) {
1365 if tx, err = historyDB.Begin(); err != nil {
1366 logging.LogErrorf("begin history tx failed: %s\n %s", err, logging.ShortStack())
1367 if strings.Contains(err.Error(), "database is locked") {
1368 os.Exit(logging.ExitCodeReadOnlyDatabase)
1369 }
1370 }
1371 return
1372}
1373
1374func commitHistoryTx(tx *sql.Tx) (err error) {
1375 if nil == tx {
1376 logging.LogErrorf("tx is nil")
1377 return errors.New("tx is nil")
1378 }
1379
1380 if err = tx.Commit(); err != nil {
1381 logging.LogErrorf("commit tx failed: %s\n %s", err, logging.ShortStack())
1382 }
1383 return
1384}
1385
1386func beginAssetContentTx() (tx *sql.Tx, err error) {
1387 if tx, err = assetContentDB.Begin(); err != nil {
1388 logging.LogErrorf("begin asset content tx failed: %s\n %s", err, logging.ShortStack())
1389 if strings.Contains(err.Error(), "database is locked") {
1390 os.Exit(logging.ExitCodeReadOnlyDatabase)
1391 }
1392 }
1393 return
1394}
1395
1396func commitAssetContentTx(tx *sql.Tx) (err error) {
1397 if nil == tx {
1398 logging.LogErrorf("tx is nil")
1399 return errors.New("tx is nil")
1400 }
1401
1402 if err = tx.Commit(); err != nil {
1403 logging.LogErrorf("commit tx failed: %s\n %s", err, logging.ShortStack())
1404 }
1405 return
1406}
1407
1408func prepareExecInsertTx(tx *sql.Tx, stmtSQL string, args []interface{}) (err error) {
1409 stmt, err := tx.Prepare(stmtSQL)
1410 if err != nil {
1411 return
1412 }
1413 if _, err = stmt.Exec(args...); err != nil {
1414 logging.LogErrorf("exec database stmt [%s] failed: %s", stmtSQL, err)
1415 return
1416 }
1417 return
1418}
1419
1420func execStmtTx(tx *sql.Tx, stmt string, args ...interface{}) (err error) {
1421 if _, err = tx.Exec(stmt, args...); err != nil {
1422 if strings.Contains(err.Error(), "database disk image is malformed") {
1423 tx.Rollback()
1424 closeDatabase()
1425 removeDatabaseFile()
1426 logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "database disk image [%s] is malformed, please restart SiYuan kernel to rebuild it", util.DBPath)
1427 }
1428 logging.LogErrorf("exec database stmt [%s] failed: %s\n %s", stmt, err, logging.ShortStack())
1429 return
1430 }
1431 return
1432}
1433
1434func nSort(n *ast.Node) int {
1435 switch n.Type {
1436 // 以下为块级元素
1437 case ast.NodeHeading:
1438 return 5
1439 case ast.NodeParagraph:
1440 return 10
1441 case ast.NodeCodeBlock:
1442 return 10
1443 case ast.NodeMathBlock:
1444 return 10
1445 case ast.NodeTable:
1446 return 10
1447 case ast.NodeHTMLBlock:
1448 return 10
1449 case ast.NodeList:
1450 return 20
1451 case ast.NodeListItem:
1452 return 20
1453 case ast.NodeBlockquote:
1454 return 20
1455 case ast.NodeSuperBlock:
1456 return 30
1457 case ast.NodeAttributeView:
1458 return 30
1459 case ast.NodeDocument:
1460 return 0
1461 case ast.NodeText, ast.NodeTextMark:
1462 if n.IsTextMarkType("tag") {
1463 return 205
1464 }
1465 return 200
1466 }
1467 return 100
1468}
1469
1470func ialAttr(ial, name string) (ret string) {
1471 idx := strings.Index(ial, name)
1472 if 0 > idx {
1473 return ""
1474 }
1475 ret = ial[idx+len(name)+2:]
1476 ret = ret[:strings.Index(ret, "\"")]
1477 return
1478}
1479
1480func removeDatabaseFile() (err error) {
1481 err = os.RemoveAll(util.DBPath)
1482 if err != nil {
1483 return
1484 }
1485 err = os.RemoveAll(util.DBPath + "-shm")
1486 if err != nil {
1487 return
1488 }
1489 err = os.RemoveAll(util.DBPath + "-wal")
1490 if err != nil {
1491 return
1492 }
1493 return
1494}
1495
1496func closeDatabase() (err error) {
1497 if nil == db {
1498 return
1499 }
1500
1501 err = db.Close()
1502 debug.FreeOSMemory()
1503 runtime.GC() // 没有这句的话文件句柄不会释放,后面就无法删除文件
1504 return
1505}
1506
1507func SQLTemplateFuncs(templateFuncMap *template.FuncMap) {
1508 (*templateFuncMap)["queryBlocks"] = func(stmt string, args ...string) (retBlocks []*Block) {
1509 for _, arg := range args {
1510 stmt = strings.Replace(stmt, "?", arg, 1)
1511 }
1512 retBlocks = SelectBlocksRawStmt(stmt, 1, 512)
1513 return
1514 }
1515 (*templateFuncMap)["getBlock"] = func(arg any) (retBlock *Block) {
1516 switch v := arg.(type) {
1517 case string:
1518 retBlock = GetBlock(v)
1519 case map[string]interface{}:
1520 if id, ok := v["id"]; ok {
1521 retBlock = GetBlock(id.(string))
1522 }
1523 }
1524 return
1525 }
1526 (*templateFuncMap)["querySpans"] = func(stmt string, args ...string) (retSpans []*Span) {
1527 for _, arg := range args {
1528 stmt = strings.Replace(stmt, "?", arg, 1)
1529 }
1530 retSpans = SelectSpansRawStmt(stmt, 512)
1531 return
1532 }
1533 (*templateFuncMap)["querySQL"] = func(stmt string) (ret []map[string]interface{}) {
1534 ret, _ = Query(stmt, 1024)
1535 return
1536 }
1537}
1538
1539func Vacuum() {
1540 if nil != db {
1541 if _, err := db.Exec("VACUUM"); nil != err {
1542 logging.LogErrorf("vacuum database failed: %s", err)
1543 }
1544 }
1545 if nil != historyDB {
1546 if _, err := historyDB.Exec("VACUUM"); nil != err {
1547 logging.LogErrorf("vacuum history database failed: %s", err)
1548 }
1549 }
1550 if nil != assetContentDB {
1551 if _, err := assetContentDB.Exec("VACUUM"); nil != err {
1552 logging.LogErrorf("vacuum asset content database failed: %s", err)
1553 }
1554 }
1555 return
1556}