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
- Variables
- func CleanPath(p string) string
- func CommitChangeSet(fs WritableFS, cs ChangeSet) (newHash string, err error)
- func ErrIsDirectory(p string) error
- func ErrNotDirectory(p string) error
- func ErrNotFound(p string) error
- func WalkDir(fsys FileSystem, root string, ...) error
- type ChangeAction
- type ChangeSet
- type FileInfo
- type FileSystem
- type PathCanonicalizer
- type PendingChangeError
- type PreconditionError
- type ReadTracker
- type RemoveAllChange
- type RemoveOpts
- type TreeOp
- type TreeSnapshot
- type TreeStaleError
- type WritableFS
- type WriteChange
- type WriteConflictPolicy
- type WriteOpts
- type WriteScopeFS
Constants ¶
const DefaultWriteConflictPolicy = PolicyHash
DefaultWriteConflictPolicy is the policy applied when none is configured.
Variables ¶
var ErrReadOnly = fmt.Errorf("read-only filesystem")
ErrReadOnly is returned by mutating operations when the substrate is in read-only mode.
Functions ¶
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 ¶
ErrIsDirectory is returned when a file operation is attempted on a directory.
func ErrNotDirectory ¶
ErrNotDirectory is returned when a directory operation is attempted on a file.
func ErrNotFound ¶
ErrNotFound is returned when a path does not exist.
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.
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
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.