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.

311 lines
9.4 KiB

  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. * Ensures that callback is called only once.
  46. * @returns {Function}
  47. */
  48. const promiseCallback = (resolve, reject) => {
  49. let hasFired = false;
  50. return (err) => {
  51. if (hasFired) {
  52. return;
  53. }
  54. hasFired = true;
  55. return err ? errorFunc(resolve, reject)(err) : resolve();
  56. };
  57. };
  58. /**
  59. * Builds instance options from arguments objects(can't be arrow function).
  60. * @returns {Object} - result options.
  61. */
  62. const buildOptions = function() {
  63. const result = {};
  64. [...arguments].forEach(options => {
  65. if (!options || typeof options !== 'object') return;
  66. Object.keys(options).forEach(i => result[i] = options[i]);
  67. });
  68. return result;
  69. };
  70. // The default prototypes for both objects and arrays.
  71. // Used by isSafeFromPollution
  72. const OBJECT_PROTOTYPE_KEYS = Object.getOwnPropertyNames(Object.prototype);
  73. const ARRAY_PROTOTYPE_KEYS = Object.getOwnPropertyNames(Array.prototype);
  74. /**
  75. * Determines whether a key insertion into an object could result in a prototype pollution
  76. * @param {Object} base - The object whose insertion we are checking
  77. * @param {string} key - The key that will be inserted
  78. */
  79. const isSafeFromPollution = (base, key) => {
  80. // We perform an instanceof check instead of Array.isArray as the former is more
  81. // permissive for cases in which the object as an Array prototype but was not constructed
  82. // via an Array constructor or literal.
  83. const TOUCHES_ARRAY_PROTOTYPE = (base instanceof Array) && ARRAY_PROTOTYPE_KEYS.includes(key);
  84. const TOUCHES_OBJECT_PROTOTYPE = OBJECT_PROTOTYPE_KEYS.includes(key);
  85. return !TOUCHES_ARRAY_PROTOTYPE && !TOUCHES_OBJECT_PROTOTYPE;
  86. };
  87. /**
  88. * Builds request fields (using to build req.body and req.files)
  89. * @param {Object} instance - request object.
  90. * @param {string} field - field name.
  91. * @param {any} value - field value.
  92. * @returns {Object}
  93. */
  94. const buildFields = (instance, field, value) => {
  95. // Do nothing if value is not set.
  96. if (value === null || value === undefined) return instance;
  97. instance = instance || Object.create(null);
  98. if (!isSafeFromPollution(instance, field)) {
  99. return instance;
  100. }
  101. // Non-array fields
  102. if (!instance[field]) {
  103. instance[field] = value;
  104. return instance;
  105. }
  106. // Array fields
  107. if (instance[field] instanceof Array) {
  108. instance[field].push(value);
  109. } else {
  110. instance[field] = [instance[field], value];
  111. }
  112. return instance;
  113. };
  114. /**
  115. * Creates a folder for file specified in the path variable
  116. * @param {Object} fileUploadOptions
  117. * @param {string} filePath
  118. * @returns {boolean}
  119. */
  120. const checkAndMakeDir = (fileUploadOptions, filePath) => {
  121. // Check upload options were set.
  122. if (!fileUploadOptions) return false;
  123. if (!fileUploadOptions.createParentPath) return false;
  124. // Check whether folder for the file exists.
  125. if (!filePath) return false;
  126. const parentPath = path.dirname(filePath);
  127. // Create folder if it doesn't exist.
  128. if (!fs.existsSync(parentPath)) fs.mkdirSync(parentPath, { recursive: true });
  129. // Checks folder again and return a results.
  130. return fs.existsSync(parentPath);
  131. };
  132. /**
  133. * Deletes a file.
  134. * @param {string} file - Path to the file to delete.
  135. * @param {Function} callback
  136. */
  137. const deleteFile = (file, callback) => fs.unlink(file, callback);
  138. /**
  139. * Copy file via streams
  140. * @param {string} src - Path to the source file
  141. * @param {string} dst - Path to the destination file.
  142. */
  143. const copyFile = (src, dst, callback) => {
  144. // cbCalled flag and runCb helps to run cb only once.
  145. let cbCalled = false;
  146. let runCb = (err) => {
  147. if (cbCalled) return;
  148. cbCalled = true;
  149. callback(err);
  150. };
  151. // Create read stream
  152. let readable = fs.createReadStream(src);
  153. readable.on('error', runCb);
  154. // Create write stream
  155. let writable = fs.createWriteStream(dst);
  156. writable.on('error', (err)=>{
  157. readable.destroy();
  158. runCb(err);
  159. });
  160. writable.on('close', () => runCb());
  161. // Copy file via piping streams.
  162. readable.pipe(writable);
  163. };
  164. /**
  165. * moveFile: moves the file from src to dst.
  166. * Firstly trying to rename the file if no luck copying it to dst and then deleteing src.
  167. * @param {string} src - Path to the source file
  168. * @param {string} dst - Path to the destination file.
  169. * @param {Function} callback - A callback function.
  170. */
  171. const moveFile = (src, dst, callback) => fs.rename(src, dst, err => (err
  172. ? copyFile(src, dst, err => err ? callback(err) : deleteFile(src, callback))
  173. : callback()
  174. ));
  175. /**
  176. * Save buffer data to a file.
  177. * @param {Buffer} buffer - buffer to save to a file.
  178. * @param {string} filePath - path to a file.
  179. */
  180. const saveBufferToFile = (buffer, filePath, callback) => {
  181. if (!Buffer.isBuffer(buffer)) {
  182. return callback(new Error('buffer variable should be type of Buffer!'));
  183. }
  184. // Setup readable stream from buffer.
  185. let streamData = buffer;
  186. let readStream = Readable();
  187. readStream._read = () => {
  188. readStream.push(streamData);
  189. streamData = null;
  190. };
  191. // Setup file system writable stream.
  192. let fstream = fs.createWriteStream(filePath);
  193. // console.log("Calling saveBuffer");
  194. fstream.on('error', err => {
  195. // console.log("err cb")
  196. callback(err);
  197. });
  198. fstream.on('close', () => {
  199. // console.log("close cb");
  200. callback();
  201. });
  202. // Copy file via piping streams.
  203. readStream.pipe(fstream);
  204. };
  205. /**
  206. * Decodes uriEncoded file names.
  207. * @param fileName {String} - file name to decode.
  208. * @returns {String}
  209. */
  210. const uriDecodeFileName = (opts, fileName) => {
  211. return opts.uriDecodeFileNames ? decodeURIComponent(fileName) : fileName;
  212. };
  213. /**
  214. * Parses filename and extension and returns object {name, extension}.
  215. * @param {boolean|integer} preserveExtension - true/false or number of characters for extension.
  216. * @param {string} fileName - file name to parse.
  217. * @returns {Object} - { name, extension }.
  218. */
  219. const parseFileNameExtension = (preserveExtension, fileName) => {
  220. const preserveExtensionLength = parseInt(preserveExtension);
  221. const result = {name: fileName, extension: ''};
  222. if (!preserveExtension && preserveExtensionLength !== 0) return result;
  223. // Define maximum extension length
  224. const maxExtLength = isNaN(preserveExtensionLength)
  225. ? MAX_EXTENSION_LENGTH
  226. : Math.abs(preserveExtensionLength);
  227. const nameParts = fileName.split('.');
  228. if (nameParts.length < 2) return result;
  229. let extension = nameParts.pop();
  230. if (
  231. extension.length > maxExtLength &&
  232. maxExtLength > 0
  233. ) {
  234. nameParts[nameParts.length - 1] +=
  235. '.' +
  236. extension.substr(0, extension.length - maxExtLength);
  237. extension = extension.substr(-maxExtLength);
  238. }
  239. result.extension = maxExtLength ? extension : '';
  240. result.name = nameParts.join('.');
  241. return result;
  242. };
  243. /**
  244. * Parse file name and extension.
  245. * @param {Object} opts - middleware options.
  246. * @param {string} fileName - Uploaded file name.
  247. * @returns {string}
  248. */
  249. const parseFileName = (opts, fileName) => {
  250. // Check fileName argument
  251. if (!fileName || typeof fileName !== 'string') return getTempFilename();
  252. // Cut off file name if it's lenght more then 255.
  253. let parsedName = fileName.length <= 255 ? fileName : fileName.substr(0, 255);
  254. // Decode file name if uriDecodeFileNames option set true.
  255. parsedName = uriDecodeFileName(opts, parsedName);
  256. // Stop parsing file name if safeFileNames options hasn't been set.
  257. if (!opts.safeFileNames) return parsedName;
  258. // Set regular expression for the file name.
  259. const nameRegex = typeof opts.safeFileNames === 'object' && opts.safeFileNames instanceof RegExp
  260. ? opts.safeFileNames
  261. : SAFE_FILE_NAME_REGEX;
  262. // Parse file name extension.
  263. let {name, extension} = parseFileNameExtension(opts.preserveExtension, parsedName);
  264. if (extension.length) extension = '.' + extension.replace(nameRegex, '');
  265. return name.replace(nameRegex, '').concat(extension);
  266. };
  267. module.exports = {
  268. isFunc,
  269. debugLog,
  270. copyFile, // For testing purpose.
  271. moveFile,
  272. errorFunc,
  273. deleteFile, // For testing purpose.
  274. buildFields,
  275. buildOptions,
  276. parseFileName,
  277. getTempFilename,
  278. promiseCallback,
  279. checkAndMakeDir,
  280. saveBufferToFile,
  281. uriDecodeFileName,
  282. isSafeFromPollution
  283. };