@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
110 lines • 4.59 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 { nowIso } from "../../utils/dates.js";
import { norm } from "../../utils/path.js";
import { rstripSlash } from "../../utils/slash.js";
import { fsError } from "../../errors/fs.js";
import { activePipe } from "../dev/device.js";
import { checkDestParents, checkRename, lookupError } from "./dest.js";
function moveAttrs(accessor, src, dst) {
const attrs = accessor.store.attrs.get(src);
if (attrs !== undefined) {
accessor.store.attrs.delete(src);
accessor.store.attrs.set(dst, attrs);
}
}
// Re-key every descendant of a renamed directory. A synthetic-directory
// store keeps subdirectories as entries of their own, 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.
function moveSubtree(accessor, s, d) {
const srcPrefix = `${rstripSlash(s)}/`;
const dstPrefix = `${rstripSlash(d)}/`;
const { store } = accessor;
for (const key of [...store.dirs]) {
if (!key.startsWith(srcPrefix))
continue;
store.dirs.delete(key);
store.dirs.add(dstPrefix + key.slice(srcPrefix.length));
}
for (const key of [...store.files.keys()]) {
if (!key.startsWith(srcPrefix))
continue;
const data = store.files.get(key);
if (data !== undefined) {
store.files.set(dstPrefix + key.slice(srcPrefix.length), data);
store.files.delete(key);
}
}
for (const key of [...store.modified.keys()]) {
if (!key.startsWith(srcPrefix))
continue;
const mod = store.modified.get(key);
if (mod !== undefined) {
store.modified.set(dstPrefix + key.slice(srcPrefix.length), mod);
store.modified.delete(key);
}
}
for (const key of [...store.attrs.keys()]) {
if (!key.startsWith(srcPrefix))
continue;
moveAttrs(accessor, key, dstPrefix + key.slice(srcPrefix.length));
}
}
/**
* Move one key, a directory with its subtree. A live descriptor at either
* end is the shell's to release, so moving one or over one is EPERM (GNU:
* `mv /dev/fd/63 /dev/fd/62` is "Operation not permitted"). Mirrors
* Python's rename.
*/
export async function rename(accessor, src, dst) {
const s = norm(src.mountPath);
const d = norm(dst.mountPath);
if (activePipe(accessor, s) !== null)
throw fsError(src, 'EPERM');
if (activePipe(accessor, d) !== null)
throw fsError(dst, 'EPERM');
const now = nowIso();
checkDestParents(accessor, dst, d);
// rename(2) onto its own name succeeds and changes nothing; moving the
// key onto itself would delete it, and a directory's whole subtree.
if (s === d) {
if (accessor.store.files.has(s) || accessor.store.dirs.has(s))
return;
throw lookupError(accessor, src, s);
}
checkRename(accessor, src, s, dst, d);
const srcFile = accessor.store.files.get(s);
if (srcFile !== undefined) {
accessor.store.files.set(d, srcFile);
accessor.store.files.delete(s);
accessor.store.modified.set(d, accessor.store.modified.get(s) ?? now);
accessor.store.modified.delete(s);
moveAttrs(accessor, s, d);
// A file has nothing beneath it: both ends take the unlink flavor.
return Promise.resolve();
}
if (accessor.store.dirs.has(s)) {
accessor.store.dirs.delete(s);
accessor.store.dirs.add(d);
accessor.store.modified.set(d, accessor.store.modified.get(s) ?? now);
accessor.store.modified.delete(s);
moveAttrs(accessor, s, d);
moveSubtree(accessor, s, d);
return Promise.resolve();
}
throw lookupError(accessor, src, s);
}
//# sourceMappingURL=rename.js.map