1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//! `inkhaven backup [--out <dir>]` — zip the project into a dated
//! archive. Mirrors what the TUI does on its auto-backup-on-exit hook,
//! but standalone so users can take ad-hoc snapshots before risky
//! operations. When `--out` is omitted the default lands at
//! `<parent-of-project>/inkhaven-backups/<project-basename>/`.
use std::path::{Path, PathBuf};
use crate::backup;
use crate::config::Config;
use crate::error::Result;
use crate::project::ProjectLayout;
use crate::store::default_user_backup_dir;
pub fn run(project: &Path, out: Option<&Path>) -> Result<()> {
let layout = ProjectLayout::new(project);
layout.require_initialized()?;
// Read the config only to derive the canonical backup dir if `out` is
// a relative path that needs resolving against the project. We do NOT
// open the store: a backup is filesystem-level, and we don't want to
// initialise duckdb/HNSW just to copy bytes.
let _cfg = Config::load(&layout.config_path())?;
let abs_project = std::fs::canonicalize(&layout.root).map_err(crate::error::Error::Io)?;
// `out` may be relative — resolve against the cwd, not the project
// root, so `inkhaven --project /foo backup --out .` lands in the
// user's actual cwd. Resolve the project's own canonical path so we
// can decide whether `out` lives inside it (and must be skip-listed).
// When omitted, fall back to the sibling-of-project default — same
// location the TUI auto-backup hook writes to.
let abs_out = match out {
Some(p) if p.is_absolute() => p.to_path_buf(),
Some(p) => std::env::current_dir()
.map_err(crate::error::Error::Io)?
.join(p),
None => default_user_backup_dir(&abs_project),
};
let skip = skip_dirs_for(&abs_project, &abs_out);
let archive = backup::create_backup(&abs_project, &abs_out, &skip, None)?;
eprintln!("wrote backup: {}", archive.display());
Ok(())
}
/// Build the list of relative-or-absolute paths that the backup walker
/// should exclude. The backup output directory itself is the obvious
/// candidate when it sits inside the project — otherwise the zip would
/// recursively try to include its own grand-parent state. Returns paths in
/// the form `create_backup` checks against: project-relative if applicable,
/// absolute otherwise.
pub fn skip_dirs_for(abs_project: &Path, abs_out: &Path) -> Vec<PathBuf> {
let mut skip: Vec<PathBuf> = Vec::new();
if let Ok(rel) = abs_out.strip_prefix(abs_project) {
if !rel.as_os_str().is_empty() {
skip.push(rel.to_path_buf());
}
}
skip.push(abs_out.to_path_buf());
skip
}