// Heavily inspired by https://github.com/stemmlerjs/apollo-cloud-file-uploads import stream from 'stream'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; import { DataSource } from 'apollo-datasource'; import AWS from 'aws-sdk'; import { File, S3UploadStream, UploadedFileResponse } from './types'; // in memory check if bucket exists const bucketExists = new Set(); export default class AWSS3Uploader extends DataSource { private s3!: AWS.S3; constructor() { if ( !process.env.AWS_S3_UPLOAD_ACCESS_KEY_ID || !process.env.AWS_S3_UPLOAD_SECRET_ACCESS_KEY ) { throw new Error( 'Cannot start AWS without both AWS_S3_UPLOAD_ACCESS_KEY_ID and AWS_S3_UPLOAD_SECRET_ACCESS_KEY in environment' ); } super(); this.s3 = new AWS.S3({ region: process.env.AWS_S3_UPLOAD_REGION || 'us-east-2', accessKeyId: process.env.AWS_S3_UPLOAD_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_S3_UPLOAD_SECRET_ACCESS_KEY, }); } /** * Finds the bucket requests it or attempts to create it if possible */ async ensureBucketExistsOrFail(bucketName: string): Promise { if (bucketExists.has(bucketName)) { return; } try { await this.s3.headBucket({ Bucket: bucketName }).promise(); bucketExists.add(bucketName); } catch (err: any) { if (err.statusCode === 403) { bucketExists.delete(bucketName); throw new Error(`AWS Bucket ${bucketName} Access Denied`); } if (err.statusCode >= 400 && err.statusCode < 500) { await this.s3.createBucket({ Bucket: bucketName }).promise(); bucketExists.add(bucketName); return; } bucketExists.delete(bucketExists); } } private createUploadSream( bucketName: string, key: string, contentType: string ): S3UploadStream { const pass = new stream.PassThrough(); return { writeStream: pass, promise: this.s3 .upload({ Bucket: bucketName, Key: key, Body: pass, ContentType: contentType, ACL: 'public-read', }) .promise(), }; } private createDestinationFilePath(file: File): string { return `${file.filename}-${uuidv4()}${path.extname(file.filename)}`; } async uploadSingleFileOrFail({ file, bucketName, }: { bucketName?: string; file: File; }): Promise { return new Promise((resolve, reject) => { if (!process.env.AWS_S3_UPLOAD_ENABLED) { reject(new Error('AWS Upload is disabled')); } const { filename, mimetype, encoding } = file; const filePath = this.createDestinationFilePath(file); if (!process.env.AWS_S3_FILE_UPLOAD_BUCKET_NAME && !bucketName) { throw new Error( 'Must sepcify either bucketName or AWS_S3_FILE_UPLOAD_BUCKET_NAME in environment' ); } const bucket = bucketName ?? process.env.AWS_S3_FILE_UPLOAD_BUCKET_NAME!; this.ensureBucketExistsOrFail(bucket) .catch(reject) .then(() => { const uploadStream = this.createUploadSream( bucket, filePath, mimetype ); const stream = file.createReadStream(); stream.on('error', (err: Error) => { reject(err); }); stream.pipe(uploadStream.writeStream); uploadStream.promise .then((result) => { resolve({ filename, mimetype, encoding, url: result.Location }); }) .catch(resolve); }); }); } }