feat(storage): cleanup buckets, add file-size validation and bulk delete

- Remove archived LIGHTWRITE and NUTRIPHI from BUCKETS
- Add missing PLANTA and PROJECTDOC buckets (were in Docker init but not in code)
- Add maxSizeBytes option to upload() and uploadMultipart() for size enforcement
- Add deleteMany() with automatic batching (1000 keys per S3 request)
- Add factories for createPlantaStorage() and createProjectDocStorage()
- Update tests (62 passing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-20 19:27:42 +01:00
parent 720602343e
commit 01c4d3a9d1
6 changed files with 106 additions and 8 deletions

View file

@ -3,6 +3,7 @@ import {
PutObjectCommand,
GetObjectCommand,
DeleteObjectCommand,
DeleteObjectsCommand,
ListObjectsV2Command,
HeadObjectCommand,
type PutObjectCommandInput,
@ -62,6 +63,15 @@ export class StorageClient {
body: Buffer | Uint8Array | string | ReadableStream,
options: UploadOptions = {}
): Promise<UploadResult> {
if (options.maxSizeBytes && typeof body !== 'string' && !(body instanceof ReadableStream)) {
const size = body.byteLength;
if (size > options.maxSizeBytes) {
throw new Error(
`File size ${size} bytes exceeds maximum allowed ${options.maxSizeBytes} bytes`
);
}
}
const input: PutObjectCommandInput = {
Bucket: this.bucket.name,
Key: key,
@ -95,6 +105,15 @@ export class StorageClient {
body: Buffer | Uint8Array | ReadableStream,
options: UploadOptions = {}
): Promise<UploadResult> {
if (options.maxSizeBytes && !(body instanceof ReadableStream)) {
const size = body.byteLength;
if (size > options.maxSizeBytes) {
throw new Error(
`File size ${size} bytes exceeds maximum allowed ${options.maxSizeBytes} bytes`
);
}
}
const upload = new Upload({
client: this.client,
params: {
@ -173,6 +192,27 @@ export class StorageClient {
await this.client.send(command);
}
/**
* Delete multiple files from the bucket in a single request.
* S3 supports up to 1000 keys per request; this method batches automatically.
*/
async deleteMany(keys: string[]): Promise<void> {
if (keys.length === 0) return;
const BATCH_SIZE = 1000;
for (let i = 0; i < keys.length; i += BATCH_SIZE) {
const batch = keys.slice(i, i + BATCH_SIZE);
const command = new DeleteObjectsCommand({
Bucket: this.bucket.name,
Delete: {
Objects: batch.map((key) => ({ Key: key })),
Quiet: true,
},
});
await this.client.send(command);
}
}
/**
* Check if a file exists
*/