import { randomUUID } from 'crypto'; import { mkdir, writeFile, readFile } from 'fs/promises'; import { basename, join } from 'path'; import { isSafeRouteName, type RouteConfig, type StoredRequest } from '../types/index.js'; import { Either, type IEither } from '@emprespresso/pengueno'; type IncomingUpload = { fieldName: string; filename: string; contentType: string; size: number; data: Uint8Array; }; function sanitizeFilename(filename: string): string { const base = basename(filename); const safe = base.replace(/[^a-zA-Z0-9._-]/g, '_'); return safe.length > 0 ? safe.slice(0, 200) : 'upload.bin'; } export class Storage { private routes: Map = new Map(); constructor(private readonly dataDir: string = './data') {} async init(): Promise> { try { await mkdir(this.dataDir, { recursive: true }); await this.loadRoutes(); return Either.right(undefined); } catch (err) { return Either.left(err instanceof Error ? err : new Error(String(err))); } } private async loadRoutes(): Promise { try { const routesPath = join(this.dataDir, 'routes.json'); const data = await readFile(routesPath, 'utf-8'); const routes = JSON.parse(data) as RouteConfig[]; for (const route of routes) { if (!isSafeRouteName(route.name)) { continue; } this.routes.set(route.name, route); } } catch { // routes file doesn't exist yet, that's ok } } private async saveRoutes(): Promise> { try { const routesPath = join(this.dataDir, 'routes.json'); const routes = Array.from(this.routes.values()); await writeFile(routesPath, JSON.stringify(routes, null, 2)); return Either.right(undefined); } catch (err) { return Either.left(err instanceof Error ? err : new Error(String(err))); } } async registerRoute(config: RouteConfig): Promise> { if (!isSafeRouteName(config.name)) { return Either.left(new Error('Invalid route name')); } this.routes.set(config.name, config); const routeDir = join(this.dataDir, config.name); try { await mkdir(routeDir, { recursive: true }); } catch (err) { return Either.left(err instanceof Error ? err : new Error(String(err))); } return this.saveRoutes(); } getRoute(name: string): RouteConfig | undefined { if (!isSafeRouteName(name)) return undefined; return this.routes.get(name); } listRoutes(): RouteConfig[] { return Array.from(this.routes.values()); } async deleteRoute(name: string): Promise> { if (!isSafeRouteName(name)) { return Either.left(new Error('Invalid route name')); } this.routes.delete(name); return this.saveRoutes(); } async storeRequest( routeName: string, method: string, headers: Record, body: unknown, uploads?: IncomingUpload[], ): Promise> { if (!isSafeRouteName(routeName)) { return Either.left(new Error('Invalid route name')); } const timestamp = Date.now(); const uuid = randomUUID(); const baseName = `${timestamp}_${uuid}`; const routeDir = join(this.dataDir, routeName); try { await mkdir(routeDir, { recursive: true }); const requestDir = join(routeDir, baseName); await mkdir(requestDir, { recursive: true }); const files: StoredRequest['files'] = uploads?.length ? await (async () => { const filesDir = join(requestDir, 'files'); await mkdir(filesDir, { recursive: true }); const storedFiles: NonNullable = []; for (let i = 0; i < uploads.length; i++) { const upload = uploads[i]; const safeOriginal = sanitizeFilename(upload.filename); const savedName = `${i}_${safeOriginal}`; const diskPath = join(filesDir, savedName); await writeFile(diskPath, Buffer.from(upload.data)); storedFiles.push({ fieldName: upload.fieldName, originalFilename: upload.filename, filename: savedName, contentType: upload.contentType, size: upload.size, path: join('files', savedName), }); } return storedFiles; })() : undefined; const stored: StoredRequest = { timestamp, uuid, routeName, method, headers, body, files, }; await writeFile(join(requestDir, 'request.json'), JSON.stringify(stored, null, 2)); await writeFile(join(requestDir, 'body.json'), JSON.stringify(body, null, 2)); return Either.right(stored); } catch (err) { return Either.left(err instanceof Error ? err : new Error(String(err))); } } }