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 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}