@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
118 lines • 5.28 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. =========
import { walkFind } from "../../../core/generic/find.js";
import { FileType } from "../../../types.js";
import { isEnotdir, isMissingPath } from "../../../errors/fs.js";
import { rstripSlash } from "../../../utils/slash.js";
import { readdirOp, statOp } from "../generic/crossmount/utils.js";
import { beforeFullWalk, treeWalk } from "../utils/tree.js";
import { callerSignal } from "../../../context/session_context.js";
function walkWith(readdir, stat, dispatch, command) {
return async (p, findType) => {
if (findType === 'f')
await beforeFullWalk(dispatch, p, command);
const unreadable = [];
const paths = await walkFind(p, { unreadable, readdir, stat }, { type: findType });
return { paths, unreadable };
};
}
/**
* The subtree listing tar and zip both walk with.
*
* Reuses find's walk so an archiver classifies an entry exactly the way find
* does (through stat, never by name). The two calls a directory operand makes
* share one readdir cache, so the second is answered from the index instead of
* the backend.
*
* walkFind stands in for a backend's native find op, so it answers in
* mount-relative keys; both archivers name members from virtual paths, so they
* are lifted back here the way findGeneric lifts them. A directory the walk
* could not open rides along (as the virtual path the walk recorded), so the
* archiver can report it the way GNU does instead of silently leaving its
* contents out. The files listed are every file the archiver reads, so the
* mount that owns the operand may refuse the walk before they are listed.
*/
export function walkOf(files, dispatch, command) {
const [readdir, stat] = treeWalk(files.readTree, files.readdir, files.stat, files.answers('read_tree'), files.signal);
return walkWith((spec) => readdir(spec), stat, dispatch, command);
}
/**
* The directory probe tar's `-C` check uses.
*
* Two channels, because a stat miss alone is not absence: on a prefix store
* a directory is the set of keys under it and nothing answers stat for it,
* so a readdir that returns anything is the second and deciding opinion.
*/
function isDirWith(stat, readdir) {
return async (p) => {
try {
return (await stat(p)).type === FileType.DIRECTORY;
}
catch (err) {
if (!isMissingPath(err) && !isEnotdir(err))
throw err;
console.debug(`archive: ${p.virtual} does not stat; asking readdir`);
}
try {
return (await readdir(p)).length > 0;
}
catch (err) {
if (!isMissingPath(err) && !isEnotdir(err))
throw err;
console.debug(`archive: ${p.virtual} is not a directory on either channel: ${String(err)}`);
return false;
}
};
}
export function isDirOf(files) {
return isDirWith(files.stat, (p) => files.readdir(p));
}
/**
* One directory as its own backend lists it, through the dispatcher.
*
* The dispatcher adds the names the namespace owes a directory (nested mount roots
* and symlinks) to every listing. The archive scan adds those itself, from
* the same tables, and a walk that followed one would descend into a link's
* target under the link's name or into a mount the scan must not cross, so
* they are left to the scan.
*/
function ownListing(readdir, owed) {
return async (p) => {
const entries = await readdir(p);
if (owed === undefined)
return entries;
const names = new Set(owed(p.virtual));
return entries.filter((entry) => {
const trimmed = rstripSlash(entry);
return !names.has(trimmed.slice(trimmed.lastIndexOf('/') + 1));
});
};
}
/**
* The subtree listing tar and zip walk with when a line spans mounts.
*
* Every directory is listed on the mount that owns it, so operands from
* several mounts go into one archive, and find's walk classifies what it
* lists exactly as it does on one backend. `readTree` is the walk's subtree
* read (`dispatchedReadTree`); undefined lists every directory.
*/
export function relayWalkOf(dispatch, owed, command, readTree) {
const [readdir, stat] = treeWalk(readTree, readdirOp(dispatch), statOp(dispatch), true, callerSignal());
return walkWith(ownListing(readdir, owed), stat, dispatch, command);
}
// The directory probe tar's `-C` check uses when a line spans mounts.
export function relayIsDirOf(dispatch) {
return isDirWith(statOp(dispatch), readdirOp(dispatch));
}
//# sourceMappingURL=archive_io.js.map