gx
chenyc
2025-06-12 7b72ac13a83764a662159d4a49b7fffb90476ecb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
'use strict';
 
const fs = require('fs');
const path = require('path');
const { Readable } = require('stream');
 
// Parameters for safe file name parsing.
const SAFE_FILE_NAME_REGEX = /[^\w-]/g;
const MAX_EXTENSION_LENGTH = 3;
 
// Parameters to generate unique temporary file names:
const TEMP_COUNTER_MAX = 65536;
const TEMP_PREFIX = 'tmp';
let tempCounter = 0;
 
/**
 * Logs message to console if debug option set to true.
 * @param {Object} options - options object.
 * @param {string} msg - message to log.
 * @returns {boolean} - false if debug is off.
 */
const debugLog = (options, msg) => {
  const opts = options || {};
  if (!opts.debug) return false;
  console.log(`Express-file-upload: ${msg}`); // eslint-disable-line
  return true;
};
 
/**
 * Generates unique temporary file name. e.g. tmp-5000-156788789789.
 * @param {string} prefix - a prefix for generated unique file name.
 * @returns {string}
 */
const getTempFilename = (prefix = TEMP_PREFIX) => {
  tempCounter = tempCounter >= TEMP_COUNTER_MAX ? 1 : tempCounter + 1;
  return `${prefix}-${tempCounter}-${Date.now()}`;
};
 
/**
 * isFunc: Checks if argument is a function.
 * @returns {boolean} - Returns true if argument is a function.
 */
const isFunc = func => func && func.constructor && func.call && func.apply ? true: false;
 
/**
 * Set errorFunc to the same value as successFunc for callback mode.
 * @returns {Function}
 */
const errorFunc = (resolve, reject) => isFunc(reject) ? reject : resolve;
 
/**
 * Return a callback function for promise resole/reject args.
 * Ensures that callback is called only once.
 * @returns {Function}
 */
const promiseCallback = (resolve, reject) => {
  let hasFired = false;
  return (err) => {
    if (hasFired) {
      return;
    }
 
    hasFired = true;
    return err ? errorFunc(resolve, reject)(err) : resolve();
  };
};
 
/**
 * Builds instance options from arguments objects(can't be arrow function).
 * @returns {Object} - result options.
 */
const buildOptions = function() {
  const result = {};
  [...arguments].forEach(options => {
    if (!options || typeof options !== 'object') return;
    Object.keys(options).forEach(i => result[i] = options[i]);
  });
  return result;
};
 
// The default prototypes for both objects and arrays.
// Used by isSafeFromPollution
const OBJECT_PROTOTYPE_KEYS = Object.getOwnPropertyNames(Object.prototype);
const ARRAY_PROTOTYPE_KEYS = Object.getOwnPropertyNames(Array.prototype);
 
/**
 * Determines whether a key insertion into an object could result in a prototype pollution
 * @param {Object} base - The object whose insertion we are checking
 * @param {string} key - The key that will be inserted
 */
const isSafeFromPollution = (base, key) => {
  // We perform an instanceof check instead of Array.isArray as the former is more
  // permissive for cases in which the object as an Array prototype but was not constructed
  // via an Array constructor or literal.
  const TOUCHES_ARRAY_PROTOTYPE = (base instanceof Array) && ARRAY_PROTOTYPE_KEYS.includes(key);
  const TOUCHES_OBJECT_PROTOTYPE = OBJECT_PROTOTYPE_KEYS.includes(key);
 
  return !TOUCHES_ARRAY_PROTOTYPE && !TOUCHES_OBJECT_PROTOTYPE;
};
 
/**
 * Builds request fields (using to build req.body and req.files)
 * @param {Object} instance - request object.
 * @param {string} field - field name.
 * @param {any} value - field value.
 * @returns {Object}
 */
const buildFields = (instance, field, value) => {
  // Do nothing if value is not set.
  if (value === null || value === undefined) return instance;
  instance = instance || Object.create(null);
 
  if (!isSafeFromPollution(instance, field)) {
    return instance;
  }
  // Non-array fields
  if (!instance[field]) {
    instance[field] = value;
    return instance;
  }
  // Array fields
  if (instance[field] instanceof Array) {
    instance[field].push(value);
  } else {
    instance[field] = [instance[field], value];
  }
  return instance;
};
 
/**
 * Creates a folder for file specified in the path variable
 * @param {Object} fileUploadOptions
 * @param {string} filePath
 * @returns {boolean}
 */
const checkAndMakeDir = (fileUploadOptions, filePath) => {
  // Check upload options were set.
  if (!fileUploadOptions) return false;
  if (!fileUploadOptions.createParentPath) return false;
  // Check whether folder for the file exists.
  if (!filePath) return false;
  const parentPath = path.dirname(filePath);
  // Create folder if it doesn't exist.
  if (!fs.existsSync(parentPath)) fs.mkdirSync(parentPath, { recursive: true });
  // Checks folder again and return a results.
  return fs.existsSync(parentPath);
};
 
/**
 * Deletes a file.
 * @param {string} file - Path to the file to delete.
 * @param {Function} callback
 */
const deleteFile = (file, callback) => fs.unlink(file, callback);
 
/**
 * Copy file via streams
 * @param {string} src - Path to the source file
 * @param {string} dst - Path to the destination file.
 */
const copyFile = (src, dst, callback) => {
  // cbCalled flag and runCb helps to run cb only once.
  let cbCalled = false;
  let runCb = (err) => {
    if (cbCalled) return;
    cbCalled = true;
    callback(err);
  };
  // Create read stream
  let readable = fs.createReadStream(src);
  readable.on('error', runCb);
  // Create write stream
  let writable = fs.createWriteStream(dst);
  writable.on('error', (err)=>{
    readable.destroy();
    runCb(err);
  });
  writable.on('close', () => runCb());
  // Copy file via piping streams.
  readable.pipe(writable);
};
 
/**
 * moveFile: moves the file from src to dst.
 * Firstly trying to rename the file if no luck copying it to dst and then deleteing src.
 * @param {string} src - Path to the source file
 * @param {string} dst - Path to the destination file.
 * @param {Function} callback - A callback function with renamed flag.
 */
const moveFile = (src, dst, callback) => fs.rename(src, dst, (err) => {
  if (err) {
    // Try to copy file if rename didn't work.
    copyFile(src, dst, (cpErr) => (cpErr ? callback(cpErr) : deleteFile(src, callback)));
    return;
  }
  // File was renamed successfully: Add true to the callback to indicate that.
  callback(null, true);
});
 
/**
 * Save buffer data to a file.
 * @param {Buffer} buffer - buffer to save to a file.
 * @param {string} filePath - path to a file.
 */
const saveBufferToFile = (buffer, filePath, callback) => {
  if (!Buffer.isBuffer(buffer)) {
    return callback(new Error('buffer variable should be type of Buffer!'));
  }
  // Setup readable stream from buffer.
  let streamData = buffer;
  let readStream = Readable();
  readStream._read = () => {
    readStream.push(streamData);
    streamData = null;
  };
  // Setup file system writable stream.
  let fstream = fs.createWriteStream(filePath);
  // console.log("Calling saveBuffer");
  fstream.on('error', err => {
    // console.log("err cb")
    callback(err);
  });
  fstream.on('close', () => {
    // console.log("close cb");
    callback();
  });
  // Copy file via piping streams.
  readStream.pipe(fstream);
};
 
/**
 * Decodes uriEncoded file names.
 * @param {Object} opts - middleware options.
 * @param fileName {String} - file name to decode.
 * @returns {String}
 */
const uriDecodeFileName = (opts, fileName) => {
  if (!opts || !opts.uriDecodeFileNames) {
    return fileName;
  }
  // Decode file name from URI with checking URI malformed errors.
  // See Issue https://github.com/richardgirges/express-fileupload/issues/342.
  try {
    return decodeURIComponent(fileName);
  } catch (err) {
    const matcher = /(%[a-f0-9]{2})/gi;
    return fileName.split(matcher)
      .map((str) => {
        try {
          return decodeURIComponent(str);
        } catch (err) {
          return '';
        }
      })
      .join('');
  }
};
 
/**
 * Parses filename and extension and returns object {name, extension}.
 * @param {boolean|integer} preserveExtension - true/false or number of characters for extension.
 * @param {string} fileName - file name to parse.
 * @returns {Object} - { name, extension }.
 */
const parseFileNameExtension = (preserveExtension, fileName) => {
  const preserveExtensionLength = parseInt(preserveExtension);
  const result = {name: fileName, extension: ''};
  if (!preserveExtension && preserveExtensionLength !== 0) return result;
  // Define maximum extension length
  const maxExtLength = isNaN(preserveExtensionLength)
    ? MAX_EXTENSION_LENGTH
    : Math.abs(preserveExtensionLength);
 
  const nameParts = fileName.split('.');
  if (nameParts.length < 2) return result;
 
  let extension = nameParts.pop();
  if (
    extension.length > maxExtLength &&
    maxExtLength > 0
  ) {
    nameParts[nameParts.length - 1] +=
      '.' +
      extension.substr(0, extension.length - maxExtLength);
    extension = extension.substr(-maxExtLength);
  }
 
  result.extension = maxExtLength ? extension : '';
  result.name = nameParts.join('.');
  return result;
};
 
/**
 * Parse file name and extension.
 * @param {Object} opts - middleware options.
 * @param {string} fileName - Uploaded file name.
 * @returns {string}
 */
const parseFileName = (opts, fileName) => {
  // Check fileName argument
  if (!fileName || typeof fileName !== 'string') return getTempFilename();
  // Cut off file name if it's lenght more then 255.
  let parsedName = fileName.length <= 255 ? fileName : fileName.substr(0, 255);
  // Decode file name if uriDecodeFileNames option set true.
  parsedName = uriDecodeFileName(opts, parsedName);
  // Stop parsing file name if safeFileNames options hasn't been set.
  if (!opts.safeFileNames) return parsedName;
  // Set regular expression for the file name.
  const nameRegex = typeof opts.safeFileNames === 'object' && opts.safeFileNames instanceof RegExp
    ? opts.safeFileNames
    : SAFE_FILE_NAME_REGEX;
  // Parse file name extension.
  let {name, extension} = parseFileNameExtension(opts.preserveExtension, parsedName);
  if (extension.length) extension = '.' + extension.replace(nameRegex, '');
 
  return name.replace(nameRegex, '').concat(extension);
};
 
module.exports = {
  isFunc,
  debugLog,
  copyFile, // For testing purpose.
  moveFile,
  errorFunc,
  deleteFile, // For testing purpose.
  buildFields,
  buildOptions,
  parseFileName,
  getTempFilename,
  promiseCallback,
  checkAndMakeDir,
  saveBufferToFile,
  uriDecodeFileName,
  isSafeFromPollution
};