upload/UploadUnit.js

/**
 * @module flitter-upload/UploadUnit
 */

const Unit = require('libflitter/Unit')
const path = require('path')
const fs = require('fs')
const uuid = require('uuid/v4')
const ncp = require('ncp')

/**
 * Unit representing the flitter-upload package.
 * When run, binds the helper functions and destination.
 * @extends {module:libflitter/Unit~Unit}
 */
class UploadUnit extends Unit {
    
    /**
     * Initializes the class.
     * Resolves the path to which files should be uploaded.
     * @param {string} [upload_directory = './uploads'] - Path where files should be uploaded to.
     */
    constructor(upload_directory = './uploads'){
        super()

        /**
         * Path to which files should be uploaded.
         * 
         * @name UploadUnit#directory
         * @type {string}
         */
        this.directory = path.resolve(upload_directory)
    }
    
    /**
     * Loads the unit.
     * Binds the handler functions to the context and global accessors.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app
     * @param {module:libflitter/Context~Context} context
     * @returns {Promise<void>}
     */
    async go(app, context){
        
        context.bind('mw', this.upload)
        context.bind('serve', this.serve)
        context.bind('model', app.d.database.model('upload:File'))
        context.bind('remove', this.delete)
        context.bind('destination', this.directory)
        
        app.global.bind('upload', this.upload)
        app.global.bind('upload_destination', this.directory)
        
    }
    
    /**
     * Get the Express middleware to upload the file in the specified input name.
     * File is saved with a UUID name in {@link module:flitter-upload/UploadUnit~UploadUnit#directory}.
     * 
     * Optionally, a type may be specified. These are strings used to categorize
     * the files so they can be used in groups. They have no effect on functionality.
     * 
     * @param {string} file - name of the input field containing the file
     * @param {string} [type = 'none'] - type to flag the file with
     * @returns {Function} - Express-compatible middleware
     */
    upload(file, type = 'none'){
        
        /*
         * Return an Express handler function.
         */
        return async (req, res, next) => {
            
            /*
             * If the file is provided in the
             * request, save it with a UUID name.
             */
            if ( file in req.files ) {
                const dir = _flitter.upload_destination
                const file_name = uuid()
                
                const destination = path.resolve(dir, file_name)

                /*
                 * Write the file with the uploaded contents.
                 */
                await fs.promises.writeFile(destination, req.files[file].data)

                /*
                 * Add the destination to the file object
                 * in the request so that later handlers can
                 * access it.
                 */
                const file_object = new _flit.uploads.model({
                    original_name: req.files[file].name,
                    new_name: file_name,
                    mime: req.files[file].mimetype,
                    type: type,
                })

                _flitter.log("Saving uploaded file.", 4)
                await file_object.save()
                req.files[file] = file_object
                next()
            }
            
            /*
             * If the file is not provided in the
             * request, just call the next handler
             * in the stack.
             */
            else {
                next()
            }
        }
    }

    /**
     * Sends the file with the specified UUID as a response to the user.
     * 
     * @param {Response} res - the Express response
     * @param {module:flitter-upload/deploy/File} file - the File model instance
     */
    serve(res, file){
        const file_path = path.resolve(_flitter.upload_destination, file.new_name)
        
        res.type(file.mime)
        res.sendFile(file_path)
    }

    /**
     * Deletes an uploaded file and removes its model instance.
     * 
     * @param {module:flitter-upload/deploy/File} file - the File model instance
     * @returns {Promise<void>}
     */
    async delete(file){
        const file_path = path.resolve(_flitter.upload_destination, file.new_name)
        
        await fs.promises.unlink(file_path)
        
        await _flitter.model('upload:File').deleteOne({new_name: file.new_name}).exec()
    }

    /**
     * Deploys the resources for flitter-upload.
     * Specifically, the File model which is used to track uploaded files.
     * 
     * @returns {Promise<void>}
     */
    async deploy(){
        const package_dir = __dirname
        const base_dir = _flitter.root
        _flitter.log("Uploads deploy from: "+package_dir, 2)
        _flitter.log("To: "+path.dirname(require.main.filename), 2)
        
        function do_copy(from, to){
            return new Promise(
                (resolve, reject) => {
                    ncp(from, to, (error) => {
                        if ( error ) reject(error)

                        resolve()
                    })
                }
            )
        }

        await do_copy(path.resolve(package_dir+'/deploy'), path.resolve(base_dir+'/app/models/upload'))
    }

    /**
     * Get the name of the unit.
     * 
     * @returns {string} "upload"
     */
    name(){
        return "upload"
    }
}

module.exports = exports = UploadUnit