OpenVeo server API for plugins

API Docs for: 7.0.0
Show:

File: lib/multipart/MultipartParser.js

'use strict';

/**
 * @module multipart
 */

var fs = require('fs');
var path = require('path');
var multer = require('multer');
var async = require('async');
var fileSystem = process.requireApi('lib/fileSystem.js');

/**
 * Defines a multipart parser to parse multipart requests.
 *
 * Use MultipartParser to get fields from multipart requests (including files).
 *
 * @example
 *
 *     // Get multipart parser
 *     var MultipartParser = require('@openveo/api').multipart.MultipartParser;
 *
 *     // Create a request parser expecting several files: files in photos "field" and a file in "videos" field
 *     var parser = new MultipartParser(request, [
 *       {
 *         name: 'photos',
 *         destinationPath: '/tmp/photos',
 *         maxCount: 2,
 *         unique: true
 *       },
 *       {
 *         name: 'videos',
 *         destinationPath: '/tmp/videos',
 *         maxCount: 1,
 *         unique: false
 *       }
 *     ], {
 *       fieldNameSize: 100,
 *       fieldSize: 1024,
 *       fields: Infinity,
 *       fileSize: Infinity,
 *       files: Infinity,
 *       parts: Infinity,
 *       headerPairs: 2000
 *     });
 *
 *     parser.parse(function(error) {
 *       if (error)
 *         console.log('Something went wrong when uploading');
 *       else
 *         console.log(request.files);
 *     });
 *
 * @class MultipartParser
 * @constructor
 * @param {Request} request HTTP Request containing a multipart body, it will be altered with parsing properties
 * @param {Array} fileFields A list of file field descriptors with:
 *   - {String} name The field name which contains the file
 *   - {String} destinationPath The destination directory where the file will be uploaded
 *   - {Number} [maxCount] The maximum number of files allowed for this field
 *   - {Boolean} [unique] true to generate unique file names for files corresponding to this field, false to generate a
 *     unique id only if a file with the same name already exists in the destination folder
 * @param {Object} [limits] Multipart limits configuration, for more information about
 * available limits see Multer documentation (https://www.npmjs.com/package/multer#limits).
 * @throws {TypeError} If request is not as expected
 */
function MultipartParser(request, fileFields, limits) {
  Object.defineProperties(this, {

    /**
     * The HTTP request containing a multipart body.
     *
     * @property request
     * @type Request
     * @final
     */
    request: {value: request},

    /**
     * The list of file field descriptors.
     *
     * @property fileFields
     * @type Array
     * @final
     */
    fileFields: {value: fileFields || []},

    /**
     * Multipart limits configuration.
     *
     * @property limits
     * @type Object
     * @final
     */
    limits: {value: limits},

    /**
     * Final paths of files detected in multipart body.
     *
     * @property detectedFilesPaths
     * @type Array
     * @final
     */
    detectedFilesPaths: {value: []}

  });

  if (!this.request)
    throw new TypeError('A MultipartParser needs a request');
}

module.exports = MultipartParser;

/**
 * Gets fields configuration by name.
 *
 * @method getField
 * @param {String} fieldName The name of the field containing files
 * @return {Object|Null} The field configuration
 */
MultipartParser.prototype.getField = function(fieldName) {
  for (var i = 0; i < this.fileFields.length; i++)
    if (this.fileFields[i].name === fieldName) return this.fileFields[i];

  return null;
};

/**
 * Builds final file name.
 *
 * It avoids collisions with existing files and sanitizes file name.
 *
 * @method getFileName
 * @async
 * @param {String} originalFileName The original file name
 * @param {String} fieldName The name of the field containing the file
 * @param {Function} callback The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 *   - **String** The computed file name
 */
MultipartParser.prototype.getFileName = function(originalFileName, fieldName, callback) {
  var extension = path.extname(originalFileName);
  var basename = path.basename(originalFileName, extension);
  var sanitizedFilename = basename.replace(/[^a-z0-9-]/gi, '-').replace(/-{2,}/g, '-').toLowerCase();
  var field = this.getField(fieldName);

  if (!field.destinationPath)
    return callback(new Error('No destination path found for field ' + fieldName));

  if (field.unique) {

    // File name must be unique
    // Add timestamp to the name of the file
    return callback(null, sanitizedFilename + '-' + Date.now() + extension);

  }

  // Test if file already exists
  fs.stat(path.join(field.destinationPath, sanitizedFilename + extension), function(error, stat) {
    var uploadedFileName;
    if (error) {
      if (error.code == 'ENOENT') {

        // File does not exist
        uploadedFileName = sanitizedFilename + extension;

      } else
        return callback(error);
    } else {

      // File already exists
      // Add timestamp to the name of the file
      uploadedFileName = sanitizedFilename + '-' + Date.now() + extension;

    }

    callback(null, uploadedFileName);
  });
};

/**
 * Parses multipart content of the request and performs uploads if any.
 *
 * @method parse
 * @async
 * @param {Function} callback The function to call when done
 *   - **Error** The error if an error occurred, null otherwise
 */
MultipartParser.prototype.parse = function(callback) {
  var self = this;

  // Multer does not remove partially parsed files when client aborts the request
  // Remove files being written to the disk when client aborts the request
  this.request.on('aborted', function() {
    var asyncFunctions = [];
    self.detectedFilesPaths.forEach(function(detectedFilePath) {
      asyncFunctions.push(function(callback) {
        fileSystem.rm(detectedFilePath, callback);
      });
    });

    async.parallel(asyncFunctions, function() {
      callback(new Error('Parsing aborted by client. Temporary files have been removed'));
    });
  });

  multer({
    storage: multer.diskStorage({
      destination: function(request, file, callback) {
        var field = self.getField(file.fieldname);

        if (field.destinationPath) {
          fileSystem.mkdir(field.destinationPath, function(error) {
            callback(error, field.destinationPath);
          });
        } else callback(new Error('No destination path found for field ' + file.fieldname));
      },
      filename: function(request, file, callback) {
        var field = self.getField(file.fieldname);

        self.getFileName(file.originalname, file.fieldname, function(error, fileName) {
          if (error) return callback(error);
          self.detectedFilesPaths.push(path.join(field.destinationPath, fileName));
          callback(null, fileName);
        });
      }
    }),
    limits: this.limits
  }).fields(this.fileFields)(this.request, null, function(error) {
    callback(error);
  });
};