UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

136 lines • 5.3 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, settleAfter, heldVersions, keepRefused } from "../../cache/context.js"; import { liftLost, lostCount, record, startOp } from "../../observe/context.js"; import { childSpec } from "../../utils/key_prefix.js"; import { enoent, enotdir, enotempty } from "../../errors/fs.js"; import { BoxApiError } from "./client.js"; import { deleteFile, deleteFolder, deleteWebLink, listFolderItems } from "./api.js"; import { GONE_STATUS, LOST_STATUS, CONFLICT_STATUS } from "./constants.js"; import { liveOf } from "./fingerprint.js"; import { liveItemOf, pathParts } from "./resolve.js"; import { deleteResolved } from "./unlink.js"; export async function rmdir(accessor, path, handed = {}) { const item = await liveItemOf(accessor, path, handed.entry); if (item === null) throw enoent(path.virtual); if (item.type !== 'folder') throw enotdir(path.virtual); await deleteEmptyFolder(accessor.tokenManager, item.id, path); } /** * Delete a folder only if it is empty: Box's 409 becomes ENOTEMPTY. Mirrors * Python's `delete_empty_folder`. */ export async function deleteEmptyFolder(tm, folderId, path) { try { await deleteFolder(tm, folderId, false); } catch (err) { if (err instanceof BoxApiError && err.status === CONFLICT_STATUS) throw enotempty(path); throw err; } } /** Delete a web link plainly, since it holds no content. Mirrors Python's `_delete_link`. */ async function deleteLink(tm, linkId, path) { try { await deleteWebLink(tm, linkId); } catch (err) { if (!(err instanceof BoxApiError) || err.status !== GONE_STATUS) throw err; console.debug(`${path.virtual} already gone: ${String(err)}`); } } /** * Delete a folder file by file, each held to the version read or listed. A file * that changed stays, with the folders above it. Returns whether the folder * itself was deleted. */ async function deleteTree(accessor, folder, path, lost) { const tm = accessor.tokenManager; const kids = await listFolderItems(tm, folder.id); const walk = kids.map((kid) => [kid, childSpec(path, kid.name)]); const held = await heldVersions(walk.map(([, spec]) => spec)); let emptied = true; for (const [i, [kid, spec]] of walk.entries()) { if (kid.type === 'folder') { emptied = (await deleteTree(accessor, kid, spec, lost)) && emptied; continue; } if (kid.type === 'web_link') { await deleteLink(tm, kid.id, spec); continue; } const live = liveOf(kid); const want = held[i] ?? live?.content ?? null; if (want !== live?.content) { lost.push([spec, want]); emptied = false; continue; } try { await deleteFile(tm, kid.id, want !== null ? live.native : null); } catch (err) { if (!(err instanceof BoxApiError)) throw err; if (err.status === GONE_STATUS) { console.debug(`${spec.virtual} already gone: ${String(err)}`); continue; } if (err.status !== LOST_STATUS) throw err; lost.push([spec, want]); emptied = false; } } if (!emptied) return false; await deleteEmptyFolder(tm, folder.id, path); return true; } /** Delete `item`: a file alone, a folder walked or whole. Mirrors Python's `_remove`. */ async function remove(accessor, path, item, lost) { if (item.type !== 'folder') await deleteResolved(accessor, path, item); else if (conditioned(path, 'delete')) await deleteTree(accessor, item, path, lost); else await deleteFolder(accessor.tokenManager, item.id, true); return true; } /** * Remove a file or folder; a conditional mount walks it file by file. Mirrors * Python's `rm_r`. */ export async function rmR(accessor, path, handed = {}) { if (pathParts(path).length === 0) return; const item = await liveItemOf(accessor, path, handed.entry); if (item === null) throw enoent(path.virtual); const upto = lostCount(); const timer = startOp(); const lost = []; await settleAfter(() => remove(accessor, path, item, lost), async (done) => { record('rm_r', path.virtual, 'box', 0, timer); const refusal = await keepRefused(lost); if (done === true && refusal !== null) throw refusal; }); liftLost(path, upto, true); } //# sourceMappingURL=rmdir.js.map