From 7d992ce1753cdd6a643be05a6e9a4dd4375d5806 Mon Sep 17 00:00:00 2001
From: Daniel <845765@qq.com>
Date: Fri, 4 Aug 2023 12:05:29 +0800
Subject: [PATCH] :sparkles: Support for searching asset content
https://github.com/siyuan-note/siyuan/issues/8874
---
app/appearance/langs/en_US.json | 4 +-
app/appearance/langs/es_ES.json | 4 +-
app/appearance/langs/fr_FR.json | 4 +-
app/appearance/langs/zh_CHT.json | 4 +-
app/appearance/langs/zh_CN.json | 4 +-
kernel/cache/asset.go | 2 +-
kernel/go.mod | 2 +-
kernel/go.sum | 2 +
kernel/job/cron.go | 1 +
kernel/main.go | 1 +
kernel/mobile/kernel.go | 1 +
kernel/model/asset_content.go | 155 ++++++++++++++++++++++++++++++
kernel/model/index.go | 12 +++
kernel/sql/asset_content.go | 96 ++++++++++++++++++
kernel/sql/database.go | 109 ++++++++++++++++++---
kernel/sql/queue_asset_content.go | 147 ++++++++++++++++++++++++++++
kernel/sql/queue_history.go | 28 +-----
kernel/task/queue.go | 30 +++---
kernel/util/runtime.go | 3 +-
kernel/util/working.go | 34 ++++---
kernel/util/working_mobile.go | 1 +
21 files changed, 568 insertions(+), 76 deletions(-)
create mode 100644 kernel/model/asset_content.go
create mode 100644 kernel/sql/asset_content.go
create mode 100644 kernel/sql/queue_asset_content.go
diff --git a/app/appearance/langs/en_US.json b/app/appearance/langs/en_US.json
index 818798c3e..52230ce99 100644
--- a/app/appearance/langs/en_US.json
+++ b/app/appearance/langs/en_US.json
@@ -1260,6 +1260,8 @@
"212": "There are some defects in the current version of cloud data sync, please upgrade to the latest version. Sorry for the inconvenience",
"213": "Cloud verification failed, please try to upgrade to the latest version and log in again before syncing",
"214": "This function needs to be signed in to use",
- "215": "Save failed: The target file is being used by another program"
+ "215": "Save failed: The target file is being used by another program",
+ "216": "Rebuilding asset content data index, please wait...",
+ "217": "[%d/%d] Created asset content data index"
}
}
diff --git a/app/appearance/langs/es_ES.json b/app/appearance/langs/es_ES.json
index 34cb88c21..b40a635ad 100644
--- a/app/appearance/langs/es_ES.json
+++ b/app/appearance/langs/es_ES.json
@@ -1260,6 +1260,8 @@
"212": "Hay algunos defectos en la versi\u00f3n actual de sincronizaci\u00f3n de datos en la nube, actualice a la versi\u00f3n m\u00e1s reciente. Disculpe las molestias",
"213": "La verificaci\u00f3n en la nube fall\u00f3, intente actualizar a la versi\u00f3n m\u00e1s reciente e inicie sesi\u00f3n de nuevo antes de sincronizar",
"214": "Esta función requiere iniciar sesión en la cuenta antes de poder usarla",
- "215": "Error al guardar: el archivo de destino está siendo utilizado por otro programa"
+ "215": "Error al guardar: el archivo de destino está siendo utilizado por otro programa",
+ "216": "Reconstruyendo el índice de datos de contenido de recursos, espere...",
+ "217": "[%d/%d] Índice de datos de contenido de activos creado"
}
}
diff --git a/app/appearance/langs/fr_FR.json b/app/appearance/langs/fr_FR.json
index 41255c4a0..cb5d12a72 100644
--- a/app/appearance/langs/fr_FR.json
+++ b/app/appearance/langs/fr_FR.json
@@ -1260,6 +1260,8 @@
"212": "Il y a quelques défauts dans la version actuelle de la synchronisation des données cloud, veuillez mettre à niveau vers la dernière version. Désolé pour le désagrément",
"213": "Échec de la vérification cloud, veuillez essayer de mettre à niveau vers la dernière version et de vous reconnecter avant de synchroniser",
"214": "La fonctionnalité nécessite un numéro de compte de connexion avant de pouvoir être utilisée",
- "215": "Échec de l'enregistrement : le fichier de destination est utilisé par un autre programme"
+ "215": "Échec de l'enregistrement : le fichier de destination est utilisé par un autre programme",
+ "216": "Reconstruction de l'index des données du contenu des ressources, veuillez patienter...",
+ "217": "[%d/%d] Création d'un index de données de contenu d'actif"
}
}
diff --git a/app/appearance/langs/zh_CHT.json b/app/appearance/langs/zh_CHT.json
index 2b98db1f1..eeea014a6 100644
--- a/app/appearance/langs/zh_CHT.json
+++ b/app/appearance/langs/zh_CHT.json
@@ -1260,6 +1260,8 @@
"212": "當前版本雲端數據同步存在一些缺陷,請升級到最新版,帶來不便,敬請諒解",
"213": "雲端校驗失敗,請嘗試升級到最新版並重新登錄後再進行同步",
"214": "該功能需要登錄賬號後才能使用",
- "215": "保存失敗:目標文件正在被其他程序佔用"
+ "215": "保存失敗:目標文件正在被其他程序佔用",
+ "216": "正在重建資源文件內容數據索引,請稍等...",
+ "217": "[%d/%d] 已經建立條資源文件內容數據索引"
}
}
diff --git a/app/appearance/langs/zh_CN.json b/app/appearance/langs/zh_CN.json
index 65f050aec..ce81da7a7 100644
--- a/app/appearance/langs/zh_CN.json
+++ b/app/appearance/langs/zh_CN.json
@@ -1260,6 +1260,8 @@
"212": "当前版本云端数据同步存在一些缺陷,请升级到最新版,带来不便,敬请谅解",
"213": "云端校验失败,请尝试升级到最新版并重新登录后再进行同步",
"214": "该功能需要登录账号后才能使用",
- "215": "保存失败:目标文件并且正在被其他程序占用"
+ "215": "保存失败:目标文件并且正在被其他程序占用",
+ "216": "正在重建资源文件内容数据索引,请稍等...",
+ "217": "[%d/%d] 已经建立条资源文件内容数据索引"
}
}
diff --git a/kernel/cache/asset.go b/kernel/cache/asset.go
index 5052d3563..3af22e8e4 100644
--- a/kernel/cache/asset.go
+++ b/kernel/cache/asset.go
@@ -79,7 +79,7 @@ func LoadAssets() {
assetsCache[path] = &Asset{
HName: hName,
Path: path,
- Updated: info.ModTime().UnixMilli(),
+ Updated: info.ModTime().Unix(),
}
return nil
})
diff --git a/kernel/go.mod b/kernel/go.mod
index 6c5358483..aa131217b 100644
--- a/kernel/go.mod
+++ b/kernel/go.mod
@@ -47,7 +47,7 @@ require (
github.com/shirou/gopsutil/v3 v3.23.6
github.com/siyuan-note/dejavu v0.0.0-20230801123133-2edc24064c33
github.com/siyuan-note/encryption v0.0.0-20220713091850-5ecd92177b75
- github.com/siyuan-note/eventbus v0.0.0-20230702081350-6dde667e7112
+ github.com/siyuan-note/eventbus v0.0.0-20230804030110-cf250f838c80
github.com/siyuan-note/filelock v0.0.0-20230615140405-d05a21d49524
github.com/siyuan-note/httpclient v0.0.0-20230728124841-53922bac2be2
github.com/siyuan-note/logging v0.0.0-20230327073243-ebe83aec1493
diff --git a/kernel/go.sum b/kernel/go.sum
index 6f1100956..8a1339fee 100644
--- a/kernel/go.sum
+++ b/kernel/go.sum
@@ -296,6 +296,8 @@ github.com/siyuan-note/encryption v0.0.0-20220713091850-5ecd92177b75 h1:Bi7/7f29
github.com/siyuan-note/encryption v0.0.0-20220713091850-5ecd92177b75/go.mod h1:H8fyqqAbp9XreANjeSbc72zEdFfKTXYN34tc1TjZwtw=
github.com/siyuan-note/eventbus v0.0.0-20230702081350-6dde667e7112 h1:lb+8C+XEEEn/lcBtoXlrf5mZEoe0y0KlqiIGG93Gozc=
github.com/siyuan-note/eventbus v0.0.0-20230702081350-6dde667e7112/go.mod h1:Sqo4FYX5lAXu7gWkbEdJF0e6P57tNNVV4WDKYDctokI=
+github.com/siyuan-note/eventbus v0.0.0-20230804030110-cf250f838c80 h1:XghjHKJd+SiL0DkGYFVC+UGUDFtnR4v9gkAbPeh9Eq8=
+github.com/siyuan-note/eventbus v0.0.0-20230804030110-cf250f838c80/go.mod h1:Sqo4FYX5lAXu7gWkbEdJF0e6P57tNNVV4WDKYDctokI=
github.com/siyuan-note/filelock v0.0.0-20230615140405-d05a21d49524 h1:ZuxN5gwqtUOd1NkOkNhM4OlVWfjujY98zsR+zFi4x9g=
github.com/siyuan-note/filelock v0.0.0-20230615140405-d05a21d49524/go.mod h1:jK5lCYfPbFOrW23/HMeU7kmpLdEd5GkennF+kUpy7Vs=
github.com/siyuan-note/httpclient v0.0.0-20230728124841-53922bac2be2 h1:z6vYbmEOVoytf30Ny6YDjyZTYdCPmazeAl4BN67+308=
diff --git a/kernel/job/cron.go b/kernel/job/cron.go
index 578e5fff0..7026d92be 100644
--- a/kernel/job/cron.go
+++ b/kernel/job/cron.go
@@ -38,6 +38,7 @@ func StartCron() {
go every(50*time.Millisecond, model.FlushTxJob)
go every(util.SQLFlushInterval, sql.FlushTxJob)
go every(util.SQLFlushInterval, sql.FlushHistoryTxJob)
+ go every(util.SQLFlushInterval, sql.FlushAssetContentTxJob)
go every(10*time.Minute, model.FixIndexJob)
go every(10*time.Minute, model.IndexEmbedBlockJob)
go every(10*time.Minute, model.CacheVirtualBlockRefJob)
diff --git a/kernel/main.go b/kernel/main.go
index 59ef67049..a1021717a 100644
--- a/kernel/main.go
+++ b/kernel/main.go
@@ -35,6 +35,7 @@ func main() {
model.InitAppearance()
sql.InitDatabase(false)
sql.InitHistoryDatabase(false)
+ sql.InitAssetContentDatabase(false)
sql.SetCaseSensitive(model.Conf.Search.CaseSensitive)
sql.SetIndexAssetPath(model.Conf.Search.IndexAssetPath)
diff --git a/kernel/mobile/kernel.go b/kernel/mobile/kernel.go
index ae89d2953..2d5b7f89c 100644
--- a/kernel/mobile/kernel.go
+++ b/kernel/mobile/kernel.go
@@ -49,6 +49,7 @@ func StartKernel(container, appDir, workspaceBaseDir, timezoneID, localIPs, lang
model.InitAppearance()
sql.InitDatabase(false)
sql.InitHistoryDatabase(false)
+ sql.InitAssetContentDatabase(false)
sql.SetCaseSensitive(model.Conf.Search.CaseSensitive)
sql.SetIndexAssetPath(model.Conf.Search.IndexAssetPath)
diff --git a/kernel/model/asset_content.go b/kernel/model/asset_content.go
new file mode 100644
index 000000000..d609bef7c
--- /dev/null
+++ b/kernel/model/asset_content.go
@@ -0,0 +1,155 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package model
+
+import (
+ "github.com/88250/gulu"
+ "github.com/88250/lute/ast"
+ "github.com/siyuan-note/eventbus"
+ "github.com/siyuan-note/filelock"
+ "github.com/siyuan-note/logging"
+ "github.com/siyuan-note/siyuan/kernel/sql"
+ "github.com/siyuan-note/siyuan/kernel/task"
+ "github.com/siyuan-note/siyuan/kernel/util"
+ "io/fs"
+ "path/filepath"
+ "strings"
+)
+
+func ReindexAssetContent() {
+ task.AppendTask(task.AssetContentDatabaseIndexFull, fullReindexAssetContent)
+ return
+}
+
+func fullReindexAssetContent() {
+ util.PushMsg(Conf.Language(216), 7*1000)
+ sql.InitAssetContentDatabase(true)
+
+ assetsSearch := NewAssetsSearcher()
+ assetsSearch.Index()
+ return
+}
+
+func init() {
+ subscribeSQLAssetContentEvents()
+}
+
+func subscribeSQLAssetContentEvents() {
+ eventbus.Subscribe(util.EvtSQLAssetContentRebuild, func() {
+ ReindexAssetContent()
+ })
+}
+
+var (
+ AssetsSearchEnabled = true
+)
+
+type AssetsSearcher struct {
+ AssetsDir string
+ Parsers map[string]AssetParser
+}
+
+func (searcher *AssetsSearcher) Index() {
+ assetsDir := searcher.AssetsDir
+ if !gulu.File.IsDir(assetsDir) {
+ return
+ }
+
+ var results []*AssetParseResult
+ filepath.Walk(assetsDir, func(absPath string, info fs.FileInfo, err error) error {
+ if nil != err {
+ logging.LogErrorf("walk dir [%s] failed: %s", absPath, err)
+ return err
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+
+ ext := strings.ToLower(filepath.Ext(absPath))
+ parser, found := searcher.Parsers[ext]
+ if !found {
+ return nil
+ }
+
+ result := parser.Parse(absPath)
+ if nil == result {
+ return nil
+ }
+
+ result.Path = "assets" + filepath.ToSlash(strings.TrimPrefix(absPath, assetsDir))
+ result.Size = info.Size()
+ result.Updated = info.ModTime().Unix()
+ results = append(results, result)
+ return nil
+ })
+
+ var assetContents []*sql.AssetContent
+ for _, result := range results {
+ assetContents = append(assetContents, &sql.AssetContent{
+ ID: ast.NewNodeID(),
+ Name: filepath.Base(result.Path),
+ Ext: filepath.Ext(result.Path),
+ Path: result.Path,
+ Size: result.Size,
+ Updated: result.Updated,
+ Content: result.Content,
+ })
+ }
+
+ sql.IndexAssetContentsQueue(assetContents)
+}
+
+func NewAssetsSearcher() *AssetsSearcher {
+ return &AssetsSearcher{
+ AssetsDir: util.GetDataAssetsAbsPath(),
+ Parsers: map[string]AssetParser{
+ ".txt": &TxtAssetParser{},
+ },
+ }
+}
+
+type AssetParseResult struct {
+ Path string
+ Size int64
+ Updated int64
+ Content string
+}
+
+type AssetParser interface {
+ Parse(absPath string) *AssetParseResult
+}
+
+type TxtAssetParser struct {
+}
+
+func (parser *TxtAssetParser) Parse(absPath string) (ret *AssetParseResult) {
+ if !strings.HasSuffix(strings.ToLower(absPath), ".txt") {
+ return
+ }
+
+ data, err := filelock.ReadFile(absPath)
+ if nil != err {
+ logging.LogErrorf("read file [%s] failed: %s", absPath, err)
+ return
+ }
+
+ ret = &AssetParseResult{
+ Content: string(data),
+ }
+ return
+}
diff --git a/kernel/model/index.go b/kernel/model/index.go
index 8ac5202c4..19e6d9b8f 100644
--- a/kernel/model/index.go
+++ b/kernel/model/index.go
@@ -303,4 +303,16 @@ func subscribeSQLEvents() {
util.SetBootDetails(msg)
util.ContextPushMsg(context, msg)
})
+
+ eventbus.Subscribe(eventbus.EvtSQLInsertAssetContent, func(context map[string]interface{}) {
+ if util.ContainerAndroid == util.Container || util.ContainerIOS == util.Container {
+ return
+ }
+
+ current := context["current"].(int)
+ total := context["total"]
+ msg := fmt.Sprintf(Conf.Language(217), current, total)
+ util.SetBootDetails(msg)
+ util.ContextPushMsg(context, msg)
+ })
}
diff --git a/kernel/sql/asset_content.go b/kernel/sql/asset_content.go
new file mode 100644
index 000000000..19ceab244
--- /dev/null
+++ b/kernel/sql/asset_content.go
@@ -0,0 +1,96 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package sql
+
+import (
+ "database/sql"
+ "fmt"
+ "strings"
+
+ "github.com/siyuan-note/eventbus"
+)
+
+type AssetContent struct {
+ ID string
+ Name string
+ Ext string
+ Path string
+ Size int64
+ Updated int64
+ Content string
+}
+
+const (
+ AssetContentsFTSCaseInsensitiveInsert = "INSERT INTO asset_contents_fts_case_insensitive (id, name, ext, path, size, updated, content) VALUES %s"
+ AssetContentsPlaceholder = "(?, ?, ?, ?, ?, ?, ?)"
+)
+
+func insertAssetContents(tx *sql.Tx, assetContents []*AssetContent, context map[string]interface{}) (err error) {
+ if 1 > len(assetContents) {
+ return
+ }
+
+ var bulk []*AssetContent
+ for _, assetContent := range assetContents {
+ bulk = append(bulk, assetContent)
+ if 512 > len(bulk) {
+ continue
+ }
+
+ if err = insertAssetContents0(tx, bulk, context); nil != err {
+ return
+ }
+ bulk = []*AssetContent{}
+ }
+ if 0 < len(bulk) {
+ if err = insertAssetContents0(tx, bulk, context); nil != err {
+ return
+ }
+ }
+ return
+}
+
+func insertAssetContents0(tx *sql.Tx, bulk []*AssetContent, context map[string]interface{}) (err error) {
+ valueStrings := make([]string, 0, len(bulk))
+ valueArgs := make([]interface{}, 0, len(bulk)*strings.Count(AssetContentsPlaceholder, "?"))
+ for _, b := range bulk {
+ valueStrings = append(valueStrings, AssetContentsPlaceholder)
+ valueArgs = append(valueArgs, b.ID)
+ valueArgs = append(valueArgs, b.Name)
+ valueArgs = append(valueArgs, b.Ext)
+ valueArgs = append(valueArgs, b.Path)
+ valueArgs = append(valueArgs, b.Size)
+ valueArgs = append(valueArgs, b.Updated)
+ valueArgs = append(valueArgs, b.Content)
+ }
+
+ stmt := fmt.Sprintf(AssetContentsFTSCaseInsensitiveInsert, strings.Join(valueStrings, ","))
+ if err = prepareExecInsertTx(tx, stmt, valueArgs); nil != err {
+ return
+ }
+
+ eventbus.Publish(eventbus.EvtSQLInsertAssetContent, context)
+ return
+}
+
+func deleteAssetContentsByPath(tx *sql.Tx, path string, context map[string]interface{}) (err error) {
+ stmt := "DELETE FROM asset_contents_fts_case_insensitive WHERE path = ?"
+ if err = execStmtTx(tx, stmt, path); nil != err {
+ return
+ }
+ return
+}
diff --git a/kernel/sql/database.go b/kernel/sql/database.go
index e6b93756a..afec892fa 100644
--- a/kernel/sql/database.go
+++ b/kernel/sql/database.go
@@ -45,8 +45,9 @@ import (
)
var (
- db *sql.DB
- historyDB *sql.DB
+ db *sql.DB
+ historyDB *sql.DB
+ assetContentDB *sql.DB
)
func init() {
@@ -193,7 +194,36 @@ func initDBTables() {
}
}
+func initDBConnection() {
+ if nil != db {
+ closeDatabase()
+ }
+ dsn := util.DBPath + "?_journal_mode=WAL" +
+ "&_synchronous=OFF" +
+ "&_mmap_size=2684354560" +
+ "&_secure_delete=OFF" +
+ "&_cache_size=-20480" +
+ "&_page_size=32768" +
+ "&_busy_timeout=7000" +
+ "&_ignore_check_constraints=ON" +
+ "&_temp_store=MEMORY" +
+ "&_case_sensitive_like=OFF"
+ var err error
+ db, err = sql.Open("sqlite3_extended", dsn)
+ if nil != err {
+ logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create database failed: %s", err)
+ }
+ db.SetMaxIdleConns(20)
+ db.SetMaxOpenConns(20)
+ db.SetConnMaxLifetime(365 * 24 * time.Hour)
+}
+
+var initHistoryDatabaseLock = sync.Mutex{}
+
func InitHistoryDatabase(forceRebuild bool) {
+ initHistoryDatabaseLock.Lock()
+ defer initHistoryDatabaseLock.Unlock()
+
initHistoryDBConnection()
if !forceRebuild && gulu.File.IsExist(util.HistoryDBPath) {
@@ -228,7 +258,7 @@ func initHistoryDBConnection() {
var err error
historyDB, err = sql.Open("sqlite3_extended", dsn)
if nil != err {
- logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create database failed: %s", err)
+ logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create history database failed: %s", err)
}
historyDB.SetMaxIdleConns(3)
historyDB.SetMaxOpenConns(3)
@@ -243,11 +273,34 @@ func initHistoryDBTables() {
}
}
-func initDBConnection() {
- if nil != db {
- closeDatabase()
+var initAssetContentDatabaseLock = sync.Mutex{}
+
+func InitAssetContentDatabase(forceRebuild bool) {
+ initAssetContentDatabaseLock.Lock()
+ defer initAssetContentDatabaseLock.Unlock()
+
+ initAssetContentDBConnection()
+
+ if !forceRebuild && gulu.File.IsExist(util.AssetContentDBPath) {
+ return
}
- dsn := util.DBPath + "?_journal_mode=WAL" +
+
+ assetContentDB.Close()
+ if err := os.RemoveAll(util.AssetContentDBPath); nil != err {
+ logging.LogErrorf("remove assets database file [%s] failed: %s", util.AssetContentDBPath, err)
+ return
+ }
+
+ initAssetContentDBConnection()
+ initAssetContentDBTables()
+}
+
+func initAssetContentDBConnection() {
+ if nil != assetContentDB {
+ assetContentDB.Close()
+ }
+
+ dsn := util.AssetContentDBPath + "?_journal_mode=WAL" +
"&_synchronous=OFF" +
"&_mmap_size=2684354560" +
"&_secure_delete=OFF" +
@@ -258,13 +311,21 @@ func initDBConnection() {
"&_temp_store=MEMORY" +
"&_case_sensitive_like=OFF"
var err error
- db, err = sql.Open("sqlite3_extended", dsn)
+ assetContentDB, err = sql.Open("sqlite3_extended", dsn)
if nil != err {
- logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create database failed: %s", err)
+ logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create assets database failed: %s", err)
+ }
+ assetContentDB.SetMaxIdleConns(3)
+ assetContentDB.SetMaxOpenConns(3)
+ assetContentDB.SetConnMaxLifetime(365 * 24 * time.Hour)
+}
+
+func initAssetContentDBTables() {
+ assetContentDB.Exec("DROP TABLE asset_contents_fts_case_insensitive")
+ _, 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\")")
+ if nil != err {
+ logging.LogFatalf(logging.ExitCodeReadOnlyDatabase, "create table [asset_contents_fts_case_insensitive] failed: %s", err)
}
- db.SetMaxIdleConns(20)
- db.SetMaxOpenConns(20)
- db.SetConnMaxLifetime(365 * 24 * time.Hour)
}
var (
@@ -1161,6 +1222,18 @@ func beginTx() (tx *sql.Tx, err error) {
return
}
+func commitTx(tx *sql.Tx) (err error) {
+ if nil == tx {
+ logging.LogErrorf("tx is nil")
+ return errors.New("tx is nil")
+ }
+
+ if err = tx.Commit(); nil != err {
+ logging.LogErrorf("commit tx failed: %s\n %s", err, logging.ShortStack())
+ }
+ return
+}
+
func beginHistoryTx() (tx *sql.Tx, err error) {
if tx, err = historyDB.Begin(); nil != err {
logging.LogErrorf("begin history tx failed: %s\n %s", err, logging.ShortStack())
@@ -1183,7 +1256,17 @@ func commitHistoryTx(tx *sql.Tx) (err error) {
return
}
-func commitTx(tx *sql.Tx) (err error) {
+func beginAssetContentTx() (tx *sql.Tx, err error) {
+ if tx, err = assetContentDB.Begin(); nil != err {
+ logging.LogErrorf("begin asset content tx failed: %s\n %s", err, logging.ShortStack())
+ if strings.Contains(err.Error(), "database is locked") {
+ os.Exit(logging.ExitCodeReadOnlyDatabase)
+ }
+ }
+ return
+}
+
+func commitAssetContentTx(tx *sql.Tx) (err error) {
if nil == tx {
logging.LogErrorf("tx is nil")
return errors.New("tx is nil")
diff --git a/kernel/sql/queue_asset_content.go b/kernel/sql/queue_asset_content.go
new file mode 100644
index 000000000..22357634e
--- /dev/null
+++ b/kernel/sql/queue_asset_content.go
@@ -0,0 +1,147 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package sql
+
+import (
+ "database/sql"
+ "errors"
+ "fmt"
+ "runtime/debug"
+ "sync"
+ "time"
+
+ "github.com/siyuan-note/eventbus"
+ "github.com/siyuan-note/logging"
+ "github.com/siyuan-note/siyuan/kernel/task"
+ "github.com/siyuan-note/siyuan/kernel/util"
+)
+
+var (
+ assetContentOperationQueue []*assetContentDBQueueOperation
+ assetContentDBQueueLock = sync.Mutex{}
+
+ assetContentTxLock = sync.Mutex{}
+)
+
+type assetContentDBQueueOperation struct {
+ inQueueTime time.Time
+ action string // index/deletePath
+
+ assetContents []*AssetContent // index
+ path string // deletePath
+}
+
+func FlushAssetContentTxJob() {
+ task.AppendTask(task.AssetContentDatabaseIndexCommit, FlushAssetContentQueue)
+}
+
+func FlushAssetContentQueue() {
+ ops := getAssetContentOperations()
+ if 1 > len(ops) {
+ return
+ }
+
+ assetContentTxLock.Lock()
+ defer assetContentTxLock.Unlock()
+ start := time.Now()
+
+ groupOpsTotal := map[string]int{}
+ for _, op := range ops {
+ groupOpsTotal[op.action]++
+ }
+
+ context := map[string]interface{}{eventbus.CtxPushMsg: eventbus.CtxPushMsgToStatusBar}
+ groupOpsCurrent := map[string]int{}
+ for i, op := range ops {
+ if util.IsExiting {
+ return
+ }
+
+ tx, err := beginAssetContentTx()
+ if nil != err {
+ return
+ }
+
+ groupOpsCurrent[op.action]++
+ context["current"] = groupOpsCurrent[op.action]
+ context["total"] = groupOpsTotal[op.action]
+
+ if err = execAssetContentOp(op, tx, context); nil != err {
+ tx.Rollback()
+ logging.LogErrorf("queue operation failed: %s", err)
+ eventbus.Publish(util.EvtSQLAssetContentRebuild)
+ return
+ }
+
+ if err = commitAssetContentTx(tx); nil != err {
+ logging.LogErrorf("commit tx failed: %s", err)
+ return
+ }
+
+ if 16 < i && 0 == i%128 {
+ debug.FreeOSMemory()
+ }
+ }
+
+ if 128 < len(ops) {
+ debug.FreeOSMemory()
+ }
+
+ elapsed := time.Now().Sub(start).Milliseconds()
+ if 7000 < elapsed {
+ logging.LogInfof("database asset content op tx [%dms]", elapsed)
+ }
+}
+
+func execAssetContentOp(op *assetContentDBQueueOperation, tx *sql.Tx, context map[string]interface{}) (err error) {
+ switch op.action {
+ case "index":
+ err = insertAssetContents(tx, op.assetContents, context)
+ case "delete":
+ err = deleteAssetContentsByPath(tx, op.path, context)
+ default:
+ msg := fmt.Sprintf("unknown asset content operation [%s]", op.action)
+ logging.LogErrorf(msg)
+ err = errors.New(msg)
+ }
+ return
+}
+
+func DeleteAssetContentsByPathQueue(path string) {
+ assetContentTxLock.Lock()
+ defer assetContentTxLock.Unlock()
+
+ newOp := &assetContentDBQueueOperation{inQueueTime: time.Now(), action: "deletePath", path: path}
+ assetContentOperationQueue = append(assetContentOperationQueue, newOp)
+}
+
+func IndexAssetContentsQueue(assetContents []*AssetContent) {
+ assetContentTxLock.Lock()
+ defer assetContentTxLock.Unlock()
+
+ newOp := &assetContentDBQueueOperation{inQueueTime: time.Now(), action: "index", assetContents: assetContents}
+ assetContentOperationQueue = append(assetContentOperationQueue, newOp)
+}
+
+func getAssetContentOperations() (ops []*assetContentDBQueueOperation) {
+ assetContentTxLock.Lock()
+ defer assetContentTxLock.Unlock()
+
+ ops = assetContentOperationQueue
+ assetContentOperationQueue = nil
+ return
+}
diff --git a/kernel/sql/queue_history.go b/kernel/sql/queue_history.go
index 1c7c4efc9..32a3a3be6 100644
--- a/kernel/sql/queue_history.go
+++ b/kernel/sql/queue_history.go
@@ -55,8 +55,8 @@ func FlushHistoryQueue() {
return
}
- txLock.Lock()
- defer txLock.Unlock()
+ historyTxLock.Lock()
+ defer historyTxLock.Unlock()
start := time.Now()
groupOpsTotal := map[string]int{}
@@ -145,27 +145,3 @@ func getHistoryOperations() (ops []*historyDBQueueOperation) {
historyOperationQueue = nil
return
}
-
-func WaitForWritingHistoryDatabase() {
- var printLog bool
- var lastPrintLog bool
- for i := 0; isWritingHistoryDatabase(); i++ {
- time.Sleep(50 * time.Millisecond)
- if 200 < i && !printLog { // 10s 后打日志
- logging.LogWarnf("history database is writing: \n%s", logging.ShortStack())
- printLog = true
- }
- if 1200 < i && !lastPrintLog { // 60s 后打日志
- logging.LogWarnf("history database is still writing")
- lastPrintLog = true
- }
- }
-}
-
-func isWritingHistoryDatabase() bool {
- time.Sleep(util.SQLFlushInterval + 50*time.Millisecond)
- if 0 < len(historyOperationQueue) || util.IsMutexLocked(&historyTxLock) {
- return true
- }
- return false
-}
diff --git a/kernel/task/queue.go b/kernel/task/queue.go
index 727fc9ea0..06dc2c8d4 100644
--- a/kernel/task/queue.go
+++ b/kernel/task/queue.go
@@ -82,19 +82,21 @@ func getCurrentActions() (ret []string) {
}
const (
- RepoCheckout = "task.repo.checkout" // 从快照中检出
- DatabaseIndexFull = "task.database.index.full" // 重建索引
- DatabaseIndex = "task.database.index" // 数据库索引
- DatabaseIndexCommit = "task.database.index.commit" // 数据库索引提交
- DatabaseIndexRef = "task.database.index.ref" // 数据库索引引用
- DatabaseIndexFix = "task.database.index.fix" // 数据库索引订正
- OCRImage = "task.ocr.image" // 图片 OCR 提取文本
- HistoryGenerateDoc = "task.history.generateDoc" // 生成文件历史
- HistoryDatabaseIndexFull = "task.history.database.index.full" // 历史数据库重建索引
- HistoryDatabaseIndexCommit = "task.history.database.index.commit" // 历史数据库索引提交
- DatabaseIndexEmbedBlock = "task.database.index.embedBlock" // 数据库索引嵌入块
- ReloadUI = "task.reload.ui" // 重载 UI
- UpgradeUserGuide = "task.upgrade.userGuide" // 升级用户指南文档笔记本
+ RepoCheckout = "task.repo.checkout" // 从快照中检出
+ DatabaseIndexFull = "task.database.index.full" // 重建索引
+ DatabaseIndex = "task.database.index" // 数据库索引
+ DatabaseIndexCommit = "task.database.index.commit" // 数据库索引提交
+ DatabaseIndexRef = "task.database.index.ref" // 数据库索引引用
+ DatabaseIndexFix = "task.database.index.fix" // 数据库索引订正
+ OCRImage = "task.ocr.image" // 图片 OCR 提取文本
+ HistoryGenerateDoc = "task.history.generateDoc" // 生成文件历史
+ HistoryDatabaseIndexFull = "task.history.database.index.full" // 历史数据库重建索引
+ HistoryDatabaseIndexCommit = "task.history.database.index.commit" // 历史数据库索引提交
+ DatabaseIndexEmbedBlock = "task.database.index.embedBlock" // 数据库索引嵌入块
+ ReloadUI = "task.reload.ui" // 重载 UI
+ UpgradeUserGuide = "task.upgrade.userGuide" // 升级用户指南文档笔记本
+ AssetContentDatabaseIndexFull = "task.asset.database.index.full" // 资源文件数据库重建索引
+ AssetContentDatabaseIndexCommit = "task.asset.database.index.commit" // 资源文件数据库索引提交
)
// uniqueActions 描述了唯一的任务,即队列中只能存在一个在执行的任务。
@@ -107,6 +109,8 @@ var uniqueActions = []string{
HistoryDatabaseIndexFull,
HistoryDatabaseIndexCommit,
DatabaseIndexEmbedBlock,
+ AssetContentDatabaseIndexFull,
+ AssetContentDatabaseIndexCommit,
}
func Contain(action string, moreActions ...string) bool {
diff --git a/kernel/util/runtime.go b/kernel/util/runtime.go
index b9acfe2e1..2d005bbb1 100644
--- a/kernel/util/runtime.go
+++ b/kernel/util/runtime.go
@@ -408,5 +408,6 @@ func existAvailabilityStatus(workspaceAbsPath string) bool {
const (
EvtConfPandocInitialized = "conf.pandoc.initialized"
- EvtSQLHistoryRebuild = "sql.history.rebuild"
+ EvtSQLHistoryRebuild = "sql.history.rebuild"
+ EvtSQLAssetContentRebuild = "sql.assetContent.rebuild"
)
diff --git a/kernel/util/working.go b/kernel/util/working.go
index df22baddc..5c1c3a43d 100644
--- a/kernel/util/working.go
+++ b/kernel/util/working.go
@@ -158,22 +158,23 @@ var (
HomeDir, _ = gulu.OS.Home()
WorkingDir, _ = os.Getwd()
- WorkspaceDir string // 工作空间目录路径
- WorkspaceLock *flock.Flock // 工作空间锁
- ConfDir string // 配置目录路径
- DataDir string // 数据目录路径
- RepoDir string // 仓库目录路径
- HistoryDir string // 数据历史目录路径
- TempDir string // 临时目录路径
- LogPath string // 配置目录下的日志文件 siyuan.log 路径
- DBName = "siyuan.db" // SQLite 数据库文件名
- DBPath string // SQLite 数据库文件路径
- HistoryDBPath string // SQLite 历史数据库文件路径
- BlockTreePath string // 区块树文件路径
- AppearancePath string // 配置目录下的外观目录 appearance/ 路径
- ThemesPath string // 配置目录下的外观目录下的 themes/ 路径
- IconsPath string // 配置目录下的外观目录下的 icons/ 路径
- SnippetsPath string // 数据目录下的 snippets/ 路径
+ WorkspaceDir string // 工作空间目录路径
+ WorkspaceLock *flock.Flock // 工作空间锁
+ ConfDir string // 配置目录路径
+ DataDir string // 数据目录路径
+ RepoDir string // 仓库目录路径
+ HistoryDir string // 数据历史目录路径
+ TempDir string // 临时目录路径
+ LogPath string // 配置目录下的日志文件 siyuan.log 路径
+ DBName = "siyuan.db" // SQLite 数据库文件名
+ DBPath string // SQLite 数据库文件路径
+ HistoryDBPath string // SQLite 历史数据库文件路径
+ AssetContentDBPath string // SQLite 资源文件内容数据库文件路径
+ BlockTreePath string // 区块树文件路径
+ AppearancePath string // 配置目录下的外观目录 appearance/ 路径
+ ThemesPath string // 配置目录下的外观目录下的 themes/ 路径
+ IconsPath string // 配置目录下的外观目录下的 icons/ 路径
+ SnippetsPath string // 数据目录下的 snippets/ 路径
UIProcessIDs = sync.Map{} // UI 进程 ID
)
@@ -247,6 +248,7 @@ func initWorkspaceDir(workspaceArg string) {
os.Setenv("TMP", osTmpDir)
DBPath = filepath.Join(TempDir, DBName)
HistoryDBPath = filepath.Join(TempDir, "history.db")
+ AssetContentDBPath = filepath.Join(TempDir, "asset_content.db")
BlockTreePath = filepath.Join(TempDir, "blocktree")
SnippetsPath = filepath.Join(DataDir, "snippets")
}
diff --git a/kernel/util/working_mobile.go b/kernel/util/working_mobile.go
index 4c11c4e14..f53222309 100644
--- a/kernel/util/working_mobile.go
+++ b/kernel/util/working_mobile.go
@@ -159,6 +159,7 @@ func initWorkspaceDirMobile(workspaceBaseDir string) {
os.Setenv("TMP", osTmpDir)
DBPath = filepath.Join(TempDir, DBName)
HistoryDBPath = filepath.Join(TempDir, "history.db")
+ AssetContentDBPath = filepath.Join(TempDir, "asset_content.db")
BlockTreePath = filepath.Join(TempDir, "blocktree")
SnippetsPath = filepath.Join(DataDir, "snippets")