JustBash.FS.Memory (JustBash v0.4.0)

View Source

JustBash'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

directory_entry()

@type directory_entry() :: %{
  type: :directory,
  mode: non_neg_integer(),
  mtime: DateTime.t()
}

file_entry()

@type file_entry() :: %{
  type: :file,
  content: binary(),
  mode: non_neg_integer(),
  mtime: DateTime.t()
}

fs_entry()

@type fs_entry() :: file_entry() | directory_entry() | symlink_entry()

t()

@type t() :: %JustBash.FS.Memory{data: %{required(String.t()) => fs_entry()}}

write_opts()

@type write_opts() :: [mode: non_neg_integer(), mtime: DateTime.t()]

Functions

append_file(fs, path, content)

@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.

chmod(fs, path, mode)

@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.

link(fs, existing_path, new_path)

@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.

lstat(fs, path)

@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.

new(initial_files \\ %{})

@spec new(map()) :: t()

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}})

readlink(fs, path)

@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.

symlink(fs, target, link_path)

@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).

validate_initial_files!(initial_files)

@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.

write_file(fs, path, content, opts \\ [])

@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.