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。
456 lines
16 KiB
Rust
456 lines
16 KiB
Rust
mod album;
|
|
mod continuous_sync;
|
|
#[cfg(feature = "linux-fuse")]
|
|
mod fuse;
|
|
mod initial_sync;
|
|
mod local_events;
|
|
mod remote_events;
|
|
#[cfg(feature = "windows-cfapi")]
|
|
mod wcf;
|
|
|
|
// 非 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,
|
|
_relative: &str,
|
|
_remote: &RemoteFileEntry,
|
|
_local_root: &std::path::Path,
|
|
_root_id: &str,
|
|
) {
|
|
// MirrorWcf 模式在非 Windows/Linux-FUSE 平台不可用,此方法不应被调用
|
|
}
|
|
}
|
|
|
|
use crate::api_client::ApiClient;
|
|
use crate::conflict_resolver::ConflictResolver;
|
|
use crate::errors::Result;
|
|
use crate::event_sink::EventSink;
|
|
use crate::file_lock::FileLockRegistry;
|
|
use crate::models::*;
|
|
use crate::sync_db::SyncDb;
|
|
use crate::worker::WorkerPool;
|
|
use dashmap::DashMap;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
#[cfg(feature = "windows-cfapi")]
|
|
use tokio::sync::mpsc;
|
|
use tokio::sync::RwLock;
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
pub struct SyncEngine {
|
|
state: RwLock<SyncState>,
|
|
db: Arc<SyncDb>,
|
|
api: Arc<ApiClient>,
|
|
config: RwLock<SyncConfig>,
|
|
conflict: RwLock<ConflictResolver>,
|
|
sync_root_id: Option<String>,
|
|
shutdown_token: std::sync::Mutex<CancellationToken>,
|
|
/// 同步操作互斥锁:防止 force_sync / run_initial_sync 并发
|
|
sync_lock: tokio::sync::Mutex<()>,
|
|
worker_pool: WorkerPool,
|
|
#[allow(dead_code)]
|
|
file_locks: Arc<FileLockRegistry>,
|
|
#[allow(dead_code)]
|
|
ensured_dirs: Arc<DashMap<String, ()>>,
|
|
event_sink: Arc<EventSink>,
|
|
/// 远程操作导致的本地路径变化,抑制本地 debouncer 自触发事件
|
|
suppress_paths: Arc<DashMap<String, std::time::Instant>>,
|
|
/// WCF 平台适配器(仅 MirrorWcf 模式下初始化)
|
|
#[cfg(feature = "windows-cfapi")]
|
|
platform_adapter: std::sync::Mutex<Option<Arc<crate::platform::wcf::WcfPlatformAdapter>>>,
|
|
/// WCF FETCH_DATA 回调接收端(在适配器初始化时提取)
|
|
#[cfg(feature = "windows-cfapi")]
|
|
wcf_fetch_rx: std::sync::Mutex<Option<mpsc::Receiver<sync_windows::FetchDataRequest>>>,
|
|
/// WCF 水合缓存:uri → 已下载的完整文件数据,避免同一文件重复下载
|
|
#[cfg(feature = "windows-cfapi")]
|
|
hydration_cache: Arc<DashMap<String, (Vec<u8>, std::time::Instant)>>,
|
|
/// 缓存的本地同步根路径(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_clone = db_path.clone();
|
|
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 conflict = ConflictResolver::new(config.conflict_strategy.clone());
|
|
|
|
let sync_root_id = match db.upsert_sync_root(&config).await {
|
|
Ok(id) => Some(id),
|
|
Err(e) => {
|
|
tracing::warn!("写入 sync_root 失败: {}", e);
|
|
None
|
|
}
|
|
};
|
|
|
|
let shutdown_token = CancellationToken::new();
|
|
let file_locks = Arc::new(FileLockRegistry::new());
|
|
let ensured_dirs = Arc::new(DashMap::new());
|
|
let event_sink = Arc::new(EventSink::new());
|
|
let suppress_paths = Arc::new(DashMap::new());
|
|
|
|
let max_workers = config.max_workers;
|
|
let client_id = config.client_id.clone();
|
|
let worker_pool = WorkerPool::new(
|
|
db.clone(),
|
|
api.clone(),
|
|
file_locks.clone(),
|
|
ensured_dirs.clone(),
|
|
event_sink.clone(),
|
|
shutdown_token.clone(),
|
|
max_workers,
|
|
&client_id,
|
|
);
|
|
|
|
Ok(Self {
|
|
state: RwLock::new(SyncState::Idle),
|
|
db,
|
|
api,
|
|
config: RwLock::new(config),
|
|
conflict: RwLock::new(conflict),
|
|
sync_root_id,
|
|
shutdown_token: std::sync::Mutex::new(shutdown_token),
|
|
sync_lock: tokio::sync::Mutex::new(()),
|
|
worker_pool,
|
|
file_locks,
|
|
ensured_dirs,
|
|
event_sink,
|
|
suppress_paths,
|
|
#[cfg(feature = "windows-cfapi")]
|
|
platform_adapter: std::sync::Mutex::new(None),
|
|
#[cfg(feature = "windows-cfapi")]
|
|
wcf_fetch_rx: std::sync::Mutex::new(None),
|
|
#[cfg(feature = "windows-cfapi")]
|
|
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()),
|
|
})
|
|
}
|
|
|
|
async fn snapshot_worker_config(&self) -> WorkerConfig {
|
|
let config = self.config.read().await;
|
|
WorkerConfig {
|
|
local_root: config.local_root.clone(),
|
|
remote_root: config.remote_root.clone(),
|
|
max_concurrent_transfers: config.max_concurrent_transfers,
|
|
bandwidth_limit: config.bandwidth_limit,
|
|
conflict_strategy: config.conflict_strategy.clone(),
|
|
wcf_delete_mode: config.wcf_delete_mode.clone(),
|
|
sync_root_id: self.sync_root_id.clone().unwrap_or_default(),
|
|
sync_mode: config.sync_mode.clone(),
|
|
}
|
|
}
|
|
|
|
/// 确保 shutdown token 未被取消(stop 后重新启动时使用)
|
|
pub fn ensure_token_fresh(&self) {
|
|
let token = self.shutdown_token.lock().unwrap().clone();
|
|
if token.is_cancelled() {
|
|
let new_token = tokio_util::sync::CancellationToken::new();
|
|
self.worker_pool.update_shutdown_token(new_token.clone());
|
|
*self.shutdown_token.lock().unwrap() = new_token;
|
|
}
|
|
}
|
|
|
|
pub async fn stop(&self) -> Result<()> {
|
|
self.shutdown_token.lock().unwrap().cancel();
|
|
*self.state.write().await = SyncState::Stopped;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn pause(&self) -> Result<()> {
|
|
*self.state.write().await = SyncState::Paused;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn resume(&self) -> Result<()> {
|
|
*self.state.write().await = SyncState::Continuous;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn force_sync(&self) -> Result<SyncSummary> {
|
|
// 取消当前所有操作(持续同步 + 正在运行的初始同步)
|
|
self.shutdown_token.lock().unwrap().cancel();
|
|
|
|
// 创建新 token,供接下来的 run_initial_sync 使用
|
|
let new_token = tokio_util::sync::CancellationToken::new();
|
|
*self.shutdown_token.lock().unwrap() = new_token.clone();
|
|
self.worker_pool.update_shutdown_token(new_token);
|
|
|
|
// run_initial_sync 会等待 sync_lock(旧同步的 worker 检测到取消后快速退出,释放锁)
|
|
self.run_initial_sync().await
|
|
}
|
|
|
|
/// 重置同步:停止任务 → 清空 DB → 清空本地目录 → 回到初始状态
|
|
pub async fn reset_sync(&self, delete_local_files: bool) -> Result<()> {
|
|
tracing::info!("开始重置同步... delete_local_files={}", delete_local_files);
|
|
|
|
// 1. 停止同步
|
|
self.stop().await?;
|
|
|
|
// 2. 清理 WCF(重置时需要彻底清理)
|
|
#[cfg(feature = "windows-cfapi")]
|
|
{
|
|
self.cleanup_wcf();
|
|
}
|
|
|
|
// 2b. 清理 FUSE
|
|
#[cfg(feature = "linux-fuse")]
|
|
{
|
|
self.cleanup_fuse();
|
|
}
|
|
|
|
// 3. 终止所有活跃 Worker
|
|
|
|
// 2. 终止所有活跃 Worker
|
|
self.worker_pool.abort_all_workers().await;
|
|
|
|
// 3. 清空 DB 业务数据
|
|
self.db.reset_sync_data().await?;
|
|
tracing::info!("同步数据库已清空");
|
|
|
|
// 4. 清空本地同步目录(保留目录本身,只删内容)
|
|
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());
|
|
}
|
|
} else {
|
|
tracing::info!("跳过清空本地同步目录");
|
|
}
|
|
|
|
// 5. 清空内存缓存
|
|
self.ensured_dirs.clear();
|
|
self.suppress_paths.clear();
|
|
|
|
// 6. 重置状态
|
|
*self.state.write().await = SyncState::Idle;
|
|
|
|
tracing::info!("同步重置完成,已回到初始状态");
|
|
Ok(())
|
|
}
|
|
|
|
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),
|
|
};
|
|
|
|
SyncStatusSnapshot {
|
|
state,
|
|
synced_files,
|
|
total_files,
|
|
uploading_count: 0,
|
|
downloading_count: 0,
|
|
conflict_count: 0,
|
|
error_count: 0,
|
|
last_sync_time: None,
|
|
error_message: None,
|
|
}
|
|
}
|
|
|
|
pub fn active_worker_count(&self) -> u32 {
|
|
self.worker_pool.active_worker_count() as u32
|
|
}
|
|
|
|
pub async fn config(&self) -> SyncConfig {
|
|
self.config.read().await.clone()
|
|
}
|
|
|
|
pub async fn update_config(&self, new_config: SyncConfig) -> Result<()> {
|
|
let old_access_token = {
|
|
let config = self.config.read().await;
|
|
config.access_token.clone()
|
|
};
|
|
|
|
*self.conflict.write().await = ConflictResolver::new(new_config.conflict_strategy.clone());
|
|
|
|
if new_config.access_token != old_access_token {
|
|
self.api.update_token(new_config.access_token.clone()).await;
|
|
}
|
|
|
|
let new_bandwidth = new_config.bandwidth_limit;
|
|
let new_conflict = format!("{:?}", new_config.conflict_strategy);
|
|
let new_wcf_delete = format!("{:?}", new_config.wcf_delete_mode);
|
|
let new_mode = format!("{:?}", new_config.sync_mode);
|
|
let new_max_concurrent = new_config.max_concurrent_transfers;
|
|
*self.config.write().await = new_config;
|
|
|
|
if new_bandwidth.is_some() {
|
|
tracing::info!("仅对下载限速生效, 由于Cloudreve实现原因, 上传限速无法生效");
|
|
}
|
|
tracing::info!(
|
|
"同步配置已更新: 模式={}, 冲突策略={}, WCF删除={}, 并发={}, 带宽限制={:?}",
|
|
new_mode,
|
|
new_conflict,
|
|
new_wcf_delete,
|
|
new_max_concurrent,
|
|
new_bandwidth
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update_access_token(&self, token: String) {
|
|
self.api.update_token(token).await;
|
|
}
|
|
|
|
pub async fn register_event_sink(
|
|
&self,
|
|
sink: crate::frb_generated::StreamSink<crate::api::ffi_types::SyncEventFfi>,
|
|
) {
|
|
self.event_sink.register(sink).await;
|
|
}
|
|
|
|
pub async fn get_active_tasks(&self) -> Result<Vec<SyncTask>> {
|
|
self.db.get_active_sync_tasks().await
|
|
}
|
|
|
|
pub async fn get_recent_tasks(&self, limit: u32) -> Result<Vec<SyncTask>> {
|
|
self.db.get_recent_sync_tasks(limit).await
|
|
}
|
|
|
|
pub async fn get_task_detail(&self, task_id: &str) -> Result<Vec<SyncTaskItem>> {
|
|
self.db.get_sync_task_items(task_id).await
|
|
}
|
|
|
|
pub async fn query_task_items(&self, filter: &TaskItemFilter) -> Result<Vec<SyncTaskItem>> {
|
|
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")]
|
|
{
|
|
let path = std::path::PathBuf::from(local_path);
|
|
if let Some(adapter) = self.platform_adapter.lock().unwrap().as_ref() {
|
|
adapter.hydrate_file(&path)?;
|
|
}
|
|
}
|
|
let _ = local_path;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn shutdown(self) -> Result<()> {
|
|
self.stop().await
|
|
}
|
|
|
|
async fn load_all_mappings(&self) -> Result<HashMap<String, FileMapping>> {
|
|
let root_id = match &self.sync_root_id {
|
|
Some(id) => id.clone(),
|
|
None => return Ok(HashMap::new()),
|
|
};
|
|
|
|
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,
|
|
local_hash, remote_hash, local_mtime, remote_mtime,
|
|
local_size, remote_size, sync_status, is_placeholder
|
|
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();
|
|
|
|
Ok(mappings)
|
|
})
|
|
.await??;
|
|
|
|
Ok(result)
|
|
}
|
|
}
|