use anyhow::{Context, Result};
use rusqlite::{Connection, params};
use std::path::PathBuf;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct FileRecord {
pub id: Option<i64>,
pub relative_path: String,
pub modified_time: String,
pub original_hash: String,
pub output_hash: String,
pub original_size: u64,
pub output_size: u64,
pub created_at: String,
}
#[derive(Debug, Clone)]
pub struct LogRecord {
pub file_path: String,
pub action: String,
pub status: String,
pub message: String,
pub timestamp: String,
}
pub struct Database {
pub conn: Connection,
}
impl Database {
pub fn new() -> Result<Self> {
let db_path = Self::get_db_path()?;
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let conn =
Connection::open(&db_path).context(format!("无法打开数据库: {}", db_path.display()))?;
let db = Database { conn };
db.init_tables()?;
Ok(db)
}
fn get_db_path() -> Result<PathBuf> {
let home_dir = dirs::home_dir().context("无法获取用户主目录")?;
Ok(home_dir.join(".xor").join("data.db"))
}
pub fn init_tables(&self) -> Result<()> {
self.conn.execute(
"CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
relative_path TEXT NOT NULL UNIQUE,
modified_time TEXT NOT NULL,
original_hash TEXT NOT NULL,
output_hash TEXT NOT NULL,
original_size INTEGER NOT NULL DEFAULT 0,
output_size INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
[],
)?;
let _ = self.conn.execute(
"ALTER TABLE files ADD COLUMN original_size INTEGER NOT NULL DEFAULT 0",
[],
);
let _ = self.conn.execute(
"ALTER TABLE files ADD COLUMN output_size INTEGER NOT NULL DEFAULT 0",
[],
);
self.conn.execute(
"CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL,
action TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
timestamp TEXT NOT NULL
)",
[],
)?;
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_files_path ON files(relative_path)",
[],
)?;
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp)",
[],
)?;
Ok(())
}
pub fn file_exists(&self, relative_path: &str) -> Result<Option<FileRecord>> {
let mut stmt = self.conn.prepare(
"SELECT id, relative_path, modified_time, original_hash, output_hash,
COALESCE(original_size, 0), COALESCE(output_size, 0), created_at
FROM files WHERE relative_path = ?1",
)?;
let mut rows = stmt.query(params![relative_path])?;
if let Some(row) = rows.next()? {
Ok(Some(FileRecord {
id: Some(row.get(0)?),
relative_path: row.get(1)?,
modified_time: row.get(2)?,
original_hash: row.get(3)?,
output_hash: row.get(4)?,
original_size: row.get(5)?,
output_size: row.get(6)?,
created_at: row.get(7)?,
}))
} else {
Ok(None)
}
}
#[allow(dead_code)]
pub fn upsert_file(&self, record: &FileRecord) -> Result<()> {
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
self.conn.execute(
"INSERT INTO files (relative_path, modified_time, original_hash, output_hash, original_size, output_size, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
ON CONFLICT(relative_path) DO UPDATE SET
modified_time = excluded.modified_time,
original_hash = excluded.original_hash,
output_hash = excluded.output_hash,
original_size = excluded.original_size,
output_size = excluded.output_size,
updated_at = excluded.updated_at",
params![
&record.relative_path,
&record.modified_time,
&record.original_hash,
&record.output_hash,
&record.original_size,
&record.output_size,
&now,
&now,
],
)?;
Ok(())
}
pub fn batch_upsert_files(&mut self, records: &[FileRecord]) -> Result<()> {
if records.is_empty() {
return Ok(());
}
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let tx = self.conn.transaction()?;
{
let mut stmt = tx.prepare(
"INSERT INTO files (relative_path, modified_time, original_hash, output_hash, original_size, output_size, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
ON CONFLICT(relative_path) DO UPDATE SET
modified_time = excluded.modified_time,
original_hash = excluded.original_hash,
output_hash = excluded.output_hash,
original_size = excluded.original_size,
output_size = excluded.output_size,
updated_at = excluded.updated_at"
)?;
for record in records {
stmt.execute(params![
&record.relative_path,
&record.modified_time,
&record.original_hash,
&record.output_hash,
&record.original_size,
&record.output_size,
&now,
&now,
])?;
}
}
tx.commit()?;
Ok(())
}
pub fn add_log(&self, log: &LogRecord) -> Result<()> {
self.conn.execute(
"INSERT INTO logs (file_path, action, status, message, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![
&log.file_path,
&log.action,
&log.status,
&log.message,
&log.timestamp,
],
)?;
Ok(())
}
pub fn batch_add_logs(&mut self, logs: &[LogRecord]) -> Result<()> {
if logs.is_empty() {
return Ok(());
}
let tx = self.conn.transaction()?;
{
let mut stmt = tx.prepare(
"INSERT INTO logs (file_path, action, status, message, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5)",
)?;
for log in logs {
stmt.execute(params![
&log.file_path,
&log.action,
&log.status,
&log.message,
&log.timestamp,
])?;
}
}
tx.commit()?;
Ok(())
}
#[allow(dead_code)]
pub fn get_all_files(&self) -> Result<Vec<FileRecord>> {
let mut stmt = self.conn.prepare(
"SELECT id, relative_path, modified_time, original_hash, output_hash,
COALESCE(original_size, 0), COALESCE(output_size, 0), created_at
FROM files ORDER BY relative_path",
)?;
let rows = stmt.query_map([], |row| {
Ok(FileRecord {
id: Some(row.get(0)?),
relative_path: row.get(1)?,
modified_time: row.get(2)?,
original_hash: row.get(3)?,
output_hash: row.get(4)?,
original_size: row.get(5)?,
output_size: row.get(6)?,
created_at: row.get(7)?,
})
})?;
let mut records = Vec::new();
for record in rows {
records.push(record?);
}
Ok(records)
}
#[allow(dead_code)]
pub fn get_recent_logs(&self, limit: usize) -> Result<Vec<LogRecord>> {
let mut stmt = self.conn.prepare(
"SELECT file_path, action, status, message, timestamp
FROM logs ORDER BY timestamp DESC LIMIT ?1",
)?;
let rows = stmt.query_map(params![limit], |row| {
Ok(LogRecord {
file_path: row.get(0)?,
action: row.get(1)?,
status: row.get(2)?,
message: row.get(3)?,
timestamp: row.get(4)?,
})
})?;
let mut logs = Vec::new();
for log in rows {
logs.push(log?);
}
Ok(logs)
}
pub fn get_db_path_string() -> Result<String> {
Ok(Self::get_db_path()?.display().to_string())
}
}