UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

202 lines • 9.08 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 { DirListing, FileStat, FileType } from "../../types.js"; import { contentTypeForPath } from "../../utils/filetype.js"; import { eacces, isEacces, listingError } from "../../errors/fs.js"; import * as kp from "../../utils/key_prefix.js"; import { rawPathOf } from "../../utils/key_prefix.js"; import { rstripSlash } from "../../utils/slash.js"; import { compareCodePoints } from "../../utils/sort.js"; import { provesDirectory } from "./stat.js"; async function probeFile(driver, conn, kpfx, named, key) { try { return (await driver.head(conn, kp.apply(kpfx, key))) !== null; } catch (err) { if (isEacces(err)) throw eacces(named); throw err; } } async function probeDir(driver, conn, kpfx, named, key) { return provesDirectory(driver, conn, kp.applyDir(kpfx, key), named); } /** Build a prefix listing over one driver. Mirrors Python's `make_readdir`. */ export function makeReaddir(driver) { return async function readdir(accessor, path, limit) { // When called from resolveGlob with a pattern (e.g. *.txt), use // path.directory for the listing. Direct callers (ls, ops) pass // pattern=null so path.virtual is used. The store lists in key order, so // a walk's limit stops it at the first names, once it has listed past // where a kept file could still prove a directory (`b` then `b/`) or a // later revision. const rawPath = rawPathOf(path.pattern !== null ? path.dir : path); const kpfx = driver.keyPrefixOf(accessor); const pfx = kp.applyDir(kpfx, rawPath); const names = new Set(); const dirKeys = new Set(); const sizes = new Map(); const times = new Map(); let sawKey = false; let cut = false; let past = null; const { conn, close } = await driver.connect(accessor); try { for await (const child of driver.listChildren(conn, pfx)) { sawKey = true; if (child.kind === 'marker') continue; const key = '/' + kp.strip(kpfx, child.key); if (past !== null) { if (compareCodePoints(child.kind === 'd' ? key + '/' : key, past) > 0) { cut = true; break; } if (!names.has(key)) { cut = true; continue; } } names.add(key); if (child.kind === 'd') { dirKeys.add(key); } else { sizes.set(key, child.size ?? null); times.set(key, child.modified ?? ''); } if (past === null && limit !== undefined && names.size >= limit) { past = ''; for (const k of names) { if (!dirKeys.has(k) && compareCodePoints(k + '/', past) > 0) past = k + '/'; } } } if (!sawKey && rstripSlash(rawPath) !== '') { // An empty directory is a zero-byte marker object keyed at the // prefix itself, so a prefix holding no key at all -- not even that // marker -- is a path the store does not have. Without this, `ls` // on a missing path rendered an empty directory and exited 0 where // every real filesystem reports ENOENT. The mount root is exempt: // it exists because it is mounted. throw await listingError(path, rawPath, (p) => probeFile(driver, conn, kpfx, path, p), (p) => probeDir(driver, conn, kpfx, path, p)); } } finally { await close(); } const unique = [...names].sort(compareCodePoints); if (unique.length > driver.scopeError) { console.warn(`${driver.vfs} readdir: ${path.virtual} returned ` + `${String(unique.length)} entries (limit ${String(driver.scopeError)})`); } const rows = unique.map((key) => { const name = key.slice(key.lastIndexOf('/') + 1); // Store "folders" are synthetic prefixes with no object of their own, // so there is no mtime or size to record. A name that is a key and a // prefix both lists as the directory, as a walk enters it. if (dirKeys.has(key)) return new FileStat({ name, type: FileType.DIRECTORY }); const modified = times.get(key) ?? ''; return new FileStat({ name, size: sizes.get(key) ?? null, modified: modified !== '' ? modified : null, type: FileType.FILE, content: contentTypeForPath(name), }); }); return cut ? new DirListing({ entries: rows, partial: true }) : rows; }; } /** * Every key at and under a path, in one recursive listing: the key at the path * itself and every key under it, as `listSubtree` gives them, markers * included; null once the listing passes `limit` keys, before it pages * further. Mirrors Python's `read_tree`. */ export async function readTree(driver, accessor, path, limit) { const stem = rstripSlash(kp.apply(driver.keyPrefixOf(accessor), path.mountPath)); const { conn, close } = await driver.connect(accessor); const entries = []; try { for await (const entry of driver.listSubtree(conn, stem)) { entries.push(entry); if (limit !== undefined && entries.length > limit) return null; } } finally { await close(); } return entries; } /** * Build `readTree` over one driver: one recursive listing, and the directory * itself under `''`. A file stats as a listing gives it (its size, time and * content type); a directory, a marker or a key prefix alike, carries nothing but its name. A * marker for `path` itself names no entry. Null, so the walk's readdir and * stat answer instead, when nothing is under the prefix, when a row has no * size, when a key has an empty segment, and when one name is both a file and * a prefix, which a readdir lists as a directory; and null for a subtree of * more than `limit` keys. Mirrors Python's `make_tree`. */ export function makeTree(driver) { return async (accessor, path, limit) => { const rows = await readTree(driver, accessor, path, limit); if (rows === null) return null; const prefix = kp.applyDir(driver.keyPrefixOf(accessor), path.mountPath); if (!rows.some((row) => row.key.startsWith(prefix))) return null; const found = new Map(); const dirs = new Set(); for (const row of rows) { if (!row.key.startsWith(prefix)) continue; const raw = row.key.slice(prefix.length); const isDir = raw.endsWith('/'); const rel = rstripSlash(raw); if (rel === '') continue; const parts = rel.split('/'); if (parts.includes('') || (!isDir && (row.size ?? null) === null)) return null; for (let depth = 1; depth < parts.length; depth++) dirs.add(parts.slice(0, depth).join('/')); if (isDir) { dirs.add(rel); continue; } const name = parts[parts.length - 1] ?? rel; found.set(rel, new FileStat({ name, size: row.size ?? null, modified: row.modified !== undefined && row.modified !== '' ? row.modified : null, type: FileType.FILE, content: contentTypeForPath(name), })); } for (const rel of dirs) { if (found.has(rel)) return null; found.set(rel, new FileStat({ name: rel.slice(rel.lastIndexOf('/') + 1), type: FileType.DIRECTORY })); } const own = rstripSlash(path.mountPath); found.set('', new FileStat({ name: own.slice(own.lastIndexOf('/') + 1) || '/', type: FileType.DIRECTORY })); return found; }; } //# sourceMappingURL=readdir.js.map