import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { randomUUID } from 'crypto'; import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3'; import { StorageProvider } from './storage-provider'; /// S3-compatible object storage (AWS S3, MinIO, etc.). @Injectable() export class S3StorageProvider implements StorageProvider { private readonly client: S3Client; private readonly bucket: string; constructor(config: ConfigService) { this.bucket = config.getOrThrow('S3_BUCKET'); this.client = new S3Client({ region: config.get('S3_REGION') ?? 'auto', endpoint: config.get('S3_ENDPOINT'), forcePathStyle: config.get('S3_FORCE_PATH_STYLE') === 'true', credentials: { accessKeyId: config.getOrThrow('S3_ACCESS_KEY_ID'), secretAccessKey: config.getOrThrow('S3_SECRET_ACCESS_KEY'), }, }); } async upload(kcId: string, filename: string, data: Buffer): Promise { const storageKey = `${kcId}/${randomUUID()}-${filename}`; await this.client.send( new PutObjectCommand({ Bucket: this.bucket, Key: storageKey, Body: data }), ); return storageKey; } async download(storageKey: string): Promise { const result = await this.client.send( new GetObjectCommand({ Bucket: this.bucket, Key: storageKey }), ); const chunks: Uint8Array[] = []; for await (const chunk of result.Body as AsyncIterable) { chunks.push(chunk); } return Buffer.concat(chunks); } async delete(storageKey: string): Promise { await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: storageKey })); } }