Skip to main content

git_ark/
restore.rs

1use crate::backup::{history_key, latest_key};
2use crate::store::ObjectStore;
3use crate::{crypto, git};
4use anyhow::{Context, Result};
5use std::path::{Path, PathBuf};
6
7pub fn list_versions(store: &dyn ObjectStore, prefix: &str, repo: &str) -> Result<Vec<String>> {
8    let dir = format!("{prefix}/{repo}/history/");
9    let mut versions: Vec<String> = store
10        .list(&dir)?
11        .into_iter()
12        .filter_map(|k| {
13            k.strip_prefix(&dir)
14                .and_then(|s| s.strip_suffix(".age"))
15                .map(|s| s.to_string())
16        })
17        .collect();
18    versions.sort();
19    Ok(versions)
20}
21
22pub fn run_restore(
23    store: &dyn ObjectStore,
24    identity: &str,
25    prefix: &str,
26    repo: &str,
27    version: Option<&str>,
28    dest: &Path,
29) -> Result<PathBuf> {
30    let key = match version {
31        Some(v) => history_key(prefix, repo, v),
32        None => latest_key(prefix, repo),
33    };
34    let ciphertext = store.get(&key).with_context(|| format!("fetching {key}"))?;
35    let bundle = crypto::decrypt(identity, &ciphertext)?;
36
37    std::fs::create_dir_all(dest)?;
38    let bundle_path = dest.join(format!("{}.bundle", repo.replace('/', "_")));
39    std::fs::write(&bundle_path, &bundle)?;
40
41    let leaf = repo.rsplit('/').next().unwrap_or(repo);
42    let clone_dir = dest.join(leaf);
43    git::clone_bundle(&bundle_path, &clone_dir)?;
44    Ok(clone_dir)
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::store::{InMemoryStore, ObjectStore};
51
52    fn seed() -> (InMemoryStore, String, String) {
53        // Encrypt a real bundle of a one-commit repo.
54        use std::process::Command;
55        let id = age::x25519::Identity::generate();
56        let work = tempfile::tempdir().unwrap();
57        let g = |a: &[&str]| {
58            assert!(Command::new("git")
59                .args(a)
60                .current_dir(work.path())
61                .status()
62                .unwrap()
63                .success());
64        };
65        g(&["init", "-q"]);
66        g(&["config", "user.email", "t@t"]);
67        g(&["config", "user.name", "t"]);
68        std::fs::write(work.path().join("f.txt"), "hi").unwrap();
69        g(&["add", "."]);
70        g(&["commit", "-qm", "c"]);
71        let bundle = crate::git::bundle_all(work.path()).unwrap();
72        let ct = crate::crypto::encrypt(&id.to_public().to_string(), &bundle).unwrap();
73        let store = InMemoryStore::new();
74        store.put("git-ark/app/latest.age", &ct).unwrap();
75        store
76            .put("git-ark/app/history/2026-08-26T00-00-00Z.age", &ct)
77            .unwrap();
78        use age::secrecy::ExposeSecret;
79        (
80            store,
81            id.to_string().expose_secret().to_string(),
82            "app".to_string(),
83        )
84    }
85
86    #[test]
87    fn restores_latest_and_clones_content() {
88        let (store, sk, repo) = seed();
89        let d = tempfile::tempdir().unwrap();
90        let clone = run_restore(&store, &sk, "git-ark", &repo, None, d.path()).unwrap();
91        assert_eq!(std::fs::read_to_string(clone.join("f.txt")).unwrap(), "hi");
92    }
93
94    #[test]
95    fn lists_versions() {
96        let (store, _sk, repo) = seed();
97        let v = list_versions(&store, "git-ark", &repo).unwrap();
98        assert_eq!(v, vec!["2026-08-26T00-00-00Z".to_string()]);
99    }
100}