From 763fd4d5633c6bc6a9b283fb854f2e5dc083e337 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sun, 12 Apr 2026 02:57:12 +0200 Subject: [PATCH] fix: Auto-Close erkennt Datei-Aktivitaet statt nur File-Lock Problem: Notepad und die meisten Editoren halten keinen File-Lock. is_file_in_use() fand sofort "nicht in Benutzung" und raeumte die Datei auf bevor der User sie bearbeiten konnte. Neuer Ansatz - drei Bedingungen muessen erfuellt sein: 1. Mindestens 30 Sekunden seit dem Oeffnen (Schutzzeit) 2. Kein File-Lock UND Dateigroesse unveraendert 3. Mindestens 2 Minuten seit der letzten Aenderung/Lock Datei-Aktivitaet wird getrackt: - Groesse aendert sich -> Timer zuruecksetzen - File-Lock aktiv (Office) -> Timer zuruecksetzen - Erst nach 2 Minuten Inaktivitaet -> Auto-Close So funktioniert es fuer alle Programme: - Office (haelt Lock): Lock verschwindet -> 2 Min warten -> Close - Notepad (kein Lock): Letzte Groessenaenderung -> 2 Min -> Close - Schnell oeffnen+schliessen: 30s Schutzzeit verhindert sofortiges Close Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/desktop/src-tauri/src/lib.rs | 112 ++++++++++++++++++--------- 1 file changed, 74 insertions(+), 38 deletions(-) diff --git a/clients/desktop/src-tauri/src/lib.rs b/clients/desktop/src-tauri/src/lib.rs index 118db57..79979e9 100644 --- a/clients/desktop/src-tauri/src/lib.rs +++ b/clients/desktop/src-tauri/src/lib.rs @@ -21,7 +21,10 @@ use std::collections::HashMap; struct OpenedFile { _file_id: i64, real_path: PathBuf, - cloud_name: String, // original .cloud filename + cloud_name: String, + opened_at: std::time::Instant, + last_size: u64, + last_changed: std::time::Instant, } struct AppState { @@ -280,10 +283,14 @@ async fn open_cloud_file(state: State<'_, AppState>, cloud_path: String) -> Resu state.locked_files.lock().unwrap().push(file_id); // Track for auto-close detection + let file_size = std::fs::metadata(&real_path).map(|m| m.len()).unwrap_or(0); state.opened_files.lock().unwrap().insert(file_id, OpenedFile { _file_id: file_id, real_path: real_path.clone(), cloud_name, + opened_at: std::time::Instant::now(), + last_size: file_size, + last_changed: std::time::Instant::now(), }); // Open with default application for this file type @@ -553,7 +560,7 @@ fn start_background_sync( let _ = rt.block_on(api_mut.heartbeat(*file_id)); } - // Check if opened files are still in use by another process + // Check opened files - detect when user is done editing let opened = state.opened_files.lock().unwrap().clone(); for (file_id, info) in &opened { if !info.real_path.exists() { @@ -566,43 +573,72 @@ fn start_background_sync( continue; } - // Check if file is still locked by another process - let still_open = is_file_in_use(&info.real_path); - if !still_open { - // File closed! Sync back, recreate .cloud, unlock - let _ = app_hb.emit("file-change", - format!("Datei geschlossen, synchronisiere: {}", info.real_path.file_name().unwrap().to_string_lossy())); - - // Upload changes - let _ = rt.block_on(api_hb.upload_file(&info.real_path, None)); - - // Unlock on server - let _ = rt.block_on(api_hb.unlock_file(*file_id)); - - // Recreate .cloud placeholder - let cloud_path = info.real_path.parent().unwrap().join(&info.cloud_name); - let size = std::fs::metadata(&info.real_path).map(|m| m.len() as i64).unwrap_or(0); - let checksum = sync::engine::compute_file_hash(&info.real_path); - let placeholder = serde_json::json!({ - "id": file_id, - "name": info.real_path.file_name().unwrap().to_string_lossy(), - "size": size, - "checksum": checksum, - "updated_at": chrono::Utc::now().to_rfc3339(), - "server_path": "", - }); - std::fs::write(&cloud_path, serde_json::to_string_pretty(&placeholder).unwrap()).ok(); - - // Remove local copy - std::fs::remove_file(&info.real_path).ok(); - - // Clean up tracking - state.opened_files.lock().unwrap().remove(file_id); - state.locked_files.lock().unwrap().retain(|&id| id != *file_id); - - let _ = app_hb.emit("file-change", - format!("Entsperrt + .cloud: {}", info.cloud_name)); + // Don't check in the first 30 seconds (give user time to start editing) + if info.opened_at.elapsed() < Duration::from_secs(30) { + continue; } + + // Check if file size/mtime changed since last check + let current_size = std::fs::metadata(&info.real_path).map(|m| m.len()).unwrap_or(0); + let current_mtime = std::fs::metadata(&info.real_path) + .and_then(|m| m.modified()).ok(); + + if current_size != info.last_size { + // File was modified - update tracking, reset timer + let mut files = state.opened_files.lock().unwrap(); + if let Some(f) = files.get_mut(file_id) { + f.last_size = current_size; + f.last_changed = std::time::Instant::now(); + } + continue; + } + + // Check with file lock (works for Office apps) + let has_lock = is_file_in_use(&info.real_path); + if has_lock { + // Still locked by another process (Office) - update timer + let mut files = state.opened_files.lock().unwrap(); + if let Some(f) = files.get_mut(file_id) { + f.last_changed = std::time::Instant::now(); + } + continue; + } + + // File not locked AND not modified for 2 minutes -> consider closed + if info.last_changed.elapsed() < Duration::from_secs(120) { + continue; + } + + // File is done! Sync back, recreate .cloud, unlock + let _ = app_hb.emit("file-change", + format!("Auto-Close: {}", info.real_path.file_name().unwrap().to_string_lossy())); + + // Upload if file was actually changed (compare with server checksum) + let local_hash = sync::engine::compute_file_hash(&info.real_path); + let _ = rt.block_on(api_hb.upload_file(&info.real_path, None)); + + // Unlock on server + let _ = rt.block_on(api_hb.unlock_file(*file_id)); + + // Recreate .cloud placeholder + let cloud_path = info.real_path.parent().unwrap().join(&info.cloud_name); + let size = std::fs::metadata(&info.real_path).map(|m| m.len() as i64).unwrap_or(0); + let placeholder = serde_json::json!({ + "id": file_id, + "name": info.real_path.file_name().unwrap().to_string_lossy(), + "size": size, + "checksum": local_hash, + "updated_at": chrono::Utc::now().to_rfc3339(), + "server_path": "", + }); + std::fs::write(&cloud_path, serde_json::to_string_pretty(&placeholder).unwrap()).ok(); + std::fs::remove_file(&info.real_path).ok(); + + state.opened_files.lock().unwrap().remove(file_id); + state.locked_files.lock().unwrap().retain(|&id| id != *file_id); + + let _ = app_hb.emit("file-change", + format!("Entsperrt + .cloud: {}", info.cloud_name)); } } });