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) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-12 02:57:12 +02:00
parent 0714d96668
commit 763fd4d563
+74 -38
View File
@@ -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));
}
}
});