feat(stage4): Rust SQLite 存储层与文件管理命令
This commit is contained in:
120
src-tauri/Cargo.lock
generated
120
src-tauri/Cargo.lock
generated
@@ -8,6 +8,18 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.5"
|
version = "1.1.5"
|
||||||
@@ -757,6 +769,18 @@ dependencies = [
|
|||||||
"typeid",
|
"typeid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -1204,12 +1228,30 @@ version = "0.12.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -1739,6 +1781,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.30.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -1896,11 +1949,13 @@ name = "new-markdown"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
|
"trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2615,6 +2670,20 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
@@ -3774,6 +3843,25 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trash"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e13ed7a76ae8b9053f33493745ebb4cd2e5ee1cccd9823bb96ec97b78ec4844"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"once_cell",
|
||||||
|
"percent-encoding",
|
||||||
|
"scopeguard",
|
||||||
|
"urlencoding",
|
||||||
|
"windows",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tray-icon"
|
name = "tray-icon"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
@@ -3839,6 +3927,12 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urlpattern"
|
name = "urlpattern"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -3878,6 +3972,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4502,6 +4602,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|||||||
@@ -17,3 +17,5 @@ tauri-plugin-dialog = "2"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
|
trash = "5"
|
||||||
|
|||||||
@@ -217,3 +217,157 @@ pub fn list_dir(path: String) -> Result<Vec<DirEntry>, String> {
|
|||||||
});
|
});
|
||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ————————————————— 阶段 4:文件状态与管理工作区命令 —————————————————
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FileStat {
|
||||||
|
pub exists: bool,
|
||||||
|
/// 修改时间(毫秒,自 Unix 纪元);文件不存在时为 0
|
||||||
|
pub mtime_ms: i64,
|
||||||
|
pub size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 文件状态探针:外部修改检测的基线与轮询都用它(只读元数据,不读内容)。
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn stat_file(path: String) -> FileStat {
|
||||||
|
match fs::metadata(&path) {
|
||||||
|
Ok(meta) => {
|
||||||
|
let mtime_ms = meta
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||||
|
.map(|d| d.as_millis() as i64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
FileStat { exists: true, mtime_ms, size: meta.len() }
|
||||||
|
}
|
||||||
|
Err(_) => FileStat { exists: false, mtime_ms: 0, size: 0 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 新建空文本文件;目标已存在时报错(不覆盖)。
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn create_text_file(path: String) -> Result<(), String> {
|
||||||
|
let p = Path::new(&path);
|
||||||
|
if p.exists() {
|
||||||
|
return Err("同名文件已存在".to_string());
|
||||||
|
}
|
||||||
|
if let Some(parent) = p.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败:{e}"))?;
|
||||||
|
}
|
||||||
|
fs::write(p, "").map_err(|e| format!("创建文件失败:{e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn create_dir(path: String) -> Result<(), String> {
|
||||||
|
fs::create_dir_all(&path).map_err(|e| format!("创建文件夹失败:{e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重命名/移动文件或文件夹;目标已存在时报错。
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn rename_path(from: String, to: String) -> Result<(), String> {
|
||||||
|
if Path::new(&to).exists() {
|
||||||
|
return Err("目标名称已存在".to_string());
|
||||||
|
}
|
||||||
|
fs::rename(&from, &to).map_err(|e| format!("重命名失败:{e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除到系统回收站(失败时退回永久删除仅限文件)。
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn delete_to_trash(path: String) -> Result<(), String> {
|
||||||
|
match trash::delete(&path) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
let p = Path::new(&path);
|
||||||
|
if p.is_file() {
|
||||||
|
fs::remove_file(p).map_err(|e2| format!("删除失败:{e};备用删除也失败:{e2}"))
|
||||||
|
} else {
|
||||||
|
Err(format!("删除失败:{e}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ————————————————— 阶段 4:SQLite 存储命令(逻辑在 storage.rs) —————————————————
|
||||||
|
|
||||||
|
use crate::storage;
|
||||||
|
use crate::AppDb;
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
fn lock_db<'a>(db: &'a State<'a, AppDb>) -> Result<std::sync::MutexGuard<'a, rusqlite::Connection>, String> {
|
||||||
|
db.0.lock().map_err(|_| "数据库锁损坏".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_recent_list(db: State<'_, AppDb>) -> Result<Vec<storage::RecentEntry>, String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::recent_list(&conn, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_recent_add(db: State<'_, AppDb>, path: String, name: String, opened_at: i64) -> Result<(), String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::recent_add(&conn, &path, &name, opened_at, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_recent_remove(db: State<'_, AppDb>, path: String) -> Result<(), String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::recent_remove(&conn, &path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_recent_clear(db: State<'_, AppDb>) -> Result<(), String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::recent_clear(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_workspace_list(db: State<'_, AppDb>) -> Result<Vec<String>, String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::workspace_list(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_workspace_add(db: State<'_, AppDb>, path: String, opened_at: i64) -> Result<(), String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::workspace_add(&conn, &path, opened_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_pref_get(db: State<'_, AppDb>, key: String) -> Result<Option<String>, String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::pref_get(&conn, &key)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_pref_set(db: State<'_, AppDb>, key: String, value: String) -> Result<(), String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::pref_set(&conn, &key, &value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_recovery_list(db: State<'_, AppDb>) -> Result<Vec<storage::RecoveryEntry>, String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::recovery_list(&conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_recovery_save(
|
||||||
|
db: State<'_, AppDb>,
|
||||||
|
doc_key: String,
|
||||||
|
path: Option<String>,
|
||||||
|
title: String,
|
||||||
|
content: String,
|
||||||
|
updated_at: i64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::recovery_save(&conn, &doc_key, path.as_deref(), &title, &content, updated_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn db_recovery_delete(db: State<'_, AppDb>, doc_key: String) -> Result<(), String> {
|
||||||
|
let conn = lock_db(&db)?;
|
||||||
|
storage::recovery_delete(&conn, &doc_key)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,47 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
|
mod storage;
|
||||||
|
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
/// 应用数据库句柄(阶段 4):连接在启动时打开并建表,命令层经 State 访问。
|
||||||
|
pub struct AppDb(pub Mutex<rusqlite::Connection>);
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.setup(|app| {
|
||||||
|
let data_dir = app.path().app_data_dir().expect("app data dir");
|
||||||
|
std::fs::create_dir_all(&data_dir).expect("create app data dir");
|
||||||
|
let conn = rusqlite::Connection::open(data_dir.join("new-markdown.db"))
|
||||||
|
.expect("open app database");
|
||||||
|
storage::init_schema(&conn).expect("init app database schema");
|
||||||
|
app.manage(AppDb(Mutex::new(conn)));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::read_text_file,
|
commands::read_text_file,
|
||||||
commands::write_text_file,
|
commands::write_text_file,
|
||||||
commands::list_dir,
|
commands::list_dir,
|
||||||
commands::copy_to_assets,
|
commands::copy_to_assets,
|
||||||
commands::save_bytes_to_assets,
|
commands::save_bytes_to_assets,
|
||||||
|
commands::stat_file,
|
||||||
|
commands::create_text_file,
|
||||||
|
commands::create_dir,
|
||||||
|
commands::rename_path,
|
||||||
|
commands::delete_to_trash,
|
||||||
|
commands::db_recent_list,
|
||||||
|
commands::db_recent_add,
|
||||||
|
commands::db_recent_remove,
|
||||||
|
commands::db_recent_clear,
|
||||||
|
commands::db_workspace_list,
|
||||||
|
commands::db_workspace_add,
|
||||||
|
commands::db_pref_get,
|
||||||
|
commands::db_pref_set,
|
||||||
|
commands::db_recovery_list,
|
||||||
|
commands::db_recovery_save,
|
||||||
|
commands::db_recovery_delete,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running new-markdown");
|
.expect("error while running new-markdown");
|
||||||
|
|||||||
192
src-tauri/src/storage.rs
Normal file
192
src-tauri/src/storage.rs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
//! SQLite 存储层(阶段 4):最近文件、工作区、偏好、崩溃恢复记录。
|
||||||
|
//! 红线(docs/02):这里永远不存文档正文的唯一副本——正文只在 .md 文件里;
|
||||||
|
//! recovery 表是未保存草稿的副本,仅用于崩溃/误关后恢复,保存或确认丢弃即删。
|
||||||
|
//! 本文件不依赖 Tauri 类型,命令层(commands.rs)只做参数转发,便于独立校验。
|
||||||
|
|
||||||
|
use rusqlite::{params, Connection};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub fn init_schema(conn: &Connection) -> Result<(), String> {
|
||||||
|
conn.execute_batch(
|
||||||
|
"CREATE TABLE IF NOT EXISTS recent_files (
|
||||||
|
path TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
opened_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS workspaces (
|
||||||
|
path TEXT PRIMARY KEY,
|
||||||
|
opened_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS prefs (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS recovery (
|
||||||
|
doc_key TEXT PRIMARY KEY,
|
||||||
|
path TEXT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);",
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("初始化数据库失败:{e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn err<E: std::fmt::Display>(e: E) -> String {
|
||||||
|
format!("数据库操作失败:{e}")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecentEntry {
|
||||||
|
pub path: String,
|
||||||
|
pub name: String,
|
||||||
|
pub opened_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recent_list(conn: &Connection, limit: i64) -> Result<Vec<RecentEntry>, String> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT path, name, opened_at FROM recent_files ORDER BY opened_at DESC LIMIT ?1")
|
||||||
|
.map_err(err)?;
|
||||||
|
let rows = stmt
|
||||||
|
.query_map(params![limit], |row| {
|
||||||
|
Ok(RecentEntry {
|
||||||
|
path: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
opened_at: row.get(2)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(err)?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for r in rows {
|
||||||
|
out.push(r.map_err(err)?);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recent_add(conn: &Connection, path: &str, name: &str, opened_at: i64, keep: i64) -> Result<(), String> {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO recent_files (path, name, opened_at) VALUES (?1, ?2, ?3)
|
||||||
|
ON CONFLICT(path) DO UPDATE SET name = excluded.name, opened_at = excluded.opened_at",
|
||||||
|
params![path, name, opened_at],
|
||||||
|
)
|
||||||
|
.map_err(err)?;
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM recent_files WHERE path NOT IN (SELECT path FROM recent_files ORDER BY opened_at DESC LIMIT ?1)",
|
||||||
|
params![keep],
|
||||||
|
)
|
||||||
|
.map_err(err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recent_remove(conn: &Connection, path: &str) -> Result<(), String> {
|
||||||
|
conn.execute("DELETE FROM recent_files WHERE path = ?1", params![path])
|
||||||
|
.map_err(err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recent_clear(conn: &Connection) -> Result<(), String> {
|
||||||
|
conn.execute("DELETE FROM recent_files", []).map_err(err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workspace_list(conn: &Connection) -> Result<Vec<String>, String> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT path FROM workspaces ORDER BY opened_at DESC")
|
||||||
|
.map_err(err)?;
|
||||||
|
let rows = stmt
|
||||||
|
.query_map([], |row| row.get::<_, String>(0))
|
||||||
|
.map_err(err)?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for r in rows {
|
||||||
|
out.push(r.map_err(err)?);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workspace_add(conn: &Connection, path: &str, opened_at: i64) -> Result<(), String> {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO workspaces (path, opened_at) VALUES (?1, ?2)
|
||||||
|
ON CONFLICT(path) DO UPDATE SET opened_at = excluded.opened_at",
|
||||||
|
params![path, opened_at],
|
||||||
|
)
|
||||||
|
.map_err(err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pref_get(conn: &Connection, key: &str) -> Result<Option<String>, String> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT value FROM prefs WHERE key = ?1")
|
||||||
|
.map_err(err)?;
|
||||||
|
let mut rows = stmt.query(params![key]).map_err(err)?;
|
||||||
|
match rows.next().map_err(err)? {
|
||||||
|
Some(row) => Ok(Some(row.get(0).map_err(err)?)),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pref_set(conn: &Connection, key: &str, value: &str) -> Result<(), String> {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO prefs (key, value) VALUES (?1, ?2)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||||
|
params![key, value],
|
||||||
|
)
|
||||||
|
.map_err(err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecoveryEntry {
|
||||||
|
pub doc_key: String,
|
||||||
|
pub path: Option<String>,
|
||||||
|
pub title: String,
|
||||||
|
pub content: String,
|
||||||
|
pub updated_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recovery_list(conn: &Connection) -> Result<Vec<RecoveryEntry>, String> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT doc_key, path, title, content, updated_at FROM recovery ORDER BY updated_at DESC")
|
||||||
|
.map_err(err)?;
|
||||||
|
let rows = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok(RecoveryEntry {
|
||||||
|
doc_key: row.get(0)?,
|
||||||
|
path: row.get(1)?,
|
||||||
|
title: row.get(2)?,
|
||||||
|
content: row.get(3)?,
|
||||||
|
updated_at: row.get(4)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(err)?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for r in rows {
|
||||||
|
out.push(r.map_err(err)?);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recovery_save(
|
||||||
|
conn: &Connection,
|
||||||
|
doc_key: &str,
|
||||||
|
path: Option<&str>,
|
||||||
|
title: &str,
|
||||||
|
content: &str,
|
||||||
|
updated_at: i64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO recovery (doc_key, path, title, content, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)
|
||||||
|
ON CONFLICT(doc_key) DO UPDATE SET path = excluded.path, title = excluded.title,
|
||||||
|
content = excluded.content, updated_at = excluded.updated_at",
|
||||||
|
params![doc_key, path, title, content, updated_at],
|
||||||
|
)
|
||||||
|
.map_err(err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recovery_delete(conn: &Connection, doc_key: &str) -> Result<(), String> {
|
||||||
|
conn.execute("DELETE FROM recovery WHERE doc_key = ?1", params![doc_key])
|
||||||
|
.map_err(err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user