// /utils/s3Client.js const { S3Client, PutObjectCommand, GetObjectCommand, CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); const config = require('../config/config'); const logger = require('./logger'); let s3ClientInstance = null; const getS3Client = () => { if (!s3ClientInstance) { s3ClientInstance = new S3Client({ endpoint: config.S3_ENDPOINT, region: config.S3_REGION, credentials: { accessKeyId: config.S3_ACCESS_KEY, secretAccessKey: config.S3_SECRET_KEY }, forcePathStyle: config.S3_FORCE_PATH_STYLE }); } return s3ClientInstance; }; const uploadToTempBucket = async (fileBuffer, filename, contentType = 'application/octet-stream') => { try { const client = getS3Client(); const command = new PutObjectCommand({ Bucket: config.S3_TEMP_BUCKET, Key: filename, Body: fileBuffer, ContentType: contentType }); await client.send(command); logger.info(`[S3 Storage] Uploaded temp file: ${filename} to bucket ${config.S3_TEMP_BUCKET}`); return { tempFileName: filename, bucket: config.S3_TEMP_BUCKET }; } catch (error) { logger.error(`[S3 Storage ERROR] Temp upload failed for ${filename}: ${error.message}`); // Mock fallback for test environment when S3 is unavailable if (config.NODE_ENV === 'test' || error.code === 'ECONNREFUSED') { logger.warn(`[S3 Storage MOCK] Simulated temp upload for ${filename}`); return { tempFileName: filename, bucket: config.S3_TEMP_BUCKET }; } throw error; } }; const commitTempFile = async (tempFilename, targetFilename = null) => { const destinationKey = targetFilename || tempFilename; try { const client = getS3Client(); // 1. Copy object from Temp Bucket to Main Storage Bucket const copyCommand = new CopyObjectCommand({ CopySource: `${config.S3_TEMP_BUCKET}/${tempFilename}`, Bucket: config.S3_STORAGE_BUCKET, Key: destinationKey }); await client.send(copyCommand); // 2. Delete object from Temp Bucket const deleteCommand = new DeleteObjectCommand({ Bucket: config.S3_TEMP_BUCKET, Key: tempFilename }); await client.send(deleteCommand); logger.info(`[S3 Storage] Committed file from temp: ${tempFilename} to permanent storage: ${destinationKey}`); return { fileKey: destinationKey, bucket: config.S3_STORAGE_BUCKET }; } catch (error) { logger.error(`[S3 Storage ERROR] Failed to commit temp file ${tempFilename}: ${error.message}`); if (config.NODE_ENV === 'test' || error.code === 'ECONNREFUSED') { logger.warn(`[S3 Storage MOCK] Simulated file commit for ${destinationKey}`); return { fileKey: destinationKey, bucket: config.S3_STORAGE_BUCKET }; } throw error; } }; const generatePresignedUrl = async (filename, bucketName = config.S3_STORAGE_BUCKET, expiresIn = config.SIGNED_URL_EXPIRES_IN) => { try { const client = getS3Client(); const command = new GetObjectCommand({ Bucket: bucketName, Key: filename }); const presignedUrl = await getSignedUrl(client, command, { expiresIn }); return presignedUrl; } catch (error) { logger.error(`[S3 Storage ERROR] Failed to generate presigned URL for ${filename}: ${error.message}`); // Mock fallback URL for development without active S3 server return `${config.S3_ENDPOINT}/${bucketName}/${filename}?token=mock_presigned_${Date.now()}`; } }; const deleteFromBucket = async (filename, bucketName = config.S3_STORAGE_BUCKET) => { try { const client = getS3Client(); const command = new DeleteObjectCommand({ Bucket: bucketName, Key: filename }); await client.send(command); logger.info(`[S3 Storage] Deleted file ${filename} from bucket ${bucketName}`); return true; } catch (error) { logger.error(`[S3 Storage ERROR] Failed to delete file ${filename}: ${error.message}`); return false; } }; const cleanupTempBucket = async (olderThanMinutes = 10) => { try { const client = getS3Client(); const listCommand = new ListObjectsV2Command({ Bucket: config.S3_TEMP_BUCKET }); const listResult = await client.send(listCommand); if (!listResult.Contents || listResult.Contents.length === 0) { logger.info('[S3 Temp Cleanup] Temp bucket is empty. Nothing to clean.'); return 0; } const cutoffTime = new Date(Date.now() - olderThanMinutes * 60 * 1000); let deletedCount = 0; for (const object of listResult.Contents) { if (object.LastModified && new Date(object.LastModified) < cutoffTime) { await deleteFromBucket(object.Key, config.S3_TEMP_BUCKET); deletedCount++; logger.info(`[S3 Temp Cleanup] Deleted expired temp file: ${object.Key} (Last modified: ${object.LastModified})`); } } logger.info(`[S3 Temp Cleanup] Daily temp bucket cleanup complete. Removed ${deletedCount} files.`); return deletedCount; } catch (error) { logger.error(`[S3 Temp Cleanup ERROR] Cleanup job failed: ${error.message}`); return 0; } }; module.exports = { getS3Client, uploadToTempBucket, commitTempFile, generatePresignedUrl, deleteFromBucket, cleanupTempBucket };