From 0845659c84a85be074cfbb8f046cfa1b2099c115 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Sun, 12 Apr 2026 03:01:02 +0200 Subject: [PATCH] refactor: Auto-Close komplett entfernt - Nextcloud-Ansatz .cloud oeffnen = Download + Datei bleibt als echte Datei (wie Nextcloud). Aenderungen werden automatisch vom Watcher gesynct. "Nicht mehr offline" per Rechtsklick im Datei-Browser -> .cloud zurueck. Entfernt: - Auto-Close Detection (is_file_in_use, OpenedFile tracking, Heartbeat, Lock/Unlock beim Oeffnen) - Lock-Kommandos (lock_file_cmd, unlock_file_cmd) - opened_files HashMap, locked_files Vec - is_file_in_use Funktion - ~100 Zeilen Code weniger Beibehalten: - Token-Refresh Thread (alle 10 Min) - File-Locking API im Backend (wird vom Web-UI weiterhin genutzt) - Watcher + Sofort-Sync - mark_offline / unmark_offline Kommandos Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/desktop/src-tauri/src/lib.rs | 191 ++------------------------- 1 file changed, 10 insertions(+), 181 deletions(-) diff --git a/clients/desktop/src-tauri/src/lib.rs b/clients/desktop/src-tauri/src/lib.rs index 79979e9..f751541 100644 --- a/clients/desktop/src-tauri/src/lib.rs +++ b/clients/desktop/src-tauri/src/lib.rs @@ -1,6 +1,6 @@ mod sync; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::Duration; use tauri::{ @@ -14,27 +14,12 @@ use sync::config::AppConfig; use sync::engine::{SyncEngine, SyncMode, SyncPath}; use sync::watcher::{FileWatcher, ChangeKind}; -use std::collections::HashMap; - -/// Tracks a file opened from a .cloud placeholder -#[derive(Clone, Debug)] -struct OpenedFile { - _file_id: i64, - real_path: PathBuf, - cloud_name: String, - opened_at: std::time::Instant, - last_size: u64, - last_changed: std::time::Instant, -} - struct AppState { api: Mutex>, sync_engine: Mutex>, username: Mutex>, watchers: Mutex>, sync_running: Arc>, - locked_files: Mutex>, - opened_files: Mutex>, // file_id -> opened file info sync_paths: Mutex>, } @@ -271,28 +256,11 @@ async fn open_cloud_file(state: State<'_, AppState>, cloud_path: String) -> Resu } eprintln!("[OpenCloud] Downloaded {} bytes", std::fs::metadata(&real_path).map(|m| m.len()).unwrap_or(0)); - // Remove .cloud placeholder + // Remove .cloud placeholder - file stays as real file (like "offline markieren") + // Changes will be synced automatically by the file watcher + // User can manually "unmark offline" via right-click to get .cloud back std::fs::remove_file(&path).ok(); - // Lock on server - let cloud_name = path.file_name().unwrap().to_string_lossy().to_string(); - match api.lock_file(file_id, "Desktop Sync Client").await { - Ok(_) => eprintln!("[OpenCloud] Locked file {}", file_id), - Err(e) => eprintln!("[OpenCloud] Lock failed (continuing): {}", e), - } - 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 eprintln!("[OpenCloud] Opening with default app: {}", real_path.display()); open::that(&real_path) @@ -325,21 +293,6 @@ async fn get_status(state: State<'_, AppState>) -> 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()) -} // --- Local File Browser --- @@ -529,116 +482,22 @@ fn start_background_sync( } }); - // Heartbeat + token refresh + check if opened files still in use + // Token refresh every 10 minutes 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 token_refresh_counter = 0u32; loop { - std::thread::sleep(Duration::from_secs(10)); + std::thread::sleep(Duration::from_secs(600)); let state = app_hb.state::(); - // Refresh JWT token every 10 minutes (before 15 min expiry) - token_refresh_counter += 10; - if token_refresh_counter >= 600 { - token_refresh_counter = 0; - if let Ok(new_token) = rt.block_on(api_mut.refresh_token()) { - // Update the shared API instance with new token - if let Some(ref mut api) = *state.api.lock().unwrap() { - api.access_token = new_token; - } - eprintln!("[Auth] Token refreshed"); + 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 for locked files - let locked = state.locked_files.lock().unwrap().clone(); - for file_id in &locked { - let _ = rt.block_on(api_mut.heartbeat(*file_id)); - } - - // 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() { - // File was deleted/moved - clean up - state.opened_files.lock().unwrap().remove(file_id); - state.locked_files.lock().unwrap().retain(|&id| id != *file_id); - let _ = rt.block_on(api_hb.unlock_file(*file_id)); - let _ = app_hb.emit("file-change", - format!("Geschlossen + entsperrt: {}", info.cloud_name)); - continue; - } - - // 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)); + eprintln!("[Auth] Token refreshed"); } } }); @@ -691,32 +550,6 @@ fn start_background_sync( // --- App Setup --- /// Check if another instance is running. If yes, pass the .cloud file to it and exit. -/// Check if a file is still being used by another process -fn is_file_in_use(path: &Path) -> bool { - // Try to open the file with exclusive access - // If it fails, another process has it open - #[cfg(target_os = "windows")] - { - use std::fs::OpenOptions; - // On Windows, try to open with write access - fails if file is locked by Office etc. - match OpenOptions::new().write(true).open(path) { - Ok(_) => false, // We could open it -> not in use - Err(_) => true, // Can't open -> still in use - } - } - #[cfg(not(target_os = "windows"))] - { - // On Linux/Mac, check /proc or lsof - let output = std::process::Command::new("lsof") - .arg(path.to_string_lossy().as_ref()) - .output(); - match output { - Ok(o) => !o.stdout.is_empty(), // lsof found processes -> in use - Err(_) => false, // lsof not available -> assume not in use - } - } -} - /// Single instance per user. On terminal servers each user gets their own /// instance because the lock file is in %APPDATA% (user-specific). fn handle_single_instance() { @@ -784,8 +617,6 @@ 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()), - opened_files: Mutex::new(HashMap::new()), sync_paths: Mutex::new(Vec::new()), }) .on_window_event(|window, event| { @@ -895,8 +726,6 @@ pub fn run() { open_cloud_file, get_file_tree, get_status, - lock_file_cmd, - unlock_file_cmd, browse_sync_folder, mark_offline, unmark_offline,