/*! * @license * Copyright Squiz Australia Pty Ltd. All Rights Reserved. */ import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, HeadObjectCommandOutput, PutObjectCommand, PutObjectCommandInput, S3Client, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { inject, injectable } from 'inversify'; import 'reflect-metadata'; /** * Repository class for interacting with S3. * @export * @class S3RepositoryBase */ @injectable() export class S3RepositoryBase { protected bucketName: string; public constructor( @inject(S3Client) protected s3Client: S3Client, bucketName: string, ) { this.bucketName = bucketName; } /** * Validates S3 key to prevent malicious input * @param {string} key The S3 object key to validate * @private */ private validateS3Key(key: string): void { if (!key || typeof key !== 'string') { throw new Error('Invalid S3 key: must be a non-empty string'); } if (key.includes('..') || key.startsWith('/')) { throw new Error('Invalid S3 key: contains suspicious patterns'); } } /** * Save object into S3. * @param {string} key The S3 object key. * @param {PutObjectCommandInputType['Body']} body The body of the S3 object. * @return {Promise} * @memberof S3RepositoryBase */ async put(key: string, body: PutObjectCommandInput['Body']): Promise { this.validateS3Key(key); await this.s3Client.send( new PutObjectCommand({ Bucket: this.bucketName, Key: key, Body: body, }), ); } /** * Delete object from S3. * * @param {string} key The S3 object key. * @memberof S3RepositoryBase */ public async delete(key: string): Promise { this.validateS3Key(key); await this.s3Client.send( new DeleteObjectCommand({ Bucket: this.bucketName, Key: key, }), ); } /** * Retrieve object from S3. * @param {string} key The S3 object key. * @return {Promise} The S3 object. * @memberof S3RepositoryBase */ async get(key: string): Promise { this.validateS3Key(key); const s3Object = await this.s3Client.send( new GetObjectCommand({ Bucket: this.bucketName, Key: key, }), ); return s3Object.Body?.transformToString() || null; } /** * Check if the S3 object key exists. * @param {string} key The S3 object key. * @return {Promise} * @memberof S3RepositoryBase */ async check(key: string): Promise { this.validateS3Key(key); return this.s3Client.send( new HeadObjectCommand({ Bucket: this.bucketName, Key: key, }), ); } async getS3SignedUrl(key: string): Promise { this.validateS3Key(key); const command = new GetObjectCommand({ Bucket: this.bucketName, Key: key, }); const res = await getSignedUrl(this.s3Client, command, { // In some instances ECS tasks can take 30 minutes from PROVISIONING -> RUNNING. // https://squizgroup.atlassian.net/browse/PLATFORM-2402 expiresIn: 3600, // 1 hour }); return res; } }