Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.idea/*

# OS X
.DS_Store*
Icon?
Expand Down
68 changes: 64 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,29 @@ Key | Description
--- | ---
`fieldName` | Field name specified in the form
`originalName` | Name of the file on the user's computer (`undefined` if no filename was supplied by the client)
`size` | Size of the file in bytes
`stream` | Stream of file
`size` | Size of the file in bytes <sup>2</sup>
`stream` | A new readable stream for the stored file <sup>2</sup>
`path` | The full path where the file is stored <sup>2</sup>
`detectedMimeType` | The detected mime-type, or null if we failed to detect
`detectedFileExtension` | The typical file extension for files of the detected type, or empty string if we failed to detect (with leading `.` to match `path.extname`)
`clientReportedMimeType` | The mime type reported by the client using the `Content-Type` header, or null<sup>1</sup> if the header was absent
`clientReportedFileExtension` | The extension of the file uploaded (as reported by `path.extname`)

<sup>1</sup> Currently returns `text/plain` if header is absent, this is a bug and it will be fixed in a patch release. Do not rely on this behavior.

<sup>2</sup> Available only when the `handler` option is not used

### `multer(opts)`

Multer accepts an options object, the following are the options that can be
passed to Multer.
The following are the options that can be passed to Multer. All of them are optional.
The `opts` parameter can also be a string with a path in which case it will be used
as the destination to store files.


Key | Description
-------- | -----------
`dest` | The destination path to store files. If no destination is provided the os temporary folder is used.
`handler` | A function that allows you supply your own writable stream for customization of file storage. Using this causes `dest` to be ignored. See [using streams](#using-streams) for more information
`limits` | Limits of the uploaded data [(full description)](#limits)

#### `.single(fieldname)`
Expand Down Expand Up @@ -147,6 +154,59 @@ Key | Description | Default

Specifying the limits can help protect your site against denial of service (DoS) attacks.

### Using streams

Using handlers allows the efficient use of any stream implementation to store files anywhere.
To achieve this, just pass a function to Multer in the `handler` property that will be invoked with `req` and `file`. You
will have to return a new stream or an object specifying how the writable streams will be created. It is also
possible to return a promise from a handler in case you need some async before creating the stream.

If you provide an object instead of a stream this are the properties you should set. Only the `stream` property is required.

#### stream

A writable stream to pipe for each incoming file. By default core `fs` streams are used.

#### event

The event that finish the writes. Defaults to `'close'`. You can change this to another value
like `'finish'` or any event that your custom stream implements (Not all
writable streams emit the 'close' event so make sure to change accordingly).

#### finish

A post-processing function that executes after the event specified in the previous property is triggered.
This also gives you an opportunity to extend the file object or inspect the consumed stream. If any arguments
were received from the event they will be available as the parameters of the function. Promises are supported
here as well to allow additional processing like hashing a file, etc.

A handler could be as simple as

```javascript
function handler(req, file) {
return new writableStream()
}
```

or more complex like

```javascript
function handler(req, file) {
return doSomeAsync().then(() => {
return {
stream: createWriteStream(),
event: 'landed',
finish: function() {
return hashFile().then((hash) => {
file.metadata = { hash };
file.stream = createReadStream()
})
}
}
}
}
```

## Error handling

When encountering an error, multer will delegate the error to express. You can
Expand Down
31 changes: 21 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,51 @@
var createFileFilter = require('./lib/file-filter')
var createMiddleware = require('./lib/middleware')
var streamHandler = require('./lib/stream-handler')
var os = require('os')

function _middleware (limits, fields, fileStrategy) {
function _middleware (limits, handler, fields, fileStrategy) {
return createMiddleware(function setup () {
return {
fields: fields,
limits: limits,
handler: handler,
fileFilter: createFileFilter(fields),
fileStrategy: fileStrategy
}
})
}

function Multer (options) {
this.limits = options.limits
if (typeof options === 'string') {
this.handler = streamHandler.createHandler(options)
} else {
this.limits = options.limits
this.handler = options.handler || streamHandler.createHandler(options.dest || os.tmpdir())
}
}

Multer.prototype.single = function (name) {
return _middleware(this.limits, [{ name: name, maxCount: 1 }], 'VALUE')
return _middleware(this.limits, this.handler, [{name: name, maxCount: 1}], 'VALUE')
}

Multer.prototype.array = function (name, maxCount) {
return _middleware(this.limits, [{ name: name, maxCount: maxCount }], 'ARRAY')
return _middleware(this.limits, this.handler, [{name: name, maxCount: maxCount}], 'ARRAY')
}

Multer.prototype.fields = function (fields) {
return _middleware(this.limits, fields, 'OBJECT')
return _middleware(this.limits, this.handler, fields, 'OBJECT')
}

Multer.prototype.none = function () {
return _middleware(this.limits, [], 'NONE')
return _middleware(this.limits, this.handler, [], 'NONE')
}

Multer.prototype.any = function () {
function setup () {
return {
fields: [],
limits: this.limits,
handler: this.handler,
fileFilter: function () {},
fileStrategy: 'ARRAY'
}
Expand All @@ -47,11 +56,13 @@ Multer.prototype.any = function () {

function multer (options) {
if (options === undefined) options = {}
if (options === null) throw new TypeError('Expected object for arugment "options", got null')
if (typeof options !== 'object') throw new TypeError('Expected object for arugment "options", got ' + (typeof options))
if (options === null) throw new TypeError('Expected object for argument "options", got null')
if (typeof options !== 'object' && typeof options !== 'string') throw new TypeError('Expected object or string for argument "options", got ' + (typeof options))

if (options.handler && typeof options.handler !== 'function') throw new TypeError('The handler must be a function')

if (options.dest || options.storage || options.fileFilter) {
throw new Error('The "dest", "storage" and "fileFilter" options where removed in Multer 2.0. Please refer to the latest documentation for new usage.')
if (options.storage || options.fileFilter) {
throw new Error('The "storage" and "fileFilter" options where removed in Multer 2.0. Please refer to the latest documentation for new usage.')
}

return new Multer(options)
Expand Down
10 changes: 1 addition & 9 deletions lib/middleware.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
var is = require('type-is')
var fs = require('fs')
var appendField = require('append-field')

var createFileAppender = require('./file-appender')
Expand All @@ -11,7 +10,7 @@ module.exports = function createMiddleware (setup) {

var options = setup()

readBody(req, options.limits, options.fileFilter)
readBody(req, options)
.then(function (result) {
req.body = Object.create(null)

Expand All @@ -22,15 +21,8 @@ module.exports = function createMiddleware (setup) {
var appendFile = createFileAppender(options.fileStrategy, req, options.fields)

result.files.forEach(function (file) {
file.stream = fs.createReadStream(file.path)

file.stream.on('open', function () {
fs.unlink(file.path, function () {})
})

appendFile(file)
})

next()
})
.catch(next)
Expand Down
85 changes: 53 additions & 32 deletions lib/read-body.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
var path = require('path')
var pify = require('pify')
var temp = require('fs-temp')
var Busboy = require('busboy')
var FileType = require('stream-file-type')
var normalize = require('./stream-handler').normalize

var pump = pify(require('pump'))
var onFinished = pify(require('on-finished'))
Expand All @@ -13,9 +13,10 @@ function drainStream (stream) {
stream.on('readable', stream.read.bind(stream))
}

function collectFields (busboy, limits) {
function collectFields (busboy, options) {
return new Promise(function (resolve, reject) {
var result = []
var limits = options.limits

busboy.on('field', function (fieldname, value, fieldnameTruncated, valueTruncated) {
if (fieldnameTruncated) return reject(new MulterError('LIMIT_FIELD_KEY'))
Expand All @@ -26,7 +27,7 @@ function collectFields (busboy, limits) {
if (fieldname.length > limits.fieldNameSize) return reject(new MulterError('LIMIT_FIELD_KEY'))
}

result.push({ key: fieldname, value: value })
result.push({key: fieldname, value: value})
})

busboy.on('finish', function () {
Expand All @@ -35,9 +36,11 @@ function collectFields (busboy, limits) {
})
}

function collectFiles (busboy, limits, fileFilter) {
function collectFiles (req, busboy, options) {
return new Promise(function (resolve, reject) {
var result = []
var limits = options.limits
var fileFilter = options.fileFilter

busboy.on('file', function (fieldname, fileStream, filename, encoding, mimetype) {
// Catch all errors on file stream
Expand Down Expand Up @@ -68,30 +71,48 @@ function collectFiles (busboy, limits, fileFilter) {
reject(new MulterError('LIMIT_FILE_SIZE', fieldname))
})

var target = temp.createWriteStream()
var detector = new FileType()

var fileClosed = new Promise(function (resolve) {
target.on('close', resolve)
})

var promise = pump(fileStream, detector, target)
.then(function () {
return fileClosed
return Promise.resolve(options.handler(req, file))
.then(function (config) {
var detector = new FileType()
var handler = normalize(config)
var target = handler.stream

var fileClosed = new Promise(function (resolve, reject) {
var evt = handler.event || 'close'

target.on(evt, function () {
var finish = handler.finish
if (!finish) {
return resolve()
}

// Different stream implementations could have custom events with unknown number of arguments
// This is why the finish function can be used to gather this arguments and merge them with the file object
// Right after the stream has been consumed
Promise.resolve(finish.apply(null, arguments))
.then(function () {
resolve()
})
.catch(reject)
})
})

var promise = pump(fileStream, detector, target)
.then(function () {
return fileClosed
})
.then(function () {
return detector.fileTypePromise()
})
.then(function (fileType) {
file.detectedMimeType = (fileType ? fileType.mime : null)
file.detectedFileExtension = (fileType ? '.' + fileType.ext : '')
return file
})
.catch(reject)

result.push(promise)
})
.then(function () {
return detector.fileTypePromise()
})
.then(function (fileType) {
file.path = target.path
file.size = target.bytesWritten
file.detectedMimeType = (fileType ? fileType.mime : null)
file.detectedFileExtension = (fileType ? '.' + fileType.ext : '')
return file
})
.catch(reject)

result.push(promise)
})
.catch(reject)
})
Expand All @@ -102,17 +123,17 @@ function collectFiles (busboy, limits, fileFilter) {
})
}

function readBody (req, limits, fileFilter) {
function readBody (req, options) {
var busboy

try {
busboy = new Busboy({ headers: req.headers, limits: limits })
busboy = new Busboy({headers: req.headers, limits: options.limits})
} catch (err) {
return Promise.reject(err)
}

var fields = collectFields(busboy, limits)
var files = collectFiles(busboy, limits, fileFilter)
var fields = collectFields(busboy, options)
var files = collectFiles(req, busboy, options)
var guard = new Promise(function (resolve, reject) {
req.on('error', function (err) { reject(err) })
busboy.on('error', function (err) { reject(err) })
Expand All @@ -129,7 +150,7 @@ function readBody (req, limits, fileFilter) {

return Promise.all([fields, files, guard])
.then(function (result) {
return { fields: result[0], files: result[1] }
return {fields: result[0], files: result[1]}
})
.catch(function (err) {
req.unpipe(busboy)
Expand Down
43 changes: 43 additions & 0 deletions lib/stream-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict'

var crytpo = require('crypto')
var fs = require('fs')
var path = require('path')
var isStream = require('is-stream')

function randomBytes () {
return new Promise(function (resolve, reject) {
crytpo.randomBytes(16, function (err, buf) {
if (err) {
return reject(err)
}
resolve(buf)
})
})
}

module.exports.createHandler = function createHandler (destination) {
return function handler (req, file) {
return randomBytes().then(function (buf) {
var stream
var filename = buf.toString('hex')
stream = fs.createWriteStream(path.join(destination, filename))
return {
stream: stream,
event: 'close',
finish: function () {
file.size = stream.bytesWritten
file.path = stream.path
file.stream = fs.createReadStream(stream.path)
}
}
})
}
}

module.exports.normalize = function normalize (handler) {
if (isStream.writable(handler)) {
return {stream: handler}
}
return handler
}
Loading