130 lines
3.3 KiB
TypeScript
130 lines
3.3 KiB
TypeScript
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<string, string>
|
|
) => Promise<void>;
|
|
|
|
export interface IRouteMatch {
|
|
handler: RouteHandler;
|
|
params: Record<string, string>;
|
|
}
|
|
|
|
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<string, string> = {};
|
|
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);
|
|
}
|
|
}
|