fix: Sync-Richtung korrekt - Checksum-Tracking statt Timestamps

Problem: Timestamps waren unzuverlaessig fuer die Sync-Richtung
(Download setzt lokale mtime auf 'jetzt', Timezone-Differenzen).
Offline-markierte Dateien wurden nie vom Server aktualisiert.

Loesung: known_checksums HashMap trackt den Server-Checksum
beim letzten Sync. Bei unterschiedlichen Checksums:

| Lokal geaendert | Server geaendert | Aktion |
|-----------------|------------------|--------|
| Nein | Ja | Server->Lokal (Download) |
| Ja | Nein | Lokal->Server (Upload) |
| Ja | Ja | KONFLIKT (lokale Kopie umbenennen, Server runterladen) |

Erster Sync (kein known_checksum): Server gewinnt immer (Download).
Danach wird jeder Server-Checksum gespeichert.

Betrifft: sync_virtual, sync_upload_new, sync_full_upload

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-12 02:15:53 +02:00
parent b3da50e6ce
commit 9ede2d6bdb
1 changed files with 75 additions and 38 deletions

View File

@ -36,11 +36,14 @@ pub struct SyncEngine {
pub api: MiniCloudApi,
pub sync_paths: Vec<SyncPath>,
last_sync: Option<String>,
/// Checksums from last sync - used to detect who changed a file
/// Key: file path (relative), Value: server checksum at last sync
known_checksums: HashMap<String, String>,
}
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 {