import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { S3Client, PutObjectCommand, DeleteObjectCommand, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; @Injectable() export class RustfsStorageService { private readonly logger = new Logger(RustfsStorageService.name); private s3Client: S3Client; private bucket: string; private endpoint: string; constructor(private configService: ConfigService) { this.endpoint = this.configService.get('rustfsStorage.endpoint') || ''; const accessKeyId = this.configService.get('rustfsStorage.accessKey') || ''; const secretAccessKey = this.configService.get('rustfsStorage.secretKey') || ''; this.bucket = this.configService.get('rustfsStorage.bucket') || 'streamflix'; const region = this.configService.get('rustfsStorage.region') || 'us-east-1'; this.s3Client = new S3Client({ region, endpoint: this.endpoint, credentials: { accessKeyId, secretAccessKey, }, forcePathStyle: true, }); } /** * Get folder path based on file type */ private getFolderPath(fileType: string): string { const folderMap: Record = { IMAGE: 'images', DOCUMENT: 'documents', VIDEO: 'videos', }; return folderMap[fileType.toUpperCase()] || 'others'; } /** * Generate presigned upload URL using S3 */ async getPresignedUploadUrl(fileName: string, fileType?: string) { try { const folder = fileType ? this.getFolderPath(fileType) : 'others'; const timestamp = Date.now(); const filePath = `${folder}/${timestamp}-${fileName}`; const command = new PutObjectCommand({ Bucket: this.bucket, Key: filePath, }); const uploadUrl = await getSignedUrl(this.s3Client, command, { expiresIn: 3600, }); const fileUrl = `${this.endpoint}/${this.bucket}/${filePath}`; this.logger.log( `Generated presigned upload URL for: ${filePath}${fileType ? ` (type: ${fileType})` : ''}`, ); return { uploadUrl, fileUrl, filePath, expiresIn: 3600, ...(fileType && { fileType }), }; } catch (error) { this.logger.error('Failed to generate presigned upload URL', error); throw error; } } /** * Delete a file from RustFS Storage */ async deleteFile(filePath: string): Promise { try { const command = new DeleteObjectCommand({ Bucket: this.bucket, Key: filePath, }); const result = await this.s3Client.send(command); this.logger.log(`Successfully deleted file: ${filePath}`); return { success: true, deletedPath: filePath, result, }; } catch (error) { this.logger.error(`Failed to delete file: ${filePath}`, error); throw new NotFoundException('File not found or already deleted'); } } /** * Delete a file by URL */ async deleteFileByUrl(url: string): Promise { try { const filePath = this.extractFilePathFromUrl(url); if (!filePath) { throw new NotFoundException('Could not extract file path from URL'); } const result = await this.deleteFile(filePath); this.logger.log(`Successfully deleted file from URL: ${url}`); return { ...result, deletedUrl: url, }; } catch (error) { if (error instanceof NotFoundException) { throw error; } this.logger.error(`Failed to delete file by URL: ${url}`, error); throw new NotFoundException('File not found or already deleted'); } } /** * Extract file path from RustFS Storage URL */ private extractFilePathFromUrl(url: string): string | null { try { const urlObj = new URL(url); const pathParts = urlObj.pathname.split('/'); const bucketIndex = pathParts.indexOf(this.bucket); if (bucketIndex !== -1 && bucketIndex < pathParts.length - 1) { return pathParts.slice(bucketIndex + 1).join('/'); } return pathParts.slice(1).join('/'); } catch { return null; } } }