All files / services imageStorageService.ts

62.5% Statements 15/24
100% Branches 1/1
66.66% Functions 2/3
62.5% Lines 15/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 712x         2x 2x 2x   2x   2x   2x         2x 2x               2x         2x 2x             2x       2x 2x                                                  
import {
  S3Client,
  PutObjectCommand,
  DeleteObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { config } from '../config/env';
import { InternalError } from '../common/errors';
import { IImageStorageService } from './imageStorageService.interface';
import { getLogger } from '../common/logger';
 
const logger = getLogger('ImageStorageService');
 
export class ImageStorageService implements IImageStorageService {
  private s3Client: S3Client;
  private bucketName: string;
 
  constructor() {
    this.s3Client = new S3Client({ region: config.awsRegion });
    this.bucketName = config.s3BucketName;
  }
 
  async getPresignedUploadUrl(
    imageId: string,
    contentType: string,
    expiresIn = 300
  ): Promise<string> {
    logger.info('getPresignedUploadUrl called', {
      imageId,
      contentType,
      expiresIn,
    });
    try {
      const command = new PutObjectCommand({
        Bucket: this.bucketName,
        Key: imageId,
        // Don't include ContentType here - let client set it during upload
        // This avoids signature mismatch if client doesn't send exact header
      });
 
      const signedUrl = await getSignedUrl(this.s3Client, command, {
        expiresIn,
      });
 
      logger.info(`Presigned URL generated for: ${imageId}`);
      return signedUrl;
    } catch (error) {
      logger.error(
        `S3 presigned URL generation error: ${(error as Error).message}`
      );
      throw new InternalError('Failed to generate presigned upload URL');
    }
  }
 
  async deleteImage(imageId: string): Promise<void> {
    logger.info('deleteImage called', { imageId });
    try {
      const command = new DeleteObjectCommand({
        Bucket: this.bucketName,
        Key: imageId,
      });
 
      await this.s3Client.send(command);
      logger.info(`Image deleted from S3: ${imageId}`);
    } catch (error) {
      logger.error(`S3 delete error: ${(error as Error).message}`);
      throw new InternalError('Failed to delete image from storage');
    }
  }
}