utilities.js 7.5 KB

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