import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import * as Minio from "minio"; import type { IStorageService } from "../common/interfaces/storage.interface"; @Injectable() export class S3Service implements OnModuleInit, IStorageService { private readonly logger = new Logger(S3Service.name); private minioClient: Minio.Client; private readonly bucketName: string; constructor(private readonly configService: ConfigService) { this.minioClient = new Minio.Client({ endPoint: this.configService.get("S3_ENDPOINT", "localhost"), port: Number.parseInt(this.configService.get("S3_PORT", "9000"), 10), useSSL: this.configService.get("S3_USE_SSL") === "true", accessKey: this.configService.get("S3_ACCESS_KEY", "minioadmin"), secretKey: this.configService.get("S3_SECRET_KEY", "minioadmin"), }); this.bucketName = this.configService.get( "S3_BUCKET_NAME", "memegoat", ); } async onModuleInit() { await this.ensureBucketExists(this.bucketName); } async ensureBucketExists(bucketName: string) { try { const exists = await this.minioClient.bucketExists(bucketName); if (!exists) { await this.minioClient.makeBucket(bucketName); this.logger.log(`Bucket "${bucketName}" created successfully.`); } } catch (error) { this.logger.error( `Error checking/creating bucket "${bucketName}": ${error.message}`, ); throw error; } } async uploadFile( fileName: string, file: Buffer, mimeType: string, metaData: Minio.ItemBucketMetadata = {}, bucketName: string = this.bucketName, ) { try { await this.minioClient.putObject(bucketName, fileName, file, file.length, { ...metaData, "Content-Type": mimeType, }); return fileName; } catch (error) { this.logger.error(`Error uploading file to ${bucketName}: ${error.message}`); throw error; } } async getFile(fileName: string, bucketName: string = this.bucketName) { try { return await this.minioClient.getObject(bucketName, fileName); } catch (error) { this.logger.error(`Error getting file from ${bucketName}: ${error.message}`); throw error; } } async getFileUrl( fileName: string, expiry = 3600, bucketName: string = this.bucketName, ) { try { return await this.minioClient.presignedUrl( "GET", bucketName, fileName, expiry, ); } catch (error) { this.logger.error( `Error getting file URL from ${bucketName}: ${error.message}`, ); throw error; } } async getUploadUrl( fileName: string, expiry = 3600, bucketName: string = this.bucketName, ) { try { return await this.minioClient.presignedUrl( "PUT", bucketName, fileName, expiry, ); } catch (error) { this.logger.error( `Error getting upload URL for ${bucketName}: ${error.message}`, ); throw error; } } async deleteFile(fileName: string, bucketName: string = this.bucketName) { try { await this.minioClient.removeObject(bucketName, fileName); } catch (error) { this.logger.error( `Error deleting file from ${bucketName}: ${error.message}`, ); throw error; } } async getFileInfo(fileName: string, bucketName: string = this.bucketName) { try { return await this.minioClient.statObject(bucketName, fileName); } catch (error) { this.logger.error( `Error getting file info from ${bucketName}: ${error.message}`, ); throw error; } } async moveFile( sourceFileName: string, destinationFileName: string, sourceBucketName: string = this.bucketName, destinationBucketName: string = this.bucketName, ) { try { const conds = new Minio.CopyConditions(); await this.minioClient.copyObject( destinationBucketName, destinationFileName, `/${sourceBucketName}/${sourceFileName}`, conds, ); await this.minioClient.removeObject(sourceBucketName, sourceFileName); return destinationFileName; } catch (error) { this.logger.error( `Error moving file from ${sourceBucketName}/${sourceFileName} to ${destinationBucketName}/${destinationFileName}: ${error.message}`, ); throw error; } } }