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' + ) + }) +})