'use strict'; const fs = require('fs'); const path = require('path'); const { Readable } = require('stream'); // Parameters for safe file name parsing. const SAFE_FILE_NAME_REGEX = /[^\w-]/g; const MAX_EXTENSION_LENGTH = 3; // Parameters to generate unique temporary file names: const TEMP_COUNTER_MAX = 65536; const TEMP_PREFIX = 'tmp'; let tempCounter = 0; /** * Logs message to console if debug option set to true. * @param {Object} options - options object. * @param {string} msg - message to log. * @returns {boolean} - false if debug is off. */ const debugLog = (options, msg) => { const opts = options || {}; if (!opts.debug) return false; console.log(`Express-file-upload: ${msg}`); // eslint-disable-line return true; }; /** * Generates unique temporary file name. e.g. tmp-5000-156788789789. * @param {string} prefix - a prefix for generated unique file name. * @returns {string} */ const getTempFilename = (prefix = TEMP_PREFIX) => { tempCounter = tempCounter >= TEMP_COUNTER_MAX ? 1 : tempCounter + 1; return `${prefix}-${tempCounter}-${Date.now()}`; }; /** * isFunc: Checks if argument is a function. * @returns {boolean} - Returns true if argument is a function. */ const isFunc = func => func && func.constructor && func.call && func.apply ? true: false; /** * Set errorFunc to the same value as successFunc for callback mode. * @returns {Function} */ const errorFunc = (resolve, reject) => isFunc(reject) ? reject : resolve; /** * Return a callback function for promise resole/reject args. * @returns {Function} */ const promiseCallback = (resolve, reject) => { return err => err ? errorFunc(resolve, reject)(err) : resolve(); }; /** * Builds instance options from arguments objects(can't be arrow function). * @returns {Object} - result options. */ const buildOptions = function() { const result = {}; [...arguments].forEach(options => { if (!options || typeof options !== 'object') return; Object.keys(options).forEach(i => result[i] = options[i]); }); return result; }; /** * Builds request fields (using to build req.body and req.files) * @param {Object} instance - request object. * @param {string} field - field name. * @param {any} value - field value. * @returns {Object} */ const buildFields = (instance, field, value) => { // Do nothing if value is not set. if (value === null || value === undefined) return instance; instance = instance || {}; // Non-array fields if (!instance[field]) { instance[field] = value; return instance; } // Array fields if (instance[field] instanceof Array) { instance[field].push(value); } else { instance[field] = [instance[field], value]; } return instance; }; /** * Creates a folder for file specified in the path variable * @param {Object} fileUploadOptions * @param {string} filePath * @returns {boolean} */ const checkAndMakeDir = (fileUploadOptions, filePath) => { // Check upload options were set. if (!fileUploadOptions) return false; if (!fileUploadOptions.createParentPath) return false; // Check whether folder for the file exists. if (!filePath) return false; const parentPath = path.dirname(filePath); // Create folder if it doesn't exist. if (!fs.existsSync(parentPath)) fs.mkdirSync(parentPath, { recursive: true }); // Checks folder again and return a results. return fs.existsSync(parentPath); }; /** * Deletes a file. * @param {string} file - Path to the file to delete. * @param {Function} callback */ const deleteFile = (file, callback) => fs.unlink(file, callback); /** * Copy file via streams * @param {string} src - Path to the source file * @param {string} dst - Path to the destination file. */ const copyFile = (src, dst, callback) => { // cbCalled flag and runCb helps to run cb only once. let cbCalled = false; let runCb = (err) => { if (cbCalled) return; cbCalled = true; callback(err); }; // Create read stream let readable = fs.createReadStream(src); readable.on('error', runCb); // Create write stream let writable = fs.createWriteStream(dst); writable.on('error', (err)=>{ readable.destroy(); runCb(err); }); writable.on('close', () => runCb()); // Copy file via piping streams. readable.pipe(writable); }; /** * moveFile: moves the file from src to dst. * Firstly trying to rename the file if no luck copying it to dst and then deleteing src. * @param {string} src - Path to the source file * @param {string} dst - Path to the destination file. * @param {Function} callback - A callback function. */ const moveFile = (src, dst, callback) => fs.rename(src, dst, err => (err ? copyFile(src, dst, err => err ? callback(err) : deleteFile(src, callback)) : callback() )); /** * Save buffer data to a file. * @param {Buffer} buffer - buffer to save to a file. * @param {string} filePath - path to a file. */ const saveBufferToFile = (buffer, filePath, callback) => { if (!Buffer.isBuffer(buffer)) { return callback(new Error('buffer variable should be type of Buffer!')); } // Setup readable stream from buffer. let streamData = buffer; let readStream = Readable(); readStream._read = () => { readStream.push(streamData); streamData = null; }; // Setup file system writable stream. let fstream = fs.createWriteStream(filePath); fstream.on('error', err => callback(err)); fstream.on('close', () => callback()); // Copy file via piping streams. readStream.pipe(fstream); }; /** * Decodes uriEncoded file names. * @param fileName {String} - file name to decode. * @returns {String} */ const uriDecodeFileName = (opts, fileName) => { return opts.uriDecodeFileNames ? decodeURIComponent(fileName) : fileName; }; /** * Parses filename and extension and returns object {name, extension}. * @param {boolean|integer} preserveExtension - true/false or number of characters for extension. * @param {string} fileName - file name to parse. * @returns {Object} - { name, extension }. */ const parseFileNameExtension = (preserveExtension, fileName) => { const preserveExtensionLengh = parseInt(preserveExtension); const result = {name: fileName, extension: ''}; if (!preserveExtension && preserveExtensionLengh !== 0) return result; // Define maximum extension length const maxExtLength = isNaN(preserveExtensionLengh) ? MAX_EXTENSION_LENGTH : Math.abs(preserveExtensionLengh); const nameParts = fileName.split('.'); if (nameParts.length < 2) return result; let extension = nameParts.pop(); if ( extension.length > maxExtLength && maxExtLength > 0 ) { nameParts[nameParts.length - 1] += '.' + extension.substr(0, extension.length - maxExtLength); extension = extension.substr(-maxExtLength); } result.extension = maxExtLength ? extension : ''; result.name = nameParts.join('.'); return result; }; /** * Parse file name and extension. * @param {Object} opts - middleware options. * @param {string} fileName - Uploaded file name. * @returns {string} */ const parseFileName = (opts, fileName) => { // Check fileName argument if (!fileName || typeof fileName !== 'string') return getTempFilename(); // Cut off file name if it's lenght more then 255. let parsedName = fileName.length <= 255 ? fileName : fileName.substr(0, 255); // Decode file name if uriDecodeFileNames option set true. parsedName = uriDecodeFileName(opts, parsedName); // Stop parsing file name if safeFileNames options hasn't been set. if (!opts.safeFileNames) return parsedName; // Set regular expression for the file name. const nameRegex = typeof opts.safeFileNames === 'object' && opts.safeFileNames instanceof RegExp ? opts.safeFileNames : SAFE_FILE_NAME_REGEX; // Parse file name extension. let {name, extension} = parseFileNameExtension(opts.preserveExtension, parsedName); if (extension.length) extension = '.' + extension.replace(nameRegex, ''); return name.replace(nameRegex, '').concat(extension); }; module.exports = { isFunc, debugLog, copyFile, // For testing purpose. moveFile, errorFunc, deleteFile, // For testing purpose. buildFields, buildOptions, parseFileName, getTempFilename, promiseCallback, checkAndMakeDir, saveBufferToFile, uriDecodeFileName };