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](https://github.com/Barre/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

```rust
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:

```rust
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