feat(storage): add deleteByPrefix, copy, getMetadata and prod lifecycle rules

- Add deleteByPrefix(prefix) for bulk user data deletion (account cleanup)
- Add copy(sourceKey, destKey) via CopyObjectCommand for file duplication
- Add getMetadata(key) via HeadObjectCommand for content-type/size/metadata
- Add FileMetadata type for structured metadata responses
- Add minio-init container to docker-compose.macmini.yml with bucket creation,
  public access policies, and lifecycle rules (matching dev compose)
- 96 tests passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-20 19:40:17 +01:00
parent b0e5a9c5ff
commit 152fa5fe08
5 changed files with 191 additions and 0 deletions

View file

@ -6,6 +6,7 @@ import {
DeleteObjectsCommand,
ListObjectsV2Command,
HeadObjectCommand,
CopyObjectCommand,
CreateMultipartUploadCommand,
UploadPartCommand,
CompleteMultipartUploadCommand,
@ -24,6 +25,7 @@ import type {
MultipartUploadPart,
UploadResult,
FileInfo,
FileMetadata,
} from './types';
/**
@ -370,6 +372,59 @@ export class StorageClient {
return this.bucket.name;
}
/**
* Delete all files matching a prefix.
* Useful for account deletion (e.g., deleteByPrefix('users/user-123/'))
*/
async deleteByPrefix(prefix: string): Promise<number> {
const files = await this.list(prefix);
if (files.length === 0) return 0;
const keys = files.map((f) => f.key);
await this.deleteMany(keys);
return keys.length;
}
/**
* Copy a file within the same bucket.
* For move operations, call copy() then delete() the source.
*/
async copy(sourceKey: string, destKey: string): Promise<UploadResult> {
const command = new CopyObjectCommand({
Bucket: this.bucket.name,
CopySource: `${this.bucket.name}/${sourceKey}`,
Key: destKey,
});
const result = await this.client.send(command);
return {
key: destKey,
url: this.getPublicUrl(destKey),
etag: result.CopyObjectResult?.ETag,
};
}
/**
* Get file metadata without downloading the file.
*/
async getMetadata(key: string): Promise<FileMetadata> {
const command = new HeadObjectCommand({
Bucket: this.bucket.name,
Key: key,
});
const response = await this.client.send(command);
return {
contentType: response.ContentType,
size: response.ContentLength ?? 0,
lastModified: response.LastModified,
etag: response.ETag,
metadata: response.Metadata,
};
}
// ── Presigned Multipart Upload (browser direct-upload) ──────────────
/**