UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

281 lines • 11.8 kB
// ========= 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