diff --git a/build-output/minicloud-sync.exe b/build-output/minicloud-sync.exe new file mode 100755 index 0000000..ea44653 Binary files /dev/null and b/build-output/minicloud-sync.exe differ diff --git a/build-output/nsis/MiniCloud Sync_0.1.0_x64-setup.exe b/build-output/nsis/MiniCloud Sync_0.1.0_x64-setup.exe new file mode 100644 index 0000000..8d7f41b Binary files /dev/null and b/build-output/nsis/MiniCloud Sync_0.1.0_x64-setup.exe differ diff --git a/clients/desktop/src-tauri/src/lib.rs b/clients/desktop/src-tauri/src/lib.rs index 0515f1e..be19031 100644 --- a/clients/desktop/src-tauri/src/lib.rs +++ b/clients/desktop/src-tauri/src/lib.rs @@ -1,17 +1,26 @@ mod sync; use std::path::PathBuf; -use std::sync::Mutex; -use tauri::State; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tauri::{ + menu::{Menu, MenuItem}, + tray::TrayIconBuilder, + AppHandle, Emitter, Manager, State, +}; use sync::api::MiniCloudApi; use sync::engine::SyncEngine; +use sync::watcher::{FileWatcher, ChangeKind}; struct AppState { api: Mutex>, sync_engine: Mutex>, sync_dir: Mutex>, username: Mutex>, + watcher: Mutex>, + sync_running: Arc>, + locked_files: Mutex>, // file IDs we have locked } #[tauri::command] @@ -44,28 +53,36 @@ fn set_sync_dir(state: State<'_, AppState>, path: String) -> Result) -> Result, String> { +async fn start_sync(app: AppHandle, state: State<'_, AppState>) -> Result, String> { let api = state.api.lock().unwrap().clone() .ok_or("Nicht eingeloggt")?; let sync_dir = state.sync_dir.lock().unwrap().clone() .ok_or("Kein Sync-Ordner gesetzt")?; - let mut engine = SyncEngine::new(sync_dir, api); + // Full sync + let mut engine = SyncEngine::new(sync_dir.clone(), api.clone()); let log = engine.full_sync().await?; *state.sync_engine.lock().unwrap() = Some(engine); + + // Start file watcher + let watcher = FileWatcher::new(&sync_dir)?; + *state.watcher.lock().unwrap() = Some(watcher); + + // Start background auto-sync + watcher processing + start_background_sync(app, state.sync_running.clone(), api, sync_dir); + + let _ = app.emit("sync-status", "synced"); Ok(log) } #[tauri::command] async fn delta_sync(state: State<'_, AppState>) -> Result, String> { - // Extract engine from state, dropping the MutexGuard before .await let mut engine = { let mut guard = state.sync_engine.lock().unwrap(); guard.take().ok_or("Sync nicht gestartet")? }; let result = engine.delta_sync().await; - // Put engine back *state.sync_engine.lock().unwrap() = Some(engine); result } @@ -75,11 +92,15 @@ async fn get_status(state: State<'_, AppState>) -> Result) -> Result, file_id: i64) -> Result { + let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?; + api.lock_file(file_id, "Desktop Sync Client").await?; + state.locked_files.lock().unwrap().push(file_id); + Ok("Datei gesperrt".to_string()) +} + +#[tauri::command] +async fn unlock_file_cmd(state: State<'_, AppState>, file_id: i64) -> Result { + let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?; + api.unlock_file(file_id).await?; + state.locked_files.lock().unwrap().retain(|&id| id != file_id); + Ok("Datei entsperrt".to_string()) +} + +/// Background sync: auto-sync every 30s + process file watcher events + heartbeat for locks +fn start_background_sync( + app: AppHandle, + sync_running: Arc>, + api: MiniCloudApi, + sync_dir: PathBuf, +) { + // Auto-sync thread (delta sync every 30 seconds) + let app_sync = app.clone(); + let api_sync = api.clone(); + let sync_dir_clone = sync_dir.clone(); + let running = sync_running.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let mut engine = SyncEngine::new(sync_dir_clone, api_sync); + + loop { + std::thread::sleep(Duration::from_secs(30)); + + *running.lock().unwrap() = true; + let _ = app_sync.emit("sync-status", "syncing"); + + match rt.block_on(engine.delta_sync()) { + Ok(log) => { + if !log.is_empty() { + let _ = app_sync.emit("sync-log", log); + } + } + Err(e) => { + let _ = app_sync.emit("sync-error", e); + } + } + + *running.lock().unwrap() = false; + let _ = app_sync.emit("sync-status", "synced"); + } + }); + + // Heartbeat thread (every 60 seconds for locked files) + let app_hb = app.clone(); + let api_hb = api.clone(); + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + loop { + std::thread::sleep(Duration::from_secs(60)); + + let state = app_hb.state::(); + let locked = state.locked_files.lock().unwrap().clone(); + + for file_id in locked { + let _ = rt.block_on(api_hb.heartbeat(file_id)); + } + } + }); + + // File watcher processing thread + let app_watcher = app.clone(); + std::thread::spawn(move || { + loop { + std::thread::sleep(Duration::from_secs(2)); + + let state = app_watcher.state::(); + let watcher_guard = state.watcher.lock().unwrap(); + if let Some(watcher) = watcher_guard.as_ref() { + while let Ok(change) = watcher.receiver.try_recv() { + let rel_path = change.path.strip_prefix(&sync_dir) + .unwrap_or(&change.path) + .to_string_lossy() + .to_string(); + + let msg = match change.kind { + ChangeKind::Created => format!("Neu: {}", rel_path), + ChangeKind::Modified => format!("Geaendert: {}", rel_path), + ChangeKind::Deleted => format!("Geloescht: {}", rel_path), + }; + let _ = app_watcher.emit("file-change", msg); + } + } + } + }); +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_notification::init()) .manage(AppState { api: Mutex::new(None), sync_engine: Mutex::new(None), sync_dir: Mutex::new(None), username: Mutex::new(None), + watcher: Mutex::new(None), + sync_running: Arc::new(Mutex::new(false)), + locked_files: Mutex::new(Vec::new()), + }) + .setup(|app| { + // System Tray + let quit = MenuItem::with_id(app, "quit", "Beenden", true, None::<&str>)?; + let show = MenuItem::with_id(app, "show", "Oeffnen", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show, &quit])?; + + TrayIconBuilder::new() + .tooltip("Mini-Cloud Sync") + .menu(&menu) + .on_menu_event(|app, event| { + match event.id.as_ref() { + "quit" => std::process::exit(0), + "show" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + _ => {} + } + }) + .build(app)?; + + Ok(()) }) .invoke_handler(tauri::generate_handler![ login, @@ -108,6 +257,8 @@ pub fn run() { delta_sync, get_status, get_file_tree, + lock_file_cmd, + unlock_file_cmd, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/clients/desktop/src/App.vue b/clients/desktop/src/App.vue index 9bb2812..3176735 100644 --- a/clients/desktop/src/App.vue +++ b/clients/desktop/src/App.vue @@ -1,6 +1,7 @@