s5_fs

Crates.ios5_fs
lib.rss5_fs
version1.0.0-beta.1
created_at2025-11-26 05:02:48.023227+00
updated_at2025-11-26 05:02:48.023227+00
descriptionContent-addressed filesystem abstraction for S5
homepage
repositoryhttps://github.com/s5-dev/s5-rs
max_upload_size
id1950969
size239,939
(redsolver)

documentation

README

S5 File-system (FS5)

High-level, content-addressed, optionally encrypted directory tree. Everything is an immutable DirV1 snapshot; mutability is simulated through actors that rewrite parent snapshots atomically.

Quick Start

  • Open a local root, put/get a file, create an encrypted subdir, and save changes.
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(())
}

Scoped subdirectories

  • FS5::subdir("path/to/dir") returns a new FS5 handle that is logically rooted at the given subdirectory.
  • If the subdirectory (or any of its parents) does not exist yet, it is created automatically.
  • When the parent directory is encrypted, newly created subdirectories inherit encryption.

Features

  • Content addressed metadata snapshots (DirV1 via CBOR) with durable persistence.
  • Actor-based single-writer per directory for deterministic ordering.
  • Optional directory encryption (XChaCha20-Poly1305; keys stored under 0x0e).
  • Registry-backed directories (Ed25519) for decentralized pointers.
  • Cursor-based listing over large directories (flat logical view, even when sharded).
  • Per-file version chains with tombstone deletes and LWW snapshot merge.

Reachability and Garbage Collection

  • FS5 directory snapshots (root.fs5.cbor, snapshots.fs5.cbor, and metadata in the FS5 meta store) form the reachability graph for content blobs.
  • The helper 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).
  • The helper 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.
  • The 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.

Directory Listing (Cursors)

// 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.

Versioning & Tombstones

  • Each FileRef can carry a version chain via prev, first_version, and version_count.
  • Deleting a file uses tombstones: FS5::file_delete(path) creates a FileRefType::Tombstone head that records when the delete happened and what the previous live version was.
  • Live reads (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.

Sharding

  • Shard metadata lives in the header (DirHeader.shards: Option<BTreeMap<u8, DirRef>>).
  • Name→bucket routing uses XXH3-64 (fast, non-crypto) for index selection.
  • Directories are automatically sharded when their encoded DirV1 exceeds ~64 KiB; shard actors are created and saved behind the scenes.
  • Sharding is a storage/layout optimization only: the FS5 API (file_get, file_exists, list, list_at, export_snapshot(_at)) always sees a flat logical directory and transparently aggregates data across shards.

Encryption

  • create_dir(path, enable_encryption = true) derives/stores per-directory keys and transparently encrypts directory snapshots.
  • On load, metadata is decrypted with keys from the context (keys can be inherited/merged from parents).

Compatibility

  • This crate is pre‑v1; on‑disk schema may change between versions.
  • Snapshot format: CBOR; see src/dir.rs for field indices and types.

Roadmap

See TODOs and proposed features in s5_fs/TODO.md.

Commit count: 0

cargo fmt