snapdir for Rust
The snapdir-api crate is snapdir's
canonical core surface: the stable, semver-versioned async facade over
snapdir-core that the CLI and every other binding consume. The manifests and
snapshot IDs it produces are bit-identical to snapdir id, snapdir push, and
every other binding. Every public function returns Result<T, SnapdirError> —
no anyhow leaks into the surface.
Install
cargo add snapdir-api
cargo add tokio --features full # async runtime for push/pull/fetch/diff/sync
Native crate — built from source by cargo. Unlike the C/C++, Zig, and Go
bindings, Rust does not go through the snapdir-ffi C ABI: snapdir-api is a
plain crate that cargo compiles like any dependency. No cbindgen, no C
toolchain, no vendored native blob. The sync operations (id, manifest,
id_from_manifest, stage) need no runtime; the distribution operations are
async and require a Tokio (or compatible) runtime.
Usage
Paths are &Path, the store URI is an argument parsed into a typed
StoreUri, and snapshot IDs are the typed SnapshotId newtype (.to_hex()
renders the 64-character lowercase hex). Use file://$PWD/store for a no-setup
local store, or gs://bucket/prefix for Google Cloud Storage.
use snapdir_api::{
diff, fetch, id, manifest, pull, push, CheckoutOptions, DiffOptions,
ManifestOptions, PushSource, StoreUri, TransferOptions,
};
use std::path::Path;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = StoreUri::parse(&format!(
"file://{}/store",
std::env::current_dir()?.display()
))?;
// Snapshot ID — pure, deterministic, no store touched.
let snapshot_id = id(Path::new("./my-dir"), &ManifestOptions::default())?;
println!("{}", snapshot_id.to_hex()); // 64-char lowercase hex
// Full manifest: per-entry path, checksum, and size (u64).
let m = manifest(Path::new("./my-dir"), &ManifestOptions::default())?;
for entry in &m.entries {
println!("{} {}", entry.path.display(), entry.size);
}
// Push to the store; returns the same 64-hex ID.
let sid = push(
PushSource::Path(Path::new("./my-dir")),
&store,
&TransferOptions::default(),
)
.await?;
// Fetch into the local cache, or pull + materialize into a directory.
fetch(&sid, &store, &TransferOptions::default()).await?;
pull(&sid, &store, Path::new("./restored"), &CheckoutOptions::default()).await?;
// Diff two stores: `from` and `to` are vecs of store URIs. A pinned
// snapshot reference uses the store@id convention — the last '@' splits
// the store URI from the 64-hex snapshot ID.
let next = StoreUri::parse(&format!("{store}-next"))?;
let entries = diff(&DiffOptions {
from: vec![store.clone()],
to: vec![next],
..DiffOptions::default()
})
.await?;
for e in &entries {
// status glyph is A / D / M / = — byte-identical to the CLI.
println!("{}\t{}", e.status, e.path.display());
}
Ok(())
}
For Google Cloud, swap the store URI for a gs:// URL:
let store = StoreUri::parse("gs://my-bucket/snapshots")?;
let sid = push(PushSource::Path(Path::new("./my-dir")), &store, &TransferOptions::default()).await?;
Errors
Every failure is a typed SnapdirError. It implements std::error::Error, and
.code() returns one of the eight stable SCREAMING_SNAKE_CASE codes shared
across all bindings and the CLI, so you can match on the variant or the code:
use snapdir_api::{push, PushSource, SnapdirError, StoreUri, TransferOptions};
use std::path::Path;
let store = StoreUri::parse("file://$PWD/store")?;
match push(PushSource::Path(Path::new("./missing")), &store, &TransferOptions::default()).await {
Ok(id) => println!("{}", id.to_hex()),
Err(err) => {
match err {
SnapdirError::Io(_) => {} // IO_ERROR — filesystem read/write failure
SnapdirError::HashMismatch { .. } => {} // HASH_MISMATCH — content did not match its checksum
SnapdirError::StoreError(_) => {} // STORE_ERROR — object-store transport/backend failure
SnapdirError::InFlux { .. } => {} // IN_FLUX — a file changed while being read
SnapdirError::CatalogError { .. } => {} // CATALOG_ERROR — manifest/catalog could not be parsed
SnapdirError::InvalidId { .. } => {} // INVALID_ID — snapshot ID is not 64-hex
SnapdirError::InvalidStore { .. } => {} // INVALID_STORE — store URI is malformed or unsupported
SnapdirError::Conflict { .. } => {} // CONFLICT — a concurrent write conflicted
}
eprintln!("{}: {err}", err.code());
}
}
Platform support
Every binding is CI-verified against its live public registry (crates.io) before release.
The Rust binding builds and links on both glibc and musl. The only binding-wide exception to musl support is Java, which is glibc-only today (musl planned for 1.11.1); the Rust binding has no such caveat. Windows is unsupported — snapdir is Unix-only.