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}