From 505545f26c8758f64ba2a01f5b27a6078eed5037 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sun, 12 Apr 2026 00:44:57 +0200 Subject: [PATCH] feat: Watcher triggert sofort Sync + Offline-Markierung pro Datei MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sofort-Sync statt 30s-Polling: - Filesystem-Watcher erkennt lokale Aenderungen sofort - 3 Sekunden Debounce (wartet ob noch mehr kommt) - Dann sofortiger Sync-Trigger statt auf den naechsten 30s-Zyklus zu warten - .cloud-Dateien werden vom Watcher ignoriert (kein Loop) - Fallback: alle 60s Sync auch ohne Aenderungen (Server-Aenderungen holen) - UI zeigt "→ Sync ausgeloest" bei Watcher-Trigger Offline-Markierung: - mark_offline: .cloud -> echte Datei runterladen, bleibt permanent lokal - unmark_offline: echte Datei -> zurueck zu .cloud Platzhalter - Offline-Dateien werden bei jedem Sync automatisch aktualisiert (Checksum-Vergleich in sync_virtual) Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/desktop/src-tauri/src/lib.rs | 103 +++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/clients/desktop/src-tauri/src/lib.rs b/clients/desktop/src-tauri/src/lib.rs index b8b0be8..ee04d25 100644 --- a/clients/desktop/src-tauri/src/lib.rs +++ b/clients/desktop/src-tauri/src/lib.rs @@ -216,6 +216,55 @@ async fn unlock_file_cmd(state: State<'_, AppState>, file_id: i64) -> Result, cloud_path: String) -> Result { + // Read .cloud placeholder, download real file, keep it local + let path = PathBuf::from(&cloud_path); + let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; + let placeholder: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?; + let file_id = placeholder.get("id").and_then(|v| v.as_i64()).ok_or("Keine ID")?; + let file_name = placeholder.get("name").and_then(|v| v.as_str()).unwrap_or("file"); + + let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?; + let real_path = path.parent().unwrap().join(file_name); + + // Download + api.download_file(file_id, &real_path).await?; + + // Remove .cloud placeholder (real file stays permanently) + std::fs::remove_file(&path).ok(); + + Ok(format!("{} ist jetzt offline verfuegbar", file_name)) +} + +#[tauri::command] +fn unmark_offline(cloud_path: String) -> Result { + // Convert real file back to .cloud placeholder + let path = PathBuf::from(&cloud_path); + if !path.exists() { return Err("Datei nicht gefunden".to_string()); } + + let name = path.file_name().unwrap().to_string_lossy().to_string(); + let size = std::fs::metadata(&path).map(|m| m.len() as i64).unwrap_or(0); + let checksum = sync::engine::compute_file_hash(&path); + + let placeholder = serde_json::json!({ + "id": 0, // will be updated on next sync + "name": name, + "size": size, + "checksum": checksum, + "updated_at": chrono::Utc::now().to_rfc3339(), + "server_path": "", + }); + + let cloud_path = path.parent().unwrap().join(format!("{}.cloud", name)); + std::fs::write(&cloud_path, serde_json::to_string_pretty(&placeholder).unwrap()).ok(); + std::fs::remove_file(&path).ok(); + + Ok(format!("{} ist nicht mehr offline", name)) +} + // --- Background Threads --- fn start_background_sync( @@ -224,19 +273,41 @@ fn start_background_sync( api: MiniCloudApi, paths: Vec, ) { - // Auto-sync every 30 seconds + // Shared flag: watcher sets true when changes detected, sync thread checks it + let watcher_triggered = Arc::new(Mutex::new(false)); + + // Main sync thread: syncs on watcher trigger OR every 60s as fallback let app_sync = app.clone(); let api_sync = api.clone(); let paths_sync = paths.clone(); + let trigger_sync = watcher_triggered.clone(); std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); let mut engine = SyncEngine::new(api_sync); engine.sync_paths = paths_sync; + let mut idle_counter = 0u32; loop { - std::thread::sleep(Duration::from_secs(30)); + // Check every 2 seconds if watcher triggered + std::thread::sleep(Duration::from_secs(2)); + idle_counter += 2; + let should_sync = { + let mut triggered = trigger_sync.lock().unwrap(); + if *triggered { + *triggered = false; + true + } else { + // Fallback: sync every 60 seconds even without changes + idle_counter >= 60 + } + }; + + if !should_sync { continue; } + idle_counter = 0; + + // Run sync *sync_running.lock().unwrap() = true; let _ = app_sync.emit("sync-status", "syncing"); @@ -269,25 +340,47 @@ fn start_background_sync( } }); - // File watcher processing + // File watcher: detects changes and triggers immediate sync let app_w = app.clone(); + let trigger_w = watcher_triggered.clone(); std::thread::spawn(move || { + // Debounce: wait 3 seconds after last change before triggering sync + let mut last_change = std::time::Instant::now() - Duration::from_secs(100); + let mut pending = false; + loop { - std::thread::sleep(Duration::from_secs(2)); + std::thread::sleep(Duration::from_millis(500)); + let state = app_w.state::(); let watchers = state.watchers.lock().unwrap(); + let mut had_changes = false; + for watcher in watchers.iter() { while let Ok(change) = watcher.receiver.try_recv() { let name = change.path.file_name() .and_then(|n| n.to_str()).unwrap_or("?"); + + // Skip .cloud files from triggering sync + if name.ends_with(".cloud") { continue; } + let msg = match change.kind { ChangeKind::Created => format!("Neu: {}", name), ChangeKind::Modified => format!("Geaendert: {}", name), ChangeKind::Deleted => format!("Geloescht: {}", name), }; let _ = app_w.emit("file-change", msg); + had_changes = true; + last_change = std::time::Instant::now(); + pending = true; } } + + // Debounce: trigger sync 3 seconds after last change + if pending && last_change.elapsed() >= Duration::from_secs(3) { + *trigger_w.lock().unwrap() = true; + pending = false; + let _ = app_w.emit("file-change", "→ Sync ausgeloest".to_string()); + } } }); } @@ -348,6 +441,8 @@ pub fn run() { get_status, lock_file_cmd, unlock_file_cmd, + mark_offline, + unmark_offline, ]) .run(tauri::generate_context!()) .expect("error while running tauri application");