refactor(fase1-s3): split s3-controller into s3/ modules + guards

This commit is contained in:
asepharyana committed 2026-09-14 17:25:07 +07:00
1 parent 5d01a9405f
commit d323260ed5
8 files changed
+1875 -1778

No files matched your search

@@ -0,0 +1,111 @@
import { bucketRepository, fileRepository } from '../../../../infrastructure/di';
import { BucketNameSchema, parseOrNull } from '../../../../shared/validation/schemas';
import { bucketVersioningConfigurationXml, listBucketsXml, s3ErrorResponse } from '../../../s3/xml';
import { resolveBucketOr404, s3Response } from './s3-common';
// ─────── Bucket Operations ───────
/**
* Handles GET / — lists all buckets as an S3 ListAllMyBuckets XML response.
*
* @param reqId - The request identifier for S3 headers.
* @returns An S3 XML response with the bucket list.
*/
export const handleListBuckets = async (reqId: string): Promise<Response> => {
const buckets = await bucketRepository.list();
const xml = listBucketsXml(buckets, reqId);
return s3Response(xml, 200, reqId, { 'content-type': 'application/xml' });
};
/**
* Handles PUT /{bucket} — creates a new S3 bucket.
*
* Validates the bucket name format (BucketNameSchema, incl. the stricter M13
* rules: no consecutive dots, no IP format, no `xn--` prefix) and checks for
* duplicates.
*
* @param bucketName - The requested bucket name.
* @param reqId - The request identifier for S3 headers.
* @returns An S3 XML response indicating success or failure.
*/
export const handleCreateBucket = async (bucketName: string, reqId: string): Promise<Response> => {
if (parseOrNull(BucketNameSchema, bucketName) === null) {
return s3ErrorResponse(
'InvalidBucketName',
'The specified bucket is not valid.',
`/${bucketName}`,
400,
reqId,
);
}
const existing = await bucketRepository.findByName(bucketName);
if (existing) {
return s3ErrorResponse(
'BucketAlreadyExists',
'The requested bucket name is not available.',
`/${bucketName}`,
409,
reqId,
);
}
await bucketRepository.create(bucketName);
return s3Response(null, 200, reqId);
};
/**
* Handles HEAD /{bucket} — checks whether a bucket exists.
*
* @param bucketName - The bucket name to check.
* @param reqId - The request identifier for S3 headers.
* @returns A 200 response when the bucket exists, or an S3 XML error.
*/
export const handleHeadBucket = async (bucketName: string, reqId: string): Promise<Response> => {
const bucket = await resolveBucketOr404(bucketRepository, bucketName, `/${bucketName}`, reqId);
if (bucket instanceof Response) return bucket;
return s3Response(null, 200, reqId);
};
/**
* Handles DELETE /{bucket} — deletes a bucket.
*
* Fails with `BucketNotEmpty` if the bucket still contains objects.
*
* @param bucketName - The bucket name to delete.
* @param reqId - The request identifier for S3 headers.
* @returns A 204 response on success, or an S3 XML error.
*/
export const handleDeleteBucket = async (bucketName: string, reqId: string): Promise<Response> => {
const bucket = await resolveBucketOr404(bucketRepository, bucketName, `/${bucketName}`, reqId);
if (bucket instanceof Response) return bucket;
const objCount = await fileRepository.countByBucket(bucket.id);
if (objCount > 0) {
return s3ErrorResponse(
'BucketNotEmpty',
'The bucket you tried to delete is not empty.',
`/${bucketName}`,
409,
reqId,
);
}
await bucketRepository.delete(bucketName);
return s3Response(null, 204, reqId);
};
/**
* Handles GET /{bucket}?versioning — returns the bucket versioning
* configuration (always disabled in this implementation).
*
* @param bucketName - The bucket name.
* @param reqId - The request identifier for S3 headers.
* @returns An S3 XML response with the versioning configuration.
*/
export const handleGetBucketVersioning = async (
bucketName: string,
reqId: string,
): Promise<Response> => {
const bucket = await resolveBucketOr404(bucketRepository, bucketName, `/${bucketName}`, reqId);
if (bucket instanceof Response) return bucket;
return s3Response(bucketVersioningConfigurationXml(), 200, reqId, {
'content-type': 'application/xml',
});
};