snapdir for Java
The Java binding wraps the same Rust core (snapdir-api → snapdir-core) as
every other binding and the CLI, so the manifests and snapshot IDs it produces
are bit-identical: a snapshot ID computed in Java is the exact 64-character
lowercase hex string you get from snapdir id. It talks to the native
libsnapdir_ffi C ABI through the JDK Foreign Function API.
Install
Add the Maven Central coordinate (Maven org.snapdir:snapdir:1.11.0 or Gradle
implementation("org.snapdir:snapdir:1.11.0")). The jar is prebuilt: it
embeds the native libsnapdir_ffi library and extracts it at runtime, so no
Rust toolchain or C compiler is needed. Because it uses the incubator Foreign
Function module, compile and run on JDK 17 with the module enabled:
java --add-modules jdk.incubator.foreign --enable-native-access=ALL-UNNAMED ...
See Install snapdir for Java for the full setup.
Usage
The surface mirrors every other binding: synchronous id / manifest, plus
async push / pull / fetch / diff that return CompletableFuture. The
store URI is always an argument — file://$PWD/store for a local store,
gs://bucket/prefix for Google Cloud, s3://… for S3, and so on.
import io.snapdir.DiffEntry;
import io.snapdir.DiffStatus;
import io.snapdir.Snapdir;
import java.util.List;
public class Example {
public static void main(String[] args) throws Exception {
String dir = "./demo";
String store = "file://" + System.getProperty("user.dir") + "/store";
// Synchronous: hash the directory and read its canonical manifest.
String id = Snapdir.id(dir, null); // 64-char lowercase hex
String manifest = Snapdir.manifest(dir, null);
System.out.println(id);
// Async: push returns the same id; get() blocks on the CompletableFuture.
String pushed = Snapdir.push(dir, store, null).get();
// Materialise a snapshot into a fresh directory (verifies on fetch).
Snapdir.pull(pushed, store, "./restored", null).get();
// fetch() populates the local cache without materialising a tree.
Snapdir.fetch(pushed, store, null).get();
// diff() compares two STORE contents; statuses A / D / M / = .
String storeB = "gs://my-bucket/snapshots";
List<DiffEntry> entries = Snapdir.diff(store, storeB, null).get();
for (DiffEntry e : entries) {
System.out.printf("%s\t%s%n", e.status(), e.path());
}
}
}
To diff two pinned snapshots, follow the store@id convention — the last
@ splits the store URI from the 64-hex ID:
// "file://$PWD/store@<snapshot-id>" → { "file://$PWD/store", "<snapshot-id>" }
static String[] parseRef(String ref) {
int at = ref.lastIndexOf('@');
if (at == -1) return new String[]{ref, ""};
return new String[]{ref.substring(0, at), ref.substring(at + 1)};
}
Errors
All fallible calls throw a checked SnapdirException (unwrapped from the
ExecutionException that CompletableFuture.get() raises). Its code is drawn
from the stable 8-code taxonomy, shared byte-for-byte with every other binding:
import io.snapdir.Snapdir;
import io.snapdir.SnapdirException;
try {
Snapdir.pull(id, store, "./restored", null).get();
} catch (java.util.concurrent.ExecutionException wrapped) {
if (wrapped.getCause() instanceof SnapdirException e) {
switch (e.code()) {
case IO_ERROR -> {} // filesystem read/write failure
case HASH_MISMATCH -> {} // fetched object failed re-hash
case STORE_ERROR -> {} // remote store / transport failure
case IN_FLUX -> {} // source changed mid-snapshot
case CATALOG_ERROR -> {} // catalog / index inconsistency
case INVALID_ID -> {} // not a 64-hex snapshot id
case INVALID_STORE -> {} // unparseable / unsupported store URI
case CONFLICT -> {} // destination already exists
}
}
}
Platform support
Every binding is CI-verified against its live public registry before release.
The embedded native library is glibc-linked, so the Java jar is
glibc-only today — it will not load on an Alpine or other musl JVM
(UnsatisfiedLinkError). This is the one binding-wide exception: Node, Python,
Go, C/C++, Zig, and Rust all run on Alpine musl today. Musl support for Java is
planned for 1.11.1. Windows is unsupported, since snapdir is Unix-only.