zerofs-client 0.2.0

Async, path-based client library for ZeroFS over 9P
Documentation

zerofs-client

An async, path-based Rust client for a ZeroFS server. It speaks the private 9P2000.L.Z dialect over TCP or a unix socket.

The primary surface is one-shot path operations on a shared Client (read, write, stat, rename, mkdir), with File and Dir handles where statefulness pays off (chunked I/O, incremental listing, openat-style child operations). Paths are bytes, as on POSIX and the 9P wire: every path parameter is impl AsRef<Path>, so non-UTF-8 names work with no separate API.

The client asserts its uid at connect time and assumes a trusted network.

Usage

use zerofs_client::{Client, OpenOptions};

#[tokio::main]
async fn main() -> Result<(), zerofs_client::ZeroFsError> {
    let fs = Client::connect("unix:/run/zerofs/9p.sock").await?;

    fs.create_dir_all("/projects/demo", 0o755).await?;
    fs.write("/projects/demo/hello.txt", b"hello from zerofs").await?;

    let data = fs.read("/projects/demo/hello.txt").await?;
    assert_eq!(&data[..], b"hello from zerofs");

    for entry in fs.read_dir("/projects/demo").await? {
        let size = entry.metadata.size;
        println!("{size:>8}  {}", entry.name);
    }

    let file = fs
        .open("/projects/demo/big.bin", OpenOptions::read_write().create(true))
        .await?;
    file.write_at(0, &vec![0u8; 4 << 20]).await?;
    file.sync_all().await?;
    file.close().await;
    Ok(())
}

Connection targets

Client::connect accepts:

  • unix:/path/to/sock: unix socket
  • tcp://host:port: TCP
  • host:port / host: TCP (default port 5564)
  • a bare filesystem path (/run/zerofs/9p.sock, ./sock): unix socket

For an HA pair, pass both nodes as one comma-separated string:

let fs = Client::connect("node-a:5564,node-b:5564").await?;

The client races the targets to find the serving leader and re-probes the set after a disconnect. The initially negotiated msize is retained for the logical session, and reconnect candidates that negotiate another value are skipped. Every node in an HA or rolling deployment must therefore accept the session's established msize. Rust callers that already hold separate strings can use Client::connect_multi(&targets).

Use Client::connect_with for explicit identity (uid/gid/uname), the attach subtree (aname), msize, and connect_timeout_ms.

Lifecycles

  • Close and drop. close() marks the handle closed and is idempotent and non-blocking. Dropping the handle schedules its server fid for release and recycling. Dropping an open handle performs the same cleanup.
  • Cancellation. Dropping a Rust operation future releases any temporary fids it owns. If it is dropped while a fid-state request is dispatched and unsettled, the client retires the connection that carried the request. If it is still current, the session reconnects and replays before other operations resume. A dispatched mutation may still complete, leaving an ambiguous outcome. zerofs-ffi timeouts abandon the caller's wait without cancelling the Rust operation.
  • Connection loss. The session reconnects with backoff and restores fids and locks before accepting requests. Undispatched calls wait while no server is reachable. In-flight mutations retain their operation envelope across resends. Resends stop at the 120-second protocol horizon; unresolved outcomes return an ambiguous connection failure. Reconnect requires the established msize. An open-unlinked handle cannot be replayed and returns ESTALE after connection loss; other handles remain usable.

Reads return Bytes

read, read_range, and File::read_at return [bytes::Bytes] (re-exported as zerofs_client::Bytes): cheap to clone and slice, derefs to &[u8], and a read served by a single round trip comes back with no copy. ZeroFsError converts to std::io::Error (impl From), so client calls propagate with ? in functions returning io::Result.

Features

Feature-gated additions are Rust-only and are excluded from the FFI boundary.

  • tokio-io (off by default): File::cursor() returning an io::FileCursor that implements AsyncRead + AsyncWrite + AsyncSeek over the positioned API, so tokio::io::copy and friends work.
  • stream (off by default): Dir::entries() returning a futures::Stream of DirEntry, for StreamExt consumers.
  • serde (off by default): Serialize/Deserialize on the plain data records (Metadata, DirEntry, StatFs, Timestamp, ...).

SystemTime converts to/from Timestamp and into SetTime, so the standard time types work directly with set_times and the metadata accessors.

License

AGPL-3.0