You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
310 lines
9.5 KiB
310 lines
9.5 KiB
'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 = tempCounter >= TEMP_COUNTER_MAX ? 1 : 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) => {
|
|
let hasFired = false;
|
|
return (err) => {
|
|
if (hasFired) {
|
|
return;
|
|
}
|
|
|
|
hasFired = true;
|
|
return 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(key => result[key] = options[key]);
|
|
});
|
|
return result;
|
|
};
|
|
|
|
// The default prototypes for both objects and arrays.
|
|
// Used by isSafeFromPollution
|
|
const OBJECT_PROTOTYPE_KEYS = Object.getOwnPropertyNames(Object.prototype);
|
|
const ARRAY_PROTOTYPE_KEYS = Object.getOwnPropertyNames(Array.prototype);
|
|
|
|
/**
|
|
* Determines whether a key insertion into an object could result in a prototype pollution
|
|
* @param {Object} base - The object whose insertion we are checking
|
|
* @param {string} key - The key that will be inserted
|
|
*/
|
|
const isSafeFromPollution = (base, key) => {
|
|
// We perform an instanceof check instead of Array.isArray as the former is more
|
|
// permissive for cases in which the object as an Array prototype but was not constructed
|
|
// via an Array constructor or literal.
|
|
const TOUCHES_ARRAY_PROTOTYPE = (base instanceof Array) && ARRAY_PROTOTYPE_KEYS.includes(key);
|
|
const TOUCHES_OBJECT_PROTOTYPE = OBJECT_PROTOTYPE_KEYS.includes(key);
|
|
|
|
return !TOUCHES_ARRAY_PROTOTYPE && !TOUCHES_OBJECT_PROTOTYPE;
|
|
};
|
|
|
|
/**
|
|
* 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 || Object.create(null);
|
|
|
|
if (!isSafeFromPollution(instance, field)) {
|
|
return 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
|
|
* @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 does not exist.
|
|
if (!fs.existsSync(parentPath)) fs.mkdirSync(parentPath, { recursive: true });
|
|
// Check folder again and return the result.
|
|
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', error => callback(error));
|
|
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 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) => {
|
|
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 opts {Object} - middleware options.
|
|
* @param fileName {String} - Uploaded file name.
|
|
* @returns {String}
|
|
*/
|
|
const parseFileName = (opts, fileName) => {
|
|
// 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);
|
|
// Set regular expression for the file name.
|
|
const nameRegex = typeof opts.safeFileNames === 'object' && opts.safeFileNames instanceof RegExp
|
|
? opts.safeFileNames
|
|
: SAFE_FILE_NAME_REGEX;
|
|
// Set string to replace spaces with
|
|
const spaceReplacement = typeof opts.spaceFileNameReplacement === 'string'
|
|
? opts.spaceFileNameReplacement
|
|
: ' ';
|
|
// Parse file name extension.
|
|
let {name, extension} = parseFileNameExtension(opts.preserveExtension, parsedName);
|
|
if (extension.length) extension = '.' + extension.replace(nameRegex, '');
|
|
let finalName = name;
|
|
if (opts.safeFileNames !== false) {
|
|
finalName = finalName.replace(nameRegex, '');
|
|
}
|
|
if (opts.spaceFileNameReplacement !== false) {
|
|
finalName = finalName.replace(/\s+/g, spaceReplacement);
|
|
}
|
|
return { filename: finalName.concat(extension), extension };
|
|
};
|
|
|
|
module.exports = {
|
|
debugLog,
|
|
isFunc,
|
|
errorFunc,
|
|
promiseCallback,
|
|
buildOptions,
|
|
buildFields,
|
|
checkAndMakeDir,
|
|
deleteFile, // For testing purpose.
|
|
copyFile, // For testing purpose.
|
|
moveFile,
|
|
saveBufferToFile,
|
|
parseFileName,
|
|
getTempFilename,
|
|
uriDecodeFileName,
|
|
isSafeFromPollution,
|
|
};
|
|
|