feat: Auto-Unlock wenn Datei geschlossen wird
Problem: Nach Oeffnen einer .cloud-Datei blieb die Sperre auf dem Server bestehen, auch wenn Word/Excel geschlossen wurde. Loesung: Hintergrund-Thread prueft alle 10 Sekunden ob geoeffnete Dateien noch von einem Prozess benutzt werden: Windows: Versucht exklusiven Schreibzugriff - wenn erfolgreich ist die Datei nicht mehr in Benutzung (Office gibt den Lock frei) Linux/Mac: lsof prueft ob ein Prozess die Datei offen hat Wenn Datei geschlossen: 1. Aenderungen werden zum Server hochgeladen 2. Server-Lock wird aufgehoben 3. .cloud Platzhalter wird neu erstellt (mit aktuellem Checksum) 4. Lokale Kopie wird geloescht 5. UI zeigt "Geschlossen + entsperrt: datei.cloud" Tracking: opened_files HashMap speichert file_id -> Pfad + Cloud-Name fuer alle via .cloud geoeffneten Dateien. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4673423e2f
commit
b937351556
|
|
@ -14,6 +14,16 @@ use sync::config::AppConfig;
|
||||||
use sync::engine::{SyncEngine, SyncMode, SyncPath};
|
use sync::engine::{SyncEngine, SyncMode, SyncPath};
|
||||||
use sync::watcher::{FileWatcher, ChangeKind};
|
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, // original .cloud filename
|
||||||
|
}
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
api: Mutex<Option<MiniCloudApi>>,
|
api: Mutex<Option<MiniCloudApi>>,
|
||||||
sync_engine: Mutex<Option<SyncEngine>>,
|
sync_engine: Mutex<Option<SyncEngine>>,
|
||||||
|
|
@ -21,6 +31,7 @@ struct AppState {
|
||||||
watchers: Mutex<Vec<FileWatcher>>,
|
watchers: Mutex<Vec<FileWatcher>>,
|
||||||
sync_running: Arc<Mutex<bool>>,
|
sync_running: Arc<Mutex<bool>>,
|
||||||
locked_files: Mutex<Vec<i64>>,
|
locked_files: Mutex<Vec<i64>>,
|
||||||
|
opened_files: Mutex<HashMap<i64, OpenedFile>>, // file_id -> opened file info
|
||||||
sync_paths: Mutex<Vec<SyncPath>>,
|
sync_paths: Mutex<Vec<SyncPath>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,10 +236,17 @@ async fn open_cloud_file(state: State<'_, AppState>, cloud_path: String) -> Resu
|
||||||
// Remove placeholder
|
// Remove placeholder
|
||||||
std::fs::remove_file(&path).ok();
|
std::fs::remove_file(&path).ok();
|
||||||
|
|
||||||
// Lock
|
// Lock on server
|
||||||
let _ = engine.lock_file(file_id, "Desktop Sync Client").await;
|
let _ = engine.lock_file(file_id, "Desktop Sync Client").await;
|
||||||
state.locked_files.lock().unwrap().push(file_id);
|
state.locked_files.lock().unwrap().push(file_id);
|
||||||
|
|
||||||
|
// Track opened file for auto-close detection
|
||||||
|
state.opened_files.lock().unwrap().insert(file_id, OpenedFile {
|
||||||
|
file_id,
|
||||||
|
real_path: real_path.clone(),
|
||||||
|
cloud_name: path.file_name().unwrap().to_string_lossy().to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
// Open with default application
|
// Open with default application
|
||||||
let _ = open::that(&real_path);
|
let _ = open::that(&real_path);
|
||||||
|
|
||||||
|
|
@ -463,17 +481,71 @@ fn start_background_sync(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Heartbeat every 60 seconds
|
// Heartbeat every 60s + check if opened files are still in use
|
||||||
let app_hb = app.clone();
|
let app_hb = app.clone();
|
||||||
let api_hb = api.clone();
|
let api_hb = api.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(Duration::from_secs(60));
|
std::thread::sleep(Duration::from_secs(10));
|
||||||
let state = app_hb.state::<AppState>();
|
let state = app_hb.state::<AppState>();
|
||||||
|
|
||||||
|
// Heartbeat for locked files
|
||||||
let locked = state.locked_files.lock().unwrap().clone();
|
let locked = state.locked_files.lock().unwrap().clone();
|
||||||
for file_id in locked {
|
for file_id in &locked {
|
||||||
let _ = rt.block_on(api_hb.heartbeat(file_id));
|
let _ = rt.block_on(api_hb.heartbeat(*file_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if opened files are still in use by another process
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -526,6 +598,32 @@ fn start_background_sync(
|
||||||
// --- App Setup ---
|
// --- App Setup ---
|
||||||
|
|
||||||
/// Check if another instance is running. If yes, pass the .cloud file to it and exit.
|
/// 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
|
/// Single instance per user. On terminal servers each user gets their own
|
||||||
/// instance because the lock file is in %APPDATA% (user-specific).
|
/// instance because the lock file is in %APPDATA% (user-specific).
|
||||||
fn handle_single_instance() {
|
fn handle_single_instance() {
|
||||||
|
|
@ -594,6 +692,7 @@ pub fn run() {
|
||||||
watchers: Mutex::new(Vec::new()),
|
watchers: Mutex::new(Vec::new()),
|
||||||
sync_running: Arc::new(Mutex::new(false)),
|
sync_running: Arc::new(Mutex::new(false)),
|
||||||
locked_files: Mutex::new(Vec::new()),
|
locked_files: Mutex::new(Vec::new()),
|
||||||
|
opened_files: Mutex::new(HashMap::new()),
|
||||||
sync_paths: Mutex::new(Vec::new()),
|
sync_paths: Mutex::new(Vec::new()),
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue