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}} -> ...
endSee VFS.Error for the full set of :kind values.
Summary
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
@type capability() ::
:read | :write | :mkdir | :native_walk | :native_stream_read | :lazy
Capability flags reported by capabilities/1.
:read—stream_read/3,stat/2,readdir/2,walk/3work.:write—write_file/4andrm/3work on file paths. (Does NOTimply `mkdir/3`; flat-keyed backends like S3 / postgres support `:write` without `:mkdir`.):mkdir—mkdir/3works (the backend models empty directories).:lazy—materialize/2does meaningful work (the backend hasa cache that can be pre-warmed).:native_walk—walk/3is implemented natively (faster thanthe default composed-from-stat-and-readdir).:native_stream_read—stream_read/3is implemented natively.
@type path() :: String.t()
An absolute, normalized path (see VFS.Path).
@type t() :: struct()
Any struct that implements VFS.Mountable.
Functions
@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.
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.
@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.
@spec mkdir(t(), path(), keyword()) :: {:ok, t()} | {:error, VFS.Error.t()}
Create a directory at path. Options:
:parents— create missing intermediate directories (mkdir -p)
@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.
@spec rm(t(), path(), keyword()) :: {:ok, t()} | {:error, VFS.Error.t()}
Remove path. Options:
:recursive— remove a directory and all contents (defaultfalse). Without:recursive,rmon a directory returns{:error, %VFS.Error{kind: :eisdir}}.
@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.
@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— default64 * 1024:byte_range—{start, length}— start is 0-based, returns up tolengthbytes starting atstart:line_range—{first, last}— 1-based, inclusive line numbers;lastmay be:endto 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).
@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 (defaultfalse)
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 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.