| var EventEmitter = require('events').EventEmitter | ||
| function Counter () { | ||
| EventEmitter.call(this) | ||
| this.value = 0 | ||
| } | ||
| Counter.prototype = Object.create(EventEmitter.prototype) | ||
| Counter.prototype.increment = function increment () { | ||
| this.value++ | ||
| } | ||
| Counter.prototype.decrement = function decrement () { | ||
| if (--this.value === 0) this.emit('zero') | ||
| } | ||
| Counter.prototype.isZero = function isZero () { | ||
| return (this.value === 0) | ||
| } | ||
| Counter.prototype.onceZero = function onceZero (fn) { | ||
| if (this.isZero()) return fn() | ||
| this.once('zero', fn) | ||
| } | ||
| module.exports = Counter |
| var is = require('type-is') | ||
| var Busboy = require('busboy') | ||
| var extend = require('xtend') | ||
| var appendField = require('append-field') | ||
| var Counter = require('./counter') | ||
| var MulterError = require('./multer-error') | ||
| var FileAppender = require('./file-appender') | ||
| var removeUploadedFiles = require('./remove-uploaded-files') | ||
| function drainStream (stream) { | ||
| stream.on('readable', () => { | ||
| while (stream.read() !== null) {} | ||
| }) | ||
| } | ||
| function makeMiddleware (setup) { | ||
| return function multerMiddleware (req, res, next) { | ||
| if (!is(req, ['multipart'])) return next() | ||
| var options = setup() | ||
| var limits = options.limits | ||
| var storage = options.storage | ||
| var fileFilter = options.fileFilter | ||
| var fileStrategy = options.fileStrategy | ||
| var preservePath = options.preservePath | ||
| req.body = Object.create(null) | ||
| req.on('error', function (err) { | ||
| abortWithError(err) | ||
| }) | ||
| var busboy | ||
| try { | ||
| busboy = Busboy({ headers: req.headers, limits: limits, preservePath: preservePath }) | ||
| } catch (err) { | ||
| return next(err) | ||
| } | ||
| var appender = new FileAppender(fileStrategy, req) | ||
| var isDone = false | ||
| var readFinished = false | ||
| var errorOccured = false | ||
| var pendingWrites = new Counter() | ||
| var uploadedFiles = [] | ||
| function done (err) { | ||
| if (isDone) return | ||
| isDone = true | ||
| req.unpipe(busboy) | ||
| drainStream(req) | ||
| req.resume() | ||
| setImmediate(() => { | ||
| busboy.removeAllListeners() | ||
| }) | ||
| next(err) | ||
| } | ||
| function indicateDone () { | ||
| if (readFinished && pendingWrites.isZero() && !errorOccured) done() | ||
| } | ||
| function abortWithError (uploadError) { | ||
| if (errorOccured) return | ||
| errorOccured = true | ||
| pendingWrites.onceZero(function () { | ||
| function remove (file, cb) { | ||
| storage._removeFile(req, file, cb) | ||
| } | ||
| removeUploadedFiles(uploadedFiles, remove, function (err, storageErrors) { | ||
| if (err) return done(err) | ||
| uploadError.storageErrors = storageErrors | ||
| done(uploadError) | ||
| }) | ||
| }) | ||
| } | ||
| function abortWithCode (code, optionalField) { | ||
| abortWithError(new MulterError(code, optionalField)) | ||
| } | ||
| // handle text field data | ||
| busboy.on('field', function (fieldname, value, { nameTruncated, valueTruncated }) { | ||
| if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME') | ||
| if (nameTruncated) return abortWithCode('LIMIT_FIELD_KEY') | ||
| if (valueTruncated) return abortWithCode('LIMIT_FIELD_VALUE', fieldname) | ||
| // Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6) | ||
| if (limits && Object.prototype.hasOwnProperty.call(limits, 'fieldNameSize')) { | ||
| if (fieldname.length > limits.fieldNameSize) return abortWithCode('LIMIT_FIELD_KEY') | ||
| } | ||
| appendField(req.body, fieldname, value) | ||
| }) | ||
| // handle files | ||
| busboy.on('file', function (fieldname, fileStream, { filename, encoding, mimeType }) { | ||
| // don't attach to the files object, if there is no file | ||
| if (!filename) return fileStream.resume() | ||
| // Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6) | ||
| if (limits && Object.prototype.hasOwnProperty.call(limits, 'fieldNameSize')) { | ||
| if (fieldname.length > limits.fieldNameSize) return abortWithCode('LIMIT_FIELD_KEY') | ||
| } | ||
| var file = { | ||
| fieldname: fieldname, | ||
| originalname: filename, | ||
| encoding: encoding, | ||
| mimetype: mimeType | ||
| } | ||
| var placeholder = appender.insertPlaceholder(file) | ||
| fileFilter(req, file, function (err, includeFile) { | ||
| if (err) { | ||
| appender.removePlaceholder(placeholder) | ||
| return abortWithError(err) | ||
| } | ||
| if (!includeFile) { | ||
| appender.removePlaceholder(placeholder) | ||
| return fileStream.resume() | ||
| } | ||
| var aborting = false | ||
| pendingWrites.increment() | ||
| Object.defineProperty(file, 'stream', { | ||
| configurable: true, | ||
| enumerable: false, | ||
| value: fileStream | ||
| }) | ||
| fileStream.on('error', function (err) { | ||
| pendingWrites.decrement() | ||
| abortWithError(err) | ||
| }) | ||
| fileStream.on('limit', function () { | ||
| aborting = true | ||
| abortWithCode('LIMIT_FILE_SIZE', fieldname) | ||
| }) | ||
| storage._handleFile(req, file, function (err, info) { | ||
| if (aborting) { | ||
| appender.removePlaceholder(placeholder) | ||
| uploadedFiles.push(extend(file, info)) | ||
| return pendingWrites.decrement() | ||
| } | ||
| if (err) { | ||
| appender.removePlaceholder(placeholder) | ||
| pendingWrites.decrement() | ||
| return abortWithError(err) | ||
| } | ||
| var fileInfo = extend(file, info) | ||
| appender.replacePlaceholder(placeholder, fileInfo) | ||
| uploadedFiles.push(fileInfo) | ||
| pendingWrites.decrement() | ||
| indicateDone() | ||
| }) | ||
| }) | ||
| }) | ||
| busboy.on('error', function (err) { abortWithError(err) }) | ||
| busboy.on('partsLimit', function () { abortWithCode('LIMIT_PART_COUNT') }) | ||
| busboy.on('filesLimit', function () { abortWithCode('LIMIT_FILE_COUNT') }) | ||
| busboy.on('fieldsLimit', function () { abortWithCode('LIMIT_FIELD_COUNT') }) | ||
| busboy.on('close', function () { | ||
| readFinished = true | ||
| indicateDone() | ||
| }) | ||
| req.pipe(busboy) | ||
| } | ||
| } | ||
| module.exports = makeMiddleware |
| var util = require('util') | ||
| var errorMessages = { | ||
| LIMIT_PART_COUNT: 'Too many parts', | ||
| LIMIT_FILE_SIZE: 'File too large', | ||
| LIMIT_FILE_COUNT: 'Too many files', | ||
| LIMIT_FIELD_KEY: 'Field name too long', | ||
| LIMIT_FIELD_VALUE: 'Field value too long', | ||
| LIMIT_FIELD_COUNT: 'Too many fields', | ||
| LIMIT_UNEXPECTED_FILE: 'Unexpected field', | ||
| MISSING_FIELD_NAME: 'Field name missing' | ||
| } | ||
| function MulterError (code, field) { | ||
| Error.captureStackTrace(this, this.constructor) | ||
| this.name = this.constructor.name | ||
| this.message = errorMessages[code] | ||
| this.code = code | ||
| if (field) this.field = field | ||
| } | ||
| util.inherits(MulterError, Error) | ||
| module.exports = MulterError |
| function removeUploadedFiles (uploadedFiles, remove, cb) { | ||
| var length = uploadedFiles.length | ||
| var errors = [] | ||
| if (length === 0) return cb(null, errors) | ||
| function handleFile (idx) { | ||
| var file = uploadedFiles[idx] | ||
| remove(file, function (err) { | ||
| if (err) { | ||
| err.file = file | ||
| err.field = file.fieldname | ||
| errors.push(err) | ||
| } | ||
| if (idx < length - 1) { | ||
| handleFile(idx + 1) | ||
| } else { | ||
| cb(null, errors) | ||
| } | ||
| }) | ||
| } | ||
| handleFile(0) | ||
| } | ||
| module.exports = removeUploadedFiles |
| var fs = require('fs') | ||
| var os = require('os') | ||
| var path = require('path') | ||
| var crypto = require('crypto') | ||
| var mkdirp = require('mkdirp') | ||
| function getFilename (req, file, cb) { | ||
| crypto.randomBytes(16, function (err, raw) { | ||
| cb(err, err ? undefined : raw.toString('hex')) | ||
| }) | ||
| } | ||
| function getDestination (req, file, cb) { | ||
| cb(null, os.tmpdir()) | ||
| } | ||
| function DiskStorage (opts) { | ||
| this.getFilename = (opts.filename || getFilename) | ||
| if (typeof opts.destination === 'string') { | ||
| mkdirp.sync(opts.destination) | ||
| this.getDestination = function ($0, $1, cb) { cb(null, opts.destination) } | ||
| } else { | ||
| this.getDestination = (opts.destination || getDestination) | ||
| } | ||
| } | ||
| DiskStorage.prototype._handleFile = function _handleFile (req, file, cb) { | ||
| var that = this | ||
| that.getDestination(req, file, function (err, destination) { | ||
| if (err) return cb(err) | ||
| that.getFilename(req, file, function (err, filename) { | ||
| if (err) return cb(err) | ||
| var finalPath = path.join(destination, filename) | ||
| var outStream = fs.createWriteStream(finalPath) | ||
| file.stream.pipe(outStream) | ||
| outStream.on('error', cb) | ||
| outStream.on('finish', function () { | ||
| cb(null, { | ||
| destination: destination, | ||
| filename: filename, | ||
| path: finalPath, | ||
| size: outStream.bytesWritten | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
| } | ||
| DiskStorage.prototype._removeFile = function _removeFile (req, file, cb) { | ||
| var path = file.path | ||
| delete file.destination | ||
| delete file.filename | ||
| delete file.path | ||
| fs.unlink(path, cb) | ||
| } | ||
| module.exports = function (opts) { | ||
| return new DiskStorage(opts) | ||
| } |
| var concat = require('concat-stream') | ||
| function MemoryStorage (opts) {} | ||
| MemoryStorage.prototype._handleFile = function _handleFile (req, file, cb) { | ||
| file.stream.pipe(concat({ encoding: 'buffer' }, function (data) { | ||
| cb(null, { | ||
| buffer: data, | ||
| size: data.length | ||
| }) | ||
| })) | ||
| } | ||
| MemoryStorage.prototype._removeFile = function _removeFile (req, file, cb) { | ||
| delete file.buffer | ||
| cb(null) | ||
| } | ||
| module.exports = function (opts) { | ||
| return new MemoryStorage(opts) | ||
| } |
+83
-51
@@ -1,72 +0,104 @@ | ||
| import bytes from 'bytes' | ||
| var makeMiddleware = require('./lib/make-middleware') | ||
| import createFileFilter from './lib/file-filter.js' | ||
| import createMiddleware from './lib/middleware.js' | ||
| var diskStorage = require('./storage/disk') | ||
| var memoryStorage = require('./storage/memory') | ||
| var MulterError = require('./lib/multer-error') | ||
| const kLimits = Symbol('limits') | ||
| function parseLimit (limits, key, defaultValue) { | ||
| const input = limits[key] == null ? defaultValue : limits[key] | ||
| const value = bytes.parse(input) | ||
| if (!Number.isFinite(value)) throw new Error(`Invalid limit "${key}" given: ${limits[key]}`) | ||
| if (!Number.isInteger(value)) throw new Error(`Invalid limit "${key}" given: ${value}`) | ||
| return value | ||
| function allowAll (req, file, cb) { | ||
| cb(null, true) | ||
| } | ||
| function _middleware (limits, fields, fileStrategy) { | ||
| return createMiddleware(() => ({ | ||
| fields: fields, | ||
| limits: limits, | ||
| fileFilter: createFileFilter(fields), | ||
| fileStrategy: fileStrategy | ||
| })) | ||
| function Multer (options) { | ||
| if (options.storage) { | ||
| this.storage = options.storage | ||
| } else if (options.dest) { | ||
| this.storage = diskStorage({ destination: options.dest }) | ||
| } else { | ||
| this.storage = memoryStorage() | ||
| } | ||
| this.limits = options.limits | ||
| this.preservePath = options.preservePath | ||
| this.fileFilter = options.fileFilter || allowAll | ||
| } | ||
| class Multer { | ||
| constructor (options) { | ||
| this[kLimits] = { | ||
| fieldNameSize: parseLimit(options.limits || {}, 'fieldNameSize', '100B'), | ||
| fieldSize: parseLimit(options.limits || {}, 'fieldSize', '8KB'), | ||
| fields: parseLimit(options.limits || {}, 'fields', 1000), | ||
| fileSize: parseLimit(options.limits || {}, 'fileSize', '8MB'), | ||
| files: parseLimit(options.limits || {}, 'files', 10), | ||
| headerPairs: parseLimit(options.limits || {}, 'headerPairs', 2000) | ||
| 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)) | ||
| } | ||
| filesLeft[file.fieldname] -= 1 | ||
| fileFilter(req, file, cb) | ||
| } | ||
| } | ||
| single (name) { | ||
| return _middleware(this[kLimits], [{ name: name, maxCount: 1 }], 'VALUE') | ||
| return { | ||
| limits: this.limits, | ||
| preservePath: this.preservePath, | ||
| storage: this.storage, | ||
| fileFilter: wrappedFileFilter, | ||
| fileStrategy: fileStrategy | ||
| } | ||
| } | ||
| array (name, maxCount) { | ||
| return _middleware(this[kLimits], [{ name: name, maxCount: maxCount }], 'ARRAY') | ||
| } | ||
| return makeMiddleware(setup.bind(this)) | ||
| } | ||
| fields (fields) { | ||
| return _middleware(this[kLimits], fields, 'OBJECT') | ||
| } | ||
| Multer.prototype.single = function (name) { | ||
| return this._makeMiddleware([{ name: name, maxCount: 1 }], 'VALUE') | ||
| } | ||
| none () { | ||
| return _middleware(this[kLimits], [], 'NONE') | ||
| } | ||
| Multer.prototype.array = function (name, maxCount) { | ||
| return this._makeMiddleware([{ name: name, maxCount: maxCount }], 'ARRAY') | ||
| } | ||
| any () { | ||
| return createMiddleware(() => ({ | ||
| fields: [], | ||
| limits: this[kLimits], | ||
| fileFilter: () => {}, | ||
| Multer.prototype.fields = function (fields) { | ||
| return this._makeMiddleware(fields, 'OBJECT') | ||
| } | ||
| Multer.prototype.none = function () { | ||
| return this._makeMiddleware([], 'NONE') | ||
| } | ||
| Multer.prototype.any = function () { | ||
| function setup () { | ||
| return { | ||
| limits: this.limits, | ||
| preservePath: this.preservePath, | ||
| storage: this.storage, | ||
| fileFilter: this.fileFilter, | ||
| fileStrategy: 'ARRAY' | ||
| })) | ||
| } | ||
| } | ||
| return makeMiddleware(setup.bind(this)) | ||
| } | ||
| export default function multer (options = {}) { | ||
| if (options === null) throw new TypeError('Expected object for argument "options", got null') | ||
| if (typeof options !== 'object') throw new TypeError(`Expected object for argument "options", got ${typeof options}`) | ||
| function multer (options) { | ||
| if (options === undefined) { | ||
| return new Multer({}) | ||
| } | ||
| if (options.dest || options.storage || options.fileFilter) { | ||
| throw new Error('The "dest", "storage" and "fileFilter" options where removed in Multer 2.0. Please refer to the latest documentation for new usage.') | ||
| if (typeof options === 'object' && options !== null) { | ||
| return new Multer(options) | ||
| } | ||
| 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 |
+57
-14
@@ -1,24 +0,67 @@ | ||
| export default function createFileAppender (strategy, req, fields) { | ||
| var objectAssign = require('object-assign') | ||
| function arrayRemove (arr, item) { | ||
| var idx = arr.indexOf(item) | ||
| if (~idx) arr.splice(idx, 1) | ||
| } | ||
| function FileAppender (strategy, req) { | ||
| this.strategy = strategy | ||
| this.req = req | ||
| switch (strategy) { | ||
| case 'NONE': break | ||
| case 'VALUE': req.file = null; break | ||
| case 'VALUE': break | ||
| case 'ARRAY': req.files = []; break | ||
| case 'OBJECT': req.files = Object.create(null); break | ||
| /* c8 ignore next */ | ||
| default: throw new Error(`Unknown file strategy: ${strategy}`) | ||
| default: throw new Error('Unknown file strategy: ' + strategy) | ||
| } | ||
| } | ||
| if (strategy === 'OBJECT') { | ||
| for (const field of fields) { | ||
| req.files[field.name] = [] | ||
| } | ||
| FileAppender.prototype.insertPlaceholder = function (file) { | ||
| var placeholder = { | ||
| fieldname: file.fieldname | ||
| } | ||
| return function append (file) { | ||
| switch (strategy) { | ||
| case 'VALUE': req.file = file; break | ||
| case 'ARRAY': req.files.push(file); break | ||
| case 'OBJECT': req.files[file.fieldName].push(file); break | ||
| } | ||
| switch (this.strategy) { | ||
| case 'NONE': break | ||
| case 'VALUE': break | ||
| case 'ARRAY': this.req.files.push(placeholder); break | ||
| case 'OBJECT': | ||
| if (this.req.files[file.fieldname]) { | ||
| this.req.files[file.fieldname].push(placeholder) | ||
| } else { | ||
| this.req.files[file.fieldname] = [placeholder] | ||
| } | ||
| break | ||
| } | ||
| return placeholder | ||
| } | ||
| FileAppender.prototype.removePlaceholder = function (placeholder) { | ||
| switch (this.strategy) { | ||
| case 'NONE': break | ||
| case 'VALUE': break | ||
| case 'ARRAY': arrayRemove(this.req.files, placeholder); break | ||
| case 'OBJECT': | ||
| if (this.req.files[placeholder.fieldname].length === 1) { | ||
| delete this.req.files[placeholder.fieldname] | ||
| } else { | ||
| arrayRemove(this.req.files[placeholder.fieldname], placeholder) | ||
| } | ||
| break | ||
| } | ||
| } | ||
| FileAppender.prototype.replacePlaceholder = function (placeholder, file) { | ||
| if (this.strategy === 'VALUE') { | ||
| this.req.file = file | ||
| return | ||
| } | ||
| delete placeholder.fieldname | ||
| objectAssign(placeholder, file) | ||
| } | ||
| module.exports = FileAppender |
+22
-21
| { | ||
| "name": "multer", | ||
| "description": "Middleware for handling `multipart/form-data`.", | ||
| "version": "2.0.0-rc.4", | ||
| "version": "2.0.0", | ||
| "contributors": [ | ||
@@ -12,4 +12,2 @@ "Hage Yaapa <captain@hacksparrow.com> (http://www.hacksparrow.com)", | ||
| "repository": "expressjs/multer", | ||
| "type": "module", | ||
| "exports": "./index.js", | ||
| "keywords": [ | ||
@@ -25,24 +23,22 @@ "form", | ||
| "dependencies": { | ||
| "@fastify/busboy": "^1.0.0", | ||
| "append-field": "^2.0.0", | ||
| "bytes": "^3.1.0", | ||
| "fs-temp": "^2.0.1", | ||
| "has-own-property": "^2.0.0", | ||
| "on-finished": "^2.3.0", | ||
| "stream-file-type": "^0.6.1", | ||
| "type-is": "^1.6.18" | ||
| "append-field": "^1.0.0", | ||
| "busboy": "^1.0.0", | ||
| "concat-stream": "^1.5.2", | ||
| "mkdirp": "^0.5.4", | ||
| "object-assign": "^4.1.1", | ||
| "type-is": "^1.6.4", | ||
| "xtend": "^4.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "c8": "^7.7.3", | ||
| "express": "^4.16.4", | ||
| "form-data": "^4.0.0", | ||
| "get-stream": "^6.0.1", | ||
| "hasha": "^5.2.0", | ||
| "mocha": "^9.0.3", | ||
| "recursive-nullify": "^1.0.0", | ||
| "standard": "^16.0.3", | ||
| "deep-equal": "^2.0.3", | ||
| "express": "^4.13.1", | ||
| "form-data": "^1.0.0-rc1", | ||
| "fs-temp": "^1.1.2", | ||
| "mocha": "^11.3.0", | ||
| "rimraf": "^2.4.1", | ||
| "standard": "^14.3.3", | ||
| "testdata-w3c-json-form": "^1.0.0" | ||
| }, | ||
| "engines": { | ||
| "node": "^12.20.0 || ^14.13.1 || >=16.0.0" | ||
| "node": ">= 10.16.0" | ||
| }, | ||
@@ -52,7 +48,12 @@ "files": [ | ||
| "index.js", | ||
| "storage/", | ||
| "lib/" | ||
| ], | ||
| "scripts": { | ||
| "test": "standard && c8 --check-coverage --statements 100 mocha" | ||
| "lint": "standard", | ||
| "lint:fix": "standard --fix", | ||
| "test": "mocha --reporter spec --exit --check-leaks test/", | ||
| "test-ci": "nyc --reporter=lcov --reporter=text npm test", | ||
| "test-cov": "nyc --reporter=html --reporter=text npm test" | ||
| } | ||
| } |
+209
-48
@@ -1,2 +0,2 @@ | ||
| # Multer [](https://travis-ci.org/expressjs/multer) [](https://badge.fury.io/js/multer) [](https://github.com/feross/standard) | ||
| # Multer [](https://github.com/expressjs/multer/actions/workflows/ci.yml) [](https://coveralls.io/r/expressjs/multer?branch=master) [](https://badge.fury.io/js/multer) [](https://github.com/feross/standard) | ||
@@ -8,6 +8,17 @@ Multer is a node.js middleware for handling `multipart/form-data`, which is primarily used for uploading files. It is written | ||
| ## Translations | ||
| This README is also available in other languages: | ||
| - [Español](https://github.com/expressjs/multer/blob/master/doc/README-es.md) (Spanish) | ||
| - [简体中文](https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md) (Chinese) | ||
| - [한국어](https://github.com/expressjs/multer/blob/master/doc/README-ko.md) (Korean) | ||
| - [Русский язык](https://github.com/expressjs/multer/blob/master/doc/README-ru.md) (Russian) | ||
| - [Việt Nam](https://github.com/expressjs/multer/blob/master/doc/README-vi.md) (Vietnam) | ||
| - [Português](https://github.com/expressjs/multer/blob/master/doc/README-pt-br.md) (Portuguese Brazil) | ||
| ## Installation | ||
| ```sh | ||
| npm install --save multer | ||
| $ npm install --save multer | ||
| ``` | ||
@@ -21,10 +32,18 @@ | ||
| Don't forget the `enctype="multipart/form-data"` in your form. | ||
| ```html | ||
| <form action="/profile" method="post" enctype="multipart/form-data"> | ||
| <input type="file" name="avatar" /> | ||
| </form> | ||
| ``` | ||
| ```javascript | ||
| import multer from 'multer' | ||
| import express from 'express' | ||
| const express = require('express') | ||
| const multer = require('multer') | ||
| const upload = multer({ dest: 'uploads/' }) | ||
| const app = express() | ||
| const upload = multer() | ||
| app.post('/profile', upload.single('avatar'), (req, res, next) => { | ||
| app.post('/profile', upload.single('avatar'), function (req, res, next) { | ||
| // req.file is the `avatar` file | ||
@@ -34,3 +53,3 @@ // req.body will hold the text fields, if there were any | ||
| app.post('/photos/upload', upload.array('photos', 12), (req, res, next) => { | ||
| app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) { | ||
| // req.files is array of `photos` files | ||
@@ -41,3 +60,3 @@ // req.body will contain the text fields, if there were any | ||
| const cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }]) | ||
| app.post('/cool-profile', cpUpload, (req, res, next) => { | ||
| app.post('/cool-profile', cpUpload, function (req, res, next) { | ||
| // req.files is an object (String -> Array) where fieldname is the key, and the value is array of files | ||
@@ -53,12 +72,11 @@ // | ||
| In case you need to handle a text-only multipart form, you can use the `.none()` method, example: | ||
| In case you need to handle a text-only multipart form, you should use the `.none()` method: | ||
| ```javascript | ||
| import multer from 'multer' | ||
| import express from 'express' | ||
| const express = require('express') | ||
| const app = express() | ||
| const multer = require('multer') | ||
| const upload = multer() | ||
| app.post('/profile', upload.none(), (req, res, next) => { | ||
| app.post('/profile', upload.none(), function (req, res, next) { | ||
| // req.body contains the text fields | ||
@@ -68,2 +86,28 @@ }) | ||
| Here's an example on how multer is used an HTML form. Take special note of the `enctype="multipart/form-data"` and `name="uploaded_file"` fields: | ||
| ```html | ||
| <form action="/stats" enctype="multipart/form-data" method="post"> | ||
| <div class="form-group"> | ||
| <input type="file" class="form-control-file" name="uploaded_file"> | ||
| <input type="text" class="form-control" placeholder="Number of speakers" name="nspeakers"> | ||
| <input type="submit" value="Get me the stats!" class="btn btn-default"> | ||
| </div> | ||
| </form> | ||
| ``` | ||
| Then in your javascript file you would add these lines to access both the file and the body. It is important that you use the `name` field value from the form in your upload function. This tells multer which field on the request it should look for the files in. If these fields aren't the same in the HTML form and on your server, your upload will fail: | ||
| ```javascript | ||
| const multer = require('multer') | ||
| const upload = multer({ dest: './public/data/uploads/' }) | ||
| app.post('/stats', upload.single('uploaded_file'), function (req, res) { | ||
| // req.file is the name of your file in the form above, here 'uploaded_file' | ||
| // req.body will hold the text fields, if there were any | ||
| console.log(req.file, req.body) | ||
| }); | ||
| ``` | ||
| ## API | ||
@@ -75,24 +119,43 @@ | ||
| Key | Description | Note | ||
| --- | --- | --- | ||
| `fieldname` | Field name specified in the form | | ||
| `originalname` | Name of the file on the user's computer | | ||
| `encoding` | Encoding type of the file | | ||
| `mimetype` | Mime type of the file | | ||
| `size` | Size of the file in bytes | | ||
| `destination` | The folder to which the file has been saved | `DiskStorage` | ||
| `filename` | The name of the file within the `destination` | `DiskStorage` | ||
| `path` | The full path to the uploaded file | `DiskStorage` | ||
| `buffer` | A `Buffer` of the entire file | `MemoryStorage` | ||
| ### `multer(opts)` | ||
| Multer accepts an options object, the most basic of which is the `dest` | ||
| property, which tells Multer where to upload the files. In case you omit the | ||
| options object, the files will be kept in memory and never written to disk. | ||
| By default, Multer will rename the files so as to avoid naming conflicts. The | ||
| renaming function can be customized according to your needs. | ||
| The following are the options that can be passed to Multer. | ||
| Key | Description | ||
| --- | --- | ||
| `fieldName` | Field name specified in the form | ||
| `originalName` | Name of the file on the user's computer (`undefined` if no filename was supplied by the client) | ||
| `size` | Total size of the file in bytes | ||
| `stream` | Readable stream of file data | ||
| `detectedMimeType` | The detected mime-type, or null if we failed to detect | ||
| `detectedFileExtension` | The typical file extension for files of the detected type, or empty string if we failed to detect (with leading `.` to match `path.extname`) | ||
| `clientReportedMimeType` | The mime type reported by the client using the `Content-Type` header, or null<sup>1</sup> if the header was absent | ||
| `clientReportedFileExtension` | The extension of the file uploaded (as reported by `path.extname`) | ||
| `dest` or `storage` | Where to store the files | ||
| `fileFilter` | Function to control which files are accepted | ||
| `limits` | Limits of the uploaded data | ||
| `preservePath` | Keep the full path of files instead of just the base name | ||
| <sup>1</sup> Currently returns `text/plain` if header is absent, this is a bug and it will be fixed in a patch release. Do not rely on this behavior. | ||
| In an average web app, only `dest` might be required, and configured as shown in | ||
| the following example. | ||
| ### `multer(opts)` | ||
| ```javascript | ||
| const upload = multer({ dest: 'uploads/' }) | ||
| ``` | ||
| Multer accepts an options object, the following are the options that can be | ||
| passed to Multer. | ||
| If you want more control over your uploads, you'll want to use the `storage` | ||
| option instead of `dest`. Multer ships with storage engines `DiskStorage` | ||
| and `MemoryStorage`; More engines are available from third parties. | ||
| Key | Description | ||
| -------- | ----------- | ||
| `limits` | Limits of the uploaded data [(full description)](#limits) | ||
| #### `.single(fieldname)` | ||
@@ -127,3 +190,3 @@ | ||
| Accept only text fields. If any file upload is made, error with code | ||
| "LIMIT\_UNEXPECTED\_FILE" will be issued. This is the same as doing `upload.fields([])`. | ||
| "LIMIT\_UNEXPECTED\_FILE" will be issued. | ||
@@ -140,2 +203,68 @@ #### `.any()` | ||
| ### `storage` | ||
| #### `DiskStorage` | ||
| The disk storage engine gives you full control on storing files to disk. | ||
| ```javascript | ||
| const storage = multer.diskStorage({ | ||
| destination: function (req, file, cb) { | ||
| cb(null, '/tmp/my-uploads') | ||
| }, | ||
| filename: function (req, file, cb) { | ||
| const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) | ||
| cb(null, file.fieldname + '-' + uniqueSuffix) | ||
| } | ||
| }) | ||
| const upload = multer({ storage: storage }) | ||
| ``` | ||
| There are two options available, `destination` and `filename`. They are both | ||
| functions that determine where the file should be stored. | ||
| `destination` is used to determine within which folder the uploaded files should | ||
| be stored. This can also be given as a `string` (e.g. `'/tmp/uploads'`). If no | ||
| `destination` is given, the operating system's default directory for temporary | ||
| files is used. | ||
| **Note:** You are responsible for creating the directory when providing | ||
| `destination` as a function. When passing a string, multer will make sure that | ||
| the directory is created for you. | ||
| `filename` is used to determine what the file should be named inside the folder. | ||
| If no `filename` is given, each file will be given a random name that doesn't | ||
| include any file extension. | ||
| **Note:** Multer will not append any file extension for you, your function | ||
| should return a filename complete with an file extension. | ||
| Each function gets passed both the request (`req`) and some information about | ||
| the file (`file`) to aid with the decision. | ||
| Note that `req.body` might not have been fully populated yet. It depends on the | ||
| order that the client transmits fields and files to the server. | ||
| For understanding the calling convention used in the callback (needing to pass | ||
| null as the first param), refer to | ||
| [Node.js error handling](https://www.joyent.com/node-js/production/design/errors) | ||
| #### `MemoryStorage` | ||
| The memory storage engine stores the files in memory as `Buffer` objects. It | ||
| doesn't have any options. | ||
| ```javascript | ||
| const storage = multer.memoryStorage() | ||
| const upload = multer({ storage: storage }) | ||
| ``` | ||
| When using memory storage, the file info will contain a field called | ||
| `buffer` that contains the entire file. | ||
| **WARNING**: Uploading very large files, or relatively small files in large | ||
| numbers very quickly, can cause your application to run out of memory when | ||
| memory storage is used. | ||
| ### `limits` | ||
@@ -145,38 +274,70 @@ | ||
| The following limits are available: | ||
| The following integer values are available: | ||
| Key | Description | Default | ||
| --- | --- | --- | ||
| `fieldNameSize` | Max number of bytes per field name | `'100B'` | ||
| `fieldSize` | Max number of bytes per field value | `'8KB'` | ||
| `fields` | Max number of fields per request | `1000` | ||
| `fileSize` | Max number of bytes per file | `'8MB'` | ||
| `files` | Max number of files per request | `10` | ||
| `headerPairs` | Max number of header key-value pairs | `2000` (same as Node's http) | ||
| `fieldNameSize` | Max field name size | 100 bytes | ||
| `fieldSize` | Max field value size (in bytes) | 1MB | ||
| `fields` | Max number of non-file fields | Infinity | ||
| `fileSize` | For multipart forms, the max file size (in bytes) | Infinity | ||
| `files` | For multipart forms, the max number of file fields | Infinity | ||
| `parts` | For multipart forms, the max number of parts (fields + files) | Infinity | ||
| `headerPairs` | For multipart forms, the max number of header key=>value pairs to parse | 2000 | ||
| Bytes limits can be passed either as a number, or as a string with an appropriate prefix. | ||
| Specifying the limits can help protect your site against denial of service (DoS) attacks. | ||
| ### `fileFilter` | ||
| Set this to a function to control which files should be uploaded and which | ||
| should be skipped. The function should look like this: | ||
| ```javascript | ||
| function fileFilter (req, file, cb) { | ||
| // The function should call `cb` with a boolean | ||
| // to indicate if the file should be accepted | ||
| // To reject this file pass `false`, like so: | ||
| cb(null, false) | ||
| // To accept the file pass `true`, like so: | ||
| cb(null, true) | ||
| // You can always pass an error if something goes wrong: | ||
| cb(new Error('I don\'t have a clue!')) | ||
| } | ||
| ``` | ||
| ## Error handling | ||
| When encountering an error, multer will delegate the error to express. You can | ||
| When encountering an error, Multer will delegate the error to Express. You can | ||
| display a nice error page using [the standard express way](http://expressjs.com/guide/error-handling.html). | ||
| If you want to catch errors specifically from multer, you can call the | ||
| middleware function by yourself. | ||
| If you want to catch errors specifically from Multer, you can call the | ||
| middleware function by yourself. Also, if you want to catch only [the Multer errors](https://github.com/expressjs/multer/blob/master/lib/multer-error.js), you can use the `MulterError` class that is attached to the `multer` object itself (e.g. `err instanceof multer.MulterError`). | ||
| ```javascript | ||
| const multer = require('multer') | ||
| const upload = multer().single('avatar') | ||
| app.post('/profile', (req, res) => { | ||
| upload(req, res, (err) => { | ||
| if (err) { | ||
| // An error occurred when uploading | ||
| return | ||
| app.post('/profile', function (req, res) { | ||
| upload(req, res, function (err) { | ||
| if (err instanceof multer.MulterError) { | ||
| // A Multer error occurred when uploading. | ||
| } else if (err) { | ||
| // An unknown error occurred when uploading. | ||
| } | ||
| // Everything went fine | ||
| // Everything went fine. | ||
| }) | ||
| }) | ||
| ``` | ||
| ## Custom storage engine | ||
| For information on how to build your own storage engine, see [Multer Storage Engine](https://github.com/expressjs/multer/blob/master/StorageEngine.md). | ||
| ## License | ||
| [MIT](LICENSE) |
-21
| const errorMessages = new Map([ | ||
| ['CLIENT_ABORTED', 'Client aborted'], | ||
| ['LIMIT_FILE_SIZE', 'File too large'], | ||
| ['LIMIT_FILE_COUNT', 'Too many files'], | ||
| ['LIMIT_FIELD_KEY', 'Field name too long'], | ||
| ['LIMIT_FIELD_VALUE', 'Field value too long'], | ||
| ['LIMIT_FIELD_COUNT', 'Too many fields'], | ||
| ['LIMIT_UNEXPECTED_FILE', 'Unexpected file field'] | ||
| ]) | ||
| export default class MulterError extends Error { | ||
| constructor (code, optionalField) { | ||
| super(errorMessages.get(code)) | ||
| this.code = code | ||
| this.name = this.constructor.name | ||
| if (optionalField) this.field = optionalField | ||
| Error.captureStackTrace(this, this.constructor) | ||
| } | ||
| } |
| import MulterError from './error.js' | ||
| export default function createFileFilter (fields) { | ||
| const filesLeft = new Map() | ||
| for (const field of fields) { | ||
| if (typeof field.maxCount === 'number') { | ||
| filesLeft.set(field.name, field.maxCount) | ||
| } else { | ||
| filesLeft.set(field.name, Infinity) | ||
| } | ||
| } | ||
| return function fileFilter (file) { | ||
| if (!filesLeft.has(file.fieldName)) { | ||
| throw new MulterError('LIMIT_UNEXPECTED_FILE', file.fieldName) | ||
| } | ||
| const left = filesLeft.get(file.fieldName) | ||
| if (left <= 0) { | ||
| throw new MulterError('LIMIT_FILE_COUNT', file.fieldName) | ||
| } | ||
| filesLeft.set(file.fieldName, left - 1) | ||
| } | ||
| } |
| import fs from 'node:fs' | ||
| import appendField from 'append-field' | ||
| import is from 'type-is' | ||
| import createFileAppender from './file-appender.js' | ||
| import readBody from './read-body.js' | ||
| async function handleRequest (setup, req) { | ||
| const options = setup() | ||
| const result = await readBody(req, options.limits, options.fileFilter) | ||
| req.body = Object.create(null) | ||
| for (const field of result.fields) { | ||
| appendField(req.body, field.key, field.value) | ||
| } | ||
| const appendFile = createFileAppender(options.fileStrategy, req, options.fields) | ||
| for (const file of result.files) { | ||
| file.stream = fs.createReadStream(file.path) | ||
| file.stream.on('open', () => fs.unlink(file.path, () => {})) | ||
| appendFile(file) | ||
| } | ||
| } | ||
| export default function createMiddleware (setup) { | ||
| return function multerMiddleware (req, _, next) { | ||
| if (!is(req, ['multipart'])) return next() | ||
| handleRequest(setup, req).then(next, next) | ||
| } | ||
| } |
-130
| import { extname } from 'node:path' | ||
| import { pipeline as _pipeline } from 'node:stream' | ||
| import { promisify } from 'node:util' | ||
| import Busboy from '@fastify/busboy' | ||
| import { createWriteStream } from 'fs-temp' | ||
| import hasOwnProperty from 'has-own-property' | ||
| import _onFinished from 'on-finished' | ||
| import FileType from 'stream-file-type' | ||
| import MulterError from './error.js' | ||
| const onFinished = promisify(_onFinished) | ||
| const pipeline = promisify(_pipeline) | ||
| function drainStream (stream) { | ||
| stream.on('readable', stream.read.bind(stream)) | ||
| } | ||
| function collectFields (busboy, limits) { | ||
| return new Promise((resolve, reject) => { | ||
| const result = [] | ||
| busboy.on('field', (fieldname, value, fieldnameTruncated, valueTruncated) => { | ||
| // Currently not implemented (https://github.com/mscdex/busboy/issues/6) | ||
| /* c8 ignore next */ | ||
| if (fieldnameTruncated) return reject(new MulterError('LIMIT_FIELD_KEY')) | ||
| if (valueTruncated) return reject(new MulterError('LIMIT_FIELD_VALUE', fieldname)) | ||
| // Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6) | ||
| if (limits && hasOwnProperty(limits, 'fieldNameSize') && fieldname.length > limits.fieldNameSize) { | ||
| return reject(new MulterError('LIMIT_FIELD_KEY')) | ||
| } | ||
| result.push({ key: fieldname, value: value }) | ||
| }) | ||
| busboy.on('finish', () => resolve(result)) | ||
| }) | ||
| } | ||
| function collectFiles (busboy, limits, fileFilter) { | ||
| return new Promise((resolve, reject) => { | ||
| const result = [] | ||
| busboy.on('file', async (fieldname, fileStream, filename, encoding, mimetype) => { | ||
| // Catch all errors on file stream | ||
| fileStream.on('error', reject) | ||
| // Catch limit exceeded on file stream | ||
| fileStream.on('limit', () => { | ||
| reject(new MulterError('LIMIT_FILE_SIZE', fieldname)) | ||
| }) | ||
| // Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6) | ||
| if (limits && hasOwnProperty(limits, 'fieldNameSize') && fieldname.length > limits.fieldNameSize) { | ||
| return reject(new MulterError('LIMIT_FIELD_KEY')) | ||
| } | ||
| const file = { | ||
| fieldName: fieldname, | ||
| originalName: filename, | ||
| clientReportedMimeType: mimetype, | ||
| clientReportedFileExtension: extname(filename || '') | ||
| } | ||
| try { | ||
| fileFilter(file) | ||
| } catch (err) { | ||
| return reject(err) | ||
| } | ||
| const target = createWriteStream() | ||
| const detector = new FileType() | ||
| const fileClosed = new Promise((resolve) => target.on('close', resolve)) | ||
| const promise = pipeline(fileStream, detector, target) | ||
| .then(async () => { | ||
| await fileClosed | ||
| file.path = target.path | ||
| file.size = target.bytesWritten | ||
| const fileType = await detector.fileTypePromise() | ||
| file.detectedMimeType = (fileType ? fileType.mime : null) | ||
| file.detectedFileExtension = (fileType ? `.${fileType.ext}` : '') | ||
| return file | ||
| }) | ||
| .catch(reject) | ||
| result.push(promise) | ||
| }) | ||
| busboy.on('finish', () => resolve(Promise.all(result))) | ||
| }) | ||
| } | ||
| export default async function readBody (req, limits, fileFilter) { | ||
| const busboy = new Busboy({ headers: req.headers, limits: limits }) | ||
| const fields = collectFields(busboy, limits) | ||
| const files = collectFiles(busboy, limits, fileFilter) | ||
| const guard = new Promise((resolve, reject) => { | ||
| req.on('error', (err) => reject(err)) | ||
| busboy.on('error', (err) => reject(err)) | ||
| req.on('aborted', () => reject(new MulterError('CLIENT_ABORTED'))) | ||
| busboy.on('filesLimit', () => reject(new MulterError('LIMIT_FILE_COUNT'))) | ||
| busboy.on('fieldsLimit', () => reject(new MulterError('LIMIT_FIELD_COUNT'))) | ||
| busboy.on('finish', resolve) | ||
| }) | ||
| req.pipe(busboy) | ||
| try { | ||
| const result = await Promise.all([fields, files, guard]) | ||
| return { fields: result[0], files: result[1] } | ||
| } catch (err) { | ||
| req.unpipe(busboy) | ||
| drainStream(req) | ||
| busboy.removeAllListeners() | ||
| // Wait for request to close, finish, or error | ||
| await onFinished(req).catch(/* c8 ignore next: Already handled by req.on('error', _) */ () => {}) | ||
| throw err | ||
| } | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
28280
59.34%7
-12.5%8
-11.11%11
22.22%423
72.65%0
-100%1
-50%334
93.06%2
Infinity%No
NaN+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated