zerofs_client/lib.rs
1//! Async, path-based client for a ZeroFS server, speaking the private
2//! `9P2000.L.Z` dialect over TCP or a unix socket natively, and over
3//! WebSocket in browser WASM builds.
4//!
5//! The primary surface is one-shot path operations on a shared [`Client`]
6//! (read, write, stat, rename, mkdir), with [`File`] and [`Dir`] handles
7//! for chunked I/O, incremental listing, and openat-style child operations.
8//! Paths are byte-oriented. Native APIs accept non-UTF-8 paths; browser paths
9//! originate as UTF-8 JavaScript strings. The API uses owned data, an
10//! exhaustive error enum, `Arc` handles, explicit close, and cancellation
11//! cleanup for FFI compatibility.
12//!
13//! The client asserts its uid at connect time and assumes a trusted network.
14//!
15//! ```no_run
16//! use zerofs_client::{Client, OpenOptions};
17//!
18//! # async fn demo() -> Result<(), zerofs_client::ZeroFsError> {
19//! let fs = Client::connect("unix:/tmp/zerofs.9p.sock").await?;
20//!
21//! fs.create_dir_all("/projects/demo", 0o755).await?;
22//! fs.write("/projects/demo/hello.txt", b"hello from zerofs").await?;
23//!
24//! let data = fs.read("/projects/demo/hello.txt").await?;
25//! assert_eq!(&data[..], b"hello from zerofs");
26//!
27//! for entry in fs.read_dir("/projects/demo").await? {
28//! let size = entry.metadata.size;
29//! println!("{size:>8} {}", entry.name);
30//! }
31//!
32//! let file = fs
33//! .open("/projects/demo/big.bin", OpenOptions::read_write().create(true))
34//! .await?;
35//! file.write_at(0, &vec![0u8; 4 << 20]).await?;
36//! file.sync_all().await?;
37//! file.close().await;
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! # Lifecycles
43//!
44//! **Close and drop.** `close()` marks the handle closed and is idempotent and
45//! non-blocking. Dropping the handle schedules its fid for release and recycling.
46//!
47//! **Cancellation.** Dropping a Rust operation future releases any temporary
48//! fids it owns. If the future is dropped while a fid-state request is dispatched
49//! and unsettled, the connection that carried the request is retired. If it is
50//! still current, the session reconnects and replays before other operations
51//! resume. A dispatched mutation may still complete, leaving an ambiguous
52//! outcome. Callers supply per-operation timeouts when this ambiguity is
53//! acceptable. `zerofs-ffi` timeouts abandon a timed-out wait without cancelling
54//! the Rust future.
55//!
56//! **Connection loss.** The session reconnects with backoff and restores state
57//! before accepting requests. In-flight mutations retain their op-id across
58//! resends. The retry horizon bounds automatic resends; expiry returns an
59//! ambiguous connection failure. An open-unlinked handle cannot be replayed and
60//! returns `ESTALE` after connection loss; other handles remain usable. The
61//! negotiated `msize` is fixed for the logical session; every reconnect target
62//! must accept that value.
63
64#![warn(missing_docs)]
65
66mod client;
67mod dir;
68mod error;
69mod file;
70#[cfg(feature = "tokio-io")]
71pub mod io;
72mod linux;
73mod path;
74mod runtime;
75mod session;
76#[cfg(feature = "stream")]
77pub mod stream;
78mod types;
79
80pub use client::Client;
81pub use dir::Dir;
82pub use error::ZeroFsError;
83pub use file::File;
84#[cfg(feature = "stream")]
85pub use stream::DirStream;
86pub use types::{
87 Capabilities, ConnectOptions, DirEntry, FileType, Metadata, NodeKind, OpenOptions, SetAttrs,
88 SetTime, StatFs, Timestamp,
89};
90
91/// Re-exported so callers need not depend on `bytes` directly; it is the return
92/// type of every read (cheap to clone and slice, derefs to `&[u8]`).
93pub use bytes::Bytes;
94pub use ninep_client::TrafficStats;