@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
135 lines • 6.29 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 { conditionVersion, nativeCondition, stale, writeCondition } from "../../cache/context.js";
import { childSpec } from "../../utils/key_prefix.js";
import { uploadStat } from "../../utils/upload.js";
import { eisdir, enoent, enotdir } from "../../errors/fs.js";
import { liftLost, lostCount, record, startOp } from "../../observe/context.js";
import { BoxApiError } from "./client.js";
import { CONFLICT_STATUS } from "./constants.js";
import { absentOn404, copyFile, copyFolder, deleteFile, listFolderItems, refused, } from "./api.js";
import { liveOf } from "./fingerprint.js";
import { statFromItem } from "./readdir.js";
import { itemOf, liveItem, liveItemOf, parentFolder, pathParts } from "./resolve.js";
/**
* Clear the file a copy or move lands on, held to the version read. Returns
* the op's condition, null when unconditional. Mirrors Python's `replace_file`.
*/
export async function replaceFile(accessor, dst, lead) {
let existing = lead;
const cond = await writeCondition(dst, 'copy');
if (cond !== null)
existing = await liveItem(accessor, dst, existing);
const etag = await nativeCondition(dst, cond, liveOf(existing), 'copy');
if (existing !== null) {
try {
await deleteFile(accessor.tokenManager, existing.id, etag);
}
catch (err) {
throw (await refused(dst, err, cond, etag)) ?? err;
}
}
return cond;
}
/**
* The refusal for a copy or move whose cleared destination came back. The
* condition the op sent stays held, whatever took the name. Mirrors Python's
* `retaken`.
*/
export async function retaken(err, cond, dst) {
if (cond === null || !(err instanceof BoxApiError) || err.status !== CONFLICT_STATUS)
return null;
const held = conditionVersion(cond);
return stale(dst, { version: held === '' ? null : held });
}
/**
* Record a path the copy changed, as soon as it changed. A later step of the
* same copy can take a while, and another stage of the line may read the path
* meanwhile: recorded now, the retract stays older than that read, and the
* dispatcher serves no cached bytes for a path the line retracted. Mirrors
* Python's `_landed`.
*/
function landed(dst, whole, timer, upto) {
record(whole ? 'copy_prefix' : 'copy', dst.virtual, 'box', 0, timer);
liftLost(dst, upto, whole);
}
/**
* Copy `item` to `dst`, merging a folder into a folder: `folderId` is the
* folder `dst` lands in, `existing` what is at `dst`. Answers the copy's stat,
* null for a merge or a reply that names none. Mirrors Python's `_copy_into`.
*/
async function copyInto(accessor, item, dst, folderId, existing) {
const tm = accessor.tokenManager;
const timer = startOp();
const upto = lostCount();
if (item.type === 'folder' && existing !== null && existing.type === 'folder') {
// Merge into an existing folder, as cp -r does: copy each child rather
// than replacing the folder, so pre-existing entries survive. A folder the
// listing named but Box no longer has is ENOENT, which has the mount list
// again and copy into what is there now.
const there = new Map((await absentOn404(dst.virtual, () => listFolderItems(tm, existing.id))).map((kid) => [kid.name, kid]));
const kids = await absentOn404(dst.virtual, () => listFolderItems(tm, item.id));
for (const child of kids) {
await copyInto(accessor, child, childSpec(dst, child.name), existing.id, there.get(child.name) ?? null);
}
return null;
}
const dstParts = pathParts(dst);
const newName = dstParts[dstParts.length - 1] ?? '';
let cond = null;
if (existing !== null && existing.id !== item.id) {
// Folder onto folder already merged above, so what is left is a type
// mismatch or a file replacing a file. cp refuses either mismatch
// (rename(2)'s own errnos), mirroring gdrive and the msgraph copyTree;
// only a file gives way to a file.
if (existing.type === 'folder')
throw eisdir(dst.virtual);
if (item.type === 'folder')
throw enotdir(dst.virtual);
cond = await replaceFile(accessor, dst, existing);
}
else if (existing === null && item.type === 'file') {
cond = await replaceFile(accessor, dst, null);
}
let made;
try {
if (item.type === 'folder') {
made = await copyFolder(tm, item.id, folderId, newName);
landed(dst, true, timer, upto);
}
else {
made = await copyFile(tm, item.id, folderId, newName);
landed(dst, false, timer, upto);
}
}
catch (err) {
throw (await retaken(err, cond, dst)) ?? err;
}
return uploadStat(made, statFromItem, dst.virtual);
}
/**
* Copy a file or folder server-side, recording each path it changed. Each path
* is recorded as it lands; a replaced file only once its copy lands, so through
* the clear the line keeps its version. A landed path also lifts the line's lost
* marks on it, and beneath it for a folder copied whole. The dispatcher evicts
* what the copy changed, also when it fails. Answers the copy's stat, null for
* a merge or a reply that names none.
*/
export async function copy(accessor, src, dst, handed = {}) {
const item = itemOf(handed.entry);
if (item === null)
throw enoent(src.virtual);
return copyInto(accessor, item, dst, parentFolder(accessor, dst, handed.dstParent), await liveItemOf(accessor, dst, handed.dstEntry));
}
//# sourceMappingURL=copy.js.map