@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
102 lines • 3.96 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 { FileType } from "../../types.js";
import { deleteFile, deleteDirectory, listDirectoryContents } from "./client.js";
import { isNotFound } from "./errors.js";
import { backendPath, virtualPath } from "./path.js";
import { stat } from "./stat.js";
import { unlink } from "./unlink.js";
import { enoent, enotempty } from "../../errors/fs.js";
/** Whether a directory lists any entry; a missing one holds none. Mirrors Python's `holds_child`. */
export async function holdsChild(accessor, remoteDir) {
try {
return (await listDirectoryContents(accessor, remoteDir)).length > 0;
}
catch (exc) {
if (isNotFound(exc))
return false;
throw exc;
}
}
// Whether a move copied this entry as it is listed now. Mirrors Python's
// `_as_copied`.
function asCopied(copied, entry) {
const seen = copied.get(entry.path);
return (seen?.isDirectory === entry.isDirectory &&
seen.fileSize === entry.fileSize &&
seen.lastModified === entry.lastModified);
}
async function removeTreeRecurse(accessor, remoteDir, removed, copied) {
let left = false;
for (const entry of await listDirectoryContents(accessor, remoteDir)) {
if (copied !== null && !asCopied(copied, entry)) {
left = true;
}
else if (entry.isDirectory) {
left = (await removeTreeRecurse(accessor, entry.path, removed, copied)) || left;
}
else {
await deleteFile(accessor, entry.path);
removed.push(entry.path);
}
}
if (left)
return true;
try {
await deleteDirectory(accessor, remoteDir);
}
catch (exc) {
// The Files API deletes only an empty directory, so a file written
// after the listing refuses it.
if (isNotFound(exc) || !(await holdsChild(accessor, remoteDir)))
throw exc;
return true;
}
removed.push(remoteDir);
return false;
}
/**
* Remove a directory tree, or only what a move copied out of it (`copied`,
* the entries by backend path as their listing showed them; only an entry
* still listed the same is deleted, and null deletes every entry listed).
* The walk lists each directory once, so a file written under it since, or
* an entry the move never copied or that changed since its copy, stays and
* keeps its directories: the tree is ENOTEMPTY. Returns the backend paths removed. Mirrors Python's
* `remove_tree`.
*/
export async function removeTree(accessor, path, copied = null) {
const removed = [];
let left;
try {
left = await removeTreeRecurse(accessor, backendPath(accessor.config, path), removed, copied);
}
catch (exc) {
if (isNotFound(exc))
throw enoent(path.virtual);
throw exc;
}
if (left)
throw enotempty(path.virtual);
return removed;
}
export async function rmRecursive(accessor, path) {
const fileStat = await stat(accessor, path);
if (fileStat.type !== FileType.DIRECTORY) {
await unlink(accessor, path);
return [path.mountPath];
}
const removed = await removeTree(accessor, path);
return removed.map((backend) => virtualPath(accessor.config, backend, ''));
}
//# sourceMappingURL=rm.js.map