Compare commits

...

2 Commits

Author SHA1 Message Date
Stefan Hacker 0845659c84 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) <noreply@anthropic.com>
2026-04-12 03:01:02 +02:00
Stefan Hacker 763fd4d563 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>
2026-04-12 02:57:12 +02:00
+10 -145
View File
@@ -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,24 +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, // original .cloud filename
}
struct AppState {
api: Mutex<Option<MiniCloudApi>>,
sync_engine: Mutex<Option<SyncEngine>>,
username: Mutex<Option<String>>,
watchers: Mutex<Vec<FileWatcher>>,
sync_running: Arc<Mutex<bool>>,
locked_files: Mutex<Vec<i64>>,
opened_files: Mutex<HashMap<i64, OpenedFile>>, // file_id -> opened file info
sync_paths: Mutex<Vec<SyncPath>>,
}
@@ -268,24 +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
state.opened_files.lock().unwrap().insert(file_id, OpenedFile {
_file_id: file_id,
real_path: real_path.clone(),
cloud_name,
});
// Open with default application for this file type
eprintln!("[OpenCloud] Opening with default app: {}", real_path.display());
open::that(&real_path)
@@ -318,21 +293,6 @@ async fn get_status(state: State<'_, AppState>) -> Result<serde_json::Value, Str
}))
}
#[tauri::command]
async fn lock_file_cmd(state: State<'_, AppState>, file_id: i64) -> Result<String, String> {
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<String, String> {
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 ---
@@ -522,87 +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::<AppState>();
// 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");
}
}
// 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 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));
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");
}
}
});
@@ -655,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() {
@@ -748,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| {
@@ -859,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,