- 1
//! `vak backup export|import` (docs/design/29-personal-os.md P3) over - 2
//! `vak_core::backup`. The CLI layer adds the human guardrails the library - 3
//! deliberately leaves out: refusing to export onto (or import from) the - 4
//! live home itself, and a loud terminal warning when secrets ride along. - 5
- 6
use std::path::{Path, PathBuf}; - 7
- 8
use vak_core::Core; - 9
use vak_core::backup::{self, Conflict}; - 10
- 11
pub fn run_backup(cwd: PathBuf, action: crate::cli::BackupAction) -> i32 { - 12
let core = match Core::new(cwd) { - 13
Ok(c) => c, - 14
Err(e) => { - 15
eprintln!("error: {e}"); - 16
return 2; - 17
} - 18
}; - 19
let home = core.sessions_home(); - 20
match action { - 21
crate::cli::BackupAction::Export { - 22
dir, - 23
include_secrets, - 24
} => export(&home, &dir, include_secrets), - 25
crate::cli::BackupAction::Import { dir, conflict } => { - 26
let Some(mode) = parse_conflict(&conflict) else { - 27
eprintln!("error: unknown --conflict '{conflict}' (skip | rename)"); - 28
return 2; - 29
}; - 30
import(&home, &dir, mode) - 31
} - 32
} - 33
} - 34
- 35
fn parse_conflict(word: &str) -> Option<Conflict> { - 36
match word { - 37
"skip" => Some(Conflict::Skip), - 38
"rename" => Some(Conflict::Rename), - 39
_ => None, - 40
} - 41
} - 42
- 43
fn export(home: &Path, dir: &Path, include_secrets: bool) -> i32 { - 44
if same_dir(dir, home) { - 45
eprintln!( - 46
"error: refusing to export into the live vak home ({}) — pick a directory outside it", - 47
home.display() - 48
); - 49
return 2; - 50
} - 51
if include_secrets { - 52
eprintln!(); - 53
eprintln!("!! WARNING: --include-secrets will copy the local encrypted"); - 54
eprintln!("!! credential store (if this host uses one) into the destination,"); - 55
eprintln!("!! including the key that decrypts it. A host using the OS"); - 56
eprintln!("!! keychain has nothing to copy here — that store is never backed"); - 57
eprintln!("!! up by this command."); - 58
eprintln!("!! Store that directory encrypted and share it with no one."); - 59
eprintln!(); - 60
} - 61
match backup::export_to(home, dir, include_secrets) { - 62
Ok(manifest) => { - 63
println!( - 64
"backed up {} file(s), {} bytes → {}", - 65
manifest.file_count, - 66
manifest.total_bytes, - 67
dir.display() - 68
); - 69
println!("manifest: {}", dir.join("manifest.json").display()); - 70
if include_secrets && !manifest.secrets_copied { - 71
println!( - 72
"note: nothing secret was copied (either none stored, or this host uses the OS keychain, which this command cannot back up)" - 73
); - 74
} - 75
0 - 76
} - 77
Err(e) => { - 78
eprintln!("error: export failed: {e}"); - 79
1 - 80
} - 81
} - 82
} - 83
- 84
fn import(home: &Path, dir: &Path, conflict: Conflict) -> i32 { - 85
if same_dir(dir, home) { - 86
eprintln!( - 87
"error: refusing to import from the live vak home ({}) — pick a backup directory outside it", - 88
home.display() - 89
); - 90
return 2; - 91
} - 92
match backup::import_from(dir, home, conflict) { - 93
Ok(report) => { - 94
println!( - 95
"restored {} file(s) into {} ({} renamed aside, {} skipped as already present)", - 96
report.copied, - 97
home.display(), - 98
report.renamed, - 99
report.skipped - 100
); - 101
0 - 102
} - 103
Err(e) => { - 104
eprintln!("error: import failed: {e}"); - 105
1 - 106
} - 107
} - 108
} - 109
- 110
/// Same-directory refusal must survive symlinks and trailing separators, - 111
/// so compare canonical forms when both sides resolve. - 112
fn same_dir(a: &Path, b: &Path) -> bool { - 113
match (a.canonicalize(), b.canonicalize()) { - 114
(Ok(ca), Ok(cb)) => ca == cb, - 115
_ => a == b, - 116
} - 117
} - 118
- 119
#[cfg(test)] - 120
mod tests { - 121
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 122
use super::*; - 123
- 124
#[test] - 125
fn same_dir_detects_alias_forms() { - 126
let dir = tempfile::tempdir().unwrap(); - 127
let inner = dir.path().join("home"); - 128
std::fs::create_dir_all(&inner).unwrap(); - 129
assert!(same_dir(&inner, &inner)); - 130
assert!(same_dir( - 131
&inner.join("."), - 132
&std::fs::canonicalize(&inner).unwrap() - 133
)); - 134
assert!(!same_dir(&inner, &dir.path().join("other"))); - 135
} - 136
- 137
#[test] - 138
fn conflict_words_map_to_modes() { - 139
assert_eq!(parse_conflict("skip"), Some(Conflict::Skip)); - 140
assert_eq!(parse_conflict("rename"), Some(Conflict::Rename)); - 141
assert_eq!(parse_conflict("merge"), None); - 142
assert_eq!(parse_conflict(""), None); - 143
} - 144
} - 145
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.