botapp/src/desktop/sync.rs

383 lines
11 KiB
Rust
Raw Normal View History

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<Option<Child>> = Mutex::new(None);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncStatus {
pub status: String,
pub is_running: bool,
pub last_sync: Option<String>,
pub files_synced: u64,
pub bytes_transferred: u64,
pub current_file: Option<String>,
pub error: Option<String>,
}
#[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<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SyncMode {
Push,
Pull,
Bisync,
}
impl Default for SyncConfig {
fn default() -> Self {
2025-12-21 23:40:38 -03:00
let local_path = dirs::home_dir().map_or_else(
|| "~/GeneralBots".to_string(),
|p| p.join("GeneralBots").to_string_lossy().to_string(),
);
Self {
2025-12-21 23:40:38 -03:00
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]
2025-12-21 23:40:38 -03:00
#[must_use]
pub fn get_sync_status() -> SyncStatus {
2025-12-21 23:40:38 -03:00
let process_guard = RCLONE_PROCESS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let is_running = process_guard.is_some();
2025-12-21 23:40:38 -03:00
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,
}
}
2025-12-24 09:29:25 -03:00
/// # Errors
/// Returns an error if sync is already running, directory creation fails, or rclone is not found.
#[tauri::command]
2025-12-21 23:40:38 -03:00
pub fn start_sync(window: Window, config: Option<SyncConfig>) -> Result<SyncStatus, String> {
let config = config.unwrap_or_default();
{
2025-12-21 23:40:38 -03:00
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)
2025-12-21 23:40:38 -03:00
.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 {
2025-12-21 23:40:38 -03:00
format!("Failed to start rclone: {e}")
}
})?;
{
2025-12-21 23:40:38 -03:00
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 || {
2025-12-21 23:40:38 -03:00
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,
})
}
2025-12-24 09:29:25 -03:00
/// # Errors
/// Returns an error if no sync process is currently running.
#[tauri::command]
pub fn stop_sync() -> Result<SyncStatus, String> {
2025-12-21 23:40:38 -03:00
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();
2025-12-21 23:40:38 -03:00
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,
}
})
}
2025-12-24 09:29:25 -03:00
/// # Errors
/// Returns an error if rclone configuration fails.
#[tauri::command]
pub fn configure_remote(
2025-12-21 23:40:38 -03:00
remote_name: &str,
endpoint: &str,
access_key: &str,
secret_key: &str,
bucket: &str,
) -> Result<(), String> {
let output = Command::new("rclone")
.args([
"config",
"create",
2025-12-21 23:40:38 -03:00
remote_name,
"s3",
"provider",
"Minio",
"endpoint",
2025-12-21 23:40:38 -03:00
endpoint,
"access_key_id",
2025-12-21 23:40:38 -03:00
access_key,
"secret_access_key",
2025-12-21 23:40:38 -03:00
secret_key,
"acl",
"private",
])
.output()
2025-12-21 23:40:38 -03:00
.map_err(|e| format!("Failed to configure rclone: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
2025-12-21 23:40:38 -03:00
return Err(format!("rclone config failed: {stderr}"));
}
let _ = Command::new("rclone")
2025-12-21 23:40:38 -03:00
.args(["config", "update", remote_name, "bucket", bucket])
.output();
Ok(())
}
2025-12-24 09:29:25 -03:00
/// # Errors
/// Returns an error if rclone is not installed or the version check fails.
#[tauri::command]
pub fn check_rclone_installed() -> Result<String, String> {
let output = Command::new("rclone")
.arg("version")
.output()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
"rclone not installed".to_string()
} else {
2025-12-21 23:40:38 -03:00
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())
}
}
2025-12-24 09:29:25 -03:00
/// # Errors
/// Returns an error if listing rclone remotes fails.
#[tauri::command]
pub fn list_remotes() -> Result<Vec<String>, String> {
let output = Command::new("rclone")
.args(["listremotes"])
.output()
2025-12-21 23:40:38 -03:00
.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]
2025-12-21 23:40:38 -03:00
#[must_use]
pub fn get_sync_folder() -> String {
2025-12-21 23:40:38 -03:00
dirs::home_dir().map_or_else(
|| "~/GeneralBots".to_string(),
|p| p.join("GeneralBots").to_string_lossy().to_string(),
)
}
2025-12-24 09:29:25 -03:00
/// # Errors
/// Returns an error if the directory cannot be created or the path is not a directory.
#[tauri::command]
2025-12-21 23:40:38 -03:00
pub fn set_sync_folder(path: &str) -> Result<(), String> {
let path = PathBuf::from(path);
if !path.exists() {
2025-12-21 23:40:38 -03:00
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(())
}
2025-12-21 23:40:38 -03:00
fn monitor_sync_process(window: &Window) {
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
2025-12-21 23:40:38 -03:00
let mut process_guard = RCLONE_PROCESS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
2025-12-21 23:40:38 -03:00
let status_opt = if let Some(ref mut child) = *process_guard {
match child.try_wait() {
2025-12-21 23:40:38 -03:00
Ok(Some(exit_status)) => {
let success = exit_status.success();
*process_guard = None;
2025-12-21 23:40:38 -03:00
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 {
2025-12-21 23:40:38 -03:00
Some(format!("Exit code: {:?}", exit_status.code()))
},
};
2025-12-21 23:40:38 -03:00
if success {
let _ = window.emit("sync_completed", &status);
} else {
let _ = window.emit("sync_error", &status);
}
return;
}
Ok(None) => {
2025-12-21 23:40:38 -03:00
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,
2025-12-21 23:40:38 -03:00
})
}
Err(e) => {
*process_guard = None;
2025-12-21 23:40:38 -03:00
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,
2025-12-21 23:40:38 -03:00
error: Some(format!("Process error: {e}")),
};
let _ = window.emit("sync_error", &status);
2025-12-21 23:40:38 -03:00
return;
}
}
} else {
2025-12-21 23:40:38 -03:00
drop(process_guard);
return;
};
if let Some(status) = status_opt {
let _ = window.emit("sync_progress", &status);
}
}
}