feat(smarts3-server): Introduce native custom S3 server implementation (Smarts3Server) with routing, middleware, context, filesystem store, controllers and XML utilities; add SmartXml and AWS SDK test; keep optional legacy s3rver backend.

This commit is contained in:
2025-11-21 14:32:19 +00:00
parent 3ab667049a
commit 18a2eb7e3f
18 changed files with 1949 additions and 632 deletions

129
ts/classes/router.ts Normal file
View File

@@ -0,0 +1,129 @@
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);
}
}