feat(sync): add rsync-like sync command with dry-run and bump version to v0.5.0
This commit is contained in:
+220
@@ -201,6 +201,48 @@ enum Commands {
|
||||
full: bool,
|
||||
},
|
||||
|
||||
/// Synchronisiert Dateien und Ordner zwischen Host und Container (rsync-artig, ohne Größenbeschränkung)
|
||||
Sync {
|
||||
/// Pfad zur .sanctum Containerdatei
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Quellpfad (lokales Verzeichnis/Datei oder Vault-Pfad bei --pull)
|
||||
source: String,
|
||||
|
||||
/// Zielpfad (Vault-Pfad wie z. B. '/Downloads' oder lokaler Pfad bei --pull; Standard: '/')
|
||||
#[arg(default_value = "/")]
|
||||
target: String,
|
||||
|
||||
/// Kehrt die Richtung um: Lädt Daten aus dem Tresor auf die lokale Festplatte (Pull)
|
||||
#[arg(long, default_value_t = false)]
|
||||
pull: bool,
|
||||
|
||||
/// Löscht Dateien im Ziel, die an der Quelle nicht mehr existieren (Spiegelung)
|
||||
#[arg(long, default_value_t = false)]
|
||||
delete: bool,
|
||||
|
||||
/// Führt eine Simulation aus: Zeigt Änderungen an, ohne Dateien zu schreiben oder zu löschen
|
||||
#[arg(short = 'n', long, default_value_t = false)]
|
||||
dry_run: bool,
|
||||
|
||||
/// Vergleicht Dateien anhand kryptografischer Hashes statt nur Zeitstempel und Dateigröße
|
||||
#[arg(short = 'c', long, default_value_t = false)]
|
||||
checksum: bool,
|
||||
|
||||
/// Schließt Dateien oder Ordner anhand von Mustern aus (z. B. '*.tmp', '*.crdownload')
|
||||
#[arg(long)]
|
||||
exclude: Vec<String>,
|
||||
|
||||
/// Unterdrückt Pro-Datei-Ausgaben
|
||||
#[arg(short, long, default_value_t = false)]
|
||||
quiet: bool,
|
||||
|
||||
/// Optionaler 24-Wort Notfallschlüssel (umgeht Passwortabfrage)
|
||||
#[arg(long, num_args = 0..=1, default_missing_value = "")]
|
||||
recovery_key: Option<String>,
|
||||
},
|
||||
|
||||
/// Registriert .sanctum Containerdateien im Windows Explorer (Doppelklick & Kontextmenü, keine Adminrechte)
|
||||
Register,
|
||||
|
||||
@@ -884,6 +926,159 @@ fn handle_verify(container_path: &Path, full: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_sync(
|
||||
container_path: &Path,
|
||||
source: &str,
|
||||
target: &str,
|
||||
pull: bool,
|
||||
delete: bool,
|
||||
dry_run: bool,
|
||||
checksum: bool,
|
||||
exclude: Vec<String>,
|
||||
quiet: bool,
|
||||
recovery_key: Option<&str>,
|
||||
) -> Result<()> {
|
||||
if !container_path.exists() {
|
||||
bail!("Containerdatei '{}' existiert nicht.", container_path.display());
|
||||
}
|
||||
|
||||
let auth = if let Some(key_arg) = recovery_key {
|
||||
let phrase_str = if key_arg.trim().is_empty() {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ 🔑 Maskierte interaktive Notfallschlüssel-Eingabe │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
rpassword::prompt_password("24-Wort Notfall-Wiederherstellungsschlüssel: ")
|
||||
.context("Fehler beim Einlesen des Schlüssels")?
|
||||
} else {
|
||||
key_arg.to_string()
|
||||
};
|
||||
ContainerAuth::RecoveryKey(Zeroizing::new(phrase_str))
|
||||
} else {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Ordner- & Dateisynchronisation (Sync) │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" Container: {}", container_path.display());
|
||||
println!();
|
||||
let pass = rpassword::prompt_password("Master-Passwort: ")
|
||||
.context("Fehler beim Einlesen des Passworts")?;
|
||||
ContainerAuth::Password(Zeroizing::new(pass))
|
||||
};
|
||||
|
||||
let db = Database::open(container_path)
|
||||
.context("Konnte Container-Datenbank nicht öffnen")?;
|
||||
let meta = db.read_meta().context("Konnte Container-Header nicht lesen")?;
|
||||
|
||||
let (dek, version, vault_id) = match auth {
|
||||
ContainerAuth::Password(ref password) => {
|
||||
let keys = meta
|
||||
.authenticate(password)
|
||||
.ok_or_else(|| anyhow::anyhow!("Ungültiges Master-Passwort!"))?;
|
||||
(keys.dek().clone(), meta.version, keys.slot_id())
|
||||
}
|
||||
ContainerAuth::RecoveryKey(ref phrase) => {
|
||||
let dek = mnemonic_to_dek(phrase)
|
||||
.context("Ungültiger 24-Wort Notfallschlüssel")?;
|
||||
let is_hidden = {
|
||||
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
|
||||
!children.is_empty()
|
||||
};
|
||||
let v_id = if is_hidden { 1 } else { 0 };
|
||||
(dek, meta.version, v_id)
|
||||
}
|
||||
};
|
||||
|
||||
// Richtung ermitteln
|
||||
let direction = if pull || source.starts_with('/') {
|
||||
sanctum::sync::SyncDirection::Pull
|
||||
} else {
|
||||
sanctum::sync::SyncDirection::Push
|
||||
};
|
||||
|
||||
let options = sanctum::sync::SyncOptions {
|
||||
direction,
|
||||
delete,
|
||||
dry_run,
|
||||
checksum,
|
||||
exclude_patterns: exclude,
|
||||
quiet,
|
||||
};
|
||||
|
||||
if !quiet {
|
||||
let dir_str = match direction {
|
||||
sanctum::sync::SyncDirection::Push => "Push (Host -> Container)",
|
||||
sanctum::sync::SyncDirection::Pull => "Pull (Container -> Host)",
|
||||
};
|
||||
|
||||
println!();
|
||||
if dry_run {
|
||||
println!(" {} Ausführung im SIMULATIONS-MODUS (-n / --dry-run)!", ui::yellow("[!]"));
|
||||
}
|
||||
println!(" • Richtung: {}", ui::cyan(dir_str));
|
||||
println!(" • Quelle: {}", source);
|
||||
println!(" • Ziel: {}", target);
|
||||
if delete {
|
||||
println!(" • Spiegelung: {} (Dateien im Ziel ohne Entsprechung werden gelöscht)", ui::red("--delete aktiv"));
|
||||
}
|
||||
if checksum {
|
||||
println!(" • Modus: Kryptografischer Inhaltsabgleich (--checksum aktiv)");
|
||||
}
|
||||
if !options.exclude_patterns.is_empty() {
|
||||
println!(" • Filter: {} Ausschlussmuster", options.exclude_patterns.len());
|
||||
}
|
||||
println!();
|
||||
println!(" {} Starte Synchronisation ...", ui::dim("[-]"));
|
||||
println!();
|
||||
}
|
||||
|
||||
let stats = sanctum::sync::run_sync(
|
||||
&db,
|
||||
vault_id,
|
||||
&dek,
|
||||
version,
|
||||
source,
|
||||
target,
|
||||
&options,
|
||||
)?;
|
||||
|
||||
if !dry_run {
|
||||
let _ = db.checkpoint();
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
println!();
|
||||
if dry_run {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ℹ Dry-Run Simulation erfolgreich abgeschlossen │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" • Gescannt: {} Dateien/Objekte", stats.files_scanned);
|
||||
println!(" • Zu übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred));
|
||||
println!(" • Bereits aktuell: {} Dateien (übersprungen)", stats.files_skipped);
|
||||
if delete {
|
||||
println!(" • Zu löschen: {} Dateien/Ordner", stats.files_deleted);
|
||||
}
|
||||
println!(" • Hinweis: {} Es wurden keine Änderungen vorgenommen.", ui::yellow("✔"));
|
||||
} else {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Synchronisation erfolgreich abgeschlossen │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!(" • Gescannt: {} Dateien/Objekte", stats.files_scanned);
|
||||
println!(" • Übertragen: {} Dateien ({})", stats.files_transferred, ui::format_bytes(stats.bytes_transferred));
|
||||
println!(" • Übersprungen: {} Dateien (bereits aktuell)", stats.files_skipped);
|
||||
if delete {
|
||||
println!(" • Gelöscht: {} verwaiste Dateien/Ordner", stats.files_deleted);
|
||||
}
|
||||
let secs = stats.elapsed.as_secs_f64();
|
||||
let mb = (stats.bytes_transferred as f64) / (1024.0 * 1024.0);
|
||||
let speed = if secs > 0.05 { mb / secs } else { 0.0 };
|
||||
println!(" • Dauer: {:.2}s (Durchschnitt: {:.2} MB/s)", secs, speed);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
@@ -1006,6 +1201,31 @@ async fn run() -> Result<()> {
|
||||
Commands::Verify { path, full } => {
|
||||
handle_verify(&path, full)?;
|
||||
}
|
||||
Commands::Sync {
|
||||
path,
|
||||
source,
|
||||
target,
|
||||
pull,
|
||||
delete,
|
||||
dry_run,
|
||||
checksum,
|
||||
exclude,
|
||||
quiet,
|
||||
recovery_key,
|
||||
} => {
|
||||
handle_sync(
|
||||
&path,
|
||||
&source,
|
||||
&target,
|
||||
pull,
|
||||
delete,
|
||||
dry_run,
|
||||
checksum,
|
||||
exclude,
|
||||
quiet,
|
||||
recovery_key.as_deref(),
|
||||
)?;
|
||||
}
|
||||
Commands::Register => {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Windows Explorer Integration einrichten │");
|
||||
|
||||
Reference in New Issue
Block a user