@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
765 lines • 46.2 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
import { NOOPAccessor } from "../accessor/base.js";
import { enotsup } from "../errors/fs.js";
import { closeQuietly } from "../io/stream.js";
import { CapacityState, ListingVersion } from "../types.js";
import { methodName, vfsCall } from "./call.js";
import { WALK_LIMIT } from "./constants.js";
import { redactConfigWithSchema } from "./secrets.js";
import { Effect, Target } from "./types.js";
// The accessor a driver that brings none runs over: the default Python's
// `BaseVFS.accessor` class attribute carries too, so a mount and a caller
// never branch on its absence.
const NO_ACCESSOR = new NOOPAccessor();
/**
* The brand every driver carries, keyed through the symbol registry so a
* script file that loaded its own copy of this package still passes the
* loader's check: the class identity may differ, the brand does not.
*/
export const VFS_BRAND = Symbol.for('mirage.BaseVFS');
/**
* A backend: an accessor, the facts about it, and its functions.
*
* A VFS answers `readdir`, `read` and `stat`; every other function
* (`write`, `unlink`, a native `find` or `search`, ...) is optional, and a VFS answers exactly the ones it defines. Every generic
* shell command (`ls`, `cat`, `grep`, `find`, `head`, `wc`, ...) runs on
* the three required ones, and a line that needs a function the VFS does
* not define answers `Operation not supported` at that call, so `gzip -c`
* and `tar -t` still run as readers on a read-only backend. A method
* marked `@vfsCall` is also reachable by name through the dispatcher
* (`ws.dispatch('search_abc', path)`), with every check the dispatcher runs; the
* built-in ones are marked here, and an override keeps the mark.
*
* Everything a tree needs to run one (the placement, the index store, the
* registered commands, the reference it was built from) lives on the
* mount, so an author never sees it.
*
* Snapshots and versions see one of two things, and a subclass picks
* which by what it owns. Content the VFS holds itself (an in-memory
* store) is mirage-owned state: override `getState` and `loadState` to
* carry it, register the class under its name, and a snapshot or a
* version rebuilds the mount with that content and no override. Content
* that lives in a remote service is only observed: keep the default
* state, set `supportsSnapshot` and fill `FileStat.fingerprint`, and a
* snapshot pins what it read while `Workspace.load` asks for the live VFS
* back. Mirrors Python's `BaseVFS`.
*/
let BaseVFS = (() => {
let _instanceExtraInitializers = [];
let _readdir_decorators;
let _read_decorators;
let _stat_decorators;
let _firstLines_decorators;
let _lastLines_decorators;
let _lineCount_decorators;
let _checksum_decorators;
let _follow_decorators;
let _readTree_decorators;
let _write_decorators;
let _append_decorators;
let _pwrite_decorators;
let _create_decorators;
let _mkdir_decorators;
let _unlink_decorators;
let _rmdir_decorators;
let _rmR_decorators;
let _rename_decorators;
let _copy_decorators;
let _dirCopy_decorators;
let _truncate_decorators;
let _setattr_decorators;
let _search_decorators;
let _filesContaining_decorators;
let _linesContaining_decorators;
let _beforeFullWalk_decorators;
return class BaseVFS {
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
_readdir_decorators = [vfsCall({ effect: Effect.READ, target: Target.DIR })];
_read_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })];
_stat_decorators = [vfsCall({ effect: Effect.METADATA })];
_firstLines_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })];
_lastLines_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })];
_lineCount_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })];
_checksum_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })];
_follow_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })];
_readTree_decorators = [vfsCall({ effect: Effect.READ, target: Target.DIR, subtree: true })];
_write_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })];
_append_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })];
_pwrite_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })];
_create_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })];
_mkdir_decorators = [vfsCall({ effect: Effect.CREATE, target: Target.DIR })];
_unlink_decorators = [vfsCall({ effect: Effect.REMOVE, target: Target.FILE })];
_rmdir_decorators = [vfsCall({ effect: Effect.REMOVE, target: Target.DIR })];
_rmR_decorators = [vfsCall({ effect: Effect.REMOVE, target: Target.DIR, subtree: true })];
_rename_decorators = [vfsCall({ effect: Effect.RENAME, subtree: true })];
_copy_decorators = [vfsCall({ effect: Effect.COPY, target: Target.FILE })];
_dirCopy_decorators = [vfsCall({ effect: Effect.COPY, target: Target.DIR, subtree: true })];
_truncate_decorators = [vfsCall({ effect: Effect.WRITE, target: Target.FILE, creates: true })];
_setattr_decorators = [vfsCall({ effect: Effect.ATTR })];
_search_decorators = [vfsCall({ effect: Effect.READ, subtree: true })];
_filesContaining_decorators = [vfsCall({ effect: Effect.READ, target: Target.DIR, subtree: true })];
_linesContaining_decorators = [vfsCall({ effect: Effect.READ, target: Target.FILE })];
_beforeFullWalk_decorators = [vfsCall({ effect: Effect.READ, target: Target.DIR })];
__esDecorate(this, null, _readdir_decorators, { kind: "method", name: "readdir", static: false, private: false, access: { has: obj => "readdir" in obj, get: obj => obj.readdir }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _read_decorators, { kind: "method", name: "read", static: false, private: false, access: { has: obj => "read" in obj, get: obj => obj.read }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _stat_decorators, { kind: "method", name: "stat", static: false, private: false, access: { has: obj => "stat" in obj, get: obj => obj.stat }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _firstLines_decorators, { kind: "method", name: "firstLines", static: false, private: false, access: { has: obj => "firstLines" in obj, get: obj => obj.firstLines }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _lastLines_decorators, { kind: "method", name: "lastLines", static: false, private: false, access: { has: obj => "lastLines" in obj, get: obj => obj.lastLines }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _lineCount_decorators, { kind: "method", name: "lineCount", static: false, private: false, access: { has: obj => "lineCount" in obj, get: obj => obj.lineCount }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _checksum_decorators, { kind: "method", name: "checksum", static: false, private: false, access: { has: obj => "checksum" in obj, get: obj => obj.checksum }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _follow_decorators, { kind: "method", name: "follow", static: false, private: false, access: { has: obj => "follow" in obj, get: obj => obj.follow }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _readTree_decorators, { kind: "method", name: "readTree", static: false, private: false, access: { has: obj => "readTree" in obj, get: obj => obj.readTree }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _write_decorators, { kind: "method", name: "write", static: false, private: false, access: { has: obj => "write" in obj, get: obj => obj.write }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _append_decorators, { kind: "method", name: "append", static: false, private: false, access: { has: obj => "append" in obj, get: obj => obj.append }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _pwrite_decorators, { kind: "method", name: "pwrite", static: false, private: false, access: { has: obj => "pwrite" in obj, get: obj => obj.pwrite }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _create_decorators, { kind: "method", name: "create", static: false, private: false, access: { has: obj => "create" in obj, get: obj => obj.create }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _mkdir_decorators, { kind: "method", name: "mkdir", static: false, private: false, access: { has: obj => "mkdir" in obj, get: obj => obj.mkdir }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _unlink_decorators, { kind: "method", name: "unlink", static: false, private: false, access: { has: obj => "unlink" in obj, get: obj => obj.unlink }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _rmdir_decorators, { kind: "method", name: "rmdir", static: false, private: false, access: { has: obj => "rmdir" in obj, get: obj => obj.rmdir }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _rmR_decorators, { kind: "method", name: "rmR", static: false, private: false, access: { has: obj => "rmR" in obj, get: obj => obj.rmR }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _rename_decorators, { kind: "method", name: "rename", static: false, private: false, access: { has: obj => "rename" in obj, get: obj => obj.rename }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _copy_decorators, { kind: "method", name: "copy", static: false, private: false, access: { has: obj => "copy" in obj, get: obj => obj.copy }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _dirCopy_decorators, { kind: "method", name: "dirCopy", static: false, private: false, access: { has: obj => "dirCopy" in obj, get: obj => obj.dirCopy }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _truncate_decorators, { kind: "method", name: "truncate", static: false, private: false, access: { has: obj => "truncate" in obj, get: obj => obj.truncate }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _setattr_decorators, { kind: "method", name: "setattr", static: false, private: false, access: { has: obj => "setattr" in obj, get: obj => obj.setattr }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _search_decorators, { kind: "method", name: "search", static: false, private: false, access: { has: obj => "search" in obj, get: obj => obj.search }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _filesContaining_decorators, { kind: "method", name: "filesContaining", static: false, private: false, access: { has: obj => "filesContaining" in obj, get: obj => obj.filesContaining }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _linesContaining_decorators, { kind: "method", name: "linesContaining", static: false, private: false, access: { has: obj => "linesContaining" in obj, get: obj => obj.linesContaining }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(this, null, _beforeFullWalk_decorators, { kind: "method", name: "beforeFullWalk", static: false, private: false, access: { has: obj => "beforeFullWalk" in obj, get: obj => obj.beforeFullWalk }, metadata: _metadata }, null, _instanceExtraInitializers);
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
}
[VFS_BRAND] = (__runInitializers(this, _instanceExtraInitializers), true);
name = 'base';
get conditionSupport() {
return {};
}
vfsMd = '';
indexTtl = 600;
/**
* Whether reads of this VFS may be served from / written to the
* local file cache. A network-backed VFS whose content is read-mostly
* (e.g. object storage) sets this to true so reads can be cached; a
* VFS whose content is live (e.g. a database collection) leaves it
* false so reads always hit the backend and live follows (`tail -f`)
* are not masked by a cached snapshot.
*/
cachesReads = false;
/**
* Whether this VFS carries enough version information for
* snapshot+replay drift detection. When true, the driver's `stat` op
* must populate {@link FileStat.fingerprint}
* (and optionally {@link FileStat.revision}) with stable per-path
* markers. When false (the default), reads are treated as live-only
* at replay time: no fingerprint is captured at snapshot, no drift
* check fires at load.
*/
supportsSnapshot = false;
/**
* Whether the driver's `stat` op can size every regular file without
* fetching its content, i.e. {@link FileStat.size} is null only for
* directories. True for byte stores that keep a length in their
* metadata (ram, disk, redis, s3, gridfs); false for mounts that
* render content on read, where the size is unknowable until the bytes
* exist (slack, gmail, notion, postgres rows.jsonl, dify documents).
*
* FUSE does not need this: direct_io + attrTimeout '0' + hydrate-on-open
* make size-unknown files read correctly anyway. FSKit has no direct_io
* equivalent, so a mount there is driven entirely by the reported size
* and a false VFS would serve silent empty files. Mirrors Python's
* `BaseVFS.sizes_always_known`.
*/
sizesAlwaysKnown = false;
/**
* Whether a `read: fresh` mount can actually be revalidated against this
* backend: the driver's `stat` op and the read record must stamp the
* *same kind* of content token, so the gate can compare them with `===`. False
* (the default) is refused at mount time rather than degraded, because a
* mount that declares fresh and silently serves bounded is the bug the
* policy exists to prevent.
*
* Distinct from {@link BaseVFS.supportsSnapshot}, which asks whether a
* token exists at all, and from {@link BaseVFS.cachesReads}, which asks
* whether the gate can fire. A backend can have a token on both sides
* and still fail this one, by stamping two different kinds.
*
* A declarer must stamp the token on every read, not only while a
* recorder is active: node's read_revalidatable.test.ts holds each one to
* that (#1165). onedrive and sharepoint qualify because every unpinned
* byte read fetches the item's cTag before its bytes, recorded or not; a
* stream stamps only under a recorder, the one place its token can land.
*
* Mirrors Python's `BaseVFS.read_revalidatable`.
*/
readRevalidatable = false;
/**
* What a `read: fresh` mount checks a cached listing against before it
* lists again: nothing (NONE, the default), one version for the whole mount
* answered by a stat of its root (MOUNT), or each folder's own version
* answered by a stat of that folder (FOLDER). A declarer stores with each
* listing it writes a version no newer than its rows: taken from the same
* response (github's tree names its head), or read first and the rows then
* read at it or after it (hf walks the tree at the commit its revision
* request answered; disk stats a folder before it scans), so a change in
* between leaves the stored version behind and the next check re-lists.
* Its `stat` must answer the same kind of token: node's
* listing_version.test.ts holds each one to that.
*
* Mirrors Python's `BaseVFS.listing_version`.
*/
listingVersion = ListingVersion.NONE;
/**
* The stats each of this VFS's functions is handed, by method name: a
* function named here gets a `Handed` holding each stat it lists, found
* in the mount's stored listings, where it takes its index otherwise.
* Python reads the same off each function's parameters.
*/
handed = {};
/**
* The version every listing of this mount is pinned at, when its ref names
* a commit outright (github's full-sha ref; see `pinOf` in
* vfs/github/github.ts for why that cannot move). A stored listing whose
* version equals it is served without a check. It depends on the mount's config, so
* an instance sets it; null pins nothing.
*/
listingsPin = null;
/**
* Whether `read` fetches a byte window from the store itself. When false
* the caller reads the whole file and slices it, so `read` is only ever
* handed a window by a VFS that sets this.
*/
readsRanges = false;
/**
* Whether the data lives on the host filesystem, which lets a command
* aggregate on the host instead of streaming through mirage.
*/
local = false;
/** The store's capacity and counted bytes, set by a backend whose config
* caps it; null when uncapped. */
usage = null;
/**
* The files `filesContaining` answers for, as mount-relative globs matched
* segment by segment (a glob naming a directory covers what is below it);
* null for every file. grep and rg read any other file whatever the
* answer, as a full scan would.
*/
searchable = null;
/**
* `filesContaining` answers only a whole-word search (grep -w, rg -w);
* any other pattern reads every file.
*/
wholeWordSearch = false;
/**
* The most files one command walks under a directory of this mount: grep
* -r, rg and git grep stop reading there, find, ls -R, tree, du and
* getfattr -R stop listing, git status and the other verbs that walk a
* work tree fail, a glob matching more is refused, and so are tar -c, zip
* -r, cp -r, rm -r, chmod -R, chown -R, chgrp -R, mv to another mount and
* diff -r of a directory holding more (`beforeFullWalk`); -1 for no cap.
* This is the backend's own value: the mount's `'@all'` command limit, else
* the workspace's, replaces it when the VFS is mounted.
*/
walkLimit = WALK_LIMIT;
/**
* Extensions whose `read` is a rendering rather than the stored bytes,
* each to the name of the method that renders it, which takes `read`'s
* arguments, window included. A rendered read is never served from or
* kept in the file cache, and a `raw` read asks for `read` itself.
*/
renderers = {};
/**
* The backend handle every function takes. A builtin declares and
* assigns its own; one that brings none runs over a no-op accessor.
*/
accessor = NO_ACCESSOR;
#commands;
#closed = false;
/**
* Set the facts a subclass does not declare as members. Every option is
* optional, so a class that declares its facts as members calls
* `super()` bare.
*/
constructor(options = {}) {
if (options.name !== undefined) {
if (options.name === '')
throw new Error('a VFS needs a non-empty name');
this.name = options.name;
}
if (options.accessor !== undefined)
this.accessor = options.accessor;
if (options.vfsMd !== undefined)
this.vfsMd = options.vfsMd;
if (options.cachesReads !== undefined)
this.cachesReads = options.cachesReads;
if (options.sizesAlwaysKnown !== undefined)
this.sizesAlwaysKnown = options.sizesAlwaysKnown;
if (options.supportsSnapshot !== undefined)
this.supportsSnapshot = options.supportsSnapshot;
if (options.readRevalidatable !== undefined) {
this.readRevalidatable = options.readRevalidatable;
}
this.#commands = [...(options.commands ?? [])];
}
/**
* Whether this VFS defines the function `name`. A function the base
* declares is supported once a subclass or the instance itself replaces
* it; one only a subclass declares (a custom `@vfsCall`) is supported
* because it exists.
*/
supports(name) {
const method = methodName(name);
const own = this[method];
return (typeof own === 'function' &&
own !== BaseVFS.prototype[method]);
}
/** The bespoke commands and command trees this VFS was handed. */
commands() {
return this.#commands;
}
/**
* List the children of a directory, each as its stat answers it.
*
* On a walk, `handed.limit` is the most entries the walk can still use;
* a readdir may stop once it holds that many and answer them as
* `new DirListing({ entries, partial: true })`. A glob hands the word it
* matches as `path.pattern`; a backend that filters by it answers
* `partial: true` too. Mirrors Python's `readdir`.
*/
readdir(path, _handed) {
return Promise.reject(enotsup(this.name, 'readdir', path));
}
/**
* Read a file's bytes, or a window of them. Answer with the bytes, or with
* an async iterable that hands them out as the caller pulls, so a large
* file never sits in memory: an `async` method that resolves to either, or
* an `async *` one that yields chunks. Mirage collects a stream for a
* caller that needs the bytes whole and hands bytes to a streaming caller
* as one chunk. A window reaches this only when `readsRanges` is set: the
* caller otherwise reads the whole file and slices it.
*/
read(path, _handed, _offset = 0, _size = null, _signal) {
return Promise.reject(enotsup(this.name, 'read', path));
}
/** Describe a path; rejects with ENOENT when nothing is there. */
stat(path, _handed) {
return Promise.reject(enotsup(this.name, 'stat', path));
}
/** Whether anything is at `path`; derived from `stat` when not defined. */
exists(path, _handed) {
return Promise.reject(enotsup(this.name, 'exists', path));
}
/**
* The first `n` lines of a file, without reading the rest.
*
* `head -n N` asks this before it reads, so a backend that can fetch the
* start of a file on its own (a `LIMIT` on a table) does not send the whole
* file. The answer stands in for the file: exactly the first `n` lines a
* read gives, each with its newline, and the whole file when it has fewer.
* A file the read cache holds is read instead. Resolves null to have the
* file read the usual way; rejects with the file's read error otherwise
* (`EFBIG` when the answer passes a ceiling on one read, such as
* `max_read_rows`).
*/
firstLines(path, _n, _handed) {
return Promise.reject(enotsup(this.name, 'firstLines', path));
}
/**
* The last `n` lines of a file, without reading the rest.
*
* `tail -n N` asks this before it reads, and `tail -f` for its first print.
* The answer stands in for the file: exactly the last `n` lines a read
* gives, each with its newline, and the whole file when it has fewer. A
* file the read cache holds is read instead. Resolves null to have the file
* read the usual way; rejects with the file's read error otherwise (`EFBIG`
* when the answer passes a ceiling on one read).
*/
lastLines(path, _n, _handed) {
return Promise.reject(enotsup(this.name, 'lastLines', path));
}
/**
* How many newlines a read of the file gives, without reading it.
*
* `wc -l` asks this before it reads (a `COUNT(*)` on a table), and only
* when it counts lines alone. A file the read cache holds is read instead.
* Resolves null to have the file read the usual way; rejects with the
* file's read error otherwise.
*/
lineCount(path, _handed) {
return Promise.reject(enotsup(this.name, 'lineCount', path));
}
/**
* The digest the backend keeps for a file, without reading it.
*
* `md5sum` and its siblings ask this before they read, except under `-c`,
* which checks the bytes themselves, so a backend that stores a file's
* digest (an object's MD5 ETag, a drive's `md5Checksum`) does not send the
* file. Answer only a digest of exactly the bytes a read gives, in
* lowercase hex: a stored value that can be anything else (a multipart or
* encrypted object's ETag) is null. `algorithm` is the hashlib name, `md5`,
* `sha1`, `sha224`, `sha256`, `sha384`, `sha512` or `blake2b`, or
* `blake2b-N` for BLAKE2b at N bits. A file the read cache holds is read
* instead. Resolves null to have the file read and hashed; rejects
* with the file's read error otherwise.
*/
checksum(path, _algorithm, _handed) {
return Promise.reject(enotsup(this.name, 'checksum', path));
}
/**
* The last `n` lines of a file, then each line added to it, as it lands.
*
* `tail -f` on one file asks this for all it prints, so a backend that can
* be told about new data (a change stream) does not have to be polled, one
* that can be asked for only what is new does not read the whole file again
* on each poll, and the lines it prints first and the ones it follows meet
* at one point: none is lost or printed twice. The stream yields whole
* lines, each with its newline, in the order a read would show them, and
* ends only when the caller closes it; closing it ends a wait for the next
* line. `interval` is `tail -s`, the seconds between two asks for what is
* new; a change stream has no use for it. Resolves null to have `tail` read
* the file and poll it every `-s` seconds; rejects with the file's read
* error otherwise.
*/
follow(path, _n, _interval, _handed) {
return Promise.reject(enotsup(this.name, 'follow', path));
}
/**
* Every entry under a directory, in one answer.
*
* `find`, `du`, `ls -R`, `tree`, `grep -r` and `rg` ask this before they
* walk a directory, so a backend that can list a subtree on its own (a
* recursive listing, one query over many folders) does not take a
* `readdir` per directory. The answer stands in for the walk: every file and
* directory below `path`, keyed by its path below `path` (`a`, `a/b.txt`),
* each with the stat `stat` gives for it. The walk lists each directory from it in name order.
* Resolve null to have the command walk `readdir` the usual way; reject with
* what `readdir` of `path` rejects with otherwise. `handed.limit` is the
* most entries the walk can still use; resolve null once the tree holds
* more. A walk that reads one directory at a time asks with `level: true`,
* which the mount answers from its own listing (the directory's children,
* each with its stat) and never asks this function. Mirrors Python's
* `read_tree`.
*/
readTree(path, _handed) {
return Promise.reject(enotsup(this.name, 'readTree', path));
}
/**
* Replace a file's bytes, creating it when missing. `data` is the whole
* content, or an async iterable that hands it over as it is produced (a
* program's output redirected into the file); `materialize(data)` collects
* it for a store that takes one body. A stream that lands as it arrives
* lets a reader watch the file grow, and `atomic` asks that nothing show
* until it has ended whole, which a write that collects first already does.
*/
write(path, _data, _signal, _handed, _atomic = false) {
return Promise.reject(enotsup(this.name, 'write', path));
}
async _replace(request, _signal, _handed) {
await closeQuietly(request.body);
throw enotsup(this.name, 'conditional replacement', request.path);
}
/**
* Add bytes to the end of a file, creating it when missing. `data` is
* bytes or a stream, as `write` takes it. A VFS that defines `write` and
* not this is appended to by reading the file and writing it back.
*/
append(path, _data, _handed, _signal) {
return Promise.reject(enotsup(this.name, 'append', path));
}
/**
* Write bytes at an offset, keeping every byte outside them. As
* pwrite(2): a gap past the end reads back as zeros and a missing file
* is created. A VFS that defines `write` and not this is written by
* reading the file and writing it back.
*/
pwrite(path, _data, _offset, _handed, _signal) {
return Promise.reject(enotsup(this.name, 'pwrite', path));
}
/** Create an empty file, leaving an existing one as it is. */
create(path, _handed) {
return Promise.reject(enotsup(this.name, 'create', path));
}
/** Make a directory; `parents` makes missing parents too, as `mkdir -p`. */
mkdir(path, _parents = false, _handed) {
return Promise.reject(enotsup(this.name, 'mkdir', path));
}
/** Remove a file. */
unlink(path, _handed) {
return Promise.reject(enotsup(this.name, 'unlink', path));
}
/**
* Remove an empty directory. The index is the mount's, which a refused
* rmdir's hidden-remnant walk lists through. A store that checks
* emptiness, then deletes, looks again after: a child written in between
* keeps the directory, which stays as it was, and the call rejects with
* ENOTEMPTY.
*/
rmdir(path, _handed) {
return Promise.reject(enotsup(this.name, 'rmdir', path));
}
/**
* Remove a subtree in one call instead of entry by entry. A store that
* lists, then deletes, looks again after: a child written under the root
* since its listing stays, inside its directories, and the call rejects
* with ENOTEMPTY on the root, never success over a key it left.
*/
rmR(path, _handed) {
return Promise.reject(enotsup(this.name, 'rmR', path));
}
/**
* Move a name within this VFS. A store that moves a directory by listing
* it moves only what it listed: a child written under `src` since stays
* there, inside its directories, and the call rejects with ENOTEMPTY on
* `src`.
*/
rename(src, _dst, _handed) {
return Promise.reject(enotsup(this.name, 'rename', src));
}
/** Copy a file within this VFS without moving its bytes through mirage. */
copy(_src, dst, _signal, _handed) {
return Promise.reject(enotsup(this.name, 'copy', dst));
}
/** Copy a directory tree within this VFS in one call. */
/**
* Copy a file from another mount onto this one without moving its bytes
* through mirage.
*
* `cp` and `mv` between two mounts ask this before they read the file and
* write it here, once the dispatcher has judged the source as a read and
* the destination as a create. It is no dispatcher call of its own, so
* nothing reaches it unjudged. `source` is the source mount's VFS and `src`
* the file there. Reject EXDEV when this backend cannot copy from `source`
* (another kind of store, other credentials, a refusal), and the bytes move
* through the read and the write instead.
*/
copyFrom(_source, _src, dst) {
return Promise.reject(enotsup(this.name, 'copyFrom', dst));
}
dirCopy(_src, dst, _handed) {
return Promise.reject(enotsup(this.name, 'dirCopy', dst));
}
/**
* Resize a file, padding with zeros or cutting the end. `noCreate`
* refuses a missing file instead of creating it; a VFS that cannot hold
* that atomically rejects with ENOTSUP before writing.
*/
truncate(path, _length, _noCreate = false, _handed) {
return Promise.reject(enotsup(this.name, 'truncate', path));
}
/**
* Store metadata fields the backend keeps itself. Resolves to the fields
* it stored; the rest land in the namespace's attribute overlay.
*/
setattr(path, _fields, _handed) {
return Promise.reject(enotsup(this.name, 'setattr', path));
}
/**
* Search the resource under `path`; null declines, [] is none. One
* `[path, record]` pair per hit: the hit's virtual path, which the
* dispatcher drops the hit for when the session hides it, and the record
* the `search` CLI prints, a header line naming the path (`path:score`
* where the store ranks) and then the hit's text. Errors and incomplete
* results reject, never answered as a miss.
*/
search(path, _query, _handed) {
return Promise.reject(enotsup(this.name, 'search', path));
}
/** Search several scopes as one ranked query. */
searchMany(paths, _query, _handed) {
return Promise.reject(enotsup(this.name, 'search', paths[0] ?? ''));
}
/**
* Read several whole files in one request, for a read-ahead. Keyed by each
* answered path's `virtual`. A file may be left out (one larger than
* `limit` bytes, one the request could not read), and its own read answers
* it later; an error of the whole request rejects. Each file read is
* recorded as `read` records it, fingerprint included, so the read cache
* keeps its token. A VFS that needs what the listings hold declares
* `readMany` in `handed` and gets each path's `Handed`, in the order of
* `paths`. Mirrors Python's `read_many`.
*/
readMany(paths, _limit, _handed) {
return Promise.reject(enotsup(this.name, 'read', paths[0] ?? ''));
}
/**
* Files under `path` whose content may contain `text`. grep and rg still
* walk, filter, order and label every file, and read only the ones answered
* here, matched on `vfsPath` without case, so an extra file costs a read and
* a missing one is a wrong answer. A search that holds only keys names each one with
* `mountedPath(path, '/' + key)`. Resolve null when the answer may be
* incomplete (an error, a truncated result, an index that lags writes), and
* every file is read; reject to refuse the command, and the error's message
* is what it prints. The dispatcher drops every file the session hides
* from the answer.
*
* @param path a directory walked: one of grep's directory operands under -r
* or -R, or of rg's operands or the cwd; asked once per directory.
* @param text plain text every match holds, never a pattern: the pattern
* or each -e of grep and rg, or for a regex a fixed piece of at least
* three characters every match contains. Asked once per text; a file any
* answer holds is read.
* @param opts `wholeWord` is true under -w or -x with a plain-text
* pattern, where `text` is a whole word of every match, and false
* otherwise, where it may sit inside a word. `ignoreCase` is true under
* -i, and under rg -S with a lowercase pattern; folding case when false
* is fine.
*/
filesContaining(_path, _text, _opts, _handed) {
return Promise.resolve(null);
}
/**
* The lines of `path` that may contain `text`, in file order. grep and rg
* match each line themselves, so an extra line is fine and a missing one is
* a wrong answer. Each line keeps the newline the file has, whole or
* streamed. The lines stand in for the file when the output shows no line
* positions (no -n, -b, --column, --vimgrep), no context (no -A, -B, -C) and
* there is one text; otherwise the file is read only when some answer holds
* a line, and only a stream's first chunk is pulled. null reads the file;
* answer null for a file that may hold a NUL byte, since grep and rg call
* such a file binary from bytes outside its matching lines. Reject to refuse
* the command.
*
* @param path the file.
* @param text plain text every match holds, as `filesContaining` gets it.
* @param opts `ignoreCase` is true under -i, and under rg -S with a
* lowercase pattern.
*/
linesContaining(_path, _text, _opts, _handed) {
return Promise.resolve(null);
}
/**
* Called before a command acts on every file under `path`, once per
* directory a command copies, compares, changes or removes whole, before
* the first file it touches there: tar -c, zip -r, cp -r, mv across
* mounts, diff -r, rm -r, chmod -R, chown -R and chgrp -R. Resolve to let
* the walk run; reject to refuse it, and the error's message is what the
* command prints. The dispatcher has already refused more than the
* command's `walkLimit` files, so the default lets the walk run. Searches
* and listings are never refused: they stop after `walkLimit` files.
*
* @param path the directory about to be walked.
* @param files how many files the session sees under `path`, with every
* hidden file dropped: the mount's `readTree`, or under a
* `walkLimit` a walk of its listings that stops one file past it;
* null when neither counts, and the guard counts them itself.
*/
beforeFullWalk(_path, _files, _handed) {
return Promise.resolve();
}
/** Whether the backend is there to answer at all. */
isMounted() {
return true;
}
/**
* Where this driver's bytes live, as one string a person can read:
* `disk:/srv/data`, `s3:aws:my-bucket/prefix`. Two mounts with the
* same location address the same bytes, which is how `cp` and `mv`
* across mounts refuse to copy a file onto itself. Null, the default,
* means unknown, and the mount then treats this instance as a location
* of its own, which is the safe direction to be wrong in: a false
* "different" only keeps the pre-existing behavior, while a false
* "same" would refuse a legitimate move. A driver whose config pins
* the storage (a disk root, a bucket and key prefix) overrides this so
* two instances pointing at one target compare equal.
*/
storageLocation() {
return null;
}
/**
* How much space this backend has, for `df`. The default is UNKNOWN,
* which `df` renders as `-`. A driver that can answer truthfully (a
* real filesystem, a provider that exposes a storage quota) overrides
* this. Never fabricate a number.
*/
capacity() {
return Promise.resolve({ state: CapacityState.UNKNOWN });
}
/**
* What a snapshot records for this driver: its type and `needs_override`,
* since the base cannot know a subclass's constructor, so both loaders
* then require the mount to be handed back live (`load`'s overrides;
* `copy()` does this itself). A driver that owns its content (an
* in-memory store) overrides this and {@link BaseVFS.loadState} to carry
* it and drops the flag; a driver over a remote service keeps the default
* and pins what it read through `supportsSnapshot` fingerprints instead.
* `toStateDict` calls this and `loadState` on every mount, which is why
* neither is optional. Mirrors Python `BaseVFS.get_state`.
*/
getState() {
return { type: this.name, needs_override: true };
}
/** Snapshot a config-backed driver through its schema's secret markers. */
configState(schema, config) {
return { type: this.name, config: redactConfigWithSchema(schema, config) };
}
/**
* Take back what {@link BaseVFS.getState} put out. A no-op by
* default, because the bare `{type}` carries nothing to restore.
* Mirrors Python `BaseVFS.load_state`.
*/
loadState(_state) {
// Nothing to take back.
}
get isClosed() {
return this.#closed;
}
/**
* Release what this driver owns, exactly once: its accessor's handles
* are its own to close, since the Accessor seam carries no lifecycle.
* A backend with handles of its own (a db pool, an ssh channel)
* overrides this and calls `super.close()`. Mirrors Python
* `BaseVFS.close`.
*/
close() {
this.#closed = true;
return Promise.resolve();
}
};
})();
export { BaseVFS };
//# sourceMappingURL=base.js.map