Sign in

    moon-nfs3

    NFSv3 over TCP file client for MoonBit

    nfs
    rpc
    storage
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    6 hours ago
    Downloads
    5

    #MoonNFS3

    MoonNFS3 是用 MoonBit 实现的 NFSv3/TCP 客户端,可以直接访问服务器导出的文件和目录,无需在操作系统中挂载 NFS。

    项目支持文件读写、目录管理、流式传输、稳定写入和 COMMIT 校验恢复,并提供有资源上限的目录遍历、目录树传输、前缀校验续传、原子文件发布,以及不同连接之间的复制能力。适合在 MoonBit 后端程序、命令行工具和文件备份流程中接入 NFS 存储。

    NFS 客户端和连接管理逻辑由本项目实现;XDR 编解码与 RPC 线格式使用 kekeshuo/moonxdr@0.2.0,网络与文件异步 I/O 使用 moonbitlang/async@0.22.1。

    #从 MoonCake 安装

    在现有 MoonBit 项目中添加依赖:

    moon add cauchyQ/moon-nfs3@0.1.0

    在使用该库的 moon.pkg 中导入 "cauchyQ/moon-nfs3" @nfs,具体用法见下文“作为库使用”。

    #从源码运行

    git clone https://github.com/cauchyQ/moon-nfs3.git cd moon-nfs3 moon update moon check --deny-warn moon test --deny-warn moon run cmd/main -- --help

    项目默认使用 wasm 后端,CI 同时检查 Linux native 后端。

    本地开发和 CI 固定使用编译器 v0.10.14+7d59c7ec9,以避免较新格式化工具迁移语法或整数类型名称所带来的差异。Linux 下可按以下方式安装对应工具链:

    curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash -s -- '0.10.14+7d59c7ec9'

    #连接与文件传输

    命令行参数的基本顺序为:命令 服务器 导出路径 [操作参数]。下面的 server.example 和 /shared 需要替换为实际服务器地址和导出路径。

    moon run cmd/main -- ls server.example /shared / moon run cmd/main -- stat server.example /shared reports/data.bin moon run cmd/main -- get server.example /shared reports/data.bin ./data.bin moon run cmd/main -- put server.example /shared ./output.bin reports/output.bin moon run cmd/main -- mkdir server.example /shared reports/new moon run cmd/main -- mv server.example /shared reports/output.bin reports/new/output.bin moon run cmd/main -- rm server.example /shared reports/new/output.bin moon run cmd/main -- rmdir server.example /shared reports/new moon run cmd/main -- space server.example /shared

    get 创建新的本地文件,put 创建新的远端文件;目标已存在时会报错。下载会先检查远端源文件,上传会先打开并检查本地普通文件,再创建目标。传输中途失败可能留下不完整文件;普通上传、下载采用流式处理,不提供并发修改下的文件快照。mv 遵循服务器的 RENAME 语义,可能替换已存在的目标。

    默认 UID/GID 为 65534。AUTH_SYS 传输的是调用者声明的数字身份,不提供加密或强身份认证,应配合可信网络和正确的服务器导出权限使用。部分服务器要求客户端使用保留源端口,目前未提供绑定此类端口的接口。

    可通过以下环境变量调整命令行行为:

    环境变量默认值说明
    NFS_UID, NFS_GID65534AUTH_SYS 使用的 UID 和 GID
    NFS_PORT0NFS TCP 端口;0 表示通过 portmapper v2 发现
    NFS_MOUNT_PORT0MOUNTv3 TCP 端口;0 表示自动发现
    NFS_BLOCK_SIZE65536客户端传输缓冲区上限,单位为字节
    NFS_MAX_RECORD1048576RPC/XDR 记录的大小上限,单位为字节
    NFS_TIMEOUT_MS10000单次 RPC 的超时时间,单位为毫秒
    NFS_MAX_ENTRIES100000单目录条目上限;本地目录树传输等操作也将其用作总节点上限
    NFS_DEST_PORT, NFS_DEST_MOUNT_PORT源连接的端口配置跨连接复制时的目标端口
    NFS_DEST_UID, NFS_DEST_GID源连接的身份配置目标连接使用的 AUTH_SYS 身份

    verify 默认使用较小的传输块和记录上限(分别为 4096、8192 字节),用于检验分块和目录分页。它会创建、验证并删除一个独立命名的测试目录,请在可写的临时测试导出中运行。

    #目录传输、续传与原子发布

    moon run cmd/main -- tree server.example /shared reports moon run cmd/main -- cp-tree server.example /shared reports backups/reports moon run cmd/main -- get-tree server.example /shared reports ./reports-copy moon run cmd/main -- verify-tree server.example /shared ./reports-copy reports moon run cmd/main -- resume-get server.example /shared large.bin ./partial.bin moon run cmd/main -- resume-put server.example /shared ./large.bin partial.bin moon run cmd/main -- atomic-put server.example /shared ./result.bin result.bin .result-upload.tmp moon run cmd/main -- copy-to source-server /source data.bin target-server /target data.bin

    目录传输要求目标尚不存在。续传要求目标已存在,并逐字节校验已有前缀;前缀不一致或元数据校验发现文件变化时会报错。原子发布先写入新建的暂存文件,再发布到最终路径;失败或操作结果不明时,可能需要检查并处理暂存文件。

    完整参数、覆盖规则、资源上限和异步 I/O 用法见进阶使用说明。

    #作为库使用

    在 moon.pkg 中导入 "cauchyQ/moon-nfs3" @nfs,并在 moonbitlang/async 运行时中调用异步接口。以下代码应放在异步入口函数体内:

    let client = @nfs.connect_export("server.example", "/shared")
    defer client.close()
    let handle = client.resolve("reports/data.bin")
    let attrs = client.getattr(handle)
    let first = client.read_at(handle, 0UL, 1024)
    println("文件大小=\{attrs.size},首块字节数=\{first.data.length()}")

    connect_export 支持自动发现或显式指定端口,通过 MOUNTv3 获取根文件句柄,并通过 FSINFO 协商读写上限。Options 可配置 UID/GID、附加组、超时和资源上限。

    同一个客户端连接只允许同时执行一个 RPC;并行操作请使用独立客户端。NFS 文件句柄是不透明值,不需要像打开的文件描述符一样在服务器端关闭。close() 关闭 TCP 连接;如需清理 mountd 的挂载记录,可另行调用 unmount_export。

    功能主要接口
    属性与状态stat, getattr, metadata, access, readlink, space, fsinfo, pathconf, health_check, ping
    查找与路径root_handle, lookup, resolve, canonical_path, resolve_following, exists
    读取read_at, read_exactly_at, read_bytes, read_text, download_to, download, download_range, open_reader
    写入write_at, write_all, commit, write_committed, write_bytes, write_text, upload_from, upload, upload_range, open_writer
    文件管理create_file, mkdir, ensure_directory, setattr, set_attributes, chmod, chown, truncate, touch, rename, remove, symlink, hard_link, mknod
    目录遍历read_directory, list_dir, scan_directory, open_directory, walk, collect_tree, disk_usage
    复制与续传copy_file, copy_file_checked, copy_range, copy_tree, upload_tree, download_tree, resume_upload, resume_download
    原子发布upload_atomic, download_atomic, copy_atomic
    跨连接复制copy_file_to, copy_tree_to, resume_copy_to
    校验与诊断compare_files, compare_trees, verify_local_file, verify_local_tree, benchmark_read, statistics
    原始字节文件名lookup_bytes, create_entry, remove_entry, rename_entry

    read_directory 支持按页处理,list_dir 最多收集 Options.max_entries 个条目。原始目录名和符号链接目标使用 Bytes,高层路径使用 UTF-8 字符串。普通路径解析拒绝父级遍历,也不会隐式跟随符号链接;这些客户端规则不能替代服务器端的导出权限控制。

    stat 和下载会复用最后一次 LOOKUP 返回的文件属性,服务器已提供属性时可省去一次 GETATTR;属性缺失或查询导出根目录时会回退到 GETATTR,不跨调用缓存属性。

    #协议行为与支持范围

    • 短读按实际返回字节数推进位置,是否到达文件结尾以服务器的 EOF 标志为准;短写只继续发送尚未确认的数据。零进度、长度不一致和 64 位偏移溢出均会报错。
    • write_at、write_all 默认要求 FILE_SYNC,服务器返回更弱的承诺时会报错。write_committed 保留有大小上限的数据窗口,提交 UNSTABLE 写入;仍有未持久化的数据或元数据时再校验 COMMIT。若所有写入已确认 FILE_SYNC,则省去额外 COMMIT。待提交数据的服务器重启校验值发生变化时,会在有限重试次数内重放窗口;此机制不提供并发写入保护。
    • 目录 cookie 是不透明的分页标识。cookie 失效、校验值变化或分页游标循环时会报错,不会静默重新遍历。
    • RPC 帧格式错误、XID 不匹配或传输失败会关闭连接。修改操作的 RPC/NFS 回复丢失或无法解码时会报告 OutcomeUnknown,因为服务器可能已执行该操作,客户端不会盲目重试。
    • create_file 默认使用 GUARDED 模式。显式传入八字节校验值时改用 EXCLUSIVE 模式,并按协议要求随后设置属性。

    当前实现覆盖使用 TCP 和 AUTH_SYS 的 NFSv3 过程号 0 至 21。NFSv4、UDP、Kerberos/RPCSEC_GSS、NLM 锁及内核挂载不在本版本范围内。服务器状态码通过 NfsError::Status 保留;特殊节点创建及其他可选能力仍取决于服务器支持和导出权限。

    #测试与验证

    moon check --deny-warn moon build --deny-warn moon test --deny-warn moon info moon fmt git diff --exit-code python validation/source_lines.py --min-implementation 4000

    故障测试使用独立的模拟 TCP 对端,覆盖短读写、64 位偏移、写入校验值变化、XID 不匹配、回复丢失、过期目录 cookie 和分页循环等场景。该对端仅为测试夹具。

    完整联调使用 NFS-Ganesha 4.3 的 VFS 后端,在真实本地文件系统上验证指定端口和端口发现两种连接方式,覆盖二进制及空文件往返、SHA-256 比对、多页目录、属性与时间戳、链接和 FIFO 创建、COMMIT、重命名、截断、清理、覆盖保护、重叠区间复制、续传、原子发布、目录树及跨连接复制。CI 会重复执行这些外部服务器检查。

    复现步骤和测试证据见真实服务器联调说明。尚未验证断电后的物理持久性、大型稀疏文件、其他 NAS 产品或与其他客户端的性能对比。

    validation/source_lines.py 会分别统计实现和测试代码,实现行数排除空行、整行注释、依赖、生成接口、测试文件、内嵌测试及 CLI 的 verify 自检代码。CI 会检查实现代码保持在 4,000 行以上;代码行数不能替代行为验证。

    #参考资料与来源说明

    协议实现依据:

    设计背景参考 Sandberg 等人的 Design and Implementation of the Sun Network Filesystem(USENIX 1985),以及 Pawlowski 等人的 NFS Version 3: Design and Implementation(USENIX 1994)。协议行为以 RFC 为准,不以历史论文中的性能数字作为本项目的性能依据。

    #许可证

    本项目采用 Apache-2.0,详见 LICENSE,开发过程中使用了 AI 辅助。MoonXDR 采用 MIT 许可证,官方 MoonBit core/async 采用 Apache-2.0。仓库未打包 libnfs、go-nfs-client 或 NFS-Ganesha 的源码;测试数据由本地生成,Ganesha 配置与验证脚本由本项目编写。

    NfsError

    pub suberror NfsError {
    InvalidArgument(String)
    ProtocolError(String)
    RpcFailure(String)
    Status(UInt)
    OutcomeUnknown(String)
    }

    impl Show for NfsError

    NfsError::message

    fn NfsError::message(self : NfsError) -> String

    NfsError::output

    fn NfsError::output(self : NfsError, logger : &Logger) -> Unit

    NfsError::to_string

    fn NfsError::to_string(self : NfsError) -> String

    AttributeUpdate

    pub(all) struct AttributeUpdate {
    mode : UInt?
    uid : UInt?
    gid : UInt?
    size : UInt64?
    accessed : TimeUpdate
    modified : TimeUpdate
    }

    AttributeUpdate::default

    AttributeUpdate::validate

    fn AttributeUpdate::validate(self : AttributeUpdate) -> Unit raise

    Attributes

    pub(all) struct Attributes {
    kind : UInt
    mode : UInt
    links : UInt
    uid : UInt
    gid : UInt
    size : UInt64
    used : UInt64
    fsid : UInt64
    fileid : UInt64
    modified_seconds : UInt
    }

    Client

    pub struct Client {
    // private fields
    }

    Client::access

    async fn Client::access(self : Client, handle : Handle, requested : UInt) -> UInt

    Client::benchmark_read

    async fn Client::benchmark_read(self : Client, path : String, rounds? : Int, max_bytes? : UInt64) -> ReadBenchmark

    Measures real reads through this connection, with no application-level cache. Server/OS caches can affect results. Time is wall-clock milliseconds.

    Client::canonical_path

    async fn Client::canonical_path(self : Client, path : String, max_links? : Int) -> String

    Explicit link following; absolute targets are relative to the export root. Targets escaping that root are rejected. resolve() itself never follows links.

    Client::chmod

    async fn Client::chmod(self : Client, path : String, mode : UInt) -> Unit

    Client::chown

    async fn Client::chown(self : Client, path : String, uid : UInt?, gid : UInt?) -> Unit

    Client::close

    fn Client::close(self : Client) -> Unit

    Closes the transport. Use unmount_export separately to remove mountd bookkeeping.

    Client::collect_tree

    async fn Client::collect_tree(self : Client, path : String, max_depth? : Int, max_nodes? : Int) -> Array[TreeEntry]

    Client::commit

    async fn Client::commit(self : Client, handle : Handle, offset : UInt64, count : UInt) -> Bytes

    Client::compare_files

    async fn Client::compare_files(self : Client, left : String, right : String) -> FileComparison

    Compares byte content using bounded blocks, not timestamps or file sizes alone.

    Client::compare_trees

    async fn Client::compare_trees(self : Client, left : String, right : String, compare_mode? : Bool) -> Array[TreeDifference]

    Client::copy_atomic

    async fn Client::copy_atomic(self : Client, source : String, destination : String, staging : String, replace? : Bool) -> UInt64

    Copy to a fresh sibling staging file, validate the source version, then publish. No-replace publication uses LINK so another creator cannot be overwritten.

    Client::copy_file

    async fn Client::copy_file(self : Client, source : String, destination : String, preserve_mode? : Bool) -> UInt64

    Client::copy_file_checked

    async fn Client::copy_file_checked(self : Client, source : String, destination : String) -> UInt64

    Detects changes visible in the server's size, identity and timestamp fields. This is optimistic validation, not a lock or a filesystem snapshot.

    Client::copy_file_to

    async fn Client::copy_file_to(self : Client, destination_client : Client, source : String, destination : String, preserve_mode? : Bool) -> UInt64

    Client::copy_range

    async fn Client::copy_range(self : Client, source : Handle, destination : Handle, source_offset : UInt64, destination_offset : UInt64, length : UInt64) -> UInt64

    Bounded memmove-style copy. Overlapping ranges in one object copy backwards when needed, including when different handles name the same hard-linked inode.

    Client::copy_tree

    async fn Client::copy_tree(self : Client, source : String, destination : String, preserve_mode? : Bool) -> TreeTransfer

    Client::copy_tree_to

    async fn Client::copy_tree_to(self : Client, destination_client : Client, source : String, destination : String, preserve_mode? : Bool) -> TreeTransfer

    Copies a pre-collected tree into a fresh destination on another connection. Local buffering is bounded by the source block size; modes are restored last.

    Client::create_entry

    async fn Client::create_entry(self : Client, parent : Handle, name : Bytes, mode? : UInt, directory? : Bool) -> Handle

    Client::create_file

    async fn Client::create_file(self : Client, path : String, mode? : UInt, verifier? : Bytes?) -> Handle

    GUARDED create by default: existing files are not overwritten. An eight-byte verifier selects EXCLUSIVE create for caller-managed replay.

    Client::disk_usage

    async fn Client::disk_usage(self : Client, path : String) -> DiskUsage

    Client::download

    async fn Client::download(self : Client, remote_path : String, local_path : String) -> UInt64

    Creates a new local file; existing destinations are never truncated.

    Client::download_atomic

    async fn Client::download_atomic(self : Client, source : String, destination : String, staging : String) -> UInt64

    Staging is created exclusively and synced before a no-replace local rename. The caller should put staging and destination on the same local filesystem.

    Client::download_range

    async fn[W :
    Writer
    ] Client::download_range(self : Client, handle : Handle, sink : W, offset : UInt64, length : UInt64) -> UInt64

    Client::download_to

    async fn[W :
    Writer
    ] Client::download_to(self : Client, path : String, sink : W) -> UInt64

    Streams until the server's EOF. Concurrent file changes are not a snapshot.

    Client::download_tree

    async fn Client::download_tree(self : Client, remote_path : String, local_path : String) -> TreeTransfer

    Creates a fresh local tree. Rejects links and special nodes before writing files.

    Client::ensure_directory

    async fn Client::ensure_directory(self : Client, path : String, mode? : UInt) -> Handle

    Client::exists

    async fn Client::exists(self : Client, path : String) -> Bool

    Client::fsinfo

    async fn Client::fsinfo(self : Client, handle? : Handle?) -> FileSystemInfo

    Client::getattr

    async fn Client::getattr(self : Client, handle : Handle) -> Attributes

    async fn Client::hard_link(self : Client, source : String, destination : String) -> Unit

    Client::health_check

    async fn Client::health_check(self : Client, path? : String) -> HealthReport

    Read-only service diagnostics: validates the RPC endpoint, selected object, access mask, negotiated filesystem capabilities and capacity replies.

    Client::list_dir

    async fn Client::list_dir(self : Client, path : String, plus? : Bool) -> Array[DirectoryEntry]

    A bounded collection for a static directory, not a concurrent directory snapshot. BAD_COOKIE is surfaced to the caller rather than silently restarting traversal.

    Client::lookup

    async fn Client::lookup(self : Client, directory : Handle, name : String) -> Handle

    Client::lookup_bytes

    async fn Client::lookup_bytes(self : Client, parent : Handle, name : Bytes) -> Handle

    Handle-relative operations preserve non-UTF-8 filenames without lossy decoding.

    Client::metadata

    async fn Client::metadata(self : Client, handle : Handle) -> Metadata

    Client::mkdir

    async fn Client::mkdir(self : Client, path : String, mode? : UInt) -> Handle

    Client::mknod

    async fn Client::mknod(self : Client, path : String, kind : SpecialNode, mode? : UInt) -> Handle

    Client::negotiate

    async fn Client::negotiate(self : Client) -> Unit

    Sets transfer block sizes using FSINFO; no file data is cached.

    Client::open_directory

    async fn Client::open_directory(self : Client, path : String, plus? : Bool) -> DirectoryCursor

    Client::open_reader

    async fn Client::open_reader(self : Client, path : String, offset? : UInt64, length? : UInt64?) ->
    MemoryReader

    Bridges NFS reads to async/io using the public backpressured pipe adapter. Close the returned reader to cancel a partially consumed transfer.

    Client::open_writer

    async fn Client::open_writer(self : Client, path : String, mode? : UInt) -> RemoteWriter

    Client::pathconf

    async fn Client::pathconf(self : Client, handle? : Handle?) -> PathConfig

    Client::ping

    async fn Client::ping(self : Client) -> Unit

    Client::read_at

    async fn Client::read_at(self : Client, handle : Handle, offset : UInt64, count : Int) -> ReadResult

    Client::read_bytes

    async fn Client::read_bytes(self : Client, path : String, max_bytes? : Int) -> Bytes

    Client::read_directory

    async fn Client::read_directory(self : Client, directory : Handle, cookie? : UInt64, verifier? : Bytes, plus? : Bool) -> DirectoryPage

    Client::read_exactly_at

    async fn Client::read_exactly_at(self : Client, handle : Handle, offset : UInt64, count : Int) -> Bytes

    Reads an exact bounded range. EOF before count is an error, even after a short read.

    Client::read_text

    async fn Client::read_text(self : Client, path : String, max_bytes? : Int) -> String

    async fn Client::readlink(self : Client, handle : Handle) -> Bytes

    Client::remove

    async fn Client::remove(self : Client, path : String, directory? : Bool) -> Unit

    Client::remove_entry

    async fn Client::remove_entry(self : Client, parent : Handle, name : Bytes, directory? : Bool) -> Unit

    Client::remove_tree

    async fn Client::remove_tree(self : Client, path : String) -> Int

    Collects and validates the traversal before the first deletion. Never follows links.

    Client::rename

    async fn Client::rename(self : Client, source : String, destination : String) -> Unit

    Client::rename_entry

    async fn Client::rename_entry(self : Client, source_parent : Handle, source_name : Bytes, destination_parent : Handle, destination_name : Bytes) -> Unit

    Client::require_version

    async fn Client::require_version(self : Client, handle : Handle, expected : FileVersion) -> Unit

    Client::resolve

    async fn Client::resolve(self : Client, path : String) -> Handle

    Client::resolve_following

    async fn Client::resolve_following(self : Client, path : String, max_links? : Int) -> Handle

    Client::resume_copy_to

    async fn Client::resume_copy_to(self : Client, destination_client : Client, source : String, destination : String) -> UInt64

    Resumes an existing destination after comparing all prefix bytes. Distinct client connections allow source and destination to be different exports.

    Client::resume_download

    async fn Client::resume_download(self : Client, remote_path : String, local_path : String) -> UInt64

    Resume only after verifying every existing prefix byte. Files are not snapshots.

    Client::resume_upload

    async fn Client::resume_upload(self : Client, local_path : String, remote_path : String) -> UInt64

    Client::root_handle

    fn Client::root_handle(self : Client) -> Handle

    Client::scan_directory

    fn Client::scan_directory(self : Client, handle : Handle, plus? : Bool) -> DirectoryCursor

    Client::set_attributes

    async fn Client::set_attributes(self : Client, handle : Handle, update : AttributeUpdate, ctime_guard? : Timestamp?) -> Unit

    Client::setattr

    async fn Client::setattr(self : Client, handle : Handle, mode? : UInt?, size? : UInt64?) -> Unit

    Client::space

    async fn Client::space(self : Client, handle? : Handle?) -> Space

    Client::stat

    async fn Client::stat(self : Client, path : String) -> Attributes

    Client::statistics

    fn Client::statistics(self : Client) -> RpcStatistics

    async fn Client::symlink(self : Client, path : String, target : Bytes, mode? : UInt) -> Handle

    Client::touch

    async fn Client::touch(self : Client, path : String) -> Unit

    Client::truncate

    async fn Client::truncate(self : Client, path : String, size : UInt64) -> Unit

    Client::upload

    async fn Client::upload(self : Client, local_path : String, remote_path : String, committed? : Bool) -> UInt64

    Client::upload_atomic

    async fn Client::upload_atomic(self : Client, local_path : String, destination : String, staging : String, replace? : Bool, mode? : UInt) -> UInt64

    Publish a complete upload with LINK (no replacement) or RENAME (replacement). A failed or ambiguous operation can leave staging behind for caller recovery.

    Client::upload_from

    async fn[R :
    Reader
    ] Client::upload_from(self : Client, path : String, source : R, committed? : Bool) -> UInt64

    Streams to a fresh remote file. On failure, a partial file can remain.

    Client::upload_range

    async fn[R :
    Reader
    ] Client::upload_range(self : Client, handle : Handle, source : R, offset : UInt64, length : UInt64, committed? : Bool) -> UInt64

    Client::upload_tree

    async fn Client::upload_tree(self : Client, local_path : String, remote_path : String) -> TreeTransfer

    Creates a fresh remote tree. Special nodes and local symlinks are rejected.

    Client::verify_local_file

    async fn Client::verify_local_file(self : Client, local_path : String, remote_path : String) -> FileComparison

    Client::verify_local_tree

    async fn Client::verify_local_tree(self : Client, local_path : String, remote_path : String) -> LocalTreeComparison

    Verifies directory round trips by reading each regular file's content.

    Client::walk

    async fn Client::walk(self : Client, path : String, max_depth? : Int, max_nodes? : Int) -> TreeWalker

    Client::write_all

    async fn Client::write_all(self : Client, handle : Handle, offset : UInt64, data : Bytes) -> Unit

    Stable bounded-buffer writes, correctly advancing after short writes.

    Client::write_at

    async fn Client::write_at(self : Client, handle : Handle, offset : UInt64, data : Bytes, stability? : Stability) -> WriteResult

    Client::write_bytes

    async fn Client::write_bytes(self : Client, path : String, bytes : Bytes, mode? : UInt) -> Handle

    Client::write_committed

    async fn Client::write_committed(self : Client, handle : Handle, offset : UInt64, data : Bytes, retries? : Int) -> Unit

    Writes one bounded replay window, verifying COMMIT for pending data or metadata. Retains the data until durability is confirmed; recovery has a finite retry budget.

    Client::write_text

    async fn Client::write_text(self : Client, path : String, text : String, mode? : UInt) -> Handle

    Client::writer_for

    fn Client::writer_for(self : Client, handle : Handle, offset? : UInt64) -> RemoteWriter

    DirectoryCursor

    pub struct DirectoryCursor {
    // private fields
    }

    DirectoryCursor::close

    fn DirectoryCursor::close(self : DirectoryCursor) -> Unit

    DirectoryCursor::next

    DirectoryEntry

    pub(all) struct DirectoryEntry {
    fileid : UInt64
    name : Bytes
    cookie : UInt64
    attributes : Attributes?
    handle : Handle?
    }

    DirectoryPage

    pub(all) struct DirectoryPage {
    entries : Array[DirectoryEntry]
    verifier : Bytes
    eof : Bool
    }

    DiskUsage

    pub(all) struct DiskUsage {
    files : UInt64
    directories : UInt64
    symlinks : UInt64
    special : UInt64
    logical_bytes : UInt64
    allocated_bytes : UInt64
    }

    Export

    pub(all) struct Export {
    directory : Bytes
    groups : Array[Bytes]
    }

    FileComparison

    pub(all) struct FileComparison {
    equal : Bool
    compared_bytes : UInt64
    first_difference : UInt64?
    }

    FileSystemInfo

    pub(all) struct FileSystemInfo {
    read_max : UInt
    read_preferred : UInt
    read_multiple : UInt
    write_max : UInt
    write_preferred : UInt
    write_multiple : UInt
    directory_preferred : UInt
    max_file_size : UInt64
    time_delta : Timestamp
    properties : UInt
    }

    FileSystemInfo::can_set_time

    fn FileSystemInfo::can_set_time(self : FileSystemInfo) -> Bool

    fn FileSystemInfo::supports_links(self : FileSystemInfo) -> Bool

    fn FileSystemInfo::supports_symlinks(self : FileSystemInfo) -> Bool

    FileVersion

    pub(all) struct FileVersion {
    fsid : UInt64
    fileid : UInt64
    size : UInt64
    modified : Timestamp
    changed : Timestamp
    }

    FileVersion::matches

    fn FileVersion::matches(self : FileVersion, other : FileVersion) -> Bool

    Handle

    pub(all) struct Handle {
    bytes : Bytes
    }

    Owned opaque file handle. Instances are validated before each wire use.

    HealthReport

    pub(all) struct HealthReport {
    attributes : Attributes
    granted_access : UInt
    filesystem : FileSystemInfo
    space : Space
    paths : PathConfig
    rpc : RpcStatistics
    elapsed_ms : Int64
    }

    HealthReport::to_json

    fn HealthReport::to_json(self : HealthReport) -> Json

    LocalTreeComparison

    pub(all) struct LocalTreeComparison {
    differences : Array[TreeDifference]
    compared_bytes : UInt64
    }

    Metadata

    pub(all) struct Metadata {
    attributes : Attributes
    device_major : UInt
    device_minor : UInt
    accessed : Timestamp
    modified : Timestamp
    changed : Timestamp
    }

    Metadata::version

    fn Metadata::version(self : Metadata) -> FileVersion

    MountEntry

    pub(all) struct MountEntry {
    host : Bytes
    directory : Bytes
    }

    Options

    pub(all) struct Options {
    uid : UInt
    gid : UInt
    groups : Array[UInt]
    timeout_ms : Int
    block_size : Int
    max_record : Int
    max_entries : Int
    }

    TCP connections are sequential: one outstanding RPC per connection.

    Options::default

    fn Options::default() -> Options

    Options::validate

    fn Options::validate(self : Options) -> Unit raise NfsError

    PathConfig

    pub(all) struct PathConfig {
    link_max : UInt
    name_max : UInt
    no_truncation : Bool
    chown_restricted : Bool
    case_insensitive : Bool
    case_preserving : Bool
    }

    ReadBenchmark

    pub(all) struct ReadBenchmark {
    rounds : Int
    bytes : UInt64
    elapsed_ms : Int64
    fastest_ms : Int64
    slowest_ms : Int64
    rpc : RpcStatistics
    }

    ReadBenchmark::mebibytes_per_second

    fn ReadBenchmark::mebibytes_per_second(self : ReadBenchmark) -> Double

    ReadResult

    pub(all) struct ReadResult {
    data : Bytes
    eof : Bool
    }

    RemoteWriter

    pub struct RemoteWriter {
    // private fields
    }

    RemoteWriter::close

    fn RemoteWriter::close(self : RemoteWriter) -> Unit

    All accepted data uses FILE_SYNC; closing releases no server-side descriptor.

    RemoteWriter::position

    fn RemoteWriter::position(self : RemoteWriter) -> UInt64

    RemoteWriter::write

    async fn RemoteWriter::write(self : RemoteWriter, data : &
    Data
    ) -> Unit

    RemoteWriter::write_once

    async fn RemoteWriter::write_once(self : RemoteWriter, bytes : Bytes, offset~ : Int, len~ : Int) -> Int

    RemoteWriter::write_reader

    async fn RemoteWriter::write_reader(self : RemoteWriter, reader : &
    Reader
    ) -> Unit

    Rpc

    pub struct Rpc {
    // private fields
    }

    Rpc::call

    async fn Rpc::call(self : Rpc, program : UInt, version : UInt, procedure : UInt, arguments : Array[Byte], modifying? : Bool) ->
    Reader

    Rpc::close

    fn Rpc::close(self : Rpc) -> Unit

    Rpc::connect

    async fn Rpc::connect(host : String, port : Int, options? : Options) -> Rpc

    Rpc::statistics

    fn Rpc::statistics(self : Rpc) -> RpcStatistics

    RpcStatistics

    pub(all) struct RpcStatistics {
    calls : UInt64
    replies : UInt64
    request_bytes : UInt64
    response_bytes : UInt64
    failures : UInt64
    }

    Payload counts exclude TCP and record-marker overhead. Requests count attempts; a lost reply can follow a request the server has already executed.

    RpcStatistics::since

    fn RpcStatistics::since(self : RpcStatistics, before : RpcStatistics) -> RpcStatistics raise

    ServiceMapping

    pub(all) struct ServiceMapping {
    program : UInt
    version : UInt
    protocol : UInt
    port : UInt
    }

    Space

    pub(all) struct Space {
    total_bytes : UInt64
    free_bytes : UInt64
    available_bytes : UInt64
    total_files : UInt64
    free_files : UInt64
    available_files : UInt64
    invariant_seconds : UInt
    }

    SpecialNode

    pub(all) enum SpecialNode {
    BlockDevice(UInt, UInt)
    CharacterDevice(UInt, UInt)
    UnixSocket
    NamedPipe
    }

    Stability

    pub(all) enum Stability {
    Unstable
    DataSync
    FileSync
    }

    TimeUpdate

    pub(all) enum TimeUpdate {
    KeepTime
    ServerTime
    ExactTime(Timestamp)
    }

    Timestamp

    pub(all) struct Timestamp {
    seconds : UInt
    nanoseconds : UInt
    }

    Timestamp::validate

    fn Timestamp::validate(self : Timestamp) -> Unit raise NfsError

    TreeDifference

    pub(all) struct TreeDifference {
    relative : String
    kind : TreeDifferenceKind
    }

    TreeDifferenceKind

    pub(all) enum TreeDifferenceKind {
    MissingLeft
    MissingRight
    DifferentType
    DifferentSize
    DifferentContent
    DifferentMode
    DifferentLinkTarget
    }

    TreeDifferenceKind::name

    fn TreeDifferenceKind::name(self : TreeDifferenceKind) -> String

    TreeEntry

    pub(all) struct TreeEntry {
    path : String
    relative : String
    depth : Int
    handle : Handle
    attributes : Attributes
    }

    TreeTransfer

    pub(all) struct TreeTransfer {
    files : Int
    directories : Int
    symlinks : Int
    bytes : UInt64
    }

    TreeWalker

    pub struct TreeWalker {
    // private fields
    }

    TreeWalker::close

    fn TreeWalker::close(self : TreeWalker) -> Unit

    TreeWalker::next

    async fn TreeWalker::next(self : TreeWalker) -> TreeEntry?

    WriteResult

    pub(all) struct WriteResult {
    count : Int
    committed : Stability
    verifier : Bytes
    }

    checked_offset

    fn checked_offset(offset : UInt64, count : Int) -> UInt64 raise NfsError

    connect_export

    async fn connect_export(host : String, export_path : String, mount_port? : Int, nfs_port? : Int, options? : Options) -> Client

    Explicit ports avoid portmapper when desired. No system mount is performed.

    file_kind_name

    fn file_kind_name(kind : UInt) -> String

    join_path

    fn join_path(parent : String, child : String) -> String raise NfsError

    list_exports

    async fn list_exports(host : String, mount_port? : Int, options? : Options) -> Array[Export]

    list_mounts

    async fn list_mounts(host : String, mount_port? : Int, options? : Options) -> Array[MountEntry]

    list_services

    async fn list_services(host : String, options? : Options) -> Array[ServiceMapping]

    normalize_path

    fn normalize_path(path : String) -> String raise NfsError

    path_components

    fn path_components(path : String) -> Array[String] raise NfsError

    Rejects parent traversal. Intermediate symbolic links are not followed.

    permission_string

    fn permission_string(kind : UInt, mode : UInt) -> String

    relative_path

    fn relative_path(base : String, path : String) -> String raise NfsError

    service_port

    async fn service_port(host : String, program : UInt, version : UInt, options? : Options) -> Int

    Resolve an IPv4 TCP service through portmapper v2. Port zero is unavailable.

    status_name

    fn status_name(code : UInt) -> String

    unmount_export

    async fn unmount_export(host : String, export_path : String, mount_port? : Int, options? : Options) -> Unit