主要合入内容:
- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、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。
This commit is contained in:
+156
-24
@@ -175,7 +175,9 @@ impl SyncDb {
|
||||
// 添加 task_item 唯一索引(去重:保留 id 最小的记录)
|
||||
// 先清理重复数据,再建唯一索引
|
||||
let existing: Vec<String> = conn
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_task_item_unique'")?
|
||||
.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();
|
||||
@@ -200,7 +202,8 @@ impl SyncDb {
|
||||
SyncMode::Full => "full",
|
||||
SyncMode::UploadOnly => "upload_only",
|
||||
SyncMode::DownloadOnly => "download_only",
|
||||
SyncMode::Album => "album",
|
||||
SyncMode::AlbumUpload => "album_upload",
|
||||
SyncMode::AlbumDownload => "album_download",
|
||||
SyncMode::MirrorWcf => "mirror_wcf",
|
||||
};
|
||||
|
||||
@@ -327,12 +330,38 @@ impl SyncDb {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 查询所有占位符文件映射(用于 WCF 退出时清理)
|
||||
#[cfg(feature = "windows-cfapi")]
|
||||
pub async fn list_placeholder_mappings(
|
||||
/// 删除指定 remote_uri 的文件映射
|
||||
pub async fn delete_mapping_by_remote_uri(
|
||||
&self,
|
||||
sync_root_id: &str,
|
||||
) -> Result<Vec<FileMapping>> {
|
||||
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();
|
||||
|
||||
@@ -345,26 +374,30 @@ impl SyncDb {
|
||||
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();
|
||||
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??;
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -727,6 +760,46 @@ impl SyncDb {
|
||||
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;
|
||||
@@ -848,6 +921,63 @@ impl SyncDb {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 从 DB 聚合累积统计(跨所有同步任务)
|
||||
/// downloaded 包含 download + hydration(create_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;
|
||||
@@ -933,6 +1063,8 @@ fn sync_task_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<SyncTask> {
|
||||
"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)?;
|
||||
|
||||
Reference in New Issue
Block a user