utilities.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { Readable } = require('stream');
  5. // Parameters for safe file name parsing.
  6. const SAFE_FILE_NAME_REGEX = /[^\w-]/g;
  7. const MAX_EXTENSION_LENGTH = 3;
  8. // Parameters to generate unique temporary file names:
  9. const TEMP_COUNTER_MAX = 65536;
  10. const TEMP_PREFIX = 'tmp';
  11. let tempCounter = 0;
  12. /**
  13. * Logs message to console if debug option set to true.
  14. * @param {Object} options - options object.
  15. * @param {string} msg - message to log.
  16. * @returns {boolean} - false if debug is off.
  17. */
  18. const debugLog = (options, msg) => {
  19. const opts = options || {};
  20. if (!opts.debug) return false;
  21. console.log(`Express-file-upload: ${msg}`); // eslint-disable-line
  22. return true;
  23. };
  24. /**
  25. * Generates unique temporary file name. e.g. tmp-5000-156788789789.
  26. * @param {string} prefix - a prefix for generated unique file name.
  27. * @returns {string}
  28. */
  29. const getTempFilename = (prefix = TEMP_PREFIX) => {
  30. tempCounter = tempCounter >= TEMP_COUNTER_MAX ? 1 : tempCounter + 1;
  31. return `${prefix}-${tempCounter}-${Date.now()}`;
  32. };
  33. /**
  34. * isFunc: Checks if argument is a function.
  35. * @returns {boolean} - Returns true if argument is a function.
  36. */
  37. const isFunc = func => func && func.constructor && func.call && func.apply ? true: false;
  38. /**
  39. * Set errorFunc to the same value as successFunc for callback mode.
  40. * @returns {Function}
  41. */
  42. const errorFunc = (resolve, reject) => isFunc(reject) ? reject : resolve;
  43. /**
  44. * Return a callback function for promise resole/reject args.
  45. * @returns {Function}
  46. */
  47. const promiseCallback = (resolve, reject) => {
  48. return err => err ? errorFunc(resolve, reject)(err) : resolve();
  49. };
  50. /**
  51. * Builds instance options from arguments objects(can't be arrow function).
  52. * @returns {Object} - result options.
  53. */
  54. const buildOptions = function() {
  55. const result = {};
  56. [...arguments].forEach(options => {
  57. if (!options || typeof options !== 'object') return;
  58. Object.keys(options).forEach(i => result[i] = options[i]);
  59. });
  60. return result;
  61. };
  62. /**
  63. * Builds request fields (using to build req.body and req.files)
  64. * @param {Object} instance - request object.
  65. * @param {string} field - field name.
  66. * @param {any} value - field value.
  67. * @returns {Object}
  68. */
  69. const buildFields = (instance, field, value) => {
  70. // Do nothing if value is not set.
  71. if (value === null || value === undefined) return instance;
  72. instance = instance || {};
  73. // Non-array fields
  74. if (!instance[field]) {
  75. instance[field] = value;
  76. return instance;
  77. }
  78. // Array fields
  79. if (instance[field] instanceof Array) {
  80. instance[field].push(value);
  81. } else {
  82. instance[field] = [instance[field], value];
  83. }
  84. return instance;
  85. };
  86. /**
  87. * Creates a folder for file specified in the path variable
  88. * @param {Object} fileUploadOptions
  89. * @param {string} filePath
  90. * @returns {boolean}
  91. */
  92. const checkAndMakeDir = (fileUploadOptions, filePath) => {
  93. // Check upload options were set.
  94. if (!fileUploadOptions) return false;
  95. if (!fileUploadOptions.createParentPath) return false;
  96. // Check whether folder for the file exists.
  97. if (!filePath) return false;
  98. const parentPath = path.dirname(filePath);
  99. // Create folder if it doesn't exist.
  100. if (!fs.existsSync(parentPath)) fs.mkdirSync(parentPath, { recursive: true });
  101. // Checks folder again and return a results.
  102. return fs.existsSync(parentPath);
  103. };
  104. /**
  105. * Deletes a file.
  106. * @param {string} file - Path to the file to delete.
  107. * @param {Function} callback
  108. */
  109. const deleteFile = (file, callback) => fs.unlink(file, callback);
  110. /**
  111. * Copy file via streams
  112. * @param {string} src - Path to the source file
  113. * @param {string} dst - Path to the destination file.
  114. */
  115. const copyFile = (src, dst, callback) => {
  116. // cbCalled flag and runCb helps to run cb only once.
  117. let cbCalled = false;
  118. let runCb = (err) => {
  119. if (cbCalled) return;
  120. cbCalled = true;
  121. callback(err);
  122. };
  123. // Create read stream
  124. let readable = fs.createReadStream(src);
  125. readable.on('error', runCb);
  126. // Create write stream
  127. let writable = fs.createWriteStream(dst);
  128. writable.on('error', (err)=>{
  129. readable.destroy();
  130. runCb(err);
  131. });
  132. writable.on('close', () => runCb());
  133. // Copy file via piping streams.
  134. readable.pipe(writable);
  135. };
  136. /**
  137. * moveFile: moves the file from src to dst.
  138. * Firstly trying to rename the file if no luck copying it to dst and then deleteing src.
  139. * @param {string} src - Path to the source file
  140. * @param {string} dst - Path to the destination file.
  141. * @param {Function} callback - A callback function.
  142. */
  143. const moveFile = (src, dst, callback) => fs.rename(src, dst, err => (err
  144. ? copyFile(src, dst, err => err ? callback(err) : deleteFile(src, callback))
  145. : callback()
  146. ));
  147. /**
  148. * Save buffer data to a file.
  149. * @param {Buffer} buffer - buffer to save to a file.
  150. * @param {string} filePath - path to a file.
  151. */
  152. const saveBufferToFile = (buffer, filePath, callback) => {
  153. if (!Buffer.isBuffer(buffer)) {
  154. return callback(new Error('buffer variable should be type of Buffer!'));
  155. }
  156. // Setup readable stream from buffer.
  157. let streamData = buffer;
  158. let readStream = Readable();
  159. readStream._read = () => {
  160. readStream.push(streamData);
  161. streamData = null;
  162. };
  163. // Setup file system writable stream.
  164. let fstream = fs.createWriteStream(filePath);
  165. fstream.on('error', err => callback(err));
  166. fstream.on('close', () => callback());
  167. // Copy file via piping streams.
  168. readStream.pipe(fstream);
  169. };
  170. /**
  171. * Decodes uriEncoded file names.
  172. * @param fileName {String} - file name to decode.
  173. * @returns {String}
  174. */
  175. const uriDecodeFileName = (opts, fileName) => {
  176. return opts.uriDecodeFileNames ? decodeURIComponent(fileName) : fileName;
  177. };
  178. /**
  179. * Parses filename and extension and returns object {name, extension}.
  180. * @param {boolean|integer} preserveExtension - true/false or number of characters for extension.
  181. * @param {string} fileName - file name to parse.
  182. * @returns {Object} - { name, extension }.
  183. */
  184. const parseFileNameExtension = (preserveExtension, fileName) => {
  185. const preserveExtensionLengh = parseInt(preserveExtension);
  186. const result = {name: fileName, extension: ''};
  187. if (!preserveExtension && preserveExtensionLengh !== 0) return result;
  188. // Define maximum extension length
  189. const maxExtLength = isNaN(preserveExtensionLengh)
  190. ? MAX_EXTENSION_LENGTH
  191. : Math.abs(preserveExtensionLengh);
  192. const nameParts = fileName.split('.');
  193. if (nameParts.length < 2) return result;
  194. let extension = nameParts.pop();
  195. if (
  196. extension.length > maxExtLength &&
  197. maxExtLength > 0
  198. ) {
  199. nameParts[nameParts.length - 1] +=
  200. '.' +
  201. extension.substr(0, extension.length - maxExtLength);
  202. extension = extension.substr(-maxExtLength);
  203. }
  204. result.extension = maxExtLength ? extension : '';
  205. result.name = nameParts.join('.');
  206. return result;
  207. };
  208. /**
  209. * Parse file name and extension.
  210. * @param {Object} opts - middleware options.
  211. * @param {string} fileName - Uploaded file name.
  212. * @returns {string}
  213. */
  214. const parseFileName = (opts, fileName) => {
  215. // Check fileName argument
  216. if (!fileName || typeof fileName !== 'string') return getTempFilename();
  217. // Cut off file name if it's lenght more then 255.
  218. let parsedName = fileName.length <= 255 ? fileName : fileName.substr(0, 255);
  219. // Decode file name if uriDecodeFileNames option set true.
  220. parsedName = uriDecodeFileName(opts, parsedName);
  221. // Stop parsing file name if safeFileNames options hasn't been set.
  222. if (!opts.safeFileNames) return parsedName;
  223. // Set regular expression for the file name.
  224. const nameRegex = typeof opts.safeFileNames === 'object' && opts.safeFileNames instanceof RegExp
  225. ? opts.safeFileNames
  226. : SAFE_FILE_NAME_REGEX;
  227. // Parse file name extension.
  228. let {name, extension} = parseFileNameExtension(opts.preserveExtension, parsedName);
  229. if (extension.length) extension = '.' + extension.replace(nameRegex, '');
  230. return name.replace(nameRegex, '').concat(extension);
  231. };
  232. module.exports = {
  233. isFunc,
  234. debugLog,
  235. copyFile, // For testing purpose.
  236. moveFile,
  237. errorFunc,
  238. deleteFile, // For testing purpose.
  239. buildFields,
  240. buildOptions,
  241. parseFileName,
  242. getTempFilename,
  243. promiseCallback,
  244. checkAndMakeDir,
  245. saveBufferToFile,
  246. uriDecodeFileName
  247. };