diff --git a/clients/desktop/src-tauri/src/sync/engine.rs b/clients/desktop/src-tauri/src/sync/engine.rs index 1325960..446b9b4 100644 --- a/clients/desktop/src-tauri/src/sync/engine.rs +++ b/clients/desktop/src-tauri/src/sync/engine.rs @@ -36,11 +36,14 @@ pub struct SyncEngine { pub api: MiniCloudApi, pub sync_paths: Vec, last_sync: Option, + /// Checksums from last sync - used to detect who changed a file + /// Key: file path (relative), Value: server checksum at last sync + known_checksums: HashMap, } impl SyncEngine { pub fn new(api: MiniCloudApi) -> Self { - Self { api, sync_paths: Vec::new(), last_sync: None } + Self { api, sync_paths: Vec::new(), last_sync: None, known_checksums: HashMap::new() } } /// Sync all configured paths @@ -100,34 +103,53 @@ impl SyncEngine { if local_path.exists() { let local_hash = compute_file_hash(&local_path); let server_hash = entry.checksum.as_deref().unwrap_or(""); + let file_key = format!("{}/{}", server_path, entry.name); + if local_hash != server_hash { if entry.locked.unwrap_or(false) { log.push(format!("Zurueckgehalten (gesperrt): {}", entry.name)); continue; } - // Who is newer? - let local_modified = std::fs::metadata(&local_path) - .and_then(|m| m.modified()).ok(); - let server_modified = entry.updated_at.as_deref() - .and_then(parse_server_time); - let server_is_newer = match (local_modified, server_modified) { - (Some(lt), Some(st)) => st > lt, - _ => false, + // Check if WE changed the file locally + let last_known = self.known_checksums.get(&file_key); + let local_changed = match last_known { + Some(known) => local_hash != *known, // local differs from last sync + None => false, // first sync, don't assume local changed + }; + let server_changed = match last_known { + Some(known) => server_hash != known, // server differs from last sync + None => true, // first sync, trust server }; - if server_is_newer { + if server_changed && !local_changed { + // Only server changed -> download match self.api.download_file(entry.id, &local_path).await { Ok(_) => log.push(format!("Server->Lokal: {}", entry.name)), Err(e) => log.push(format!("Download-Fehler {}: {}", entry.name, e)), } - } else { + } else if local_changed && !server_changed { + // Only local changed -> upload match self.api.upload_file(&local_path, None).await { Ok(_) => log.push(format!("Lokal->Server: {}", entry.name)), Err(e) => log.push(format!("Upload-Fehler {}: {}", entry.name, e)), } + } else { + // Both changed -> conflict! Download server, keep local as conflict copy + let conflict_name = format!("{} (Konflikt).{}", + local_path.file_stem().unwrap().to_string_lossy(), + local_path.extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default()); + let conflict_path = local_path.parent().unwrap().join(&conflict_name); + std::fs::rename(&local_path, &conflict_path).ok(); + match self.api.download_file(entry.id, &local_path).await { + Ok(_) => log.push(format!("KONFLIKT: {} (lokale Kopie: {})", entry.name, conflict_name)), + Err(e) => log.push(format!("Download-Fehler {}: {}", entry.name, e)), + } } } + + // Track current server checksum + self.known_checksums.insert(file_key, server_hash.to_string()); continue; } @@ -213,7 +235,6 @@ impl SyncEngine { Err(e) => log.push(format!("Upload-Fehler {}: {}", name, e)), } } else { - // Existing file: check if changed (checksum compare) if let Some(se) = server_entries.iter().find(|e| e.name == name) { if se.locked.unwrap_or(false) { log.push(format!("Zurueckgehalten (gesperrt): {}", name)); @@ -224,33 +245,40 @@ impl SyncEngine { let server_hash = se.checksum.as_deref().unwrap_or(""); if local_hash != server_hash { - // Hashes differ - who is newer? - let local_modified = std::fs::metadata(&path) - .and_then(|m| m.modified()) - .ok(); - let server_modified = se.updated_at.as_deref() - .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) - .map(|dt| std::time::SystemTime::from(dt)); - - let server_is_newer = match (local_modified, server_modified) { - (Some(local_t), Some(server_t)) => server_t > local_t, - _ => false, // if we can't compare, don't overwrite server + let file_key = name.clone(); + let last_known = self.known_checksums.get(&file_key); + let local_changed = match last_known { + Some(known) => local_hash != *known, + None => false, + }; + let server_changed = match last_known { + Some(known) => server_hash != known, + None => true, }; - if server_is_newer { - // Server is newer -> download + if server_changed && !local_changed { match self.api.download_file(se.id, &path).await { Ok(_) => log.push(format!("Server->Lokal: {}", name)), Err(e) => log.push(format!("Download-Fehler {}: {}", name, e)), } - } else { - // Local is newer -> upload + } else if local_changed && !server_changed { match self.api.upload_file(&path, parent_id).await { Ok(_) => log.push(format!("Lokal->Server: {}", name)), Err(e) => log.push(format!("Upload-Fehler {}: {}", name, e)), } + } else { + // Both changed -> server wins, local becomes conflict copy + let ext = path.extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default(); + let stem = path.file_stem().unwrap().to_string_lossy(); + let conflict_path = path.parent().unwrap().join(format!("{} (Konflikt).{}", stem, ext)); + std::fs::rename(&path, &conflict_path).ok(); + match self.api.download_file(se.id, &path).await { + Ok(_) => log.push(format!("KONFLIKT: {} -> {}", name, conflict_path.file_name().unwrap().to_string_lossy())), + Err(e) => log.push(format!("Download-Fehler {}: {}", name, e)), + } } } + self.known_checksums.insert(name, server_hash.to_string()); } } } @@ -339,29 +367,38 @@ impl SyncEngine { let local_hash = compute_file_hash(&path); let server_hash = se.checksum.as_deref().unwrap_or(""); if local_hash != server_hash { - // Who is newer? - let local_modified = std::fs::metadata(&path) - .and_then(|m| m.modified()).ok(); - let server_modified = se.updated_at.as_deref() - .and_then(parse_server_time); - - let server_is_newer = match (local_modified, server_modified) { - (Some(lt), Some(st)) => st > lt, - _ => false, + let last_known = self.known_checksums.get(&name); + let local_changed = match last_known { + Some(known) => local_hash != *known, + None => false, + }; + let server_changed = match last_known { + Some(known) => server_hash != known, + None => true, }; - if server_is_newer { + if server_changed && !local_changed { match self.api.download_file(se.id, &path).await { Ok(_) => log.push(format!("Server->Lokal: {}", name)), Err(e) => log.push(format!("Download-Fehler {}: {}", name, e)), } - } else { + } else if local_changed && !server_changed { match self.api.upload_file(&path, parent_id).await { Ok(_) => log.push(format!("Lokal->Server: {}", name)), Err(e) => log.push(format!("Upload-Fehler {}: {}", name, e)), } + } else { + let ext = path.extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default(); + let stem = path.file_stem().unwrap().to_string_lossy(); + let conflict_path = path.parent().unwrap().join(format!("{} (Konflikt).{}", stem, ext)); + std::fs::rename(&path, &conflict_path).ok(); + match self.api.download_file(se.id, &path).await { + Ok(_) => log.push(format!("KONFLIKT: {} -> {}", name, conflict_path.file_name().unwrap().to_string_lossy())), + Err(e) => log.push(format!("Download-Fehler {}: {}", name, e)), + } } } + self.known_checksums.insert(name, server_hash.to_string()); } else { // New file, not on server match self.api.upload_file(&path, parent_id).await {