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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
mod sync;
|
mod sync;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
@@ -14,27 +14,12 @@ 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,
|
|
||||||
opened_at: std::time::Instant,
|
|
||||||
last_size: u64,
|
|
||||||
last_changed: std::time::Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
api: Mutex<Option<MiniCloudApi>>,
|
api: Mutex<Option<MiniCloudApi>>,
|
||||||
sync_engine: Mutex<Option<SyncEngine>>,
|
sync_engine: Mutex<Option<SyncEngine>>,
|
||||||
username: Mutex<Option<String>>,
|
username: Mutex<Option<String>>,
|
||||||
watchers: Mutex<Vec<FileWatcher>>,
|
watchers: Mutex<Vec<FileWatcher>>,
|
||||||
sync_running: Arc<Mutex<bool>>,
|
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>>,
|
sync_paths: Mutex<Vec<SyncPath>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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));
|
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();
|
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
|
// Open with default application for this file type
|
||||||
eprintln!("[OpenCloud] Opening with default app: {}", real_path.display());
|
eprintln!("[OpenCloud] Opening with default app: {}", real_path.display());
|
||||||
open::that(&real_path)
|
open::that(&real_path)
|
||||||
@@ -325,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 ---
|
// --- 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 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();
|
||||||
let mut api_mut = api_hb.clone();
|
let mut api_mut = api_hb.clone();
|
||||||
let mut token_refresh_counter = 0u32;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(Duration::from_secs(10));
|
std::thread::sleep(Duration::from_secs(600));
|
||||||
let state = app_hb.state::<AppState>();
|
let state = app_hb.state::<AppState>();
|
||||||
|
|
||||||
// Refresh JWT token every 10 minutes (before 15 min expiry)
|
if let Ok(new_token) = rt.block_on(api_mut.refresh_token()) {
|
||||||
token_refresh_counter += 10;
|
if let Some(ref mut api) = *state.api.lock().unwrap() {
|
||||||
if token_refresh_counter >= 600 {
|
api.access_token = new_token;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
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 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -691,32 +550,6 @@ 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() {
|
||||||
@@ -784,8 +617,6 @@ pub fn run() {
|
|||||||
username: Mutex::new(None),
|
username: Mutex::new(None),
|
||||||
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()),
|
|
||||||
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| {
|
||||||
@@ -895,8 +726,6 @@ pub fn run() {
|
|||||||
open_cloud_file,
|
open_cloud_file,
|
||||||
get_file_tree,
|
get_file_tree,
|
||||||
get_status,
|
get_status,
|
||||||
lock_file_cmd,
|
|
||||||
unlock_file_cmd,
|
|
||||||
browse_sync_folder,
|
browse_sync_folder,
|
||||||
mark_offline,
|
mark_offline,
|
||||||
unmark_offline,
|
unmark_offline,
|
||||||
|
|||||||
Reference in New Issue
Block a user