Files
app/native/sync-core/src/sync_db.rs
T
gongyun b02daf1448 主要合入内容:
- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、Ctrl/Cmd+F搜索、Esc 关闭搜索、鼠标右滑返回上级。
- 移动端同步状态页、同步统计卡片、同步任务/累计统计事件、同步页布局更新。
- Android 相册同步默认路径改为 DCIM/Camera,下载路径改用 external_path。
- Rust sync-core 更新:Android 日志依赖、AlbumUpload/AlbumDownload、MirrorWcf 统计修复、Linux FUSE镜像挂载与读写同步支持。

- 依赖更新:external_path、fl_chart、pdfrx 升级,并修了 pdfrx 新版本的deprecated API。
- 新增文件包括:sort_options.dart、sync_page_android.dart、sync_stats_card.dart、platform/fuse.rs、sync_engine/fuse.rs。
2026-06-04 07:11:43 +08:00

1105 lines
42 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use r2d2::CustomizeConnection;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::Connection;
use std::path::Path;
use tokio::sync::Mutex;
use crate::errors::Result;
use crate::models::*;
pub struct SyncDb {
write_conn: Mutex<Connection>,
read_pool: r2d2::Pool<SqliteConnectionManager>,
}
#[derive(Debug)]
struct SyncDbConnectionCustomizer;
impl CustomizeConnection<Connection, rusqlite::Error> for SyncDbConnectionCustomizer {
fn on_acquire(&self, conn: &mut Connection) -> std::result::Result<(), rusqlite::Error> {
conn.execute_batch(
"PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;
PRAGMA synchronous=NORMAL;",
)?;
Ok(())
}
}
impl SyncDb {
pub fn read_pool(&self) -> r2d2::Pool<SqliteConnectionManager> {
self.read_pool.clone()
}
pub fn open(db_path: &Path) -> Result<Self> {
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let write_conn = Connection::open(db_path)?;
write_conn.execute_batch(
"PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;
PRAGMA synchronous=NORMAL;
PRAGMA foreign_keys=ON;
PRAGMA temp_store=MEMORY;",
)?;
Self::run_migrations(&write_conn)?;
let manager = SqliteConnectionManager::file(db_path);
let read_pool = r2d2::Pool::builder()
.max_size(4)
.connection_customizer(Box::new(SyncDbConnectionCustomizer))
.build(manager)?;
Ok(Self {
write_conn: Mutex::new(write_conn),
read_pool,
})
}
fn run_migrations(conn: &Connection) -> Result<()> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS sync_root (
id TEXT PRIMARY KEY,
local_path TEXT NOT NULL UNIQUE,
remote_uri TEXT NOT NULL,
sync_mode TEXT NOT NULL DEFAULT 'full',
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file_mapping (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sync_root_id TEXT NOT NULL REFERENCES sync_root(id),
local_path TEXT NOT NULL,
remote_uri TEXT NOT NULL,
remote_file_id TEXT,
local_hash TEXT,
remote_hash TEXT,
local_mtime INTEGER,
remote_mtime INTEGER,
local_size INTEGER,
remote_size INTEGER,
sync_status TEXT NOT NULL DEFAULT 'synced',
is_placeholder INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL,
UNIQUE(sync_root_id, local_path)
);
CREATE INDEX IF NOT EXISTS idx_file_mapping_remote ON file_mapping(sync_root_id, remote_uri);
CREATE INDEX IF NOT EXISTS idx_file_mapping_status ON file_mapping(sync_status);
CREATE TABLE IF NOT EXISTS conflict (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sync_root_id TEXT NOT NULL REFERENCES sync_root(id),
local_path TEXT NOT NULL,
conflict_type TEXT NOT NULL,
resolution TEXT,
local_hash TEXT,
remote_hash TEXT,
created_at TEXT NOT NULL,
resolved_at TEXT
);
CREATE TABLE IF NOT EXISTS transfer_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sync_root_id TEXT NOT NULL REFERENCES sync_root(id),
file_mapping_id INTEGER REFERENCES file_mapping(id),
direction TEXT NOT NULL,
local_path TEXT NOT NULL,
remote_uri TEXT NOT NULL,
file_size INTEGER NOT NULL,
bytes_done INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
retry_count INTEGER NOT NULL DEFAULT 0,
max_retries INTEGER NOT NULL DEFAULT 5,
error_message TEXT,
session_id TEXT,
chunk_index INTEGER,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_transfer_status ON transfer_queue(status);
CREATE TABLE IF NOT EXISTS sync_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sync_root_id TEXT NOT NULL,
event_type TEXT NOT NULL,
details TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS album_sync_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
local_path TEXT NOT NULL UNIQUE,
remote_uri TEXT NOT NULL,
file_hash TEXT,
synced_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sync_task (
id TEXT PRIMARY KEY,
trigger TEXT NOT NULL,
total_count INTEGER NOT NULL DEFAULT 0,
completed_count INTEGER NOT NULL DEFAULT 0,
failed_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
finished_at TEXT
);
CREATE TABLE IF NOT EXISTS sync_task_item (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL REFERENCES sync_task(id),
relative_path TEXT NOT NULL,
action_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
file_size INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_task_item_task ON sync_task_item(task_id);
CREATE INDEX IF NOT EXISTS idx_task_item_path ON sync_task_item(relative_path);
CREATE INDEX IF NOT EXISTS idx_task_item_action ON sync_task_item(action_type);
CREATE INDEX IF NOT EXISTS idx_task_item_status ON sync_task_item(status);
CREATE INDEX IF NOT EXISTS idx_task_status ON sync_task(status);",
)?;
// 添加 task_item 唯一索引(去重:保留 id 最小的记录)
// 先清理重复数据,再建唯一索引
let existing: Vec<String> = conn
.prepare(
"SELECT name FROM sqlite_master WHERE type='index' AND name='idx_task_item_unique'",
)?
.query_map([], |r| r.get(0))?
.filter_map(|r| r.ok())
.collect();
if existing.is_empty() {
conn.execute_batch(
"DELETE FROM sync_task_item WHERE id NOT IN (
SELECT MIN(id) FROM sync_task_item GROUP BY task_id, relative_path, action_type
);
CREATE UNIQUE INDEX idx_task_item_unique ON sync_task_item(task_id, relative_path, action_type);"
)?;
}
Ok(())
}
// ===== sync_root 操作 =====
pub async fn upsert_sync_root(&self, root: &SyncConfig) -> Result<String> {
let local_path = root.local_root.to_string_lossy().to_string();
let now = chrono::Utc::now().to_rfc3339();
let sync_mode = match root.sync_mode {
SyncMode::Full => "full",
SyncMode::UploadOnly => "upload_only",
SyncMode::DownloadOnly => "download_only",
SyncMode::AlbumUpload => "album_upload",
SyncMode::AlbumDownload => "album_download",
SyncMode::MirrorWcf => "mirror_wcf",
};
let conn = self.write_conn.lock().await;
// 先查找是否已存在
let existing_id: Option<String> = conn
.query_row(
"SELECT id FROM sync_root WHERE local_path = ?1",
rusqlite::params![local_path],
|row| row.get(0),
)
.ok();
if let Some(id) = existing_id {
// 更新已有记录,保留 id 和 created_at
conn.execute(
"UPDATE sync_root SET remote_uri = ?1, sync_mode = ?2, updated_at = ?3 WHERE id = ?4",
rusqlite::params![root.remote_root, sync_mode, now, id],
)?;
Ok(id)
} else {
// 新建记录
let id = uuid::Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO sync_root (id, local_path, remote_uri, sync_mode, enabled, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, 1, ?5, ?5)",
rusqlite::params![id, local_path, root.remote_root, sync_mode, now],
)?;
Ok(id)
}
}
// ===== file_mapping 操作 =====
pub async fn get_file_mapping(
&self,
sync_root_id: &str,
local_path: &str,
) -> Result<Option<FileMapping>> {
let pool = self.read_pool.clone();
let sync_root_id = sync_root_id.to_string();
let local_path = local_path.to_string();
let result = tokio::task::spawn_blocking(move || -> Result<Option<FileMapping>> {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"SELECT id, sync_root_id, local_path, remote_uri, remote_file_id,
local_hash, remote_hash, local_mtime, remote_mtime,
local_size, remote_size, sync_status, is_placeholder
FROM file_mapping WHERE sync_root_id = ?1 AND local_path = ?2",
)?;
let mapping = stmt
.query_row(rusqlite::params![sync_root_id, local_path], |row| {
Ok(FileMapping {
id: row.get(0)?,
sync_root_id: row.get(1)?,
local_path: std::path::PathBuf::from(row.get::<_, String>(2)?),
remote_uri: row.get(3)?,
remote_file_id: row.get(4)?,
local_hash: row.get(5)?,
remote_hash: row.get(6)?,
local_mtime: row.get(7)?,
remote_mtime: row.get(8)?,
local_size: row.get(9)?,
remote_size: row.get(10)?,
sync_status: parse_sync_status(&row.get::<_, String>(11)?),
is_placeholder: row.get::<_, i32>(12)? != 0,
})
})
.ok();
Ok(mapping)
})
.await??;
Ok(result)
}
/// 通过远程 URI 查找文件映射(WCF 水合时使用)
pub async fn find_mapping_by_remote_uri(
&self,
sync_root_id: &str,
remote_uri: &str,
) -> Result<Option<FileMapping>> {
let pool = self.read_pool.clone();
let sync_root_id = sync_root_id.to_string();
let remote_uri = remote_uri.to_string();
let result = tokio::task::spawn_blocking(move || -> Result<Option<FileMapping>> {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"SELECT id, sync_root_id, local_path, remote_uri, remote_file_id,
local_hash, remote_hash, local_mtime, remote_mtime,
local_size, remote_size, sync_status, is_placeholder
FROM file_mapping WHERE sync_root_id = ?1 AND remote_uri = ?2",
)?;
let mapping = stmt
.query_row(rusqlite::params![sync_root_id, remote_uri], |row| {
Ok(FileMapping {
id: row.get(0)?,
sync_root_id: row.get(1)?,
local_path: std::path::PathBuf::from(row.get::<_, String>(2)?),
remote_uri: row.get(3)?,
remote_file_id: row.get(4)?,
local_hash: row.get(5)?,
remote_hash: row.get(6)?,
local_mtime: row.get(7)?,
remote_mtime: row.get(8)?,
local_size: row.get(9)?,
remote_size: row.get(10)?,
sync_status: parse_sync_status(&row.get::<_, String>(11)?),
is_placeholder: row.get::<_, i32>(12)? != 0,
})
})
.ok();
Ok(mapping)
})
.await??;
Ok(result)
}
/// 删除指定 remote_uri 的文件映射
pub async fn delete_mapping_by_remote_uri(
&self,
sync_root_id: &str,
remote_uri: &str,
) -> Result<()> {
let conn = self.write_conn.lock().await;
conn.execute(
"DELETE FROM file_mapping WHERE sync_root_id = ?1 AND remote_uri = ?2",
rusqlite::params![sync_root_id, remote_uri],
)?;
Ok(())
}
/// 更新文件映射的 remote_uri(重命名/移动时使用)
pub async fn update_mapping_remote_uri(
&self,
sync_root_id: &str,
old_remote_uri: &str,
new_remote_uri: &str,
) -> Result<()> {
let conn = self.write_conn.lock().await;
conn.execute(
"UPDATE file_mapping SET remote_uri = ?3 WHERE sync_root_id = ?1 AND remote_uri = ?2",
rusqlite::params![sync_root_id, old_remote_uri, new_remote_uri],
)?;
Ok(())
}
/// 查询所有占位符文件映射(用于 WCF 退出时清理)
#[cfg(feature = "windows-cfapi")]
pub async fn list_placeholder_mappings(&self, sync_root_id: &str) -> Result<Vec<FileMapping>> {
let pool = self.read_pool.clone();
let sync_root_id = sync_root_id.to_string();
let result = tokio::task::spawn_blocking(move || -> Result<Vec<FileMapping>> {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"SELECT id, sync_root_id, local_path, remote_uri, remote_file_id,
local_hash, remote_hash, local_mtime, remote_mtime,
local_size, remote_size, sync_status, is_placeholder
FROM file_mapping WHERE sync_root_id = ?1 AND is_placeholder = 1",
)?;
let mappings = stmt
.query_map(rusqlite::params![sync_root_id], |row| {
Ok(FileMapping {
id: row.get(0)?,
sync_root_id: row.get(1)?,
local_path: std::path::PathBuf::from(row.get::<_, String>(2)?),
remote_uri: row.get(3)?,
remote_file_id: row.get(4)?,
local_hash: row.get(5)?,
remote_hash: row.get(6)?,
local_mtime: row.get(7)?,
remote_mtime: row.get(8)?,
local_size: row.get(9)?,
remote_size: row.get(10)?,
sync_status: parse_sync_status(&row.get::<_, String>(11)?),
is_placeholder: row.get::<_, i32>(12)? != 0,
})
})?
.filter_map(|m| m.ok())
.collect();
Ok(mappings)
})
.await??;
Ok(result)
}
pub async fn upsert_file_mapping(&self, mapping: &FileMapping) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
let status_str = sync_status_str(&mapping.sync_status);
let local_path = crate::utils::normalize_path(&mapping.local_path.to_string_lossy());
let is_placeholder = if mapping.is_placeholder { 1 } else { 0 };
conn.execute(
"INSERT OR REPLACE INTO file_mapping
(sync_root_id, local_path, remote_uri, remote_file_id,
local_hash, remote_hash, local_mtime, remote_mtime,
local_size, remote_size, sync_status, is_placeholder, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
rusqlite::params![
mapping.sync_root_id,
local_path,
mapping.remote_uri,
mapping.remote_file_id,
mapping.local_hash,
mapping.remote_hash,
mapping.local_mtime,
mapping.remote_mtime,
mapping.local_size,
mapping.remote_size,
status_str,
is_placeholder,
now,
],
)?;
Ok(())
}
pub async fn delete_file_mapping(&self, sync_root_id: &str, local_path: &str) -> Result<bool> {
let conn = self.write_conn.lock().await;
let rows = conn.execute(
"DELETE FROM file_mapping WHERE sync_root_id = ?1 AND local_path = ?2",
rusqlite::params![sync_root_id, local_path],
)?;
Ok(rows > 0)
}
/// 更新文件映射的路径(重命名时使用)
pub async fn update_file_mapping_path(
&self,
sync_root_id: &str,
old_relative_path: &str,
new_relative_path: &str,
new_remote_uri: &str,
) -> Result<bool> {
let conn = self.write_conn.lock().await;
let rows = conn.execute(
"UPDATE file_mapping SET local_path = ?1, remote_uri = ?2 WHERE sync_root_id = ?3 AND local_path = ?4",
rusqlite::params![new_relative_path, new_remote_uri, sync_root_id, old_relative_path],
)?;
Ok(rows > 0)
}
/// 删除指定目录前缀下的所有文件映射(含目录自身)
pub async fn delete_file_mapping_prefix(
&self,
sync_root_id: &str,
dir_prefix: &str,
) -> Result<u64> {
let conn = self.write_conn.lock().await;
// 匹配: dir_prefix 自身、dir_prefix/... 子路径
let exact = dir_prefix;
let child_prefix = format!("{}/", dir_prefix);
let rows = conn.execute(
"DELETE FROM file_mapping WHERE sync_root_id = ?1 AND (local_path = ?2 OR local_path LIKE ?3 || '%')",
rusqlite::params![sync_root_id, exact, child_prefix],
)?;
Ok(rows as u64)
}
// ===== transfer_queue 操作 =====
pub async fn add_transfer(&self, task: &TransferTask) -> Result<i64> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
let direction = match task.direction {
TransferDirection::Upload => "upload",
TransferDirection::Download => "download",
};
let status = transfer_status_str(&task.status);
let local_path = task.local_path.to_string_lossy().to_string();
conn.execute(
"INSERT INTO transfer_queue
(sync_root_id, file_mapping_id, direction, local_path, remote_uri,
file_size, bytes_done, status, retry_count, max_retries,
error_message, session_id, chunk_index, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?14)",
rusqlite::params![
task.sync_root_id,
task.file_mapping_id,
direction,
local_path,
task.remote_uri,
task.file_size,
task.bytes_done,
status,
task.retry_count,
task.max_retries,
task.error_message,
task.session_id,
task.chunk_index,
now,
],
)?;
Ok(conn.last_insert_rowid())
}
pub async fn update_transfer_progress(&self, id: i64, bytes_done: u64) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE transfer_queue SET bytes_done = ?1, updated_at = ?2 WHERE id = ?3",
rusqlite::params![bytes_done, now, id],
)?;
Ok(())
}
pub async fn complete_transfer(&self, id: i64) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE transfer_queue SET status = 'completed', updated_at = ?1 WHERE id = ?2",
rusqlite::params![now, id],
)?;
Ok(())
}
pub async fn fail_transfer(&self, id: i64, error: &str) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE transfer_queue SET status = 'failed', error_message = ?1, updated_at = ?2 WHERE id = ?3",
rusqlite::params![error, now, id],
)?;
Ok(())
}
pub async fn get_interrupted_transfers(&self) -> Result<Vec<TransferTask>> {
let pool = self.read_pool.clone();
let result = tokio::task::spawn_blocking(move || -> Result<Vec<TransferTask>> {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"SELECT id, sync_root_id, file_mapping_id, direction, local_path, remote_uri,
file_size, bytes_done, status, retry_count, max_retries,
error_message, session_id, chunk_index
FROM transfer_queue WHERE status IN ('active', 'pending')",
)?;
let tasks = stmt
.query_map([], |row| Ok(transfer_task_from_row(row)))?
.filter_map(|t| t.ok())
.collect();
Ok(tasks)
})
.await??;
Ok(result)
}
// ===== album_sync_record 操作 =====
pub async fn get_album_sync_records(
&self,
) -> Result<std::collections::HashMap<String, String>> {
let pool = self.read_pool.clone();
let result = tokio::task::spawn_blocking(
move || -> Result<std::collections::HashMap<String, String>> {
let conn = pool.get()?;
let mut stmt =
conn.prepare("SELECT local_path, remote_uri FROM album_sync_record")?;
let records = stmt
.query_map([], |row| {
let local: String = row.get(0)?;
let remote: String = row.get(1)?;
Ok((local, remote))
})?
.filter_map(|r| r.ok())
.collect();
Ok(records)
},
)
.await??;
Ok(result)
}
pub async fn add_album_sync_record(
&self,
local_path: &str,
remote_uri: &str,
file_hash: &str,
) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT OR REPLACE INTO album_sync_record (local_path, remote_uri, file_hash, synced_at) VALUES (?1, ?2, ?3, ?4)",
rusqlite::params![local_path, remote_uri, file_hash, now],
)?;
Ok(())
}
// ===== sync_log 操作 =====
pub async fn add_log(
&self,
sync_root_id: &str,
event_type: &str,
details: Option<&str>,
) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT INTO sync_log (sync_root_id, event_type, details, created_at) VALUES (?1, ?2, ?3, ?4)",
rusqlite::params![sync_root_id, event_type, details, now],
)?;
Ok(())
}
// ===== sync_task 操作 =====
pub async fn create_sync_task(&self, task: &SyncTask) -> Result<()> {
let conn = self.write_conn.lock().await;
conn.execute(
"INSERT INTO sync_task (id, trigger, total_count, completed_count, failed_count, status, created_at, updated_at, finished_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
rusqlite::params![
task.id,
task.trigger.as_str(),
task.total_count,
task.completed_count,
task.failed_count,
task.status.as_str(),
task.created_at,
task.updated_at,
task.finished_at,
],
)?;
Ok(())
}
pub async fn update_sync_task_status(
&self,
task_id: &str,
status: &WorkerStatus,
) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE sync_task SET status = ?1, updated_at = ?2 WHERE id = ?3",
rusqlite::params![status.as_str(), now, task_id],
)?;
Ok(())
}
pub async fn update_sync_task_total_count(
&self,
task_id: &str,
total_count: u32,
) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE sync_task SET total_count = ?1, updated_at = ?2 WHERE id = ?3",
rusqlite::params![total_count, now, task_id],
)?;
Ok(())
}
pub async fn finish_sync_task(
&self,
task_id: &str,
status: &WorkerStatus,
completed: u32,
failed: u32,
) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE sync_task SET status = ?1, completed_count = ?2, failed_count = ?3, updated_at = ?4, finished_at = ?5 WHERE id = ?6",
rusqlite::params![status.as_str(), completed, failed, now, now, task_id],
)?;
Ok(())
}
pub async fn increment_task_completed(&self, task_id: &str) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE sync_task SET completed_count = completed_count + 1, updated_at = ?1 WHERE id = ?2",
rusqlite::params![now, task_id],
)?;
Ok(())
}
pub async fn increment_task_failed(&self, task_id: &str) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE sync_task SET failed_count = failed_count + 1, updated_at = ?1 WHERE id = ?2",
rusqlite::params![now, task_id],
)?;
Ok(())
}
pub async fn get_active_sync_tasks(&self) -> Result<Vec<SyncTask>> {
let pool = self.read_pool.clone();
let result = tokio::task::spawn_blocking(move || -> Result<Vec<SyncTask>> {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"SELECT id, trigger, total_count, completed_count, failed_count, status, created_at, updated_at, finished_at
FROM sync_task WHERE status IN ('pending', 'running')
ORDER BY created_at DESC"
)?;
let tasks = stmt.query_map([], sync_task_from_row)?
.filter_map(|t| t.ok()).collect();
Ok(tasks)
}).await??;
Ok(result)
}
pub async fn get_recent_sync_tasks(&self, limit: u32) -> Result<Vec<SyncTask>> {
let pool = self.read_pool.clone();
let result = tokio::task::spawn_blocking(move || -> Result<Vec<SyncTask>> {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"SELECT id, trigger, total_count, completed_count, failed_count, status, created_at, updated_at, finished_at
FROM sync_task ORDER BY created_at DESC LIMIT ?1"
)?;
let tasks = stmt.query_map(rusqlite::params![limit], sync_task_from_row)?
.filter_map(|t| t.ok()).collect();
Ok(tasks)
}).await??;
Ok(result)
}
// ===== sync_task_item 操作 =====
pub async fn create_sync_task_item(&self, item: &SyncTaskItem) -> Result<i64> {
let conn = self.write_conn.lock().await;
conn.execute(
"INSERT INTO sync_task_item (task_id, relative_path, action_type, status, file_size, error_message, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
rusqlite::params![
item.task_id,
item.relative_path,
item.action_type.as_str(),
item.status.as_str(),
item.file_size,
item.error_message,
item.created_at,
item.updated_at,
],
)?;
Ok(conn.last_insert_rowid())
}
/// 创建独立的 task + task_item 记录(用于 WCF 等绕过 WorkerPool 的操作统计)
pub async fn record_standalone_task_item(
&self,
trigger: &WorkerTrigger,
item: &SyncTaskItem,
) -> Result<()> {
let conn = self.write_conn.lock().await;
let tx = conn.unchecked_transaction()?;
tx.execute(
"INSERT INTO sync_task (id, trigger, total_count, completed_count, failed_count, status, created_at, updated_at, finished_at)
VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7, ?8)",
rusqlite::params![
item.task_id,
trigger.as_str(),
if item.status == TaskItemStatus::Failed { 0 } else { 1 },
if item.status == TaskItemStatus::Failed { 1 } else { 0 },
if item.status == TaskItemStatus::Failed { WorkerStatus::Failed.as_str() } else { WorkerStatus::Completed.as_str() },
item.created_at,
item.updated_at,
item.updated_at,
],
)?;
tx.execute(
"INSERT INTO sync_task_item (task_id, relative_path, action_type, status, file_size, error_message, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
rusqlite::params![
item.task_id,
item.relative_path,
item.action_type.as_str(),
item.status.as_str(),
item.file_size,
item.error_message,
item.created_at,
item.updated_at,
],
)?;
tx.commit()?;
Ok(())
}
/// 批量插入 task_item(单次持锁,用事务包裹)
pub async fn create_sync_task_items_batch(&self, items: &[SyncTaskItem]) -> Result<()> {
let conn = self.write_conn.lock().await;
let tx = conn.unchecked_transaction()?;
for item in items {
tx.execute(
"INSERT INTO sync_task_item (task_id, relative_path, action_type, status, file_size, error_message, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
rusqlite::params![
item.task_id,
item.relative_path,
item.action_type.as_str(),
item.status.as_str(),
item.file_size,
item.error_message,
item.created_at,
item.updated_at,
],
)?;
}
tx.commit()?;
Ok(())
}
pub async fn update_sync_task_item_status(
&self,
item_id: i64,
status: &TaskItemStatus,
error_message: Option<&str>,
) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE sync_task_item SET status = ?1, error_message = ?2, updated_at = ?3 WHERE id = ?4",
rusqlite::params![status.as_str(), error_message, now, item_id],
)?;
Ok(())
}
/// 按 task_id + relative_path + action_type 更新 task_item 状态
pub async fn update_task_item_status_by_path(
&self,
task_id: &str,
relative_path: &str,
action_type: &str,
status: &TaskItemStatus,
error_message: Option<&str>,
) -> Result<()> {
let conn = self.write_conn.lock().await;
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"UPDATE sync_task_item SET status = ?1, error_message = ?2, updated_at = ?3 WHERE task_id = ?4 AND relative_path = ?5 AND action_type = ?6",
rusqlite::params![status.as_str(), error_message, now, task_id, relative_path, action_type],
)?;
Ok(())
}
pub async fn get_sync_task_items(&self, task_id: &str) -> Result<Vec<SyncTaskItem>> {
let pool = self.read_pool.clone();
let task_id = task_id.to_string();
let result = tokio::task::spawn_blocking(move || -> Result<Vec<SyncTaskItem>> {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"SELECT id, task_id, relative_path, action_type, status, file_size, error_message, created_at, updated_at
FROM sync_task_item WHERE task_id = ?1
ORDER BY CASE status
WHEN 'running' THEN 0
WHEN 'pending' THEN 1
WHEN 'failed' THEN 2
WHEN 'completed' THEN 3
WHEN 'skipped' THEN 4
ELSE 5 END, id"
)?;
let items = stmt.query_map(rusqlite::params![task_id], sync_task_item_from_row)?
.filter_map(|i| i.ok()).collect();
Ok(items)
}).await??;
Ok(result)
}
pub async fn query_task_items(&self, filter: &TaskItemFilter) -> Result<Vec<SyncTaskItem>> {
let pool = self.read_pool.clone();
let filter = filter.clone();
let result = tokio::task::spawn_blocking(move || -> Result<Vec<SyncTaskItem>> {
let conn = pool.get()?;
let mut sql = String::from(
"SELECT id, task_id, relative_path, action_type, status, file_size, error_message, created_at, updated_at
FROM sync_task_item WHERE 1=1"
);
let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
if let Some(ref task_id) = filter.task_id {
sql.push_str(" AND task_id = ?");
params.push(Box::new(task_id.clone()));
}
if let Some(ref path_contains) = filter.relative_path_contains {
sql.push_str(" AND relative_path LIKE ?");
params.push(Box::new(format!("%{}%", path_contains)));
}
if let Some(ref action_type) = filter.action_type {
sql.push_str(" AND action_type = ?");
params.push(Box::new(action_type.clone()));
}
if let Some(ref status) = filter.status {
sql.push_str(" AND status = ?");
params.push(Box::new(status.clone()));
}
sql.push_str(" ORDER BY CASE status WHEN 'running' THEN 0 WHEN 'pending' THEN 1 WHEN 'failed' THEN 2 WHEN 'completed' THEN 3 WHEN 'skipped' THEN 4 ELSE 5 END, id DESC LIMIT ? OFFSET ?");
params.push(Box::new(filter.limit));
params.push(Box::new(filter.offset));
let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
let mut stmt = conn.prepare(&sql)?;
let items = stmt.query_map(param_refs.as_slice(), sync_task_item_from_row)?
.filter_map(|i| i.ok()).collect();
Ok(items)
}).await??;
Ok(result)
}
/// 从 DB 聚合累积统计(跨所有同步任务)
/// downloaded 包含 download + hydrationcreate_placeholder 不计入,仅重建映射非实际下载)
pub async fn get_cum_stats(&self) -> Result<SyncCumStats> {
let pool = self.read_pool.clone();
let result = tokio::task::spawn_blocking(move || -> Result<SyncCumStats> {
let conn = pool.get()?;
let uploaded: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'upload' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let downloaded: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type IN ('download', 'hydration') AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let renamed: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'rename' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let moved: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'move' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let failed: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE status = 'failed'",
[], |r| r.get(0),
).unwrap_or(0);
let conflicts: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'conflict_resolve' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let deleted_local: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'delete_local' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let deleted_remote: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE action_type = 'delete_remote' AND status = 'completed'",
[], |r| r.get(0),
).unwrap_or(0);
let skipped: u32 = conn.query_row(
"SELECT COUNT(*) FROM sync_task_item WHERE status = 'skipped'",
[], |r| r.get(0),
).unwrap_or(0);
Ok(SyncCumStats { uploaded, downloaded, renamed, moved, failed, conflicts, deleted_local, deleted_remote, skipped })
}).await??;
Ok(result)
}
/// 清空所有同步数据(保留 sync_root 和 album_sync_record
pub async fn reset_sync_data(&self) -> Result<()> {
let conn = self.write_conn.lock().await;
conn.execute_batch(
"DELETE FROM sync_task_item;
DELETE FROM sync_task;
DELETE FROM file_mapping;
DELETE FROM conflict;
DELETE FROM transfer_queue;
DELETE FROM sync_log;",
)?;
Ok(())
}
}
pub fn parse_sync_status(s: &str) -> SyncFileStatus {
match s {
"uploading" => SyncFileStatus::Uploading,
"downloading" => SyncFileStatus::Downloading,
"conflict" => SyncFileStatus::Conflict,
"placeholder" => SyncFileStatus::Placeholder,
_ => SyncFileStatus::Synced,
}
}
fn sync_status_str(s: &SyncFileStatus) -> &'static str {
match s {
SyncFileStatus::Synced => "synced",
SyncFileStatus::Uploading => "uploading",
SyncFileStatus::Downloading => "downloading",
SyncFileStatus::Conflict => "conflict",
SyncFileStatus::Placeholder => "placeholder",
}
}
fn transfer_status_str(s: &TransferStatus) -> &'static str {
match s {
TransferStatus::Pending => "pending",
TransferStatus::Active => "active",
TransferStatus::Paused => "paused",
TransferStatus::Completed => "completed",
TransferStatus::Failed => "failed",
}
}
fn transfer_task_from_row(row: &rusqlite::Row<'_>) -> TransferTask {
let direction_str: String = row.get(3).unwrap_or_default();
let direction = if direction_str == "upload" {
TransferDirection::Upload
} else {
TransferDirection::Download
};
let status_str: String = row.get(8).unwrap_or_default();
let status = match status_str.as_str() {
"active" => TransferStatus::Active,
"paused" => TransferStatus::Paused,
"completed" => TransferStatus::Completed,
"failed" => TransferStatus::Failed,
_ => TransferStatus::Pending,
};
TransferTask {
id: row.get(0).unwrap_or(0),
sync_root_id: row.get(1).unwrap_or_default(),
file_mapping_id: row.get(2).ok(),
direction,
local_path: std::path::PathBuf::from(row.get::<_, String>(4).unwrap_or_default()),
remote_uri: row.get(5).unwrap_or_default(),
file_size: row.get(6).unwrap_or(0),
bytes_done: row.get(7).unwrap_or(0),
status,
retry_count: row.get(9).unwrap_or(0),
max_retries: row.get(10).unwrap_or(5),
error_message: row.get(11).ok(),
session_id: row.get(12).ok(),
chunk_index: row.get(13).ok(),
}
}
fn sync_task_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<SyncTask> {
let trigger_str: String = row.get(1)?;
let trigger = match trigger_str.as_str() {
"initial_sync" => WorkerTrigger::InitialSync,
"continuous" => WorkerTrigger::Continuous,
"manual" => WorkerTrigger::Manual,
"hydration" => WorkerTrigger::Hydration,
"wcf_event" => WorkerTrigger::WcfEvent,
_ => WorkerTrigger::Manual,
};
let status_str: String = row.get(5)?;
Ok(SyncTask {
id: row.get(0)?,
trigger,
total_count: row.get(2)?,
completed_count: row.get(3)?,
failed_count: row.get(4)?,
status: status_str
.parse::<WorkerStatus>()
.unwrap_or(WorkerStatus::Pending),
created_at: row.get(6)?,
updated_at: row.get(7)?,
finished_at: row.get(8)?,
})
}
fn sync_task_item_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<SyncTaskItem> {
let action_str: String = row.get(3)?;
let status_str: String = row.get(4)?;
Ok(SyncTaskItem {
id: row.get(0)?,
task_id: row.get(1)?,
relative_path: row.get(2)?,
action_type: action_str
.parse::<TaskActionType>()
.unwrap_or(TaskActionType::Upload),
status: status_str
.parse::<TaskItemStatus>()
.unwrap_or(TaskItemStatus::Pending),
file_size: row.get(5)?,
error_message: row.get(6)?,
created_at: row.get(7)?,
updated_at: row.get(8)?,
})
}