UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

200 lines • 9.16 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 { conditioned, deleteCondition, knownVersions, settleAfter, stale, } from "../../cache/context.js"; import { liftLost, lostCount, record, startOp } from "../../observe/context.js"; import { eisdir, enoent, enotempty } from "../../errors/fs.js"; import * as kp from "../../utils/key_prefix.js"; import { rstripSlash } from "../../utils/slash.js"; import { isDir } from "../../utils/stat_view.js"; import { holdsChild, keepWalk, refused, requireHook, } from "./driver.js"; import { ConditionLostError } from "./errors.js"; import { makeStat } from "./stat.js"; /** Build single-key deletion over one driver. */ export function makeUnlink(driver) { const stat = makeStat(driver); return async function unlink(accessor, path) { // unlink(2) answers ENOENT for a missing name and EISDIR for a // directory; a store's delete is silent for both, and only the // command builders check first (see `makeRmdir`). const found = await stat(accessor, path); if (isDir(found)) throw eisdir(path); const key = kp.apply(driver.keyPrefixOf(accessor), path.mountPath); // A delete needs no prior read, only that nobody wrote since. const cond = await deleteCondition(path, found.fingerprint ?? null); const timer = startOp(); const settle = () => { // Also when the delete threw: one that throws part-way has already // removed keys, and a pin that outlives the object it names fails // the next snapshot load. record('unlink', path.virtual, driver.vfs, 0, timer); return Promise.resolve(); }; // The connect is outside, because a connection that never opened // removed nothing. const { conn, close } = await driver.connect(accessor); const upto = lostCount(); try { await settleAfter(async () => { try { if (cond !== null) await requireHook(driver.deleteIf)(conn, key, cond); else await driver.deleteFile(conn, key); } finally { await close(); } }, settle); } catch (err) { if (err instanceof ConditionLostError) throw await refused(path, err, cond); throw err; } liftLost(path, upto); }; } /** * Build recursive prefix deletion over one driver. * * Serves both the `rmR` and `rmdir` slots: on a keyed store an empty * directory is its marker object, so removing it and removing a subtree * are the same prefix delete. */ export function makeRemovePrefix(driver) { return async function removePrefix(accessor, path) { const kpfx = driver.keyPrefixOf(accessor); const pfx = kp.applyDir(kpfx, path.mountPath); // Keeps and reports a key changed since the agent (or walk) saw it. const conditional = conditioned(path, 'delete'); const timer = startOp(); const settle = () => { // A prefix delete is a paginated walk, so a failure mid-walk has // already removed keys, as in unlink. record('rm_r', path.virtual, driver.vfs, 0, timer); return Promise.resolve(); }; const { conn, close } = await driver.connect(accessor); const upto = lostCount(); let left; try { left = await settleAfter(async () => { try { if (conditional) { await requireHook(driver.deletePrefixIf)(conn, pfx, knownVersions(path, kpfx)); } else await driver.deletePrefix(conn, pfx); // The walk lists once, so a key written under the prefix after its // page was read stays; as GNU rm's last rmdir meets an entry // created after its read, that is ENOTEMPTY. return await holdsChild(driver, conn, pfx); } finally { await close(); } }, settle); } catch (err) { if (!(err instanceof ConditionLostError)) throw err; const key = await keepWalk(path, kpfx, err); const sent = err.versions.get(key); throw await stale(kp.keyPath(path, kpfx, key), { version: typeof sent === 'string' ? sent : null, }); } liftLost(path, upto, true); if (left) throw enotempty(path); }; } /** * Build POSIX `rmdir` over one driver: refuse a non-empty prefix. * * Not `makeRemovePrefix`. On a keyed store an empty directory is its * zero-byte marker object, so a prefix delete removes an empty directory * correctly and a *non-empty* one recursively, which is `rm -r`, not * `rmdir`. Sharing one function between the two slots made `rmdir` destroy * a whole subtree for every caller that does not pre-check emptiness * itself, and the command builders are the only callers that do: FUSE, * `ws.vfs` and the sandbox runtimes all reach the op directly. * * The listing is the same one `readdir` reads, so the two agree on what a * child is: a `marker` entry is the prefix's own marker (or a key the * delimiter listing could not classify) and proves only that the directory * exists, while any `f` or `d` entry is a child and makes this ENOTEMPTY. * The walk stops at the first child rather than listing the whole directory * to answer a yes/no question. * * A prefix holding no key at all -- not even the marker -- is a directory * the store does not have, and rmdir(2) reports ENOENT for it. * `makeRemovePrefix` stays silent there on purpose, because `rm -r` owns * that refusal through its own `-f` handling. */ export function makeRmdir(driver) { return async function rmdir(accessor, path) { const pfx = kp.applyDir(driver.keyPrefixOf(accessor), path.mountPath); const isRoot = rstripSlash(path.mountPath) === ''; const timer = startOp(); const { conn, close } = await driver.connect(accessor); let sawKey = false; let hasChild = false; let deleted = false; try { for await (const child of driver.listChildren(conn, pfx)) { sawKey = true; if (child.kind !== 'marker') { hasChild = true; break; } } // The marker only, never the prefix: a child created between the // listing above and this delete would be taken down with it, which // is the subtree loss this function exists to stop in a smaller // window. A root holding no key has no marker and falls through as // the no-op the prefix delete already was. if (!hasChild && sawKey) { await driver.deleteFile(conn, pfx); deleted = true; // A child written between the listing and the delete still holds // the directory, so rmdir(2) is ENOTEMPTY and the marker goes back: // a refused rmdir changes nothing. hasChild = await holdsChild(driver, conn, pfx); if (hasChild && driver.markersSupported !== false) { await driver.put(conn, pfx, new Uint8Array()); } } } finally { // Gated on the delete having run, not on "did not throw": the // keyless root above deletes nothing and throws nothing, and a // listing that fails after its first marker reaches here having // deleted nothing either. Recording in those cases would retract a // pin for an object no one touched. A delete that rejects is the // one case `unlink` treats the other way; here it is immaterial, // because what rmdir removes is the "d/" marker and a pin can only // ever name the "d" object beside it. if (deleted) record('rmdir', path.virtual, driver.vfs, 0, timer); await close(); } if (hasChild) throw enotempty(path); if (!sawKey && !isRoot) throw enoent(path); }; } //# sourceMappingURL=remove.js.map