@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
121 lines • 5.14 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 { enoent, enotempty } from "../../errors/fs.js";
import { rstripSlash } from "../../utils/slash.js";
import { compareCodePoints } from "../../utils/sort.js";
import { nowIso } from "../../utils/dates.js";
import { norm } from "../../utils/path.js";
import { checkDestParents, checkRename, keepLeft, lookupError } from "./dest.js";
// Re-key every descendant of a renamed directory. A synthetic-directory
// store keeps subdirectories as members of their own set, so moving only
// the files leaves those behind. The phantom tree the orphans imply makes
// the old name reappear in its parent's listing and then stat as missing
// -- the same shape checkDestParents refuses on the way in. The whole new
// tree is written before any old key is deleted: a full Redis refuses
// writes but still runs deletes, so a refused write must find the old tree
// intact. Returns the directories removed, whose metadata the caller drops
// once it knows which ones a late key keeps.
async function moveSubtree(store, s, d) {
const prefix = rstripSlash(s) + '/';
const dPrefix = rstripSlash(d) + '/';
const dirs = [...(await store.listDirs())]
.sort(compareCodePoints)
.filter((k) => k.startsWith(prefix));
const moved = [];
for (const key of dirs) {
const newKey = dPrefix + key.slice(prefix.length);
const mod = await store.getModified(key);
const attrs = await store.getAttrs(key);
await store.addDir(newKey);
if (mod !== null)
await store.setModified(newKey, mod);
if (Object.keys(attrs).length > 0)
await store.setAttrs(newKey, attrs);
}
for (const key of await store.listFiles()) {
if (!key.startsWith(prefix))
continue;
const newKey = dPrefix + key.slice(prefix.length);
const data = await store.getFile(key);
if (data === null)
continue;
const mod = await store.getModified(key);
const attrs = await store.getAttrs(key);
await store.setFile(newKey, data);
if (mod !== null)
await store.setModified(newKey, mod);
if (Object.keys(attrs).length > 0)
await store.setAttrs(newKey, attrs);
moved.push(key);
}
for (const key of moved) {
await store.delFile(key);
await store.delModified(key);
await store.delAttrs(key);
}
for (const key of dirs)
await store.removeDir(key);
return dirs;
}
export async function rename(accessor, src, dst) {
const s = norm(src.mountPath);
const d = norm(dst.mountPath);
const now = nowIso();
const store = accessor.store;
await checkDestParents(store, dst, d);
await checkRename(store, src, s, dst, d);
// Onto itself is a no-op: the write-then-delete below would erase it.
if (s === d && ((await store.hasFile(s)) || (await store.hasDir(s))))
return;
if (await store.hasFile(s)) {
const data = await store.getFile(s);
const mod = await store.getModified(s);
const attrs = await store.getAttrs(s);
if (data === null)
throw enoent(src);
await store.setFile(d, data);
await store.setModified(d, mod ?? now);
if (Object.keys(attrs).length > 0)
await store.setAttrs(d, attrs);
await store.delFile(s);
await store.delModified(s);
await store.delAttrs(s);
// A file has nothing beneath it: both ends take the unlink flavor.
return;
}
if (await store.hasDir(s)) {
const mod = await store.getModified(s);
const attrs = await store.getAttrs(s);
await store.addDir(d);
await store.setModified(d, mod ?? now);
if (Object.keys(attrs).length > 0)
await store.setAttrs(d, attrs);
const removed = await moveSubtree(store, s, d);
await store.removeDir(s);
// A key written under the source while the subtree moved stayed, and the
// directories that hold it keep their metadata.
const kept = await keepLeft(store, s);
for (const key of [...removed, s]) {
if (kept.has(key))
continue;
await store.delModified(key);
await store.delAttrs(key);
}
if (kept.size > 0)
throw enotempty(src);
return;
}
throw await lookupError(store, src, s);
}
//# sourceMappingURL=rename.js.map