import * as plugins from '../plugins.js'; import type { S3Context } from './context.js'; export type RouteHandler = ( req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, ctx: S3Context, params: Record ) => Promise; export interface IRouteMatch { handler: RouteHandler; params: Record; } interface IRoute { method: string; pattern: RegExp; paramNames: string[]; handler: RouteHandler; } /** * Simple HTTP router with pattern matching for S3 routes */ export class S3Router { private routes: IRoute[] = []; /** * Add a route with pattern matching * Supports patterns like: * - "/" (exact match) * - "/:bucket" (single param) * - "/:bucket/:key*" (param with wildcard - captures everything after) */ public add(method: string, pattern: string, handler: RouteHandler): void { const { regex, paramNames } = this.convertPatternToRegex(pattern); this.routes.push({ method: method.toUpperCase(), pattern: regex, paramNames, handler, }); } /** * Match a request to a route */ public match(method: string, pathname: string): IRouteMatch | null { // Normalize pathname: remove trailing slash unless it's root const normalizedPath = pathname === '/' ? pathname : pathname.replace(/\/$/, ''); for (const route of this.routes) { if (route.method !== method.toUpperCase()) { continue; } const match = normalizedPath.match(route.pattern); if (match) { // Extract params from captured groups const params: Record = {}; for (let i = 0; i < route.paramNames.length; i++) { params[route.paramNames[i]] = decodeURIComponent(match[i + 1] || ''); } return { handler: route.handler, params, }; } } return null; } /** * Convert path pattern to RegExp * Examples: * - "/" → /^\/$/ * - "/:bucket" → /^\/([^/]+)$/ * - "/:bucket/:key*" → /^\/([^/]+)\/(.+)$/ */ private convertPatternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } { const paramNames: string[] = []; let regexStr = pattern; // Process all params in a single pass to maintain order regexStr = regexStr.replace(/:(\w+)(\*)?/g, (match, paramName, isWildcard) => { paramNames.push(paramName); // :param* captures rest of path, :param captures single segment return isWildcard ? '(.+)' : '([^/]+)'; }); // Escape special regex characters regexStr = regexStr.replace(/\//g, '\\/'); // Add anchors regexStr = `^${regexStr}$`; return { regex: new RegExp(regexStr), paramNames, }; } /** * Convenience methods for common HTTP methods */ public get(pattern: string, handler: RouteHandler): void { this.add('GET', pattern, handler); } public put(pattern: string, handler: RouteHandler): void { this.add('PUT', pattern, handler); } public post(pattern: string, handler: RouteHandler): void { this.add('POST', pattern, handler); } public delete(pattern: string, handler: RouteHandler): void { this.add('DELETE', pattern, handler); } public head(pattern: string, handler: RouteHandler): void { this.add('HEAD', pattern, handler); } }