UNPKG

8.6 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 * moveFile - moves the file from src to dst.
152 * Firstly trying to rename the file if no luck copying it to dst and then 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 */
157const moveFile = (src, dst, callback) => fs.rename(src, dst, err => (!err
158 ? callback()
159 : copyFile(src, dst, err => err ? callback(err) : deleteFile(src, callback))
160));
161
162/**
163 * Save buffer data to a file.
164 * @param {Buffer} buffer - buffer to save to a file.
165 * @param {String} filePath - path to a file.
166 */
167const saveBufferToFile = (buffer, filePath, callback) => {
168 if (!Buffer.isBuffer(buffer)) {
169 return callback(new Error('buffer variable should be type of Buffer!'));
170 }
171 // Setup readable stream from buffer.
172 let streamData = buffer;
173 let readStream = Readable();
174 readStream._read = () => {
175 readStream.push(streamData);
176 streamData = null;
177 };
178 // Setup file system writable stream.
179 let fstream = fs.createWriteStream(filePath);
180 fstream.on('error', error => callback(error));
181 fstream.on('close', () => callback());
182 // Copy file via piping streams.
183 readStream.pipe(fstream);
184};
185
186/**
187 * Decodes uriEncoded file names.
188 * @param fileName {String} - file name to decode.
189 * @returns {String}
190 */
191const uriDecodeFileName = (opts, fileName) => {
192 return opts.uriDecodeFileNames ? decodeURIComponent(fileName) : fileName;
193};
194
195/**
196 * Parses filename and extension and returns object {name, extension}.
197 * @param preserveExtension {Boolean, Integer} - true/false or number of characters for extension.
198 * @param fileName {String} - file name to parse.
199 * @returns {Object} - {name, extension}.
200 */
201const parseFileNameExtension = (preserveExtension, fileName) => {
202 const preserveExtensionLengh = parseInt(preserveExtension);
203 const result = {name: fileName, extension: ''};
204 if (!preserveExtension && preserveExtensionLengh !== 0) return result;
205 // Define maximum extension length
206 const maxExtLength = isNaN(preserveExtensionLengh)
207 ? MAX_EXTENSION_LENGTH
208 : Math.abs(preserveExtensionLengh);
209
210 const nameParts = fileName.split('.');
211 if (nameParts.length < 2) return result;
212
213 let extension = nameParts.pop();
214 if (
215 extension.length > maxExtLength &&
216 maxExtLength > 0
217 ) {
218 nameParts[nameParts.length - 1] +=
219 '.' +
220 extension.substr(0, extension.length - maxExtLength);
221 extension = extension.substr(-maxExtLength);
222 }
223
224 result.extension = maxExtLength ? extension : '';
225 result.name = nameParts.join('.');
226 return result;
227};
228
229/**
230 * Parse file name and extension.
231 * @param {Object} opts - middleware options.
232 * @param {string} fileName - Uploaded file name.
233 * @returns {string}
234 */
235const parseFileName = (opts, fileName) => {
236 // Check fileName argument
237 if (!fileName || typeof fileName !== 'string') return getTempFilename();
238 // Cut off file name if it's lenght more then 255.
239 let parsedName = fileName.length <= 255 ? fileName : fileName.substr(0, 255);
240 // Decode file name if uriDecodeFileNames option set true.
241 parsedName = uriDecodeFileName(opts, parsedName);
242 // Stop parsing file name if safeFileNames options hasn't been set.
243 if (!opts.safeFileNames) return parsedName;
244 // Set regular expression for the file name.
245 const nameRegex = typeof opts.safeFileNames === 'object' && opts.safeFileNames instanceof RegExp
246 ? opts.safeFileNames
247 : SAFE_FILE_NAME_REGEX;
248 // Parse file name extension.
249 let {name, extension} = parseFileNameExtension(opts.preserveExtension, parsedName);
250 if (extension.length) extension = '.' + extension.replace(nameRegex, '');
251
252 return name.replace(nameRegex, '').concat(extension);
253};
254
255module.exports = {
256 debugLog,
257 isFunc,
258 errorFunc,
259 promiseCallback,
260 buildOptions,
261 buildFields,
262 checkAndMakeDir,
263 deleteFile, // For testing purpose.
264 copyFile, // For testing purpose.
265 moveFile,
266 saveBufferToFile,
267 parseFileName,
268 getTempFilename,
269 uriDecodeFileName
270};