import { StorageProvider } from '@/lib/storage/storage.interface'; import { TypedResult, wrap } from '@/utils/types/results'; import { DeleteObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { z } from 'zod'; /** * Configuration for S3 storage adapter */ export const S3StorageConfig = z.object({ endpoint: z.string(), bucket: z.string(), region: z.string(), accessKey: z.string(), secretKey: z.string(), }); export type S3StorageConfig = z.infer; /** * AWS S3 storage adapter * Uploads files to S3 and returns public URLs */ export class S3StorageAdapter implements StorageProvider { private readonly s3Client: S3Client; private readonly endpoint: string; private readonly bucketName: string; readonly get: ( ...args: Parameters ) => Promise>; readonly put: ( ...args: Parameters ) => Promise>; readonly exists: ( ...args: Parameters ) => Promise; readonly delete: ( ...args: Parameters ) => Promise>; constructor(config: S3StorageConfig, s3Client?: S3Client) { this.endpoint = config.endpoint; this.bucketName = config.bucket; this.s3Client = s3Client || new S3Client({ endpoint: config.endpoint, region: config.region, forcePathStyle: true, credentials: { accessKeyId: config.accessKey, secretAccessKey: config.secretKey, }, }); this.get = wrap(this._get.bind(this)); this.put = wrap(this._put.bind(this)); this.delete = wrap(this._delete.bind(this)); this.exists = this._exists.bind(this); } private async _get(key: string): Promise { return `${this.endpoint}/${this.bucketName}/${key}`; } private async _put(key: string, contentType: string): Promise { const command = new PutObjectCommand({ Bucket: this.bucketName, Key: key, ContentType: contentType, }); return await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); } private async _exists(key: string): Promise { try { await this.s3Client.send( new HeadObjectCommand({ Bucket: this.bucketName, Key: key, }) ); return true; } catch { return false; } } private async _delete(key: string): Promise { await this.s3Client.send( new DeleteObjectCommand({ Bucket: this.bucketName, Key: key, }) ); } } export const new_s3_storage_adapter = (): S3StorageAdapter => { const config = S3StorageConfig.parse({ endpoint: process.env.S3_ENDPOINT, bucket: process.env.S3_BUCKET_NAME, region: process.env.S3_REGION, accessKey: process.env.S3_ACCESS_KEY, secretKey: process.env.S3_SECRET_KEY, }); return new S3StorageAdapter(config); };