Tahr.NFSv3.Backend behaviour (tahr v0.1.0)

Copy Markdown View Source

Behaviour for the filesystem layer the NFSv3 handler delegates to.

Keeps Tahr decoupled from any particular backing store — the same handler can be wired to an in-memory test fixture, a posix filesystem mount, a clustered storage layer, etc. The procedure handler itself is purely XDR-decode → callback → XDR-encode; every semantic decision lives in the backend.

Callback shape

Every callback returns {:ok, ...} | {:error, nfsstat3()}. The handler maps the status atom to the wire integer. Where the RFC expects a post_op_attr even on failure (most procedures), the handler attaches nil if the backend doesn't supply one, or the backend can return {:error, nfsstat3, post_op_attr} for a richer error reply (see individual callbacks).

ctx

The ctx map is the RPC context the dispatcher hands to the handler — %{call: Tahr.RPC.Message.Call.t()} plus any keys downstream code attaches (auth credential, peer address, etc.). Treat it as opaque; pull what you need by key.

Summary

Types

Opaque context — the handler passes the RPC ctx through.

RFC 1813 §3.3.19 FSINFO reply body. properties is an OR of the FSF3 flag constants (see Tahr.NFSv3.Types-adjacent documentation in the handler).

RFC 1813 §3.3.18 FSSTAT reply body (everything except the leading post_op_attr).

RFC 1813 §3.3.20 PATHCONF reply body.

Read-back from read/5 — a chunk-iterator (any Enumerable.t() whose elements are binaries), the EOF flag, and a post-op attr for the file. The iterator must not materialise the whole file in memory — the handler streams chunks straight to the socket.

An entry returned by readdir/6{fileid, name, cookie}. Cookies are opaque to the handler; the backend chooses how they encode position.

An entry returned by readdirplus/7readdir_entry plus a post_op_attr and post_op_fh3 per RFC 1813 §3.3.17. Both optional fields may be nil.

Callbacks

ACCESS — RFC 1813 §3.3.4. granted_mask is the subset of requested_mask (the same ACCESS3_* bit flags) the server is willing to permit. Even on success the server includes a post_op_attr so the client can refresh its cache.

COMMIT — RFC 1813 §3.3.21. Flush any unstable writes covering [offset, offset+count) to stable storage. Returns the parent file's wcc_data plus the per-instance writeverf3 — clients use the verf to detect a server restart and resend lost unstable writes.

CREATE — RFC 1813 §3.3.8. Create a regular file under dir_fh / name. The mode discriminated union (createhow3) selects

FSINFO — RFC 1813 §3.3.19.

FSSTAT — RFC 1813 §3.3.18.

GETATTR — RFC 1813 §3.3.1.

LINK — RFC 1813 §3.3.15. Create a hard link from link_dir_fh / name to file_fh. Servers without hard-link support return NFS3ERR_NOTSUPP per RFC 1813.

LOOKUP — RFC 1813 §3.3.3. Returns the looked-up file's handle, its optional post_op_attr, and the directory's post_op_attr. On failure, the directory post_op_attr is still returned where available.

MKDIR — RFC 1813 §3.3.9. Create a directory at dir_fh / name. Reply carries the new dir's optional post_op_fh3, optional post_op_attr, and the parent's wcc_data.

MKNOD — RFC 1813 §3.3.11. Create a special file (block / char device, FIFO, or socket). RFC 1813 explicitly allows servers that don't support special files to return NFS3ERR_NOTSUPP; most non-POSIX-stack servers do exactly that.

PATHCONF — RFC 1813 §3.3.20.

READ — RFC 1813 §3.3.6. The data field of the success reply must be a chunk-iterator (Enumerable.t() of binary segments) whose total bytes equal at most count. Returning a flat binary is equivalent to a one-element iterator but defeats the streaming invariant the handler relies on.

READDIR — RFC 1813 §3.3.16. The handler decides where to splice in . / ..; the backend just returns its own entries. Cookies are opaque — the backend may encode position in any way it prefers.

READDIRPLUS — RFC 1813 §3.3.17. dircount is the byte budget for entry names; maxcount is the byte budget for the whole reply (including post-op attrs and fhandles). The backend should honour maxcount and stop early if the next entry would push the budget over.

READLINK — RFC 1813 §3.3.5.

REMOVE — RFC 1813 §3.3.12. Unlink a file by (dir_fh, name). Reply carries the parent's wcc_data.

RENAME — RFC 1813 §3.3.14. Cross-directory rename. The reply carries wcc_data for both parent directories (fromdir_wcc and todir_wcc) so the client can refresh the parent caches on either side.

RMDIR — RFC 1813 §3.3.13. Remove an empty directory by (dir_fh, name). Returns NFS3ERR_NOTEMPTY if the directory is non-empty and NFS3ERR_NOTDIR if the target is not a directory. Reply carries the parent's wcc_data.

SETATTR — RFC 1813 §3.3.2. Apply the requested sattr3 fields (any field set to nil means "leave unchanged"; :atime / :mtime carry RFC 1813's time_set semantics — :set_to_server_time or {:client, %Nfstime3{}}).

SYMLINK — RFC 1813 §3.3.10. Create a symbolic link at dir_fh / name pointing at target (an opaque NFS path string — resolution is the client's responsibility, the server just stores bytes). Reply carries the new symlink's optional post_op_fh3, optional post_op_attr, and the parent's wcc_data.

WRITE — RFC 1813 §3.3.7. Write data at offset with the requested stable hint. The server returns the actual count written (may be less than the request), the stability level it achieved (must be >= stable requested), and the per-instance writeverf3. Backends that always commit synchronously may return :file_sync regardless of the requested stability — the client gets stronger guarantees than asked for, which RFC 1813 permits.

Types

ctx()

@type ctx() :: map()

Opaque context — the handler passes the RPC ctx through.

fsinfo_reply()

@type fsinfo_reply() :: %{
  rtmax: non_neg_integer(),
  rtpref: non_neg_integer(),
  rtmult: non_neg_integer(),
  wtmax: non_neg_integer(),
  wtpref: non_neg_integer(),
  wtmult: non_neg_integer(),
  dtpref: non_neg_integer(),
  maxfilesize: non_neg_integer(),
  time_delta: Tahr.NFSv3.Types.Nfstime3.t(),
  properties: non_neg_integer()
}

RFC 1813 §3.3.19 FSINFO reply body. properties is an OR of the FSF3 flag constants (see Tahr.NFSv3.Types-adjacent documentation in the handler).

fsstat_reply()

@type fsstat_reply() :: %{
  tbytes: non_neg_integer(),
  fbytes: non_neg_integer(),
  abytes: non_neg_integer(),
  tfiles: non_neg_integer(),
  ffiles: non_neg_integer(),
  afiles: non_neg_integer(),
  invarsec: non_neg_integer()
}

RFC 1813 §3.3.18 FSSTAT reply body (everything except the leading post_op_attr).

pathconf_reply()

@type pathconf_reply() :: %{
  linkmax: non_neg_integer(),
  name_max: non_neg_integer(),
  no_trunc: boolean(),
  chown_restricted: boolean(),
  case_insensitive: boolean(),
  case_preserving: boolean()
}

RFC 1813 §3.3.20 PATHCONF reply body.

read_reply()

@type read_reply() :: %{
  :data => Enumerable.t(),
  :eof => boolean(),
  optional(:post_op) => Tahr.NFSv3.Types.Fattr3.t() | nil
}

Read-back from read/5 — a chunk-iterator (any Enumerable.t() whose elements are binaries), the EOF flag, and a post-op attr for the file. The iterator must not materialise the whole file in memory — the handler streams chunks straight to the socket.

readdir_entry()

An entry returned by readdir/6{fileid, name, cookie}. Cookies are opaque to the handler; the backend chooses how they encode position.

readdirplus_entry()

An entry returned by readdirplus/7readdir_entry plus a post_op_attr and post_op_fh3 per RFC 1813 §3.3.17. Both optional fields may be nil.

Callbacks

access(fhandle3, requested_mask, credential, ctx)

@callback access(
  Tahr.NFSv3.Types.fhandle3(),
  requested_mask :: non_neg_integer(),
  Tahr.RPC.Auth.credential(),
  ctx()
) ::
  {:ok, granted_mask :: non_neg_integer(), Tahr.NFSv3.Types.Fattr3.t() | nil}
  | {:error, Tahr.NFSv3.Types.nfsstat3(), Tahr.NFSv3.Types.Fattr3.t() | nil}

ACCESS — RFC 1813 §3.3.4. granted_mask is the subset of requested_mask (the same ACCESS3_* bit flags) the server is willing to permit. Even on success the server includes a post_op_attr so the client can refresh its cache.

commit(fh, offset, count, credential, ctx)

COMMIT — RFC 1813 §3.3.21. Flush any unstable writes covering [offset, offset+count) to stable storage. Returns the parent file's wcc_data plus the per-instance writeverf3 — clients use the verf to detect a server restart and resend lost unstable writes.

count == 0 means "commit through end-of-file", per RFC 1813.

create(dir_fh, name, mode, credential, ctx)

CREATE — RFC 1813 §3.3.8. Create a regular file under dir_fh / name. The mode discriminated union (createhow3) selects:

  • {:unchecked, %Sattr3{}} — overwrite if exists.
  • {:guarded, %Sattr3{}} — fail with NFS3ERR_EXIST if exists.
  • {:exclusive, createverf3} — atomic create-if-not-exists keyed by the 8-byte verifier. Retried CREATE with the same verf must be idempotent: the second call observes the same file rather than NFS3ERR_EXIST.

On success returns the new file's optional post_op_fh3 plus its post_op_attr, plus the parent directory's wcc_data. On failure returns the parent's wcc_data so the client can refresh its cache.

fsinfo(fhandle3, credential, ctx)

FSINFO — RFC 1813 §3.3.19.

fsstat(fhandle3, credential, ctx)

FSSTAT — RFC 1813 §3.3.18.

getattr(fhandle3, credential, ctx)

GETATTR — RFC 1813 §3.3.1.

link(file_fh, link_dir_fh, name, credential, ctx)

LINK — RFC 1813 §3.3.15. Create a hard link from link_dir_fh / name to file_fh. Servers without hard-link support return NFS3ERR_NOTSUPP per RFC 1813.

lookup(dir, name, credential, ctx)

LOOKUP — RFC 1813 §3.3.3. Returns the looked-up file's handle, its optional post_op_attr, and the directory's post_op_attr. On failure, the directory post_op_attr is still returned where available.

mkdir(dir_fh, name, attrs, credential, ctx)

MKDIR — RFC 1813 §3.3.9. Create a directory at dir_fh / name. Reply carries the new dir's optional post_op_fh3, optional post_op_attr, and the parent's wcc_data.

mknod(dir_fh, name, credential, ctx)

MKNOD — RFC 1813 §3.3.11. Create a special file (block / char device, FIFO, or socket). RFC 1813 explicitly allows servers that don't support special files to return NFS3ERR_NOTSUPP; most non-POSIX-stack servers do exactly that.

pathconf(fhandle3, credential, ctx)

PATHCONF — RFC 1813 §3.3.20.

read(fhandle3, offset3, count3, credential, ctx)

READ — RFC 1813 §3.3.6. The data field of the success reply must be a chunk-iterator (Enumerable.t() of binary segments) whose total bytes equal at most count. Returning a flat binary is equivalent to a one-element iterator but defeats the streaming invariant the handler relies on.

readdir(fhandle3, cookie, cookieverf, count, credential, ctx)

READDIR — RFC 1813 §3.3.16. The handler decides where to splice in . / ..; the backend just returns its own entries. Cookies are opaque — the backend may encode position in any way it prefers.

readdirplus(fhandle3, cookie, cookieverf, dircount, maxcount, credential, ctx)

READDIRPLUS — RFC 1813 §3.3.17. dircount is the byte budget for entry names; maxcount is the byte budget for the whole reply (including post-op attrs and fhandles). The backend should honour maxcount and stop early if the next entry would push the budget over.

readlink(fhandle3, credential, ctx)

READLINK — RFC 1813 §3.3.5.

remove(dir_fh, name, credential, ctx)

REMOVE — RFC 1813 §3.3.12. Unlink a file by (dir_fh, name). Reply carries the parent's wcc_data.

rename(from_dir, from_name, to_dir, to_name, credential, ctx)

RENAME — RFC 1813 §3.3.14. Cross-directory rename. The reply carries wcc_data for both parent directories (fromdir_wcc and todir_wcc) so the client can refresh the parent caches on either side.

Backends that fence cross-node renames atomically should return NFS3ERR_INVAL for cycles like rename(/a, /a/b) and NFS3ERR_NOTEMPTY if the destination is a non-empty directory.

rmdir(dir_fh, name, credential, ctx)

RMDIR — RFC 1813 §3.3.13. Remove an empty directory by (dir_fh, name). Returns NFS3ERR_NOTEMPTY if the directory is non-empty and NFS3ERR_NOTDIR if the target is not a directory. Reply carries the parent's wcc_data.

setattr(fhandle3, t, guard_ctime, credential, ctx)

SETATTR — RFC 1813 §3.3.2. Apply the requested sattr3 fields (any field set to nil means "leave unchanged"; :atime / :mtime carry RFC 1813's time_set semantics — :set_to_server_time or {:client, %Nfstime3{}}).

When guard_ctime is non-nil, the backend MUST compare it to the file's current ctime and return {:error, :not_sync, wcc} if they differ — RFC 1813 §3.3.2's "guarded SETATTR" precondition.

Reply carries wcc_data (pre_op wcc_attr + post_op fattr3) on both success and failure so the client can refresh its close-to-open cache. Backends that cannot supply pre- or post-op attrs may pass nil for either side.

symlink(dir_fh, name, attrs, target, credential, ctx)

SYMLINK — RFC 1813 §3.3.10. Create a symbolic link at dir_fh / name pointing at target (an opaque NFS path string — resolution is the client's responsibility, the server just stores bytes). Reply carries the new symlink's optional post_op_fh3, optional post_op_attr, and the parent's wcc_data.

write(fh, offset, data, stable, credential, ctx)

WRITE — RFC 1813 §3.3.7. Write data at offset with the requested stable hint. The server returns the actual count written (may be less than the request), the stability level it achieved (must be >= stable requested), and the per-instance writeverf3. Backends that always commit synchronously may return :file_sync regardless of the requested stability — the client gets stronger guarantees than asked for, which RFC 1813 permits.

The handler trims data against the per-write count cap before this callback fires, so backends can rely on byte_size(data) == count.