From 2bd8a2e1b5366fb2003c4dea4cb4b8feab90b20e Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sun, 12 Apr 2026 03:04:28 +0200 Subject: [PATCH] feat: Heartbeat fuer Locks - vergessene Locks laufen nach 15 Min ab Wenn jemand vergisst zu entsperren: - Client laeuft -> Heartbeat alle 60s -> Lock bleibt aktiv - Client geschlossen -> kein Heartbeat -> Lock laeuft nach 15 Min ab - Laptop zugeklappt -> gleicher Effekt -> 15 Min -> frei Tracking: locked_files Vec merkt sich welche Dateien wir gesperrt haben. Heartbeat laeuft im Token-Refresh Thread mit (alle 60s Heartbeat, alle 10 Min Token-Refresh). Lock wird beim Oeffnen getrackt, beim Entsperren/Unmark-Offline entfernt. Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/desktop/src-tauri/src/lib.rs | 34 ++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/clients/desktop/src-tauri/src/lib.rs b/clients/desktop/src-tauri/src/lib.rs index 5c8fef1..e37eea8 100644 --- a/clients/desktop/src-tauri/src/lib.rs +++ b/clients/desktop/src-tauri/src/lib.rs @@ -19,6 +19,7 @@ struct AppState { sync_engine: Mutex>, username: Mutex>, watchers: Mutex>, + locked_files: Mutex>, // file IDs we have locked on server sync_running: Arc>, sync_paths: Mutex>, } @@ -263,7 +264,10 @@ async fn open_cloud_file(state: State<'_, AppState>, cloud_path: String) -> Resu // Lock on server (prevents others from editing) match api.lock_file(file_id, "Desktop Sync Client").await { - Ok(_) => eprintln!("[OpenCloud] Locked on server"), + Ok(_) => { + eprintln!("[OpenCloud] Locked on server"); + state.locked_files.lock().unwrap().push(file_id); + } Err(e) => eprintln!("[OpenCloud] Lock failed (file may be locked by someone else): {}", e), } @@ -279,6 +283,7 @@ async fn open_cloud_file(state: State<'_, AppState>, cloud_path: String) -> Resu 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()) } @@ -495,22 +500,36 @@ fn start_background_sync( } }); - // Token refresh every 10 minutes + // Token refresh (every 10 min) + Heartbeat for locks (every 60s) let app_hb = app.clone(); let api_hb = api.clone(); std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); let mut api_mut = api_hb.clone(); + let mut tick = 0u32; loop { - std::thread::sleep(Duration::from_secs(600)); + std::thread::sleep(Duration::from_secs(10)); + tick += 10; let state = app_hb.state::(); - if let Ok(new_token) = rt.block_on(api_mut.refresh_token()) { - if let Some(ref mut api) = *state.api.lock().unwrap() { - api.access_token = new_token; + // Heartbeat every 60 seconds for locked files + if tick % 60 == 0 { + let locked = state.locked_files.lock().unwrap().clone(); + for file_id in &locked { + let _ = rt.block_on(api_mut.heartbeat(*file_id)); + } + } + + // Token refresh every 10 minutes + if tick >= 600 { + tick = 0; + if let Ok(new_token) = rt.block_on(api_mut.refresh_token()) { + if let Some(ref mut api) = *state.api.lock().unwrap() { + api.access_token = new_token; + } + eprintln!("[Auth] Token refreshed"); } - eprintln!("[Auth] Token refreshed"); } } }); @@ -630,6 +649,7 @@ pub fn run() { username: Mutex::new(None), watchers: Mutex::new(Vec::new()), sync_running: Arc::new(Mutex::new(false)), + locked_files: Mutex::new(Vec::new()), sync_paths: Mutex::new(Vec::new()), }) .on_window_event(|window, event| {