VFS.Mountable protocol (VFS v0.1.0)

Copy Markdown View Source

Pluggable virtual filesystem.

Implementations are plain structs; the protocol dispatches on the struct type. Backend authors typically use VFS.Skeleton inside their defimpl block to inherit defaults for walk/3 and materialize/2, then override whichever they want native impls for.

Path contract

All paths reaching a backend are absolute, already normalized, with a leading /. Backends behave as if rooted at /. Mount-prefix stripping happens in VFS's defimpl before the call reaches a leaf backend.

State threading

Every operation — including reads — returns the (possibly updated) impl as the last element of its success tuple. Lazy backends (e.g. an exgit-backed mount with a partial-clone repo) cache fetched data in their struct on read; throwing the updated struct away destroys those caches. Callers thread the new state forward.

The one exception is walk/3, which returns a bare Enumerable.t/0. Cache state populated during enumeration does not escape — call materialize/2 first if you need the cache primed before iteration.

Errors

All errors are %VFS.Error{} exceptions. Pattern match on :kind for flow control:

case VFS.read_file(fs, path) do
  {:ok, bin, fs} -> ...
  {:error, %VFS.Error{kind: :enoent}} -> ...
  {:error, %VFS.Error{kind: :eisdir}} -> ...
end

See VFS.Error for the full set of :kind values.

Summary

Types

Capability flags reported by capabilities/1.

An absolute, normalized path (see VFS.Path).

t()

Any struct that implements VFS.Mountable.

Functions

Return the set of capabilities this impl supports.

Return whether path exists, plus the possibly cache-updated impl.

Pre-warm any internal cache. No-op for non-lazy backends. Lazy backends (e.g. partial-clone exgit repos) use this to avoid per-blob round-trips during a subsequent bulk traversal.

Create a directory at path. Options

Return entries directly under directory path — names only, no leading slash, no path separators within names.

Remove path. Options

Return metadata for path.

Open path for streaming read. Returns an Enumerable.t/0 that emits binary chunks. Options

Lazily walk the tree under root. Returns an Enumerable.t/0 that emits {path, %VFS.Stat{}} tuples. Options

Write content to path.

Types

capability()

@type capability() ::
  :read | :write | :mkdir | :native_walk | :native_stream_read | :lazy

Capability flags reported by capabilities/1.

  • :readstream_read/3, stat/2, readdir/2, walk/3 work.
  • :writewrite_file/4 and rm/3 work on file paths. (Does NOT
            imply `mkdir/3`; flat-keyed backends like S3 / postgres
            support `:write` without `:mkdir`.)
  • :mkdirmkdir/3 works (the backend models empty directories).
  • :lazymaterialize/2 does meaningful work (the backend has
            a cache that can be pre-warmed).
  • :native_walkwalk/3 is implemented natively (faster than
                        the default composed-from-stat-and-readdir).
  • :native_stream_readstream_read/3 is implemented natively.

path()

@type path() :: String.t()

An absolute, normalized path (see VFS.Path).

t()

@type t() :: struct()

Any struct that implements VFS.Mountable.

Functions

capabilities(impl)

@spec capabilities(t()) :: MapSet.t(capability())

Return the set of capabilities this impl supports.

Capability claims should match observed behavior: for example, a backend without :mkdir should consistently return :erofs or :enotsup from mkdir/3.

exists?(impl, path)

@spec exists?(t(), path()) :: {boolean(), t()}

Return whether path exists, plus the possibly cache-updated impl.

Implementations normalize path, check both files and directories, and return the updated impl even for negative lookups so lazy backends can keep metadata caches warm.

materialize(impl, opts)

@spec materialize(
  t(),
  keyword()
) :: {:ok, t()} | {:error, VFS.Error.t()}

Pre-warm any internal cache. No-op for non-lazy backends. Lazy backends (e.g. partial-clone exgit repos) use this to avoid per-blob round-trips during a subsequent bulk traversal.

mkdir(impl, path, opts)

@spec mkdir(t(), path(), keyword()) :: {:ok, t()} | {:error, VFS.Error.t()}

Create a directory at path. Options:

  • :parents — create missing intermediate directories (mkdir -p)

readdir(impl, path)

@spec readdir(t(), path()) ::
  {:ok, Enumerable.t(String.t()), t()} | {:error, VFS.Error.t()}

Return entries directly under directory path — names only, no leading slash, no path separators within names.

Returns an Enumerable.t/0 of strings. Backends with bounded directories should return a list (the simplest Enumerable; length/1 and Enum.sort/1 work natively on it). Backends with paginated or unbounded listings (S3 with many keys, a database-backed store, a virtual /integers/N namespace) should return a Stream. Consumers should treat the result as an Enumerable: use Enum.to_list/1 or Stream.take/2 as appropriate; do not assume length/1 works.

Order: bounded backends should return entries in lexicographic order by convention (matches POSIX, S3 ListObjects). Unbounded backends document their order in the impl's moduledoc.

rm(impl, path, opts)

@spec rm(t(), path(), keyword()) :: {:ok, t()} | {:error, VFS.Error.t()}

Remove path. Options:

  • :recursive — remove a directory and all contents (default false). Without :recursive, rm on a directory returns {:error, %VFS.Error{kind: :eisdir}}.

stat(impl, path)

@spec stat(t(), path()) :: {:ok, VFS.Stat.t(), t()} | {:error, VFS.Error.t()}

Return metadata for path.

Success returns %VFS.Stat{} and the updated impl. Failure returns a structured %VFS.Error{}; callers pattern-match on error.kind.

stream_read(impl, path, opts)

@spec stream_read(t(), path(), keyword()) ::
  {:ok, Enumerable.t(binary()), t()} | {:error, VFS.Error.t()}

Open path for streaming read. Returns an Enumerable.t/0 that emits binary chunks. Options:

  • :chunk_size — default 64 * 1024
  • :byte_range{start, length} — start is 0-based, returns up to length bytes starting at start
  • :line_range{first, last} — 1-based, inclusive line numbers; last may be :end to read to EOF

The returned impl reflects state needed to open the stream; cache state populated during enumeration does not escape (see walk/3 caveat).

walk(impl, root, opts)

@spec walk(t(), path(), keyword()) :: Enumerable.t({path(), VFS.Stat.t()})

Lazily walk the tree under root. Returns an Enumerable.t/0 that emits {path, %VFS.Stat{}} tuples. Options:

  • :max_depth:infinity (default) or non-neg integer
  • :include_dirs — emit directory entries themselves (default false)

Returns a bare Enumerable.t/0, not a {:ok, _, t} tuple. Cache state populated during enumeration does not escape; call materialize/2 first for agent loops that re-touch files after a walk.

Default-implementation traversal is depth-first and fully lazy: Stream.take/2 halts the traversal as soon as the consumer has enough, including over backends whose readdir/2 returns an unbounded Enumerable.t/0 (paginated S3 listings, virtual /integers/N-style namespaces, database cursors). The default walker pulls one entry at a time from each directory's readdir output.

When the mount table contains overlapping mounts, walk respects longest-prefix shadowing — paths reachable only through a shadowed prefix are not yielded, matching read_file/2's point-lookup behavior.

write_file(impl, path, content, opts)

@spec write_file(t(), path(), binary(), keyword()) ::
  {:ok, t()} | {:error, VFS.Error.t()}

Write content to path.

Success returns the updated impl. Read-only wrappers should return :erofs; backends that fundamentally cannot write should return :enotsup. Options are reserved for future use.