主要合入内容:
- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、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:
Generated
+65
@@ -379,6 +379,16 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
@@ -482,6 +492,22 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuser"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
"nix",
|
||||
"page_size",
|
||||
"pkg-config",
|
||||
"smallvec",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
@@ -1194,6 +1220,18 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "7.0.0"
|
||||
@@ -1316,6 +1354,16 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -1821,6 +1869,16 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -1870,9 +1928,11 @@ dependencies = [
|
||||
name = "sync-android"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"jni",
|
||||
"log",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -1882,6 +1942,7 @@ dependencies = [
|
||||
name = "sync-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bytes",
|
||||
@@ -1889,10 +1950,13 @@ dependencies = [
|
||||
"dashmap 6.1.0",
|
||||
"filetime",
|
||||
"flutter_rust_bridge",
|
||||
"fuser",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"notify-debouncer-full",
|
||||
"once_cell",
|
||||
@@ -2060,6 +2124,7 @@ dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
rust_input: crate::api
|
||||
rust_root: sync-core/
|
||||
dart_output: D:/flutter_projects/cloudreve4_flutter/lib/src/rust/
|
||||
dart_output: ../lib/src/rust/
|
||||
dart_entrypoint_class_name: RustSyncApi
|
||||
dart_enums_style: true
|
||||
|
||||
@@ -14,3 +14,10 @@ tracing = "0.1"
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
async-trait = "0.1"
|
||||
|
||||
# 通用日志接口
|
||||
log = "0.4"
|
||||
|
||||
# 仅在 Android 平台下生效的日志后端
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.15"
|
||||
|
||||
@@ -11,7 +11,7 @@ crate-type = ["cdylib", "lib"]
|
||||
flutter_rust_bridge = "=2.12.0"
|
||||
|
||||
# 异步运行时
|
||||
tokio = { workspace = true }
|
||||
tokio = { workspace = true, features = ["signal"] }
|
||||
|
||||
# HTTP
|
||||
reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls"], default-features = false }
|
||||
@@ -67,15 +67,20 @@ windows = { version = "0.58", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
sync-linux = { path = "../sync-linux", optional = true }
|
||||
fuser = { version = "0.15", optional = true }
|
||||
libc = { version = "0.2", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
sync-android = { path = "../sync-android", optional = true }
|
||||
jni = { version = "0.21", optional = true }
|
||||
android_logger = "0.15"
|
||||
log = "0.4"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
windows-cfapi = ["sync-windows", "windows"]
|
||||
linux-notify = ["sync-linux"]
|
||||
linux-fuse = ["fuser", "libc"]
|
||||
android-media = ["sync-android", "jni"]
|
||||
event_sink_enabled = []
|
||||
|
||||
|
||||
+268
-42
@@ -4,6 +4,75 @@ use std::sync::Arc;
|
||||
use crate::api::ffi_types::*;
|
||||
use crate::sync_engine::SyncEngine;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android_log {
|
||||
use std::fmt::Write;
|
||||
use tracing::{Event, Level, Subscriber};
|
||||
use tracing_subscriber::layer::{Context, Layer};
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
|
||||
/// Tracing Layer:将事件转发到 `log` crate → android_logger → Logcat
|
||||
pub struct AndroidLogLayer;
|
||||
|
||||
impl<S> Layer<S> for AndroidLogLayer
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
{
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||
let log_level = match *event.metadata().level() {
|
||||
Level::ERROR => log::Level::Error,
|
||||
Level::WARN => log::Level::Warn,
|
||||
Level::INFO => log::Level::Info,
|
||||
Level::DEBUG => log::Level::Debug,
|
||||
Level::TRACE => log::Level::Trace,
|
||||
};
|
||||
|
||||
let mut visitor = EventVisitor::default();
|
||||
event.record(&mut visitor);
|
||||
|
||||
let target = event.metadata().target();
|
||||
if visitor.message.is_empty() {
|
||||
log::log!(log_level, "[{}] {}", target, visitor.fields.trim_end());
|
||||
} else if visitor.fields.is_empty() {
|
||||
log::log!(log_level, "[{}] {}", target, visitor.message);
|
||||
} else {
|
||||
log::log!(
|
||||
log_level,
|
||||
"[{}] {} {}",
|
||||
target,
|
||||
visitor.message,
|
||||
visitor.fields.trim_end()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct EventVisitor {
|
||||
message: String,
|
||||
fields: String,
|
||||
}
|
||||
|
||||
impl tracing::field::Visit for EventVisitor {
|
||||
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||
if field.name() == "message" {
|
||||
// message 字段由 tracing::field::display() 包装,其 Debug 实际走 Display,无多余引号
|
||||
write!(self.message, "{:?}", value).unwrap();
|
||||
} else {
|
||||
write!(self.fields, "{}={:?} ", field.name(), value).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_android_logger() {
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default()
|
||||
.with_max_level(log::LevelFilter::Trace)
|
||||
.with_tag("RustSyncCore"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 全局引擎实例
|
||||
static ENGINE: once_cell::sync::OnceCell<Arc<SyncEngine>> = once_cell::sync::OnceCell::new();
|
||||
|
||||
@@ -36,7 +105,8 @@ fn config_from_ffi(ffi: SyncConfigFfi) -> crate::models::SyncConfig {
|
||||
let sync_mode = match ffi.sync_mode.as_str() {
|
||||
"upload_only" => SyncMode::UploadOnly,
|
||||
"download_only" => SyncMode::DownloadOnly,
|
||||
"album" => SyncMode::Album,
|
||||
"album_upload" => SyncMode::AlbumUpload,
|
||||
"album_download" => SyncMode::AlbumDownload,
|
||||
"mirror_wcf" => SyncMode::MirrorWcf,
|
||||
_ => SyncMode::Full,
|
||||
};
|
||||
@@ -86,7 +156,8 @@ fn config_to_ffi(c: &crate::models::SyncConfig) -> SyncConfigFfi {
|
||||
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",
|
||||
};
|
||||
|
||||
@@ -174,12 +245,17 @@ fn album_result_to_ffi(r: crate::models::CloudAlbumCheckResult) -> CloudAlbumChe
|
||||
pictures_exists: r.pictures_exists,
|
||||
dcim_uri: r.dcim_uri,
|
||||
pictures_uri: r.pictures_uri,
|
||||
camera_exists: r.camera_exists,
|
||||
camera_uri: r.camera_uri,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取引擎引用,未初始化则返回错误
|
||||
fn get_engine() -> Result<&'static SyncEngine, SyncErrorFfi> {
|
||||
ENGINE.get().map(|arc| arc.as_ref()).ok_or(SyncErrorFfi::NotInitialized)
|
||||
ENGINE
|
||||
.get()
|
||||
.map(|arc| arc.as_ref())
|
||||
.ok_or(SyncErrorFfi::NotInitialized)
|
||||
}
|
||||
|
||||
/// 内部:应用日志级别到 reload handle
|
||||
@@ -244,6 +320,10 @@ pub async fn init_sync_engine(config: SyncConfigFfi) -> Result<(), SyncErrorFfi>
|
||||
eprintln!("[sync-core] 警告: 无法创建日志文件 {}", log_path.display());
|
||||
}
|
||||
|
||||
// Android: 初始化 Logcat 日志后端(tracing → log → android_logger → Logcat)
|
||||
#[cfg(target_os = "android")]
|
||||
android_log::init_android_logger();
|
||||
|
||||
// 尝试初始化 subscriber(仅首次有效,后续调用忽略)
|
||||
{
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
@@ -258,18 +338,22 @@ pub async fn init_sync_engine(config: SyncConfigFfi) -> Result<(), SyncErrorFfi>
|
||||
|
||||
let registry = tracing_subscriber::registry().with(reload_filter);
|
||||
|
||||
// Android: 添加 Logcat 桥接层
|
||||
#[cfg(target_os = "android")]
|
||||
let registry = registry.with(android_log::AndroidLogLayer);
|
||||
|
||||
if let Some(file) = log_file {
|
||||
let _ = registry
|
||||
.with(tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::sync::Mutex::new(file))
|
||||
.with_ansi(false))
|
||||
.with(tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::io::stderr))
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::sync::Mutex::new(file))
|
||||
.with_ansi(false),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
|
||||
.try_init();
|
||||
} else {
|
||||
let _ = registry
|
||||
.with(tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::io::stderr))
|
||||
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
|
||||
.try_init();
|
||||
}
|
||||
}
|
||||
@@ -281,10 +365,12 @@ pub async fn init_sync_engine(config: SyncConfigFfi) -> Result<(), SyncErrorFfi>
|
||||
let log_bandwidth = config.bandwidth_limit_kbps;
|
||||
let log_level = config.log_level.clone();
|
||||
|
||||
let engine = SyncEngine::new(config_from_ffi(config)).await
|
||||
let engine = SyncEngine::new(config_from_ffi(config))
|
||||
.await
|
||||
.map_err(error_to_ffi)?;
|
||||
|
||||
ENGINE.set(Arc::new(engine))
|
||||
ENGINE
|
||||
.set(Arc::new(engine))
|
||||
.map_err(|_| SyncErrorFfi::InternalError {
|
||||
message: "引擎已初始化".to_string(),
|
||||
})?;
|
||||
@@ -292,7 +378,10 @@ pub async fn init_sync_engine(config: SyncConfigFfi) -> Result<(), SyncErrorFfi>
|
||||
tracing::info!("同步引擎初始化完成, 日志文件: {}", log_path.display());
|
||||
tracing::info!(
|
||||
"配置: 模式={}, 冲突策略={}, 并发={}, 带宽限制={}kbps",
|
||||
log_sync_mode, log_conflict_strategy, log_max_concurrent, log_bandwidth,
|
||||
log_sync_mode,
|
||||
log_conflict_strategy,
|
||||
log_max_concurrent,
|
||||
log_bandwidth,
|
||||
);
|
||||
if log_bandwidth > 0 {
|
||||
tracing::info!("仅对下载限速生效, 由于Cloudreve实现原因, 上传限速无法生效");
|
||||
@@ -301,6 +390,41 @@ pub async fn init_sync_engine(config: SyncConfigFfi) -> Result<(), SyncErrorFfi>
|
||||
// 应用配置中的日志级别(热修改覆盖默认 debug)
|
||||
apply_log_level(&log_level);
|
||||
|
||||
// 注册 SIGINT/SIGTERM 信号处理,确保 FUSE/WCF 等资源被优雅清理
|
||||
#[cfg(unix)]
|
||||
{
|
||||
tokio::spawn(async {
|
||||
let mut sigterm =
|
||||
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!("无法注册 SIGTERM 处理: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
tracing::info!("收到 SIGINT,开始清理...");
|
||||
}
|
||||
_ = sigterm.recv() => {
|
||||
tracing::info!("收到 SIGTERM,开始清理...");
|
||||
}
|
||||
}
|
||||
sync_shutdown().ok();
|
||||
std::process::exit(0);
|
||||
});
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
tokio::spawn(async {
|
||||
if tokio::signal::ctrl_c().await.is_ok() {
|
||||
tracing::info!("收到 Ctrl+C,开始清理...");
|
||||
sync_shutdown().ok();
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -316,11 +440,16 @@ pub async fn dispose_sync_engine() -> Result<(), SyncErrorFfi> {
|
||||
engine.cleanup_wcf();
|
||||
}
|
||||
|
||||
#[cfg(feature = "linux-fuse")]
|
||||
{
|
||||
engine.cleanup_fuse();
|
||||
}
|
||||
|
||||
tracing::info!("同步引擎已停止");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 进程退出前同步清理(WCF 模式下必须调用,确保占位符释放)
|
||||
/// 进程退出前同步清理(WCF/FUSE 模式下必须调用,确保占位符释放和挂载点卸载)
|
||||
/// 此函数是同步的,不依赖 tokio runtime,可安全在 exit(0) 前调用
|
||||
#[frb]
|
||||
pub fn sync_shutdown() -> Result<(), SyncErrorFfi> {
|
||||
@@ -333,6 +462,14 @@ pub fn sync_shutdown() -> Result<(), SyncErrorFfi> {
|
||||
};
|
||||
engine.cleanup_wcf();
|
||||
}
|
||||
#[cfg(feature = "linux-fuse")]
|
||||
{
|
||||
let engine = match ENGINE.get() {
|
||||
Some(e) => e,
|
||||
None => return Ok(()),
|
||||
};
|
||||
engine.cleanup_fuse();
|
||||
}
|
||||
tracing::info!("同步引擎已同步清理");
|
||||
Ok(())
|
||||
}
|
||||
@@ -345,10 +482,17 @@ pub async fn start_initial_sync() -> Result<SyncSummaryFfi, SyncErrorFfi> {
|
||||
tracing::debug!("[FFI] start_initial_sync ←");
|
||||
let engine = get_engine()?;
|
||||
engine.ensure_token_fresh();
|
||||
engine.run_initial_sync().await
|
||||
engine
|
||||
.run_initial_sync()
|
||||
.await
|
||||
.map(|s| {
|
||||
tracing::debug!("[FFI] start_initial_sync → uploaded={}, downloaded={}, conflicts={}, failed={}",
|
||||
s.uploaded, s.downloaded, s.conflicts, s.failed);
|
||||
tracing::debug!(
|
||||
"[FFI] start_initial_sync → uploaded={}, downloaded={}, conflicts={}, failed={}",
|
||||
s.uploaded,
|
||||
s.downloaded,
|
||||
s.conflicts,
|
||||
s.failed
|
||||
);
|
||||
summary_to_ffi(s)
|
||||
})
|
||||
.map_err(error_to_ffi)
|
||||
@@ -398,21 +542,34 @@ pub async fn resume_sync() -> Result<(), SyncErrorFfi> {
|
||||
pub async fn force_sync() -> Result<SyncSummaryFfi, SyncErrorFfi> {
|
||||
tracing::debug!("[FFI] force_sync ←");
|
||||
let engine = get_engine()?;
|
||||
engine.force_sync().await
|
||||
engine
|
||||
.force_sync()
|
||||
.await
|
||||
.map(|s| {
|
||||
tracing::debug!("[FFI] force_sync → uploaded={}, downloaded={}, conflicts={}, failed={}",
|
||||
s.uploaded, s.downloaded, s.conflicts, s.failed);
|
||||
tracing::debug!(
|
||||
"[FFI] force_sync → uploaded={}, downloaded={}, conflicts={}, failed={}",
|
||||
s.uploaded,
|
||||
s.downloaded,
|
||||
s.conflicts,
|
||||
s.failed
|
||||
);
|
||||
summary_to_ffi(s)
|
||||
})
|
||||
.map_err(error_to_ffi)
|
||||
}
|
||||
|
||||
/// 重置同步:停止任务 → 清空 DB → 清空本地目录 → 回到初始状态
|
||||
/// 重置同步:停止任务 → 清空 DB → (可选)清空本地目录 → 回到初始状态
|
||||
#[frb]
|
||||
pub async fn reset_sync() -> Result<(), SyncErrorFfi> {
|
||||
tracing::debug!("[FFI] reset_sync ←");
|
||||
pub async fn reset_sync(delete_local_files: bool) -> Result<(), SyncErrorFfi> {
|
||||
tracing::debug!(
|
||||
"[FFI] reset_sync ← delete_local_files={}",
|
||||
delete_local_files
|
||||
);
|
||||
let engine = get_engine()?;
|
||||
engine.reset_sync().await.map_err(error_to_ffi)
|
||||
engine
|
||||
.reset_sync(delete_local_files)
|
||||
.await
|
||||
.map_err(error_to_ffi)
|
||||
}
|
||||
|
||||
// ========== 状态查询 ==========
|
||||
@@ -421,8 +578,13 @@ pub async fn reset_sync() -> Result<(), SyncErrorFfi> {
|
||||
#[frb]
|
||||
pub async fn get_sync_status() -> Result<SyncStatusFfi, SyncErrorFfi> {
|
||||
let engine = get_engine()?;
|
||||
let s = engine.status();
|
||||
tracing::trace!("[FFI] get_sync_status → state={:?}, synced={}, total={}", s.state, s.synced_files, s.total_files);
|
||||
let s = engine.status().await;
|
||||
tracing::trace!(
|
||||
"[FFI] get_sync_status → state={:?}, synced={}, total={}",
|
||||
s.state,
|
||||
s.synced_files,
|
||||
s.total_files
|
||||
);
|
||||
Ok(status_to_ffi(s))
|
||||
}
|
||||
|
||||
@@ -440,7 +602,11 @@ pub async fn get_active_worker_count() -> Result<u32, SyncErrorFfi> {
|
||||
pub async fn get_sync_config() -> Result<SyncConfigFfi, SyncErrorFfi> {
|
||||
let engine = get_engine()?;
|
||||
let c = engine.config().await;
|
||||
tracing::trace!("[FFI] get_sync_config → mode={:?}, conflict={:?}", c.sync_mode, c.conflict_strategy);
|
||||
tracing::trace!(
|
||||
"[FFI] get_sync_config → mode={:?}, conflict={:?}",
|
||||
c.sync_mode,
|
||||
c.conflict_strategy
|
||||
);
|
||||
Ok(config_to_ffi(&c))
|
||||
}
|
||||
|
||||
@@ -450,7 +616,10 @@ pub async fn update_sync_config(config: SyncConfigFfi) -> Result<(), SyncErrorFf
|
||||
tracing::debug!("[FFI] update_sync_config ← mode={}, conflict={}, wcf_delete={}, concurrent={}, bandwidth={}kbps",
|
||||
config.sync_mode, config.conflict_strategy, config.wcf_delete_mode, config.max_concurrent_transfers, config.bandwidth_limit_kbps);
|
||||
let engine = get_engine()?;
|
||||
engine.update_config(config_from_ffi(config)).await.map_err(error_to_ffi)
|
||||
engine
|
||||
.update_config(config_from_ffi(config))
|
||||
.await
|
||||
.map_err(error_to_ffi)
|
||||
}
|
||||
|
||||
// ========== Token 管理 ==========
|
||||
@@ -482,19 +651,34 @@ pub async fn sync_album_to_cloud(
|
||||
album_paths: Vec<String>,
|
||||
remote_dcim_uri: String,
|
||||
) -> Result<(), SyncErrorFfi> {
|
||||
tracing::debug!("[FFI] sync_album_to_cloud ← paths={}, uri={}", album_paths.len(), remote_dcim_uri);
|
||||
tracing::debug!(
|
||||
"[FFI] sync_album_to_cloud ← paths={}, uri={}",
|
||||
album_paths.len(),
|
||||
remote_dcim_uri
|
||||
);
|
||||
let engine = get_engine()?;
|
||||
engine.sync_album(album_paths, &remote_dcim_uri).await.map_err(error_to_ffi)
|
||||
engine
|
||||
.sync_album(album_paths, &remote_dcim_uri)
|
||||
.await
|
||||
.map_err(error_to_ffi)
|
||||
}
|
||||
|
||||
/// 检查云端是否存在 DCIM/Pictures 目录
|
||||
#[frb]
|
||||
pub async fn check_cloud_album_dirs(base_uri: String) -> Result<CloudAlbumCheckResultFfi, SyncErrorFfi> {
|
||||
pub async fn check_cloud_album_dirs(
|
||||
base_uri: String,
|
||||
) -> Result<CloudAlbumCheckResultFfi, SyncErrorFfi> {
|
||||
tracing::debug!("[FFI] check_cloud_album_dirs ← uri={}", base_uri);
|
||||
let engine = get_engine()?;
|
||||
engine.check_album_dirs(&base_uri).await
|
||||
engine
|
||||
.check_album_dirs(&base_uri)
|
||||
.await
|
||||
.map(|r| {
|
||||
tracing::debug!("[FFI] check_cloud_album_dirs → dcim={}, pictures={}", r.dcim_exists, r.pictures_exists);
|
||||
tracing::debug!(
|
||||
"[FFI] check_cloud_album_dirs → dcim={}, pictures={}",
|
||||
r.dcim_exists,
|
||||
r.pictures_exists
|
||||
);
|
||||
album_result_to_ffi(r)
|
||||
})
|
||||
.map_err(error_to_ffi)
|
||||
@@ -505,18 +689,26 @@ pub async fn check_cloud_album_dirs(base_uri: String) -> Result<CloudAlbumCheckR
|
||||
pub async fn create_cloud_album_dirs(base_uri: String) -> Result<(), SyncErrorFfi> {
|
||||
tracing::debug!("[FFI] create_cloud_album_dirs ← uri={}", base_uri);
|
||||
let engine = get_engine()?;
|
||||
engine.create_album_dirs(&base_uri).await.map_err(error_to_ffi)
|
||||
engine
|
||||
.create_album_dirs(&base_uri)
|
||||
.await
|
||||
.map_err(error_to_ffi)
|
||||
}
|
||||
|
||||
// ========== 事件推送 ==========
|
||||
|
||||
/// 注册 Rust→Dart 事件推送通道
|
||||
#[frb]
|
||||
pub fn register_sync_event_sink(sink: crate::frb_generated::StreamSink<SyncEventFfi>) -> Result<(), SyncErrorFfi> {
|
||||
pub fn register_sync_event_sink(
|
||||
sink: crate::frb_generated::StreamSink<SyncEventFfi>,
|
||||
) -> Result<(), SyncErrorFfi> {
|
||||
tracing::debug!("[FFI] register_sync_event_sink ←");
|
||||
let engine = get_engine()?;
|
||||
// 使用 tokio runtime 注册
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
// flutter_rust_bridge 可能在非 Tokio 线程调用此同步函数,
|
||||
// 使用 spawn_blocking + block_on 确保 runtime 上下文可用
|
||||
let rt = tokio::runtime::Runtime::new().map_err(|e| SyncErrorFfi::InternalError {
|
||||
message: format!("创建 Tokio runtime 失败: {}", e),
|
||||
})?;
|
||||
rt.block_on(engine.register_event_sink(sink));
|
||||
Ok(())
|
||||
}
|
||||
@@ -569,16 +761,26 @@ pub async fn get_recent_tasks(limit: u32) -> Result<Vec<SyncTaskFfi>, SyncErrorF
|
||||
pub async fn get_task_detail(task_id: String) -> Result<Vec<SyncTaskItemFfi>, SyncErrorFfi> {
|
||||
tracing::trace!("[FFI] get_task_detail ← task_id={}", task_id);
|
||||
let engine = get_engine()?;
|
||||
let items = engine.get_task_detail(&task_id).await.map_err(error_to_ffi)?;
|
||||
let items = engine
|
||||
.get_task_detail(&task_id)
|
||||
.await
|
||||
.map_err(error_to_ffi)?;
|
||||
tracing::trace!("[FFI] get_task_detail → count={}", items.len());
|
||||
Ok(items.into_iter().map(task_item_to_ffi).collect())
|
||||
}
|
||||
|
||||
/// 多维度查询任务项
|
||||
#[frb]
|
||||
pub async fn query_task_items(filter: TaskItemFilterFfi) -> Result<Vec<SyncTaskItemFfi>, SyncErrorFfi> {
|
||||
tracing::trace!("[FFI] query_task_items ← task_id={:?}, action={:?}, status={:?}, limit={}",
|
||||
filter.task_id, filter.action_type, filter.status, filter.limit);
|
||||
pub async fn query_task_items(
|
||||
filter: TaskItemFilterFfi,
|
||||
) -> Result<Vec<SyncTaskItemFfi>, SyncErrorFfi> {
|
||||
tracing::trace!(
|
||||
"[FFI] query_task_items ← task_id={:?}, action={:?}, status={:?}, limit={}",
|
||||
filter.task_id,
|
||||
filter.action_type,
|
||||
filter.status,
|
||||
filter.limit
|
||||
);
|
||||
let engine = get_engine()?;
|
||||
let model_filter = crate::models::TaskItemFilter {
|
||||
task_id: filter.task_id,
|
||||
@@ -588,11 +790,35 @@ pub async fn query_task_items(filter: TaskItemFilterFfi) -> Result<Vec<SyncTaskI
|
||||
limit: filter.limit.max(1).min(1000),
|
||||
offset: filter.offset,
|
||||
};
|
||||
let items = engine.query_task_items(&model_filter).await.map_err(error_to_ffi)?;
|
||||
let items = engine
|
||||
.query_task_items(&model_filter)
|
||||
.await
|
||||
.map_err(error_to_ffi)?;
|
||||
tracing::trace!("[FFI] query_task_items → count={}", items.len());
|
||||
Ok(items.into_iter().map(task_item_to_ffi).collect())
|
||||
}
|
||||
|
||||
/// 获取累积统计(从 DB 聚合,跨所有同步任务)
|
||||
#[frb]
|
||||
pub async fn get_sync_cum_stats() -> Result<SyncCumStatsFfi, SyncErrorFfi> {
|
||||
tracing::trace!("[FFI] get_sync_cum_stats ←");
|
||||
let engine = get_engine()?;
|
||||
let stats = engine.get_cum_stats().await.map_err(error_to_ffi)?;
|
||||
tracing::trace!("[FFI] get_sync_cum_stats → uploaded={}, downloaded={}, renamed={}, moved={}, failed={}, conflicts={}, deleted_local={}, deleted_remote={}, skipped={}",
|
||||
stats.uploaded, stats.downloaded, stats.renamed, stats.moved, stats.failed, stats.conflicts, stats.deleted_local, stats.deleted_remote, stats.skipped);
|
||||
Ok(SyncCumStatsFfi {
|
||||
uploaded: stats.uploaded,
|
||||
downloaded: stats.downloaded,
|
||||
renamed: stats.renamed,
|
||||
moved: stats.moved,
|
||||
failed: stats.failed,
|
||||
conflicts: stats.conflicts,
|
||||
deleted_local: stats.deleted_local,
|
||||
deleted_remote: stats.deleted_remote,
|
||||
skipped: stats.skipped,
|
||||
})
|
||||
}
|
||||
|
||||
fn task_to_ffi(t: crate::models::SyncTask) -> SyncTaskFfi {
|
||||
SyncTaskFfi {
|
||||
id: t.id,
|
||||
|
||||
@@ -61,7 +61,9 @@ pub struct SyncSummaryFfi {
|
||||
/// 同步事件(Rust → Dart 推送)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SyncEventFfi {
|
||||
StateChanged { new_state: String },
|
||||
StateChanged {
|
||||
new_state: String,
|
||||
},
|
||||
Progress {
|
||||
synced: u64,
|
||||
total: u64,
|
||||
@@ -84,8 +86,12 @@ pub enum SyncEventFfi {
|
||||
recoverable: bool,
|
||||
},
|
||||
TokenExpired,
|
||||
DiskSpaceWarning { available_mb: u64 },
|
||||
InitialSyncComplete { summary: SyncSummaryFfi },
|
||||
DiskSpaceWarning {
|
||||
available_mb: u64,
|
||||
},
|
||||
InitialSyncComplete {
|
||||
summary: SyncSummaryFfi,
|
||||
},
|
||||
|
||||
// Worker 事件
|
||||
WorkerStarted {
|
||||
@@ -122,6 +128,8 @@ pub struct CloudAlbumCheckResultFfi {
|
||||
pub pictures_exists: bool,
|
||||
pub dcim_uri: Option<String>,
|
||||
pub pictures_uri: Option<String>,
|
||||
pub camera_exists: bool,
|
||||
pub camera_uri: Option<String>,
|
||||
}
|
||||
|
||||
/// 同步任务摘要(FFI)
|
||||
@@ -152,6 +160,20 @@ pub struct SyncTaskItemFfi {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// 累积统计(FFI)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SyncCumStatsFfi {
|
||||
pub uploaded: u32,
|
||||
pub downloaded: u32,
|
||||
pub renamed: u32,
|
||||
pub moved: u32,
|
||||
pub failed: u32,
|
||||
pub conflicts: u32,
|
||||
pub deleted_local: u32,
|
||||
pub deleted_remote: u32,
|
||||
pub skipped: u32,
|
||||
}
|
||||
|
||||
/// 任务项查询过滤器(FFI)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TaskItemFilterFfi {
|
||||
|
||||
@@ -14,7 +14,12 @@ pub fn compute_diff(
|
||||
// 构建索引: relative_path → entry(统一正斜杠)
|
||||
let local_map: HashMap<String, &LocalFileEntry> = local_files
|
||||
.iter()
|
||||
.map(|e| (crate::utils::normalize_path(&e.relative_path.to_string_lossy()), e))
|
||||
.map(|e| {
|
||||
(
|
||||
crate::utils::normalize_path(&e.relative_path.to_string_lossy()),
|
||||
e,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let remote_map: HashMap<String, &RemoteFileEntry> = remote_files
|
||||
@@ -43,9 +48,9 @@ pub fn compute_diff(
|
||||
let db = db_mappings.get(path.as_str());
|
||||
|
||||
match (local, remote, db) {
|
||||
// 本地有,远程无 → 上传(UploadOnly、Full 和 MirrorWcf)
|
||||
// 本地有,远程无 → 上传(UploadOnly、Full、MirrorWcf、AlbumUpload)
|
||||
(Some(l), None, _) => {
|
||||
if matches!(sync_mode, SyncMode::DownloadOnly) {
|
||||
if matches!(sync_mode, SyncMode::DownloadOnly | SyncMode::AlbumDownload) {
|
||||
continue;
|
||||
}
|
||||
if !l.is_dir && l.size == 0 {
|
||||
@@ -70,9 +75,9 @@ pub fn compute_diff(
|
||||
}
|
||||
}
|
||||
|
||||
// 远程有,本地无 → 下载(DownloadOnly、Full 和 MirrorWcf)
|
||||
// 远程有,本地无 → 下载(DownloadOnly、Full、MirrorWcf、AlbumDownload)
|
||||
(None, Some(r), _) => {
|
||||
if matches!(sync_mode, SyncMode::UploadOnly) {
|
||||
if matches!(sync_mode, SyncMode::UploadOnly | SyncMode::AlbumUpload) {
|
||||
continue;
|
||||
}
|
||||
if r.is_dir {
|
||||
@@ -125,8 +130,8 @@ pub fn compute_diff(
|
||||
}
|
||||
}
|
||||
|
||||
// 远程目录结构(UploadOnly、Full 和 MirrorWcf)
|
||||
if !matches!(sync_mode, SyncMode::DownloadOnly) {
|
||||
// 远程目录结构(UploadOnly、Full、MirrorWcf、AlbumUpload)
|
||||
if !matches!(sync_mode, SyncMode::DownloadOnly | SyncMode::AlbumDownload) {
|
||||
for (path, local) in &local_map {
|
||||
if local.is_dir && !remote_map.contains_key(path.as_str()) {
|
||||
plan.mkdirs_remote.push(path.clone());
|
||||
@@ -149,7 +154,7 @@ fn resolve_conflicts_by_mode(plan: &mut SyncPlan, sync_mode: &SyncMode) {
|
||||
let conflicts = std::mem::take(&mut plan.conflicts);
|
||||
for conflict in conflicts {
|
||||
match sync_mode {
|
||||
SyncMode::UploadOnly | SyncMode::MirrorWcf => {
|
||||
SyncMode::UploadOnly | SyncMode::MirrorWcf | SyncMode::AlbumUpload => {
|
||||
// 冲突一律覆盖上传(MirrorWcf 本地编辑优先)
|
||||
let action = SyncAction {
|
||||
relative_path: conflict.relative_path.clone(),
|
||||
@@ -159,7 +164,7 @@ fn resolve_conflicts_by_mode(plan: &mut SyncPlan, sync_mode: &SyncMode) {
|
||||
};
|
||||
plan.uploads.push(action);
|
||||
}
|
||||
SyncMode::DownloadOnly => {
|
||||
SyncMode::DownloadOnly | SyncMode::AlbumDownload => {
|
||||
// 冲突一律覆盖下载
|
||||
let action = SyncAction {
|
||||
relative_path: conflict.relative_path.clone(),
|
||||
|
||||
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 264517153;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1656309947;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -331,6 +331,41 @@ fn wire__crate__api__ffi__get_sync_config_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__ffi__get_sync_cum_stats_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "get_sync_cum_stats",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, crate::api::ffi_types::SyncErrorFfi>(
|
||||
(move || async move {
|
||||
let output_ok = crate::api::ffi::get_sync_cum_stats().await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__ffi__get_sync_status_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -604,11 +639,12 @@ fn wire__crate__api__ffi__reset_sync_impl(
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
let api_delete_local_files = <bool>::sse_decode(&mut deserializer);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, crate::api::ffi_types::SyncErrorFfi>(
|
||||
(move || async move {
|
||||
let output_ok = crate::api::ffi::reset_sync().await?;
|
||||
let output_ok = crate::api::ffi::reset_sync(api_delete_local_files).await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
@@ -981,11 +1017,15 @@ impl SseDecode for crate::api::ffi_types::CloudAlbumCheckResultFfi {
|
||||
let mut var_picturesExists = <bool>::sse_decode(deserializer);
|
||||
let mut var_dcimUri = <Option<String>>::sse_decode(deserializer);
|
||||
let mut var_picturesUri = <Option<String>>::sse_decode(deserializer);
|
||||
let mut var_cameraExists = <bool>::sse_decode(deserializer);
|
||||
let mut var_cameraUri = <Option<String>>::sse_decode(deserializer);
|
||||
return crate::api::ffi_types::CloudAlbumCheckResultFfi {
|
||||
dcim_exists: var_dcimExists,
|
||||
pictures_exists: var_picturesExists,
|
||||
dcim_uri: var_dcimUri,
|
||||
pictures_uri: var_picturesUri,
|
||||
camera_exists: var_cameraExists,
|
||||
camera_uri: var_cameraUri,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1098,6 +1138,32 @@ impl SseDecode for crate::api::ffi_types::SyncConfigFfi {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::ffi_types::SyncCumStatsFfi {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut var_uploaded = <u32>::sse_decode(deserializer);
|
||||
let mut var_downloaded = <u32>::sse_decode(deserializer);
|
||||
let mut var_renamed = <u32>::sse_decode(deserializer);
|
||||
let mut var_moved = <u32>::sse_decode(deserializer);
|
||||
let mut var_failed = <u32>::sse_decode(deserializer);
|
||||
let mut var_conflicts = <u32>::sse_decode(deserializer);
|
||||
let mut var_deletedLocal = <u32>::sse_decode(deserializer);
|
||||
let mut var_deletedRemote = <u32>::sse_decode(deserializer);
|
||||
let mut var_skipped = <u32>::sse_decode(deserializer);
|
||||
return crate::api::ffi_types::SyncCumStatsFfi {
|
||||
uploaded: var_uploaded,
|
||||
downloaded: var_downloaded,
|
||||
renamed: var_renamed,
|
||||
moved: var_moved,
|
||||
failed: var_failed,
|
||||
conflicts: var_conflicts,
|
||||
deleted_local: var_deletedLocal,
|
||||
deleted_remote: var_deletedRemote,
|
||||
skipped: var_skipped,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::ffi_types::SyncErrorFfi {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -1445,25 +1511,26 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
6 => wire__crate__api__ffi__get_active_worker_count_impl(port, ptr, rust_vec_len, data_len),
|
||||
7 => wire__crate__api__ffi__get_recent_tasks_impl(port, ptr, rust_vec_len, data_len),
|
||||
8 => wire__crate__api__ffi__get_sync_config_impl(port, ptr, rust_vec_len, data_len),
|
||||
9 => wire__crate__api__ffi__get_sync_status_impl(port, ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__ffi__get_task_detail_impl(port, ptr, rust_vec_len, data_len),
|
||||
11 => wire__crate__api__ffi__hydrate_file_impl(port, ptr, rust_vec_len, data_len),
|
||||
12 => wire__crate__api__ffi__init_sync_engine_impl(port, ptr, rust_vec_len, data_len),
|
||||
13 => wire__crate__api__ffi__pause_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
14 => wire__crate__api__ffi__query_task_items_impl(port, ptr, rust_vec_len, data_len),
|
||||
15 => {
|
||||
9 => wire__crate__api__ffi__get_sync_cum_stats_impl(port, ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__ffi__get_sync_status_impl(port, ptr, rust_vec_len, data_len),
|
||||
11 => wire__crate__api__ffi__get_task_detail_impl(port, ptr, rust_vec_len, data_len),
|
||||
12 => wire__crate__api__ffi__hydrate_file_impl(port, ptr, rust_vec_len, data_len),
|
||||
13 => wire__crate__api__ffi__init_sync_engine_impl(port, ptr, rust_vec_len, data_len),
|
||||
14 => wire__crate__api__ffi__pause_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
15 => wire__crate__api__ffi__query_task_items_impl(port, ptr, rust_vec_len, data_len),
|
||||
16 => {
|
||||
wire__crate__api__ffi__register_sync_event_sink_impl(port, ptr, rust_vec_len, data_len)
|
||||
}
|
||||
16 => wire__crate__api__ffi__reset_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
17 => wire__crate__api__ffi__resume_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
18 => wire__crate__api__ffi__set_sync_log_level_impl(port, ptr, rust_vec_len, data_len),
|
||||
19 => wire__crate__api__ffi__start_continuous_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__api__ffi__start_initial_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__api__ffi__stop_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__ffi__sync_album_to_cloud_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__api__ffi__sync_shutdown_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__ffi__update_sync_config_impl(port, ptr, rust_vec_len, data_len),
|
||||
25 => wire__crate__api__ffi__update_tokens_impl(port, ptr, rust_vec_len, data_len),
|
||||
17 => wire__crate__api__ffi__reset_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
18 => wire__crate__api__ffi__resume_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
19 => wire__crate__api__ffi__set_sync_log_level_impl(port, ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__api__ffi__start_continuous_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__api__ffi__start_initial_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__ffi__stop_sync_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__api__ffi__sync_album_to_cloud_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__ffi__sync_shutdown_impl(port, ptr, rust_vec_len, data_len),
|
||||
25 => wire__crate__api__ffi__update_sync_config_impl(port, ptr, rust_vec_len, data_len),
|
||||
26 => wire__crate__api__ffi__update_tokens_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -1490,6 +1557,8 @@ impl flutter_rust_bridge::IntoDart for crate::api::ffi_types::CloudAlbumCheckRes
|
||||
self.pictures_exists.into_into_dart().into_dart(),
|
||||
self.dcim_uri.into_into_dart().into_dart(),
|
||||
self.pictures_uri.into_into_dart().into_dart(),
|
||||
self.camera_exists.into_into_dart().into_dart(),
|
||||
self.camera_uri.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
@@ -1540,6 +1609,34 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::ffi_types::SyncConfigFfi>
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::ffi_types::SyncCumStatsFfi {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
self.uploaded.into_into_dart().into_dart(),
|
||||
self.downloaded.into_into_dart().into_dart(),
|
||||
self.renamed.into_into_dart().into_dart(),
|
||||
self.moved.into_into_dart().into_dart(),
|
||||
self.failed.into_into_dart().into_dart(),
|
||||
self.conflicts.into_into_dart().into_dart(),
|
||||
self.deleted_local.into_into_dart().into_dart(),
|
||||
self.deleted_remote.into_into_dart().into_dart(),
|
||||
self.skipped.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||
for crate::api::ffi_types::SyncCumStatsFfi
|
||||
{
|
||||
}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::api::ffi_types::SyncCumStatsFfi>
|
||||
for crate::api::ffi_types::SyncCumStatsFfi
|
||||
{
|
||||
fn into_into_dart(self) -> crate::api::ffi_types::SyncCumStatsFfi {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::ffi_types::SyncErrorFfi {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
match self {
|
||||
@@ -1887,6 +1984,8 @@ impl SseEncode for crate::api::ffi_types::CloudAlbumCheckResultFfi {
|
||||
<bool>::sse_encode(self.pictures_exists, serializer);
|
||||
<Option<String>>::sse_encode(self.dcim_uri, serializer);
|
||||
<Option<String>>::sse_encode(self.pictures_uri, serializer);
|
||||
<bool>::sse_encode(self.camera_exists, serializer);
|
||||
<Option<String>>::sse_encode(self.camera_uri, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1968,6 +2067,21 @@ impl SseEncode for crate::api::ffi_types::SyncConfigFfi {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::ffi_types::SyncCumStatsFfi {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<u32>::sse_encode(self.uploaded, serializer);
|
||||
<u32>::sse_encode(self.downloaded, serializer);
|
||||
<u32>::sse_encode(self.renamed, serializer);
|
||||
<u32>::sse_encode(self.moved, serializer);
|
||||
<u32>::sse_encode(self.failed, serializer);
|
||||
<u32>::sse_encode(self.conflicts, serializer);
|
||||
<u32>::sse_encode(self.deleted_local, serializer);
|
||||
<u32>::sse_encode(self.deleted_remote, serializer);
|
||||
<u32>::sse_encode(self.skipped, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::ffi_types::SyncErrorFfi {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
#[cfg(unix)]
|
||||
use std::collections::HashSet;
|
||||
use crate::errors::Result;
|
||||
use crate::models::LocalFileEntry;
|
||||
use crate::utils::quick_hash;
|
||||
#[cfg(unix)]
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// 需要跳过的文件/目录名前缀和名称
|
||||
pub const SKIP_NAMES: &[&str] = &[
|
||||
".DS_Store",
|
||||
"Thumbs.db",
|
||||
"desktop.ini",
|
||||
];
|
||||
pub const SKIP_NAMES: &[&str] = &[".DS_Store", "Thumbs.db", "desktop.ini"];
|
||||
|
||||
/// 需要跳过的文件扩展名(同步临时文件)
|
||||
pub const SKIP_EXTENSIONS: &[&str] = &[
|
||||
"sync_tmp",
|
||||
"sync_temp",
|
||||
];
|
||||
pub const SKIP_EXTENSIONS: &[&str] = &["sync_tmp", "sync_temp"];
|
||||
|
||||
pub struct FsScanner;
|
||||
|
||||
@@ -58,6 +51,15 @@ impl FsScanner {
|
||||
}
|
||||
};
|
||||
|
||||
let file_name = entry.file_name().to_string_lossy();
|
||||
let depth = entry.depth();
|
||||
tracing::trace!(
|
||||
"扫描: depth={}, is_dir={}, name={}",
|
||||
depth,
|
||||
entry.file_type().is_dir(),
|
||||
file_name
|
||||
);
|
||||
|
||||
// 符号链接处理
|
||||
if entry.path_is_symlink() && !follow_symlinks {
|
||||
continue;
|
||||
@@ -68,6 +70,10 @@ impl FsScanner {
|
||||
if SKIP_NAMES.iter().any(|s| file_name == *s) {
|
||||
continue;
|
||||
}
|
||||
// 跳过隐藏目录/文件(以 . 开头)
|
||||
if file_name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
if file_name.starts_with(".sync_") {
|
||||
continue;
|
||||
}
|
||||
@@ -100,7 +106,9 @@ impl FsScanner {
|
||||
}
|
||||
}
|
||||
|
||||
let relative_path = entry.path().strip_prefix(root)
|
||||
let relative_path = entry
|
||||
.path()
|
||||
.strip_prefix(root)
|
||||
.unwrap_or(entry.path())
|
||||
.to_path_buf();
|
||||
|
||||
@@ -120,7 +128,8 @@ impl FsScanner {
|
||||
});
|
||||
} else if metadata.is_file() {
|
||||
let size = metadata.len();
|
||||
let mtime_ms = metadata.modified()
|
||||
let mtime_ms = metadata
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_millis() as i64)
|
||||
@@ -144,13 +153,20 @@ impl FsScanner {
|
||||
}
|
||||
}
|
||||
|
||||
let dirs = entries.iter().filter(|e| e.is_dir).count();
|
||||
let files = entries.iter().filter(|e| !e.is_dir).count();
|
||||
tracing::debug!(
|
||||
"扫描完成: {} 个条目 ({} 目录, {} 文件)",
|
||||
entries.len(),
|
||||
dirs,
|
||||
files
|
||||
);
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据文件扩展名推断 MIME 类型
|
||||
pub fn guess_mime_type(path: &Path) -> Option<String> {
|
||||
mime_guess::from_path(path)
|
||||
.first()
|
||||
.map(|m| m.to_string())
|
||||
mime_guess::from_path(path).first().map(|m| m.to_string())
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ pub enum SyncMode {
|
||||
Full,
|
||||
UploadOnly,
|
||||
DownloadOnly,
|
||||
Album,
|
||||
AlbumUpload,
|
||||
AlbumDownload,
|
||||
MirrorWcf,
|
||||
}
|
||||
|
||||
@@ -241,8 +242,14 @@ pub enum LocalFileEvent {
|
||||
Created(Vec<PathBuf>),
|
||||
Modified(Vec<PathBuf>),
|
||||
Deleted(Vec<PathBuf>),
|
||||
Renamed { old_paths: Vec<PathBuf>, new_paths: Vec<PathBuf> },
|
||||
Moved { old_paths: Vec<PathBuf>, new_paths: Vec<PathBuf> },
|
||||
Renamed {
|
||||
old_paths: Vec<PathBuf>,
|
||||
new_paths: Vec<PathBuf>,
|
||||
},
|
||||
Moved {
|
||||
old_paths: Vec<PathBuf>,
|
||||
new_paths: Vec<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LocalFileEvent {
|
||||
@@ -262,9 +269,18 @@ impl LocalFileEvent {
|
||||
pub enum RemoteFileEvent {
|
||||
Created(RemoteFileEntry),
|
||||
Modified(RemoteFileEntry),
|
||||
Deleted { uri: String, name: String },
|
||||
Renamed { old_uri: String, new_entry: RemoteFileEntry },
|
||||
Moved { old_uri: String, new_entry: RemoteFileEntry },
|
||||
Deleted {
|
||||
uri: String,
|
||||
name: String,
|
||||
},
|
||||
Renamed {
|
||||
old_uri: String,
|
||||
new_entry: RemoteFileEntry,
|
||||
},
|
||||
Moved {
|
||||
old_uri: String,
|
||||
new_entry: RemoteFileEntry,
|
||||
},
|
||||
}
|
||||
|
||||
// ===== 平台回调事件 (Windows CFApi) =====
|
||||
@@ -439,6 +455,10 @@ pub enum WorkerTrigger {
|
||||
InitialSync,
|
||||
Continuous,
|
||||
Manual,
|
||||
/// WCF 按需水合
|
||||
Hydration,
|
||||
/// WCF 远程事件(占位符/删除/重命名/移动)
|
||||
WcfEvent,
|
||||
}
|
||||
|
||||
impl WorkerTrigger {
|
||||
@@ -447,6 +467,8 @@ impl WorkerTrigger {
|
||||
WorkerTrigger::InitialSync => "initial_sync",
|
||||
WorkerTrigger::Continuous => "continuous",
|
||||
WorkerTrigger::Manual => "manual",
|
||||
WorkerTrigger::Hydration => "hydration",
|
||||
WorkerTrigger::WcfEvent => "wcf_event",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -471,7 +493,6 @@ impl WorkerStatus {
|
||||
WorkerStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl std::str::FromStr for WorkerStatus {
|
||||
@@ -509,7 +530,6 @@ impl TaskItemStatus {
|
||||
TaskItemStatus::Skipped => "skipped",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl std::str::FromStr for TaskItemStatus {
|
||||
@@ -540,6 +560,8 @@ pub enum TaskActionType {
|
||||
MkdirLocal,
|
||||
ConflictResolve,
|
||||
CreatePlaceholder,
|
||||
/// WCF 按需水合(用户打开占位符文件时触发下载)
|
||||
Hydration,
|
||||
}
|
||||
|
||||
impl TaskActionType {
|
||||
@@ -555,9 +577,9 @@ impl TaskActionType {
|
||||
TaskActionType::MkdirLocal => "mkdir_local",
|
||||
TaskActionType::ConflictResolve => "conflict_resolve",
|
||||
TaskActionType::CreatePlaceholder => "create_placeholder",
|
||||
TaskActionType::Hydration => "hydration",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl std::str::FromStr for TaskActionType {
|
||||
@@ -575,6 +597,7 @@ impl std::str::FromStr for TaskActionType {
|
||||
"mkdir_local" => Ok(TaskActionType::MkdirLocal),
|
||||
"conflict_resolve" => Ok(TaskActionType::ConflictResolve),
|
||||
"create_placeholder" => Ok(TaskActionType::CreatePlaceholder),
|
||||
"hydration" => Ok(TaskActionType::Hydration),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
@@ -608,6 +631,20 @@ pub struct SyncTaskItem {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// 累积统计(从 DB 聚合,跨所有同步任务)
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SyncCumStats {
|
||||
pub uploaded: u32,
|
||||
pub downloaded: u32,
|
||||
pub renamed: u32,
|
||||
pub moved: u32,
|
||||
pub failed: u32,
|
||||
pub conflicts: u32,
|
||||
pub deleted_local: u32,
|
||||
pub deleted_remote: u32,
|
||||
pub skipped: u32,
|
||||
}
|
||||
|
||||
/// 任务项查询过滤器
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TaskItemFilter {
|
||||
@@ -627,4 +664,6 @@ pub struct CloudAlbumCheckResult {
|
||||
pub pictures_exists: bool,
|
||||
pub dcim_uri: Option<String>,
|
||||
pub pictures_uri: Option<String>,
|
||||
pub camera_exists: bool,
|
||||
pub camera_uri: Option<String>,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,16 @@
|
||||
/// 平台适配器 trait — 各平台(Windows WCF / Linux / Android)实现此接口
|
||||
#[cfg(feature = "windows-cfapi")]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "windows-cfapi")]
|
||||
#[cfg(any(feature = "windows-cfapi", feature = "linux-fuse"))]
|
||||
use crate::errors::Result;
|
||||
#[cfg(feature = "windows-cfapi")]
|
||||
#[cfg(any(feature = "windows-cfapi", feature = "linux-fuse"))]
|
||||
use crate::models::{LocalFileEvent, RemoteFileEntry};
|
||||
#[cfg(feature = "windows-cfapi")]
|
||||
/// 平台适配器 trait — 各平台(Windows WCF / Linux FUSE / Android)实现此接口
|
||||
#[cfg(any(feature = "windows-cfapi", feature = "linux-fuse"))]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(any(feature = "windows-cfapi", feature = "linux-fuse"))]
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "windows-cfapi")]
|
||||
#[cfg(any(feature = "windows-cfapi", feature = "linux-fuse"))]
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[cfg(feature = "windows-cfapi")]
|
||||
#[cfg(any(feature = "windows-cfapi", feature = "linux-fuse"))]
|
||||
#[async_trait]
|
||||
pub trait PlatformAdapter: Send + Sync {
|
||||
/// 初始同步后的平台初始化
|
||||
@@ -34,3 +34,6 @@ pub trait PlatformAdapter: Send + Sync {
|
||||
|
||||
#[cfg(feature = "windows-cfapi")]
|
||||
pub mod wcf;
|
||||
|
||||
#[cfg(feature = "linux-fuse")]
|
||||
pub mod fuse;
|
||||
|
||||
+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)?;
|
||||
|
||||
@@ -7,29 +7,54 @@ use super::SyncEngine;
|
||||
impl SyncEngine {
|
||||
pub async fn sync_album(&self, album_paths: Vec<String>, remote_dcim_uri: &str) -> Result<()> {
|
||||
let synced = self.db.get_album_sync_records().await?;
|
||||
let new_photos: Vec<_> = album_paths.iter().filter(|p| !synced.contains_key(*p)).collect();
|
||||
let new_photos: Vec<_> = album_paths
|
||||
.iter()
|
||||
.filter(|p| !synced.contains_key(*p))
|
||||
.collect();
|
||||
let total = new_photos.len();
|
||||
if total == 0 { return Ok(()); }
|
||||
if total == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (i, photo_path) in new_photos.iter().enumerate() {
|
||||
let local_path = Path::new(photo_path);
|
||||
let file_name = local_path.file_name()
|
||||
let file_name = local_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| format!("photo_{}", i));
|
||||
|
||||
match tokio::fs::metadata(photo_path).await {
|
||||
Ok(metadata) => {
|
||||
let file_size = metadata.len();
|
||||
match self.api.create_upload_session(remote_dcim_uri, file_size, false, None, None, None).await {
|
||||
match self
|
||||
.api
|
||||
.create_upload_session(remote_dcim_uri, file_size, false, None, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(session) => {
|
||||
match crate::uploader::upload_file_chunked(&self.api, local_path, &session, "album").await {
|
||||
match crate::uploader::upload_file_chunked(
|
||||
&self.api, local_path, &session, "album",
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let remote_uri = format!("{}/{}", remote_dcim_uri, file_name);
|
||||
let hash = crate::utils::quick_hash(local_path, file_size).await.unwrap_or_default();
|
||||
if let Err(e) = self.db.add_album_sync_record(photo_path, &remote_uri, &hash).await {
|
||||
let hash = crate::utils::quick_hash(local_path, file_size)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if let Err(e) = self
|
||||
.db
|
||||
.add_album_sync_record(photo_path, &remote_uri, &hash)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("记录同步状态失败: {}", e);
|
||||
}
|
||||
tracing::info!("照片上传完成 ({}/{}): {}", i + 1, total, file_name);
|
||||
tracing::info!(
|
||||
"照片上传完成 ({}/{}): {}",
|
||||
i + 1,
|
||||
total,
|
||||
file_name
|
||||
);
|
||||
}
|
||||
Err(e) => tracing::error!("上传照片失败 {}: {}", file_name, e),
|
||||
}
|
||||
@@ -47,17 +72,47 @@ impl SyncEngine {
|
||||
let files = self.api.list_files_page(base_uri, 0, 200, None).await?;
|
||||
let dcim_exists = files.files.iter().any(|f| f.name == "DCIM" && f.is_dir);
|
||||
let pictures_exists = files.files.iter().any(|f| f.name == "Pictures" && f.is_dir);
|
||||
let dcim_uri = if dcim_exists {
|
||||
Some(format!("{}/DCIM", base_uri))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 检查 DCIM/Camera 子目录
|
||||
let camera_exists = if let Some(ref dcim_uri) = dcim_uri {
|
||||
let dcim_files = self.api.list_files_page(dcim_uri, 0, 200, None).await?;
|
||||
dcim_files
|
||||
.files
|
||||
.iter()
|
||||
.any(|f| f.name == "Camera" && f.is_dir)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(CloudAlbumCheckResult {
|
||||
dcim_exists,
|
||||
pictures_exists,
|
||||
dcim_uri: if dcim_exists { Some(format!("{}/DCIM", base_uri)) } else { None },
|
||||
pictures_uri: if pictures_exists { Some(format!("{}/Pictures", base_uri)) } else { None },
|
||||
dcim_uri,
|
||||
pictures_uri: if pictures_exists {
|
||||
Some(format!("{}/Pictures", base_uri))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
camera_exists,
|
||||
camera_uri: if camera_exists {
|
||||
Some(format!("{}/DCIM/Camera", base_uri))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_album_dirs(&self, base_uri: &str) -> Result<()> {
|
||||
self.api.create_directory(base_uri, "DCIM").await?;
|
||||
self.api.create_directory(base_uri, "Pictures").await?;
|
||||
// 创建 DCIM/Camera 子目录
|
||||
let dcim_uri = format!("{}/DCIM", base_uri);
|
||||
self.api.create_directory(&dcim_uri, "Camera").await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,27 +7,37 @@ use super::SyncEngine;
|
||||
impl SyncEngine {
|
||||
/// 持续同步:双事件源驱动 (SSE + 本地文件监听),按 sync_mode 选择事件源
|
||||
pub async fn run_continuous(&self) -> Result<()> {
|
||||
let event_handler = EventHandler::new(
|
||||
self.api.clone(),
|
||||
self.api.client_id().to_string(),
|
||||
);
|
||||
let event_handler = EventHandler::new(self.api.clone(), self.api.client_id().to_string());
|
||||
|
||||
let (local_root, remote_root, sync_mode) = {
|
||||
let config = self.config.read().await;
|
||||
(config.local_root.clone(), config.remote_root.clone(), config.sync_mode.clone())
|
||||
(
|
||||
config.local_root.clone(),
|
||||
config.remote_root.clone(),
|
||||
config.sync_mode.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
// 仅 DownloadOnly、Full 和 MirrorWcf 订阅 SSE
|
||||
let mut remote_rx = if matches!(sync_mode, SyncMode::DownloadOnly | SyncMode::Full | SyncMode::MirrorWcf) {
|
||||
// 仅 DownloadOnly、Full、MirrorWcf、AlbumDownload 订阅 SSE
|
||||
let mut remote_rx = if matches!(
|
||||
sync_mode,
|
||||
SyncMode::DownloadOnly | SyncMode::Full | SyncMode::MirrorWcf | SyncMode::AlbumDownload
|
||||
) {
|
||||
Some(event_handler.subscribe_sse(&remote_root).await?)
|
||||
} else {
|
||||
tracing::info!("仅上传模式: 不订阅 SSE 远程事件");
|
||||
None
|
||||
};
|
||||
|
||||
// 仅 UploadOnly、Full 和 MirrorWcf 启动本地文件监听
|
||||
let mut local_rx = if matches!(sync_mode, SyncMode::UploadOnly | SyncMode::Full | SyncMode::MirrorWcf) {
|
||||
Some(spawn_local_watcher(&local_root, self.shutdown_token.lock().unwrap().clone()))
|
||||
// 仅 UploadOnly、Full、MirrorWcf、AlbumUpload 启动本地文件监听
|
||||
let mut local_rx = if matches!(
|
||||
sync_mode,
|
||||
SyncMode::UploadOnly | SyncMode::Full | SyncMode::MirrorWcf | SyncMode::AlbumUpload
|
||||
) {
|
||||
Some(spawn_local_watcher(
|
||||
&local_root,
|
||||
self.shutdown_token.lock().unwrap().clone(),
|
||||
))
|
||||
} else {
|
||||
tracing::info!("仅下载模式: 不启动本地文件监听");
|
||||
None
|
||||
@@ -46,9 +56,21 @@ impl SyncEngine {
|
||||
#[cfg(not(feature = "windows-cfapi"))]
|
||||
let _wcf_fetch_rx: Option<()> = None;
|
||||
|
||||
let mut debounce = crate::event_handler::EventDebouncer::new(
|
||||
std::time::Duration::from_millis(500),
|
||||
);
|
||||
// MirrorFUSE: 取走 FUSE 请求接收端
|
||||
#[cfg(feature = "linux-fuse")]
|
||||
let mut fuse_request_rx = if matches!(sync_mode, SyncMode::MirrorWcf) {
|
||||
self.fuse_request_rx
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut rx| rx.take())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(not(feature = "linux-fuse"))]
|
||||
let _fuse_request_rx: Option<()> = None;
|
||||
|
||||
let mut debounce =
|
||||
crate::event_handler::EventDebouncer::new(std::time::Duration::from_millis(500));
|
||||
|
||||
let shutdown_token = self.shutdown_token.lock().unwrap().clone();
|
||||
loop {
|
||||
@@ -109,6 +131,29 @@ impl SyncEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// FUSE 请求(仅 MirrorFUSE)
|
||||
request = async {
|
||||
#[cfg(feature = "linux-fuse")]
|
||||
{
|
||||
match &mut fuse_request_rx {
|
||||
Some(rx) => rx.recv().await,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "linux-fuse"))]
|
||||
{
|
||||
let _: Option<()> = std::future::pending().await;
|
||||
None::<()>
|
||||
}
|
||||
} => {
|
||||
if let Some(req) = request {
|
||||
#[cfg(feature = "linux-fuse")]
|
||||
self.handle_fuse_request(req, &local_root).await;
|
||||
#[cfg(not(feature = "linux-fuse"))]
|
||||
let _: () = req;
|
||||
}
|
||||
}
|
||||
|
||||
// 定期心跳
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {
|
||||
tracing::trace!("持续同步心跳");
|
||||
@@ -130,9 +175,9 @@ fn spawn_local_watcher(
|
||||
let watch_root = watch_root.to_path_buf();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
use notify_debouncer_full::notify::{RecursiveMode, EventKind};
|
||||
use notify_debouncer_full::notify::event::{ModifyKind, RenameMode};
|
||||
use notify_debouncer_full::new_debouncer;
|
||||
use notify_debouncer_full::notify::event::{ModifyKind, RenameMode};
|
||||
use notify_debouncer_full::notify::{EventKind, RecursiveMode};
|
||||
|
||||
let tx = local_tx.clone();
|
||||
let shutdown = shutdown_token.clone();
|
||||
@@ -140,51 +185,54 @@ fn spawn_local_watcher(
|
||||
let mut debouncer = match new_debouncer(
|
||||
std::time::Duration::from_millis(500),
|
||||
None,
|
||||
move |result: notify_debouncer_full::DebounceEventResult| {
|
||||
match result {
|
||||
Ok(events) => {
|
||||
for event in events {
|
||||
if shutdown.is_cancelled() { return; }
|
||||
let kind = event.kind;
|
||||
let paths = &event.paths;
|
||||
move |result: notify_debouncer_full::DebounceEventResult| match result {
|
||||
Ok(events) => {
|
||||
for event in events {
|
||||
if shutdown.is_cancelled() {
|
||||
return;
|
||||
}
|
||||
let kind = event.kind;
|
||||
let paths = &event.paths;
|
||||
|
||||
let filtered: Vec<_> = paths.iter()
|
||||
.filter(|p| !p.extension().map(|e| e == "sync_tmp").unwrap_or(false))
|
||||
.cloned()
|
||||
.collect();
|
||||
if filtered.is_empty() { continue; }
|
||||
let filtered: Vec<_> = paths
|
||||
.iter()
|
||||
.filter(|p| !p.extension().map(|e| e == "sync_tmp").unwrap_or(false))
|
||||
.cloned()
|
||||
.collect();
|
||||
if filtered.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match kind {
|
||||
EventKind::Create(_) => {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Created(filtered));
|
||||
}
|
||||
EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {}
|
||||
EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {}
|
||||
EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
|
||||
if filtered.len() == 2 {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Renamed {
|
||||
old_paths: vec![filtered[0].clone()],
|
||||
new_paths: vec![filtered[1].clone()],
|
||||
});
|
||||
}
|
||||
}
|
||||
EventKind::Modify(ModifyKind::Name(RenameMode::Other)) => {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Modified(filtered));
|
||||
}
|
||||
EventKind::Modify(_) => {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Modified(filtered));
|
||||
}
|
||||
EventKind::Remove(_) => {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Deleted(filtered));
|
||||
}
|
||||
_ => {}
|
||||
match kind {
|
||||
EventKind::Create(_) => {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Created(filtered));
|
||||
}
|
||||
EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {}
|
||||
EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {}
|
||||
EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
|
||||
if filtered.len() == 2 {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Renamed {
|
||||
old_paths: vec![filtered[0].clone()],
|
||||
new_paths: vec![filtered[1].clone()],
|
||||
});
|
||||
}
|
||||
}
|
||||
EventKind::Modify(ModifyKind::Name(RenameMode::Other)) => {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Modified(filtered));
|
||||
}
|
||||
EventKind::Modify(_) => {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Modified(filtered));
|
||||
}
|
||||
EventKind::Remove(_) => {
|
||||
let _ = tx.blocking_send(LocalFileEvent::Deleted(filtered));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err(errors) => {
|
||||
for e in errors {
|
||||
tracing::warn!("文件监听去抖错误: {}", e);
|
||||
}
|
||||
}
|
||||
Err(errors) => {
|
||||
for e in errors {
|
||||
tracing::warn!("文件监听去抖错误: {}", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
//! FUSE (Linux) 相关的 SyncEngine 方法
|
||||
//! 仅在 linux-fuse feature 启用时编译
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
use super::SyncEngine;
|
||||
|
||||
#[cfg(feature = "linux-fuse")]
|
||||
impl SyncEngine {
|
||||
/// 处理 FUSE 请求(统一分发)
|
||||
pub(crate) async fn handle_fuse_request(
|
||||
&self,
|
||||
request: crate::platform::fuse::FuseRequest,
|
||||
local_root: &std::path::Path,
|
||||
) {
|
||||
use crate::platform::fuse::FuseRequest;
|
||||
match request {
|
||||
FuseRequest::Read {
|
||||
inode,
|
||||
remote_uri,
|
||||
offset,
|
||||
length,
|
||||
reply_tx,
|
||||
} => {
|
||||
self.handle_fuse_read(inode, &remote_uri, offset, length, reply_tx, local_root)
|
||||
.await;
|
||||
}
|
||||
FuseRequest::Upload {
|
||||
inode,
|
||||
parent_ino,
|
||||
name,
|
||||
relative_path,
|
||||
data,
|
||||
tmp_path,
|
||||
mtime_ms,
|
||||
overwrite,
|
||||
reply_tx,
|
||||
} => {
|
||||
self.handle_fuse_upload(
|
||||
inode,
|
||||
parent_ino,
|
||||
&name,
|
||||
&relative_path,
|
||||
data,
|
||||
tmp_path.as_deref(),
|
||||
mtime_ms,
|
||||
overwrite,
|
||||
reply_tx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
FuseRequest::Mkdir {
|
||||
inode,
|
||||
parent_ino,
|
||||
name,
|
||||
relative_path,
|
||||
reply_tx,
|
||||
} => {
|
||||
self.handle_fuse_mkdir(inode, parent_ino, &name, &relative_path, reply_tx)
|
||||
.await;
|
||||
}
|
||||
FuseRequest::Unlink {
|
||||
inode,
|
||||
name,
|
||||
is_dir,
|
||||
remote_uri,
|
||||
relative_path,
|
||||
reply_tx,
|
||||
} => {
|
||||
self.handle_fuse_unlink(
|
||||
inode,
|
||||
&name,
|
||||
is_dir,
|
||||
&remote_uri,
|
||||
&relative_path,
|
||||
reply_tx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
FuseRequest::Rename {
|
||||
inode,
|
||||
old_name,
|
||||
old_relative_path,
|
||||
old_remote_uri,
|
||||
new_parent_ino,
|
||||
new_name,
|
||||
new_relative_path,
|
||||
reply_tx,
|
||||
} => {
|
||||
self.handle_fuse_rename(
|
||||
inode,
|
||||
&old_name,
|
||||
&old_relative_path,
|
||||
&old_remote_uri,
|
||||
new_parent_ino,
|
||||
&new_name,
|
||||
&new_relative_path,
|
||||
reply_tx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MirrorFUSE: 处理 FUSE read 水合请求(按需下载)
|
||||
pub(crate) async fn handle_fuse_read(
|
||||
&self,
|
||||
inode: u64,
|
||||
remote_uri: &str,
|
||||
offset: i64,
|
||||
length: i64,
|
||||
reply_tx: tokio::sync::oneshot::Sender<Result<Vec<u8>, String>>,
|
||||
_local_root: &std::path::Path,
|
||||
) {
|
||||
tracing::debug!(
|
||||
"FUSE 水合请求: ino={}, uri={}, offset={}, length={}",
|
||||
inode,
|
||||
remote_uri,
|
||||
offset,
|
||||
length
|
||||
);
|
||||
|
||||
let root_id = match &self.sync_root_id {
|
||||
Some(id) => id.clone(),
|
||||
None => {
|
||||
let _ = reply_tx.send(Err("sync_root_id 为空".to_string()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let remote_uri_owned = remote_uri.to_string();
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
self.hydration_cache
|
||||
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < 300);
|
||||
|
||||
let data = if let Some(cached) = self.hydration_cache.get(&remote_uri_owned) {
|
||||
tracing::debug!("FUSE 水合缓存命中: {}", remote_uri_owned);
|
||||
cached.0.clone()
|
||||
} else {
|
||||
tracing::info!("FUSE 水合下载: {}", remote_uri_owned);
|
||||
let config = self.snapshot_worker_config().await;
|
||||
|
||||
let download_result = async {
|
||||
let urls = self.api.get_download_url(&[&remote_uri_owned]).await;
|
||||
let urls = match urls {
|
||||
Ok(u) => u,
|
||||
Err(crate::errors::SyncError::Auth(_)) => {
|
||||
tracing::info!("FUSE 水合: token 过期,尝试刷新后重试");
|
||||
self.api.refresh_access_token().await?;
|
||||
self.api.get_download_url(&[&remote_uri_owned]).await?
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let download_url = urls.into_iter().next().ok_or_else(|| {
|
||||
crate::errors::SyncError::Network("获取下载 URL 返回空列表".into())
|
||||
})?;
|
||||
|
||||
let data = crate::downloader::download_to_buffer(
|
||||
&self.api,
|
||||
&download_url,
|
||||
config.bandwidth_limit,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok::<Vec<u8>, crate::errors::SyncError>(data)
|
||||
}
|
||||
.await;
|
||||
|
||||
match download_result {
|
||||
Ok(data) => {
|
||||
self.hydration_cache.insert(
|
||||
remote_uri_owned.clone(),
|
||||
(data.clone(), std::time::Instant::now()),
|
||||
);
|
||||
data
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("FUSE 水合下载失败: {}: {}", remote_uri_owned, e);
|
||||
let _ = reply_tx.send(Err(format!("下载失败: {}", e)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(Some(mapping)) = self
|
||||
.db
|
||||
.find_mapping_by_remote_uri(&root_id, &remote_uri_owned)
|
||||
.await
|
||||
{
|
||||
let _ = self
|
||||
.db
|
||||
.upsert_file_mapping(&FileMapping {
|
||||
id: mapping.id,
|
||||
sync_root_id: mapping.sync_root_id,
|
||||
local_path: mapping.local_path.clone(),
|
||||
remote_uri: mapping.remote_uri.clone(),
|
||||
remote_file_id: mapping.remote_file_id.clone(),
|
||||
local_hash: None,
|
||||
remote_hash: mapping.remote_hash.clone(),
|
||||
local_mtime: mapping.local_mtime,
|
||||
remote_mtime: mapping.remote_mtime,
|
||||
local_size: None,
|
||||
remote_size: Some(data.len() as u64),
|
||||
sync_status: SyncFileStatus::Synced,
|
||||
is_placeholder: false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
let _ = reply_tx.send(Ok(data));
|
||||
}
|
||||
|
||||
/// FUSE 上传:将写入的文件上传到云端
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_fuse_upload(
|
||||
&self,
|
||||
inode: u64,
|
||||
_parent_ino: u64,
|
||||
name: &str,
|
||||
relative_path: &str,
|
||||
data: Vec<u8>,
|
||||
tmp_path: Option<&str>,
|
||||
_mtime_ms: i64,
|
||||
overwrite: bool,
|
||||
reply_tx: tokio::sync::oneshot::Sender<Result<crate::platform::fuse::UploadResult, String>>,
|
||||
) {
|
||||
let root_id = match &self.sync_root_id {
|
||||
Some(id) => id.clone(),
|
||||
None => {
|
||||
let _ = reply_tx.send(Err("sync_root_id 为空".to_string()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let config = self.snapshot_worker_config().await;
|
||||
let remote_root = &config.remote_root;
|
||||
let file_uri = format!("{}/{}", remote_root, relative_path);
|
||||
|
||||
tracing::info!(
|
||||
"FUSE 上传: {} ({}bytes, overwrite={})",
|
||||
relative_path,
|
||||
data.len(),
|
||||
overwrite
|
||||
);
|
||||
|
||||
// 确保远程父目录存在
|
||||
if let Some(parent) = std::path::PathBuf::from(relative_path).parent() {
|
||||
let parent_str = parent.to_string_lossy().to_string();
|
||||
if !parent_str.is_empty() {
|
||||
if let Err(e) = crate::uploader::ensure_remote_dirs(
|
||||
"fuse",
|
||||
remote_root,
|
||||
&parent_str,
|
||||
&self.api,
|
||||
&self.ensured_dirs,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("FUSE 上传: 确保远程父目录失败 {}: {}", parent_str, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 读取文件数据
|
||||
let file_data = if !data.is_empty() {
|
||||
data
|
||||
} else if let Some(tmp) = tmp_path {
|
||||
match tokio::fs::read(tmp).await {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
let _ = reply_tx.send(Err(format!("读取临时文件失败: {}", e)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let file_size = file_data.len() as u64;
|
||||
|
||||
// 创建上传会话
|
||||
let session = match crate::uploader::retry_upload_session(
|
||||
"fuse", &file_uri, file_size, 3, overwrite, None, None, None, &self.api,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
let _ = reply_tx.send(Err(format!("创建上传会话失败: {}", e)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let chunk_size = session.chunk_size as usize;
|
||||
|
||||
// 逐块上传
|
||||
let mut offset = 0usize;
|
||||
let mut index: u32 = 0;
|
||||
while offset < file_data.len() {
|
||||
let end = (offset + chunk_size).min(file_data.len());
|
||||
let chunk = &file_data[offset..end];
|
||||
let mut chunk_retries = 0u32;
|
||||
loop {
|
||||
match self
|
||||
.api
|
||||
.upload_chunk(&session, index, chunk, file_size, "fuse")
|
||||
.await
|
||||
{
|
||||
Ok(_) => break,
|
||||
Err(e) if chunk_retries < 3 => {
|
||||
chunk_retries += 1;
|
||||
tracing::warn!("FUSE 上传重试 ({}/{}): {}", chunk_retries, 3, e);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = reply_tx.send(Err(format!("上传分片失败: {}", e)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
offset = end;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
// 远程存储策略回调
|
||||
if session.is_remote_storage() {
|
||||
if let Err(e) = self.api.callback_upload_complete(&session, "fuse").await {
|
||||
tracing::warn!("FUSE 上传完成回调失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取远程文件信息
|
||||
let (remote_file_id, remote_hash) = match self.api.get_file_info(&file_uri).await {
|
||||
Ok(info) => (info.file_id.clone(), info.hash.clone()),
|
||||
Err(e) => {
|
||||
tracing::warn!("FUSE 上传后获取文件信息失败: {}", e);
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
|
||||
// 更新 DB mapping
|
||||
let _ = self
|
||||
.db
|
||||
.upsert_file_mapping(&FileMapping {
|
||||
id: 0,
|
||||
sync_root_id: root_id,
|
||||
local_path: std::path::PathBuf::from(relative_path),
|
||||
remote_uri: file_uri.clone(),
|
||||
remote_file_id,
|
||||
local_hash: None,
|
||||
remote_hash: remote_hash.clone(),
|
||||
local_mtime: Some(_mtime_ms),
|
||||
remote_mtime: Some(_mtime_ms),
|
||||
local_size: Some(file_size),
|
||||
remote_size: Some(file_size),
|
||||
sync_status: SyncFileStatus::Synced,
|
||||
is_placeholder: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
// 抑制 SSE 回弹
|
||||
self.suppress_paths
|
||||
.insert(relative_path.to_string(), std::time::Instant::now());
|
||||
|
||||
tracing::info!(
|
||||
"FUSE 上传完成: {} → {} ({}bytes)",
|
||||
name,
|
||||
file_uri,
|
||||
file_size
|
||||
);
|
||||
|
||||
let _ = reply_tx.send(Ok(crate::platform::fuse::UploadResult {
|
||||
remote_uri: file_uri,
|
||||
remote_hash,
|
||||
size: file_size,
|
||||
}));
|
||||
|
||||
let _ = inode; // used for logging
|
||||
}
|
||||
|
||||
/// FUSE 创建远程目录
|
||||
async fn handle_fuse_mkdir(
|
||||
&self,
|
||||
_inode: u64,
|
||||
_parent_ino: u64,
|
||||
name: &str,
|
||||
relative_path: &str,
|
||||
reply_tx: tokio::sync::oneshot::Sender<Result<(), String>>,
|
||||
) {
|
||||
let root_id = match &self.sync_root_id {
|
||||
Some(id) => id.clone(),
|
||||
None => {
|
||||
let _ = reply_tx.send(Err("sync_root_id 为空".to_string()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let config = self.snapshot_worker_config().await;
|
||||
let parent_rel = std::path::PathBuf::from(relative_path)
|
||||
.parent()
|
||||
.map(|p| crate::utils::normalize_path(&p.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
let parent_uri = if parent_rel.is_empty() {
|
||||
config.remote_root.clone()
|
||||
} else {
|
||||
format!("{}/{}", config.remote_root, parent_rel)
|
||||
};
|
||||
|
||||
tracing::info!("FUSE 创建目录: {} (parent_uri={})", name, parent_uri);
|
||||
|
||||
match self.api.create_directory(&parent_uri, name).await {
|
||||
Ok(remote_entry) => {
|
||||
let remote_uri = remote_entry.uri.clone();
|
||||
|
||||
// 更新 InodeStore 中的 remote_uri(在 await 之前释放锁)
|
||||
{
|
||||
let adapter = match self.fuse_adapter.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
let _ = reply_tx.send(Err(format!("锁失败: {}", e)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(ref fuse) = *adapter {
|
||||
fuse.inode_store().update_remote_uri(_inode, &remote_uri);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 DB mapping
|
||||
let _ = self
|
||||
.db
|
||||
.upsert_file_mapping(&FileMapping {
|
||||
id: 0,
|
||||
sync_root_id: root_id,
|
||||
local_path: std::path::PathBuf::from(relative_path),
|
||||
remote_uri: remote_uri.clone(),
|
||||
remote_file_id: remote_entry.file_id.clone(),
|
||||
local_hash: None,
|
||||
remote_hash: remote_entry.hash.clone(),
|
||||
local_mtime: None,
|
||||
remote_mtime: Some(remote_entry.mtime_ms),
|
||||
local_size: None,
|
||||
remote_size: Some(0),
|
||||
sync_status: SyncFileStatus::Synced,
|
||||
is_placeholder: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
// 抑制 SSE 回弹
|
||||
self.suppress_paths
|
||||
.insert(relative_path.to_string(), std::time::Instant::now());
|
||||
|
||||
tracing::info!("FUSE 目录创建成功: {} → {}", name, remote_uri);
|
||||
let _ = reply_tx.send(Ok(()));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("FUSE 创建目录失败: {}: {}", name, e);
|
||||
let _ = reply_tx.send(Err(format!("创建目录失败: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FUSE 删除远程文件/目录
|
||||
async fn handle_fuse_unlink(
|
||||
&self,
|
||||
_inode: u64,
|
||||
name: &str,
|
||||
_is_dir: bool,
|
||||
remote_uri: &str,
|
||||
relative_path: &str,
|
||||
reply_tx: tokio::sync::oneshot::Sender<Result<(), String>>,
|
||||
) {
|
||||
let root_id = match &self.sync_root_id {
|
||||
Some(id) => id.clone(),
|
||||
None => {
|
||||
let _ = reply_tx.send(Err("sync_root_id 为空".to_string()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::info!("FUSE 删除: {} ({})", name, remote_uri);
|
||||
|
||||
match self.api.delete_files(&[remote_uri]).await {
|
||||
Ok(()) => {
|
||||
// 删除 DB mapping
|
||||
let _ = self
|
||||
.db
|
||||
.delete_mapping_by_remote_uri(&root_id, remote_uri)
|
||||
.await;
|
||||
|
||||
// 抑制 SSE 回弹
|
||||
self.suppress_paths
|
||||
.insert(relative_path.to_string(), std::time::Instant::now());
|
||||
|
||||
tracing::info!("FUSE 删除成功: {}", name);
|
||||
let _ = reply_tx.send(Ok(()));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("FUSE 删除失败: {}: {}", name, e);
|
||||
let _ = reply_tx.send(Err(format!("删除失败: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FUSE 重命名/移动
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_fuse_rename(
|
||||
&self,
|
||||
_inode: u64,
|
||||
old_name: &str,
|
||||
old_relative_path: &str,
|
||||
old_remote_uri: &str,
|
||||
new_parent_ino: u64,
|
||||
new_name: &str,
|
||||
new_relative_path: &str,
|
||||
reply_tx: tokio::sync::oneshot::Sender<Result<(), String>>,
|
||||
) {
|
||||
let root_id = match &self.sync_root_id {
|
||||
Some(id) => id.clone(),
|
||||
None => {
|
||||
let _ = reply_tx.send(Err("sync_root_id 为空".to_string()));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let config = self.snapshot_worker_config().await;
|
||||
let _ = (new_parent_ino, new_name);
|
||||
|
||||
// 判断是同目录重命名还是跨目录移动
|
||||
let old_parent_rel = std::path::PathBuf::from(old_relative_path)
|
||||
.parent()
|
||||
.map(|p| crate::utils::normalize_path(&p.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
let new_parent_rel = std::path::PathBuf::from(new_relative_path)
|
||||
.parent()
|
||||
.map(|p| crate::utils::normalize_path(&p.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
|
||||
let result = if old_parent_rel == new_parent_rel {
|
||||
// 同目录重命名
|
||||
tracing::info!("FUSE 重命名: {} → {}", old_name, new_name);
|
||||
self.api.rename_file(old_remote_uri, new_name).await
|
||||
} else {
|
||||
// 跨目录移动
|
||||
let dst_uri = if new_parent_rel.is_empty() {
|
||||
config.remote_root.clone()
|
||||
} else {
|
||||
format!("{}/{}", config.remote_root, new_parent_rel)
|
||||
};
|
||||
tracing::info!("FUSE 移动: {} → {}", old_remote_uri, dst_uri);
|
||||
self.api
|
||||
.move_files(&[old_remote_uri], &dst_uri, false)
|
||||
.await
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let new_remote_uri = format!("{}/{}", config.remote_root, new_relative_path);
|
||||
|
||||
// 更新 InodeStore 中的 remote_uri(在 await 之前释放锁)
|
||||
{
|
||||
let adapter = match self.fuse_adapter.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
let _ = reply_tx.send(Err(format!("锁失败: {}", e)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(ref fuse) = *adapter {
|
||||
fuse.inode_store()
|
||||
.update_remote_uri(_inode, &new_remote_uri);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 DB mapping
|
||||
let _ = self
|
||||
.db
|
||||
.update_mapping_remote_uri(&root_id, old_remote_uri, &new_remote_uri)
|
||||
.await;
|
||||
|
||||
// 抑制 SSE 回弹
|
||||
self.suppress_paths
|
||||
.insert(old_relative_path.to_string(), std::time::Instant::now());
|
||||
self.suppress_paths
|
||||
.insert(new_relative_path.to_string(), std::time::Instant::now());
|
||||
|
||||
tracing::info!(
|
||||
"FUSE 重命名/移动成功: {} → {}",
|
||||
old_relative_path,
|
||||
new_relative_path
|
||||
);
|
||||
let _ = reply_tx.send(Ok(()));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("FUSE 重命名/移动失败: {}: {}", old_name, e);
|
||||
let _ = reply_tx.send(Err(format!("重命名/移动失败: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MirrorFUSE: 为远程文件注册 FUSE inode(持续同步时远程新建/修改文件调用)
|
||||
pub(crate) async fn _create_placeholder_for_remote(
|
||||
&self,
|
||||
relative: &str,
|
||||
remote: &RemoteFileEntry,
|
||||
_local_root: &std::path::Path,
|
||||
_root_id: &str,
|
||||
) {
|
||||
let adapter = match self.fuse_adapter.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(e) => {
|
||||
tracing::error!("FUSE adapter lock 失败: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(ref fuse) = *adapter {
|
||||
let parent_rel = std::path::PathBuf::from(relative)
|
||||
.parent()
|
||||
.map(|p| crate::utils::normalize_path(&p.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
|
||||
let name = std::path::PathBuf::from(relative)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
fuse.create_placeholder_for_remote(
|
||||
&parent_rel,
|
||||
&name,
|
||||
relative,
|
||||
remote.is_dir,
|
||||
remote.size,
|
||||
&remote.uri,
|
||||
remote.hash.as_deref(),
|
||||
remote.mtime_ms,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// FUSE 清理(卸载挂载点)
|
||||
pub(crate) fn cleanup_fuse(&self) {
|
||||
let adapter_opt = match self.fuse_adapter.lock() {
|
||||
Ok(mut guard) => guard.take(),
|
||||
Err(e) => {
|
||||
tracing::error!("FUSE adapter lock 失败: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(adapter) = adapter_opt {
|
||||
if let Err(e) = adapter.unmount() {
|
||||
tracing::warn!("FUSE 卸载失败: {}", e);
|
||||
}
|
||||
tracing::info!("FUSE 适配器已清理");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,11 @@ impl SyncEngine {
|
||||
|
||||
let (local_root, remote_root, sync_mode) = {
|
||||
let config = self.config.read().await;
|
||||
(config.local_root.clone(), config.remote_root.clone(), config.sync_mode.clone())
|
||||
(
|
||||
config.local_root.clone(),
|
||||
config.remote_root.clone(),
|
||||
config.sync_mode.clone(),
|
||||
)
|
||||
};
|
||||
tracing::info!("开始初始同步, 模式={:?}", sync_mode);
|
||||
|
||||
@@ -47,7 +51,13 @@ impl SyncEngine {
|
||||
}
|
||||
|
||||
let db_mappings = self.load_all_mappings().await?;
|
||||
let plan = crate::diff::compute_diff(&local_files, &remote_files, &db_mappings, &remote_root, &sync_mode);
|
||||
let plan = crate::diff::compute_diff(
|
||||
&local_files,
|
||||
&remote_files,
|
||||
&db_mappings,
|
||||
&remote_root,
|
||||
&sync_mode,
|
||||
);
|
||||
tracing::info!(
|
||||
"差异计算完成: 上传={}, 下载={}, 删本地={}, 删远程={}, 冲突={}",
|
||||
plan.uploads.len(),
|
||||
@@ -83,7 +93,8 @@ impl SyncEngine {
|
||||
self.db.clone(),
|
||||
self.api.clone(),
|
||||
config.clone(),
|
||||
).map_err(|e| crate::errors::SyncError::Internal(e.to_string()))?;
|
||||
)
|
||||
.map_err(|e| crate::errors::SyncError::Internal(e.to_string()))?;
|
||||
let fetch_rx = adapter.take_fetch_receiver();
|
||||
*self.wcf_fetch_rx.lock().unwrap() = fetch_rx;
|
||||
let adapter_arc = std::sync::Arc::new(adapter);
|
||||
@@ -96,9 +107,79 @@ impl SyncEngine {
|
||||
}
|
||||
}
|
||||
|
||||
let result = self.worker_pool.submit(
|
||||
plan, worker_config, WorkerTrigger::InitialSync, conflict_resolver,
|
||||
).await;
|
||||
// MirrorFUSE 模式:初始化 FUSE 平台适配器(直接挂载到 local_root)
|
||||
#[cfg(feature = "linux-fuse")]
|
||||
if matches!(sync_mode, SyncMode::MirrorWcf) {
|
||||
let already_initialized = self
|
||||
.fuse_adapter
|
||||
.lock()
|
||||
.map(|g| g.is_some())
|
||||
.unwrap_or(false);
|
||||
if !already_initialized {
|
||||
let config = self.config.read().await;
|
||||
let mount_path = config.local_root.clone();
|
||||
let adapter = crate::platform::fuse::FusePlatformAdapter::new(
|
||||
&mount_path,
|
||||
self.db.clone(),
|
||||
self.api.clone(),
|
||||
config.clone(),
|
||||
)
|
||||
.map_err(|e| crate::errors::SyncError::Internal(e.to_string()))?;
|
||||
|
||||
// 注册所有远程文件到 FUSE inode 表
|
||||
for remote in &remote_files {
|
||||
let relative = crate::diff::remote_relative_path(
|
||||
&remote_root,
|
||||
&remote.path,
|
||||
&remote.name,
|
||||
remote.is_dir,
|
||||
);
|
||||
let parent_rel = std::path::PathBuf::from(&relative)
|
||||
.parent()
|
||||
.map(|p| crate::utils::normalize_path(&p.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
let name = std::path::PathBuf::from(&relative)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
adapter.create_placeholder_for_remote(
|
||||
&parent_rel,
|
||||
&name,
|
||||
&relative,
|
||||
remote.is_dir,
|
||||
remote.size,
|
||||
&remote.uri,
|
||||
remote.hash.as_deref(),
|
||||
remote.mtime_ms,
|
||||
);
|
||||
}
|
||||
|
||||
let request_rx = adapter.take_request_receiver();
|
||||
if let Ok(mut rx) = self.fuse_request_rx.lock() {
|
||||
*rx = request_rx;
|
||||
}
|
||||
if let Ok(mut adapter_guard) = self.fuse_adapter.lock() {
|
||||
*adapter_guard = Some(std::sync::Arc::new(adapter));
|
||||
}
|
||||
tracing::info!(
|
||||
"MirrorFUSE: FUSE 平台适配器已初始化, 挂载点={}, inode 数={}",
|
||||
mount_path.display(),
|
||||
remote_files.len()
|
||||
);
|
||||
} else {
|
||||
tracing::info!("MirrorFUSE: FUSE 平台适配器已存在,跳过重复初始化");
|
||||
}
|
||||
}
|
||||
|
||||
let result = self
|
||||
.worker_pool
|
||||
.submit(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::InitialSync,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(summary) => {
|
||||
|
||||
@@ -24,28 +24,60 @@ impl SyncEngine {
|
||||
|
||||
// 清理过期的 suppress 记录(超过 30 秒)
|
||||
let now = std::time::Instant::now();
|
||||
self.suppress_paths.retain(|_, ts| now.duration_since(*ts).as_secs() < 30);
|
||||
self.suppress_paths
|
||||
.retain(|_, ts| now.duration_since(*ts).as_secs() < 30);
|
||||
|
||||
// === 第一步:提取 Renamed/Moved 事件,查 DB 构建操作 ===
|
||||
let mut rename_remote: Vec<RenameAction> = Vec::new();
|
||||
let mut move_remote: Vec<MoveAction> = Vec::new();
|
||||
let mut handled_old_rels: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut handled_new_rels: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut handled_old_rels: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
let mut handled_new_rels: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
|
||||
for event in &all_events {
|
||||
match event {
|
||||
LocalFileEvent::Renamed { old_paths, new_paths } => {
|
||||
LocalFileEvent::Renamed {
|
||||
old_paths,
|
||||
new_paths,
|
||||
} => {
|
||||
for (old_path, new_path) in old_paths.iter().zip(new_paths.iter()) {
|
||||
if let Some((old_rel, new_rel)) = rel_pair(local_root, old_path, new_path) {
|
||||
if self.suppress_paths.contains_key(&old_rel) || self.suppress_paths.contains_key(&new_rel) {
|
||||
tracing::trace!("本地重命名被抑制(远程操作导致): {} -> {}", old_rel, new_rel);
|
||||
if self.suppress_paths.contains_key(&old_rel)
|
||||
|| self.suppress_paths.contains_key(&new_rel)
|
||||
{
|
||||
tracing::trace!(
|
||||
"本地重命名被抑制(远程操作导致): {} -> {}",
|
||||
old_rel,
|
||||
new_rel
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let new_name = new_path.file_name()
|
||||
let new_name = new_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Ok(Some(mapping)) = self.db.get_file_mapping(&root_id, &old_rel).await {
|
||||
// Android 回收站:.trashed- 前缀视为本地删除,不重命名远程
|
||||
if new_name.starts_with(".trashed-") {
|
||||
if let Ok(Some(_)) =
|
||||
self.db.get_file_mapping(&root_id, &old_rel).await
|
||||
{
|
||||
tracing::info!(
|
||||
"检测到本地回收站重命名,视为删除: {} -> {}",
|
||||
old_rel,
|
||||
new_rel
|
||||
);
|
||||
let _ = self.db.delete_file_mapping(&root_id, &old_rel).await;
|
||||
handled_old_rels.insert(old_rel);
|
||||
handled_new_rels.insert(new_rel);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(Some(mapping)) =
|
||||
self.db.get_file_mapping(&root_id, &old_rel).await
|
||||
{
|
||||
tracing::info!("检测到本地重命名: {} -> {}", old_rel, new_rel);
|
||||
rename_remote.push(RenameAction {
|
||||
old_relative_path: old_rel.clone(),
|
||||
@@ -56,27 +88,67 @@ impl SyncEngine {
|
||||
handled_old_rels.insert(old_rel);
|
||||
handled_new_rels.insert(new_rel);
|
||||
} else {
|
||||
tracing::info!("本地重命名但旧路径无DB映射,按新建处理: {} -> {}", old_rel, new_rel);
|
||||
tracing::info!(
|
||||
"本地重命名但旧路径无DB映射,按新建处理: {} -> {}",
|
||||
old_rel,
|
||||
new_rel
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LocalFileEvent::Moved { old_paths, new_paths } => {
|
||||
LocalFileEvent::Moved {
|
||||
old_paths,
|
||||
new_paths,
|
||||
} => {
|
||||
for (old_path, new_path) in old_paths.iter().zip(new_paths.iter()) {
|
||||
if let Some((old_rel, new_rel)) = rel_pair(local_root, old_path, new_path) {
|
||||
if self.suppress_paths.contains_key(&old_rel) || self.suppress_paths.contains_key(&new_rel) {
|
||||
tracing::trace!("本地移动被抑制(远程操作导致): {} -> {}", old_rel, new_rel);
|
||||
if self.suppress_paths.contains_key(&old_rel)
|
||||
|| self.suppress_paths.contains_key(&new_rel)
|
||||
{
|
||||
tracing::trace!(
|
||||
"本地移动被抑制(远程操作导致): {} -> {}",
|
||||
old_rel,
|
||||
new_rel
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if let Ok(Some(mapping)) = self.db.get_file_mapping(&root_id, &old_rel).await {
|
||||
let new_name = new_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Android 回收站:移动到 .trashed- 前缀文件,视为本地删除
|
||||
if new_name.starts_with(".trashed-") {
|
||||
if let Ok(Some(_)) =
|
||||
self.db.get_file_mapping(&root_id, &old_rel).await
|
||||
{
|
||||
tracing::info!(
|
||||
"检测到本地回收站移动,视为删除: {} -> {}",
|
||||
old_rel,
|
||||
new_rel
|
||||
);
|
||||
let _ = self.db.delete_file_mapping(&root_id, &old_rel).await;
|
||||
handled_old_rels.insert(old_rel);
|
||||
handled_new_rels.insert(new_rel);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(Some(mapping)) =
|
||||
self.db.get_file_mapping(&root_id, &old_rel).await
|
||||
{
|
||||
let remote_root = { self.config.read().await.remote_root.clone() };
|
||||
let new_rel_path = std::path::PathBuf::from(&new_rel);
|
||||
let dst_dir_rel = new_rel_path.parent()
|
||||
let dst_dir_rel = new_rel_path
|
||||
.parent()
|
||||
.map(|p| crate::utils::normalize_path(&p.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
let dst_remote_dir_uri = format!("{}/{}",
|
||||
let dst_remote_dir_uri = format!(
|
||||
"{}/{}",
|
||||
remote_root.trim_end_matches('/'),
|
||||
dst_dir_rel.trim_start_matches('/'));
|
||||
dst_dir_rel.trim_start_matches('/')
|
||||
);
|
||||
|
||||
tracing::info!("检测到本地移动: {} -> {}", old_rel, new_rel);
|
||||
move_remote.push(MoveAction {
|
||||
@@ -88,7 +160,11 @@ impl SyncEngine {
|
||||
handled_old_rels.insert(old_rel);
|
||||
handled_new_rels.insert(new_rel);
|
||||
} else {
|
||||
tracing::info!("本地移动但旧路径无DB映射,按新建处理: {} -> {}", old_rel, new_rel);
|
||||
tracing::info!(
|
||||
"本地移动但旧路径无DB映射,按新建处理: {} -> {}",
|
||||
old_rel,
|
||||
new_rel
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,8 +174,10 @@ impl SyncEngine {
|
||||
}
|
||||
|
||||
// === 第二步:按事件类型分类路径,跳过已识别为 rename/move 的路径 ===
|
||||
let mut create_paths: std::collections::BTreeMap<String, std::path::PathBuf> = std::collections::BTreeMap::new();
|
||||
let mut delete_paths: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
|
||||
let mut create_paths: std::collections::BTreeMap<String, std::path::PathBuf> =
|
||||
std::collections::BTreeMap::new();
|
||||
let mut delete_paths: std::collections::BTreeSet<String> =
|
||||
std::collections::BTreeSet::new();
|
||||
|
||||
for event in &all_events {
|
||||
for path in event.paths() {
|
||||
@@ -107,21 +185,31 @@ impl SyncEngine {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||||
if crate::fs_scanner::SKIP_NAMES.iter().any(|s| file_name == *s)
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
if crate::fs_scanner::SKIP_NAMES
|
||||
.iter()
|
||||
.any(|s| file_name == *s)
|
||||
|| file_name.starts_with(".sync_")
|
||||
|| crate::utils::is_conflict_file(&file_name) {
|
||||
|| file_name.starts_with(".trashed-")
|
||||
|| crate::utils::is_conflict_file(&file_name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let relative = path.strip_prefix(local_root)
|
||||
let relative = path
|
||||
.strip_prefix(local_root)
|
||||
.unwrap_or(path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let relative = crate::utils::normalize_path(&relative);
|
||||
|
||||
if handled_old_rels.contains(&relative) || handled_new_rels.contains(&relative)
|
||||
|| self.suppress_paths.contains_key(&relative) {
|
||||
if handled_old_rels.contains(&relative)
|
||||
|| handled_new_rels.contains(&relative)
|
||||
|| self.suppress_paths.contains_key(&relative)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -145,33 +233,54 @@ impl SyncEngine {
|
||||
|
||||
// === hash 匹配回退:检测 delete+create 为 rename 的情况 ===
|
||||
// MirrorWcf: 跳过 hash 匹配回退,因为读取占位符文件会被 CFApi 拦截导致 426 超时
|
||||
if !delete_paths.is_empty() && !create_paths.is_empty() && !matches!(sync_mode, SyncMode::MirrorWcf) {
|
||||
let mut matched_deletes: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut matched_creates: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
if !delete_paths.is_empty()
|
||||
&& !create_paths.is_empty()
|
||||
&& !matches!(sync_mode, SyncMode::MirrorWcf)
|
||||
{
|
||||
let mut matched_deletes: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
let mut matched_creates: std::collections::HashSet<String> =
|
||||
std::collections::HashSet::new();
|
||||
|
||||
for (new_rel, new_path) in &create_paths {
|
||||
if let Ok(metadata) = tokio::fs::metadata(new_path).await {
|
||||
if metadata.is_dir() || metadata.len() == 0 { continue; }
|
||||
let new_hash = crate::utils::quick_hash(new_path, metadata.len()).await.unwrap_or_default();
|
||||
if new_hash.is_empty() { continue; }
|
||||
if metadata.is_dir() || metadata.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
let new_hash = crate::utils::quick_hash(new_path, metadata.len())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if new_hash.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for del_rel in &delete_paths {
|
||||
if matched_deletes.contains(del_rel.as_str()) { continue; }
|
||||
if let Ok(Some(mapping)) = self.db.get_file_mapping(&root_id, del_rel).await {
|
||||
if matched_deletes.contains(del_rel.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(Some(mapping)) = self.db.get_file_mapping(&root_id, del_rel).await
|
||||
{
|
||||
if mapping.local_hash.as_deref() == Some(&new_hash) {
|
||||
let new_name = new_path.file_name()
|
||||
let new_name = new_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let old_dir = std::path::PathBuf::from(del_rel).parent()
|
||||
let old_dir = std::path::PathBuf::from(del_rel)
|
||||
.parent()
|
||||
.map(|p| crate::utils::normalize_path(&p.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
let new_dir = std::path::PathBuf::from(new_rel.as_str()).parent()
|
||||
let new_dir = std::path::PathBuf::from(new_rel.as_str())
|
||||
.parent()
|
||||
.map(|p| crate::utils::normalize_path(&p.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
|
||||
if old_dir == new_dir {
|
||||
tracing::info!("hash匹配检测到重命名: {} -> {}", del_rel, new_rel);
|
||||
tracing::info!(
|
||||
"hash匹配检测到重命名: {} -> {}",
|
||||
del_rel,
|
||||
new_rel
|
||||
);
|
||||
rename_remote.push(RenameAction {
|
||||
old_relative_path: del_rel.clone(),
|
||||
new_relative_path: new_rel.clone(),
|
||||
@@ -179,11 +288,18 @@ impl SyncEngine {
|
||||
new_name,
|
||||
});
|
||||
} else {
|
||||
let remote_root = { self.config.read().await.remote_root.clone() };
|
||||
let dst_remote_dir_uri = format!("{}/{}",
|
||||
let remote_root =
|
||||
{ self.config.read().await.remote_root.clone() };
|
||||
let dst_remote_dir_uri = format!(
|
||||
"{}/{}",
|
||||
remote_root.trim_end_matches('/'),
|
||||
new_dir.trim_start_matches('/'));
|
||||
tracing::info!("hash匹配检测到移动: {} -> {}", del_rel, new_rel);
|
||||
new_dir.trim_start_matches('/')
|
||||
);
|
||||
tracing::info!(
|
||||
"hash匹配检测到移动: {} -> {}",
|
||||
del_rel,
|
||||
new_rel
|
||||
);
|
||||
move_remote.push(MoveAction {
|
||||
old_relative_path: del_rel.clone(),
|
||||
new_relative_path: new_rel.clone(),
|
||||
@@ -212,9 +328,14 @@ impl SyncEngine {
|
||||
};
|
||||
let worker_config = self.snapshot_worker_config().await;
|
||||
let conflict_resolver = self.conflict.read().await.clone();
|
||||
self.worker_pool.submit_background(
|
||||
plan, worker_config, WorkerTrigger::Continuous, conflict_resolver,
|
||||
).await;
|
||||
self.worker_pool
|
||||
.submit_background(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::Continuous,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// === 提交移动任务 ===
|
||||
@@ -225,9 +346,14 @@ impl SyncEngine {
|
||||
};
|
||||
let worker_config = self.snapshot_worker_config().await;
|
||||
let conflict_resolver = self.conflict.read().await.clone();
|
||||
self.worker_pool.submit_background(
|
||||
plan, worker_config, WorkerTrigger::Continuous, conflict_resolver,
|
||||
).await;
|
||||
self.worker_pool
|
||||
.submit_background(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::Continuous,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// === 提交上传任务 (Create/Modify) ===
|
||||
@@ -265,12 +391,21 @@ impl SyncEngine {
|
||||
let quick_hash = if matches!(sync_mode, SyncMode::MirrorWcf) {
|
||||
String::new()
|
||||
} else {
|
||||
crate::utils::quick_hash(path, size).await.unwrap_or_default()
|
||||
crate::utils::quick_hash(path, size)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let db_mapping = self.db.get_file_mapping(&root_id, relative).await.ok().flatten();
|
||||
let db_mapping = self
|
||||
.db
|
||||
.get_file_mapping(&root_id, relative)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(ref mapping) = db_mapping {
|
||||
if !quick_hash.is_empty() && mapping.local_hash.as_deref() == Some(&quick_hash) {
|
||||
if !quick_hash.is_empty()
|
||||
&& mapping.local_hash.as_deref() == Some(&quick_hash)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if mapping.is_placeholder {
|
||||
@@ -278,7 +413,8 @@ impl SyncEngine {
|
||||
}
|
||||
}
|
||||
|
||||
let mtime_ms = metadata.modified()
|
||||
let mtime_ms = metadata
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_millis() as i64)
|
||||
@@ -303,28 +439,33 @@ impl SyncEngine {
|
||||
|
||||
let scan_dirs = find_top_level_dirs(&dir_paths);
|
||||
|
||||
let all_handled: std::collections::HashSet<&String> = handled_old_rels.iter()
|
||||
let all_handled: std::collections::HashSet<&String> = handled_old_rels
|
||||
.iter()
|
||||
.chain(handled_new_rels.iter())
|
||||
.collect();
|
||||
let filtered_scan_dirs: Vec<String> = scan_dirs.into_iter().filter(|dir| {
|
||||
if all_handled.iter().any(|rel| {
|
||||
rel.starts_with(dir.as_str())
|
||||
&& rel.as_bytes().get(dir.len()) == Some(&b'/')
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
for entry in self.suppress_paths.iter() {
|
||||
let rel = entry.key();
|
||||
if rel.starts_with(dir.as_str())
|
||||
&& rel.as_bytes().get(dir.len()) == Some(&b'/') {
|
||||
let filtered_scan_dirs: Vec<String> = scan_dirs
|
||||
.into_iter()
|
||||
.filter(|dir| {
|
||||
if all_handled.iter().any(|rel| {
|
||||
rel.starts_with(dir.as_str())
|
||||
&& rel.as_bytes().get(dir.len()) == Some(&b'/')
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
if dir.as_str() == rel.as_str() {
|
||||
return false;
|
||||
for entry in self.suppress_paths.iter() {
|
||||
let rel = entry.key();
|
||||
if rel.starts_with(dir.as_str())
|
||||
&& rel.as_bytes().get(dir.len()) == Some(&b'/')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if dir.as_str() == rel.as_str() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}).collect();
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !filtered_scan_dirs.is_empty() {
|
||||
uploads.retain(|action| {
|
||||
@@ -338,8 +479,10 @@ impl SyncEngine {
|
||||
if !uploads.is_empty() || !filtered_scan_dirs.is_empty() {
|
||||
tracing::info!(
|
||||
"本地事件收集完成: 上传={}, 目录扫描={:?}, 跳过(未稳定)={}, 跳过(重复上传)={}",
|
||||
uploads.len(), filtered_scan_dirs,
|
||||
skipped_unstable, skipped_uploading,
|
||||
uploads.len(),
|
||||
filtered_scan_dirs,
|
||||
skipped_unstable,
|
||||
skipped_uploading,
|
||||
);
|
||||
let plan = SyncPlan {
|
||||
uploads,
|
||||
@@ -348,9 +491,14 @@ impl SyncEngine {
|
||||
};
|
||||
let worker_config = self.snapshot_worker_config().await;
|
||||
let conflict_resolver = self.conflict.read().await.clone();
|
||||
self.worker_pool.submit_background(
|
||||
plan, worker_config, WorkerTrigger::Continuous, conflict_resolver,
|
||||
).await;
|
||||
self.worker_pool
|
||||
.submit_background(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::Continuous,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +507,9 @@ impl SyncEngine {
|
||||
// MirrorWcf 模式:仅在 wcf_delete_mode == SyncRemote 时删除远程,否则仅删除本地(保留远程以便重新水合)
|
||||
let wcf_should_delete_remote = matches!(sync_mode, SyncMode::MirrorWcf)
|
||||
&& matches!(wcf_delete_mode, WcfDeleteMode::SyncRemote);
|
||||
if !delete_paths.is_empty() && (matches!(sync_mode, SyncMode::Full) || wcf_should_delete_remote) {
|
||||
if !delete_paths.is_empty()
|
||||
&& (matches!(sync_mode, SyncMode::Full) || wcf_should_delete_remote)
|
||||
{
|
||||
let mut delete_remote: Vec<SyncAction> = Vec::new();
|
||||
for relative in &delete_paths {
|
||||
tracing::info!("检测到本地文件删除: {}", relative);
|
||||
@@ -392,9 +542,14 @@ impl SyncEngine {
|
||||
};
|
||||
let worker_config = self.snapshot_worker_config().await;
|
||||
let conflict_resolver = self.conflict.read().await.clone();
|
||||
self.worker_pool.submit_background(
|
||||
plan, worker_config, WorkerTrigger::Continuous, conflict_resolver,
|
||||
).await;
|
||||
self.worker_pool
|
||||
.submit_background(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::Continuous,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,20 +560,47 @@ impl SyncEngine {
|
||||
tracing::debug!("仅上传模式: 本地删除仅清理映射,不删除远程: {}", relative);
|
||||
}
|
||||
}
|
||||
|
||||
// MirrorWcf DeleteLocalOnly 模式下:本地删除仅清理 DB 映射,保留远程可重新水合
|
||||
let wcf_delete_local_only = matches!(sync_mode, SyncMode::MirrorWcf)
|
||||
&& !matches!(wcf_delete_mode, WcfDeleteMode::SyncRemote);
|
||||
if !delete_paths.is_empty() && wcf_delete_local_only {
|
||||
for relative in &delete_paths {
|
||||
if let Ok(Some(_)) = self.db.get_file_mapping(&root_id, relative).await {
|
||||
let _ = self.db.delete_file_mapping(&root_id, relative).await;
|
||||
tracing::info!("MirrorWcf(仅删除本地): 清理映射,保留远程: {}", relative);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 从两个绝对路径生成相对于 local_root 的相对路径对
|
||||
fn rel_pair(local_root: &std::path::Path, old_path: &std::path::Path, new_path: &std::path::Path) -> Option<(String, String)> {
|
||||
let old_rel = old_path.strip_prefix(local_root).ok()?
|
||||
.to_string_lossy().to_string();
|
||||
let new_rel = new_path.strip_prefix(local_root).ok()?
|
||||
.to_string_lossy().to_string();
|
||||
Some((crate::utils::normalize_path(&old_rel), crate::utils::normalize_path(&new_rel)))
|
||||
fn rel_pair(
|
||||
local_root: &std::path::Path,
|
||||
old_path: &std::path::Path,
|
||||
new_path: &std::path::Path,
|
||||
) -> Option<(String, String)> {
|
||||
let old_rel = old_path
|
||||
.strip_prefix(local_root)
|
||||
.ok()?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let new_rel = new_path
|
||||
.strip_prefix(local_root)
|
||||
.ok()?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
Some((
|
||||
crate::utils::normalize_path(&old_rel),
|
||||
crate::utils::normalize_path(&new_rel),
|
||||
))
|
||||
}
|
||||
|
||||
fn find_top_level_dirs(dirs: &[String]) -> Vec<String> {
|
||||
if dirs.is_empty() { return Vec::new(); }
|
||||
if dirs.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut sorted: Vec<&String> = dirs.iter().collect();
|
||||
sorted.sort();
|
||||
@@ -426,8 +608,7 @@ fn find_top_level_dirs(dirs: &[String]) -> Vec<String> {
|
||||
let mut top_level = Vec::new();
|
||||
for dir in &sorted {
|
||||
let dominated = top_level.iter().any(|parent: &String| {
|
||||
dir.starts_with(parent.as_str())
|
||||
&& dir.as_bytes().get(parent.len()) == Some(&b'/')
|
||||
dir.starts_with(parent.as_str()) && dir.as_bytes().get(parent.len()) == Some(&b'/')
|
||||
});
|
||||
if !dominated {
|
||||
top_level.retain(|existing: &String| {
|
||||
@@ -449,13 +630,11 @@ async fn is_file_stable(path: &Path) -> bool {
|
||||
};
|
||||
|
||||
let path_display = path.display().to_string();
|
||||
let can_open = tokio::task::spawn_blocking(move || {
|
||||
match std::fs::File::open(&path_display) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
tracing::trace!("文件稳定性检测打开失败: {}", e);
|
||||
false
|
||||
}
|
||||
let can_open = tokio::task::spawn_blocking(move || match std::fs::File::open(&path_display) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
tracing::trace!("文件稳定性检测打开失败: {}", e);
|
||||
false
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ impl SyncEngine {
|
||||
remote_root: &str,
|
||||
) {
|
||||
// UploadOnly: 忽略所有远程事件
|
||||
// AlbumDownload: 忽略删除事件(不删本地照片)
|
||||
let sync_mode = {
|
||||
let config = self.config.read().await;
|
||||
config.sync_mode.clone()
|
||||
@@ -30,12 +31,21 @@ impl SyncEngine {
|
||||
&remote.name,
|
||||
remote.is_dir,
|
||||
);
|
||||
tracing::info!("[远程事件] {}/{:?}: {}", event_type_name(&event), remote.file_id, relative);
|
||||
tracing::info!(
|
||||
"[远程事件] {}/{:?}: {}",
|
||||
event_type_name(&event),
|
||||
remote.file_id,
|
||||
relative
|
||||
);
|
||||
|
||||
let remote_entry = if remote.size == 0 && !remote.is_dir {
|
||||
match self.api.get_file_info(&remote.uri).await {
|
||||
Ok(info) => {
|
||||
tracing::debug!("[远程事件] 获取文件详情成功: {} ({}bytes)", relative, info.size);
|
||||
tracing::debug!(
|
||||
"[远程事件] 获取文件详情成功: {} ({}bytes)",
|
||||
relative,
|
||||
info.size
|
||||
);
|
||||
info
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -58,8 +68,19 @@ impl SyncEngine {
|
||||
|
||||
if is_mirror_wcf {
|
||||
self._create_placeholder_for_remote(
|
||||
&relative, &remote_entry, local_root, &root_id,
|
||||
).await;
|
||||
&relative,
|
||||
&remote_entry,
|
||||
local_root,
|
||||
&root_id,
|
||||
)
|
||||
.await;
|
||||
self._record_wcf_stats(
|
||||
&relative,
|
||||
TaskActionType::CreatePlaceholder,
|
||||
remote_entry.size,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
let plan = SyncPlan {
|
||||
downloads: vec![SyncAction {
|
||||
@@ -72,24 +93,32 @@ impl SyncEngine {
|
||||
};
|
||||
let worker_config = self.snapshot_worker_config().await;
|
||||
let conflict_resolver = self.conflict.read().await.clone();
|
||||
self.worker_pool.submit_background(
|
||||
plan, worker_config, WorkerTrigger::Continuous, conflict_resolver,
|
||||
).await;
|
||||
self.worker_pool
|
||||
.submit_background(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::Continuous,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
RemoteFileEvent::Deleted { uri, name } => {
|
||||
// 清理过期抑制条目
|
||||
let now = std::time::Instant::now();
|
||||
self.suppress_paths.retain(|_, ts| now.duration_since(*ts).as_secs() < 30);
|
||||
self.suppress_paths
|
||||
.retain(|_, ts| now.duration_since(*ts).as_secs() < 30);
|
||||
|
||||
let relative = crate::diff::remote_relative_path(
|
||||
remote_root,
|
||||
uri,
|
||||
name,
|
||||
false,
|
||||
);
|
||||
let relative = crate::diff::remote_relative_path(remote_root, uri, name, false);
|
||||
tracing::info!("[远程事件] 删除: {}", relative);
|
||||
|
||||
// AlbumDownload: 远程删除不删除本地文件,仅清理 DB 映射
|
||||
if matches!(sync_mode, SyncMode::AlbumDownload) {
|
||||
tracing::info!("[远程事件] AlbumDownload 模式,跳过本地删除: {}", relative);
|
||||
let _ = self.db.delete_file_mapping(&root_id, &relative).await;
|
||||
return;
|
||||
}
|
||||
|
||||
// 被抑制的路径:上传失败清理远端碎片等场景,不应删除本地文件
|
||||
if self.suppress_paths.contains_key(&relative) {
|
||||
tracing::info!("[远程事件] 删除已抑制,跳过本地删除: {}", relative);
|
||||
@@ -98,7 +127,8 @@ impl SyncEngine {
|
||||
}
|
||||
|
||||
let local_path = local_root.join(&relative);
|
||||
if local_path.exists() {
|
||||
let existed = local_path.exists();
|
||||
if existed {
|
||||
if local_path.is_dir() {
|
||||
let _ = tokio::fs::remove_dir_all(&local_path).await;
|
||||
} else {
|
||||
@@ -107,15 +137,27 @@ impl SyncEngine {
|
||||
tracing::info!("[远程事件] 已删除本地文件: {}", relative);
|
||||
}
|
||||
let _ = self.db.delete_file_mapping(&root_id, &relative).await;
|
||||
self.suppress_paths.insert(relative.clone(), std::time::Instant::now());
|
||||
self.suppress_paths
|
||||
.insert(relative.clone(), std::time::Instant::now());
|
||||
|
||||
// MirrorFUSE: 远程删除 → 从 FUSE inode 缓存移除
|
||||
#[cfg(feature = "linux-fuse")]
|
||||
if is_mirror_wcf {
|
||||
let adapter = self.fuse_adapter.lock().unwrap();
|
||||
if let Some(ref fuse) = *adapter {
|
||||
fuse.remove_inode(&relative);
|
||||
}
|
||||
}
|
||||
|
||||
// MirrorWcf: 远程删除 → 删本地,记录统计
|
||||
if is_mirror_wcf && existed {
|
||||
self._record_wcf_stats(&relative, TaskActionType::DeleteLocal, 0, None)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
RemoteFileEvent::Renamed { old_uri, new_entry } => {
|
||||
let old_relative = crate::diff::remote_relative_path(
|
||||
remote_root,
|
||||
old_uri,
|
||||
&new_entry.name,
|
||||
false,
|
||||
);
|
||||
let old_relative =
|
||||
crate::diff::remote_relative_path(remote_root, old_uri, &new_entry.name, false);
|
||||
let new_relative = crate::diff::remote_relative_path(
|
||||
remote_root,
|
||||
&new_entry.path,
|
||||
@@ -141,14 +183,30 @@ impl SyncEngine {
|
||||
};
|
||||
let worker_config = self.snapshot_worker_config().await;
|
||||
let conflict_resolver = self.conflict.read().await.clone();
|
||||
self.worker_pool.submit_background(
|
||||
plan, worker_config, WorkerTrigger::Continuous, conflict_resolver,
|
||||
).await;
|
||||
self.worker_pool
|
||||
.submit_background(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::Continuous,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
} else if is_mirror_wcf {
|
||||
let remote_entry = self.get_remote_entry_or_fallback(new_entry).await;
|
||||
self._create_placeholder_for_remote(
|
||||
&new_relative, &remote_entry, local_root, &root_id,
|
||||
).await;
|
||||
&new_relative,
|
||||
&remote_entry,
|
||||
local_root,
|
||||
&root_id,
|
||||
)
|
||||
.await;
|
||||
self._record_wcf_stats(
|
||||
&format!("{} -> {}", old_relative, new_relative),
|
||||
TaskActionType::Rename,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
let remote_entry = self.get_remote_entry_or_fallback(new_entry).await;
|
||||
let plan = SyncPlan {
|
||||
@@ -162,18 +220,19 @@ impl SyncEngine {
|
||||
};
|
||||
let worker_config = self.snapshot_worker_config().await;
|
||||
let conflict_resolver = self.conflict.read().await.clone();
|
||||
self.worker_pool.submit_background(
|
||||
plan, worker_config, WorkerTrigger::Continuous, conflict_resolver,
|
||||
).await;
|
||||
self.worker_pool
|
||||
.submit_background(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::Continuous,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
RemoteFileEvent::Moved { old_uri, new_entry } => {
|
||||
let old_relative = crate::diff::remote_relative_path(
|
||||
remote_root,
|
||||
old_uri,
|
||||
&new_entry.name,
|
||||
false,
|
||||
);
|
||||
let old_relative =
|
||||
crate::diff::remote_relative_path(remote_root, old_uri, &new_entry.name, false);
|
||||
let new_relative = crate::diff::remote_relative_path(
|
||||
remote_root,
|
||||
&new_entry.path,
|
||||
@@ -199,14 +258,30 @@ impl SyncEngine {
|
||||
};
|
||||
let worker_config = self.snapshot_worker_config().await;
|
||||
let conflict_resolver = self.conflict.read().await.clone();
|
||||
self.worker_pool.submit_background(
|
||||
plan, worker_config, WorkerTrigger::Continuous, conflict_resolver,
|
||||
).await;
|
||||
self.worker_pool
|
||||
.submit_background(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::Continuous,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
} else if is_mirror_wcf {
|
||||
let remote_entry = self.get_remote_entry_or_fallback(new_entry).await;
|
||||
self._create_placeholder_for_remote(
|
||||
&new_relative, &remote_entry, local_root, &root_id,
|
||||
).await;
|
||||
&new_relative,
|
||||
&remote_entry,
|
||||
local_root,
|
||||
&root_id,
|
||||
)
|
||||
.await;
|
||||
self._record_wcf_stats(
|
||||
&format!("{} -> {}", old_relative, new_relative),
|
||||
TaskActionType::Move,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
let remote_entry = self.get_remote_entry_or_fallback(new_entry).await;
|
||||
let plan = SyncPlan {
|
||||
@@ -220,16 +295,61 @@ impl SyncEngine {
|
||||
};
|
||||
let worker_config = self.snapshot_worker_config().await;
|
||||
let conflict_resolver = self.conflict.read().await.clone();
|
||||
self.worker_pool.submit_background(
|
||||
plan, worker_config, WorkerTrigger::Continuous, conflict_resolver,
|
||||
).await;
|
||||
self.worker_pool
|
||||
.submit_background(
|
||||
plan,
|
||||
worker_config,
|
||||
WorkerTrigger::Continuous,
|
||||
conflict_resolver,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MirrorWcf 专用:记录绕过 WorkerPool 的操作统计
|
||||
async fn _record_wcf_stats(
|
||||
&self,
|
||||
relative_path: &str,
|
||||
action_type: TaskActionType,
|
||||
file_size: u64,
|
||||
error_message: Option<String>,
|
||||
) {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let status = if error_message.is_none() {
|
||||
TaskItemStatus::Completed
|
||||
} else {
|
||||
TaskItemStatus::Failed
|
||||
};
|
||||
let task_id = format!("wcf_{}", uuid::Uuid::new_v4());
|
||||
if let Err(e) = self
|
||||
.db
|
||||
.record_standalone_task_item(
|
||||
&WorkerTrigger::WcfEvent,
|
||||
&SyncTaskItem {
|
||||
id: 0,
|
||||
task_id,
|
||||
relative_path: relative_path.to_string(),
|
||||
action_type,
|
||||
status,
|
||||
file_size,
|
||||
error_message,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("WCF 统计记录失败: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取远程文件详情,失败则使用 SSE 数据回退
|
||||
pub(crate) async fn get_remote_entry_or_fallback(&self, entry: &RemoteFileEntry) -> RemoteFileEntry {
|
||||
pub(crate) async fn get_remote_entry_or_fallback(
|
||||
&self,
|
||||
entry: &RemoteFileEntry,
|
||||
) -> RemoteFileEntry {
|
||||
if entry.size == 0 && !entry.is_dir {
|
||||
match self.api.get_file_info(&entry.uri).await {
|
||||
Ok(info) => info,
|
||||
|
||||
@@ -18,7 +18,8 @@ impl SyncEngine {
|
||||
Err(e) => {
|
||||
tracing::error!("WCF 水合: FileIdentity 反序列化失败: {}", e);
|
||||
let _ = crate::platform::wcf::WcfPlatformAdapter::reject_fetch_data(
|
||||
request.connection_key, request.transfer_key,
|
||||
request.connection_key,
|
||||
request.transfer_key,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -31,19 +32,26 @@ impl SyncEngine {
|
||||
if remote_uri.is_empty() {
|
||||
tracing::error!("WCF 水合: FileIdentity 中 uri 为空");
|
||||
let _ = crate::platform::wcf::WcfPlatformAdapter::reject_fetch_data(
|
||||
request.connection_key, request.transfer_key,
|
||||
request.connection_key,
|
||||
request.transfer_key,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::debug!("WCF 水合请求: uri={}, size={}, offset={}, length={}",
|
||||
remote_uri, remote_size, request.required_offset, request.required_length);
|
||||
tracing::debug!(
|
||||
"WCF 水合请求: uri={}, size={}, offset={}, length={}",
|
||||
remote_uri,
|
||||
remote_size,
|
||||
request.required_offset,
|
||||
request.required_length
|
||||
);
|
||||
|
||||
let root_id = match &self.sync_root_id {
|
||||
Some(id) => id.clone(),
|
||||
None => {
|
||||
let _ = crate::platform::wcf::WcfPlatformAdapter::reject_fetch_data(
|
||||
request.connection_key, request.transfer_key,
|
||||
request.connection_key,
|
||||
request.transfer_key,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -51,12 +59,13 @@ impl SyncEngine {
|
||||
|
||||
// 清理过期缓存(超过 5 分钟)
|
||||
let now = std::time::Instant::now();
|
||||
self.hydration_cache.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < 300);
|
||||
self.hydration_cache
|
||||
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < 300);
|
||||
|
||||
// 尝试从缓存获取已下载的数据
|
||||
let data = if let Some(cached) = self.hydration_cache.get(&remote_uri) {
|
||||
// 尝试从缓存获取已下载的数据;cache miss 时下载并标记 is_new_download
|
||||
let (data, is_new_download) = if let Some(cached) = self.hydration_cache.get(&remote_uri) {
|
||||
tracing::debug!("WCF 水合缓存命中: {}", remote_uri);
|
||||
cached.0.clone()
|
||||
(cached.0.clone(), false)
|
||||
} else {
|
||||
tracing::info!("WCF 水合下载: {} ({}bytes)", remote_uri, remote_size);
|
||||
let config = self.snapshot_worker_config().await;
|
||||
@@ -72,27 +81,57 @@ impl SyncEngine {
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let download_url = urls.into_iter().next()
|
||||
.ok_or_else(|| crate::errors::SyncError::Network("获取下载 URL 返回空列表".into()))?;
|
||||
let download_url = urls.into_iter().next().ok_or_else(|| {
|
||||
crate::errors::SyncError::Network("获取下载 URL 返回空列表".into())
|
||||
})?;
|
||||
|
||||
let data = crate::downloader::download_to_buffer(
|
||||
&self.api,
|
||||
&download_url,
|
||||
config.bandwidth_limit,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok::<Vec<u8>, crate::errors::SyncError>(data)
|
||||
}.await;
|
||||
}
|
||||
.await;
|
||||
|
||||
match download_result {
|
||||
Ok(data) => {
|
||||
self.hydration_cache.insert(remote_uri.clone(), (data.clone(), std::time::Instant::now()));
|
||||
data
|
||||
self.hydration_cache.insert(
|
||||
remote_uri.clone(),
|
||||
(data.clone(), std::time::Instant::now()),
|
||||
);
|
||||
(data, true)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("WCF 水合下载失败: {}: {}", remote_uri, e);
|
||||
// 下载失败时记录一次统计
|
||||
let now_str = chrono::Utc::now().to_rfc3339();
|
||||
let task_id = format!("hydration_{}", uuid::Uuid::new_v4());
|
||||
if let Err(db_err) = self
|
||||
.db
|
||||
.record_standalone_task_item(
|
||||
&crate::models::WorkerTrigger::Hydration,
|
||||
&crate::models::SyncTaskItem {
|
||||
id: 0,
|
||||
task_id,
|
||||
relative_path: remote_uri.clone(),
|
||||
action_type: crate::models::TaskActionType::Hydration,
|
||||
status: crate::models::TaskItemStatus::Failed,
|
||||
file_size: remote_size,
|
||||
error_message: Some(e.to_string()),
|
||||
created_at: now_str.clone(),
|
||||
updated_at: now_str,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("WCF 水合失败统计记录失败: {}", db_err);
|
||||
}
|
||||
let _ = crate::platform::wcf::WcfPlatformAdapter::reject_fetch_data(
|
||||
request.connection_key, request.transfer_key,
|
||||
request.connection_key,
|
||||
request.transfer_key,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -120,25 +159,72 @@ impl SyncEngine {
|
||||
offset as i64,
|
||||
) {
|
||||
Ok(_) => {
|
||||
tracing::debug!("WCF 水合数据推送: {} offset={} len={}", remote_uri, offset, transfer_data.len());
|
||||
tracing::debug!(
|
||||
"WCF 水合数据推送: {} offset={} len={}",
|
||||
remote_uri,
|
||||
offset,
|
||||
transfer_data.len()
|
||||
);
|
||||
|
||||
if let Ok(Some(mapping)) = self.db.find_mapping_by_remote_uri(&root_id, &remote_uri).await {
|
||||
self.suppress_paths.insert(mapping.local_path.to_string_lossy().into_owned(), std::time::Instant::now());
|
||||
let _ = self.db.upsert_file_mapping(&FileMapping {
|
||||
id: mapping.id,
|
||||
sync_root_id: mapping.sync_root_id,
|
||||
local_path: mapping.local_path.clone(),
|
||||
remote_uri: mapping.remote_uri.clone(),
|
||||
remote_file_id: mapping.remote_file_id.clone(),
|
||||
local_hash: None,
|
||||
remote_hash: if remote_hash.is_empty() { mapping.remote_hash.clone() } else { Some(remote_hash.clone()) },
|
||||
local_mtime: mapping.local_mtime,
|
||||
remote_mtime: mapping.remote_mtime,
|
||||
local_size: mapping.local_size,
|
||||
remote_size: Some(remote_size),
|
||||
sync_status: SyncFileStatus::Synced,
|
||||
is_placeholder: false,
|
||||
}).await;
|
||||
// 仅在首次下载(cache miss)时更新映射和记录统计,避免同一文件多次 range 请求重复计数
|
||||
if is_new_download {
|
||||
if let Ok(Some(mapping)) = self
|
||||
.db
|
||||
.find_mapping_by_remote_uri(&root_id, &remote_uri)
|
||||
.await
|
||||
{
|
||||
self.suppress_paths.insert(
|
||||
mapping.local_path.to_string_lossy().into_owned(),
|
||||
std::time::Instant::now(),
|
||||
);
|
||||
let _ = self
|
||||
.db
|
||||
.upsert_file_mapping(&FileMapping {
|
||||
id: mapping.id,
|
||||
sync_root_id: mapping.sync_root_id,
|
||||
local_path: mapping.local_path.clone(),
|
||||
remote_uri: mapping.remote_uri.clone(),
|
||||
remote_file_id: mapping.remote_file_id.clone(),
|
||||
local_hash: None,
|
||||
remote_hash: if remote_hash.is_empty() {
|
||||
mapping.remote_hash.clone()
|
||||
} else {
|
||||
Some(remote_hash.clone())
|
||||
},
|
||||
local_mtime: mapping.local_mtime,
|
||||
remote_mtime: mapping.remote_mtime,
|
||||
local_size: mapping.local_size,
|
||||
remote_size: Some(remote_size),
|
||||
sync_status: SyncFileStatus::Synced,
|
||||
is_placeholder: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
// 记录水合操作到统计
|
||||
let now_str = chrono::Utc::now().to_rfc3339();
|
||||
let local_path_str = mapping.local_path.to_string_lossy().to_string();
|
||||
let task_id = format!("hydration_{}", uuid::Uuid::new_v4());
|
||||
if let Err(e) = self
|
||||
.db
|
||||
.record_standalone_task_item(
|
||||
&crate::models::WorkerTrigger::Hydration,
|
||||
&crate::models::SyncTaskItem {
|
||||
id: 0,
|
||||
task_id,
|
||||
relative_path: local_path_str,
|
||||
action_type: crate::models::TaskActionType::Hydration,
|
||||
status: crate::models::TaskItemStatus::Completed,
|
||||
file_size: remote_size,
|
||||
error_message: None,
|
||||
created_at: now_str.clone(),
|
||||
updated_at: now_str,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("WCF 水合统计记录失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -169,7 +255,11 @@ impl SyncEngine {
|
||||
if let Some(adapter) = self.platform_adapter.lock().unwrap().as_ref() {
|
||||
match adapter.create_placeholder_for_remote(
|
||||
local_path.parent().unwrap_or(local_root),
|
||||
local_path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default().as_str(),
|
||||
local_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default()
|
||||
.as_str(),
|
||||
remote.size,
|
||||
&remote.uri,
|
||||
remote.hash.as_deref(),
|
||||
@@ -181,21 +271,24 @@ impl SyncEngine {
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.db.upsert_file_mapping(&FileMapping {
|
||||
id: 0,
|
||||
sync_root_id: root_id.to_string(),
|
||||
local_path: std::path::PathBuf::from(relative),
|
||||
remote_uri: remote.uri.clone(),
|
||||
remote_file_id: remote.file_id.clone(),
|
||||
local_hash: None,
|
||||
remote_hash: remote.hash.clone(),
|
||||
local_mtime: None,
|
||||
remote_mtime: Some(remote.mtime_ms),
|
||||
local_size: None,
|
||||
remote_size: Some(remote.size),
|
||||
sync_status: SyncFileStatus::Placeholder,
|
||||
is_placeholder: true,
|
||||
}).await;
|
||||
let _ = self
|
||||
.db
|
||||
.upsert_file_mapping(&FileMapping {
|
||||
id: 0,
|
||||
sync_root_id: root_id.to_string(),
|
||||
local_path: std::path::PathBuf::from(relative),
|
||||
remote_uri: remote.uri.clone(),
|
||||
remote_file_id: remote.file_id.clone(),
|
||||
local_hash: None,
|
||||
remote_hash: remote.hash.clone(),
|
||||
local_mtime: None,
|
||||
remote_mtime: Some(remote.mtime_ms),
|
||||
local_size: None,
|
||||
remote_size: Some(remote.size),
|
||||
sync_status: SyncFileStatus::Placeholder,
|
||||
is_placeholder: true,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,31 +334,36 @@ impl SyncEngine {
|
||||
fn list_placeholders_sync(&self, sync_root_id: &str) -> anyhow::Result<Vec<FileMapping>> {
|
||||
let pool = self.db.read_pool();
|
||||
let conn = pool.get().map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, sync_root_id, local_path, remote_uri, remote_file_id,
|
||||
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",
|
||||
).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||
|
||||
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: crate::sync_db::parse_sync_status(&row.get::<_, String>(11)?),
|
||||
is_placeholder: row.get::<_, i32>(12)? != 0,
|
||||
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: crate::sync_db::parse_sync_status(&row.get::<_, String>(11)?),
|
||||
is_placeholder: row.get::<_, i32>(12)? != 0,
|
||||
})
|
||||
})
|
||||
}).map_err(|e| anyhow::anyhow!("{}", e))?
|
||||
.filter_map(|m| m.ok()).collect();
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))?
|
||||
.filter_map(|m| m.ok())
|
||||
.collect();
|
||||
|
||||
Ok(mappings)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,24 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// 发射任务项状态变更事件(供 UI 实时更新进度)
|
||||
async fn emit_item_updated(
|
||||
&self,
|
||||
task_id: &str,
|
||||
relative_path: &str,
|
||||
action: &str,
|
||||
status: &str,
|
||||
) {
|
||||
self.event_sink
|
||||
.emit(crate::api::ffi_types::SyncEventFfi::TaskItemUpdated {
|
||||
task_id: task_id.to_string(),
|
||||
relative_path: relative_path.to_string(),
|
||||
action: action.to_string(),
|
||||
status: status.to_string(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 执行 Worker 任务(编排器,调用各步骤方法)
|
||||
pub async fn run(mut self) -> Result<SyncSummary> {
|
||||
let start = Instant::now();
|
||||
@@ -113,9 +131,12 @@ impl Worker {
|
||||
self.step_rename_remote(&mut summary).await;
|
||||
self.step_move_remote(&mut summary).await;
|
||||
self.step_scan_new_dirs(&mut summary).await;
|
||||
self.step_resolve_conflicts(&mut summary, &transfer_semaphore).await;
|
||||
self.step_execute_uploads(&mut summary, &transfer_semaphore).await;
|
||||
self.step_execute_downloads_or_placeholders(&mut summary, &transfer_semaphore).await;
|
||||
self.step_resolve_conflicts(&mut summary, &transfer_semaphore)
|
||||
.await;
|
||||
self.step_execute_uploads(&mut summary, &transfer_semaphore)
|
||||
.await;
|
||||
self.step_execute_downloads_or_placeholders(&mut summary, &transfer_semaphore)
|
||||
.await;
|
||||
self.step_delete_local(&mut summary).await;
|
||||
self.step_rename_local(&mut summary).await;
|
||||
self.step_move_local(&mut summary).await;
|
||||
@@ -172,7 +193,10 @@ impl Worker {
|
||||
|
||||
/// 1. 创建远程目录结构(UploadOnly / Full / MirrorWcf)
|
||||
async fn step_create_remote_dirs(&self) {
|
||||
if matches!(self.config.sync_mode, SyncMode::DownloadOnly) {
|
||||
if matches!(
|
||||
self.config.sync_mode,
|
||||
SyncMode::DownloadOnly | SyncMode::AlbumDownload
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let tid = &self.task_id;
|
||||
@@ -197,7 +221,10 @@ impl Worker {
|
||||
|
||||
/// 2. 创建本地目录结构(DownloadOnly / Full / MirrorWcf)
|
||||
async fn step_create_local_dirs(&self) {
|
||||
if matches!(self.config.sync_mode, SyncMode::UploadOnly) {
|
||||
if matches!(
|
||||
self.config.sync_mode,
|
||||
SyncMode::UploadOnly | SyncMode::AlbumUpload
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let tid = &self.task_id;
|
||||
@@ -212,9 +239,12 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// 2.1 执行远程重命名(UploadOnly / Full / MirrorWcf)
|
||||
/// 2.1 执行远程重命名(UploadOnly / Full / MirrorWcf / AlbumUpload)
|
||||
async fn step_rename_remote(&self, summary: &mut SyncSummary) {
|
||||
if matches!(self.config.sync_mode, SyncMode::DownloadOnly) {
|
||||
if matches!(
|
||||
self.config.sync_mode,
|
||||
SyncMode::DownloadOnly | SyncMode::AlbumDownload
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let tid = &self.task_id;
|
||||
@@ -242,6 +272,8 @@ impl Worker {
|
||||
);
|
||||
summary.renamed += 1;
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(tid, &rename.new_relative_path, "rename", "completed")
|
||||
.await;
|
||||
let new_remote_uri = {
|
||||
let uri = &rename.remote_uri;
|
||||
let last_slash = uri.trim_end_matches('/').rfind('/').unwrap_or(0);
|
||||
@@ -293,7 +325,10 @@ impl Worker {
|
||||
|
||||
/// 2.2 执行远程移动(本地触发)
|
||||
async fn step_move_remote(&self, summary: &mut SyncSummary) {
|
||||
if matches!(self.config.sync_mode, SyncMode::DownloadOnly) {
|
||||
if matches!(
|
||||
self.config.sync_mode,
|
||||
SyncMode::DownloadOnly | SyncMode::AlbumDownload
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let tid = &self.task_id;
|
||||
@@ -318,6 +353,8 @@ impl Worker {
|
||||
);
|
||||
summary.moved += 1;
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(tid, &mov.new_relative_path, "move", "completed")
|
||||
.await;
|
||||
let _ = self
|
||||
.db
|
||||
.update_file_mapping_path(
|
||||
@@ -421,8 +458,7 @@ impl Worker {
|
||||
self.plan.mkdirs_remote.push(full_relative.clone());
|
||||
} else {
|
||||
let mut local_entry = entry.clone();
|
||||
local_entry.relative_path =
|
||||
std::path::PathBuf::from(&full_relative);
|
||||
local_entry.relative_path = std::path::PathBuf::from(&full_relative);
|
||||
self.plan.uploads.push(SyncAction {
|
||||
relative_path: full_relative,
|
||||
local_entry: Some(local_entry),
|
||||
@@ -491,7 +527,11 @@ impl Worker {
|
||||
}
|
||||
|
||||
/// 3. 处理冲突(串行)
|
||||
async fn step_resolve_conflicts(&self, summary: &mut SyncSummary, transfer_semaphore: &Arc<Semaphore>) {
|
||||
async fn step_resolve_conflicts(
|
||||
&self,
|
||||
summary: &mut SyncSummary,
|
||||
transfer_semaphore: &Arc<Semaphore>,
|
||||
) {
|
||||
let tid = &self.task_id;
|
||||
let root_id = &self.config.sync_root_id;
|
||||
|
||||
@@ -702,6 +742,13 @@ impl Worker {
|
||||
};
|
||||
if conflict_ok {
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(
|
||||
tid,
|
||||
&conflict.relative_path,
|
||||
"conflict_resolve",
|
||||
"completed",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let _ = self
|
||||
.db
|
||||
@@ -716,9 +763,16 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// 4. 并发上传(UploadOnly / Full / MirrorWcf)
|
||||
async fn step_execute_uploads(&self, summary: &mut SyncSummary, transfer_semaphore: &Arc<Semaphore>) {
|
||||
if matches!(self.config.sync_mode, SyncMode::DownloadOnly) {
|
||||
/// 4. 并发上传(UploadOnly / Full / MirrorWcf / AlbumUpload)
|
||||
async fn step_execute_uploads(
|
||||
&self,
|
||||
summary: &mut SyncSummary,
|
||||
transfer_semaphore: &Arc<Semaphore>,
|
||||
) {
|
||||
if matches!(
|
||||
self.config.sync_mode,
|
||||
SyncMode::DownloadOnly | SyncMode::AlbumDownload
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let tid = &self.task_id;
|
||||
@@ -762,6 +816,8 @@ impl Worker {
|
||||
Ok(Ok(_)) => {
|
||||
summary.uploaded += 1;
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(tid, &rel_path, "upload", "completed")
|
||||
.await;
|
||||
let _ = self
|
||||
.db
|
||||
.update_task_item_status_by_path(
|
||||
@@ -777,6 +833,8 @@ impl Worker {
|
||||
tracing::error!("[{}] 上传失败: {}: {}", tid, rel_path, e);
|
||||
summary.failed += 1;
|
||||
let _ = self.db.increment_task_failed(tid).await;
|
||||
self.emit_item_updated(tid, &rel_path, "upload", "failed")
|
||||
.await;
|
||||
let _ = self
|
||||
.db
|
||||
.update_task_item_status_by_path(
|
||||
@@ -794,6 +852,8 @@ impl Worker {
|
||||
tracing::error!("[{}] 上传任务异常: {}: {}", tid, rel_path, e);
|
||||
summary.failed += 1;
|
||||
let _ = self.db.increment_task_failed(tid).await;
|
||||
self.emit_item_updated(tid, &rel_path, "upload", "failed")
|
||||
.await;
|
||||
let _ = self
|
||||
.db
|
||||
.update_task_item_status_by_path(
|
||||
@@ -812,7 +872,11 @@ impl Worker {
|
||||
}
|
||||
|
||||
/// 5. 并发下载(DownloadOnly / Full)或 创建占位符(MirrorWcf)
|
||||
async fn step_execute_downloads_or_placeholders(&self, summary: &mut SyncSummary, transfer_semaphore: &Arc<Semaphore>) {
|
||||
async fn step_execute_downloads_or_placeholders(
|
||||
&self,
|
||||
summary: &mut SyncSummary,
|
||||
transfer_semaphore: &Arc<Semaphore>,
|
||||
) {
|
||||
let tid = &self.task_id;
|
||||
let root_id = self.config.sync_root_id.clone();
|
||||
|
||||
@@ -824,13 +888,17 @@ impl Worker {
|
||||
}
|
||||
let relative = &action.relative_path;
|
||||
let local_path = self.config.local_root.join(relative);
|
||||
let _ = &local_path; // FUSE 模式下可能未使用
|
||||
|
||||
// FUSE 模式下不需要创建本地目录/文件,FUSE inode 已在 initial_sync 中注册
|
||||
#[cfg(not(feature = "linux-fuse"))]
|
||||
if let Some(parent) = local_path.parent() {
|
||||
let _ = tokio::fs::create_dir_all(parent).await;
|
||||
}
|
||||
|
||||
if let Some(ref remote) = action.remote_entry {
|
||||
if remote.is_dir {
|
||||
#[cfg(not(feature = "linux-fuse"))]
|
||||
let _ = tokio::fs::create_dir_all(&local_path).await;
|
||||
} else {
|
||||
#[cfg(feature = "windows-cfapi")]
|
||||
@@ -878,7 +946,11 @@ impl Worker {
|
||||
let _ = tokio::fs::write(&local_path, []).await;
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "windows-cfapi"))]
|
||||
#[cfg(all(feature = "linux-fuse", not(feature = "windows-cfapi")))]
|
||||
{
|
||||
// FUSE 模式: inode 已在 initial_sync 中注册,无需创建本地文件
|
||||
}
|
||||
#[cfg(not(any(feature = "windows-cfapi", feature = "linux-fuse")))]
|
||||
{
|
||||
let _ = tokio::fs::write(&local_path, []).await;
|
||||
tracing::warn!(
|
||||
@@ -910,6 +982,8 @@ impl Worker {
|
||||
}
|
||||
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(tid, relative, "create_placeholder", "completed")
|
||||
.await;
|
||||
let _ = self
|
||||
.db
|
||||
.update_task_item_status_by_path(
|
||||
@@ -921,7 +995,10 @@ impl Worker {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else if !matches!(self.config.sync_mode, SyncMode::UploadOnly) {
|
||||
} else if !matches!(
|
||||
self.config.sync_mode,
|
||||
SyncMode::UploadOnly | SyncMode::AlbumUpload
|
||||
) {
|
||||
let mut download_handles: Vec<(String, tokio::task::JoinHandle<Result<()>>)> =
|
||||
Vec::new();
|
||||
for action in &self.plan.downloads {
|
||||
@@ -959,6 +1036,8 @@ impl Worker {
|
||||
Ok(Ok(_)) => {
|
||||
summary.downloaded += 1;
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(tid, &rel_path, "download", "completed")
|
||||
.await;
|
||||
let _ = self
|
||||
.db
|
||||
.update_task_item_status_by_path(
|
||||
@@ -974,6 +1053,8 @@ impl Worker {
|
||||
tracing::error!("[{}] 下载失败: {}: {}", tid, rel_path, e);
|
||||
summary.failed += 1;
|
||||
let _ = self.db.increment_task_failed(tid).await;
|
||||
self.emit_item_updated(tid, &rel_path, "download", "failed")
|
||||
.await;
|
||||
let _ = self
|
||||
.db
|
||||
.update_task_item_status_by_path(
|
||||
@@ -989,6 +1070,8 @@ impl Worker {
|
||||
tracing::error!("[{}] 下载任务异常: {}: {}", tid, rel_path, e);
|
||||
summary.failed += 1;
|
||||
let _ = self.db.increment_task_failed(tid).await;
|
||||
self.emit_item_updated(tid, &rel_path, "download", "failed")
|
||||
.await;
|
||||
let _ = self
|
||||
.db
|
||||
.update_task_item_status_by_path(
|
||||
@@ -1006,8 +1089,12 @@ impl Worker {
|
||||
}
|
||||
|
||||
/// 6. 删除本地文件(DownloadOnly / Full / MirrorWcf — 远程删除触发的本地删除)
|
||||
/// AlbumDownload 跳过:远程删除不应删除本地相册照片
|
||||
async fn step_delete_local(&self, summary: &mut SyncSummary) {
|
||||
if matches!(self.config.sync_mode, SyncMode::UploadOnly) {
|
||||
if matches!(
|
||||
self.config.sync_mode,
|
||||
SyncMode::UploadOnly | SyncMode::AlbumUpload | SyncMode::AlbumDownload
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let tid = &self.task_id;
|
||||
@@ -1023,6 +1110,13 @@ impl Worker {
|
||||
Ok(_) => {
|
||||
summary.deleted_local += 1;
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(
|
||||
tid,
|
||||
&action.relative_path,
|
||||
"delete_local",
|
||||
"completed",
|
||||
)
|
||||
.await;
|
||||
tracing::info!("[{}] 删除本地: {}", tid, action.relative_path);
|
||||
let _ = self
|
||||
.db
|
||||
@@ -1064,7 +1158,10 @@ impl Worker {
|
||||
|
||||
/// 6.5 本地重命名(远程触发 → 本地执行 rename)
|
||||
async fn step_rename_local(&self, summary: &mut SyncSummary) {
|
||||
if matches!(self.config.sync_mode, SyncMode::UploadOnly) {
|
||||
if matches!(
|
||||
self.config.sync_mode,
|
||||
SyncMode::UploadOnly | SyncMode::AlbumUpload
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let tid = &self.task_id;
|
||||
@@ -1089,6 +1186,8 @@ impl Worker {
|
||||
Ok(_) => {
|
||||
summary.renamed += 1;
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(tid, &action.new_relative_path, "rename", "completed")
|
||||
.await;
|
||||
tracing::info!(
|
||||
"[{}] 本地重命名: {} -> {}",
|
||||
tid,
|
||||
@@ -1141,7 +1240,10 @@ impl Worker {
|
||||
|
||||
/// 6.6 本地移动(远程触发 → 本地执行 move)
|
||||
async fn step_move_local(&self, summary: &mut SyncSummary) {
|
||||
if matches!(self.config.sync_mode, SyncMode::UploadOnly) {
|
||||
if matches!(
|
||||
self.config.sync_mode,
|
||||
SyncMode::UploadOnly | SyncMode::AlbumUpload
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let tid = &self.task_id;
|
||||
@@ -1166,6 +1268,8 @@ impl Worker {
|
||||
Ok(_) => {
|
||||
summary.moved += 1;
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(tid, &action.new_relative_path, "move", "completed")
|
||||
.await;
|
||||
tracing::info!(
|
||||
"[{}] 本地移动: {} -> {}",
|
||||
tid,
|
||||
@@ -1224,7 +1328,8 @@ impl Worker {
|
||||
let tid = &self.task_id;
|
||||
|
||||
// 先标记抑制,30s 内 SSE 删除事件不会删本地文件
|
||||
self.suppress_paths.insert(relative_path.to_string(), std::time::Instant::now());
|
||||
self.suppress_paths
|
||||
.insert(relative_path.to_string(), std::time::Instant::now());
|
||||
|
||||
match self.api.delete_files(&[&remote_uri]).await {
|
||||
Ok(_) => tracing::info!("[{}] 上传失败清理-已删除远端碎片: {}", tid, remote_uri),
|
||||
@@ -1252,8 +1357,15 @@ impl Worker {
|
||||
match self.api.delete_files(&remote_uris).await {
|
||||
Ok(_) => {
|
||||
summary.deleted_remote += remote_uris.len() as u32;
|
||||
for _ in &remote_uris {
|
||||
for action in &self.plan.delete_remote {
|
||||
let _ = self.db.increment_task_completed(tid).await;
|
||||
self.emit_item_updated(
|
||||
tid,
|
||||
&action.relative_path,
|
||||
"delete_remote",
|
||||
"completed",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
for uri in &remote_uris {
|
||||
tracing::info!("[{}] 删除远程: {}", tid, uri);
|
||||
|
||||
Reference in New Issue
Block a user