import fs from 'node:fs'; import fsp from 'node:fs/promises'; import path from 'node:path'; function storageError(message, code, details = {}) { return Object.assign(new Error(message), { code, ...details }); } export function normalizeMindSpaceStorageKey(key) { const raw = String(key ?? '').trim(); if (!raw) { throw storageError('storage key is required', 'invalid_storage_key'); } if (raw.includes('\0')) { throw storageError('storage key must not contain null bytes', 'invalid_storage_key'); } if (raw.includes('\\')) { throw storageError('storage key must use forward slashes', 'invalid_storage_key'); } if (path.isAbsolute(raw) || /^[A-Za-z]:\//.test(raw)) { throw storageError('storage key must be relative', 'invalid_storage_key'); } if (raw.split('/').includes('..')) { throw storageError('storage key must not traverse parent directories', 'invalid_storage_key'); } const normalized = path.posix.normalize(raw.replace(/^\/+/, '')); if (normalized === '.' || normalized.startsWith('../') || normalized.includes('/../')) { throw storageError('storage key must not traverse parent directories', 'invalid_storage_key'); } return normalized; } export function resolveStoragePath(storageRoot, key) { const root = path.resolve(String(storageRoot ?? '')); if (!root) { throw storageError('storage root is required', 'invalid_storage_root'); } const normalizedKey = normalizeMindSpaceStorageKey(key); const target = path.resolve(root, ...normalizedKey.split('/')); if (target !== root && !target.startsWith(`${root}${path.sep}`)) { throw storageError('storage key resolves outside storage root', 'invalid_storage_key', { key: normalizedKey, }); } return target; } async function walkFiles(root, dir, prefix, results) { const entries = await fsp.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const absolutePath = path.join(dir, entry.name); const relativePath = path.relative(root, absolutePath).split(path.sep).join('/'); if (entry.isDirectory()) { await walkFiles(root, absolutePath, prefix, results); } else if (!prefix || relativePath === prefix || relativePath.startsWith(`${prefix}/`)) { const stat = await fsp.stat(absolutePath); results.push({ key: relativePath, sizeBytes: stat.size, updatedAt: stat.mtimeMs, }); } } } export function createLocalMindSpaceStorageAdapter(storageRoot) { const root = path.resolve(storageRoot); return { kind: 'local-fs', root, async putObject(key, body) { const target = resolveStoragePath(root, key); await fsp.mkdir(path.dirname(target), { recursive: true }); await fsp.writeFile(target, body); const stat = await fsp.stat(target); return { key: normalizeMindSpaceStorageKey(key), sizeBytes: stat.size, updatedAt: stat.mtimeMs, }; }, async getObject(key) { return fsp.readFile(resolveStoragePath(root, key)); }, async statObject(key) { try { const stat = await fsp.stat(resolveStoragePath(root, key)); return { key: normalizeMindSpaceStorageKey(key), exists: true, sizeBytes: stat.size, updatedAt: stat.mtimeMs, isDirectory: stat.isDirectory(), }; } catch (error) { if (error?.code !== 'ENOENT') throw error; return { key: normalizeMindSpaceStorageKey(key), exists: false, sizeBytes: 0, updatedAt: null, isDirectory: false, }; } }, async deleteObject(key) { await fsp.rm(resolveStoragePath(root, key), { force: true }); return { key: normalizeMindSpaceStorageKey(key), deleted: true }; }, async listObjects(prefix = '') { const normalizedPrefix = prefix ? normalizeMindSpaceStorageKey(prefix).replace(/\/$/, '') : ''; const results = []; try { await walkFiles(root, root, normalizedPrefix, results); } catch (error) { if (error?.code !== 'ENOENT') throw error; } return results.sort((a, b) => a.key.localeCompare(b.key)); }, async copyObject(sourceKey, targetKey) { const source = resolveStoragePath(root, sourceKey); const target = resolveStoragePath(root, targetKey); await fsp.mkdir(path.dirname(target), { recursive: true }); await fsp.copyFile(source, target); const stat = await fsp.stat(target); return { key: normalizeMindSpaceStorageKey(targetKey), sourceKey: normalizeMindSpaceStorageKey(sourceKey), sizeBytes: stat.size, updatedAt: stat.mtimeMs, }; }, createReadStream(key) { return fs.createReadStream(resolveStoragePath(root, key)); }, createWriteStream(key) { const target = resolveStoragePath(root, key); fs.mkdirSync(path.dirname(target), { recursive: true }); return fs.createWriteStream(target); }, async getSignedUrl() { throw storageError('local fs adapter does not support signed URLs', 'signed_url_not_supported'); }, }; } export const mindspaceStorageAdapterInternals = { storageError, };