主要合入内容:

- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、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:
2026-06-04 07:11:43 +08:00
parent 5ee6ba1e28
commit b02daf1448
56 changed files with 7589 additions and 1200 deletions
+131 -58
View File
@@ -1,13 +1,15 @@
mod initial_sync;
mod album;
mod continuous_sync;
#[cfg(feature = "linux-fuse")]
mod fuse;
mod initial_sync;
mod local_events;
mod remote_events;
mod album;
#[cfg(feature = "windows-cfapi")]
mod wcf;
// 非 WCF feature 下的 stub 方法,供 remote_events.rs 编译通过
#[cfg(not(feature = "windows-cfapi"))]
// 非 WCF/FUSE feature 下的 stub 方法,供 remote_events.rs 编译通过
#[cfg(not(any(feature = "windows-cfapi", feature = "linux-fuse")))]
impl SyncEngine {
async fn _create_placeholder_for_remote(
&self,
@@ -16,7 +18,7 @@ impl SyncEngine {
_local_root: &std::path::Path,
_root_id: &str,
) {
// MirrorWcf 模式在非 Windows 平台不可用,此方法不应被调用
// MirrorWcf 模式在非 Windows/Linux-FUSE 平台不可用,此方法不应被调用
}
}
@@ -31,9 +33,9 @@ use crate::worker::WorkerPool;
use dashmap::DashMap;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[cfg(feature = "windows-cfapi")]
use tokio::sync::mpsc;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
pub struct SyncEngine {
@@ -66,15 +68,35 @@ pub struct SyncEngine {
/// 缓存的本地同步根路径(WCF 清理时同步读取,避免 await)
#[cfg(feature = "windows-cfapi")]
cached_local_root: std::sync::Mutex<std::path::PathBuf>,
/// FUSE 平台适配器(仅 MirrorWcf + linux-fuse 模式下初始化)
#[cfg(feature = "linux-fuse")]
fuse_adapter: std::sync::Mutex<Option<Arc<crate::platform::fuse::FusePlatformAdapter>>>,
/// FUSE 请求接收端(在适配器初始化时提取)
#[cfg(feature = "linux-fuse")]
fuse_request_rx:
std::sync::Mutex<Option<tokio::sync::mpsc::Receiver<crate::platform::fuse::FuseRequest>>>,
/// FUSE 水合缓存:uri → 已下载的完整文件数据
#[cfg(feature = "linux-fuse")]
hydration_cache: Arc<DashMap<String, (Vec<u8>, std::time::Instant)>>,
}
impl SyncEngine {
pub async fn new(config: SyncConfig) -> Result<Self> {
let db_path = config.data_dir.join("sync_core").join("datas").join(".sync_db.sqlite3");
let db_path = config
.data_dir
.join("sync_core")
.join("datas")
.join(".sync_db.sqlite3");
let db_path_clone = db_path.clone();
let db = Arc::new(tokio::task::spawn_blocking(move || SyncDb::open(&db_path_clone)).await??);
let db =
Arc::new(tokio::task::spawn_blocking(move || SyncDb::open(&db_path_clone)).await??);
let api = Arc::new(ApiClient::new(&config.base_url, &config.access_token, &config.refresh_token, &config.client_id));
let api = Arc::new(ApiClient::new(
&config.base_url,
&config.access_token,
&config.refresh_token,
&config.client_id,
));
let conflict = ConflictResolver::new(config.conflict_strategy.clone());
@@ -127,6 +149,12 @@ impl SyncEngine {
hydration_cache: Arc::new(DashMap::new()),
#[cfg(feature = "windows-cfapi")]
cached_local_root: std::sync::Mutex::new(std::path::PathBuf::new()),
#[cfg(feature = "linux-fuse")]
fuse_adapter: std::sync::Mutex::new(None),
#[cfg(feature = "linux-fuse")]
fuse_request_rx: std::sync::Mutex::new(None),
#[cfg(feature = "linux-fuse")]
hydration_cache: Arc::new(DashMap::new()),
})
}
@@ -184,8 +212,8 @@ impl SyncEngine {
}
/// 重置同步:停止任务 → 清空 DB → 清空本地目录 → 回到初始状态
pub async fn reset_sync(&self) -> Result<()> {
tracing::info!("开始重置同步...");
pub async fn reset_sync(&self, delete_local_files: bool) -> Result<()> {
tracing::info!("开始重置同步... delete_local_files={}", delete_local_files);
// 1. 停止同步
self.stop().await?;
@@ -196,6 +224,12 @@ impl SyncEngine {
self.cleanup_wcf();
}
// 2b. 清理 FUSE
#[cfg(feature = "linux-fuse")]
{
self.cleanup_fuse();
}
// 3. 终止所有活跃 Worker
// 2. 终止所有活跃 Worker
@@ -206,19 +240,27 @@ impl SyncEngine {
tracing::info!("同步数据库已清空");
// 4. 清空本地同步目录(保留目录本身,只删内容)
let local_root = self.config.read().await.local_root.clone();
if local_root.exists() {
let entries = std::fs::read_dir(&local_root)
.map_err(|_| crate::errors::SyncError::DiskFull { needed: 0, available: 0 })?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let _ = std::fs::remove_dir_all(&path);
} else {
let _ = std::fs::remove_file(&path);
if delete_local_files {
let local_root = self.config.read().await.local_root.clone();
if local_root.exists() {
let entries = std::fs::read_dir(&local_root).map_err(|_| {
crate::errors::SyncError::DiskFull {
needed: 0,
available: 0,
}
})?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let _ = std::fs::remove_dir_all(&path);
} else {
let _ = std::fs::remove_file(&path);
}
}
tracing::info!("本地同步目录已清空: {}", local_root.display());
}
tracing::info!("本地同步目录已清空: {}", local_root.display());
} else {
tracing::info!("跳过清空本地同步目录");
}
// 5. 清空内存缓存
@@ -232,14 +274,30 @@ impl SyncEngine {
Ok(())
}
pub fn status(&self) -> SyncStatusSnapshot {
let state = self.state.try_read().map(|g| g.clone()).unwrap_or(SyncState::Idle);
pub async fn status(&self) -> SyncStatusSnapshot {
let state = self
.state
.try_read()
.map(|g| g.clone())
.unwrap_or(SyncState::Idle);
let (synced_files, total_files) = match &state {
SyncState::InitialSync { progress } => {
let done = progress.uploaded + progress.downloaded;
(done, progress.total_to_sync)
}
SyncState::Continuous | SyncState::Paused => {
// 持续同步/暂停:从活跃任务聚合进度
let mut synced: u64 = 0;
let mut total: u64 = 0;
if let Ok(tasks) = self.db.get_active_sync_tasks().await {
for t in &tasks {
synced += t.completed_count as u64;
total += t.total_count as u64;
}
}
(synced, total)
}
_ => (0, 0),
};
@@ -288,7 +346,11 @@ impl SyncEngine {
}
tracing::info!(
"同步配置已更新: 模式={}, 冲突策略={}, WCF删除={}, 并发={}, 带宽限制={:?}",
new_mode, new_conflict, new_wcf_delete, new_max_concurrent, new_bandwidth
new_mode,
new_conflict,
new_wcf_delete,
new_max_concurrent,
new_bandwidth
);
Ok(())
}
@@ -297,7 +359,10 @@ impl SyncEngine {
self.api.update_token(token).await;
}
pub async fn register_event_sink(&self, sink: crate::frb_generated::StreamSink<crate::api::ffi_types::SyncEventFfi>) {
pub async fn register_event_sink(
&self,
sink: crate::frb_generated::StreamSink<crate::api::ffi_types::SyncEventFfi>,
) {
self.event_sink.register(sink).await;
}
@@ -317,6 +382,10 @@ impl SyncEngine {
self.db.query_task_items(filter).await
}
pub async fn get_cum_stats(&self) -> Result<SyncCumStats> {
self.db.get_cum_stats().await
}
pub async fn hydrate_file(&self, local_path: &str) -> Result<()> {
#[cfg(feature = "windows-cfapi")]
{
@@ -340,42 +409,46 @@ impl SyncEngine {
};
let pool = self.db.read_pool();
let result = tokio::task::spawn_blocking(move || -> Result<HashMap<String, FileMapping>> {
let conn = pool.get()?;
let mut stmt = conn.prepare(
"SELECT id, sync_root_id, local_path, remote_uri, remote_file_id,
let result =
tokio::task::spawn_blocking(move || -> Result<HashMap<String, 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"
)?;
FROM file_mapping WHERE sync_root_id = ?1",
)?;
let mappings: HashMap<String, FileMapping> = stmt.query_map(
rusqlite::params![root_id],
|row| {
let local_path: String = row.get(2)?;
Ok((
crate::utils::normalize_path(&local_path),
FileMapping {
id: row.get(0)?,
sync_root_id: row.get(1)?,
local_path: std::path::PathBuf::from(local_path),
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: crate::diff::parse_sync_status_from_str(&row.get::<_, String>(11)?),
is_placeholder: row.get::<_, i32>(12)? != 0,
},
))
},
)?.filter_map(|r| r.ok()).collect();
let mappings: HashMap<String, FileMapping> = stmt
.query_map(rusqlite::params![root_id], |row| {
let local_path: String = row.get(2)?;
Ok((
crate::utils::normalize_path(&local_path),
FileMapping {
id: row.get(0)?,
sync_root_id: row.get(1)?,
local_path: std::path::PathBuf::from(local_path),
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: crate::diff::parse_sync_status_from_str(
&row.get::<_, String>(11)?,
),
is_placeholder: row.get::<_, i32>(12)? != 0,
},
))
})?
.filter_map(|r| r.ok())
.collect();
Ok(mappings)
}).await??;
Ok(mappings)
})
.await??;
Ok(result)
}