115 lines
3.0 KiB
TypeScript
115 lines
3.0 KiB
TypeScript
|
|
import * as plugins from '../plugins.js';
|
||
|
|
import { S3Error } from './s3-error.js';
|
||
|
|
import { createXml } from '../utils/xml.utils.js';
|
||
|
|
import type { FilesystemStore } from './filesystem-store.js';
|
||
|
|
import type { Readable } from 'stream';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* S3 request context with helper methods
|
||
|
|
*/
|
||
|
|
export class S3Context {
|
||
|
|
public method: string;
|
||
|
|
public url: URL;
|
||
|
|
public headers: plugins.http.IncomingHttpHeaders;
|
||
|
|
public params: Record<string, string> = {};
|
||
|
|
public query: Record<string, string> = {};
|
||
|
|
public store: FilesystemStore;
|
||
|
|
|
||
|
|
private req: plugins.http.IncomingMessage;
|
||
|
|
private res: plugins.http.ServerResponse;
|
||
|
|
private statusCode: number = 200;
|
||
|
|
private responseHeaders: Record<string, string> = {};
|
||
|
|
|
||
|
|
constructor(
|
||
|
|
req: plugins.http.IncomingMessage,
|
||
|
|
res: plugins.http.ServerResponse,
|
||
|
|
store: FilesystemStore
|
||
|
|
) {
|
||
|
|
this.req = req;
|
||
|
|
this.res = res;
|
||
|
|
this.store = store;
|
||
|
|
this.method = req.method || 'GET';
|
||
|
|
this.headers = req.headers;
|
||
|
|
|
||
|
|
// Parse URL and query string
|
||
|
|
const fullUrl = `http://${req.headers.host || 'localhost'}${req.url || '/'}`;
|
||
|
|
this.url = new URL(fullUrl);
|
||
|
|
|
||
|
|
// Parse query string into object
|
||
|
|
this.url.searchParams.forEach((value, key) => {
|
||
|
|
this.query[key] = value;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set response status code
|
||
|
|
*/
|
||
|
|
public status(code: number): this {
|
||
|
|
this.statusCode = code;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set response header
|
||
|
|
*/
|
||
|
|
public setHeader(name: string, value: string | number): this {
|
||
|
|
this.responseHeaders[name] = value.toString();
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Send response body (string, Buffer, or Stream)
|
||
|
|
*/
|
||
|
|
public async send(body: string | Buffer | Readable | NodeJS.ReadableStream): Promise<void> {
|
||
|
|
// Write status and headers
|
||
|
|
this.res.writeHead(this.statusCode, this.responseHeaders);
|
||
|
|
|
||
|
|
// Handle different body types
|
||
|
|
if (typeof body === 'string' || body instanceof Buffer) {
|
||
|
|
this.res.end(body);
|
||
|
|
} else if (body && typeof (body as any).pipe === 'function') {
|
||
|
|
// It's a stream
|
||
|
|
(body as Readable).pipe(this.res);
|
||
|
|
} else {
|
||
|
|
this.res.end();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Send XML response
|
||
|
|
*/
|
||
|
|
public async sendXML(obj: any): Promise<void> {
|
||
|
|
const xml = createXml(obj, { format: true });
|
||
|
|
this.setHeader('Content-Type', 'application/xml');
|
||
|
|
this.setHeader('Content-Length', Buffer.byteLength(xml));
|
||
|
|
await this.send(xml);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Throw an S3 error
|
||
|
|
*/
|
||
|
|
public throw(code: string, message: string, detail?: Record<string, any>): never {
|
||
|
|
throw new S3Error(code, message, detail);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Read and parse request body as string
|
||
|
|
*/
|
||
|
|
public async readBody(): Promise<string> {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const chunks: Buffer[] = [];
|
||
|
|
|
||
|
|
this.req.on('data', (chunk) => chunks.push(chunk));
|
||
|
|
this.req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||
|
|
this.req.on('error', reject);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the request stream (for streaming uploads)
|
||
|
|
*/
|
||
|
|
public getRequestStream(): NodeJS.ReadableStream {
|
||
|
|
return this.req;
|
||
|
|
}
|
||
|
|
}
|