use crate::transfer::client::TransferClient; use crate::transfer::protocol::FolderSyncPairConfig; use chrono::Local; use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use std::time::Duration; use tokio::sync::{mpsc, RwLock}; use tracing::info; #[derive(Debug, Clone)] pub struct SyncStatusUpdate { pub pair_id: String, pub message: String, pub is_error: bool, pub timestamp: String, } pub struct FolderSyncEngine { client: Arc, pairs: Arc>>, status_tx: mpsc::UnboundedSender, } impl FolderSyncEngine { pub fn new( client: Arc, pairs: Vec, status_tx: mpsc::UnboundedSender, ) -> Self { Self { client, pairs: Arc::new(RwLock::new(pairs)), status_tx, } } pub fn pairs_handle(&self) -> Arc>> { self.pairs.clone() } pub async fn start_background_sync( self: Arc, peers_map: Arc>>, ) { info!("Folder auto-sync engine started."); loop { tokio::time::sleep(Duration::from_secs(8)).await; let pairs = self.pairs.read().await.clone(); for pair in pairs { if !pair.is_active { continue; } // Check if peer is currently online let peer_opt = { let peers = peers_map.read().await; peers.get(&pair.remote_peer_id).cloned() }; let peer = match peer_opt { Some(p) if p.is_online => p, _ => continue, }; let peer_url = peer.transfer_url(); let client = self.client.clone(); let status_tx = self.status_tx.clone(); let pair_id = pair.id.clone(); let pair_name = pair.name.clone(); let local_path = pair.local_path.clone(); let remote_folder_id = pair.remote_folder_id.clone(); tokio::spawn(async move { if let Err(e) = Self::sync_pair( &client, &peer_url, &local_path, &remote_folder_id, &pair_id, &pair_name, &status_tx, ) .await { let _ = status_tx.send(SyncStatusUpdate { pair_id, message: format!("Sync error: {}", e), is_error: true, timestamp: Local::now().format("%H:%M:%S").to_string(), }); } }); } } } async fn sync_pair( client: &TransferClient, peer_url: &str, local_path: &Path, remote_folder_id: &str, pair_id: &str, pair_name: &str, status_tx: &mpsc::UnboundedSender, ) -> Result<(), String> { if !local_path.exists() { let _ = tokio::fs::create_dir_all(local_path).await; } // Fetch remote directory tree let remote_tree = client .fetch_shared_tree(peer_url, remote_folder_id, None, None) .await?; // 1. Scan local files let mut local_files: HashMap = HashMap::new(); // rel_path -> (size, mtime) if let Ok(entries) = walkdir_local(local_path) { for (rel, size, mtime) in entries { local_files.insert(rel, (size, mtime)); } } // 2. Map remote files let mut remote_files: HashMap = HashMap::new(); for entry in &remote_tree.entries { if !entry.is_dir { remote_files.insert(entry.relative_path.clone(), entry.size); } } let mut synced_count = 0; // A. Upload newly created or changed local files to remote if !remote_tree.read_only { for (rel, (size, _)) in &local_files { let need_upload = match remote_files.get(rel) { Some(remote_size) => *remote_size != *size, None => true, // New local file }; if need_upload { let file_full_path = local_path.join(rel); let subpath = Path::new(rel).parent().and_then(|p| p.to_str()).unwrap_or(""); if let Ok(_) = client .upload_to_shared_folder( peer_url, remote_folder_id, subpath, &file_full_path, None, ) .await { synced_count += 1; } } } } // B. Download newly created remote files to local for (rel, remote_size) in &remote_files { let need_download = match local_files.get(rel) { Some((local_size, _)) => *local_size != *remote_size, None => true, // New remote file }; if need_download { let target_file_path = local_path.join(rel); if let Some(parent) = target_file_path.parent() { let _ = tokio::fs::create_dir_all(parent).await; } // Download file if let Ok(_) = client .download_shared_file(peer_url, remote_folder_id, rel, &target_file_path, None) .await { synced_count += 1; } } } if synced_count > 0 { let _ = status_tx.send(SyncStatusUpdate { pair_id: pair_id.to_string(), message: format!("Synced {} item(s) in '{}'", synced_count, pair_name), is_error: false, timestamp: Local::now().format("%H:%M:%S").to_string(), }); } Ok(()) } } fn walkdir_local(base: &Path) -> Result, std::io::Error> { let mut results = Vec::new(); for entry in walkdir::WalkDir::new(base).into_iter().filter_map(|e| e.ok()) { if entry.file_type().is_file() { if let Ok(rel) = entry.path().strip_prefix(base) { let rel_str = rel.to_string_lossy().replace('\\', "/"); let meta = entry.metadata().ok(); let size = meta.as_ref().map(|m| m.len()).unwrap_or(0); let mtime = meta .as_ref() .and_then(|m| m.modified().ok()) .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .map(|d| d.as_secs()) .unwrap_or(0); results.push((rel_str, size, mtime)); } } } Ok(results) }