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

View File

@@ -0,0 +1,130 @@
import * as plugins from '../plugins.js';
import type { S3Context } from '../classes/context.js';
/**
* Bucket-level operations
*/
export class BucketController {
/**
* HEAD /:bucket - Check if bucket exists
*/
public static async headBucket(
req: plugins.http.IncomingMessage,
res: plugins.http.ServerResponse,
ctx: S3Context,
params: Record<string, string>
): Promise<void> {
const { bucket } = params;
if (await ctx.store.bucketExists(bucket)) {
ctx.status(200).send('');
} else {
ctx.throw('NoSuchBucket', 'The specified bucket does not exist');
}
}
/**
* PUT /:bucket - Create bucket
*/
public static async createBucket(
req: plugins.http.IncomingMessage,
res: plugins.http.ServerResponse,
ctx: S3Context,
params: Record<string, string>
): Promise<void> {
const { bucket } = params;
await ctx.store.createBucket(bucket);
ctx.status(200).send('');
}
/**
* DELETE /:bucket - Delete bucket
*/
public static async deleteBucket(
req: plugins.http.IncomingMessage,
res: plugins.http.ServerResponse,
ctx: S3Context,
params: Record<string, string>
): Promise<void> {
const { bucket } = params;
await ctx.store.deleteBucket(bucket);
ctx.status(204).send('');
}
/**
* GET /:bucket - List objects
* Supports both V1 and V2 listing (V2 uses list-type=2 query param)
*/
public static async listObjects(
req: plugins.http.IncomingMessage,
res: plugins.http.ServerResponse,
ctx: S3Context,
params: Record<string, string>
): Promise<void> {
const { bucket } = params;
const isV2 = ctx.query['list-type'] === '2';
const result = await ctx.store.listObjects(bucket, {
prefix: ctx.query.prefix,
delimiter: ctx.query.delimiter,
maxKeys: ctx.query['max-keys'] ? parseInt(ctx.query['max-keys']) : 1000,
continuationToken: ctx.query['continuation-token'],
});
if (isV2) {
// List Objects V2 response
await ctx.sendXML({
ListBucketResult: {
'@_xmlns': 'http://s3.amazonaws.com/doc/2006-03-01/',
Name: bucket,
Prefix: result.prefix || '',
MaxKeys: result.maxKeys,
KeyCount: result.contents.length,
IsTruncated: result.isTruncated,
...(result.delimiter && { Delimiter: result.delimiter }),
...(result.nextContinuationToken && {
NextContinuationToken: result.nextContinuationToken,
}),
...(result.commonPrefixes.length > 0 && {
CommonPrefixes: result.commonPrefixes.map((prefix) => ({
Prefix: prefix,
})),
}),
Contents: result.contents.map((obj) => ({
Key: obj.key,
LastModified: obj.lastModified.toISOString(),
ETag: `"${obj.md5}"`,
Size: obj.size,
StorageClass: 'STANDARD',
})),
},
});
} else {
// List Objects V1 response
await ctx.sendXML({
ListBucketResult: {
'@_xmlns': 'http://s3.amazonaws.com/doc/2006-03-01/',
Name: bucket,
Prefix: result.prefix || '',
MaxKeys: result.maxKeys,
IsTruncated: result.isTruncated,
...(result.delimiter && { Delimiter: result.delimiter }),
...(result.commonPrefixes.length > 0 && {
CommonPrefixes: result.commonPrefixes.map((prefix) => ({
Prefix: prefix,
})),
}),
Contents: result.contents.map((obj) => ({
Key: obj.key,
LastModified: obj.lastModified.toISOString(),
ETag: `"${obj.md5}"`,
Size: obj.size,
StorageClass: 'STANDARD',
})),
},
});
}
}
}