diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..4d84292e
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,27 @@
+name: Node CI
+
+on:
+ pull_request:
+ branches:
+ - master
+ push:
+ branches:
+ - explore-new-api
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macOS-latest, windows-latest]
+ node: [12.20.0, 14.13.1, 16.0.0]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js ${{ matrix.node }}
+ uses: actions/setup-node@v2
+ with:
+ node-version: ${{ matrix.node }}
+ - name: Install dependencies
+ run: npm install
+ - name: Run tests
+ run: npm test
diff --git a/.gitignore b/.gitignore
index 38602882..23926199 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,6 @@ node_modules
*.log
*.gz
-
-# Coveralls
-coverage
+# Code Coverage
+/.nyc_output/
+/coverage/
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 00000000..43c97e71
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b5d1eddf..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-sudo: false
-language: node_js
-node_js:
- - "0.10"
- - "0.12"
- - "iojs-v1.8.4"
- - "iojs-v2.5.0"
- - "iojs-v3.3.0"
- - "4"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 082aabbb..3ed70458 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,62 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
+## 2.0.0-rc.3 - 2021-06-29
+
+- Breaking: Convert package to ESM
+
+Migration Guide:
+
+This relases changes the package from a Common JS module to an EcmaScript module, and drops support for older versions of Node.
+
+- The minimum version of Node.js supported is now: `12.20.0`, `14.13.1`, and `16.0.0`
+- The package must now be imported using the native `import` syntax instead of with `require`
+
+## 2.0.0-rc.2 - 2020-03-15
+
+- Allow limits to be passed as string, e.g. `'12MB'`
+- Remove `parts` limit in favour of `fields` & `files`
+- Set reasonable defaults for all limits
+
+## 2.0.0-rc.1 - 2020-02-26
+
+- Breaking: drop support for Node.js < 10.13.x
+- Internal: achive 100% code coverage
+- Internal: test on macOS & Windows
+
+## 2.0.0-beta.1 - 2019-11-23
+
+- Breaking: drop support for Node.js < 8.3.x
+
+## 2.0.0-alpha.7 - 2019-05-03
+
+- Breaking: drop support for Node.js < 6.x
+
+## 2.0.0-alpha.6 - 2017-02-18
+
+- Fix: handle client aborting request
+
+## 2.0.0-alpha.5 - 2017-02-14
+
+- Fix: allow files without filename
+
+## 2.0.0-alpha.4 - 2017-02-14
+
+- Feature: add file type detection
+
+## 2.0.0-alpha.3 - 2016-12-22
+
+- Feature: unlink file as soon as it's opened
+
+## 2.0.0-alpha.2 - 2016-10-02
+
+- Feature: use LIMIT_FILE_COUNT when receiving too many files
+
+## 2.0.0-alpha.1 - 2016-10-01
+
+- Feature: switch to stream based API
+- Feature: throw error when passing old options
+
## 1.2.0 - 2016-08-04
- Feature: add .none() for accepting only fields
diff --git a/README.md b/README.md
index 420ffaae..f8b7dbf2 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ on top of [busboy](https://github.com/mscdex/busboy) for maximum efficiency.
## Installation
```sh
-$ npm install --save multer
+npm install --save multer
```
## Usage
@@ -18,24 +18,24 @@ Multer adds a `body` object and a `file` or `files` object to the `request` obje
Basic usage example:
```javascript
-var express = require('express')
-var multer = require('multer')
-var upload = multer({ dest: 'uploads/' })
+import multer from 'multer'
+import express from 'express'
-var app = express()
+const app = express()
+const upload = multer()
-app.post('/profile', upload.single('avatar'), function (req, res, next) {
+app.post('/profile', upload.single('avatar'), (req, res, next) => {
// req.file is the `avatar` file
// req.body will hold the text fields, if there were any
})
-app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) {
+app.post('/photos/upload', upload.array('photos', 12), (req, res, next) => {
// req.files is array of `photos` files
// req.body will contain the text fields, if there were any
})
-var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])
-app.post('/cool-profile', cpUpload, function (req, res, next) {
+const cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])
+app.post('/cool-profile', cpUpload, (req, res, next) => {
// req.files is an object (String -> Array) where fieldname is the key, and the value is array of files
//
// e.g.
@@ -46,15 +46,16 @@ app.post('/cool-profile', cpUpload, function (req, res, next) {
})
```
-In case you need to handle a text-only multipart form, you can use any of the multer methods (`.single()`, `.array()`, `fields()`). Here is an example using `.array()`:
+In case you need to handle a text-only multipart form, you can use the `.none()` method, example:
```javascript
-var express = require('express')
-var app = express()
-var multer = require('multer')
-var upload = multer()
+import multer from 'multer'
+import express from 'express'
-app.post('/profile', upload.array(), function (req, res, next) {
+const app = express()
+const upload = multer()
+
+app.post('/profile', upload.none(), (req, res, next) => {
// req.body contains the text fields
})
```
@@ -65,45 +66,27 @@ app.post('/profile', upload.array(), function (req, res, next) {
Each file contains the following information:
-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
--- | ---
-`dest` or `storage` | Where to store the files
-`fileFilter` | Function to control which files are accepted
-`limits` | Limits of the uploaded data
+`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 null1 if the header was absent
+`clientReportedFileExtension` | The extension of the file uploaded (as reported by `path.extname`)
-In an average web app, only `dest` might be required, and configured as shown in
-the following example.
+1 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.
-```javascript
-var upload = multer({ dest: 'uploads/' })
-```
+### `multer(opts)`
+
+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)`
@@ -146,107 +129,24 @@ Never add multer as a global middleware since a malicious user could upload
files to a route that you didn't anticipate. Only use this function on routes
where you are handling the uploaded files.
-### `storage`
-
-#### `DiskStorage`
-
-The disk storage engine gives you full control on storing files to disk.
-
-```javascript
-var storage = multer.diskStorage({
- destination: function (req, file, cb) {
- cb(null, '/tmp/my-uploads')
- },
- filename: function (req, file, cb) {
- cb(null, file.fieldname + '-' + Date.now())
- }
-})
-
-var 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.
-
-#### `MemoryStorage`
-
-The memory storage engine stores the files in memory as `Buffer` objects. It
-doesn't have any options.
-
-```javascript
-var storage = multer.memoryStorage()
-var 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`
An object specifying the size limits of the following optional properties. Multer passes this object into busboy directly, and the details of the properties can be found on [busboy's page](https://github.com/mscdex/busboy#busboy-methods).
-The following integer values are available:
+The following limits are available:
Key | Description | Default
--- | --- | ---
-`fieldNameSize` | Max field name size | 100 bytes
-`fieldSize` | Max field value size | 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
-
-Specifying the limits can help protect your site against denial of service (DoS) attacks.
-
-### `fileFilter`
+`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)
-Set this to a function to control which files should be uploaded and which
-should be skipped. The function should look like this:
+Bytes limits can be passed either as a number, or as a string with an appropriate prefix.
-```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!'))
-
-}
-```
+Specifying the limits can help protect your site against denial of service (DoS) attacks.
## Error handling
@@ -257,10 +157,10 @@ If you want to catch errors specifically from multer, you can call the
middleware function by yourself.
```javascript
-var upload = multer().single('avatar')
+const upload = multer().single('avatar')
-app.post('/profile', function (req, res) {
- upload(req, res, function (err) {
+app.post('/profile', (req, res) => {
+ upload(req, res, (err) => {
if (err) {
// An error occurred when uploading
return
@@ -270,12 +170,3 @@ app.post('/profile', function (req, res) {
})
})
```
-
-## Custom storage engine
-
-See [the documentation here](/StorageEngine.md) if you want to build your own
-storage engine.
-
-## License
-
-[MIT](LICENSE)
diff --git a/StorageEngine.md b/StorageEngine.md
deleted file mode 100644
index 4b411ab1..00000000
--- a/StorageEngine.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# Multer Storage Engine
-
-Storage engines are classes that expose two functions: `_handleFile` and `_removeFile`.
-Follow the template below to get started with your own custom storage engine.
-
-When asking the user for input (such as where to save this file), always give
-them the parameters `req, file, cb`, in this order. This makes it easier for
-developers to switch between storage engines.
-
-For example, in the template below, the engine saves the files to the disk. The
-user tells the engine where to save the file, and this is done by
-providing the `destination` parameter:
-
-```javascript
-var storage = myCustomStorage({
- destination: function (req, file, cb) {
- cb(null, '/var/www/uploads/' + file.originalname)
- }
-})
-```
-
-Your engine is responsible for storing the file and returning information on how to
-access the file in the future. This is done by the `_handleFile` function.
-
-The file data will be given to you as a stream (`file.stream`). You should pipe
-this data somewhere, and when you are done, call `cb` with some information on the
-file.
-
-The information you provide in the callback will be merged with multer's file object,
-and then presented to the user via `req.files`.
-
-Your engine is also responsible for removing files if an error is encountered
-later on. Multer will decide which files to delete and when. Your storage class must
-implement the `_removeFile` function. It will receive the same arguments as
-`_handleFile`. Invoke the callback once the file has been removed.
-
-## Template
-
-```javascript
-var fs = require('fs')
-
-function getDestination (req, file, cb) {
- cb(null, '/dev/null')
-}
-
-function MyCustomStorage (opts) {
- this.getDestination = (opts.destination || getDestination)
-}
-
-MyCustomStorage.prototype._handleFile = function _handleFile (req, file, cb) {
- this.getDestination(req, file, function (err, path) {
- if (err) return cb(err)
-
- var outStream = fs.createWriteStream(path)
-
- file.stream.pipe(outStream)
- outStream.on('error', cb)
- outStream.on('finish', function () {
- cb(null, {
- path: path,
- size: outStream.bytesWritten
- })
- })
- })
-}
-
-MyCustomStorage.prototype._removeFile = function _removeFile (req, file, cb) {
- fs.unlink(file.path, cb)
-}
-
-module.exports = function (opts) {
- return new MyCustomStorage(opts)
-}
-```
diff --git a/index.js b/index.js
index 2a98039b..a6bcf85c 100644
--- a/index.js
+++ b/index.js
@@ -1,100 +1,72 @@
-var makeError = require('./lib/make-error')
-var makeMiddleware = require('./lib/make-middleware')
+import bytes from 'bytes'
-var diskStorage = require('./storage/disk')
-var memoryStorage = require('./storage/memory')
+import createFileFilter from './lib/file-filter.js'
+import createMiddleware from './lib/middleware.js'
-function allowAll (req, file, cb) {
- cb(null, true)
-}
-
-function Multer (options) {
- if (options.storage) {
- this.storage = options.storage
- } else if (options.dest) {
- this.storage = diskStorage({ destination: options.dest })
- } else {
- this.storage = memoryStorage()
- }
+const kLimits = Symbol('limits')
- this.limits = options.limits
- this.fileFilter = options.fileFilter || allowAll
+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
}
-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(makeError('LIMIT_UNEXPECTED_FILE', file.fieldname))
- }
-
- filesLeft[file.fieldname] -= 1
- fileFilter(req, file, cb)
- }
+function _middleware (limits, fields, fileStrategy) {
+ return createMiddleware(() => ({
+ fields: fields,
+ limits: limits,
+ fileFilter: createFileFilter(fields),
+ fileStrategy: fileStrategy
+ }))
+}
- return {
- limits: this.limits,
- storage: this.storage,
- fileFilter: wrappedFileFilter,
- fileStrategy: fileStrategy
+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)
}
}
- return makeMiddleware(setup.bind(this))
-}
-
-Multer.prototype.single = function (name) {
- return this._makeMiddleware([{ name: name, maxCount: 1 }], 'VALUE')
-}
+ single (name) {
+ return _middleware(this[kLimits], [{ name: name, maxCount: 1 }], 'VALUE')
+ }
-Multer.prototype.array = function (name, maxCount) {
- return this._makeMiddleware([{ name: name, maxCount: maxCount }], 'ARRAY')
-}
+ array (name, maxCount) {
+ return _middleware(this[kLimits], [{ name: name, maxCount: maxCount }], 'ARRAY')
+ }
-Multer.prototype.fields = function (fields) {
- return this._makeMiddleware(fields, 'OBJECT')
-}
+ fields (fields) {
+ return _middleware(this[kLimits], fields, 'OBJECT')
+ }
-Multer.prototype.none = function () {
- return this._makeMiddleware([], 'NONE')
-}
+ none () {
+ return _middleware(this[kLimits], [], 'NONE')
+ }
-Multer.prototype.any = function () {
- function setup () {
- return {
- limits: this.limits,
- storage: this.storage,
- fileFilter: this.fileFilter,
+ any () {
+ return createMiddleware(() => ({
+ fields: [],
+ limits: this[kLimits],
+ fileFilter: () => {},
fileStrategy: 'ARRAY'
- }
+ }))
}
-
- return makeMiddleware(setup.bind(this))
}
-function multer (options) {
- if (options === undefined) {
- return new Multer({})
- }
+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}`)
- if (typeof options === 'object' && options !== null) {
- return new Multer(options)
+ 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.')
}
- throw new TypeError('Expected object for argument options')
+ return new Multer(options)
}
-
-module.exports = multer
-module.exports.diskStorage = diskStorage
-module.exports.memoryStorage = memoryStorage
diff --git a/lib/counter.js b/lib/counter.js
deleted file mode 100644
index 29c410c7..00000000
--- a/lib/counter.js
+++ /dev/null
@@ -1,28 +0,0 @@
-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
diff --git a/lib/error.js b/lib/error.js
new file mode 100644
index 00000000..92b6171d
--- /dev/null
+++ b/lib/error.js
@@ -0,0 +1,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)
+ }
+}
diff --git a/lib/file-appender.js b/lib/file-appender.js
index 1a2c5e76..55eaa50b 100644
--- a/lib/file-appender.js
+++ b/lib/file-appender.js
@@ -1,67 +1,24 @@
-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
-
+export default function createFileAppender (strategy, req, fields) {
switch (strategy) {
case 'NONE': break
- case 'VALUE': break
+ case 'VALUE': req.file = null; break
case 'ARRAY': req.files = []; break
case 'OBJECT': req.files = Object.create(null); break
- default: throw new Error('Unknown file strategy: ' + strategy)
- }
-}
-
-FileAppender.prototype.insertPlaceholder = function (file) {
- var placeholder = {
- fieldname: file.fieldname
- }
-
- 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
+ /* c8 ignore next */
+ default: throw new Error(`Unknown file strategy: ${strategy}`)
}
- 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
+ if (strategy === 'OBJECT') {
+ for (const field of fields) {
+ req.files[field.name] = []
+ }
}
-}
-FileAppender.prototype.replacePlaceholder = function (placeholder, file) {
- if (this.strategy === 'VALUE') {
- this.req.file = file
- return
+ 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
+ }
}
-
- delete placeholder.fieldname
- objectAssign(placeholder, file)
}
-
-module.exports = FileAppender
diff --git a/lib/file-filter.js b/lib/file-filter.js
new file mode 100644
index 00000000..f364735a
--- /dev/null
+++ b/lib/file-filter.js
@@ -0,0 +1,27 @@
+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)
+ }
+}
diff --git a/lib/make-error.js b/lib/make-error.js
deleted file mode 100644
index 01d74c78..00000000
--- a/lib/make-error.js
+++ /dev/null
@@ -1,18 +0,0 @@
-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'
-}
-
-function makeError (code, optionalField) {
- var err = new Error(errorMessages[code])
- err.code = code
- if (optionalField) err.field = optionalField
- return err
-}
-
-module.exports = makeError
diff --git a/lib/make-middleware.js b/lib/make-middleware.js
deleted file mode 100644
index 25ba90d0..00000000
--- a/lib/make-middleware.js
+++ /dev/null
@@ -1,178 +0,0 @@
-var is = require('type-is')
-var Busboy = require('busboy')
-var extend = require('xtend')
-var onFinished = require('on-finished')
-var appendField = require('append-field')
-
-var Counter = require('./counter')
-var makeError = require('./make-error')
-var FileAppender = require('./file-appender')
-var removeUploadedFiles = require('./remove-uploaded-files')
-
-function drainStream (stream) {
- stream.on('readable', stream.read.bind(stream))
-}
-
-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
-
- req.body = Object.create(null)
-
- var busboy
-
- try {
- busboy = new Busboy({ headers: req.headers, limits: limits })
- } 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)
- busboy.removeAllListeners()
-
- onFinished(req, function () { next(err) })
- }
-
- function indicateDone () {
- if (readFinished && pendingWrites.isZero() && !errorOccured) done()
- }
-
- function abortWithError (uploadError) {
- 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(makeError(code, optionalField))
- }
-
- // handle text field data
- busboy.on('field', function (fieldname, value, fieldnameTruncated, valueTruncated) {
- if (fieldnameTruncated) 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 && limits.hasOwnProperty('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 && limits.hasOwnProperty('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('finish', function () {
- readFinished = true
- indicateDone()
- })
-
- req.pipe(busboy)
- }
-}
-
-module.exports = makeMiddleware
diff --git a/lib/middleware.js b/lib/middleware.js
new file mode 100644
index 00000000..f19611bf
--- /dev/null
+++ b/lib/middleware.js
@@ -0,0 +1,34 @@
+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)
+ }
+}
diff --git a/lib/read-body.js b/lib/read-body.js
new file mode 100644
index 00000000..c7277504
--- /dev/null
+++ b/lib/read-body.js
@@ -0,0 +1,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
+ }
+}
diff --git a/lib/remove-uploaded-files.js b/lib/remove-uploaded-files.js
deleted file mode 100644
index f0b16ea5..00000000
--- a/lib/remove-uploaded-files.js
+++ /dev/null
@@ -1,28 +0,0 @@
-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
diff --git a/package.json b/package.json
index fc11e47f..36fbe612 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "multer",
"description": "Middleware for handling `multipart/form-data`.",
- "version": "1.2.0",
+ "version": "2.0.0-rc.4",
"contributors": [
"Hage Yaapa (http://www.hacksparrow.com)",
"Jaret Pfluger ",
@@ -9,6 +9,8 @@
],
"license": "MIT",
"repository": "expressjs/multer",
+ "type": "module",
+ "exports": "./index.js",
"keywords": [
"form",
"post",
@@ -19,34 +21,35 @@
"middleware"
],
"dependencies": {
- "append-field": "^0.1.0",
- "busboy": "^0.2.11",
- "concat-stream": "^1.5.0",
- "mkdirp": "^0.5.1",
- "object-assign": "^3.0.0",
+ "@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",
- "type-is": "^1.6.4",
- "xtend": "^4.0.0"
+ "stream-file-type": "^0.6.1",
+ "type-is": "^1.6.18"
},
"devDependencies": {
- "express": "^4.13.1",
- "form-data": "^1.0.0-rc1",
- "fs-temp": "^0.1.2",
- "mocha": "^2.2.5",
- "rimraf": "^2.4.1",
- "standard": "^8.2.0",
- "testdata-w3c-json-form": "^0.2.0"
+ "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",
+ "testdata-w3c-json-form": "^1.0.0"
},
"engines": {
- "node": ">= 0.10.0"
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"files": [
"LICENSE",
"index.js",
- "storage/",
"lib/"
],
"scripts": {
- "test": "standard && mocha"
+ "test": "standard && c8 --check-coverage --statements 100 mocha"
}
}
diff --git a/storage/disk.js b/storage/disk.js
deleted file mode 100644
index dfbe8893..00000000
--- a/storage/disk.js
+++ /dev/null
@@ -1,66 +0,0 @@
-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.pseudoRandomBytes(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)
-}
diff --git a/storage/memory.js b/storage/memory.js
deleted file mode 100644
index da41aaa1..00000000
--- a/storage/memory.js
+++ /dev/null
@@ -1,21 +0,0 @@
-var concat = require('concat-stream')
-
-function MemoryStorage (opts) {}
-
-MemoryStorage.prototype._handleFile = function _handleFile (req, file, cb) {
- file.stream.pipe(concat(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)
-}
diff --git a/test/_util.js b/test/_util.js
index e62bdafc..8cbc8fbb 100644
--- a/test/_util.js
+++ b/test/_util.js
@@ -1,35 +1,109 @@
-var fs = require('fs')
-var path = require('path')
-var stream = require('stream')
-var onFinished = require('on-finished')
+import assert from 'node:assert'
+import fs from 'node:fs'
+import stream from 'node:stream'
+import { promisify } from 'node:util'
-exports.file = function file (name) {
- return fs.createReadStream(path.join(__dirname, 'files', name))
+import hasha from 'hasha'
+import _onFinished from 'on-finished'
+
+const onFinished = promisify(_onFinished)
+
+const files = new Map([
+ ['empty', {
+ clientReportedMimeType: 'application/octet-stream',
+ detectedFileExtension: '',
+ detectedMimeType: null,
+ extension: '.dat',
+ hash: 'd41d8cd98f00b204e9800998ecf8427e',
+ size: 0
+ }],
+ ['large', {
+ clientReportedMimeType: 'application/octet-stream',
+ detectedFileExtension: '',
+ detectedMimeType: null,
+ extension: '',
+ hash: 'd5554977e0b856fa5ad94fff283616fb',
+ size: 2413677
+ }],
+ ['medium', {
+ clientReportedMimeType: 'application/octet-stream',
+ detectedFileExtension: '.gif',
+ detectedMimeType: 'image/gif',
+ extension: '.fake',
+ hash: 'a88025890e6a2cd15edb83e0aecdddd1',
+ size: 21057
+ }],
+ ['small', {
+ clientReportedMimeType: 'application/octet-stream',
+ detectedFileExtension: '',
+ detectedMimeType: null,
+ extension: '.dat',
+ hash: '3817334ffb4cf3fcaa16c4258d888131',
+ size: 1778
+ }],
+ ['tiny', {
+ clientReportedMimeType: 'audio/midi',
+ detectedFileExtension: '.mid',
+ detectedMimeType: 'audio/midi',
+ extension: '.mid',
+ hash: 'c187e1be438cb952bb8a0e8142f4a6d1',
+ size: 248
+ }]
+])
+
+export function file (name) {
+ return fs.createReadStream(new URL(`files/${name}${files.get(name).extension}`, import.meta.url))
+}
+
+export function knownFileLength (name) {
+ return files.get(name).size
+}
+
+export async function assertFile (file, fieldName, fileName) {
+ if (!files.has(fileName)) {
+ throw new Error(`No file named "${fileName}"`)
+ }
+
+ const expected = files.get(fileName)
+
+ assert.strictEqual(file.fieldName, fieldName)
+ assert.strictEqual(file.originalName, fileName + expected.extension)
+ assert.strictEqual(file.size, expected.size)
+
+ assert.strictEqual(file.clientReportedMimeType, expected.clientReportedMimeType)
+ assert.strictEqual(file.clientReportedFileExtension, expected.extension)
+
+ assert.strictEqual(file.detectedMimeType, expected.detectedMimeType)
+ assert.strictEqual(file.detectedFileExtension, expected.detectedFileExtension)
+
+ const hash = await hasha.fromStream(file.stream, { algorithm: 'md5' })
+
+ assert.strictEqual(hash, expected.hash)
+}
+
+export async function assertFiles (files) {
+ await Promise.all(files.map((args) => assertFile(args[0], args[1], args[2])))
}
-exports.fileSize = function fileSize (path) {
- return fs.statSync(path).size
+function getLength (form) {
+ return promisify(form.getLength).call(form)
}
-exports.submitForm = function submitForm (multer, form, cb) {
- form.getLength(function (err, length) {
- if (err) return cb(err)
+export async function submitForm (multer, form) {
+ const length = await getLength(form)
+ const req = new stream.PassThrough()
- var req = new stream.PassThrough()
+ req.complete = false
+ form.once('end', () => { req.complete = true })
- req.complete = false
- form.once('end', function () {
- req.complete = true
- })
+ form.pipe(req)
+ req.headers = {
+ 'content-type': `multipart/form-data; boundary=${form.getBoundary()}`,
+ 'content-length': length
+ }
- form.pipe(req)
- req.headers = {
- 'content-type': 'multipart/form-data; boundary=' + form.getBoundary(),
- 'content-length': length
- }
+ await promisify(multer)(req, null)
+ await onFinished(req)
- multer(req, null, function (err) {
- onFinished(req, function () { cb(err, req) })
- })
- })
+ return req
}
diff --git a/test/aborted-requests.js b/test/aborted-requests.js
new file mode 100644
index 00000000..5a75972b
--- /dev/null
+++ b/test/aborted-requests.js
@@ -0,0 +1,77 @@
+/* eslint-env mocha */
+
+import assert from 'node:assert'
+import { PassThrough } from 'node:stream'
+import { promisify } from 'node:util'
+
+import FormData from 'form-data'
+
+import * as util from './_util.js'
+import multer from '../index.js'
+
+function getLength (form) {
+ return promisify(form.getLength).call(form)
+}
+
+function createAbortStream (maxBytes, aborter) {
+ let bytesPassed = 0
+
+ return new PassThrough({
+ transform (chunk, _, cb) {
+ if (bytesPassed + chunk.length < maxBytes) {
+ bytesPassed += chunk.length
+ this.push(chunk)
+ return cb()
+ }
+
+ const bytesLeft = maxBytes - bytesPassed
+
+ if (bytesLeft) {
+ bytesPassed += bytesLeft
+ this.push(chunk.slice(0, bytesLeft))
+ }
+
+ process.nextTick(() => aborter(this))
+ }
+ })
+}
+
+describe('Aborted requests', () => {
+ it('should handle clients aborting the request', async () => {
+ const form = new FormData()
+ const parser = multer().single('file')
+
+ form.append('file', util.file('small'))
+
+ const length = await getLength(form)
+ const req = createAbortStream(length - 100, (stream) => stream.emit('aborted'))
+
+ req.headers = {
+ 'content-type': `multipart/form-data; boundary=${form.getBoundary()}`,
+ 'content-length': length
+ }
+
+ const result = promisify(parser)(form.pipe(req), null)
+
+ return assert.rejects(result, err => err.code === 'CLIENT_ABORTED')
+ })
+
+ it('should handle clients erroring the request', async () => {
+ const form = new FormData()
+ const parser = multer().single('file')
+
+ form.append('file', util.file('small'))
+
+ const length = await getLength(form)
+ const req = createAbortStream(length - 100, (stream) => stream.emit('error', new Error('TEST_ERROR')))
+
+ req.headers = {
+ 'content-type': `multipart/form-data; boundary=${form.getBoundary()}`,
+ 'content-length': length
+ }
+
+ const result = promisify(parser)(form.pipe(req), null)
+
+ return assert.rejects(result, err => err.message === 'TEST_ERROR')
+ })
+})
diff --git a/test/body.js b/test/body.js
new file mode 100644
index 00000000..bd25a61c
--- /dev/null
+++ b/test/body.js
@@ -0,0 +1,126 @@
+/* eslint-env mocha */
+
+import assert from 'node:assert'
+import stream from 'node:stream'
+import { promisify } from 'node:util'
+
+import FormData from 'form-data'
+import hasOwnProperty from 'has-own-property'
+import recursiveNullify from 'recursive-nullify'
+import testData from 'testdata-w3c-json-form'
+
+import * as util from './_util.js'
+import multer from '../index.js'
+
+describe('body', () => {
+ let parser
+
+ before(() => {
+ parser = multer().none()
+ })
+
+ it('should process multiple fields', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('key', 'value')
+ form.append('abc', 'xyz')
+
+ const req = await util.submitForm(parser, form)
+
+ assert.deepStrictEqual(req.body, recursiveNullify({
+ name: 'Multer',
+ key: 'value',
+ abc: 'xyz'
+ }))
+ })
+
+ it('should process empty fields', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('key', '')
+ form.append('abc', '')
+ form.append('checkboxfull', 'cb1')
+ form.append('checkboxfull', 'cb2')
+ form.append('checkboxhalfempty', 'cb1')
+ form.append('checkboxhalfempty', '')
+ form.append('checkboxempty', '')
+ form.append('checkboxempty', '')
+
+ const req = await util.submitForm(parser, form)
+
+ assert.deepStrictEqual(req.body, recursiveNullify({
+ name: 'Multer',
+ key: '',
+ abc: '',
+ checkboxfull: ['cb1', 'cb2'],
+ checkboxhalfempty: ['cb1', ''],
+ checkboxempty: ['', '']
+ }))
+ })
+
+ it('should not process non-multipart POST request', async () => {
+ const req = new stream.PassThrough()
+
+ req.end('name=Multer')
+ req.method = 'POST'
+ req.headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ 'content-length': 11
+ }
+
+ await promisify(parser)(req, null)
+
+ assert.strictEqual(hasOwnProperty(req, 'body'), false)
+ assert.strictEqual(hasOwnProperty(req, 'files'), false)
+ })
+
+ it('should not process non-multipart GET request', async () => {
+ const req = new stream.PassThrough()
+
+ req.end('name=Multer')
+ req.method = 'GET'
+ req.headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ 'content-length': 11
+ }
+
+ await promisify(parser)(req, null)
+
+ assert.strictEqual(hasOwnProperty(req, 'body'), false)
+ assert.strictEqual(hasOwnProperty(req, 'files'), false)
+ })
+
+ for (const test of testData) {
+ it(`should handle ${test.name}`, async () => {
+ const form = new FormData()
+
+ for (const field of test.fields) {
+ form.append(field.key, field.value)
+ }
+
+ const req = await util.submitForm(parser, form)
+
+ assert.deepStrictEqual(req.body, recursiveNullify(test.expected))
+ })
+ }
+
+ it('should convert arrays into objects', async () => {
+ const form = new FormData()
+
+ form.append('obj[0]', 'a')
+ form.append('obj[2]', 'c')
+ form.append('obj[x]', 'yz')
+
+ const req = await util.submitForm(parser, form)
+
+ assert.deepStrictEqual(req.body, recursiveNullify({
+ obj: {
+ 0: 'a',
+ 2: 'c',
+ x: 'yz'
+ }
+ }))
+ })
+})
diff --git a/test/disk-storage.js b/test/disk-storage.js
deleted file mode 100644
index 7e25a82a..00000000
--- a/test/disk-storage.js
+++ /dev/null
@@ -1,187 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var fs = require('fs')
-var path = require('path')
-var util = require('./_util')
-var multer = require('../')
-var temp = require('fs-temp')
-var rimraf = require('rimraf')
-var FormData = require('form-data')
-
-describe('Disk Storage', function () {
- var uploadDir, upload
-
- beforeEach(function (done) {
- temp.mkdir(function (err, path) {
- if (err) return done(err)
-
- uploadDir = path
- upload = multer({ dest: path })
- done()
- })
- })
-
- afterEach(function (done) {
- rimraf(uploadDir, done)
- })
-
- it('should process parser/form-data POST request', function (done) {
- var form = new FormData()
- var parser = upload.single('small0')
-
- form.append('name', 'Multer')
- form.append('small0', util.file('small0.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
-
- assert.equal(req.body.name, 'Multer')
-
- assert.equal(req.file.fieldname, 'small0')
- assert.equal(req.file.originalname, 'small0.dat')
- assert.equal(req.file.size, 1778)
- assert.equal(util.fileSize(req.file.path), 1778)
-
- done()
- })
- })
-
- it('should process empty fields and an empty file', function (done) {
- var form = new FormData()
- var parser = upload.single('empty')
-
- form.append('empty', util.file('empty.dat'))
- form.append('name', 'Multer')
- form.append('version', '')
- form.append('year', '')
- form.append('checkboxfull', 'cb1')
- form.append('checkboxfull', 'cb2')
- form.append('checkboxhalfempty', 'cb1')
- form.append('checkboxhalfempty', '')
- form.append('checkboxempty', '')
- form.append('checkboxempty', '')
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
-
- assert.equal(req.body.name, 'Multer')
- assert.equal(req.body.version, '')
- assert.equal(req.body.year, '')
-
- assert.deepEqual(req.body.checkboxfull, [ 'cb1', 'cb2' ])
- assert.deepEqual(req.body.checkboxhalfempty, [ 'cb1', '' ])
- assert.deepEqual(req.body.checkboxempty, [ '', '' ])
-
- assert.equal(req.file.fieldname, 'empty')
- assert.equal(req.file.originalname, 'empty.dat')
- assert.equal(req.file.size, 0)
- assert.equal(util.fileSize(req.file.path), 0)
-
- done()
- })
- })
-
- it('should process multiple files', function (done) {
- var form = new FormData()
- var parser = upload.fields([
- { name: 'empty', maxCount: 1 },
- { name: 'tiny0', maxCount: 1 },
- { name: 'tiny1', maxCount: 1 },
- { name: 'small0', maxCount: 1 },
- { name: 'small1', maxCount: 1 },
- { name: 'medium', maxCount: 1 },
- { name: 'large', maxCount: 1 }
- ])
-
- form.append('empty', util.file('empty.dat'))
- form.append('tiny0', util.file('tiny0.dat'))
- form.append('tiny1', util.file('tiny1.dat'))
- form.append('small0', util.file('small0.dat'))
- form.append('small1', util.file('small1.dat'))
- form.append('medium', util.file('medium.dat'))
- form.append('large', util.file('large.jpg'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
-
- assert.deepEqual(req.body, {})
-
- assert.equal(req.files['empty'][0].fieldname, 'empty')
- assert.equal(req.files['empty'][0].originalname, 'empty.dat')
- assert.equal(req.files['empty'][0].size, 0)
- assert.equal(util.fileSize(req.files['empty'][0].path), 0)
-
- assert.equal(req.files['tiny0'][0].fieldname, 'tiny0')
- assert.equal(req.files['tiny0'][0].originalname, 'tiny0.dat')
- assert.equal(req.files['tiny0'][0].size, 122)
- assert.equal(util.fileSize(req.files['tiny0'][0].path), 122)
-
- assert.equal(req.files['tiny1'][0].fieldname, 'tiny1')
- assert.equal(req.files['tiny1'][0].originalname, 'tiny1.dat')
- assert.equal(req.files['tiny1'][0].size, 7)
- assert.equal(util.fileSize(req.files['tiny1'][0].path), 7)
-
- assert.equal(req.files['small0'][0].fieldname, 'small0')
- assert.equal(req.files['small0'][0].originalname, 'small0.dat')
- assert.equal(req.files['small0'][0].size, 1778)
- assert.equal(util.fileSize(req.files['small0'][0].path), 1778)
-
- assert.equal(req.files['small1'][0].fieldname, 'small1')
- assert.equal(req.files['small1'][0].originalname, 'small1.dat')
- assert.equal(req.files['small1'][0].size, 315)
- assert.equal(util.fileSize(req.files['small1'][0].path), 315)
-
- assert.equal(req.files['medium'][0].fieldname, 'medium')
- assert.equal(req.files['medium'][0].originalname, 'medium.dat')
- assert.equal(req.files['medium'][0].size, 13196)
- assert.equal(util.fileSize(req.files['medium'][0].path), 13196)
-
- assert.equal(req.files['large'][0].fieldname, 'large')
- assert.equal(req.files['large'][0].originalname, 'large.jpg')
- assert.equal(req.files['large'][0].size, 2413677)
- assert.equal(util.fileSize(req.files['large'][0].path), 2413677)
-
- done()
- })
- })
-
- it('should remove uploaded files on error', function (done) {
- var form = new FormData()
- var parser = upload.single('tiny0')
-
- form.append('tiny0', util.file('tiny0.dat'))
- form.append('small0', util.file('small0.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_UNEXPECTED_FILE')
- assert.equal(err.field, 'small0')
- assert.deepEqual(err.storageErrors, [])
-
- var files = fs.readdirSync(uploadDir)
- assert.deepEqual(files, [])
-
- done()
- })
- })
-
- it('should report error when directory doesn\'t exist', function (done) {
- var directory = path.join(temp.mkdirSync(), 'ghost')
- function dest ($0, $1, cb) { cb(null, directory) }
-
- var storage = multer.diskStorage({ destination: dest })
- var upload = multer({ storage: storage })
- var parser = upload.single('tiny0')
- var form = new FormData()
-
- form.append('tiny0', util.file('tiny0.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'ENOENT')
- assert.equal(path.dirname(err.path), directory)
-
- done()
- })
- })
-})
diff --git a/test/error-handling.js b/test/error-handling.js
index 0334292a..c4bd2ebe 100644
--- a/test/error-handling.js
+++ b/test/error-handling.js
@@ -1,169 +1,148 @@
/* eslint-env mocha */
-var assert = require('assert')
+import assert from 'node:assert'
+import stream from 'node:stream'
+import { promisify } from 'node:util'
-var util = require('./_util')
-var multer = require('../')
-var stream = require('stream')
-var FormData = require('form-data')
+import FormData from 'form-data'
+
+import * as util from './_util.js'
+import multer from '../index.js'
function withLimits (limits, fields) {
- var storage = multer.memoryStorage()
- return multer({ storage: storage, limits: limits }).fields(fields)
+ return multer({ limits: limits }).fields(fields)
}
-describe('Error Handling', function () {
- it('should respect parts limit', function (done) {
- var form = new FormData()
- var parser = withLimits({ parts: 1 }, [
- { name: 'small0', maxCount: 1 }
- ])
+function hasCode (code) {
+ return (err) => err.code === code
+}
- form.append('field0', 'BOOM!')
- form.append('small0', util.file('small0.dat'))
+function hasCodeAndField (code, field) {
+ return (err) => err.code === code && err.field === field
+}
+
+function hasMessage (message) {
+ return (err) => err.message === message
+}
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_PART_COUNT')
- done()
- })
+describe('Error Handling', () => {
+ it('should throw on null', () => {
+ assert.throws(() => multer(null))
})
- it('should respect file size limit', function (done) {
- var form = new FormData()
- var parser = withLimits({ fileSize: 1500 }, [
- { name: 'tiny0', maxCount: 1 },
- { name: 'small0', maxCount: 1 }
+ it('should throw on boolean', () => {
+ assert.throws(() => multer(true))
+ assert.throws(() => multer(false))
+ })
+
+ it('should throw on invalid limits', () => {
+ assert.throws(() => multer({ limits: { files: 3.14 } }), /Invalid limit "files" given: 3.14/)
+ assert.throws(() => multer({ limits: { fileSize: 'foobar' } }), /Invalid limit "fileSize" given: foobar/)
+ })
+
+ it('should respect file size limit', async () => {
+ const form = new FormData()
+ const parser = withLimits({ fileSize: 1500 }, [
+ { name: 'tiny', maxCount: 1 },
+ { name: 'small', maxCount: 1 }
])
- form.append('tiny0', util.file('tiny0.dat'))
- form.append('small0', util.file('small0.dat'))
+ form.append('tiny', util.file('tiny'))
+ form.append('small', util.file('small'))
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_FILE_SIZE')
- assert.equal(err.field, 'small0')
- done()
- })
+ await assert.rejects(
+ util.submitForm(parser, form),
+ hasCodeAndField('LIMIT_FILE_SIZE', 'small')
+ )
})
- it('should respect file count limit', function (done) {
- var form = new FormData()
- var parser = withLimits({ files: 1 }, [
- { name: 'small0', maxCount: 1 },
- { name: 'small1', maxCount: 1 }
+ it('should respect file count limit', async () => {
+ const form = new FormData()
+ const parser = withLimits({ files: 1 }, [
+ { name: 'small', maxCount: 1 },
+ { name: 'small', maxCount: 1 }
])
- form.append('small0', util.file('small0.dat'))
- form.append('small1', util.file('small1.dat'))
+ form.append('small', util.file('small'))
+ form.append('small', util.file('small'))
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_FILE_COUNT')
- done()
- })
+ await assert.rejects(
+ util.submitForm(parser, form),
+ hasCode('LIMIT_FILE_COUNT')
+ )
})
- it('should respect file key limit', function (done) {
- var form = new FormData()
- var parser = withLimits({ fieldNameSize: 4 }, [
- { name: 'small0', maxCount: 1 }
+ it('should respect file key limit', async () => {
+ const form = new FormData()
+ const parser = withLimits({ fieldNameSize: 4 }, [
+ { name: 'small', maxCount: 1 }
])
- form.append('small0', util.file('small0.dat'))
+ form.append('small', util.file('small'))
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_FIELD_KEY')
- done()
- })
+ await assert.rejects(
+ util.submitForm(parser, form),
+ hasCode('LIMIT_FIELD_KEY')
+ )
})
- it('should respect field key limit', function (done) {
- var form = new FormData()
- var parser = withLimits({ fieldNameSize: 4 }, [])
+ it('should respect field key limit', async () => {
+ const form = new FormData()
+ const parser = withLimits({ fieldNameSize: 4 }, [])
form.append('ok', 'SMILE')
form.append('blowup', 'BOOM!')
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_FIELD_KEY')
- done()
- })
+ await assert.rejects(
+ util.submitForm(parser, form),
+ hasCode('LIMIT_FIELD_KEY')
+ )
})
- it('should respect field value limit', function (done) {
- var form = new FormData()
- var parser = withLimits({ fieldSize: 16 }, [])
+ it('should respect field value limit', async () => {
+ const form = new FormData()
+ const parser = withLimits({ fieldSize: 16 }, [])
form.append('field0', 'This is okay')
form.append('field1', 'This will make the parser explode')
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_FIELD_VALUE')
- assert.equal(err.field, 'field1')
- done()
- })
+ await assert.rejects(
+ util.submitForm(parser, form),
+ hasCodeAndField('LIMIT_FIELD_VALUE', 'field1')
+ )
})
- it('should respect field count limit', function (done) {
- var form = new FormData()
- var parser = withLimits({ fields: 1 }, [])
+ it('should respect field count limit', async () => {
+ const form = new FormData()
+ const parser = withLimits({ fields: 1 }, [])
form.append('field0', 'BOOM!')
form.append('field1', 'BOOM!')
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_FIELD_COUNT')
- done()
- })
+ await assert.rejects(
+ util.submitForm(parser, form),
+ hasCode('LIMIT_FIELD_COUNT')
+ )
})
- it('should respect fields given', function (done) {
- var form = new FormData()
- var parser = withLimits(undefined, [
+ it('should respect fields given', async () => {
+ const form = new FormData()
+ const parser = withLimits(undefined, [
{ name: 'wrongname', maxCount: 1 }
])
- form.append('small0', util.file('small0.dat'))
+ form.append('small', util.file('small'))
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_UNEXPECTED_FILE')
- assert.equal(err.field, 'small0')
- done()
- })
+ await assert.rejects(
+ util.submitForm(parser, form),
+ hasCodeAndField('LIMIT_UNEXPECTED_FILE', 'small')
+ )
})
- it('should report errors from storage engines', function (done) {
- var storage = multer.memoryStorage()
-
- storage._removeFile = function _removeFile (req, file, cb) {
- var err = new Error('Test error')
- err.code = 'TEST'
- cb(err)
- }
-
- var form = new FormData()
- var upload = multer({ storage: storage })
- var parser = upload.single('tiny0')
-
- form.append('tiny0', util.file('tiny0.dat'))
- form.append('small0', util.file('small0.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_UNEXPECTED_FILE')
- assert.equal(err.field, 'small0')
-
- assert.equal(err.storageErrors.length, 1)
- assert.equal(err.storageErrors[0].code, 'TEST')
- assert.equal(err.storageErrors[0].field, 'tiny0')
- assert.equal(err.storageErrors[0].file, req.file)
-
- done()
- })
- })
-
- it('should report errors from busboy constructor', function (done) {
- var req = new stream.PassThrough()
- var storage = multer.memoryStorage()
- var upload = multer({ storage: storage }).single('tiny0')
- var body = 'test'
+ it('should report errors from busboy constructor', async () => {
+ const req = new stream.PassThrough()
+ const upload = multer().single('tiny')
+ const body = 'test'
req.headers = {
'content-type': 'multipart/form-data',
@@ -172,35 +151,49 @@ describe('Error Handling', function () {
req.end(body)
- upload(req, null, function (err) {
- assert.equal(err.message, 'Multipart: Boundary not found')
- done()
- })
+ await assert.rejects(
+ promisify(upload)(req, null),
+ hasMessage('Multipart: Boundary not found')
+ )
})
- it('should report errors from busboy parsing', function (done) {
- var req = new stream.PassThrough()
- var storage = multer.memoryStorage()
- var upload = multer({ storage: storage }).single('tiny0')
- var boundary = 'AaB03x'
- var body = [
- '--' + boundary,
- 'Content-Disposition: form-data; name="tiny0"; filename="test.txt"',
+ it('should report errors from busboy parsing', async () => {
+ const req = new stream.PassThrough()
+ const upload = multer().single('tiny')
+ const boundary = 'AaB03x'
+ const body = [
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="tiny"; filename="test.txt"',
'Content-Type: text/plain',
'',
'test without end boundary'
].join('\r\n')
req.headers = {
- 'content-type': 'multipart/form-data; boundary=' + boundary,
+ 'content-type': `multipart/form-data; boundary=${boundary}`,
'content-length': body.length
}
req.end(body)
- upload(req, null, function (err) {
- assert.equal(err.message, 'Unexpected end of multipart data')
- done()
- })
+ await assert.rejects(
+ promisify(upload)(req, null),
+ hasMessage('Unexpected end of multipart data')
+ )
+ })
+
+ it('should gracefully handle more than one error at a time', async () => {
+ const form = new FormData()
+ const parser = withLimits({ fileSize: 1, files: 1 }, [
+ { name: 'small', maxCount: 1 }
+ ])
+
+ form.append('small', util.file('small'))
+ form.append('small', util.file('small'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ hasCode('LIMIT_FILE_SIZE')
+ )
})
})
diff --git a/test/expected-files.js b/test/expected-files.js
deleted file mode 100644
index 46739a92..00000000
--- a/test/expected-files.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var util = require('./_util')
-var multer = require('../')
-var FormData = require('form-data')
-
-describe('Expected files', function () {
- var upload
-
- before(function (done) {
- upload = multer()
- done()
- })
-
- it('should reject single unexpected file', function (done) {
- var form = new FormData()
- var parser = upload.single('butme')
-
- form.append('notme', util.file('small0.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_UNEXPECTED_FILE')
- assert.equal(err.field, 'notme')
- done()
- })
- })
-
- it('should reject array of multiple files', function (done) {
- var form = new FormData()
- var parser = upload.array('butme', 4)
-
- form.append('notme', util.file('small0.dat'))
- form.append('notme', util.file('small1.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_UNEXPECTED_FILE')
- assert.equal(err.field, 'notme')
- done()
- })
- })
-
- it('should reject overflowing arrays', function (done) {
- var form = new FormData()
- var parser = upload.array('butme', 1)
-
- form.append('butme', util.file('small0.dat'))
- form.append('butme', util.file('small1.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_UNEXPECTED_FILE')
- assert.equal(err.field, 'butme')
- done()
- })
- })
-
- it('should accept files with expected fieldname', function (done) {
- var form = new FormData()
- var parser = upload.fields([
- { name: 'butme', maxCount: 2 },
- { name: 'andme', maxCount: 2 }
- ])
-
- form.append('butme', util.file('small0.dat'))
- form.append('butme', util.file('small1.dat'))
- form.append('andme', util.file('empty.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
-
- assert.equal(req.files['butme'].length, 2)
- assert.equal(req.files['andme'].length, 1)
-
- done()
- })
- })
-
- it('should reject files with unexpected fieldname', function (done) {
- var form = new FormData()
- var parser = upload.fields([
- { name: 'butme', maxCount: 2 },
- { name: 'andme', maxCount: 2 }
- ])
-
- form.append('butme', util.file('small0.dat'))
- form.append('butme', util.file('small1.dat'))
- form.append('andme', util.file('empty.dat'))
- form.append('notme', util.file('empty.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.code, 'LIMIT_UNEXPECTED_FILE')
- assert.equal(err.field, 'notme')
- done()
- })
- })
-
- it('should allow any file to come thru', function (done) {
- var form = new FormData()
- var parser = upload.any()
-
- form.append('butme', util.file('small0.dat'))
- form.append('butme', util.file('small1.dat'))
- form.append('andme', util.file('empty.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
- assert.equal(req.files.length, 3)
- assert.equal(req.files[0].fieldname, 'butme')
- assert.equal(req.files[1].fieldname, 'butme')
- assert.equal(req.files[2].fieldname, 'andme')
- done()
- })
- })
-})
diff --git a/test/express-integration.js b/test/express-integration.js
index b86429df..22bd988f 100644
--- a/test/express-integration.js
+++ b/test/express-integration.js
@@ -1,109 +1,108 @@
/* eslint-env mocha */
-var assert = require('assert')
+import assert from 'node:assert'
+import { promisify } from 'node:util'
-var multer = require('../')
-var util = require('./_util')
+import express from 'express'
+import FormData from 'form-data'
+import getStream from 'get-stream'
+import _onFinished from 'on-finished'
-var express = require('express')
-var FormData = require('form-data')
-var concat = require('concat-stream')
-var onFinished = require('on-finished')
+import * as util from './_util.js'
+import multer from '../index.js'
-var port = 34279
+const onFinished = promisify(_onFinished)
-describe('Express Integration', function () {
- var app
+const port = 34279
- before(function (done) {
+describe('Express Integration', () => {
+ let app, server
+
+ before((done) => {
app = express()
- app.listen(port, done)
+ server = app.listen(port, done)
+ })
+
+ after((done) => {
+ server.close(done)
})
- function submitForm (form, path, cb) {
- var req = form.submit('http://localhost:' + port + path)
+ function submitForm (form, path) {
+ return new Promise((resolve, reject) => {
+ const req = form.submit(`http://localhost:${port}${path}`)
+
+ req.on('error', reject)
+ req.on('response', (res) => {
+ res.on('error', reject)
+
+ const body = getStream.buffer(res)
+ const finished = onFinished(req)
- req.on('error', cb)
- req.on('response', function (res) {
- res.on('error', cb)
- res.pipe(concat({ encoding: 'buffer' }, function (body) {
- onFinished(req, function () { cb(null, res, body) })
- }))
+ resolve(Promise.all([body, finished]).then(([body]) => ({ res, body })))
+ })
})
}
- it('should work with express error handling', function (done) {
- var limits = { fileSize: 200 }
- var upload = multer({ limits: limits })
- var router = new express.Router()
- var form = new FormData()
+ it('should work with express error handling', async () => {
+ const limits = { fileSize: 200 }
+ const upload = multer({ limits: limits })
+ const router = new express.Router()
+ const form = new FormData()
- var routeCalled = 0
- var errorCalled = 0
+ let routeCalled = 0
+ let errorCalled = 0
- form.append('avatar', util.file('large.jpg'))
+ form.append('avatar', util.file('large'))
- router.post('/profile', upload.single('avatar'), function (req, res, next) {
+ router.post('/profile', upload.single('avatar'), (req, res, next) => {
routeCalled++
res.status(200).end('SUCCESS')
})
- router.use(function (err, req, res, next) {
- assert.equal(err.code, 'LIMIT_FILE_SIZE')
+ router.use((err, req, res, next) => {
+ assert.strictEqual(err.code, 'LIMIT_FILE_SIZE')
errorCalled++
res.status(500).end('ERROR')
})
app.use('/t1', router)
- submitForm(form, '/t1/profile', function (err, res, body) {
- assert.ifError(err)
- assert.equal(routeCalled, 0)
- assert.equal(errorCalled, 1)
- assert.equal(body.toString(), 'ERROR')
- assert.equal(res.statusCode, 500)
+ const result = await submitForm(form, '/t1/profile')
- done()
- })
+ assert.strictEqual(routeCalled, 0)
+ assert.strictEqual(errorCalled, 1)
+ assert.strictEqual(result.body.toString(), 'ERROR')
+ assert.strictEqual(result.res.statusCode, 500)
})
- it('should work when receiving error from fileFilter', function (done) {
- function fileFilter (req, file, cb) {
- cb(new Error('TEST'))
- }
-
- var upload = multer({ fileFilter: fileFilter })
- var router = new express.Router()
- var form = new FormData()
+ it('should work when uploading a file', async () => {
+ const upload = multer()
+ const router = new express.Router()
+ const form = new FormData()
- var routeCalled = 0
- var errorCalled = 0
+ let routeCalled = 0
+ let errorCalled = 0
- form.append('avatar', util.file('large.jpg'))
+ form.append('avatar', util.file('large'))
- router.post('/profile', upload.single('avatar'), function (req, res, next) {
+ router.post('/profile', upload.single('avatar'), (_, res) => {
routeCalled++
res.status(200).end('SUCCESS')
})
- router.use(function (err, req, res, next) {
- assert.equal(err.message, 'TEST')
-
+ router.use((_, __, res, ___) => {
errorCalled++
res.status(500).end('ERROR')
})
app.use('/t2', router)
- submitForm(form, '/t2/profile', function (err, res, body) {
- assert.ifError(err)
- assert.equal(routeCalled, 0)
- assert.equal(errorCalled, 1)
- assert.equal(body.toString(), 'ERROR')
- assert.equal(res.statusCode, 500)
+ const result = await submitForm(form, '/t2/profile')
- done()
- })
+ assert.strictEqual(routeCalled, 1)
+ assert.strictEqual(errorCalled, 0)
+ assert.strictEqual(result.body.toString(), 'SUCCESS')
+ assert.strictEqual(result.res.statusCode, 200)
})
})
diff --git a/test/fields.js b/test/fields.js
deleted file mode 100644
index 29ec14f1..00000000
--- a/test/fields.js
+++ /dev/null
@@ -1,134 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-var stream = require('stream')
-
-var util = require('./_util')
-var multer = require('../')
-var FormData = require('form-data')
-var testData = require('testdata-w3c-json-form')
-
-describe('Fields', function () {
- var parser
-
- before(function () {
- parser = multer().fields([])
- })
-
- it('should process multiple fields', function (done) {
- var form = new FormData()
-
- form.append('name', 'Multer')
- form.append('key', 'value')
- form.append('abc', 'xyz')
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
- assert.deepEqual(req.body, {
- name: 'Multer',
- key: 'value',
- abc: 'xyz'
- })
- done()
- })
- })
-
- it('should process empty fields', function (done) {
- var form = new FormData()
-
- form.append('name', 'Multer')
- form.append('key', '')
- form.append('abc', '')
- form.append('checkboxfull', 'cb1')
- form.append('checkboxfull', 'cb2')
- form.append('checkboxhalfempty', 'cb1')
- form.append('checkboxhalfempty', '')
- form.append('checkboxempty', '')
- form.append('checkboxempty', '')
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
- assert.deepEqual(req.body, {
- name: 'Multer',
- key: '',
- abc: '',
- checkboxfull: [ 'cb1', 'cb2' ],
- checkboxhalfempty: [ 'cb1', '' ],
- checkboxempty: [ '', '' ]
- })
- done()
- })
- })
-
- it('should not process non-multipart POST request', function (done) {
- var req = new stream.PassThrough()
-
- req.end('name=Multer')
- req.method = 'POST'
- req.headers = {
- 'content-type': 'application/x-www-form-urlencoded',
- 'content-length': 11
- }
-
- parser(req, null, function (err) {
- assert.ifError(err)
- assert.equal(req.hasOwnProperty('body'), false)
- assert.equal(req.hasOwnProperty('files'), false)
- done()
- })
- })
-
- it('should not process non-multipart GET request', function (done) {
- var req = new stream.PassThrough()
-
- req.end('name=Multer')
- req.method = 'GET'
- req.headers = {
- 'content-type': 'application/x-www-form-urlencoded',
- 'content-length': 11
- }
-
- parser(req, null, function (err) {
- assert.ifError(err)
- assert.equal(req.hasOwnProperty('body'), false)
- assert.equal(req.hasOwnProperty('files'), false)
- done()
- })
- })
-
- testData.forEach(function (test) {
- it('should handle ' + test.name, function (done) {
- var form = new FormData()
-
- test.fields.forEach(function (field) {
- form.append(field.key, field.value)
- })
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
- assert.deepEqual(req.body, test.expected)
- done()
- })
- })
- })
-
- it('should convert arrays into objects', function (done) {
- var form = new FormData()
-
- form.append('obj[0]', 'a')
- form.append('obj[2]', 'c')
- form.append('obj[x]', 'yz')
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
- assert.deepEqual(req.body, {
- obj: {
- '0': 'a',
- '2': 'c',
- 'x': 'yz'
- }
- })
- done()
- })
- })
-})
diff --git a/test/file-filter.js b/test/file-filter.js
deleted file mode 100644
index ff9d14ce..00000000
--- a/test/file-filter.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var util = require('./_util')
-var multer = require('../')
-var FormData = require('form-data')
-
-function withFilter (fileFilter) {
- return multer({ fileFilter: fileFilter })
-}
-
-function skipSpecificFile (req, file, cb) {
- cb(null, file.fieldname !== 'notme')
-}
-
-function reportFakeError (req, file, cb) {
- cb(new Error('Fake error'))
-}
-
-describe('File Filter', function () {
- it('should skip some files', function (done) {
- var form = new FormData()
- var upload = withFilter(skipSpecificFile)
- var parser = upload.fields([
- { name: 'notme', maxCount: 1 },
- { name: 'butme', maxCount: 1 }
- ])
-
- form.append('notme', util.file('tiny0.dat'))
- form.append('butme', util.file('tiny1.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
- assert.equal(req.files['notme'], undefined)
- assert.equal(req.files['butme'][0].fieldname, 'butme')
- assert.equal(req.files['butme'][0].originalname, 'tiny1.dat')
- assert.equal(req.files['butme'][0].size, 7)
- assert.equal(req.files['butme'][0].buffer.length, 7)
- done()
- })
- })
-
- it('should report errors from fileFilter', function (done) {
- var form = new FormData()
- var upload = withFilter(reportFakeError)
- var parser = upload.single('test')
-
- form.append('test', util.file('tiny0.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.equal(err.message, 'Fake error')
- done()
- })
- })
-})
diff --git a/test/file-ordering.js b/test/file-ordering.js
deleted file mode 100644
index 293ae0e7..00000000
--- a/test/file-ordering.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var util = require('./_util')
-var multer = require('../')
-var FormData = require('form-data')
-
-describe('File ordering', function () {
- it('should present files in same order as they came', function (done) {
- var storage = multer.memoryStorage()
- var upload = multer({ storage: storage })
- var parser = upload.array('themFiles', 2)
-
- var i = 0
- var calls = [{}, {}]
- var pending = 2
- var _handleFile = storage._handleFile
- storage._handleFile = function (req, file, cb) {
- var id = (i++)
-
- _handleFile.call(this, req, file, function (err, info) {
- if (err) return cb(err)
-
- calls[id].cb = cb
- calls[id].info = info
-
- if (--pending === 0) {
- calls[1].cb(null, calls[1].info)
- calls[0].cb(null, calls[0].info)
- }
- })
- }
-
- var form = new FormData()
-
- form.append('themFiles', util.file('small0.dat'))
- form.append('themFiles', util.file('small1.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
- assert.equal(req.files.length, 2)
- assert.equal(req.files[0].originalname, 'small0.dat')
- assert.equal(req.files[1].originalname, 'small1.dat')
- done()
- })
- })
-})
diff --git a/test/files/.gitattributes b/test/files/.gitattributes
new file mode 100644
index 00000000..634fb143
--- /dev/null
+++ b/test/files/.gitattributes
@@ -0,0 +1 @@
+* binary
diff --git a/test/files/large b/test/files/large
new file mode 100644
index 00000000..54e1e82b
Binary files /dev/null and b/test/files/large differ
diff --git a/test/files/large.jpg b/test/files/large.jpg
deleted file mode 100644
index a1d4a66d..00000000
Binary files a/test/files/large.jpg and /dev/null differ
diff --git a/test/files/medium.dat b/test/files/medium.dat
deleted file mode 100644
index 05a3f37d..00000000
--- a/test/files/medium.dat
+++ /dev/null
@@ -1,191 +0,0 @@
-##########################################################################
-##########################################################################
-##########################################################################
-##########################################################################
-##########################################################################
-
- .M
- .:AMMO:
- .:AMMMMMHIIIHMMM.
- .... .AMMMMMMMMMMMHHHMHHMMMML:AMF"
- .:MMMMMLAMMMMMMMHMMMMMMHHIHHIIIHMMMML.
- "WMMMMMMMMMMMMMMMMMMH:::::HMMMMMMHII:.
- .AMMMMMMMHHHMMMMMMMMMMHHHHHMMMMMMMMMAMMMHHHHL.
- .MMMMMMMMMMHHMMMMMMMMHHHHMMMMMMMMMMMMMHTWMHHHHHML
- .MMMMMMMMMMMMMMMMMMMHHHHHHHHHMHMMHHHHIII:::HMHHHHMM.
- .MMMMMMMMMMMMMMMMMMMMMMHHHHHHMHHHHHHIIIIIIIIHMHHHHHM.
- MMMMMMMMMMMMMMMMMHHMMHHHHHIIIHHH::IIHHII:::::IHHHHHHHL
- "MMMMMMMMMMMMMMMMHIIIHMMMMHHIIHHLI::IIHHHHIIIHHHHHHHHML
- .MMMMMMMMMMMMMM"WMMMHHHMMMMMMMMMMMLHHHMMMMMMHHHHHHHHHHH
- .MMMMMMMMMMMWWMW ""YYHMMMMMMMMMMMMF""HMMMMMMMMMHHHHHHHH.
- .MMMMMMMMMM W" V W"WMMMMMHHHHHHHHHH
- "MMMMMMMMMM". "WHHHMH"HHHHHHL
- MMMMMMMMMMF . IHHHHH.
- MMMMMMMMMM . . HHHHHHH
- MMMMMMMMMF. . . . HHHHHHH.
- MMMMMMMMM . ,AWMMMMML. .. . . HHHHHHH.
- :MMMMMMMMM". . F"' 'WM:. ,::HMMA, . . HHHHMMM
- :MMMMMMMMF. . ." WH.. AMM"' " . . HHHMMMM
- MMMMMMMM . . ,;AAAHHWL".. .:' HHHHHHH
- MMMMMMM:. . . -MK"OTO L :I.. ...:HMA-. "HHHHHH
-,:IIIILTMMMMI::. L,,,,. ::I.. .. K"OTO"ML 'HHHHHH
-LHT::LIIIIMMI::. . '""'.IHH:.. .. :.,,,, ' HMMMH: HLI'
-ILTT::"IIITMII::. . .IIII. . '"""" ' MMMFT:::.
-HML:::WMIINMHI:::.. . .:I. . . . . ' .M"'.....I.
-"HWHINWI:.'.HHII::.. .HHI .II. . . . . :M.',, ..I:
- "MLI"ML': :HHII::... MMHHL ::::: . :.. .'.'.'HHTML.II:
- "MMLIHHWL:IHHII::....:I:" :MHHWHI:...:W,," '':::. ..' ":.HH:II:
- "MMMHITIIHHH:::::IWF" """T99"' '"" '.':II:..'.'..' I'.HHIHI'
- YMMHII:IHHHH:::IT.. . . ... . . ''THHI::.'.' .;H.""."H"
- HHII:MHHI"::IWWL . . . . . HH"HHHIIHHH":HWWM"
- """ MMHI::HY""ML, ... . .. :" :HIIIIIILTMH"
- MMHI:.' 'HL,,,,,,,,..,,,......,:" . ''::HH "HWW
- 'MMH:.. . 'MMML,: """MM""""MMM" .'.IH'"MH"
- "MMHL.. .. "MMMMMML,MM,HMMMF . .IHM"
- "MMHHL .. "MMMMMMMMMMMM" . . '.IHF'
- 'MMMML .. "MMMMMMMM" . .'HMF
- HHHMML. .'MMF"
- IHHHHHMML. .'HMF"
- HHHHHHITMML. .'IF..
- "HHHHHHIITML,. ..:F...
- 'HHHHHHHHHMMWWWWWW::"......
- HHHHHHHMMMMMMF"'........
- HHHHHHHHHH............
- HHHHHHHH...........
- HHHHIII..........
- HHIII..........
- HII.........
- "H........
- ......
-
-##########################################################################
-##########################################################################
-##########################################################################
-##########################################################################
-##########################################################################
-
-!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!` `4!!!!!!!!!!~4!!!!!!!!!!!!!!!!!
-!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! <~: ~!!!~ .. 4!!!!!!!!!!!!!!!
-!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ~~~~~~~ ' ud$$$$$ !!!!!!!!!!!!!!!
-!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ~~~~~~~~~: ?$$$$$$$$$ !!!!!!!!!!!!!!
-!!!!!!!!!!!` ``~!!!!!!!!!!!!!! ~~~~~ "*$$$$$k `!!!!!!!!!!!!!
-!!!!!!!!!! $$$$$bu. '~!~` . '~~~~ :~~~~ `4!!!!!!!!!!!
-!!!!!!!!! $$$$$$$$$$$c .zW$$$$$E ~~~~ ~~~~~~~~ ~~~~~: '!!!!!!!!!!
-!!!!!!!!! d$$$$$$$$$$$$$$$$$$$$$$E ~~~~~ '~~~~~~~~ ~~~~~ !!!!!!!!!!
-!!!!!!!!> 9$$$$$$$$$$$$$$$$$$$$$$$ '~~~~~~~ '~~~~~~~~ ~~~~ !!!!!!!!!!
-!!!!!!!!> $$$$$$$$$$$$$$$$$$$$$$$$b ~~~ '~~~~~~~ '~~~ '!!!!!!!!!!
-!!!!!!!!> $$$$$$$$$$$$$$$$$$$$$$$$$$$cuuue$$N. ~ ~~~ !!!!!!!!!!!
-!!!!!!!!! **$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$Ne ~~~~~~~~ `!!!!!!!!!!!
-!!!!!!!!! J$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$N ~~~~~ zL '!!!!!!!!!!
-!!!!!!!! d$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$c z$$$c `!!!!!!!!!
-!!!!!!!> <$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$> 4!!!!!!!!
-!!!!!!! $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ !!!!!!!!
-!!!!!!! <$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$*" ....:!!
-!!!!!!~ 9$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$e@$N '!!!!!!!
-!!!!!! 9$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ !!!!!!!
-!!!!!! $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$""$$$$$$$$$$$~ ~~4!!!!
-!!!!!! 9$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$Lue :::!!!!
-!!!!!!> 9$$$$$$$$$$$$" '$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$ !!!!!!!
-!!!!!!! '$$*$$$$$$$$E '$$$$$$$$$$$$$$$$$$$$$$$$$$$u.@$$$$$$$$$E '!!!!!!!
-!!!!~` .eeW$$$$$$$$ :$$$$$$$$$$$$$***$$$$$$$$$$$$$$$$$$$$u. `~!!!!!
-!!> .:!h '$$$$$$$$$$$$ed$$$$$$$$$$$$Fz$$b $$$$$$$$$$$$$$$$$$$$$F '!h. !!!
-!!!!!!!!L '$**$$$$$$$$$$$$$$$$$$$$$$ *$$$ $$$$$$$$$$$$$$$$$$$$F !!!!!!!!!
-!!!!!!!!! d$$$$$$$$$$$$$$$$$$$$$$$$buud$$$$$$$$$$$$$$$$$$$$" !!!!!!!!!!
-!!!!!!! . |
- . \ \ | | / /__\ \ . | _/ .
- . ________> | | | . / \ | |\ \_______ .
- | / | | / ______ \ | | \ |
- |___________/ |___| /____/ \____\ |___| \__________| .
- . ____ __ . _____ ____ . __________ . _________
- \ \ / \ / / / \ | \ / | .
- \ \/ \/ / / \ | ___ | / ______| .
- \ / / /\ \ . | |___> | \ \
- . \ / / /__\ \ | _/. \ \ +
- \ /\ / / \ | |\ \______> | .
- \ / \ / / ______ \ | | \ / .
- . . \/ \/ /____/ \____\ |___| \____________/ LS
- . .
- . . . . .
- . . .
diff --git a/test/files/small1.dat b/test/files/small1.dat
deleted file mode 100644
index 67586bff..00000000
--- a/test/files/small1.dat
+++ /dev/null
@@ -1,15 +0,0 @@
- ____
- \__/ # ##
- `( `^=_ p _###_
- c / ) | /
- _____- //^---~ _c 3
- / ----^\ /^_\ / --,-
-( | | O_| \\_/ ,/
-| | | / \| `-- /
-(((G |-----|
- //-----\\
- // \\
- / | | ^|
- | | | |
- |____| |____|
- /______) (_____\
\ No newline at end of file
diff --git a/test/files/tiny.mid b/test/files/tiny.mid
new file mode 100644
index 00000000..dccb6e1f
Binary files /dev/null and b/test/files/tiny.mid differ
diff --git a/test/files/tiny0.dat b/test/files/tiny0.dat
deleted file mode 100644
index c7be79b0..00000000
--- a/test/files/tiny0.dat
+++ /dev/null
@@ -1,7 +0,0 @@
- ROFL:ROFL:ROFL:ROFL
- _^___
- L __/ [] \
-LOL===__ \
- L \________]
- I I
- --------/
\ No newline at end of file
diff --git a/test/files/tiny1.dat b/test/files/tiny1.dat
deleted file mode 100644
index 3120fd8e..00000000
--- a/test/files/tiny1.dat
+++ /dev/null
@@ -1 +0,0 @@
-= 0)
- assert.ok(req.files[1].path.indexOf('/testforme-') >= 0)
- done()
- })
- })
-})
diff --git a/test/issue-232.js b/test/issue-232.js
deleted file mode 100644
index 4ce0f107..00000000
--- a/test/issue-232.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var util = require('./_util')
-var multer = require('../')
-var temp = require('fs-temp')
-var rimraf = require('rimraf')
-var FormData = require('form-data')
-
-describe('Issue #232', function () {
- var uploadDir, upload
-
- before(function (done) {
- temp.mkdir(function (err, path) {
- if (err) return done(err)
-
- uploadDir = path
- upload = multer({ dest: path, limits: { fileSize: 100 } })
- done()
- })
- })
-
- after(function (done) {
- rimraf(uploadDir, done)
- })
-
- it('should report limit errors', function (done) {
- var form = new FormData()
- var parser = upload.single('file')
-
- form.append('file', util.file('large.jpg'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ok(err, 'an error was given')
-
- assert.equal(err.code, 'LIMIT_FILE_SIZE')
- assert.equal(err.field, 'file')
-
- done()
- })
- })
-})
diff --git a/test/limits.js b/test/limits.js
new file mode 100644
index 00000000..043108cf
--- /dev/null
+++ b/test/limits.js
@@ -0,0 +1,21 @@
+/* eslint-env mocha */
+
+import assert from 'node:assert'
+import FormData from 'form-data'
+
+import * as util from './_util.js'
+import multer from '../index.js'
+
+describe('limits', () => {
+ it('should report limit errors', async () => {
+ const form = new FormData()
+ const parser = multer({ limits: { fileSize: 100 } }).single('file')
+
+ form.append('file', util.file('large'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ (err) => err.code === 'LIMIT_FILE_SIZE' && err.field === 'file'
+ )
+ })
+})
diff --git a/test/memory-storage.js b/test/memory-storage.js
deleted file mode 100644
index 4090a171..00000000
--- a/test/memory-storage.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var util = require('./_util')
-var multer = require('../')
-var FormData = require('form-data')
-
-describe('Memory Storage', function () {
- var upload
-
- before(function (done) {
- upload = multer()
- done()
- })
-
- it('should process multipart/form-data POST request', function (done) {
- var form = new FormData()
- var parser = upload.single('small0')
-
- form.append('name', 'Multer')
- form.append('small0', util.file('small0.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
-
- assert.equal(req.body.name, 'Multer')
-
- assert.equal(req.file.fieldname, 'small0')
- assert.equal(req.file.originalname, 'small0.dat')
- assert.equal(req.file.size, 1778)
- assert.equal(req.file.buffer.length, 1778)
-
- done()
- })
- })
-
- it('should process empty fields and an empty file', function (done) {
- var form = new FormData()
- var parser = upload.single('empty')
-
- form.append('empty', util.file('empty.dat'))
- form.append('name', 'Multer')
- form.append('version', '')
- form.append('year', '')
- form.append('checkboxfull', 'cb1')
- form.append('checkboxfull', 'cb2')
- form.append('checkboxhalfempty', 'cb1')
- form.append('checkboxhalfempty', '')
- form.append('checkboxempty', '')
- form.append('checkboxempty', '')
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
-
- assert.equal(req.body.name, 'Multer')
- assert.equal(req.body.version, '')
- assert.equal(req.body.year, '')
-
- assert.deepEqual(req.body.checkboxfull, [ 'cb1', 'cb2' ])
- assert.deepEqual(req.body.checkboxhalfempty, [ 'cb1', '' ])
- assert.deepEqual(req.body.checkboxempty, [ '', '' ])
-
- assert.equal(req.file.fieldname, 'empty')
- assert.equal(req.file.originalname, 'empty.dat')
- assert.equal(req.file.size, 0)
- assert.equal(req.file.buffer.length, 0)
-
- done()
- })
- })
-
- it('should process multiple files', function (done) {
- var form = new FormData()
- var parser = upload.fields([
- { name: 'empty', maxCount: 1 },
- { name: 'tiny0', maxCount: 1 },
- { name: 'tiny1', maxCount: 1 },
- { name: 'small0', maxCount: 1 },
- { name: 'small1', maxCount: 1 },
- { name: 'medium', maxCount: 1 },
- { name: 'large', maxCount: 1 }
- ])
-
- form.append('empty', util.file('empty.dat'))
- form.append('tiny0', util.file('tiny0.dat'))
- form.append('tiny1', util.file('tiny1.dat'))
- form.append('small0', util.file('small0.dat'))
- form.append('small1', util.file('small1.dat'))
- form.append('medium', util.file('medium.dat'))
- form.append('large', util.file('large.jpg'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
-
- assert.deepEqual(req.body, {})
-
- assert.equal(req.files['empty'][0].fieldname, 'empty')
- assert.equal(req.files['empty'][0].originalname, 'empty.dat')
- assert.equal(req.files['empty'][0].size, 0)
- assert.equal(req.files['empty'][0].buffer.length, 0)
-
- assert.equal(req.files['tiny0'][0].fieldname, 'tiny0')
- assert.equal(req.files['tiny0'][0].originalname, 'tiny0.dat')
- assert.equal(req.files['tiny0'][0].size, 122)
- assert.equal(req.files['tiny0'][0].buffer.length, 122)
-
- assert.equal(req.files['tiny1'][0].fieldname, 'tiny1')
- assert.equal(req.files['tiny1'][0].originalname, 'tiny1.dat')
- assert.equal(req.files['tiny1'][0].size, 7)
- assert.equal(req.files['tiny1'][0].buffer.length, 7)
-
- assert.equal(req.files['small0'][0].fieldname, 'small0')
- assert.equal(req.files['small0'][0].originalname, 'small0.dat')
- assert.equal(req.files['small0'][0].size, 1778)
- assert.equal(req.files['small0'][0].buffer.length, 1778)
-
- assert.equal(req.files['small1'][0].fieldname, 'small1')
- assert.equal(req.files['small1'][0].originalname, 'small1.dat')
- assert.equal(req.files['small1'][0].size, 315)
- assert.equal(req.files['small1'][0].buffer.length, 315)
-
- assert.equal(req.files['medium'][0].fieldname, 'medium')
- assert.equal(req.files['medium'][0].originalname, 'medium.dat')
- assert.equal(req.files['medium'][0].size, 13196)
- assert.equal(req.files['medium'][0].buffer.length, 13196)
-
- assert.equal(req.files['large'][0].fieldname, 'large')
- assert.equal(req.files['large'][0].originalname, 'large.jpg')
- assert.equal(req.files['large'][0].size, 2413677)
- assert.equal(req.files['large'][0].buffer.length, 2413677)
-
- done()
- })
- })
-})
diff --git a/test/misc.js b/test/misc.js
new file mode 100644
index 00000000..5a4e8c08
--- /dev/null
+++ b/test/misc.js
@@ -0,0 +1,82 @@
+/* eslint-env mocha */
+
+import assert from 'node:assert'
+import { PassThrough, pipeline } from 'node:stream'
+import FormData from 'form-data'
+
+import * as util from './_util.js'
+import multer from '../index.js'
+
+describe('Misc', () => {
+ it('should handle unicode filenames', async () => {
+ const form = new FormData()
+ const parser = multer().single('file')
+ const filename = '\ud83d\udca9.dat'
+
+ form.append('file', util.file('small'), { filename: filename })
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.file.originalName, filename)
+
+ // Ignore content
+ req.file.stream.resume()
+ })
+
+ it('should handle absent filenames', async () => {
+ const form = new FormData()
+ const parser = multer().single('file')
+ const stream = util.file('small')
+
+ // Don't let FormData figure out a filename
+ const hidden = pipeline(stream, new PassThrough(), () => {})
+
+ form.append('file', hidden, { knownLength: util.knownFileLength('small') })
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.file.originalName, undefined)
+
+ // Ignore content
+ req.file.stream.resume()
+ })
+
+ it('should present files in same order as they came', async () => {
+ const parser = multer().array('themFiles', 2)
+ const form = new FormData()
+
+ form.append('themFiles', util.file('small'))
+ form.append('themFiles', util.file('tiny'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.files.length, 2)
+
+ util.assertFiles([
+ [req.files[0], 'themFiles', 'small'],
+ [req.files[1], 'themFiles', 'tiny']
+ ])
+ })
+
+ it('should accept multiple requests', async () => {
+ const parser = multer().array('them-files')
+
+ async function submitData (fileCount) {
+ const form = new FormData()
+
+ for (let i = 0; i < fileCount; i++) {
+ form.append('them-files', util.file('small'))
+ }
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.files.length, fileCount)
+
+ await util.assertFiles(req.files.map((file) => [file, 'them-files', 'small']))
+ }
+
+ await Promise.all([9, 1, 5, 7, 2, 8, 3, 4].map(submitData))
+ })
+
+ it('should give error on old options', () => {
+ assert.throws(() => multer({ dest: '/tmp' }))
+ assert.throws(() => multer({ storage: {} }))
+ assert.throws(() => multer({ fileFilter: () => {} }))
+ })
+})
diff --git a/test/none.js b/test/none.js
deleted file mode 100644
index c31b82b2..00000000
--- a/test/none.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var util = require('./_util')
-var multer = require('../')
-var FormData = require('form-data')
-
-describe('None', function () {
- var parser
-
- before(function () {
- parser = multer().none()
- })
-
- it('should not allow file uploads', function (done) {
- var form = new FormData()
-
- form.append('key1', 'val1')
- form.append('key2', 'val2')
- form.append('file', util.file('small0.dat'))
-
- util.submitForm(parser, form, function (err, req) {
- assert.ok(err)
- assert.equal(err.code, 'LIMIT_UNEXPECTED_FILE')
- assert.equal(req.files, undefined)
- assert.equal(req.body['key1'], 'val1')
- assert.equal(req.body['key2'], 'val2')
- done()
- })
- })
-
- it('should handle text fields', function (done) {
- var form = new FormData()
-
- form.append('key1', 'val1')
- form.append('key2', 'val2')
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
- assert.equal(req.files, undefined)
- assert.equal(req.body['key1'], 'val1')
- assert.equal(req.body['key2'], 'val2')
- done()
- })
- })
-})
diff --git a/test/reuse-middleware.js b/test/reuse-middleware.js
deleted file mode 100644
index 75bfc347..00000000
--- a/test/reuse-middleware.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var util = require('./_util')
-var multer = require('../')
-var FormData = require('form-data')
-
-describe('Reuse Middleware', function () {
- var parser
-
- before(function (done) {
- parser = multer().array('them-files')
- done()
- })
-
- it('should accept multiple requests', function (done) {
- var pending = 8
-
- function submitData (fileCount) {
- var form = new FormData()
-
- form.append('name', 'Multer')
- form.append('files', '' + fileCount)
-
- for (var i = 0; i < fileCount; i++) {
- form.append('them-files', util.file('small0.dat'))
- }
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
-
- assert.equal(req.body.name, 'Multer')
- assert.equal(req.body.files, '' + fileCount)
- assert.equal(req.files.length, fileCount)
-
- req.files.forEach(function (file) {
- assert.equal(file.fieldname, 'them-files')
- assert.equal(file.originalname, 'small0.dat')
- assert.equal(file.size, 1778)
- assert.equal(file.buffer.length, 1778)
- })
-
- if (--pending === 0) done()
- })
- }
-
- submitData(9)
- submitData(1)
- submitData(5)
- submitData(7)
- submitData(2)
- submitData(8)
- submitData(3)
- submitData(4)
- })
-})
diff --git a/test/select-field.js b/test/select-field.js
deleted file mode 100644
index 5ed0aed1..00000000
--- a/test/select-field.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var util = require('./_util')
-var multer = require('../')
-var FormData = require('form-data')
-
-function generateForm () {
- var form = new FormData()
-
- form.append('CA$|-|', util.file('empty.dat'))
- form.append('set-1', util.file('tiny0.dat'))
- form.append('set-1', util.file('empty.dat'))
- form.append('set-1', util.file('tiny1.dat'))
- form.append('set-2', util.file('tiny1.dat'))
- form.append('set-2', util.file('tiny0.dat'))
- form.append('set-2', util.file('empty.dat'))
-
- return form
-}
-
-function assertSet (files, setName, fileNames) {
- var len = fileNames.length
-
- assert.equal(files.length, len)
-
- for (var i = 0; i < len; i++) {
- assert.equal(files[i].fieldname, setName)
- assert.equal(files[i].originalname, fileNames[i])
- }
-}
-
-describe('Select Field', function () {
- var parser
-
- before(function () {
- parser = multer().fields([
- { name: 'CA$|-|', maxCount: 1 },
- { name: 'set-1', maxCount: 3 },
- { name: 'set-2', maxCount: 3 }
- ])
- })
-
- it('should select the first file with fieldname', function (done) {
- util.submitForm(parser, generateForm(), function (err, req) {
- assert.ifError(err)
-
- var file
-
- file = req.files['CA$|-|'][0]
- assert.equal(file.fieldname, 'CA$|-|')
- assert.equal(file.originalname, 'empty.dat')
-
- file = req.files['set-1'][0]
- assert.equal(file.fieldname, 'set-1')
- assert.equal(file.originalname, 'tiny0.dat')
-
- file = req.files['set-2'][0]
- assert.equal(file.fieldname, 'set-2')
- assert.equal(file.originalname, 'tiny1.dat')
-
- done()
- })
- })
-
- it('should select all files with fieldname', function (done) {
- util.submitForm(parser, generateForm(), function (err, req) {
- assert.ifError(err)
-
- assertSet(req.files['CA$|-|'], 'CA$|-|', [ 'empty.dat' ])
- assertSet(req.files['set-1'], 'set-1', [ 'tiny0.dat', 'empty.dat', 'tiny1.dat' ])
- assertSet(req.files['set-2'], 'set-2', [ 'tiny1.dat', 'tiny0.dat', 'empty.dat' ])
-
- done()
- })
- })
-})
diff --git a/test/unicode.js b/test/unicode.js
deleted file mode 100644
index 85aca4c4..00000000
--- a/test/unicode.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/* eslint-env mocha */
-
-var assert = require('assert')
-
-var path = require('path')
-var util = require('./_util')
-var multer = require('../')
-var temp = require('fs-temp')
-var rimraf = require('rimraf')
-var FormData = require('form-data')
-
-describe('Unicode', function () {
- var uploadDir, upload
-
- beforeEach(function (done) {
- temp.mkdir(function (err, path) {
- if (err) return done(err)
-
- var storage = multer.diskStorage({
- destination: path,
- filename: function (req, file, cb) {
- cb(null, file.originalname)
- }
- })
-
- uploadDir = path
- upload = multer({ storage: storage })
- done()
- })
- })
-
- afterEach(function (done) {
- rimraf(uploadDir, done)
- })
-
- it('should handle unicode filenames', function (done) {
- var form = new FormData()
- var parser = upload.single('small0')
- var filename = '\ud83d\udca9.dat'
-
- form.append('small0', util.file('small0.dat'), { filename: filename })
-
- util.submitForm(parser, form, function (err, req) {
- assert.ifError(err)
-
- assert.equal(path.basename(req.file.path), filename)
- assert.equal(req.file.originalname, filename)
-
- assert.equal(req.file.fieldname, 'small0')
- assert.equal(req.file.size, 1778)
- assert.equal(util.fileSize(req.file.path), 1778)
-
- done()
- })
- })
-})
diff --git a/test/upload-any.js b/test/upload-any.js
new file mode 100644
index 00000000..ad954bbd
--- /dev/null
+++ b/test/upload-any.js
@@ -0,0 +1,70 @@
+/* eslint-env mocha */
+
+import assert from 'node:assert'
+import FormData from 'form-data'
+
+import * as util from './_util.js'
+import multer from '../index.js'
+
+describe('upload.any', () => {
+ let parser
+
+ before(() => {
+ parser = multer().any()
+ })
+
+ it('should accept single file', async () => {
+ const form = new FormData()
+
+ form.append('test', util.file('tiny'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.files.length, 1)
+
+ await util.assertFile(req.files[0], 'test', 'tiny')
+ })
+
+ it('should accept some files', async () => {
+ const form = new FormData()
+
+ form.append('foo', util.file('empty'))
+ form.append('foo', util.file('small'))
+ form.append('test', util.file('empty'))
+ form.append('anyname', util.file('tiny'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.files.length, 4)
+
+ await util.assertFiles([
+ [req.files[0], 'foo', 'empty'],
+ [req.files[1], 'foo', 'small'],
+ [req.files[2], 'test', 'empty'],
+ [req.files[3], 'anyname', 'tiny']
+ ])
+ })
+
+ it('should accept any files', async () => {
+ const form = new FormData()
+
+ form.append('set-0', util.file('empty'))
+ form.append('set-1', util.file('tiny'))
+ form.append('set-0', util.file('empty'))
+ form.append('set-1', util.file('tiny'))
+ form.append('set-2', util.file('tiny'))
+ form.append('set-1', util.file('tiny'))
+ form.append('set-2', util.file('empty'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.files.length, 7)
+
+ await util.assertFiles([
+ [req.files[0], 'set-0', 'empty'],
+ [req.files[1], 'set-1', 'tiny'],
+ [req.files[2], 'set-0', 'empty'],
+ [req.files[3], 'set-1', 'tiny'],
+ [req.files[4], 'set-2', 'tiny'],
+ [req.files[5], 'set-1', 'tiny'],
+ [req.files[6], 'set-2', 'empty']
+ ])
+ })
+})
diff --git a/test/upload-array.js b/test/upload-array.js
new file mode 100644
index 00000000..170301b6
--- /dev/null
+++ b/test/upload-array.js
@@ -0,0 +1,74 @@
+/* eslint-env mocha */
+
+import assert from 'node:assert'
+import FormData from 'form-data'
+
+import * as util from './_util.js'
+import multer from '../index.js'
+
+describe('upload.array', () => {
+ let parser
+
+ before(() => {
+ parser = multer().array('files', 3)
+ })
+
+ it('should accept single file', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('files', util.file('small'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.body.name, 'Multer')
+ assert.strictEqual(req.files.length, 1)
+
+ await util.assertFile(req.files[0], 'files', 'small')
+ })
+
+ it('should accept array of files', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('files', util.file('empty'))
+ form.append('files', util.file('small'))
+ form.append('files', util.file('tiny'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.body.name, 'Multer')
+ assert.strictEqual(req.files.length, 3)
+
+ await util.assertFiles([
+ [req.files[0], 'files', 'empty'],
+ [req.files[1], 'files', 'small'],
+ [req.files[2], 'files', 'tiny']
+ ])
+ })
+
+ it('should reject too many files', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('files', util.file('small'))
+ form.append('files', util.file('small'))
+ form.append('files', util.file('small'))
+ form.append('files', util.file('small'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ (err) => err.code === 'LIMIT_FILE_COUNT' && err.field === 'files'
+ )
+ })
+
+ it('should reject unexpected field', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('unexpected', util.file('small'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'unexpected'
+ )
+ })
+})
diff --git a/test/upload-fields.js b/test/upload-fields.js
new file mode 100644
index 00000000..b668193c
--- /dev/null
+++ b/test/upload-fields.js
@@ -0,0 +1,104 @@
+/* eslint-env mocha */
+
+import assert from 'node:assert'
+import FormData from 'form-data'
+
+import * as util from './_util.js'
+import multer from '../index.js'
+
+describe('upload.fields', () => {
+ let parser
+
+ before(() => {
+ parser = multer().fields([
+ { name: 'CA$|-|', maxCount: 1 },
+ { name: 'set-1', maxCount: 3 },
+ { name: 'set-2', maxCount: 3 }
+ ])
+ })
+
+ it('should accept single file', async () => {
+ const form = new FormData()
+
+ form.append('set-2', util.file('tiny'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.files['CA$|-|'].length, 0)
+ assert.strictEqual(req.files['set-1'].length, 0)
+ assert.strictEqual(req.files['set-2'].length, 1)
+
+ await util.assertFile(req.files['set-2'][0], 'set-2', 'tiny')
+ })
+
+ it('should accept some files', async () => {
+ const form = new FormData()
+
+ form.append('CA$|-|', util.file('empty'))
+ form.append('set-1', util.file('small'))
+ form.append('set-1', util.file('empty'))
+ form.append('set-2', util.file('tiny'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.files['CA$|-|'].length, 1)
+ assert.strictEqual(req.files['set-1'].length, 2)
+ assert.strictEqual(req.files['set-2'].length, 1)
+
+ await util.assertFiles([
+ [req.files['CA$|-|'][0], 'CA$|-|', 'empty'],
+ [req.files['set-1'][0], 'set-1', 'small'],
+ [req.files['set-1'][1], 'set-1', 'empty'],
+ [req.files['set-2'][0], 'set-2', 'tiny']
+ ])
+ })
+
+ it('should accept all files', async () => {
+ const form = new FormData()
+
+ form.append('CA$|-|', util.file('empty'))
+ form.append('set-1', util.file('tiny'))
+ form.append('set-1', util.file('empty'))
+ form.append('set-1', util.file('tiny'))
+ form.append('set-2', util.file('tiny'))
+ form.append('set-2', util.file('tiny'))
+ form.append('set-2', util.file('empty'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.files['CA$|-|'].length, 1)
+ assert.strictEqual(req.files['set-1'].length, 3)
+ assert.strictEqual(req.files['set-2'].length, 3)
+
+ await util.assertFiles([
+ [req.files['CA$|-|'][0], 'CA$|-|', 'empty'],
+ [req.files['set-1'][0], 'set-1', 'tiny'],
+ [req.files['set-1'][1], 'set-1', 'empty'],
+ [req.files['set-1'][2], 'set-1', 'tiny'],
+ [req.files['set-2'][0], 'set-2', 'tiny'],
+ [req.files['set-2'][1], 'set-2', 'tiny'],
+ [req.files['set-2'][2], 'set-2', 'empty']
+ ])
+ })
+
+ it('should reject too many files', async () => {
+ const form = new FormData()
+
+ form.append('CA$|-|', util.file('small'))
+ form.append('CA$|-|', util.file('small'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ (err) => err.code === 'LIMIT_FILE_COUNT' && err.field === 'CA$|-|'
+ )
+ })
+
+ it('should reject unexpected field', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('unexpected', util.file('small'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'unexpected'
+ )
+ })
+})
diff --git a/test/upload-none.js b/test/upload-none.js
new file mode 100644
index 00000000..c7aa13fe
--- /dev/null
+++ b/test/upload-none.js
@@ -0,0 +1,55 @@
+/* eslint-env mocha */
+
+import assert from 'node:assert'
+import FormData from 'form-data'
+
+import * as util from './_util.js'
+import multer from '../index.js'
+
+describe('upload.none', () => {
+ let parser
+
+ before(() => {
+ parser = multer().none()
+ })
+
+ it('should handle text fields', async () => {
+ const form = new FormData()
+ const parser = multer().none()
+
+ form.append('foo', 'bar')
+ form.append('test', 'yes')
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.file, undefined)
+ assert.strictEqual(req.files, undefined)
+
+ assert.strictEqual(req.body.foo, 'bar')
+ assert.strictEqual(req.body.test, 'yes')
+ })
+
+ it('should reject single file', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('file', util.file('small'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'file'
+ )
+ })
+
+ it('should reject multiple files', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('file', util.file('tiny'))
+ form.append('file', util.file('tiny'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'file'
+ )
+ })
+})
diff --git a/test/upload-single.js b/test/upload-single.js
new file mode 100644
index 00000000..b639cbfa
--- /dev/null
+++ b/test/upload-single.js
@@ -0,0 +1,52 @@
+/* eslint-env mocha */
+
+import assert from 'node:assert'
+import FormData from 'form-data'
+
+import * as util from './_util.js'
+import multer from '../index.js'
+
+describe('upload.single', () => {
+ let parser
+
+ before(() => {
+ parser = multer().single('file')
+ })
+
+ it('should accept single file', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('file', util.file('small'))
+
+ const req = await util.submitForm(parser, form)
+ assert.strictEqual(req.body.name, 'Multer')
+
+ await util.assertFile(req.file, 'file', 'small')
+ })
+
+ it('should reject multiple files', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('file', util.file('tiny'))
+ form.append('file', util.file('tiny'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ (err) => err.code === 'LIMIT_FILE_COUNT' && err.field === 'file'
+ )
+ })
+
+ it('should reject unexpected field', async () => {
+ const form = new FormData()
+
+ form.append('name', 'Multer')
+ form.append('unexpected', util.file('tiny'))
+
+ await assert.rejects(
+ util.submitForm(parser, form),
+ (err) => err.code === 'LIMIT_UNEXPECTED_FILE' && err.field === 'unexpected'
+ )
+ })
+})