From 2e41f030f02a336c2e9866d3d56b0494da5a622e Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Mon, 15 Dec 2025 00:58:43 -0800 Subject: Remove admin route in favor of a simpler toml format --- src/storage/index.ts | 95 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 39 deletions(-) (limited to 'src/storage') diff --git a/src/storage/index.ts b/src/storage/index.ts index c3c97d8..8d7debd 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1,7 +1,9 @@ import { randomUUID } from 'crypto'; import { mkdir, writeFile, readFile } from 'fs/promises'; +import { watch } from 'fs'; import { basename, join } from 'path'; -import { isSafeRouteName, type RouteConfig, type StoredRequest } from '../types/index.js'; +import { parse as parseToml } from 'smol-toml'; +import { isSafeRouteName, isRouteConfig, type RouteConfig, type StoredRequest } from '../types/index.js'; import { Either, type IEither } from '@emprespresso/pengueno'; type IncomingUpload = { @@ -20,13 +22,20 @@ function sanitizeFilename(filename: string): string { export class Storage { private routes: Map = new Map(); + private configPath: string; - constructor(private readonly dataDir: string = './data') {} + constructor( + private readonly dataDir: string = './data', + configPath: string = './routes.toml', + ) { + this.configPath = configPath; + } async init(): Promise> { try { await mkdir(this.dataDir, { recursive: true }); await this.loadRoutes(); + this.watchConfig(); return Either.right(undefined); } catch (err) { return Either.left(err instanceof Error ? err : new Error(String(err))); @@ -35,44 +44,60 @@ export class Storage { 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[]; + const data = await readFile(this.configPath, 'utf-8'); + const parsed = parseToml(data); + + if (!parsed || typeof parsed !== 'object' || !('route' in parsed)) { + console.error('Invalid routes.toml: missing [[route]] sections'); + process.exit(1); + } + + const routes = parsed.route; + if (!Array.isArray(routes)) { + console.error('Invalid routes.toml: "route" must be an array of tables'); + process.exit(1); + } + + const newRoutes = new Map(); for (const route of routes) { - if (!isSafeRouteName(route.name)) { - continue; + if (!isRouteConfig(route)) { + console.error('Invalid route configuration:', route); + process.exit(1); + } + if (newRoutes.has(route.name)) { + console.error(`Duplicate route name: ${route.name}`); + process.exit(1); } - this.routes.set(route.name, route); + newRoutes.set(route.name, route); + + // Ensure route directory exists + const routeDir = join(this.dataDir, route.name); + await mkdir(routeDir, { recursive: true }); } - } 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); + this.routes = newRoutes; + console.log(`Loaded ${this.routes.size} route(s) from ${this.configPath}`); } catch (err) { - return Either.left(err instanceof Error ? err : new Error(String(err))); + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + console.log(`No ${this.configPath} found, starting with empty routes`); + return; + } + console.error(`Failed to load routes from ${this.configPath}:`, err); + process.exit(1); } } - async registerRoute(config: RouteConfig): Promise> { - if (!isSafeRouteName(config.name)) { - return Either.left(new Error('Invalid route name')); - } + private watchConfig(): void { + const watcher = watch(this.configPath, async (eventType) => { + if (eventType === 'change') { + console.log(`${this.configPath} changed, reloading...`); + await this.loadRoutes(); + } + }); - 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(); + watcher.on('error', (err) => { + console.error(`Error watching ${this.configPath}:`, err); + }); } getRoute(name: string): RouteConfig | undefined { @@ -84,14 +109,6 @@ export class Storage { 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, -- cgit v1.2.3-70-g09d2