vfs

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package vfs defines the virtual filesystem contract that the shell, its commands, and all backends share. It depends on nothing but the standard library: it is the seam between the shell (which operates on a filesystem) and the backends (which implement one).

Index

Constants

View Source
const DefaultWriteConflictPolicy = PolicyHash

DefaultWriteConflictPolicy is the policy applied when none is configured.

Variables

View Source
var ErrReadOnly = fmt.Errorf("read-only filesystem")

ErrReadOnly is returned by mutating operations when the substrate is in read-only mode.

Functions

func CleanPath

func CleanPath(p string) string

CleanPath normalises a path to always start with / and removes . and .. segments.

func CommitChangeSet

func CommitChangeSet(fs WritableFS, cs ChangeSet) (newHash string, err error)

CommitChangeSet replays cs against fs under the exact precondition contract it carries. A write commits its proposed bytes under its WriteOpts (unconditional / IfMatch / IfNoneMatch); a remove_all replays its whole-tree removal under its RemoveOpts; mkdir / mkdir_all / remove replay their namespace mutation. Compare-and-swap drift fails with *PreconditionError (write) or *TreeStaleError (remove_all) and commits nothing.

It performs no authorization and captures no approver — the caller is responsible for deciding a ChangeSet may commit before calling this. For a write it returns the hex SHA-256 of the committed bytes; every other action returns an empty hash.

func ErrIsDirectory

func ErrIsDirectory(p string) error

ErrIsDirectory is returned when a file operation is attempted on a directory.

func ErrNotDirectory

func ErrNotDirectory(p string) error

ErrNotDirectory is returned when a directory operation is attempted on a file.

func ErrNotFound

func ErrNotFound(p string) error

ErrNotFound is returned when a path does not exist.

func WalkDir

func WalkDir(fsys FileSystem, root string, fn func(path string, info *FileInfo, err error) error) error

WalkDir walks the filesystem tree rooted at root, calling fn for each file or directory.

Types

type ChangeAction

type ChangeAction string

ChangeAction discriminates the kind of mutation a ChangeSet describes. Every mutating WritableFS method has a corresponding action, so every namespace mutation — file writes, directory creation, and removals — flows through the ordered log as a ChangeSet. Directories are first-class namespace entries (like a special filetype), so their creation and removal are logged operations too: this is what prevents a write from racing ahead of a remove (or landing on an already-removed path) — the single serialized applier orders them.

const (
	// ChangeActionWrite is a single-file whole-object write (WriteFileAtomic).
	ChangeActionWrite ChangeAction = "write"
	// ChangeActionMkdir creates a single directory (Mkdir).
	ChangeActionMkdir ChangeAction = "mkdir"
	// ChangeActionMkdirAll creates a directory and any missing parents
	// (MkdirAll).
	ChangeActionMkdirAll ChangeAction = "mkdir_all"
	// ChangeActionRemove removes a single file or empty directory (Remove).
	ChangeActionRemove ChangeAction = "remove"
	// ChangeActionRemoveAll is an atomic whole-tree removal (RemoveAll).
	ChangeActionRemoveAll ChangeAction = "remove_all"
)

type ChangeSet

type ChangeSet struct {
	Target    string           `json:"target"`
	Action    ChangeAction     `json:"action"`
	Write     *WriteChange     `json:"write,omitempty"`
	RemoveAll *RemoveAllChange `json:"remove_all,omitempty"`
}

ChangeSet is an immutable, serializable description of one atomic mutation — everything needed to commit it later via the applier, independent of what happens to the target in the meantime.

It is the single primitive a consumer persists (e.g. while a write awaits human approval) and later replays with CommitChangeSet. It deliberately carries no approver / status / capability / proposer fields: those are the consumer's policy concern, not part of the content-addressed change.

Action selects the payload: a write carries the exact proposed bytes plus its precondition contract; a remove_all carries the delete precondition. mkdir / mkdir_all / remove need only Target.

type FileInfo

type FileInfo struct {
	FileName    string
	FilePath    string
	Content     []byte
	Dir         bool
	FileModTime time.Time
	FileSize    int64
}

FileInfo represents a file or directory in the virtual filesystem.

func Dir

func Dir(p string, modTime time.Time) *FileInfo

Dir is a convenience constructor for a directory FileInfo.

func File

func File(name, filePath string, content []byte, modTime time.Time) *FileInfo

File is a convenience constructor for a file FileInfo.

func (*FileInfo) IsDir

func (fi *FileInfo) IsDir() bool

func (*FileInfo) ModTime

func (fi *FileInfo) ModTime() time.Time

func (*FileInfo) Mode

func (fi *FileInfo) Mode() fs.FileMode

func (*FileInfo) Name

func (fi *FileInfo) Name() string

func (*FileInfo) Size

func (fi *FileInfo) Size() int64

func (*FileInfo) Sys

func (fi *FileInfo) Sys() interface{}

type FileSystem

type FileSystem interface {
	Stat(path string) (*FileInfo, error)
	ReadDir(path string) ([]FileInfo, error)
	ReadFile(path string) ([]byte, error)
}

FileSystem is the read-only filesystem interface that all commands operate on.

type PathCanonicalizer added in v0.3.0

type PathCanonicalizer interface {
	CanonicalPath(path string) string
}

PathCanonicalizer is optionally implemented by filesystems that expose more than one virtual path for the same file. Commands use it when path identity matters, such as avoiding a move from an alias onto its canonical path.

type PendingChangeError

type PendingChangeError struct {
	ChangeSet ChangeSet
	Ref       string
}

PendingChangeError is returned by a mutating operation that an admission middleware intercepted and handed off instead of committing — for example, parked to await human approval. It is NOT a failure: the change was accepted as pending, so callers should report it informationally, not as an error.

ChangeSet is the captured (immutable) mutation; Ref is the consumer-owned handle for the pending change (e.g. a held-changeset id) that callers surface so it can be resolved later.

func (*PendingChangeError) Error

func (e *PendingChangeError) Error() string

type PreconditionError

type PreconditionError struct {
	Path    string
	Current string
}

PreconditionError is returned when a WriteOpts precondition (IfMatch / IfNoneMatch) is not satisfied. Current is the hash of the existing bytes (empty if the target does not exist).

func (*PreconditionError) Error

func (e *PreconditionError) Error() string

type ReadTracker

type ReadTracker interface {
	// LastReadHash returns the hex SHA-256 recorded when path was last read
	// (or written) in this session, and whether such a record exists.
	LastReadHash(path string) (hash string, seen bool)
}

ReadTracker is optionally implemented by a session filesystem that remembers the content hash of every file read during the session. It lets the write seam compare-and-swap a whole-file overwrite against the version the caller last saw — without the caller naming a hash — so an overwrite fails if the file changed since it was last read. A successful write updates the tracked hash so repeated writes in one session chain correctly.

type RemoveAllChange

type RemoveAllChange struct {
	Opts RemoveOpts `json:"opts"`
}

RemoveAllChange is the whole-tree removal payload of a ChangeSet: the precondition contract the removal commits under, carried verbatim as the caller's own RemoveOpts. Its zero value (Expected nil) is an unconditional removal; a non-nil Expected snapshot pins the compare-and-swap base captured at proposal time.

type RemoveOpts

type RemoveOpts struct {
	// Expected, when non-nil, requires the current visible subtree at the
	// target to match this snapshot exactly before the delete commits. Any
	// drift — a changed file, an added or removed descendant, a target that
	// vanished or changed kind — fails with a *TreeStaleError and deletes
	// nothing.
	Expected *TreeSnapshot
}

RemoveOpts carries the precondition contract for an atomic whole-tree delete. The zero value (Expected nil) means an unconditional delete.

type TreeOp

type TreeOp struct {
	RelPath string
	Kind    string // "file" | "dir"
	Hash    string
	Size    int64
}

TreeOp describes one descendant within a TreeSnapshot. RelPath is relative to the snapshot Root (the root itself is RelPath "."). Kind is "file" or "dir". Hash is the hex SHA-256 of a file's content (empty for a directory); Size is the file's byte length (0 for a directory).

type TreeSnapshot

type TreeSnapshot struct {
	Root string
	Ops  []TreeOp
}

TreeSnapshot is an exact, order-independent description of a subtree, used as the compare-and-swap base for an atomic delete. Root is the cleaned target path the snapshot was taken at; Ops lists every descendant (files and directories) with paths relative to Root.

type TreeStaleError

type TreeStaleError struct {
	Path   string
	Detail string
}

TreeStaleError is returned by RemoveAll when the current subtree no longer matches opts.Expected — the tree drifted since it was snapshotted, so the delete was refused. Detail names the first divergence found.

func (*TreeStaleError) Error

func (e *TreeStaleError) Error() string

type WritableFS

type WritableFS interface {
	FileSystem

	// SetWriteable transitions the backend to writable. It returns an error
	// if the backend can't support writes at all (not if it is already
	// writable — that is an idempotent no-op success).
	SetWriteable() error

	// SetReadonly transitions the backend back to read-only. It is idempotent
	// and drains in-flight writes: a write that already passed its precondition
	// check is allowed to complete, and SetReadonly blocks until the substrate
	// is quiescent before returning.
	SetReadonly() error

	// WriteFileAtomic commits data to name as a single atomic whole-object
	// operation, honoring the WriteOpts precondition. It returns the hex
	// SHA-256 of the committed bytes. It errors when the backend is currently
	// read-only or when the precondition fails.
	WriteFileAtomic(name string, data []byte, opts WriteOpts) (newHash string, err error)

	// Mkdir creates a folder. It uses plain mkdir semantics (the parent must
	// exist) but errors if name is not strictly inside a docset — you cannot
	// create a docset, nor anything at or above a docset root, through the FS.
	Mkdir(name string) error

	// MkdirAll creates a folder and any missing ancestors (mkdir -p). Like
	// Mkdir it errors if name is not strictly inside a docset: the enclosing
	// docset root must already exist, and every folder it creates must sit
	// strictly below that root. An already-existing directory is not an error.
	MkdirAll(name string) error

	// Remove deletes a single file or empty directory at name. It errors if
	// name is a docset root or anything at or above one, and refuses a
	// non-empty directory (use RemoveAll). It participates in the readonly
	// lock like every mutation.
	Remove(name string) error

	// RemoveAll deletes name and everything under it as a single atomic
	// whole-tree operation, honoring opts.Expected as a compare-and-swap
	// precondition on the exact subtree. It errors if name is a docset root or
	// anything at or above one, and refuses the delete if the physical subtree
	// contains any descendant hidden or denied by file policy (so the atomic
	// rename cannot destroy invisible bytes). A mismatch against opts.Expected
	// returns a *TreeStaleError and removes nothing.
	RemoveAll(name string, opts RemoveOpts) error
}

WritableFS is the read+write filesystem interface. A backend only satisfies it if it can physically persist writes; embedded/read-only backends (embed.FS, plain fs.FS adapters) deliberately do not implement it.

Writes are whole-object and atomic by construction — there is no streaming, offset, or partial-write surface. The substrate's write capability is a stateful flag toggled by SetWriteable/SetReadonly; while read-only every mutating call returns an error.

type WriteChange

type WriteChange struct {
	Bytes []byte    `json:"bytes"`
	Opts  WriteOpts `json:"opts"`
}

WriteChange is the write payload of a ChangeSet: the exact proposed bytes plus the precondition contract they commit under. Opts is the caller's own WriteOpts, carried verbatim so a replay is byte-for-byte the write the caller requested — its zero value is an unconditional (last-write-wins) overwrite, an IfMatch pins a compare-and-swap base, and IfNoneMatch is create-only. This preserves every write mode through the log and through an approval hold.

type WriteConflictPolicy

type WriteConflictPolicy string

WriteConflictPolicy selects how a whole-file overwrite (the `>`, tee, sed -i and publish verbs) defends against concurrent writers. It does not affect append (`>>`) or patch, which are always compare-and-swap by construction.

const (
	// PolicyHash (the default) makes overwrites compare-and-swap: the write
	// carries an IfMatch precondition on the base the caller read, so a
	// concurrent change is rejected with a PreconditionError instead of being
	// silently clobbered. For a read-modify-write verb (sed -i) the base is the
	// content it transformed, giving true optimistic concurrency; for a blind
	// redirect the base is read at command time, so the guarantee narrows to
	// the command's own read→write window.
	PolicyHash WriteConflictPolicy = "hash"

	// PolicyLastWriteWins makes overwrites unconditional (the zero WriteOpts):
	// atomic, but the last writer silently wins. Opt-in per docset or globally.
	PolicyLastWriteWins WriteConflictPolicy = "last_write_wins"
)

func ParseWriteConflictPolicy

func ParseWriteConflictPolicy(s string) (WriteConflictPolicy, error)

ParseWriteConflictPolicy validates and normalizes a policy string. An empty string resolves to the default (hash). Unknown values are rejected.

type WriteOpts

type WriteOpts struct {
	// IfMatch, when non-nil, requires the current content hash of the target
	// to equal *IfMatch before the write commits. A nonexistent target fails
	// (you cannot match a hash that has no bytes). This is the hex SHA-256 of
	// the exact bytes ReadFile returns.
	IfMatch *string

	// IfNoneMatch, when true, requires the target to not already exist
	// (create-only). Fails with a conflict if it does.
	IfNoneMatch bool
}

WriteOpts carries the precondition contract for an atomic write. The zero value means "unconditional overwrite" (last-write-wins, but still atomic).

Precedence: IfNoneMatch (create-only) is mutually exclusive with IfMatch.

type WriteScopeFS

type WriteScopeFS interface {
	CanWrite(path string) bool
}

WriteScopeFS is optionally implemented by a session filesystem that confines writes to a subset of paths. CanWrite reports whether a write to path would be permitted by the session's scope, without performing one — used for fail-fast checks (e.g. `spawn` validating its target before scheduling a job). It says nothing about content preconditions or approval gating, only scope.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL