@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
281 lines • 11.8 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 { fsStrerror, isEacces, isEnotdir, isFsError, isMissingPath } from "../../../../errors/fs.js";
import { underPath } from "../../../../utils/key_prefix.js";
import { FileType, PathSpec } from "../../../../types.js";
import { WalkBudget, treeWalk } from "../../utils/tree.js";
import { duGeneric, facts, } from "../../generic/du.js";
import { rstripSlash, stripSlash } from "../../../../utils/slash.js";
import { commandFiles } from "../files.js";
import { specOf } from "../../../spec/builtins.js";
import { FlagView } from "../../../spec/flag_view.js";
import { compareCodePoints } from "../../../../utils/sort.js";
import { linksOf, mountView, mountsOf } from "../../utils/links.js";
/**
* What the walks of one `du` invocation met.
*
* Each operand's walk lists at most `walkLimit` entries of each mount it
* crosses (`WalkBudget`): a walk the mount's `readTree` does not answer takes
* one `readdir` per directory, which on an API-backed tree is one request per
* directory. Slack, for instance, exposes a directory per channel per day, so
* an unbounded walk of a real workspace is tens of thousands of requests.
* Where a walk ran out is kept, so the answer says it is partial.
*/
export class DuTally {
mounts;
// Where a walk ran out, in the order met.
stopped = [];
// Paths the walk was refused (a rule denied them below the operand), in
// the order it met them; the generic reports them after the walks the
// way GNU names an unreadable directory.
unreadable = [];
// Every directory the last walk met, the operand's own included, which is
// how one no file points at (an empty one, or a refused one) still gets
// GNU's row. Each walk starts it afresh, so an operand's rows are its own
// walk's.
directories = [];
// Under -L, each link that leads nowhere, with the strerror GNU adds for
// a chain that loops.
inaccessible = [];
// The mount table the limits come from.
constructor(mounts = null) {
this.mounts = mounts;
}
}
/**
* Account for an error the walk met at `path`, or rethrow it.
*
* Absence counts as zero, because an entry listed a moment ago can be
* gone by the time the walk reaches it; `isMissingPath` is that set (a
* stamped ENOENT, or a path outside every mount), with ENOTDIR for one
* whose directory became a plain file meanwhile. A refusal counts as
* zero too, but never silently: GNU `du` skips what it cannot read,
* names it on stderr and exits 1, so the path is recorded here for the
* generic to report. Everything else -- a 429, a 5xx, an aborted line --
* says the walk never saw that subtree, and a confidently wrong size is
* worse than an unknown one, so it surfaces instead of being summed as
* nothing.
*
* Both of the walk's calls come through here, because a refused `stat` is
* the same fact as a refused `readdir`: a rule denying a path outright
* refuses before the walk ever learns the entry is a directory. Recording
* only the `readdir` one left a refused subtree missing from the total
* with du still exiting 0, which no caller can tell from a small tree.
* The reported line is `cannot read directory` either way, which is the
* wording GNU uses once it knows the entry is one; a refused *file* is
* named by a noun that does not fit it, and being named loudly is still
* the better half of that trade.
*/
function accountForWalkError(err, path, tally) {
if (isEacces(err)) {
tally.unreadable.push(path.virtual);
return;
}
if (!isMissingPath(err) && !isEnotdir(err))
throw err;
}
function treeOf(files, listing) {
const [readdir, stat] = treeWalk(files.readTree, listing, files.stat, files.answers('read_tree'), files.signal);
return { readdir, stat };
}
/**
* What `du -L` follows links by, for one invocation.
*
* GNU counts each file once, under the first name its walks meet, across
* every operand of the line (coreutils 9.7): what one operand reaches is not
* counted again under another, and an operand already counted prints
* nothing. A link leading back into its operand's own tree waits for
* the walk to end, and counts only if the walk never reached the file under
* the file's own name: mirage's sorted walk prefers that name (where GNU's
* readdir order may reach the link first), and a directory the walk may not
* list leaves the link the one name that reaches the file, which GNU then
* counts it under. Under `-x` a link that leads onto another mount is left out
* whole, as GNU leaves out what stat(2) places on another device.
*/
class Dereference {
links;
mounts;
counted;
tree;
home;
waiting;
constructor(links,
// The mount boundaries `-x` keeps the walks within, null without `-x`.
mounts,
// What the walks counted so far, each as the path it resolves to.
counted = new Set(),
// The operand being walked, as the path it resolves to.
tree = '/',
// Under `-x`, the mount that operand lies on.
home = null,
// The walk's links into its tree, each with the path it resolves to.
waiting = []) {
this.links = links;
this.mounts = mounts;
this.counted = counted;
this.tree = tree;
this.home = home;
this.waiting = waiting;
}
/** This invocation's state, walking one operand. */
operand(path) {
const tree = this.links.resolve(path.virtual);
const home = this.mounts === null ? null : this.mounts.rootOf(tree);
return new Dereference(this.links, this.mounts, this.counted, tree, home);
}
/** The same walk, settling a link into its tree under the link's name. */
under(link) {
return new Dereference(this.links, this.mounts, this.counted, link.virtual, this.home, this.waiting);
}
/** Whether an earlier operand's walk already counted this one. */
repeated(path) {
return this.counted.has(this.links.resolve(path.virtual));
}
}
/**
* What `du -L` counts an entry as, null when it counts nothing. `info` is the
* entry's own stat, a link's row for a link; a link it cannot follow is
* recorded on `tally`.
*/
async function counted(follow, path, info, tally) {
let real;
let target;
try {
real = follow.links.resolve(path.virtual);
target = info.type === FileType.SYMLINK ? await follow.links.targetStat(path.virtual) : info;
}
catch (err) {
if (!isFsError(err))
throw err;
tally.inaccessible.push([path.virtual, fsStrerror(err)]);
return null;
}
if (target === null) {
tally.inaccessible.push([path.virtual, null]);
return null;
}
if (follow.home !== null && follow.mounts?.rootOf(real) !== follow.home)
return null;
if (info.type === FileType.SYMLINK && underPath(real, follow.tree)) {
follow.waiting.push([path, real]);
return null;
}
if (follow.counted.has(real))
return null;
follow.counted.add(real);
return target;
}
async function duWalk(walk, path, tally, scan, entries, follow) {
let info;
try {
info = await walk.stat(path);
}
catch (err) {
accountForWalkError(err, path, tally);
return 0;
}
if (follow !== null) {
const row = await counted(follow, path, info, tally);
if (row === null)
return 0;
info = row;
}
if (info.type !== FileType.DIRECTORY) {
const size = info.size ?? 0;
if (entries !== null)
entries.push([`/${stripSlash(path.virtual)}`, size]);
return size;
}
tally.directories.push(path.virtual);
if (!scan.opens(path.virtual))
return 0;
const limit = scan.listingLimit(path.virtual);
let children;
try {
children = await (limit === null ? walk.readdir(path) : walk.readdir(path, false, limit));
}
catch (err) {
accountForWalkError(err, path, tally);
return 0;
}
let total = 0;
for (const child of children) {
if (!scan.spend(path.virtual))
break;
total += await duWalk(walk, PathSpec.fromStrPath(child), tally, scan, entries, follow);
}
return total;
}
/**
* Walk one operand, then count each link into its tree whose file the walk
* never reached under the file's own name. `entries` collects each file's
* size under its name, null for a total alone. Mirrors Python's
* _walk_operand.
*/
async function walkOperand(walk, tally, follow, path, entries) {
tally.directories.length = 0;
const scan = new WalkBudget(path, tally.mounts);
const state = follow === null ? null : follow.operand(path);
let total = await duWalk(walk, path, tally, scan, entries, state);
if (state !== null) {
for (const [link, real] of state.waiting) {
if (!state.counted.has(real)) {
total += await duWalk(walk, link, tally, scan, entries, state.under(link));
}
}
}
for (const line of scan.stopped) {
if (!tally.stopped.includes(line))
tally.stopped.push(line);
}
return total;
}
export function walkSize(walk, tally, follow, path) {
return walkOperand(walk, tally, follow, path, null);
}
export async function walkEntries(walk, tally, follow, path) {
const entries = [];
const total = await walkOperand(walk, tally, follow, path, entries);
entries.sort((a, b) => compareCodePoints(a[0], b[0]));
return [entries, total];
}
const du = async (_accessor, paths, _texts, ctx) => {
const files = commandFiles(ctx, paths);
const flags = new FlagView(ctx.flags, specOf('du'));
const tally = new DuTally(mountsOf(ctx) ?? null);
// Kept on one filesystem, a listing leaves out the roots of the mounts
// below it, which the walk must neither list nor stat. Those names would
// count toward a cut listing the walk then finds short, so such a listing
// is never cut.
const bound = flags.asBool('one_file_system') ? mountView(ctx.vfs) : undefined;
const listing = bound === undefined
? files.readdir
: async (path, nofollow) => {
const owner = bound.rootOf(path.virtual);
return (await files.readdir(path, nofollow)).filter((e) => bound.rootOf(rstripSlash(e)) === owner);
};
const links = linksOf(ctx);
const follow = links !== undefined && flags.typedOrder('L', 'P').at(-1) === 'L'
? new Dereference(links, flags.asBool('one_file_system') ? (mountsOf(ctx) ?? null) : null)
: null;
const computeSize = (p) => walkSize(treeOf(files, listing), tally, follow, p);
const computeEntries = (p) => walkEntries(treeOf(files, listing), tally, follow, p);
return duGeneric(paths, ctx, files.resolveGlob, files.stat, computeSize, computeEntries, () => tally.stopped, () => tally.unreadable, () => tally.directories, () => tally.inaccessible, follow === null ? undefined : (p) => follow.repeated(p));
};
export const BUILDER = {
name: 'du',
facts: facts,
fn: du,
};
//# sourceMappingURL=du.js.map