1.3.21.3.2
Android APK Release / Build Android APK (push) Successful in 55m29s

This commit is contained in:
2026-05-26 16:32:18 +08:00
parent 546cef5ba6
commit 17673b2862
89 changed files with 24098 additions and 272 deletions
+972
View File
@@ -0,0 +1,972 @@
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::Album => "album",
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)
}
/// 查询所有占位符文件映射(用于 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_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)
}
/// 清空所有同步数据(保留 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,
_ => 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)?,
})
}