Protocol-based virtual filesystem.
VFS is both a mount table (a struct holding a list of
{mountpoint, backend} pairs) and the public API for working with
virtual filesystems. The struct itself implements VFS.Mountable, so
mount tables nest naturally — you can mount a %VFS{} inside another
%VFS{} for namespacing.
The data-flow ops in this module — read_file/2, stream_read/3,
write_file/4, mkdir/3, rm/3, walk/3, materialize/2 — are
wrapped in :telemetry.span/3 so consumers can attach OpenTelemetry,
log, or metric handlers. The cheap lookups (exists?/2, stat/2,
readdir/2, capabilities/1) are not instrumented. The "Telemetry
events" section below is the full, exact contract.
Quick tour
iex> fs = VFS.new() |> VFS.mount("/", VFS.Memory.new(%{"/repo/README.md" => "hello\n", "/tmp/scratch" => ""}))
iex> {:ok, "hello\n", fs} = VFS.read_file(fs, "/repo/README.md")
iex> {:ok, fs} = VFS.write_file(fs, "/tmp/scratch", "world\n")
iex> {:ok, "world\n", _fs} = VFS.read_file(fs, "/tmp/scratch")
iex> :ok
:okState threading
Every op returns the (possibly updated) %VFS{} as the last element of
its success tuple. Threading it forward preserves lazy backend caches.
See VFS.Mountable for the full contract.
Telemetry events
All under the [:vfs, _, _] prefix:
[:vfs, :read_file, :start | :stop | :exception][:vfs, :stream_read, :start | :stop | :exception][:vfs, :write_file, :start | :stop | :exception][:vfs, :mkdir, :start | :stop | :exception][:vfs, :rm, :start | :stop | :exception][:vfs, :walk, :start | :stop](terminal — emitted on enumeration end)[:vfs, :materialize, :start | :stop | :exception][:vfs, :cache, :hit | :miss](emitted by lazy backends themselves)
Metadata always includes %{impl: <impl module>}. Errors land in the
:stop event metadata as %{error: %VFS.Error{...}}.
Summary
Functions
Raise a helpful error if value does not implement VFS.Mountable.
Return the capability set supported by impl.
Return whether path exists, plus the possibly cache-updated state.
Pre-warm any internal cache. See VFS.Mountable.materialize/2.
Create directory at path.
Mount backend at mountpoint. If a mount already exists at the same
point, it is replaced. Mounts are kept sorted by mountpoint length
(longest first) so longest-prefix routing is a linear scan.
Return the list of {mountpoint, backend} pairs in longest-first order.
Read the entire content of path.
List entries directly under directory path.
Remove path.
Return metadata for path.
Open path for streaming read. See VFS.Mountable.stream_read/3.
Remove the mount at mountpoint. No-op if no such mount.
Lazily walk the tree under root. Emits {path, %VFS.Stat{}} tuples.
Telemetry: [:vfs, :walk, :start | :stop] (terminal — emitted on
enumeration completion).
Write content to path.
Types
@type mount() :: {VFS.Path.t(), VFS.Mountable.t()}
A {mountpoint, backend} pair. The backend is any VFS.Mountable.
@type t() :: %VFS{mounts: [mount()]}
Functions
@spec assert_implemented!(term()) :: :ok
Raise a helpful error if value does not implement VFS.Mountable.
Useful at trust boundaries (constructor arguments, public API entry
points) where catching a missing defimpl early beats a
Protocol.UndefinedError deep in a downstream call.
Examples
iex> VFS.assert_implemented!(VFS.Memory.new())
:ok
iex> VFS.assert_implemented!(%URI{})
** (ArgumentError) %URI{} does not implement the VFS.Mountable protocol. Add `defimpl VFS.Mountable, for: URI do ... end` or pass a struct that has one.
@spec capabilities(VFS.Mountable.t()) :: MapSet.t(VFS.Mountable.capability())
Return the capability set supported by impl.
Examples
iex> caps = VFS.capabilities(VFS.Memory.new())
iex> MapSet.subset?(MapSet.new([:read, :write, :mkdir]), caps)
true
@spec exists?(VFS.Mountable.t(), String.t()) :: {boolean(), VFS.Mountable.t()}
Return whether path exists, plus the possibly cache-updated state.
Examples
iex> mem = VFS.Memory.new(%{"/note.txt" => "hi"})
iex> {true, _mem} = VFS.exists?(mem, "/note.txt")
iex> :ok
:ok
@spec materialize( VFS.Mountable.t(), keyword() ) :: {:ok, VFS.Mountable.t()} | {:error, VFS.Error.t()}
Pre-warm any internal cache. See VFS.Mountable.materialize/2.
Non-lazy backends usually return unchanged state.
Examples
iex> mem = VFS.Memory.new(%{"/note.txt" => "hi"})
iex> {:ok, ^mem} = VFS.materialize(mem)
iex> :ok
:ok
@spec mkdir(VFS.Mountable.t(), String.t(), keyword()) :: {:ok, VFS.Mountable.t()} | {:error, VFS.Error.t()}
Create directory at path.
Pass parents: true for mkdir -p behavior.
Examples
iex> mem = VFS.Memory.new()
iex> {:ok, mem} = VFS.mkdir(mem, "/a/b", parents: true)
iex> {true, _mem} = VFS.exists?(mem, "/a/b")
iex> :ok
:ok
@spec mount(t(), String.t(), VFS.Mountable.t()) :: t()
Mount backend at mountpoint. If a mount already exists at the same
point, it is replaced. Mounts are kept sorted by mountpoint length
(longest first) so longest-prefix routing is a linear scan.
Raises ArgumentError if backend does not implement VFS.Mountable.
Examples
iex> fs = VFS.new() |> VFS.mount("/repo", VFS.Memory.new(%{"/x" => "1"}))
iex> {:ok, "1", _fs} = VFS.read_file(fs, "/repo/x")
iex> :ok
:ok
Return the list of {mountpoint, backend} pairs in longest-first order.
Examples
iex> fs = VFS.new() |> VFS.mount("/repo", VFS.Memory.new()) |> VFS.mount("/", VFS.Memory.new())
iex> fs |> VFS.mounts() |> Enum.map(&elem(&1, 0))
["/repo", "/"]
@spec new() :: t()
Build an empty mount table. Use mount/3 to attach backends.
Examples
iex> %VFS{mounts: []} = VFS.new()
iex> fs = VFS.new() |> VFS.mount("/", VFS.Memory.new(%{"/foo" => "bar"}))
iex> {:ok, "bar", _fs} = VFS.read_file(fs, "/foo")
iex> :ok
:ok
@spec read_file(VFS.Mountable.t(), String.t()) :: {:ok, binary(), VFS.Mountable.t()} | {:error, VFS.Error.t()}
Read the entire content of path.
Derived from stream_read/3 — runs the chunk stream into a binary. Use
stream_read/3 directly when you need :chunk_size, :byte_range, or
:line_range options.
Examples
iex> mem = VFS.Memory.new(%{"/hello.txt" => "hello"})
iex> {:ok, "hello", _mem} = VFS.read_file(mem, "/hello.txt")
iex> :ok
:ok
@spec readdir(VFS.Mountable.t(), String.t()) :: {:ok, Enumerable.t(String.t()), VFS.Mountable.t()} | {:error, VFS.Error.t()}
List entries directly under directory path.
Returns an Enumerable.t/0 of names: a list for bounded backends,
a Stream for paginated or unbounded ones. Treat it as an Enumerable
— Enum.to_list/1 only when you know the listing is bounded,
Stream.take/2 when you don't. See VFS.Mountable.readdir/2.
@spec rm(VFS.Mountable.t(), String.t(), keyword()) :: {:ok, VFS.Mountable.t()} | {:error, VFS.Error.t()}
Remove path.
Pass recursive: true to remove a directory tree.
Examples
iex> mem = VFS.Memory.new(%{"/note.txt" => "hi"})
iex> {:ok, mem} = VFS.rm(mem, "/note.txt")
iex> {false, _mem} = VFS.exists?(mem, "/note.txt")
iex> :ok
:ok
@spec stat(VFS.Mountable.t(), String.t()) :: {:ok, VFS.Stat.t(), VFS.Mountable.t()} | {:error, VFS.Error.t()}
Return metadata for path.
Examples
iex> mem = VFS.Memory.new(%{"/note.txt" => "hi"})
iex> {:ok, %VFS.Stat{type: :regular, size: 2}, _mem} = VFS.stat(mem, "/note.txt")
iex> :ok
:ok
@spec stream_read(VFS.Mountable.t(), String.t(), keyword()) :: {:ok, Enumerable.t(binary()), VFS.Mountable.t()} | {:error, VFS.Error.t()}
Open path for streaming read. See VFS.Mountable.stream_read/3.
Examples
iex> mem = VFS.Memory.new(%{"/hello.txt" => "hello\nworld\n"})
iex> {:ok, stream, _mem} = VFS.stream_read(mem, "/hello.txt", line_range: {2, 2})
iex> Enum.to_list(stream)
["world"]
Remove the mount at mountpoint. No-op if no such mount.
Examples
iex> fs = VFS.new() |> VFS.mount("/", VFS.Memory.new(%{"/a" => "b"})) |> VFS.umount("/")
iex> %VFS{mounts: []} = fs
@spec walk(VFS.Mountable.t(), String.t(), keyword()) :: Enumerable.t({String.t(), VFS.Stat.t()})
Lazily walk the tree under root. Emits {path, %VFS.Stat{}} tuples.
Telemetry: [:vfs, :walk, :start | :stop] (terminal — emitted on
enumeration completion).
Examples
iex> fs = VFS.new() |> VFS.mount("/", VFS.Memory.new(%{"/a" => "1", "/b/c" => "2"}))
iex> fs |> VFS.walk("/", []) |> Enum.map(&elem(&1, 0)) |> Enum.sort()
["/a", "/b/c"]
@spec write_file(VFS.Mountable.t(), String.t(), binary(), keyword()) :: {:ok, VFS.Mountable.t()} | {:error, VFS.Error.t()}
Write content to path.
Returns the updated filesystem state; thread it into subsequent calls.
Examples
iex> mem = VFS.Memory.new()
iex> {:ok, mem} = VFS.write_file(mem, "/note.txt", "hi")
iex> {:ok, "hi", _mem} = VFS.read_file(mem, "/note.txt")
iex> :ok
:ok