JustBash.FS (JustBash v0.4.0)
View SourceJustBash's filesystem layer: a thin facade over the vfs library.
new/1 builds a %VFS{} mount table with a JustBash.FS.Memory
backend mounted at /. Because the whole filesystem is a %VFS{},
additional backends (a read-only git repository via exgit, another
in-memory scratch space, a caller-provided VFS.Mountable) can be
mounted alongside it with VFS.mount/3 and every bash command sees
them transparently.
Three groups of functions:
- Core operations delegate to
VFSand follow vfs conventions: reads return{:ok, payload, fs}(thread the updatedfsforward — lazy backends cache on read), mutations return{:ok, fs}, and all errors are%VFS.Error{}structs. Paths inJustBash.FS.Special(/dev/null, and the/devdirectory that holds it) are answered here before any backend is consulted. - POSIX extensions (
lstat/2,symlink/3,readlink/2,link/3,chmod/3,append_file/3) dispatch throughJustBash.FS.POSIX, degrading gracefully on backends without them. - Compositions (
cp/4,mv/3) are built from the primitives, so they work across mounts.
Summary
Types
A path resolve_path/2 produced: an absolute string, or
{:error, %VFS.Error{kind: :enoent}} when the operand was the empty
pathname.
Functions
Append content to a file, creating it if it doesn't exist.
Get the base name (last segment) of a path. The basename of / is /.
Hold spelling — a path as an operand wrote it, read relative to base —
to what its spelling promised: {:ok, fs} unless it names nothing (an
empty operand is ENOENT) or demands a directory (see
directory_spelling?/1) and what it demands it of is not one.
Change file/directory permissions.
Copy a file, symlink, or (with recursive: true) directory tree.
Does the way path is spelled require it to name a directory?
Get the directory name (parent path) of a path.
Create a hard link.
Get stat information without following symlinks.
See VFS.mkdir/3. Pass parents: true for mkdir -p behavior.
Move/rename a file, symlink, or directory tree. Composed as a recursive copy followed by a recursive remove, so it works across mounts.
Create a new filesystem: a %VFS{} with a JustBash.FS.Memory
backend (seeded with initial_files) mounted at /.
Normalize a filesystem path. Relative input is rooted at /.
Read the target of a symbolic link.
Resolve path relative to base. Absolute paths ignore the base.
See VFS.rm/3. Pass recursive: true to remove directory trees.
See VFS.stat/2. Follows symlinks.
The conventional strerror-style message for a %VFS.Error{} or error
kind, as bash utilities print them.
Create a symbolic link at link_path pointing to target.
Validate a path => content map before seeding a filesystem with it.
See VFS.write_file/4. The default backend honors :mode and :mtime opts.
Types
@type cp_opts() :: [recursive: boolean(), deadline: JustBash.Limit.Deadline.t() | nil]
@type mkdir_opts() :: [{:parents, boolean()}]
@type resolved() :: String.t() | {:error, VFS.Error.t()}
A path resolve_path/2 produced: an absolute string, or
{:error, %VFS.Error{kind: :enoent}} when the operand was the empty
pathname.
@type rm_opts() :: [{:recursive, boolean()}]
@type t() :: VFS.t()
@type write_opts() :: [mode: non_neg_integer(), mtime: DateTime.t()]
Functions
@spec append_file(t(), resolved(), binary()) :: {:ok, t()} | {:error, VFS.Error.t()}
Append content to a file, creating it if it doesn't exist.
Get the base name (last segment) of a path. The basename of / is /.
@spec check_directory_spelling(t(), String.t(), String.t()) :: {:ok, t()} | {:error, VFS.Error.t()}
Hold spelling — a path as an operand wrote it, read relative to base —
to what its spelling promised: {:ok, fs} unless it names nothing (an
empty operand is ENOENT) or demands a directory (see
directory_spelling?/1) and what it demands it of is not one.
What it demands it of is the last component the spelling names outright, not
where resolve_path/2 says the whole thing lands: .. is collapsed
lexically, so /f/.. resolves to / — a directory whatever /f is — while
the kernel cannot walk up out of the regular file /f at all.
The error is the one stat("f/") itself gives: :enotdir when something
that is not a directory is already there, naming the operand as the caller
spelled it, and whatever stat/2 reported otherwise. :enoent means the
destination does not exist yet, which a caller about to create the directory
— a recursive copy, say — can ignore.
@spec chmod(t(), resolved(), non_neg_integer()) :: {:ok, t()} | {:error, VFS.Error.t()}
Change file/directory permissions.
Copy a file, symlink, or (with recursive: true) directory tree.
Composed from the vfs primitives plus the POSIX extensions, so it works across mounts: regular files copy content, mode, and mtime; symlinks copy the link itself when the destination backend supports symlinks.
A recursive copy whose destination lies inside the source fails with
:einval instead of recursing forever — every pass would add new
children under the source it is still walking. Both operands are
resolved through any symlinked components before that test, so a
destination that only reaches the source through a link is caught
too.
:deadline — a JustBash.Limit.Deadline (or nil). A recursive copy is a
single step as far as the step counter is concerned, so a large tree is
otherwise unbounded; with a deadline, each directory descended into checks
the wall clock and raises JustBash.Limit.ExceededError once it has passed.
Does the way path is spelled require it to name a directory?
POSIX resolves a trailing slash as a trailing /., so f/ is not another
spelling of the regular file f — it is an assertion that f is a
directory. The . and .. components the slash stands for say the same
thing. resolve_path/2 normalizes all of them away, so a caller that writes
through a user-supplied path asks this of the operand before trusting where
it resolved to.
An empty operand names nothing at all, which is a different complaint.
Get the directory name (parent path) of a path.
See VFS.exists?/2.
@spec link(t(), resolved(), resolved()) :: {:ok, t()} | {:error, VFS.Error.t()}
Create a hard link.
@spec lstat(t(), resolved()) :: {:ok, VFS.Stat.t(), t()} | {:error, VFS.Error.t()}
Get stat information without following symlinks.
@spec mkdir(t(), resolved(), mkdir_opts()) :: {:ok, t()} | {:error, VFS.Error.t()}
See VFS.mkdir/3. Pass parents: true for mkdir -p behavior.
@spec mv(t(), resolved(), resolved()) :: {:ok, t()} | {:error, VFS.Error.t()}
Move/rename a file, symlink, or directory tree. Composed as a recursive copy followed by a recursive remove, so it works across mounts.
A destination symlink is replaced, not followed (POSIX rename/2
semantics) — the opposite of cp/4, which writes through it.
Moving a directory into its own subtree fails with :einval (see
cp/4) rather than looping.
Create a new filesystem: a %VFS{} with a JustBash.FS.Memory
backend (seeded with initial_files) mounted at /.
Raises ArgumentError when initial_files 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 the
backend already holds, as %{"/" => "x"} does.
Normalize a filesystem path. Relative input is rooted at /.
@spec read_file(t(), resolved()) :: {:ok, binary(), t()} | {:error, VFS.Error.t()}
See VFS.read_file/2.
@spec readdir(t(), resolved()) :: {:ok, Enumerable.t(), t()} | {:error, VFS.Error.t()}
See VFS.readdir/2.
@spec readlink(t(), resolved()) :: {:ok, String.t(), t()} | {:error, VFS.Error.t()}
Read the target of a symbolic link.
Resolve path relative to base. Absolute paths ignore the base.
An empty pathname names nothing. VFS.Path.join/2 collapses an empty
component onto base, which made cat '' report "Is a directory" for
the working directory. POSIX is the other way: open("") is ENOENT.
@spec rm(t(), resolved(), rm_opts()) :: {:ok, t()} | {:error, VFS.Error.t()}
See VFS.rm/3. Pass recursive: true to remove directory trees.
@spec stat(t(), resolved()) :: {:ok, VFS.Stat.t(), t()} | {:error, VFS.Error.t()}
See VFS.stat/2. Follows symlinks.
@spec stream_read(t(), resolved(), keyword()) :: {:ok, Enumerable.t(), t()} | {:error, VFS.Error.t()}
See VFS.stream_read/3.
@spec strerror(VFS.Error.t() | atom()) :: String.t()
The conventional strerror-style message for a %VFS.Error{} or error
kind, as bash utilities print them.
@spec symlink(t(), String.t(), resolved()) :: {:ok, t()} | {:error, VFS.Error.t()}
Create a symbolic link at link_path pointing to target.
@spec validate_initial_files!(map()) :: :ok
Validate a path => content map before seeding a filesystem with it.
Raises ArgumentError when one entry's path runs through another
entry — %{"/m/j" => "x", "/m/j/a.md" => "y"} asks for a file inside a
regular file, which no filesystem can hold. See
JustBash.FS.Memory.validate_initial_files!/1.
@spec walk(t(), String.t(), keyword()) :: Enumerable.t()
See VFS.walk/3.
This is an unbounded enumeration and no command in lib/ uses it; a caller
that walks an untrusted tree should pipe it through
JustBash.Limit.enforce_deadline/2, the way JustBash.Commands.Seq does.
@spec write_file(t(), resolved(), binary(), write_opts()) :: {:ok, t()} | {:error, VFS.Error.t()}
See VFS.write_file/4. The default backend honors :mode and :mtime opts.