@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
136 lines • 5.3 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 { 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