snapdir for Go

The Go binding is a CGo package that links the native libsnapdir_ffi static library, wrapping the same Rust core as the snapdir CLI. It walks a directory tree, hashes every file with BLAKE3, and produces a stable snapshot ID you can push to and pull from object stores. Manifests and snapshot IDs are bit-identical to the CLI and every other binding.

Install

go get github.com/snapdir/snapdir/bindings/go@v1.11.0

This is a build-from-source binding. The module is not self-contained: you must supply the native libsnapdir_ffi.a and snapdir.h, built from the published snapdir-ffi crate (crates.io) with a Rust toolchain and cbindgen, then dropped into the module's lib/ and include/ directories (both gitignored, shipped by neither). Set CGO_ENABLED=1. The full recipe is in Install snapdir for Go.

Usage

Every operation takes a context.Context and honours cancellation. The store URI is always an argument — file://$PWD/store for a no-setup local store, gs://bucket/prefix for Google Cloud, s3://… for S3-compatible stores.

package main

import (
	"context"
	"fmt"
	"log"

	snapdir "github.com/snapdir/snapdir/bindings/go"
)

func main() {
	ctx := context.Background()
	store := "file://" + "/path/to/store" // or gs://bucket/prefix

	// Snapshot ID for a directory — 64-char lowercase hex BLAKE3.
	id, err := snapdir.ID(ctx, "./my-dir", nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("id:", id) // e.g. <snapshot-id>

	// Full manifest: per-entry type, permissions, checksum, size.
	m, err := snapdir.Manifest(ctx, "./my-dir", &snapdir.ManifestOptions{
		NoFollow: true,                            // record symlinks as links
		Exclude:  []string{`\.git$`, `\.DS_Store`}, // extended-regex patterns
	})
	if err != nil {
		log.Fatal(err)
	}
	for _, e := range m.Entries {
		fmt.Printf("%c %04o %s %d %s\n",
			e.PathType, e.Permissions, e.Checksum, e.Size, e.Path)
	}

	// Push a directory to a store; returns the snapshot ID.
	sid, err := snapdir.Push(ctx, "./my-dir", store)
	if err != nil {
		log.Fatal(err)
	}

	// Pull a snapshot from a store and materialize it at dest.
	if err := snapdir.Pull(ctx, sid, store, "./restored"); err != nil {
		log.Fatal(err)
	}

	// Fetch downloads a snapshot into the local cache without materializing.
	if err := snapdir.Fetch(ctx, sid, store); err != nil {
		log.Fatal(err)
	}

	// Diff two stores — statuses are A / D / M / = (byte-identical to the CLI).
	entries, err := snapdir.Diff(ctx, "file:///tmp/store-a", "file:///tmp/store-b")
	if err != nil {
		log.Fatal(err)
	}
	for _, e := range entries {
		fmt.Printf("%c\t%s\n", e.Status, e.Path)
	}
}

To diff two pinned snapshots by reference, use the store@id convention — the last @ splits the store URI from the 64-hex snapshot ID:

// ref := "file:///tmp/store@<snapshot-id>"
at := strings.LastIndex(ref, "@")
store, id := ref[:at], ref[at+1:]

The binding's Diff compares two whole stores; to compare two pinned snapshots you pull each into a temporary directory and push each to its own temporary file:// store, then diff those two stores.

API surface

Function Signature Notes
ID ID(ctx, path string, opts *ManifestOptions) (string, error) 64-hex BLAKE3 snapshot ID
Manifest Manifest(ctx, path string, opts *ManifestOptions) (*ManifestResult, error) Full entry list + raw text
IDFromManifest IDFromManifest(m *ManifestResult) (string, error) Sync, pure — same result as ID()
Push Push(ctx, path, storeURI string) (string, error) Upload to store; returns snapshot ID
Pull Pull(ctx, snapshotID, storeURI, dest string) error Download + materialize
Fetch Fetch(ctx, snapshotID, storeURI string) error Download to local cache
Diff Diff(ctx, fromURI, toURI string) ([]DiffEntry, error) Compare two stores
Version Version() string snapdir-api crate version string

Errors

Every failure surfaces as a *snapdir.SnapdirError, whose .Code field is one of eight stable SCREAMING_SNAKE_CASE codes. Use errors.As to inspect it:

_, err := snapdir.Push(ctx, "./missing", store)
var se *snapdir.SnapdirError
if errors.As(err, &se) {
	fmt.Println("code:   ", se.Code)    // e.g. "IO_ERROR"
	fmt.Println("message:", se.Message)
}

The taxonomy is shared across every binding:

Code Meaning
IO_ERROR A filesystem read/write failed.
HASH_MISMATCH Retrieved content did not match its expected checksum.
STORE_ERROR The object store rejected or failed the operation.
IN_FLUX A file changed while it was being read.
CATALOG_ERROR The store catalog is missing or malformed.
INVALID_ID The snapshot ID is not a valid 64-hex string.
INVALID_STORE The store URI is malformed or unsupported.
CONFLICT A concurrent operation conflicted with this one.

Context cancellation propagates too: a Push/Pull on a cancelled or timed-out context returns an error you can match with errors.Is(err, context.DeadlineExceeded).

Platform support

Every binding is CI-verified against its live public registry before release. The Go binding runs on Linux (glibc and Alpine/musl, x64 and arm64) and macOS. Windows is unsupported — snapdir is Unix-only.

Binding Linux glibc Linux musl (Alpine) macOS Windows
Go yes (x64 + arm64) yes (x64 + arm64) yes no

All bindings share this matrix except Java, which is glibc-only today — musl support is planned for 1.11.1.