Skip to main content

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}