UNPKG

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