UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

270 lines • 10 kB
// ========= 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 { readVersioned, writeCondition } from "../../cache/context.js"; import { OwnRead } from "../../cache/types.js"; import { eexist, einval, eisdir, enotsup, isEnotdir, isMissingPath } from "../../errors/fs.js"; import { FileType } from "../../types.js"; import { CHUNK_SIZE } from "../../io/cooperative.js"; import { ByteSpool } from "../../io/spool.js"; /** * Append by reading the file and writing it back whole. * * Not atomic against a concurrent writer. A zero-byte append is an open for * appending with nothing written after it (`cmd >> f` opens `f` before `cmd` * runs): it creates a missing file and leaves an existing one alone, so it * costs a stat rather than moving the whole object twice to add nothing, and * cannot put back bytes a concurrent writer had just replaced. The read takes * the caller's index, as every other read does: an id-addressed backend (Box, * Drive) turns a path into an id through it. Mirrors Python's * `append_by_rewrite`. */ export async function appendByRewrite(read, write, stat, path, data) { if (data.length === 0) { let found; try { found = await stat(path); } catch (error) { if (!isMissingPath(error)) throw error; return await write({ path, body: data, condition: await writeCondition(path, 'put', OwnRead.ABSENT), }); } if (found.type === FileType.DIRECTORY) throw eisdir(path); return; } let existing; let own; try { ; [existing, own] = await readVersioned(path, () => read(path)); own = own ?? OwnRead.UNKNOWN; } catch (error) { if (!isMissingPath(error)) throw error; return await write({ path, body: data, condition: await writeCondition(path, 'put', OwnRead.ABSENT), }); } const joined = new Uint8Array(existing.length + data.length); joined.set(existing); joined.set(data, existing.length); return await write({ path, body: joined, condition: await writeCondition(path, 'put', own) }); } /** * Stage a read-modify-write with bounded RAM and the read's version. The new * bytes are spooled before the file is read: a streamed write lets its path * go while its source is pulled, so a write landing meanwhile is in the file * the replacement is made from. */ export async function appendByStreamedRewrite(read, write, stat, path, data) { const incoming = new ByteSpool(); const spool = new ByteSpool(); try { await incoming.fill(data); if (incoming.size === 0) { let found; try { found = await stat(path); } catch (error) { if (!isMissingPath(error)) throw error; return await write({ path, body: new Uint8Array(), condition: await writeCondition(path, 'put', OwnRead.ABSENT), }); } if (found.type === FileType.DIRECTORY) throw eisdir(path); return; } let own = OwnRead.UNKNOWN; try { await spool.fill(read(path, (version) => { own = version ?? OwnRead.UNKNOWN; })); } catch (error) { if (!isMissingPath(error) || spool.size > 0) throw error; own = OwnRead.ABSENT; } return await write({ path, body: spliced(spool, spool.size, incoming), condition: await writeCondition(path, 'put', own), }); } finally { await incoming.close(); await spool.close(); } } /** * Write at an offset by spooling the file and writing it back whole, its * head, the new bytes and its tail streamed, so memory holds a chunk at a * time whatever the sizes. The new bytes are spooled before the file is * read: a streamed write lets its path go while its source is pulled, so a * write landing meanwhile is in the file the replacement is made from. * * A zero-length pwrite(2) on an existing file changes nothing and must not * read the file back: a concurrent writer's update between the stat and a * write would be clobbered by the stale contents. On a missing file it * creates an empty one. A key store answers a read of a directory's name as * a missing key, so a read that misses is checked against stat before * writing: writing there would put an object beside the directory. `read` * fills a spool with the whole file and answers the version the read * carried. Mirrors Python's `pwrite_by_streamed_rewrite`. */ export async function pwriteByStreamedRewrite(read, write, stat, path, data, offset) { const incoming = new ByteSpool(); const spool = new ByteSpool(); try { await incoming.fill(data); if (incoming.size === 0) { let found; try { found = await stat(path); } catch (error) { if (!isMissingPath(error)) throw error; await write({ path, body: new Uint8Array(), condition: await writeCondition(path, 'put', OwnRead.ABSENT), }); return; } if (found.type === FileType.DIRECTORY) throw eisdir(path); return; } let own; try { own = (await read(path, spool)) ?? OwnRead.UNKNOWN; } catch (error) { if (!isMissingPath(error) || spool.size > 0) throw error; let missing = null; try { missing = await stat(path); } catch (statError) { if (!isMissingPath(statError)) throw statError; } if (missing !== null && missing.type === FileType.DIRECTORY) throw eisdir(path); own = OwnRead.ABSENT; } await write({ path, body: spliced(spool, offset, incoming), condition: await writeCondition(path, 'put', own), }); } finally { await incoming.close(); await spool.close(); } } /** * The spooled file with new bytes laid over it from `offset`: its head, * zeros where it ends before `offset`, the new bytes, then what it holds past * them. Mirrors Python's `_spliced`. */ async function* spliced(spool, offset, incoming) { yield* spool.stream(0, offset); for (let at = spool.size; at < offset; at += CHUNK_SIZE) yield new Uint8Array(Math.min(CHUNK_SIZE, offset - at)); yield* incoming.stream(); yield* spool.stream(offset + incoming.size); } /** * Resize by reading the file and writing it back padded or cut. * * For a store with no partial write. It cannot hold `noCreate` atomically, * so it refuses that before writing anything, as it does a negative length * (EINVAL). Emptying reads nothing, so its * write carries the version the agent read. Mirrors Python's * `truncate_by_rewrite`. */ export async function truncateByRewrite(read, write, path, length, noCreate) { if (noCreate) throw enotsup('emulated', 'truncate --no-create', path); expectOffset(length, path); if (length === 0) { await write({ path, body: new Uint8Array(0), condition: await writeCondition(path, 'put') }); return; } let data; let own; try { ; [data, own] = await readVersioned(path, () => read(path)); own = own ?? OwnRead.UNKNOWN; } catch (error) { if (!isMissingPath(error)) throw error; data = new Uint8Array(0); own = OwnRead.ABSENT; } const out = new Uint8Array(length); out.set(data.subarray(0, length)); await write({ path, body: out, condition: await writeCondition(path, 'put', own) }); } /** A pwrite offset, refused with EINVAL when negative. Mirrors Python's `expect_offset`. */ export function expectOffset(offset, path) { if (!Number.isInteger(offset) || offset < 0) throw einval(path); return offset; } /** * Refuse a mkdir of a name that is taken, as mkdir(2) does. * * mkdir(2) refuses a name that exists, file or directory, and `mkdir -p` * passes only a directory. Not every backend's create says so (a Graph 409 * on a folder, Nextcloud's MKCOL 405, SFTP under `-p`), so both callers look * the name up before the create. A directory under `-p` still reaches the * create, which keeps it durable (an object store writes the marker of a * directory only a key implied), and a name that cannot be looked up is left * to it too, to answer ENOENT or ENOTDIR. Mirrors Python's `refuse_taken`. */ export async function refuseTaken(stat, path, parents) { let row; try { row = await stat(path); } catch (error) { if (isMissingPath(error) || isEnotdir(error)) return; throw error; } if (parents && row.type === FileType.DIRECTORY) return; throw eexist(path); } //# sourceMappingURL=rewrite.js.map