JustBash.FS.Memory (JustBash v0.4.0)
View SourceJustBash's in-memory VFS.Mountable backend — the default backend
mounted at / by JustBash.FS.new/1.
Why not VFS.Memory?
Not a legacy module, and deliberately not a delegate. VFS.Mountable
is a ten-operation protocol shaped to virtual-FS semantics (git blobs,
S3 objects, DB rows); vfs 0.1 intentionally cut lstat, symlink,
readlink, link, chmod, and append_file from it, and its stock
VFS.Memory backend stores bare path => binary pairs with no mode
or symlink metadata (mtimes only, and not settable through write
opts). Bash needs exactly what was cut: ln/ln -s, readlink,
chmod, test -L, ls -l modes, and mtime-honoring writes. Those
features require a richer entry model (file/directory/symlink entries
carrying mode + mtime, with link resolution), which cannot be layered
over VFS.Memory's flat binary map — so this backend owns its own
storage and implements the protocol against it.
This module is the "real consumer" case the vfs SPEC anticipated: the
ten universal operations are implemented in the VFS.Mountable
defimpl below (reach them through the helpers on VFS), and the
POSIX extensions are dispatched through the JustBash.FS.POSIX
secondary protocol, which this backend implements natively and other
backends refuse gracefully.
Features beyond VFS.Memory:
- Symbolic links (with loop detection on resolution)
- Hard links
- File permissions (mode)
- Modification times
All operations are pure: every mutation returns an updated struct.
Summary
Functions
Append content to a file, creating it if it doesn't exist.
Change file/directory permissions.
Create a hard link. Only regular files can be hard-linked.
Get stat information for a path without following symlinks.
Create a new in-memory filesystem with optional initial files.
Read the target of a symbolic link.
Create a symbolic link at link_path pointing to target.
Validate an initial-files map before it is realized as a filesystem.
Write content to a file, creating it if it doesn't exist.
Types
@type directory_entry() :: %{ type: :directory, mode: non_neg_integer(), mtime: DateTime.t() }
@type file_entry() :: %{ type: :file, content: binary(), mode: non_neg_integer(), mtime: DateTime.t() }
@type fs_entry() :: file_entry() | directory_entry() | symlink_entry()
@type symlink_entry() :: %{ type: :symlink, target: String.t(), mode: non_neg_integer(), mtime: DateTime.t() }
@type write_opts() :: [mode: non_neg_integer(), mtime: DateTime.t()]
Functions
@spec append_file(t(), String.t(), binary()) :: {:ok, t()} | {:error, VFS.Error.t()}
Append content to a file, creating it if it doesn't exist.
Follows symlinks to the final target (POSIX O_APPEND semantics): the
link survives and the target receives the bytes; appending to a
dangling symlink creates the target. Preserves the file's existing
mode, unlike a read+write composition. A non-final path component that
resolves to a regular file is :enotdir.
@spec chmod(t(), String.t(), non_neg_integer()) :: {:ok, t()} | {:error, VFS.Error.t()}
Change file/directory permissions.
Follows symlinks to the final target (POSIX chmod semantics — there
is no lchmod on Linux): the target's mode changes, the link entry
keeps its conventional 0o777.
@spec link(t(), String.t(), String.t()) :: {:ok, t()} | {:error, VFS.Error.t()}
Create a hard link. Only regular files can be hard-linked.
Entries are immutable values, not shared inodes: the two names hold the same content at link time, but a later write or append through one name does not update the other. True hard-link aliasing would need inode indirection in the entry model.
@spec lstat(t(), String.t()) :: {:ok, VFS.Stat.t(), t()} | {:error, VFS.Error.t()}
Get stat information for a path without following symlinks.
A symlink reports type: :symlink with the target's byte size. Ancestor
components still resolve — only the final one is left alone.
Create a new in-memory filesystem with optional initial files.
Initial files can be provided as a map:
- Simple form:
%{"/path/to/file" => "content"} - Extended form:
%{"/path/to/file" => %{content: "content", mode: 0o755, mtime: ~U[...]}}
Parent directories are created automatically. Raises ArgumentError
when the map is not realizable as a filesystem: either one entry's path
runs through another (see validate_initial_files!/1), or an entry
collides with a directory this backend already holds, as %{"/" => "x"}
does.
Examples
iex> fs = JustBash.FS.Memory.new()
iex> fs = JustBash.FS.Memory.new(%{"/home/user/file.txt" => "hello"})
iex> fs = JustBash.FS.Memory.new(%{"/bin/script" => %{content: "#!/bin/bash", mode: 0o755}})
@spec readlink(t(), String.t()) :: {:ok, String.t(), t()} | {:error, VFS.Error.t()}
Read the target of a symbolic link.
Ancestor components resolve; the final one is the link being read.
@spec symlink(t(), String.t(), String.t()) :: {:ok, t()} | {:error, VFS.Error.t()}
Create a symbolic link at link_path pointing to target.
The target is stored verbatim and resolved lazily on access, relative to the link's directory when not absolute. Absolute targets resolve within this backend's namespace (mount-local, chroot-like).
@spec validate_initial_files!(map()) :: :ok
Validate an initial-files map before it is realized as a filesystem.
A map like %{"/m/j" => "x", "/m/j/a.md" => "y"} describes a
filesystem that cannot exist: /m/j is a regular file, so nothing can
live under it. Seeding it anyway used to store an entry no directory
listing could reach, and which one of the two entries won depended on
map iteration order. Raise instead, naming both paths.
@spec write_file(t(), String.t(), binary(), write_opts()) :: {:ok, t()} | {:error, VFS.Error.t()}
Write content to a file, creating it if it doesn't exist.
Missing parent directories are created automatically. Accepts :mode
and :mtime options; these also flow through VFS.write_file/4 opts.
Follows symlinks in every component (POSIX path resolution), including
the final one (O_TRUNC semantics, matching append_file/3): the link
survives and the target is replaced; writing to a dangling symlink
creates the target. A non-final component that resolves to a regular
file is :enotdir.