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.

Platform Supported
Linux glibc (x64 + arm64) yes
Linux musl / Alpine (x64 + arm64) yes
macOS yes
Windows no

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.