252 lines
9.0 KiB
JavaScript
252 lines
9.0 KiB
JavaScript
var makeMiddleware = require('./lib/make-middleware')
|
|
|
|
var diskStorage = require('./storage/disk')
|
|
var memoryStorage = require('./storage/memory')
|
|
var MulterError = require('./lib/multer-error')
|
|
var validateLimits = require('./lib/validate-limits')
|
|
|
|
/**
|
|
* A file parsed from a multipart request.
|
|
*
|
|
* @typedef {Object} File
|
|
* @property {string} fieldname Name of the form field
|
|
* @property {string} originalname Name of the file on the client (client-supplied, treat as untrusted)
|
|
* @property {string} encoding Transfer encoding of the file
|
|
* @property {string} mimetype MIME type of the file
|
|
* @property {number} [size] Size of the file in bytes; set once the file has been stored
|
|
* @property {ReadableStream} [stream] The file data; only present while a storage engine's `_handleFile` runs
|
|
* @property {string} [destination] Folder the file was saved to (`DiskStorage`)
|
|
* @property {string} [filename] Name of the file within `destination` (`DiskStorage`)
|
|
* @property {string} [path] Full path of the saved file (`DiskStorage`)
|
|
* @property {Buffer} [buffer] Contents of the file (`MemoryStorage`)
|
|
*/
|
|
|
|
/**
|
|
* Size limits, passed to busboy. All are optional.
|
|
*
|
|
* @typedef {Object} Limits
|
|
* @property {number} [fieldNameSize=100] Max field name size in bytes
|
|
* @property {number} [fieldSize=1048576] Max field value size in bytes
|
|
* @property {number} [fields=Infinity] Max number of non-file fields
|
|
* @property {number} [fileSize=Infinity] Max file size in bytes (integer or `Infinity`)
|
|
* @property {number} [files=Infinity] Max number of file fields
|
|
* @property {number} [parts=Infinity] Max number of parts (fields + files)
|
|
* @property {number} [headerPairs=2000] Max number of header key/value pairs to parse
|
|
* @property {number} [fieldNestingDepth=Infinity] Max nesting depth of field names (`a[b][c]` has 2 levels)
|
|
* @property {number} [fieldArrayIndexLimit=Infinity] Max numeric array index accepted in field names
|
|
*/
|
|
|
|
/**
|
|
* Decides whether a file is accepted. Call `cb(null, true)` to accept the
|
|
* file, `cb(null, false)` to skip it silently, or `cb(err)` to abort.
|
|
*
|
|
* @callback FileFilter
|
|
* @param {Object} req The request
|
|
* @param {File} file The file being uploaded; `size`, `stream`, `path` and `buffer` are not set yet
|
|
* @param {function(?Error, boolean=): void} cb
|
|
*/
|
|
|
|
/**
|
|
* Storage engine. See StorageEngine.md for the full contract.
|
|
*
|
|
* @typedef {Object} StorageEngine
|
|
* @property {function(Object, File, function(?Error, Object=): void): void} _handleFile
|
|
* Consumes `file.stream` and calls back with the properties to merge into the file object
|
|
* @property {function(Object, File, function(?Error): void): void} _removeFile
|
|
* Removes a stored file when the request fails
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} Options
|
|
* @property {string} [dest] Folder to store files in (uses `DiskStorage`)
|
|
* @property {StorageEngine} [storage] Storage engine; defaults to `MemoryStorage` when neither `dest` nor `storage` is set
|
|
* @property {FileFilter} [fileFilter] Controls which files are accepted
|
|
* @property {Limits|function(Object): Limits} [limits] Size limits, or a function of the request returning them
|
|
* @property {boolean} [preservePath=false] Keep the full client-supplied path in `file.originalname`
|
|
* @property {string} [defParamCharset='latin1'] Charset for part header parameters (e.g. filename) without an explicit one
|
|
* @property {string} [defCharset='utf8'] Charset for text field values that do not declare one
|
|
* @property {number} [highWaterMark] `highWaterMark` of the multipart parser stream; busboy's default
|
|
* @property {number} [fileHwm] `highWaterMark` of each file stream (`file.stream`); busboy's default
|
|
* @property {function(Object, Object): void} [streamHandler] Feeds the request body to busboy; defaults to `req.pipe(busboy)`
|
|
*/
|
|
|
|
function allowAll (req, file, cb) {
|
|
cb(null, true)
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @private
|
|
* @param {Options} options
|
|
*/
|
|
function Multer (options) {
|
|
if (options.storage) {
|
|
this.storage = options.storage
|
|
} else if (options.dest) {
|
|
this.storage = diskStorage({ destination: options.dest })
|
|
} else {
|
|
this.storage = memoryStorage()
|
|
}
|
|
|
|
if (options.limits && typeof options.limits !== 'function') validateLimits(options.limits)
|
|
|
|
this.limits = options.limits
|
|
this.preservePath = options.preservePath
|
|
this.defParamCharset = options.defParamCharset || 'latin1'
|
|
this.defCharset = options.defCharset
|
|
this.highWaterMark = options.highWaterMark
|
|
this.fileHwm = options.fileHwm
|
|
this.fileFilter = options.fileFilter || allowAll
|
|
|
|
if (options.streamHandler !== undefined && typeof options.streamHandler !== 'function') {
|
|
throw new TypeError('Expected streamHandler to be a function')
|
|
}
|
|
|
|
this.streamHandler = options.streamHandler
|
|
}
|
|
|
|
Multer.prototype._makeMiddleware = function (fields, fileStrategy) {
|
|
function setup () {
|
|
var fileFilter = this.fileFilter
|
|
var filesLeft = Object.create(null)
|
|
|
|
fields.forEach(function (field) {
|
|
if (typeof field.maxCount === 'number') {
|
|
filesLeft[field.name] = field.maxCount
|
|
} else {
|
|
filesLeft[field.name] = Infinity
|
|
}
|
|
})
|
|
|
|
function wrappedFileFilter (req, file, cb) {
|
|
if ((filesLeft[file.fieldname] || 0) <= 0) {
|
|
return cb(new MulterError('LIMIT_UNEXPECTED_FILE', file.fieldname, file.originalname))
|
|
}
|
|
|
|
// Reserve the slot synchronously before yielding to the async fileFilter,
|
|
// so files emitted together in one busboy write cannot all read the
|
|
// pre-decrement count and slip past maxCount. Release it on reject/error
|
|
// (a skipped file must not consume a slot, #1419), and only once so a
|
|
// filter that calls back repeatedly cannot leak slots.
|
|
filesLeft[file.fieldname] -= 1
|
|
|
|
var settled = false
|
|
fileFilter(req, file, function (err, includeFile) {
|
|
if (!settled) {
|
|
settled = true
|
|
if (err || !includeFile) filesLeft[file.fieldname] += 1
|
|
}
|
|
|
|
cb(err, includeFile)
|
|
})
|
|
}
|
|
|
|
return {
|
|
limits: this.limits,
|
|
preservePath: this.preservePath,
|
|
defParamCharset: this.defParamCharset,
|
|
defCharset: this.defCharset,
|
|
highWaterMark: this.highWaterMark,
|
|
fileHwm: this.fileHwm,
|
|
streamHandler: this.streamHandler,
|
|
storage: this.storage,
|
|
fileFilter: wrappedFileFilter,
|
|
fileStrategy: fileStrategy
|
|
}
|
|
}
|
|
|
|
return makeMiddleware(setup.bind(this))
|
|
}
|
|
|
|
/**
|
|
* Accept a single file for the field `name`. The file is stored in `req.file`.
|
|
*
|
|
* @param {string} name
|
|
* @returns {function(Object, Object, function(?Error): void): void} Express middleware
|
|
*/
|
|
Multer.prototype.single = function (name) {
|
|
return this._makeMiddleware([{ name: name, maxCount: 1 }], 'VALUE')
|
|
}
|
|
|
|
/**
|
|
* Accept an array of files for the field `name`, stored in `req.files`.
|
|
* Files skipped by `fileFilter` do not count towards `maxCount`.
|
|
*
|
|
* @param {string} name
|
|
* @param {number} [maxCount] Error with `LIMIT_UNEXPECTED_FILE` if more files are accepted
|
|
* @returns {function(Object, Object, function(?Error): void): void} Express middleware
|
|
*/
|
|
Multer.prototype.array = function (name, maxCount) {
|
|
return this._makeMiddleware([{ name: name, maxCount: maxCount }], 'ARRAY')
|
|
}
|
|
|
|
/**
|
|
* Accept a mix of files. `req.files` is an object keyed by field name, each
|
|
* value an array of files.
|
|
*
|
|
* @param {Array<{name: string, maxCount?: number}>} fields
|
|
* @returns {function(Object, Object, function(?Error): void): void} Express middleware
|
|
*/
|
|
Multer.prototype.fields = function (fields) {
|
|
return this._makeMiddleware(fields, 'OBJECT')
|
|
}
|
|
|
|
/**
|
|
* Accept only text fields. Any file results in a `LIMIT_UNEXPECTED_FILE` error.
|
|
*
|
|
* @returns {function(Object, Object, function(?Error): void): void} Express middleware
|
|
*/
|
|
Multer.prototype.none = function () {
|
|
return this._makeMiddleware([], 'NONE')
|
|
}
|
|
|
|
/**
|
|
* Accept all files, stored as an array in `req.files`. Only use this on routes
|
|
* that handle every uploaded file.
|
|
*
|
|
* @returns {function(Object, Object, function(?Error): void): void} Express middleware
|
|
*/
|
|
Multer.prototype.any = function () {
|
|
function setup () {
|
|
return {
|
|
limits: this.limits,
|
|
preservePath: this.preservePath,
|
|
defParamCharset: this.defParamCharset,
|
|
defCharset: this.defCharset,
|
|
highWaterMark: this.highWaterMark,
|
|
fileHwm: this.fileHwm,
|
|
streamHandler: this.streamHandler,
|
|
storage: this.storage,
|
|
fileFilter: this.fileFilter,
|
|
fileStrategy: 'ARRAY'
|
|
}
|
|
}
|
|
|
|
return makeMiddleware(setup.bind(this))
|
|
}
|
|
|
|
/**
|
|
* Create a multer instance. Text fields are parsed into `req.body`; files go to
|
|
* `req.file` or `req.files` depending on the method used.
|
|
*
|
|
* @param {Options} [options]
|
|
* @returns {Multer}
|
|
* @throws {TypeError} If `options` is not an object
|
|
*/
|
|
function multer (options) {
|
|
if (options === undefined) {
|
|
return new Multer({})
|
|
}
|
|
|
|
if (typeof options === 'object' && options !== null) {
|
|
return new Multer(options)
|
|
}
|
|
|
|
throw new TypeError('Expected object for argument options')
|
|
}
|
|
|
|
module.exports = multer
|
|
module.exports.diskStorage = diskStorage
|
|
module.exports.memoryStorage = memoryStorage
|
|
module.exports.MulterError = MulterError
|