'use strict'; const fs = require('fs'); const path = require('path'); const Readable = require('stream').Readable; // Parameters for safe file name parsing. const SAFE_FILE_NAME_REGEX = /[^\w-]/g; const MAX_EXTENSION_LENGTH = 3; // Parameters which used 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} */ const debugLog = (options, msg) => { options = options || {}; if (!options.debug) return false; console.log(msg); // eslint-disable-line return true; }; /** * Generates unique temporary file name like: tmp-5000-156788789789. * @param prefix {String} - a prefix for generated unique file name. * @returns {String} */ const getTempFilename = (prefix) => { prefix = prefix || TEMP_PREFIX; tempCounter ++; if (tempCounter > TEMP_COUNTER_MAX) tempCounter = 1; return `${prefix}-${tempCounter}-${Date.now()}`; }; /** * Returns true if argument is a function. * @returns {Boolean} */ 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) => { if (err) { errorFunc(resolve, reject)(err); } else { resolve(); } }; }; /** * Builds instance options from arguments objects. * @returns {Object} - result options. */ const buildOptions = function(){ let result = {}; [...arguments].forEach(options => { if (!options || typeof options !== 'object') return; Object.keys(options).forEach(key => result[key] = options[key]); }); return result; }; /** * Builds request fields (using to build req.body and req.files) * @param {Object} instance - request object. * @param {String} field - field name. * @param 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; } else { // 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 */ const checkAndMakeDir = function(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 is not exists. if (!fs.existsSync(parentPath)) fs.mkdirSync(parentPath); return true; }; /** * Delete file. * @param {String} file - Path to the file to delete. */ const deleteFile = (file, callback) => { fs.unlink(file, (err) => { if (err) { callback(err); return; } callback(); }); }; /** * Copy file via streams * @param {String} src - Path to the source file * @param {String} dst - Path to the destination file. */ const copyFile = function(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); }; /** * Move file via streams by copieng and the 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) => { // Copy file to dst. copyFile(src, dst, (err) => { if (err) { callback(err); return; } // Delete src file. deleteFile(src, 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 = function(buffer, filePath, callback){ if (!Buffer.isBuffer(buffer)){ callback(new Error('buffer variable should be a Buffer!')); return; } //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', error => callback(error)); fstream.on('close', () => callback()); //Copy file via piping streams. readStream.pipe(fstream); }; /** * Parses filename and extension and returns object {name, extension}. * @param preserveExtension {Boolean, Integer} - true/false or number of characters for extension. * @param fileName {String} - file name to parse. * @returns {Object} - {name, extension}. */ const parseFileNameExtension = (preserveExtension, fileName) => { let preserveExtensionLengh = parseInt(preserveExtension); let result = {name: fileName, extension: ''}; if (!preserveExtension && preserveExtensionLengh !== 0){ return result; } // Define maximum extension length let maxExtLength = isNaN(preserveExtensionLengh) ? MAX_EXTENSION_LENGTH : Math.abs(preserveExtensionLengh); let 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 opts {Object} - middleware options. * @param fileName {String} - Uploaded file name. * @returns {String} */ const parseFileName = (opts, fileName) => { if (!opts.safeFileNames) { return fileName; } // Set regular expression for the file name. let safeNameRegex = typeof opts.safeFileNames === 'object' && opts.safeFileNames instanceof RegExp ? opts.safeFileNames : SAFE_FILE_NAME_REGEX; // Parse file name extension. let {name, extension} = parseFileNameExtension(opts.preserveExtension, fileName); if (extension.length) extension = '.' + extension.replace(safeNameRegex, ''); return name.replace(safeNameRegex, '').concat(extension); }; module.exports = { debugLog, isFunc, errorFunc, promiseCallback, buildOptions, buildFields, checkAndMakeDir, deleteFile, // For testing purpose. copyFile, // For testing purpose. moveFile, saveBufferToFile, parseFileName, getTempFilename };