UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

793 lines • 32.3 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 { eexist, enoent, enotdir, enotsup, listingError } from "../../errors/fs.js"; import { chunks } from "../../io/cooperative.js"; import { ByteSpool } from "../../io/spool.js"; import { record, recordingActive, recordStream, revisionFor, startOp, } from "../../observe/context.js"; import { DirListing, FileStat, FileType } from "../../types.js"; import { sleep } from "../../utils/abort.js"; import { contentTypeForPath } from "../../utils/filetype.js"; import { mountPrefixOf } from "../../utils/key_prefix.js"; import { windowFor } from "../../utils/ranges.js"; import { rstripSlash, stripSlash } from "../../utils/slash.js"; import { compareCodePoints } from "../../utils/sort.js"; import { cancelUpload, graphBatch, graphDelete, GraphError, graphGet, graphGetBytes, graphList, graphPatch, graphPost, graphPostMonitor, graphPutBytes, graphStream, pollMonitor, uploadChunk, } from "./client.js"; import { BATCH_LIMIT } from "./constants.js"; import { checkItem } from "./paths.js"; const SIMPLE_UPLOAD_MAX = 4 * 1024 * 1024; const UPLOAD_CHUNK = 10 * 327680; export class DriveLoc { drive; path; virtual; url; ref; constructor(init) { this.drive = init.drive; this.path = init.path; this.virtual = init.virtual; this.url = init.url; this.ref = init.ref; } item(action = '') { return this.url(this.path, action); } // Any item on the same drive, addressed the way this loc addresses its // own. The ancestor probes need it: a DriveLoc's `path` is already // spelled the way its url callable expects, so an ancestor of it is too. itemAt(path, action = '') { return this.url(path, action); } child(name) { return new DriveLoc({ drive: this.drive, path: this.path !== '' ? `${this.path}/${name}` : name, virtual: this.virtual !== '' ? `${this.virtual}/${name}` : name, url: this.url, ref: this.ref, }); } parent() { return parentPath(this.path); } reference(folder = '') { return this.ref(folder); } } export function parentPath(path) { const index = path.lastIndexOf('/'); return index < 0 ? '' : path.slice(0, index); } export function baseName(path) { const stripped = rstripSlash(path); const index = stripped.lastIndexOf('/'); return index < 0 ? stripped : stripped.slice(index + 1); } // The directory a listing names: a glob operand lists the folder it sits in. export function directoryPath(path) { return path.pattern !== null ? path.dir : path; } // The mount-absolute key the index files a listing or an entry under. export function virtualKey(path) { const target = directoryPath(path); const prefix = mountPrefixOf(target.virtual, target.vfsPath); return target.vfsPath !== '' ? `${prefix}/${target.vfsPath}` : prefix !== '' ? prefix : '/'; } function asString(value) { return typeof value === 'string' ? value : null; } export function asNumber(value) { return typeof value === 'number' && Number.isFinite(value) ? value : null; } function isFolder(item) { return item.folder !== undefined; } // Graph returns the folder facet's childCount by default, so `-empty` needs // no extra request. Absent (a $select that dropped it) reads as unknown. export function folderChildCount(item) { const facet = item.folder; if (facet === null || typeof facet !== 'object' || Array.isArray(facet)) return null; return asNumber(facet.childCount); } function parentReference(src, dst) { const reference = { path: dst.reference(dst.parent()) }; if (src.drive !== dst.drive && dst.drive !== '') reference.driveId = dst.drive; return reference; } async function copyOnce(config, src, dst, srcVirtual, dstVirtual) { await checkItem(config, src, srcVirtual); await checkItem(config, dst, dstVirtual); try { const monitor = await graphPostMonitor(config, src.item('/copy'), { name: baseName(dst.path), parentReference: parentReference(src, dst), }); const result = await pollMonitor(monitor, config.timeout); if (result.status === 'completed') return null; const error = result.error !== null && typeof result.error === 'object' && !Array.isArray(result.error) ? result.error : {}; if (result.status === 'failed') { return new GraphError(500, asString(error.code) ?? 'copyFailed', asString(error.message) ?? `copy ${src.path} -> ${dst.path} failed`); } throw new GraphError(504, 'copyTimeout', `copy ${src.path} -> ${dst.path} not confirmed`); } catch (error) { if (error instanceof GraphError && (error.status === 409 || error.code === 'nameAlreadyExists')) { return error; } throw error; } } export async function copyTree(config, src, dst, srcVirtual, dstVirtual) { const conflict = await copyOnce(config, src, dst, srcVirtual, dstVirtual); if (conflict === null) return; // Status, not just code: `copyOnce` reports a monitor-reported failure // as 500 and a thrown conflict as 409, so re-raising `conflict` as it // arrived made the same refusal carry a different status depending on // which of the two paths produced it. Python's `copy_tree` states the // status outright (500 for a non-conflict code, 409 for the mixed // file/folder refusal), and is the correct side. if (conflict.code !== 'nameAlreadyExists') { throw new GraphError(500, conflict.code, conflict.message); } const srcItem = await graphGet(config, src.item()); const dstItem = await graphGet(config, dst.item()); if (isFolder(srcItem) && isFolder(dstItem)) { // GNU cp -r merges into an existing directory; Graph never merges // folders, so recurse per child instead. for (const child of await graphList(config, src.item('/children'))) { const name = asString(child.name) ?? ''; await copyTree(config, src.child(name), dst.child(name), srcVirtual === undefined ? undefined : `${rstripSlash(srcVirtual)}/${name}`, dstVirtual === undefined ? undefined : `${rstripSlash(dstVirtual)}/${name}`); } return; } if (isFolder(srcItem) || isFolder(dstItem)) { throw new GraphError(409, conflict.code, conflict.message); } await graphDelete(config, dst.item()); const secondConflict = await copyOnce(config, src, dst, srcVirtual, dstVirtual); if (secondConflict !== null) { throw new GraphError(500, secondConflict.code, secondConflict.message); } } function moveBody(src, dst) { const body = { name: baseName(dst.path) }; if (src.parent() !== dst.parent() || src.drive !== dst.drive) { body.parentReference = { path: dst.reference(dst.parent()) }; } return body; } /** * Move `src` to `dst`, replacing a file or an empty folder there. Answers * the moved driveItem and whether the move replaced something at `dst` * that was not positively a file. */ export async function renameReplace(config, src, dst, srcVirtual, dstVirtual) { await checkItem(config, src, srcVirtual); await checkItem(config, dst, dstVirtual); const body = moveBody(src, dst); try { return { moved: await graphPatch(config, src.item(), body), replacedNonFile: false }; } catch (error) { if (!(error instanceof GraphError) || (error.status !== 409 && error.code !== 'nameAlreadyExists')) { throw error; } const destination = await graphGet(config, dst.item()); if (isFolder(destination)) { const children = await graphList(config, dst.item('/children')); if (children.length > 0) throw error; } await graphDelete(config, dst.item()); const replacedNonFile = !('file' in destination); return { moved: await graphPatch(config, src.item(), body), replacedNonFile }; } } async function urlItem(config, url) { try { return await graphGet(config, url); } catch (error) { if (error instanceof GraphError && error.status === 404) return null; throw error; } } /** * Create one folder, naming a refusal the way mkdir(2) does. * * "replace" is unreliable for folders on real Graph, so the create uses * "fail" and reads its 409: a folder already holding the name is success when * `existOk` (a level `mkdir -p` passes through) and EEXIST otherwise, so a * folder another client made after the lookup ran is still refused; a file * holding it is EEXIST. Graph answers a create under a missing parent and * under a file alike with 404, so the parent is looked up to tell ENOENT from * ENOTDIR. Both lookups run on a refusal only. Mirrors Python's * `create_child_folder`. */ export async function createChildFolder(config, parentUrl, name, target, existOk = true) { try { await graphPost(config, parentUrl, { name, folder: {}, '@microsoft.graph.conflictBehavior': 'fail', }); } catch (error) { if (!(error instanceof GraphError)) throw error; if (error.status === 409 || error.code === 'nameAlreadyExists') { const taken = await urlItem(config, target.item); if (existOk && (taken === null || 'folder' in taken)) return; throw eexist(target.virtual); } if (error.status !== 404) throw error; const found = await urlItem(config, target.parent); if (found !== null && !('folder' in found)) throw enotdir(target.virtual); throw enoent(target.virtual); } } async function uploadSessionWrite(config, sessionUrl, data) { const session = await graphPost(config, sessionUrl, { item: { '@microsoft.graph.conflictBehavior': 'replace' }, }); const uploadUrl = asString(session.uploadUrl); if (uploadUrl === null) throw new GraphError(502, 'missingUploadUrl', sessionUrl); const total = data instanceof ByteSpool ? data.size : data.length; let start = 0; try { while (start < total) { const chunk = data instanceof ByteSpool ? await data.read(start, UPLOAD_CHUNK) : data.slice(start, start + UPLOAD_CHUNK); const result = await uploadChunk(config, uploadUrl, chunk, start, total); const ranges = result.nextExpectedRanges; const next = Array.isArray(ranges) && typeof ranges[0] === 'string' ? Number(ranges[0].split('-')[0]) : start + chunk.length; if (!Number.isSafeInteger(next) || next <= start || next > total) throw new Error('Graph upload session made no progress'); start = next; } } catch (error) { try { await cancelUpload(config, uploadUrl); } catch (cleanup) { console.debug('Could not cancel Graph upload session', cleanup); } throw error; } } /** * Replace a drive item's content, creating it and its parents, and answer how * many bytes went. A small body is one `PUT`; anything past the simple-upload * limit goes through an upload session. A stream is spooled first, so its size * picks between the two; `signal` stops it. */ export async function writeItem(config, loc, data, signal, virtual) { await checkItem(config, loc, virtual); let body; if (data instanceof Uint8Array) body = data; else { const spool = new ByteSpool(); try { await spool.fill(chunks(data, signal)); if (spool.size > SIMPLE_UPLOAD_MAX) { await uploadSessionWrite(config, loc.item('/createUploadSession'), spool); return spool.size; } body = await spool.read(0, spool.size); } finally { await spool.close(); } } if (body.length <= SIMPLE_UPLOAD_MAX) { await graphPutBytes(config, loc.item('/content'), body); } else { await uploadSessionWrite(config, loc.item('/createUploadSession'), body); } return body.length; } function entryStat(item) { const name = asString(item.name) ?? ''; if (isFolder(item)) { // Graph's folder `size` is aggregate storage metadata, not the byte // length of any rendered content: keep it out of FileStat.size (see // CLAUDE.md FUSE rules) and expose it as `extra.size_bytes`. return new FileStat({ name, type: FileType.DIRECTORY, modified: asString(item.lastModifiedDateTime), extra: { size_bytes: asNumber(item.size), child_count: folderChildCount(item) }, }); } return new FileStat({ name, type: FileType.FILE, content: contentTypeForPath(name), size: asNumber(item.size), modified: asString(item.lastModifiedDateTime), fingerprint: asString(item.cTag), extra: { id: item.id, ctag: item.cTag, etag: item.eTag }, }); } function currentVersionId(versions) { let current = null; for (const version of versions) { if (current === null || (asString(version.lastModifiedDateTime) ?? '') > (asString(current.lastModifiedDateTime) ?? '')) { current = version; } } return current === null ? null : asString(current.id); } // The item's cTag, current version and download URL, in one GET. Callers // fetch this before the bytes and download from the URL it returns. Graph can // change the item between the two requests, and in this order a change can // only pair an older token with newer bytes, which the next freshness probe // sees as stale; the other order would label old bytes with the new token and // serve them as fresh. `versions` also expands the version history for the // current revision, which only a snapshot needs; without it the revision is // null. Every shell line records, so only reads outside one (FUSE, a runtime's // guest, `ws.vfs`) skip it. async function captureItemMetadata(config, loc, virtual, versions) { const params = versions ? { $expand: 'versions' } : undefined; const item = (await checkItem(config, loc, virtual, params)) ?? (await graphGet(config, loc.item(), params)); const history = Array.isArray(item.versions) ? item.versions.filter((value) => value !== null && typeof value === 'object' && !Array.isArray(value)) : []; return [ asString(item.cTag), currentVersionId(history), asString(item['@microsoft.graph.downloadUrl']), ]; } export async function readItem(config, loc, virtual, backend, offset = 0, size = null) { const pinned = revisionFor(virtual); const window = windowFor(offset, size); const timer = startOp(); let fingerprint = null; let revision = pinned; try { let data; if (pinned !== null) { await checkItem(config, loc, virtual); data = await graphGetBytes(config, loc.item(`/versions/${encodeURIComponent(pinned)}/content`), window); } else { let downloadUrl; [fingerprint, revision, downloadUrl] = await captureItemMetadata(config, loc, virtual, recordingActive()); data = downloadUrl === null ? await graphGetBytes(config, loc.item('/content'), window) : await graphGetBytes(config, downloadUrl, window, false); } record('read', virtual, backend, data.length, timer, { fingerprint, revision }); return data; } catch (error) { if (error instanceof GraphError && error.status === 404) throw enoent(virtual); throw error; } } export async function* streamItem(config, loc, virtual, backend) { const pinned = revisionFor(virtual); const rec = recordStream('read', virtual, backend); let url = loc.item('/content'); let auth = true; try { if (pinned !== null) { await checkItem(config, loc, virtual); url = loc.item(`/versions/${encodeURIComponent(pinned)}/content`); if (rec !== null) rec.revision = pinned; } else if (rec !== null) { let downloadUrl; [rec.fingerprint, rec.revision, downloadUrl] = await captureItemMetadata(config, loc, virtual, true); if (downloadUrl !== null) { url = downloadUrl; auth = false; } } if (pinned === null && rec === null) await checkItem(config, loc, virtual); for await (const chunk of graphStream(config, url, auth)) { if (rec !== null) rec.bytes += chunk.length; yield chunk; } } catch (error) { if (error instanceof GraphError && error.status === 404) throw enoent(virtual); throw error; } if (rec !== null) rec.complete = true; } export async function* iterTree(config, loc, virtual) { await checkItem(config, loc, virtual); yield* walkTree(config, loc); } /** Walk a checked scope using canonical names returned by Graph. */ async function* walkTree(config, loc) { for (const child of await graphList(config, loc.item('/children'))) { const childLoc = loc.child(asString(child.name) ?? ''); const folder = isFolder(child); yield [childLoc.virtual, child, folder]; if (folder) yield* walkTree(config, childLoc); } } /** * The id of the folder a driveItem sits in, '' when it names none: a listing's * item or a `/delta` feed's. Mirrors Python's `parent_id`. */ export function parentId(item) { const ref = item.parentReference; if (ref === null || typeof ref !== 'object') return ''; const id = ref.id; return typeof id === 'string' ? id : ''; } /** * Every item under a folder, each as a listing's stat renders it, from one * read of Graph's `/delta` feed started at that folder. * * One request asks the folder for its id, then the feed lists every item below * it a page at a time, where a walk sends a listing per folder. The feed names * an item by id with its name and its parent's id, never its path, and may * list one more than once (its last row is its state), so each is placed by * its parents up to the folder. An item under one a listing shows as a file (a * package's contents) is left out, as a listing of that file would leave it. * Null when no folder is at `loc`, so the walk's readdir reports what is * there, when Graph refuses the feed, when the feed does not list the folder * the first request found (another folder took its path in between, and the * feed describes that one), when the feed holds a folder shared from another * drive, whose items only that drive's feed lists, and when a file * comes without the `cTag` its listing row carries (OneDrive for Business and * SharePoint leave it out of the feed), since the mount keeps the answer as * the folders' listings. Mirrors Python's `delta_tree`. */ export async function deltaTree(config, loc, virtual) { let top; try { top = (await checkItem(config, loc, virtual)) ?? (await graphGet(config, loc.item(), { $select: 'id,folder' })); } catch (err) { if (!(err instanceof GraphError) || err.status !== 404) throw err; console.debug(`graph tree of ${loc.path} left to the walk: ${err.message}`); return null; } const topId = top.id; if (!isFolder(top) || typeof topId !== 'string') return null; let rows; try { rows = await graphList(config, loc.item('/delta')); } catch (err) { if (!(err instanceof GraphError)) throw err; console.debug(`graph feed of ${loc.path} left to the walk: ${err.message}`); return null; } const items = new Map(); for (const item of rows) { if (typeof item.id !== 'string') continue; items.delete(item.id); items.set(item.id, item); } const folders = new Map(); for (const [id, item] of items) { if (isFolder(item) && item.deleted === undefined) folders.set(id, item); } if (!folders.has(topId)) return null; for (const item of folders.values()) if (remoteFolder(item)) return null; for (const item of items.values()) { const live = item.file !== undefined && item.deleted === undefined; if (live && (typeof item.cTag !== 'string' || item.cTag === '')) return null; } const places = new Map([[topId, '']]); const found = new Map(); for (const [id, item] of items) { const name = asString(item.name); if (id === topId || item.deleted !== undefined || item.root !== undefined) continue; if (name === null || name === '') continue; const base = placed(parentId(item), folders, places); if (base !== null) found.set(base === '' ? name : `${base}/${name}`, entryStat(item)); } return found; } /** * Every item under a folder, each as its folder's listing renders it, from the * folders' listings sent `BATCH_LIMIT` to a request through Graph's JSON * batching, where a walk sends a request per folder. * * The folder's own children are its listing (`readdirItems`), so a folder that * is not there raises what a listing raises. A listing the batch does not * answer with its first page (Graph throttled or failed that one) is asked on * its own, once the longest wait the batch's answers asked for has passed since * it arrived (one wait a batch, each answer's counted from the same moment), * and a listing's later pages are read as a listing reads them. Null when * Graph refuses a batch, or when a * listing below the folder fails (a folder gone, or one the account may not * open), so the walk lists what is there and reports, or skips, each folder on * its own; null too once it holds more than `limit` items, which no listing * is paged past. Mirrors Python's `batch_tree`. */ export async function batchTree(config, loc, path, limit) { const found = new Map(); const top = await readdirItems(config, loc, path, limit); if (top instanceof DirListing) return null; let level = filed(found, '', loc, top); while (level.length > 0) { if (limit !== undefined && found.size > limit) return null; const left = limit === undefined ? undefined : limit - found.size + 1; let got = 0; const below = []; for (let start = 0; start < level.length; start += BATCH_LIMIT) { const chunk = level.slice(start, start + BATCH_LIMIT); const urls = chunk.map(([, folder]) => folder.item('/children')); const listed = []; try { const answers = await graphBatch(config, urls); let pause = 0; for (const [status, , wait] of answers) if (status !== 200) pause = Math.max(pause, wait); if (pause > 0) await sleep(pause * 1000); for (const [i, url] of urls.entries()) { if (left !== undefined && got >= left) return null; const [status, body] = answers[i] ?? [0, {}, 0]; const items = await batchPage(config, url, status, body, left === undefined ? undefined : left - got); got += items.length; listed.push(items); } } catch (err) { const failed = err instanceof GraphError || err instanceof TypeError || err instanceof DOMException; if (!failed) throw err; console.debug(`graph tree of ${loc.path} left to the walk: ${String(err)}`); return null; } for (const [i, [rel, folder]] of chunk.entries()) { const rows = (listed[i] ?? []) .map((item) => entryStat(item)) .sort((a, b) => compareCodePoints(a.name, b.name)); for (const pair of filed(found, rel, folder, rows)) below.push(pair); } } if (left !== undefined && got >= left) return null; level = below; } if (limit !== undefined && found.size > limit) return null; return found; } /** * A folder's whole listing from its answer in a batch: the page the batch held * and the pages after it, or the listing asked again on its own when the batch * did not answer it; no page past the one that brings `limit` items. Mirrors * Python's `_batch_page`. */ async function batchPage(config, url, status, body, limit) { if (status !== 200) return graphList(config, url, undefined, limit); const items = []; if (Array.isArray(body.value)) { for (const item of body.value) { if (item !== null && typeof item === 'object' && !Array.isArray(item)) { items.push(item); } } } const rest = body['@odata.nextLink']; if (typeof rest === 'string' && (limit === undefined || items.length < limit)) { const more = limit === undefined ? undefined : limit - items.length; for (const item of await graphList(config, rest, undefined, more)) items.push(item); } return items; } /** * Put a folder's rows into a tree read, and answer the folders among them, each * by its path below the read with its own address. Mirrors Python's `_filed`. */ function filed(found, rel, folder, rows) { const folders = []; for (const row of rows) { const key = rel !== '' ? `${rel}/${row.name}` : row.name; found.set(key, row); if (row.type === FileType.DIRECTORY) folders.push([key, folder.child(row.name)]); } return folders; } /** Whether a feed item is a folder kept on another drive. Mirrors Python's `_remote_folder`. */ function remoteFolder(item) { const remote = item.remoteItem; return remote !== null && typeof remote === 'object' && 'folder' in remote; } /** * A folder's path below the top of a `/delta` read, null when its parents * never reach it; each folder looked at is kept in `places`. Mirrors Python's * `_placed`. */ function placed(folder, folders, places) { const chain = []; const seen = new Set(); let current = folder; while (!places.has(current)) { const item = folders.get(current); if (item === undefined || seen.has(current)) { places.set(current, null); break; } chain.push(current); seen.add(current); current = parentId(item); } let base = places.get(current) ?? null; for (const link of chain.reverse()) { if (base !== null) { const name = asString(folders.get(link)?.name) ?? ''; base = base !== '' && name !== '' ? `${base}/${name}` : name !== '' ? name : null; } places.set(link, base); } return places.get(folder) ?? null; } // One bounded page, not graphList: the answer is a yes/no, and graphList // follows every @odata.nextLink, so asking it made a large folder download // its whole listing to produce one boolean. $top through that helper is // worse rather than better -- it only shrinks each page, so the walk pages // more times, not fewer -- which is why this sends the request itself. // $select drops a driveItem payload this never reads. export async function driveRootEmpty(config, loc, virtual) { await checkItem(config, loc, virtual); const page = await graphGet(config, loc.item('/children'), { $top: 1, $select: 'id' }); return !(Array.isArray(page.value) && page.value.length > 0); } async function itemOrNull(config, loc, path) { try { return await graphGet(config, loc.itemAt(stripSlash(path))); } catch (error) { if (error instanceof GraphError && error.status === 404) return null; throw error; } } async function isFile(config, loc, path) { const item = await itemOrNull(config, loc, path); return item !== null && !('folder' in item); } async function isDir(config, loc, path) { const item = await itemOrNull(config, loc, path); return item !== null && 'folder' in item; } /** * A folder's children, each as `stat` answers it; past a walk's `limit` Graph * is paged no further, and the listing is partial. Mirrors Python's * `readdir_items`. */ export async function readdirItems(config, loc, path, limit) { await checkItem(config, loc, path.virtual); let children; try { children = await graphList(config, loc.item('/children'), undefined, limit); } catch (error) { if (!(error instanceof GraphError) || error.status !== 404) throw error; // Graph 404s a children listing for a missing item and for one under a // file alike, so the errno comes from walking the ancestors: one item // request per component, on this failure path only. throw await listingError(path, loc.path, (p) => isFile(config, loc, p), (p) => isDir(config, loc, p)); } const rows = children .map((child) => entryStat(child)) .sort((a, b) => compareCodePoints(a.name, b.name)); if (limit !== undefined && children.length >= limit) return new DirListing({ entries: rows, partial: true }); return rows; } /** One drive item's stat, from Graph. Mirrors Python's `stat_item`. */ export async function statItem(config, loc, path) { try { return entryStat((await checkItem(config, loc, path.virtual)) ?? (await graphGet(config, loc.item()))); } catch (error) { if (error instanceof GraphError && error.status === 404) throw enoent(path); throw error; } } // Graph has no cheap HEAD for an item, so existence is a stat that tolerates // ENOENT. Both drive backends address items differently but probe // identically, so only their stat is injected. export function makeExists(stat) { return async (accessor, path) => { try { await stat(accessor, path); return true; } catch (error) { if (error.code === 'ENOENT') return false; throw error; } }; } // Graph exposes no truncate, so the whole item is rewritten. A missing item // truncates to a fresh one, matching `open(path, "w")`. export function makeTruncate(read, write) { return async (accessor, path, length, noCreate = false) => { if (noCreate) throw enotsup('msgraph', 'truncate --no-create', path); let data; try { data = await read(accessor, path); } catch (error) { if (error.code !== 'ENOENT') throw error; data = new Uint8Array(); } const resized = new Uint8Array(length); resized.set(data.slice(0, length)); await write(accessor, path, resized); }; } //# sourceMappingURL=drive.js.map