crate_seq_core/snapshot.rs
1//! `crate-seq snapshot` command: capture a crate directory as a versioned tarball.
2
3use std::path::PathBuf;
4
5use crate_seq_ledger::{load, save, LedgerEntry, LedgerStatus, VersionSource};
6
7use crate::Error;
8
9/// Configuration for the `crate-seq snapshot` command.
10pub struct SnapshotConfig {
11 /// Directory of the crate to snapshot (must contain `Cargo.toml`).
12 pub src_dir: PathBuf,
13 /// Path to the crate's `.crate-seq.toml` ledger.
14 pub ledger_path: PathBuf,
15 /// Version to record.
16 pub version: semver::Version,
17 /// Directory where tarballs are stored. Defaults to `.crate-seq-snapshots/` next to the ledger.
18 pub snapshot_store: Option<PathBuf>,
19}
20
21/// Returns the effective snapshot store directory.
22///
23/// Uses `config.snapshot_store` if set, otherwise `.crate-seq-snapshots/` adjacent
24/// to the ledger file.
25fn resolve_store(config: &SnapshotConfig) -> PathBuf {
26 config.snapshot_store.clone().unwrap_or_else(|| {
27 config
28 .ledger_path
29 .parent()
30 .unwrap_or_else(|| std::path::Path::new("."))
31 .join(".crate-seq-snapshots")
32 })
33}
34
35/// Attempts to remove `path`, logging to stderr on failure.
36fn try_remove(path: &std::path::Path) {
37 if let Err(e) = std::fs::remove_file(path) {
38 eprintln!("warning: cleanup of {} failed: {e}", path.display());
39 }
40}
41
42/// Captures a snapshot of `src_dir` and adds a `Pending` ledger entry.
43///
44/// Atomicity model (ledger-first, same as `create_tag`):
45/// 1. Pre-check: error if version already tracked in ledger.
46/// 2. Compute destination path: `<snapshot_store>/<crate_name>-<version>.tar.gz`.
47/// 3. Call `crate_seq_snapshot::capture(src_dir, dest_path)` to write the tarball.
48/// 4. Call `crate_seq_snapshot::hash_tarball(dest_path)` to get the SHA-256 hex.
49/// 5. Write `LedgerEntry { source: VersionSource::Snapshot, ref_: sha256_hex, status: Pending }`.
50/// 6. Save ledger.
51/// 7. On any failure after the tarball is written: delete the tarball, return error.
52///
53/// Returns the SHA-256 hex of the snapshot tarball.
54///
55/// # Errors
56///
57/// Returns [`Error::VersionAlreadyTracked`] if the version is already in the ledger.
58/// Returns [`Error::Ledger`] if the ledger cannot be loaded or saved.
59/// Returns [`Error::Snapshot`] if capture or hashing fails.
60/// Returns [`Error::Io`] if the snapshot store directory cannot be created.
61pub fn snapshot_version(config: &SnapshotConfig) -> Result<String, Error> {
62 let mut ledger = load(&config.ledger_path)?;
63
64 if ledger.find_version(&config.version).is_some() {
65 return Err(Error::VersionAlreadyTracked(config.version.clone()));
66 }
67
68 let store = resolve_store(config);
69 std::fs::create_dir_all(&store).map_err(|source| Error::Io {
70 path: store.clone(),
71 source,
72 })?;
73
74 let crate_name = &ledger.crate_config.name;
75 let filename = format!("{crate_name}-{}.tar.gz", config.version);
76 let dest_path = store.join(&filename);
77
78 crate_seq_snapshot::capture(&config.src_dir, &dest_path)
79 .map_err(|e| Error::Snapshot(e.to_string()))?;
80
81 let sha256 = match crate_seq_snapshot::hash_tarball(&dest_path) {
82 Ok(h) => h,
83 Err(e) => {
84 try_remove(&dest_path);
85 return Err(Error::Snapshot(e.to_string()));
86 }
87 };
88
89 ledger.entries.push(LedgerEntry {
90 version: config.version.clone(),
91 source: VersionSource::Snapshot,
92 ref_: sha256.clone(),
93 status: LedgerStatus::Pending,
94 });
95
96 if let Err(e) = save(&config.ledger_path, &ledger) {
97 try_remove(&dest_path);
98 return Err(Error::Ledger(e));
99 }
100
101 Ok(sha256)
102}