fixup
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
//! 存儲層:SQLite 連接池的建立、資料庫初始化與各模型的持久化實作。
|
||||
//!
|
||||
//! 每個模型一個子模組(如 [`account`]),各自封裝對應表的 SQL;
|
||||
//! 共用的錯誤類型 [`StorageError`] 定義於本模組。
|
||||
|
||||
pub mod account;
|
||||
pub mod database;
|
||||
|
||||
use deadpool_sqlite::{Config, Pool, Runtime};
|
||||
|
||||
/// 預設資料庫檔案路徑,可透過環境變數 `ALTERDB_DB_PATH` 覆蓋
|
||||
pub const DEFAULT_DB_PATH: &str = "alterdb.db";
|
||||
|
||||
/// 建立 SQLite 連接池
|
||||
pub fn create_pool(db_path: &str) -> Result<Pool, Box<dyn std::error::Error>> {
|
||||
Ok(Config::new(db_path).create_pool(Runtime::Tokio1)?)
|
||||
}
|
||||
|
||||
/// 初始化資料庫:啟用 WAL 日誌模式(一次寫入即持久生效)、確保各模型的
|
||||
/// 表結構存在(冪等,含輕量遷移),並播種默認管理員帳號(僅在不存在時建立)。
|
||||
pub async fn init_db(pool: &Pool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Argon2 雜湊需耗時數十至數百毫秒,放在互動閉包外先行計算
|
||||
let admin_hash = crate::models::account::hash_password(account::ADMIN_DEFAULT_PASSWORD)?;
|
||||
pool.get()
|
||||
.await?
|
||||
.interact(move |conn| {
|
||||
conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(()))?;
|
||||
account::ensure_table(conn)?;
|
||||
database::ensure_table(conn)?;
|
||||
if account::seed_admin(conn, &admin_hash)? {
|
||||
tracing::info!(
|
||||
"已自動建立默認管理員帳號:{}(初始密碼同帳號,建議儘早變更)",
|
||||
account::ADMIN_USERNAME
|
||||
);
|
||||
}
|
||||
Ok::<(), rusqlite::Error>(())
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 存儲層錯誤:涵蓋連接池、連線互動與 SQLite 執行三類來源。
|
||||
#[derive(Debug)]
|
||||
pub enum StorageError {
|
||||
/// 無法從連接池取得連線(資料庫可能不可用)
|
||||
Pool(String),
|
||||
/// 與連線的互動失敗(如 SQL 閉包 panic)
|
||||
Interact(String),
|
||||
/// SQLite 執行錯誤(語法、約束等)
|
||||
Sqlite(rusqlite::Error),
|
||||
}
|
||||
|
||||
impl StorageError {
|
||||
fn pool(err: impl std::fmt::Display) -> Self {
|
||||
StorageError::Pool(err.to_string())
|
||||
}
|
||||
|
||||
fn interact(err: impl std::fmt::Display) -> Self {
|
||||
StorageError::Interact(err.to_string())
|
||||
}
|
||||
|
||||
/// 是否為唯一性約束衝突(控制器可上報為 HTTP 409)
|
||||
pub fn is_unique_violation(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Sqlite(rusqlite::Error::SqliteFailure(err, _))
|
||||
if err.code == rusqlite::ErrorCode::ConstraintViolation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StorageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Pool(err) => write!(f, "無法取得資料庫連線:{err}"),
|
||||
Self::Interact(err) => write!(f, "資料庫連線互動失敗:{err}"),
|
||||
Self::Sqlite(err) => write!(f, "SQLite 錯誤:{err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for StorageError {}
|
||||
|
||||
/// 測試輔助:以暫存檔建立彼此獨立的資料庫,避免污染工作目錄。
|
||||
#[cfg(test)]
|
||||
pub(crate) mod testutil {
|
||||
use super::{create_pool, init_db};
|
||||
use deadpool_sqlite::Pool;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 生成測試專用的暫存資料庫路徑(以進程 ID + 奈秒時間戳保證唯一)
|
||||
fn temp_path(label: &str) -> PathBuf {
|
||||
let unique = format!(
|
||||
"{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos(),
|
||||
);
|
||||
std::env::temp_dir().join(format!("alterdb-{label}-{unique}.db"))
|
||||
}
|
||||
|
||||
/// 建立一個指向暫存檔的連接池(不做任何初始化,供遷移測試自建舊版結構)
|
||||
pub async fn raw_pool(label: &str) -> (Pool, PathBuf) {
|
||||
let path = temp_path(label);
|
||||
let pool = create_pool(path.to_str().expect("暫存路徑應為合法 UTF-8"))
|
||||
.expect("應能建立測試連接池");
|
||||
(pool, path)
|
||||
}
|
||||
|
||||
/// 建立並初始化一個指向暫存檔的連接池
|
||||
pub async fn test_pool(label: &str) -> (Pool, PathBuf) {
|
||||
let (pool, path) = raw_pool(label).await;
|
||||
init_db(&pool).await.expect("測試資料庫初始化應成功");
|
||||
(pool, path)
|
||||
}
|
||||
|
||||
/// 盡力清理測試資料庫檔案(含 WAL / SHM 旁車檔)
|
||||
pub fn cleanup(path: &Path) {
|
||||
for suffix in ["", "-wal", "-shm"] {
|
||||
let _ = std::fs::remove_file(format!("{}{suffix}", path.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user