主要合入内容:

- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、Ctrl/Cmd+F搜索、Esc 关闭搜索、鼠标右滑返回上级。
- 移动端同步状态页、同步统计卡片、同步任务/累计统计事件、同步页布局更新。
- Android 相册同步默认路径改为 DCIM/Camera,下载路径改用 external_path。
- Rust sync-core 更新:Android 日志依赖、AlbumUpload/AlbumDownload、MirrorWcf 统计修复、Linux FUSE镜像挂载与读写同步支持。

- 依赖更新:external_path、fl_chart、pdfrx 升级,并修了 pdfrx 新版本的deprecated API。
- 新增文件包括:sort_options.dart、sync_page_android.dart、sync_stats_card.dart、platform/fuse.rs、sync_engine/fuse.rs。
This commit is contained in:
2026-06-04 07:11:43 +08:00
parent 5ee6ba1e28
commit b02daf1448
56 changed files with 7589 additions and 1200 deletions
+268 -42
View File
@@ -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,