feat: Watcher triggert sofort Sync + Offline-Markierung pro Datei

Sofort-Sync statt 30s-Polling:
- Filesystem-Watcher erkennt lokale Aenderungen sofort
- 3 Sekunden Debounce (wartet ob noch mehr kommt)
- Dann sofortiger Sync-Trigger statt auf den naechsten 30s-Zyklus zu warten
- .cloud-Dateien werden vom Watcher ignoriert (kein Loop)
- Fallback: alle 60s Sync auch ohne Aenderungen (Server-Aenderungen holen)
- UI zeigt "→ Sync ausgeloest" bei Watcher-Trigger

Offline-Markierung:
- mark_offline: .cloud -> echte Datei runterladen, bleibt permanent lokal
- unmark_offline: echte Datei -> zurueck zu .cloud Platzhalter
- Offline-Dateien werden bei jedem Sync automatisch aktualisiert
  (Checksum-Vergleich in sync_virtual)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-12 00:44:57 +02:00
parent e32a64ba83
commit 505545f26c
1 changed files with 99 additions and 4 deletions

View File

@ -216,6 +216,55 @@ async fn unlock_file_cmd(state: State<'_, AppState>, file_id: i64) -> Result<Str
Ok("Datei entsperrt".to_string())
}
// --- Offline-Markierung ---
#[tauri::command]
async fn mark_offline(state: State<'_, AppState>, cloud_path: String) -> Result<String, String> {
// Read .cloud placeholder, download real file, keep it local
let path = PathBuf::from(&cloud_path);
let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
let placeholder: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
let file_id = placeholder.get("id").and_then(|v| v.as_i64()).ok_or("Keine ID")?;
let file_name = placeholder.get("name").and_then(|v| v.as_str()).unwrap_or("file");
let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?;
let real_path = path.parent().unwrap().join(file_name);
// Download
api.download_file(file_id, &real_path).await?;
// Remove .cloud placeholder (real file stays permanently)
std::fs::remove_file(&path).ok();
Ok(format!("{} ist jetzt offline verfuegbar", file_name))
}
#[tauri::command]
fn unmark_offline(cloud_path: String) -> Result<String, String> {
// Convert real file back to .cloud placeholder
let path = PathBuf::from(&cloud_path);
if !path.exists() { return Err("Datei nicht gefunden".to_string()); }
let name = path.file_name().unwrap().to_string_lossy().to_string();
let size = std::fs::metadata(&path).map(|m| m.len() as i64).unwrap_or(0);
let checksum = sync::engine::compute_file_hash(&path);
let placeholder = serde_json::json!({
"id": 0, // will be updated on next sync
"name": name,
"size": size,
"checksum": checksum,
"updated_at": chrono::Utc::now().to_rfc3339(),
"server_path": "",
});
let cloud_path = path.parent().unwrap().join(format!("{}.cloud", name));
std::fs::write(&cloud_path, serde_json::to_string_pretty(&placeholder).unwrap()).ok();
std::fs::remove_file(&path).ok();
Ok(format!("{} ist nicht mehr offline", name))
}
// --- Background Threads ---
fn start_background_sync(
@ -224,19 +273,41 @@ fn start_background_sync(
api: MiniCloudApi,
paths: Vec<SyncPath>,
) {
// Auto-sync every 30 seconds
// Shared flag: watcher sets true when changes detected, sync thread checks it
let watcher_triggered = Arc::new(Mutex::new(false));
// Main sync thread: syncs on watcher trigger OR every 60s as fallback
let app_sync = app.clone();
let api_sync = api.clone();
let paths_sync = paths.clone();
let trigger_sync = watcher_triggered.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut engine = SyncEngine::new(api_sync);
engine.sync_paths = paths_sync;
let mut idle_counter = 0u32;
loop {
std::thread::sleep(Duration::from_secs(30));
// Check every 2 seconds if watcher triggered
std::thread::sleep(Duration::from_secs(2));
idle_counter += 2;
let should_sync = {
let mut triggered = trigger_sync.lock().unwrap();
if *triggered {
*triggered = false;
true
} else {
// Fallback: sync every 60 seconds even without changes
idle_counter >= 60
}
};
if !should_sync { continue; }
idle_counter = 0;
// Run sync
*sync_running.lock().unwrap() = true;
let _ = app_sync.emit("sync-status", "syncing");
@ -269,25 +340,47 @@ fn start_background_sync(
}
});
// File watcher processing
// File watcher: detects changes and triggers immediate sync
let app_w = app.clone();
let trigger_w = watcher_triggered.clone();
std::thread::spawn(move || {
// Debounce: wait 3 seconds after last change before triggering sync
let mut last_change = std::time::Instant::now() - Duration::from_secs(100);
let mut pending = false;
loop {
std::thread::sleep(Duration::from_secs(2));
std::thread::sleep(Duration::from_millis(500));
let state = app_w.state::<AppState>();
let watchers = state.watchers.lock().unwrap();
let mut had_changes = false;
for watcher in watchers.iter() {
while let Ok(change) = watcher.receiver.try_recv() {
let name = change.path.file_name()
.and_then(|n| n.to_str()).unwrap_or("?");
// Skip .cloud files from triggering sync
if name.ends_with(".cloud") { continue; }
let msg = match change.kind {
ChangeKind::Created => format!("Neu: {}", name),
ChangeKind::Modified => format!("Geaendert: {}", name),
ChangeKind::Deleted => format!("Geloescht: {}", name),
};
let _ = app_w.emit("file-change", msg);
had_changes = true;
last_change = std::time::Instant::now();
pending = true;
}
}
// Debounce: trigger sync 3 seconds after last change
if pending && last_change.elapsed() >= Duration::from_secs(3) {
*trigger_w.lock().unwrap() = true;
pending = false;
let _ = app_w.emit("file-change", "→ Sync ausgeloest".to_string());
}
}
});
}
@ -348,6 +441,8 @@ pub fn run() {
get_status,
lock_file_cmd,
unlock_file_cmd,
mark_offline,
unmark_offline,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");