import { Readable } from "stream"; import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { Inject, Injectable, Logger } from "@nestjs/common"; import type { IS3Configs } from "src/config/s3.config"; import { S3_CONFIG } from "../constants"; import { FileType } from "../interfaces/file.interface"; import { S3UploadResult } from "../interfaces/s3.interface"; @Injectable() export class S3Service { private readonly logger = new Logger(S3Service.name); private readonly s3Client: S3Client; constructor(@Inject(S3_CONFIG) private readonly s3Configs: IS3Configs) { this.s3Client = new S3Client({ region: this.s3Configs.region, endpoint: this.s3Configs.endpoint, credentials: { accessKeyId: this.s3Configs.accessKeyId, secretAccessKey: this.s3Configs.secretAccessKey, }, }); } /** * Upload file to S3 */ async uploadFile(buffer: Buffer, key: string, contentType: string, metadata?: Record): Promise { try { const command = new PutObjectCommand({ Bucket: this.s3Configs.bucketName, Key: key, Body: buffer, ACL: "public-read", ContentType: contentType, Metadata: metadata, }); await this.s3Client.send(command); const url = `${this.s3Configs.bucketUploadedUrl}/${key}`; this.logger.log(`File uploaded to S3: ${key}`); return { key, url, bucket: this.s3Configs.bucketName, }; } catch (error: unknown) { this.logger.error(`Failed to upload file to S3: ${(error as Error).message}`, (error as Error).stack); throw error; } } /** * Get file stream from S3 */ async getFileStream(key: string): Promise { try { const command = new GetObjectCommand({ Bucket: this.s3Configs.bucketName, Key: key, }); const response = await this.s3Client.send(command); return response.Body as Readable; } catch (error: unknown) { this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack); throw error; } } /** * Generate presigned URL for file download */ async getPresignedUrl(key: string, expiresIn: number = 3600): Promise { try { const command = new GetObjectCommand({ Bucket: this.s3Configs.bucketName, Key: key, }); return await getSignedUrl(this.s3Client, command, { expiresIn }); } catch (error: unknown) { this.logger.error(`Failed to generate presigned URL: ${(error as Error).message}`, (error as Error).stack); throw error; } } /** * Delete file from S3 */ async deleteFile(key: string): Promise { try { const command = new DeleteObjectCommand({ Bucket: this.s3Configs.bucketName, Key: key, }); await this.s3Client.send(command); this.logger.log(`File deleted from S3: ${key}`); } catch (error: unknown) { this.logger.error(`Failed to delete file from S3: ${(error as Error).message}`, (error as Error).stack); throw error; } } /** * Generate S3 key for file */ generateFileKey(originalName: string, fileType: FileType): string { const timestamp = Date.now(); const randomSuffix = Math.round(Math.random() * 1e9); const extension = originalName.split(".").pop(); const basePathMap: Record = { image: "images", video: "videos", audio: "audio", document: "documents", text: "texts", unknown: "unknowns", }; const basePath = basePathMap[fileType]; return `${basePath}/${timestamp}-${randomSuffix}.${extension}`; } }