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:
parent
e32a64ba83
commit
505545f26c
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue