upload/store/AWSS3Store.js

/**
 * @module flitter-upload/store/AWSS3Store
 */

const Store = require('./Store')
const S3 = require('aws-sdk/clients/s3')
const fs = require('fs').promises
const uuid = require('uuid/v4')
const tempfile = require('tempfile')

/**
 * Amazon S3-backed file store provider.
 * @extends module:flitter-upload/store/Store~Store
 */
class AWSS3Store extends Store {
    /**
     * The S3 client.
     */
    #s3

    /**
     * Defines the services required by this store.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'utility', 'output', 'models', 'configs']
    }

    /**
     * Initializes the store.
     * @returns {Promise<void>}
     */
    async init() {
        this.#s3 = new S3({
            region: this.config.region,
            ...(this.config.aws.endpoint ? { endpoint: this.config.aws.endpoint } : {}),
        })
    }

    /**
     * Permanently store a temporarily uploaded file in this store.
     * @param {object} params
     * @param {string} params.temp_path - absolute path to the temporarily uploaded file
     * @param {string} params.original_name - the original upload name of the file
     * @param {string} params.mime_type - the MIME type of the file.
     * @returns {Promise<module:flitter-upload/model/File~File>}
     */
    async store({ temp_path, original_name, mime_type, tag = '' }) {
        const File = this.models.get('upload::File')
        const upload_name = uuid()
        const upload_key = this.upload_key(upload_name, tag)

        await this.upload_file_as_key(temp_path, upload_key)

        const f = new File({
            original_name,
            upload_name,
            mime_type,
            store: this.config.name,
            store_id: upload_name,
            tag,
        })

        await f.save()
        return f
    }

    /**
     * Send the specified file as the data for the response.
     * Sets the appropriate Content-Type and Content-Disposition headers.
     * @param {module:flitter-upload/model/File~File} file - the file to send
     * @param {express/response} response - the response
     * @returns {Promise<void>}
     */
    async send_file(file, response) {
        const file_path = tempfile()
        await this.download_file(file, file_path)

        response.setHeader('Content-Type', file.mime_type)
        response.setHeader('Content-Disposition', `inline; filename="${file.original_name}";`)
        response.sendFile(file_path)
    }

    /**
     * Download the file locally to the given destination.
     * @param {module:flitter-upload/model/File~File} file
     * @param destination
     * @return {Promise<void>}
     */
    async download_file(file, destination) {
        const params = {
            Bucket: this.config.bucket,
            Key: this.upload_key(file.store_id, file.tag)
        }

        const data = await new Promise((res, rej) => {
            this.#s3.getObject(params, async (err, data) => {
                if ( err ) return rej(err)
                else return res(await data)
            })
        })

        await fs.writeFile(destination, data.Body)
    }

    /**
     * Create a Readable stream for this file.
     * @param {module:flitter-upload/model/File~File} file
     * @return {ReadStream}
     */
    read_stream(file) {
        const params = {
            Bucket: this.config.bucket,
            Key: this.upload_key(file.store_id, file.tag)
        }

        return this.#s3.getObject(params).createReadStream()
    }

    /**
     * Upload the file path to the S3 key.
     * @param {string} file_path - the local file path
     * @param {string} key_path - the S3 key
     * @return {Promise<unknown>}
     */
    async upload_file_as_key(file_path, key_path) {
        const params = {
            Bucket: this.config.bucket,
            Key: key_path,
            Body: require('fs').createReadStream(file_path)
        }

        return new Promise((res, rej) => {
            this.#s3.upload(params, (err, data) => {
                if ( err ) {
                    return rej(err)
                } else {
                    return res(data)
                }
            })
        })
    }

    /**
     * Given an upload UUID and some tag, returns the full S3 upload key of the file.
     * @param {string} upload_uuid
     * @param {string} [tag]
     * @return {string}
     */
    upload_key(upload_uuid, tag = undefined) {
        let upload_path = upload_uuid

        if ( tag ) {
            upload_path = `${tag}/${upload_path}`
        }

        if ( this.config.prefix ) {
            upload_path = `${this.config.prefix}/${upload_path}`
        }

        return upload_path
    }
}

module.exports = exports = AWSS3Store