b02daf1448
- 文件管理器分页加载、排序偏好、桌面表头排序、移动端排序菜单、Ctrl/Cmd+F搜索、Esc 关闭搜索、鼠标右滑返回上级。 - 移动端同步状态页、同步统计卡片、同步任务/累计统计事件、同步页布局更新。 - Android 相册同步默认路径改为 DCIM/Camera,下载路径改用 external_path。 - Rust sync-core 更新:Android 日志依赖、AlbumUpload/AlbumDownload、MirrorWcf 统计修复、Linux FUSE镜像挂载与读写同步支持。 - 依赖更新:external_path、fl_chart、pdfrx 升级,并修了 pdfrx 新版本的deprecated API。 - 新增文件包括:sort_options.dart、sync_page_android.dart、sync_stats_card.dart、platform/fuse.rs、sync_engine/fuse.rs。
849 lines
28 KiB
Rust
849 lines
28 KiB
Rust
use flutter_rust_bridge::frb;
|
||
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();
|
||
|
||
/// 全局日志级别重载句柄(支持运行时热修改)
|
||
static LOG_RELOAD_HANDLE: once_cell::sync::OnceCell<
|
||
tracing_subscriber::reload::Handle<tracing_subscriber::EnvFilter, tracing_subscriber::Registry>,
|
||
> = once_cell::sync::OnceCell::new();
|
||
|
||
// 内部类型 -> FFI 类型的转换函数
|
||
|
||
fn error_to_ffi(e: crate::errors::SyncError) -> SyncErrorFfi {
|
||
match e {
|
||
crate::errors::SyncError::Network(msg) => SyncErrorFfi::NetworkError { message: msg },
|
||
crate::errors::SyncError::DiskFull { needed, available } => {
|
||
SyncErrorFfi::DiskFull { needed, available }
|
||
}
|
||
crate::errors::SyncError::Auth(msg) => SyncErrorFfi::AuthError { message: msg },
|
||
crate::errors::SyncError::Conflict { count } => SyncErrorFfi::ConflictError { count },
|
||
crate::errors::SyncError::NotInitialized => SyncErrorFfi::NotInitialized,
|
||
_ => SyncErrorFfi::InternalError {
|
||
message: e.to_string(),
|
||
},
|
||
}
|
||
}
|
||
|
||
fn config_from_ffi(ffi: SyncConfigFfi) -> crate::models::SyncConfig {
|
||
use crate::models::{ConflictStrategy, SyncMode, WcfDeleteMode};
|
||
use std::path::PathBuf;
|
||
|
||
let sync_mode = match ffi.sync_mode.as_str() {
|
||
"upload_only" => SyncMode::UploadOnly,
|
||
"download_only" => SyncMode::DownloadOnly,
|
||
"album_upload" => SyncMode::AlbumUpload,
|
||
"album_download" => SyncMode::AlbumDownload,
|
||
"mirror_wcf" => SyncMode::MirrorWcf,
|
||
_ => SyncMode::Full,
|
||
};
|
||
|
||
let conflict_strategy = match ffi.conflict_strategy.as_str() {
|
||
"keep_local" => ConflictStrategy::KeepLocal,
|
||
"keep_remote" => ConflictStrategy::KeepRemote,
|
||
"newest_wins" => ConflictStrategy::NewestWins,
|
||
"largest_wins" => ConflictStrategy::LargestWins,
|
||
"manual" => ConflictStrategy::Manual,
|
||
_ => ConflictStrategy::KeepBoth,
|
||
};
|
||
|
||
let wcf_delete_mode = match ffi.wcf_delete_mode.as_str() {
|
||
"wcf_delete_sync_remote" => WcfDeleteMode::SyncRemote,
|
||
_ => WcfDeleteMode::LocalOnly,
|
||
};
|
||
|
||
let bandwidth_limit = if ffi.bandwidth_limit_kbps > 0 {
|
||
Some(ffi.bandwidth_limit_kbps * 1024)
|
||
} else {
|
||
None
|
||
};
|
||
|
||
crate::models::SyncConfig {
|
||
base_url: ffi.base_url,
|
||
access_token: ffi.access_token,
|
||
refresh_token: ffi.refresh_token,
|
||
local_root: PathBuf::from(&ffi.local_root),
|
||
remote_root: ffi.remote_root,
|
||
sync_mode,
|
||
conflict_strategy,
|
||
wcf_delete_mode,
|
||
max_concurrent_transfers: ffi.max_concurrent_transfers as usize,
|
||
bandwidth_limit,
|
||
excluded_paths: ffi.excluded_paths,
|
||
max_workers: ffi.max_workers as usize,
|
||
data_dir: PathBuf::from(&ffi.data_dir),
|
||
client_id: ffi.client_id,
|
||
}
|
||
}
|
||
|
||
fn config_to_ffi(c: &crate::models::SyncConfig) -> SyncConfigFfi {
|
||
use crate::models::{ConflictStrategy, SyncMode, WcfDeleteMode};
|
||
|
||
let sync_mode = match c.sync_mode {
|
||
SyncMode::Full => "full",
|
||
SyncMode::UploadOnly => "upload_only",
|
||
SyncMode::DownloadOnly => "download_only",
|
||
SyncMode::AlbumUpload => "album_upload",
|
||
SyncMode::AlbumDownload => "album_download",
|
||
SyncMode::MirrorWcf => "mirror_wcf",
|
||
};
|
||
|
||
let conflict_strategy = match c.conflict_strategy {
|
||
ConflictStrategy::KeepLocal => "keep_local",
|
||
ConflictStrategy::KeepRemote => "keep_remote",
|
||
ConflictStrategy::KeepBoth => "keep_both",
|
||
ConflictStrategy::NewestWins => "newest_wins",
|
||
ConflictStrategy::LargestWins => "largest_wins",
|
||
ConflictStrategy::Manual => "manual",
|
||
};
|
||
|
||
let wcf_delete_mode = match c.wcf_delete_mode {
|
||
WcfDeleteMode::LocalOnly => "wcf_delete_local_only",
|
||
WcfDeleteMode::SyncRemote => "wcf_delete_sync_remote",
|
||
};
|
||
|
||
SyncConfigFfi {
|
||
base_url: c.base_url.clone(),
|
||
access_token: c.access_token.clone(),
|
||
refresh_token: c.refresh_token.clone(),
|
||
local_root: c.local_root.to_string_lossy().to_string(),
|
||
remote_root: c.remote_root.clone(),
|
||
sync_mode: sync_mode.to_string(),
|
||
conflict_strategy: conflict_strategy.to_string(),
|
||
wcf_delete_mode: wcf_delete_mode.to_string(),
|
||
max_concurrent_transfers: c.max_concurrent_transfers as u32,
|
||
bandwidth_limit_kbps: c.bandwidth_limit.map(|b| b / 1024).unwrap_or(0),
|
||
excluded_paths: c.excluded_paths.clone(),
|
||
max_workers: c.max_workers as u32,
|
||
data_dir: c.data_dir.to_string_lossy().to_string(),
|
||
client_id: c.client_id.clone(),
|
||
log_level: String::new(),
|
||
}
|
||
}
|
||
|
||
fn status_to_ffi(s: crate::models::SyncStatusSnapshot) -> SyncStatusFfi {
|
||
let error_msg = if let crate::models::SyncState::Error { ref message } = s.state {
|
||
Some(message.clone())
|
||
} else {
|
||
s.error_message
|
||
};
|
||
|
||
let state = match s.state {
|
||
crate::models::SyncState::Idle => "idle".to_string(),
|
||
crate::models::SyncState::Initializing => "initializing".to_string(),
|
||
crate::models::SyncState::InitialSync { .. } => "initialSync".to_string(),
|
||
crate::models::SyncState::Continuous => "continuous".to_string(),
|
||
crate::models::SyncState::Paused => "paused".to_string(),
|
||
crate::models::SyncState::Error { .. } => "error".to_string(),
|
||
crate::models::SyncState::Stopped => "stopped".to_string(),
|
||
};
|
||
|
||
SyncStatusFfi {
|
||
state,
|
||
synced_files: s.synced_files,
|
||
total_files: s.total_files,
|
||
uploading_count: s.uploading_count,
|
||
downloading_count: s.downloading_count,
|
||
conflict_count: s.conflict_count,
|
||
error_count: s.error_count,
|
||
last_sync_time: s.last_sync_time,
|
||
error_message: error_msg,
|
||
}
|
||
}
|
||
|
||
fn summary_to_ffi(s: crate::models::SyncSummary) -> SyncSummaryFfi {
|
||
SyncSummaryFfi {
|
||
uploaded: s.uploaded,
|
||
downloaded: s.downloaded,
|
||
renamed: s.renamed,
|
||
moved: s.moved,
|
||
conflicts: s.conflicts,
|
||
failed: s.failed,
|
||
skipped: s.skipped,
|
||
deleted_local: s.deleted_local,
|
||
deleted_remote: s.deleted_remote,
|
||
duration_ms: s.duration_ms,
|
||
}
|
||
}
|
||
|
||
fn album_result_to_ffi(r: crate::models::CloudAlbumCheckResult) -> CloudAlbumCheckResultFfi {
|
||
CloudAlbumCheckResultFfi {
|
||
dcim_exists: r.dcim_exists,
|
||
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)
|
||
}
|
||
|
||
/// 内部:应用日志级别到 reload handle
|
||
fn apply_log_level(level: &str) {
|
||
if level.is_empty() {
|
||
return;
|
||
}
|
||
if let Some(handle) = LOG_RELOAD_HANDLE.get() {
|
||
let directive = format!("sync_core={}", level);
|
||
match handle.modify(|filter| {
|
||
*filter = tracing_subscriber::EnvFilter::new(&directive);
|
||
}) {
|
||
Ok(()) => eprintln!("[sync-core] 日志级别已切换为: {}", level),
|
||
Err(e) => eprintln!("[sync-core] 修改日志级别失败: {}", e),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== 生命周期 ==========
|
||
|
||
/// 初始化同步引擎
|
||
#[frb]
|
||
pub async fn init_sync_engine(config: SyncConfigFfi) -> Result<(), SyncErrorFfi> {
|
||
eprintln!("[FFI] init_sync_engine ← mode={}, conflict={}, wcf_delete={}, concurrent={}, bandwidth={}kbps, log_level={}",
|
||
config.sync_mode, config.conflict_strategy, config.wcf_delete_mode, config.max_concurrent_transfers,
|
||
config.bandwidth_limit_kbps, config.log_level);
|
||
|
||
// 确保本地同步目录存在
|
||
let local_root = std::path::PathBuf::from(&config.local_root);
|
||
if !local_root.exists() {
|
||
std::fs::create_dir_all(&local_root).map_err(|e| SyncErrorFfi::InternalError {
|
||
message: format!("无法创建同步目录: {}", e),
|
||
})?;
|
||
}
|
||
|
||
// 确保程序数据目录存在
|
||
let data_dir = std::path::PathBuf::from(&config.data_dir);
|
||
let db_dir = data_dir.join("sync_core").join("datas");
|
||
let log_dir = data_dir.join("sync_core").join("logs");
|
||
if !db_dir.exists() {
|
||
std::fs::create_dir_all(&db_dir).map_err(|e| SyncErrorFfi::InternalError {
|
||
message: format!("无法创建数据库目录: {}", e),
|
||
})?;
|
||
}
|
||
if !log_dir.exists() {
|
||
std::fs::create_dir_all(&log_dir).map_err(|e| SyncErrorFfi::InternalError {
|
||
message: format!("无法创建日志目录: {}", e),
|
||
})?;
|
||
}
|
||
|
||
// 初始化 tracing 日志:输出到程序数据目录的 logs 和 stderr
|
||
let log_path = log_dir.join("sync_log.txt");
|
||
eprintln!("[sync-core] 日志文件: {}", log_path.display());
|
||
|
||
let log_file = std::fs::OpenOptions::new()
|
||
.create(true)
|
||
.append(true)
|
||
.open(&log_path)
|
||
.ok();
|
||
|
||
if log_file.is_none() {
|
||
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;
|
||
use tracing_subscriber::util::SubscriberInitExt;
|
||
|
||
let filter = tracing_subscriber::EnvFilter::from_default_env()
|
||
.add_directive("sync_core=debug".parse().unwrap());
|
||
|
||
let (reload_filter, reload_handle) =
|
||
tracing_subscriber::reload::Layer::<_, tracing_subscriber::Registry>::new(filter);
|
||
LOG_RELOAD_HANDLE.set(reload_handle).ok();
|
||
|
||
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))
|
||
.try_init();
|
||
} else {
|
||
let _ = registry
|
||
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
|
||
.try_init();
|
||
}
|
||
}
|
||
|
||
// 提取配置信息用于日志(在 move 之前)
|
||
let log_sync_mode = config.sync_mode.clone();
|
||
let log_conflict_strategy = config.conflict_strategy.clone();
|
||
let log_max_concurrent = config.max_concurrent_transfers;
|
||
let log_bandwidth = config.bandwidth_limit_kbps;
|
||
let log_level = config.log_level.clone();
|
||
|
||
let engine = SyncEngine::new(config_from_ffi(config))
|
||
.await
|
||
.map_err(error_to_ffi)?;
|
||
|
||
ENGINE
|
||
.set(Arc::new(engine))
|
||
.map_err(|_| SyncErrorFfi::InternalError {
|
||
message: "引擎已初始化".to_string(),
|
||
})?;
|
||
|
||
tracing::info!("同步引擎初始化完成, 日志文件: {}", log_path.display());
|
||
tracing::info!(
|
||
"配置: 模式={}, 冲突策略={}, 并发={}, 带宽限制={}kbps",
|
||
log_sync_mode,
|
||
log_conflict_strategy,
|
||
log_max_concurrent,
|
||
log_bandwidth,
|
||
);
|
||
if log_bandwidth > 0 {
|
||
tracing::info!("仅对下载限速生效, 由于Cloudreve实现原因, 上传限速无法生效");
|
||
}
|
||
|
||
// 应用配置中的日志级别(热修改覆盖默认 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(())
|
||
}
|
||
|
||
/// 销毁同步引擎
|
||
#[frb]
|
||
pub async fn dispose_sync_engine() -> Result<(), SyncErrorFfi> {
|
||
tracing::debug!("[FFI] dispose_sync_engine ←");
|
||
let engine = get_engine()?;
|
||
engine.stop().await.map_err(error_to_ffi)?;
|
||
|
||
#[cfg(feature = "windows-cfapi")]
|
||
{
|
||
engine.cleanup_wcf();
|
||
}
|
||
|
||
#[cfg(feature = "linux-fuse")]
|
||
{
|
||
engine.cleanup_fuse();
|
||
}
|
||
|
||
tracing::info!("同步引擎已停止");
|
||
Ok(())
|
||
}
|
||
|
||
/// 进程退出前同步清理(WCF/FUSE 模式下必须调用,确保占位符释放和挂载点卸载)
|
||
/// 此函数是同步的,不依赖 tokio runtime,可安全在 exit(0) 前调用
|
||
#[frb]
|
||
pub fn sync_shutdown() -> Result<(), SyncErrorFfi> {
|
||
tracing::debug!("[FFI] sync_shutdown ←");
|
||
#[cfg(feature = "windows-cfapi")]
|
||
{
|
||
let engine = match ENGINE.get() {
|
||
Some(e) => e,
|
||
None => return Ok(()),
|
||
};
|
||
engine.cleanup_wcf();
|
||
}
|
||
#[cfg(feature = "linux-fuse")]
|
||
{
|
||
let engine = match ENGINE.get() {
|
||
Some(e) => e,
|
||
None => return Ok(()),
|
||
};
|
||
engine.cleanup_fuse();
|
||
}
|
||
tracing::info!("同步引擎已同步清理");
|
||
Ok(())
|
||
}
|
||
|
||
// ========== 同步控制 ==========
|
||
|
||
/// 执行初始全量同步
|
||
#[frb]
|
||
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
|
||
.map(|s| {
|
||
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)
|
||
}
|
||
|
||
/// 启动持续同步(后台运行,立即返回)
|
||
#[frb]
|
||
pub async fn start_continuous_sync() -> Result<(), SyncErrorFfi> {
|
||
tracing::debug!("[FFI] start_continuous_sync ←");
|
||
let engine = get_engine()?;
|
||
let engine = engine.clone();
|
||
tokio::spawn(async move {
|
||
if let Err(e) = engine.run_continuous().await {
|
||
tracing::error!("持续同步异常退出: {}", e);
|
||
}
|
||
});
|
||
tracing::debug!("[FFI] start_continuous_sync → spawned");
|
||
Ok(())
|
||
}
|
||
|
||
/// 停止同步
|
||
#[frb]
|
||
pub async fn stop_sync() -> Result<(), SyncErrorFfi> {
|
||
tracing::debug!("[FFI] stop_sync ←");
|
||
let engine = get_engine()?;
|
||
engine.stop().await.map_err(error_to_ffi)
|
||
}
|
||
|
||
/// 暂停同步
|
||
#[frb]
|
||
pub async fn pause_sync() -> Result<(), SyncErrorFfi> {
|
||
tracing::debug!("[FFI] pause_sync ←");
|
||
let engine = get_engine()?;
|
||
engine.pause().await.map_err(error_to_ffi)
|
||
}
|
||
|
||
/// 恢复同步
|
||
#[frb]
|
||
pub async fn resume_sync() -> Result<(), SyncErrorFfi> {
|
||
tracing::debug!("[FFI] resume_sync ←");
|
||
let engine = get_engine()?;
|
||
engine.resume().await.map_err(error_to_ffi)
|
||
}
|
||
|
||
/// 强制同步(重新扫描全量差异)
|
||
#[frb]
|
||
pub async fn force_sync() -> Result<SyncSummaryFfi, SyncErrorFfi> {
|
||
tracing::debug!("[FFI] force_sync ←");
|
||
let engine = get_engine()?;
|
||
engine
|
||
.force_sync()
|
||
.await
|
||
.map(|s| {
|
||
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 → (可选)清空本地目录 → 回到初始状态
|
||
#[frb]
|
||
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(delete_local_files)
|
||
.await
|
||
.map_err(error_to_ffi)
|
||
}
|
||
|
||
// ========== 状态查询 ==========
|
||
|
||
/// 获取同步状态快照
|
||
#[frb]
|
||
pub async fn get_sync_status() -> Result<SyncStatusFfi, SyncErrorFfi> {
|
||
let engine = get_engine()?;
|
||
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))
|
||
}
|
||
|
||
/// 获取活跃 Worker 数量
|
||
#[frb]
|
||
pub async fn get_active_worker_count() -> Result<u32, SyncErrorFfi> {
|
||
let engine = get_engine()?;
|
||
let count = engine.active_worker_count();
|
||
tracing::trace!("[FFI] get_active_worker_count → {}", count);
|
||
Ok(count)
|
||
}
|
||
|
||
/// 获取同步配置
|
||
#[frb]
|
||
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
|
||
);
|
||
Ok(config_to_ffi(&c))
|
||
}
|
||
|
||
/// 更新同步配置
|
||
#[frb]
|
||
pub async fn update_sync_config(config: SyncConfigFfi) -> Result<(), SyncErrorFfi> {
|
||
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)
|
||
}
|
||
|
||
// ========== Token 管理 ==========
|
||
|
||
/// Dart 推送新 Token 给 Rust
|
||
#[frb]
|
||
pub async fn update_tokens(access_token: String) -> Result<(), SyncErrorFfi> {
|
||
tracing::debug!("[FFI] update_tokens ← token_len={}", access_token.len());
|
||
let engine = get_engine()?;
|
||
engine.update_access_token(access_token).await;
|
||
Ok(())
|
||
}
|
||
|
||
// ========== Windows 专用 ==========
|
||
|
||
/// 水合文件(Windows 按需下载)
|
||
#[frb]
|
||
pub async fn hydrate_file(local_path: String) -> Result<(), SyncErrorFfi> {
|
||
tracing::debug!("[FFI] hydrate_file ← path={}", local_path);
|
||
let engine = get_engine()?;
|
||
engine.hydrate_file(&local_path).await.map_err(error_to_ffi)
|
||
}
|
||
|
||
// ========== Android 专用 ==========
|
||
|
||
/// 同步相册到云端
|
||
#[frb]
|
||
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
|
||
);
|
||
let engine = get_engine()?;
|
||
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> {
|
||
tracing::debug!("[FFI] check_cloud_album_dirs ← uri={}", base_uri);
|
||
let engine = get_engine()?;
|
||
engine
|
||
.check_album_dirs(&base_uri)
|
||
.await
|
||
.map(|r| {
|
||
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)
|
||
}
|
||
|
||
/// 在云端创建 DCIM/Pictures 目录
|
||
#[frb]
|
||
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)
|
||
}
|
||
|
||
// ========== 事件推送 ==========
|
||
|
||
/// 注册 Rust→Dart 事件推送通道
|
||
#[frb]
|
||
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()?;
|
||
// 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(())
|
||
}
|
||
|
||
// ========== 日志级别 ==========
|
||
|
||
/// 运行时热修改日志级别(立即生效,无需重启)
|
||
#[frb]
|
||
pub fn set_sync_log_level(level: String) -> Result<(), SyncErrorFfi> {
|
||
eprintln!("[FFI] set_sync_log_level ← level={}", level);
|
||
let valid_levels = ["error", "warn", "info", "debug", "trace"];
|
||
let level_lower = level.to_lowercase();
|
||
if !valid_levels.contains(&level_lower.as_str()) {
|
||
return Err(SyncErrorFfi::InternalError {
|
||
message: format!("无效的日志级别: {}, 可选: {:?}", level, valid_levels),
|
||
});
|
||
}
|
||
|
||
if LOG_RELOAD_HANDLE.get().is_none() {
|
||
return Err(SyncErrorFfi::NotInitialized);
|
||
}
|
||
|
||
apply_log_level(&level_lower);
|
||
Ok(())
|
||
}
|
||
|
||
// ========== 任务查询 ==========
|
||
|
||
/// 获取活跃的同步任务列表
|
||
#[frb]
|
||
pub async fn get_active_tasks() -> Result<Vec<SyncTaskFfi>, SyncErrorFfi> {
|
||
let engine = get_engine()?;
|
||
let tasks = engine.get_active_tasks().await.map_err(error_to_ffi)?;
|
||
tracing::trace!("[FFI] get_active_tasks → count={}", tasks.len());
|
||
Ok(tasks.into_iter().map(task_to_ffi).collect())
|
||
}
|
||
|
||
/// 获取最近同步任务列表
|
||
#[frb]
|
||
pub async fn get_recent_tasks(limit: u32) -> Result<Vec<SyncTaskFfi>, SyncErrorFfi> {
|
||
tracing::trace!("[FFI] get_recent_tasks ← limit={}", limit);
|
||
let engine = get_engine()?;
|
||
let tasks = engine.get_recent_tasks(limit).await.map_err(error_to_ffi)?;
|
||
tracing::trace!("[FFI] get_recent_tasks → count={}", tasks.len());
|
||
Ok(tasks.into_iter().map(task_to_ffi).collect())
|
||
}
|
||
|
||
/// 获取任务详情(任务项列表)
|
||
#[frb]
|
||
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)?;
|
||
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
|
||
);
|
||
let engine = get_engine()?;
|
||
let model_filter = crate::models::TaskItemFilter {
|
||
task_id: filter.task_id,
|
||
relative_path_contains: filter.relative_path_contains,
|
||
action_type: filter.action_type,
|
||
status: filter.status,
|
||
limit: filter.limit.max(1).min(1000),
|
||
offset: filter.offset,
|
||
};
|
||
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,
|
||
trigger: t.trigger.as_str().to_string(),
|
||
total_count: t.total_count,
|
||
completed_count: t.completed_count,
|
||
failed_count: t.failed_count,
|
||
status: t.status.as_str().to_string(),
|
||
created_at: t.created_at,
|
||
updated_at: t.updated_at,
|
||
finished_at: t.finished_at,
|
||
}
|
||
}
|
||
|
||
fn task_item_to_ffi(i: crate::models::SyncTaskItem) -> SyncTaskItemFfi {
|
||
SyncTaskItemFfi {
|
||
id: i.id,
|
||
task_id: i.task_id,
|
||
relative_path: i.relative_path,
|
||
action_type: i.action_type.as_str().to_string(),
|
||
status: i.status.as_str().to_string(),
|
||
file_size: i.file_size,
|
||
error_message: i.error_message,
|
||
created_at: i.created_at,
|
||
updated_at: i.updated_at,
|
||
}
|
||
}
|