use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; use std::sync::Mutex; use tauri::{Emitter, Window}; static RCLONE_PROCESS: Mutex> = Mutex::new(None); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SyncStatus { pub status: String, pub is_running: bool, pub last_sync: Option, pub files_synced: u64, pub bytes_transferred: u64, pub current_file: Option, pub error: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SyncConfig { pub local_path: String, pub remote_name: String, pub remote_path: String, pub sync_mode: SyncMode, pub exclude_patterns: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SyncMode { Push, Pull, Bisync, } impl Default for SyncConfig { fn default() -> Self { let local_path = dirs::home_dir().map_or_else( || "~/GeneralBots".to_string(), |p| p.join("GeneralBots").to_string_lossy().to_string(), ); Self { local_path, remote_name: "gbdrive".to_string(), remote_path: "/".to_string(), sync_mode: SyncMode::Bisync, exclude_patterns: vec![ ".DS_Store".to_string(), "Thumbs.db".to_string(), "*.tmp".to_string(), ".git/**".to_string(), ], } } } #[tauri::command] #[must_use] pub fn get_sync_status() -> SyncStatus { let process_guard = RCLONE_PROCESS .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let is_running = process_guard.is_some(); drop(process_guard); SyncStatus { status: if is_running { "syncing".to_string() } else { "idle".to_string() }, is_running, last_sync: None, files_synced: 0, bytes_transferred: 0, current_file: None, error: None, } } /// # Errors /// Returns an error if sync is already running, directory creation fails, or rclone is not found. #[tauri::command] pub fn start_sync(window: Window, config: Option) -> Result { let config = config.unwrap_or_default(); { let process_guard = RCLONE_PROCESS .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if process_guard.is_some() { return Err("Sync already running".to_string()); } } let local_path = PathBuf::from(&config.local_path); if !local_path.exists() { std::fs::create_dir_all(&local_path) .map_err(|e| format!("Failed to create local directory: {e}"))?; } let mut cmd = Command::new("rclone"); match config.sync_mode { SyncMode::Push => { cmd.arg("sync"); cmd.arg(&config.local_path); cmd.arg(format!("{}:{}", config.remote_name, config.remote_path)); } SyncMode::Pull => { cmd.arg("sync"); cmd.arg(format!("{}:{}", config.remote_name, config.remote_path)); cmd.arg(&config.local_path); } SyncMode::Bisync => { cmd.arg("bisync"); cmd.arg(&config.local_path); cmd.arg(format!("{}:{}", config.remote_name, config.remote_path)); cmd.arg("--resync"); } } cmd.arg("--progress").arg("--verbose").arg("--checksum"); for pattern in &config.exclude_patterns { cmd.arg("--exclude").arg(pattern); } cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); let child = cmd.spawn().map_err(|e| { if e.kind() == std::io::ErrorKind::NotFound { "rclone not found. Please install rclone: https://rclone.org/install/".to_string() } else { format!("Failed to start rclone: {e}") } })?; { let mut process_guard = RCLONE_PROCESS .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); *process_guard = Some(child); } let _ = window.emit("sync_started", ()); std::thread::spawn(move || { monitor_sync_process(&window); }); Ok(SyncStatus { status: "syncing".to_string(), is_running: true, last_sync: None, files_synced: 0, bytes_transferred: 0, current_file: None, error: None, }) } /// # Errors /// Returns an error if no sync process is currently running. #[tauri::command] pub fn stop_sync() -> Result { let mut process_guard = RCLONE_PROCESS .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); process_guard .take() .ok_or_else(|| "No sync process running".to_string()) .map(|mut child| { let _ = child.kill(); std::thread::sleep(std::time::Duration::from_millis(500)); let _ = child.wait(); SyncStatus { status: "stopped".to_string(), is_running: false, last_sync: Some(chrono::Utc::now().to_rfc3339()), files_synced: 0, bytes_transferred: 0, current_file: None, error: None, } }) } /// # Errors /// Returns an error if rclone configuration fails. #[tauri::command] pub fn configure_remote( remote_name: &str, endpoint: &str, access_key: &str, secret_key: &str, bucket: &str, ) -> Result<(), String> { let output = Command::new("rclone") .args([ "config", "create", remote_name, "s3", "provider", "Minio", "endpoint", endpoint, "access_key_id", access_key, "secret_access_key", secret_key, "acl", "private", ]) .output() .map_err(|e| format!("Failed to configure rclone: {e}"))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("rclone config failed: {stderr}")); } let _ = Command::new("rclone") .args(["config", "update", remote_name, "bucket", bucket]) .output(); Ok(()) } /// # Errors /// Returns an error if rclone is not installed or the version check fails. #[tauri::command] pub fn check_rclone_installed() -> Result { let output = Command::new("rclone") .arg("version") .output() .map_err(|e| { if e.kind() == std::io::ErrorKind::NotFound { "rclone not installed".to_string() } else { format!("Error checking rclone: {e}") } })?; if output.status.success() { let version = String::from_utf8_lossy(&output.stdout); let first_line = version.lines().next().unwrap_or("unknown"); Ok(first_line.to_string()) } else { Err("rclone check failed".to_string()) } } /// # Errors /// Returns an error if listing rclone remotes fails. #[tauri::command] pub fn list_remotes() -> Result, String> { let output = Command::new("rclone") .args(["listremotes"]) .output() .map_err(|e| format!("Failed to list remotes: {e}"))?; if output.status.success() { let remotes = String::from_utf8_lossy(&output.stdout); Ok(remotes .lines() .map(|s| s.trim_end_matches(':').to_string()) .filter(|s| !s.is_empty()) .collect()) } else { Err("Failed to list rclone remotes".to_string()) } } #[tauri::command] #[must_use] pub fn get_sync_folder() -> String { dirs::home_dir().map_or_else( || "~/GeneralBots".to_string(), |p| p.join("GeneralBots").to_string_lossy().to_string(), ) } /// # Errors /// Returns an error if the directory cannot be created or the path is not a directory. #[tauri::command] pub fn set_sync_folder(path: &str) -> Result<(), String> { let path = PathBuf::from(path); if !path.exists() { std::fs::create_dir_all(&path).map_err(|e| format!("Failed to create directory: {e}"))?; } if !path.is_dir() { return Err("Path is not a directory".to_string()); } Ok(()) } fn monitor_sync_process(window: &Window) { loop { std::thread::sleep(std::time::Duration::from_secs(1)); let mut process_guard = RCLONE_PROCESS .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let status_opt = if let Some(ref mut child) = *process_guard { match child.try_wait() { Ok(Some(exit_status)) => { let success = exit_status.success(); *process_guard = None; drop(process_guard); let status = SyncStatus { status: if success { "completed".to_string() } else { "error".to_string() }, is_running: false, last_sync: Some(chrono::Utc::now().to_rfc3339()), files_synced: 0, bytes_transferred: 0, current_file: None, error: if success { None } else { Some(format!("Exit code: {:?}", exit_status.code())) }, }; if success { let _ = window.emit("sync_completed", &status); } else { let _ = window.emit("sync_error", &status); } return; } Ok(None) => { drop(process_guard); Some(SyncStatus { status: "syncing".to_string(), is_running: true, last_sync: None, files_synced: 0, bytes_transferred: 0, current_file: None, error: None, }) } Err(e) => { *process_guard = None; drop(process_guard); let status = SyncStatus { status: "error".to_string(), is_running: false, last_sync: Some(chrono::Utc::now().to_rfc3339()), files_synced: 0, bytes_transferred: 0, current_file: None, error: Some(format!("Process error: {e}")), }; let _ = window.emit("sync_error", &status); return; } } } else { drop(process_guard); return; }; if let Some(status) = status_opt { let _ = window.emit("sync_progress", &status); } } }