| Crates.io | s5_fs |
| lib.rs | s5_fs |
| version | 1.0.0-beta.1 |
| created_at | 2025-11-26 05:02:48.023227+00 |
| updated_at | 2025-11-26 05:02:48.023227+00 |
| description | Content-addressed filesystem abstraction for S5 |
| homepage | |
| repository | https://github.com/s5-dev/s5-rs |
| max_upload_size | |
| id | 1950969 |
| size | 239,939 |
High-level, content-addressed, optionally encrypted directory tree. Everything is an immutable DirV1 snapshot; mutability is simulated through actors that rewrite parent snapshots atomically.
use s5_fs::{DirContext, FS5, FileRef};
use bytes::Bytes;
use tempfile::tempdir;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let tmp = tempdir()?;
let ctx = DirContext::open_local_root(tmp.path())?;
// Default: open without autosave; call save() when ready
let fs = FS5::open(ctx);
// Put and get a file
let blob = Bytes::from("hello fs5");
let file_ref = FileRef::new_inline_blob(blob.clone());
fs.file_put("greeting.txt", file_ref).await; // fire-and-forget
let got = fs.file_get("greeting.txt").await.unwrap();
// Create encrypted sub-directory
fs.create_dir("secret", true).await?;
fs.file_put_sync("secret/plan.txt", FileRef::new_inline_blob(Bytes::from("top secret"))).await?;
// Work with a scoped subdirectory handle
let project_fs = fs.subdir("projects/my-app").await?;
project_fs
.file_put_sync(
"config.toml",
FileRef::new_inline_blob(Bytes::from("name = \"my-app\"")),
)
.await?;
// Batch multiple ops, then persist once
fs.batch(|fs| async move {
fs.file_put_sync("a.txt", FileRef::new_inline_blob(Bytes::from("A"))).await?;
fs.file_put_sync("b.txt", FileRef::new_inline_blob(Bytes::from("B"))).await?;
fs.file_put_sync("secret/b.txt", FileRef::new_inline_blob(Bytes::from("B"))).await?;
fs.file_delete("a.txt").await?;
Ok(())
}).await?;
// Persist metadata snapshots
fs.save().await?;
Ok(())
}
FS5::subdir("path/to/dir") returns a new FS5 handle that is logically rooted at the given subdirectory.DirV1 via CBOR) with durable persistence.0x0e).root.fs5.cbor, snapshots.fs5.cbor, and metadata in the FS5 meta store) form the reachability graph for content blobs.s5_fs::gc::collect_fs_reachable_hashes walks these snapshots to produce the set of content hashes that are still live from an FS5 root (including historical versions).s5_fs::gc::gc_store runs a conservative mark-and-sweep over a blob store: any blob with at least one pin in the node registry or whose hash is reachable from the FS5 root is kept; everything else is a GC candidate.s5 blobs gc-local and s5 blobs verify-local CLI commands are thin wrappers around these helpers for local stores; higher-level snapshot GC policies are tracked in s5_fs/TODO.md.// First page
let (entries, mut cursor) = fs.list(None, 100).await?;
for (name, kind) in entries {
println!("{name} {:?}", kind);
}
// Next page (if any)
if let Some(c) = cursor.take() {
let (more, next) = fs.list(Some(&c), 100).await?;
// ...
cursor = next;
}
Cursors are base64url-encoded CBOR carrying the last position and kind. For
large directories that have been sharded internally, list still presents a
single flat logical namespace aggregated across all shards.
FileRef can carry a version chain via prev, first_version, and
version_count.FS5::file_delete(path) creates a
FileRefType::Tombstone head that records when the delete happened and what
the previous live version was.file_get, file_exists) hide tombstones; historical versions
remain accessible via exported snapshots.merge_from_snapshot applies last-write-wins (LWW) over timestamps and
preserves the entire winning version chain, including tombstones.DirHeader.shards: Option<BTreeMap<u8, DirRef>>).DirV1 exceeds
~64 KiB; shard actors are created and saved behind the scenes.file_get,
file_exists, list, list_at, export_snapshot(_at)) always sees a flat
logical directory and transparently aggregates data across shards.create_dir(path, enable_encryption = true) derives/stores per-directory keys and transparently encrypts directory snapshots.src/dir.rs for field indices and types.See TODOs and proposed features in s5_fs/TODO.md.