UNPKG

@anthropic-ai/sdk

Version:
527 lines • 20.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LocalFileStore = exports._internals = exports.FileStore = exports.FileStoreError = void 0; exports.openFileStore = openFileStore; exports.isPathLegal = isPathLegal; const node_1 = require("./node.js"); const bytes_1 = require("./utils/bytes.js"); const fsp = node_1.fs.promises; const C = node_1.fs.constants; // Owner-only regardless of umask: the store holds downloaded user/model content. const OWNER_ONLY_DIR_MODE = 0o700; const OWNER_ONLY_FILE_MODE = 0o600; const OWNER_ONLY_EXEC_MODE = 0o700; // 0 where the platform lacks them; `open` refuses such platforms. const O_NOFOLLOW = C.O_NOFOLLOW ?? 0; const O_NONBLOCK = C.O_NONBLOCK ?? 0; var FileStoreError = /* @__PURE__ */ (() => { /** A refused operation — input the store will not act on. OS errors propagate with their `.code`. */ class FileStoreError extends Error { constructor(reason, relPath) { super(`path ${JSON.stringify(relPath)} ${reason}`); this.name = 'FileStoreError'; this.reason = reason; this.relPath = relPath; } } FileStoreError.ESCAPES_ROOT = 'escapes the store root'; FileStoreError.IS_A_SYMLINK = 'is a symlink'; FileStoreError.NOT_A_FILE = 'is not a regular file'; FileStoreError.NOT_A_DIRECTORY = 'is not a directory'; FileStoreError.NOT_UTF8 = 'is not valid utf-8'; FileStoreError.MOVE_DESTINATION_EXISTS = 'already exists'; return FileStoreError; })(); exports.FileStoreError = FileStoreError; /** Resolve `root`; creates nothing — only {@link FileStore.createRoot} makes the folder. */ async function openFileStore(root, opts) { return FileStore.open(root, opts); } /** * True for a path usable verbatim as a store location: absolute, with no `..` * components. Paths are judged in POSIX terms — they are wire values naming * locations inside a POSIX container, not host-native paths. */ function isPathLegal(p) { return p.startsWith('/') && !p.split('/').includes('..'); } var FileStore = /* @__PURE__ */ (() => { /** * One confined folder of regular files. * * Every `relPath` is relative to the root (a leading `/` also means the root) * and refused with {@link FileStoreError} when it escapes. The store holds * regular files only: symlinks are refused on read and skipped by listings — * {@link findSymlinks} reports them. A `relPath` resolving to the root itself * is banned by this interface: `put` and `get` refuse it, `move` and `remove` * do nothing. A store opened with `utf8: true` refuses binary content the * same way — on `put` of such bytes and on `get` of such a file. Only * {@link createRoot} makes the root: writes create directories below it, * never the root itself, so a root removed while the store is open stays * removed and the write fails with `ENOENT`. */ class FileStore { /** @internal — use {@link FileStore.open} / {@link openFileStore}. */ constructor(root, removedOnDispose, utf8Only = false) { /** `hashtree`'s advisory cache; every hit re-validates against a fresh stat. */ this.hashes = new Map(); this.rootPath = root; this.removedOnDispose = removedOnDispose; this.decoder = utf8Only ? new TextDecoder('utf-8', { fatal: true }) : undefined; } /** Resolve `root`; creates nothing — only {@link createRoot} makes the folder. */ static async open(root, opts) { // A deployment condition, not refused caller input — hence not FileStoreError. if (!platformSupported()) { throw new Error('FileStore requires O_NOFOLLOW support on this platform'); } let removedOnDispose = false; try { // lstat, not a follow-and-swallow existence check: following symlinks or // swallowing permission errors would mark a real directory ours to // delete on dispose. await fsp.lstat(root); } catch (e) { if (e.code !== 'ENOENT') throw e; removedOnDispose = true; } return new FileStore(node_1.path.resolve(root), removedOnDispose, opts?.utf8 ?? false); } /** Create the root directory and any missing ancestors; already existing is fine. */ async createRoot() { await makeDirAndAncestors(this.rootPath); } /** The resolved root, and what {@link dispose} will do to it. */ root() { return { path: this.rootPath, removedOnDispose: this.removedOnDispose }; } /** * Remove the root iff `open` created it; pre-existing roots are kept. * * Wired to `Symbol.asyncDispose` at runtime when the host provides it, so * `await using` works on engines with explicit resource management. */ async dispose() { if (!this.removedOnDispose) return; await fsp.rm(this.rootPath, { recursive: true, force: true }); } /** * Write `data` (`string` UTF-8 or bytes) atomically to the file at `relPath`. * * Missing directories below the root are created; a missing root is not — * the write fails with `ENOENT`. */ async put(relPath, data, opts) { // "dir/." names a directory just like a trailing "/". const tail = relPath.replace(/\\/g, '/'); if (tail.endsWith('/') || tail.endsWith('/.') || tail === '' || tail === '.') { throw new FileStoreError(FileStoreError.NOT_A_FILE, relPath); } const dest = this.resolveUnderRoot(relPath); const payload = typeof data === 'string' ? (0, bytes_1.encodeUTF8)(data) : data; this.requireUtf8(relPath, payload); await makeDirsBelowRoot(this.rootPath, node_1.path.dirname(dest)); await replaceViaTemp(dest, payload, opts?.executable ?? false); } /** The file's bytes; `null` when absent. */ async get(relPath) { const dest = this.resolveUnderRoot(relPath); let handle; try { handle = await openRegularFile(relPath, dest); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } let data; try { const buf = await handle.readFile(); data = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); } finally { await handle.close(); } this.requireUtf8(relPath, data); return data; } /** The relative path of every file under the directory `under`. */ async ls(under = '/') { const base = this.resolveUnderRoot(under); return new Set((await filenamesInDir(this.rootPath, under, base)).map(([rel]) => rel)); } /** * Every symlink under `under` — listings skip them and reads refuse them, * so a caller that must know they exist asks here. */ async findSymlinks(under = '/') { const base = this.resolveUnderRoot(under); return symlinksInDir(this.rootPath, under, base); } /** * `{relPath: sha256Hex}` of every file under the directory `under`. * * Unchanged files — same size, mtime, and ctime since the last call — * reuse their recorded hash instead of being re-read. */ async hashtree(under = '/') { const base = this.resolveUnderRoot(under); const walkStartNs = exports._internals.nowNs(); // Null prototype so a file named `__proto__` (or `constructor`) is an // ordinary own key instead of a silent prototype write / inherited read. const out = Object.create(null); for (const [rel, full] of await filenamesInDir(this.rootPath, under, base)) { const sha = await this.hashViaCache(rel, full, walkStartNs); if (sha !== null) out[rel] = sha; } return out; } /** One file's sha256; `null` when absent. Shares {@link hashtree}'s cache. */ async hashFile(relPath) { const dest = this.resolveUnderRoot(relPath); let st; try { st = await fsp.lstat(dest, { bigint: true }); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } if (st.isSymbolicLink()) throw new FileStoreError(FileStoreError.IS_A_SYMLINK, relPath); if (!st.isFile()) throw new FileStoreError(FileStoreError.NOT_A_FILE, relPath); const rel = node_1.path.relative(this.rootPath, dest).split(node_1.path.sep).join('/'); return this.hashViaCache(rel, dest, exports._internals.nowNs()); } /** * Rename `src` to `dst`; an existing `dst` is refused. The banned store * root as either end does nothing. */ async move(src, dst) { const s = this.resolveUnderRoot(src); const d = this.resolveUnderRoot(dst); if (s === this.rootPath || d === this.rootPath) return; // stat, not lstat: a dangling symlink at dst reads as absent and is // atomically replaced by the rename, like any other rename target. const dstExists = await fsp.stat(d).then(() => true, () => false); if (dstExists) throw new FileStoreError(FileStoreError.MOVE_DESTINATION_EXISTS, dst); await makeDirsBelowRoot(this.rootPath, node_1.path.dirname(d)); await fsp.rename(s, d); } /** Delete a file or subtree; absent — and the banned store root — do nothing. */ async remove(relPath) { const dest = this.resolveUnderRoot(relPath); if (dest === this.rootPath) return; let st; try { // lstat: a dangling symlink must still be unlinked. st = await fsp.lstat(dest, { bigint: true }); } catch (e) { if (e.code === 'ENOENT') return; throw e; } if (st.isDirectory()) { await fsp.rm(dest, { recursive: true, force: true }); } else { try { await fsp.unlink(dest); } catch (e) { if (e.code !== 'ENOENT') throw e; } } } resolveUnderRoot(relPath) { const norm = relPath.replace(/\\/g, '/').replace(/^\/+/, ''); const parts = norm.split('/').filter((p) => p !== '' && p !== '.'); if (node_1.path.posix.isAbsolute(norm) || parts.includes('..')) { throw new FileStoreError(FileStoreError.ESCAPES_ROOT, relPath); } return parts.length === 0 ? this.rootPath : node_1.path.join(this.rootPath, ...parts); } requireUtf8(relPath, data) { if (!this.decoder) return; try { this.decoder.decode(data); } catch { throw new FileStoreError(FileStoreError.NOT_UTF8, relPath); } } async hashViaCache(rel, full, walkStartNs) { let st; try { st = await fsp.lstat(full, { bigint: true }); } catch (e) { if (e.code === 'ENOENT') return null; // vanished since the walk: not in this snapshot throw e; } if (!st.isFile()) return null; const cached = this.hashes.get(rel); let sha; if (cached !== undefined && unchangedSinceHashed(cached, st)) { sha = cached.sha; } else { try { sha = await exports._internals.hashFile(full); } catch (e) { const code = e.code; if (code === 'ENOENT' || e instanceof FileStoreError) return null; // FreeBSD reports EMLINK rather than ELOOP for O_NOFOLLOW. if (code === 'ELOOP' || code === 'EMLINK') return null; throw e; } } if (oldEnoughToCache(st, walkStartNs)) { this.hashes.set(rel, { mtimeNs: st.mtimeNs, ctimeNs: st.ctimeNs, size: st.size, sha }); } return sha; } } FileStore.isPathLegal = isPathLegal; return FileStore; })(); exports.FileStore = FileStore; function platformSupported() { return O_NOFOLLOW !== 0; } async function makeDirAndAncestors(dir) { const missing = []; let current = dir; for (;;) { try { await fsp.stat(current); break; } catch (e) { const code = e.code; if (code !== 'ENOENT' && code !== 'ENOTDIR' && code !== 'ELOOP') throw e; } missing.push(current); const parent = node_1.path.dirname(current); if (parent === current) break; current = parent; } for (const directory of missing.reverse()) { try { await fsp.mkdir(directory, { mode: OWNER_ONLY_DIR_MODE }); } catch (e) { if (e.code !== 'EEXIST') throw e; } } } async function makeDirsBelowRoot(root, dir) { // Never the root itself: only createRoot() makes it, so a write racing an // rm -rf of the folder fails with ENOENT instead of re-creating it. const below = node_1.path.relative(root, dir); if (below === '') return; let current = root; for (const part of below.split(node_1.path.sep)) { current = node_1.path.join(current, part); try { await fsp.mkdir(current, { mode: OWNER_ONLY_DIR_MODE }); } catch (e) { if (e.code !== 'EEXIST') throw e; } } } async function replaceViaTemp(dest, data, isExecutable) { const mode = isExecutable ? OWNER_ONLY_EXEC_MODE : OWNER_ONLY_FILE_MODE; const tmp = node_1.path.join(node_1.path.dirname(dest), `.fs-${node_1.crypto.randomBytes(8).toString('hex')}.tmp`); let handle; try { handle = await fsp.open(tmp, C.O_WRONLY | C.O_CREAT | C.O_EXCL | O_NOFOLLOW, mode); await handle.writeFile(data); await handle.close(); handle = undefined; await fsp.rename(tmp, dest); } catch (err) { // Best-effort temp cleanup; never mask the original error. if (handle) await handle.close().catch(() => { }); await fsp.unlink(tmp).catch(() => { }); throw err; } } async function openRegularFile(relPath, dest) { // O_NONBLOCK: a FIFO fails the fstat check below instead of blocking the open. let handle; try { handle = await fsp.open(dest, C.O_RDONLY | O_NOFOLLOW | O_NONBLOCK); } catch (e) { // FreeBSD reports EMLINK rather than ELOOP for O_NOFOLLOW. const code = e.code; if (code === 'ELOOP' || code === 'EMLINK') { throw new FileStoreError(FileStoreError.IS_A_SYMLINK, relPath); } throw e; } try { const st = await handle.stat(); if (!st.isFile()) throw new FileStoreError(FileStoreError.NOT_A_FILE, relPath); } catch (e) { await handle.close().catch(() => { }); throw e; } return handle; } /** sha256 of a file's contents, streamed — constant memory on any file size. */ async function hashFile(full) { const digest = node_1.crypto.createHash('sha256'); const handle = await openRegularFile(node_1.path.basename(full), full); const buf = new Uint8Array(1024 * 1024); try { for (;;) { const { bytesRead } = await handle.read(buf, 0, buf.length); if (bytesRead === 0) break; digest.update(buf.subarray(0, bytesRead)); } } finally { await handle.close(); } return digest.digest('hex'); } /** * `[rel, path]` for every regular file under the directory `base`; an absent * `base` is empty, a present non-directory is refused. The walk never * descends symlinked directories. */ async function filenamesInDir(root, under, base) { if (!(await requireDir(under, base))) return []; const out = []; await walk(base, (full, entry) => { if (entry.isFile()) out.push([node_1.path.relative(root, full).split(node_1.path.sep).join('/'), full]); }); out.sort(); return out; } async function symlinksInDir(root, under, base) { const relOf = (full) => node_1.path.relative(root, full).split(node_1.path.sep).join('/'); let st; try { st = await fsp.lstat(base, { bigint: true }); } catch (e) { const code = e.code; if (code === 'ENOENT' || code === 'ENOTDIR') return new Set(); throw e; } if (st.isSymbolicLink()) return new Set([relOf(base)]); if (!st.isDirectory()) throw new FileStoreError(FileStoreError.NOT_A_DIRECTORY, under); const out = new Set(); // Symlinks to directories are reported, never descended, like the rest. await walk(base, (full, entry) => { if (entry.isSymbolicLink()) out.add(relOf(full)); }); return out; } /** `false` when `base` is absent, refused when present but not a directory. */ async function requireDir(under, base) { let st; try { st = await fsp.lstat(base, { bigint: true }); } catch (e) { // Absent — including "under a file" (ENOTDIR) — is an empty listing. const code = e.code; if (code === 'ENOENT' || code === 'ENOTDIR') return false; throw e; } if (!st.isDirectory()) throw new FileStoreError(FileStoreError.NOT_A_DIRECTORY, under); return true; } /** Visit every entry under `base` without descending symlinked directories. */ async function walk(base, visit) { const stack = [base]; while (stack.length) { const dir = stack.pop(); let entries; try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch (e) { if (e.code === 'ENOENT') continue; // a listing is a snapshot, not a lock throw e; } for (const entry of entries) { const full = node_1.path.join(dir, entry.name); visit(full, entry); if (entry.isDirectory() && !entry.isSymbolicLink()) stack.push(full); } } } function unchangedSinceHashed(cached, st) { return st.mtimeNs === cached.mtimeNs && st.ctimeNs === cached.ctimeNs && st.size === cached.size; } function oldEnoughToCache(st, walkStartNs) { const newestNs = st.mtimeNs > st.ctimeNs ? st.mtimeNs : st.ctimeNs; return newestNs < walkStartNs - exports._internals.timestampTrustMarginNs; } // Filesystems stamp times with coarse clocks, so a rewrite shortly after a // hashed write can reuse the exact stamps. Files younger than the margin are // simply re-hashed next walk. const TIMESTAMP_TRUST_MARGIN_NS = 2000000000n; /** Test seam — the hasher, the trust margin, and the walk clock. @internal */ exports._internals = { hashFile, timestampTrustMarginNs: TIMESTAMP_TRUST_MARGIN_NS, nowNs: () => BigInt(Date.now()) * 1000000n, }; exports.LocalFileStore = FileStore; // Wire `Symbol.asyncDispose` at runtime when the host provides it — the // repo's tsconfig targets ES2020 so the type-level `AsyncDisposable` lib is // not available, but `await using` callers on newer engines still work. const asyncDispose = Symbol.asyncDispose; if (asyncDispose) { Object.defineProperty(FileStore.prototype, asyncDispose, { value: FileStore.prototype.dispose, configurable: true, writable: true, }); } //# sourceMappingURL=file-store.js.map