diff --git a/README.md b/README.md index b22eeb9..8f14050 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # Description Simple express middleware for uploading files. +# Version 0.1.0 Breaking Change +As of `v0.1.0`, there is NO MORE `application/x-www-form-urlencoded` SUPPORT! + +If you want to parse `urlencoded` requests, [use body-parser](https://github.com/expressjs/body-parser#bodyparserurlencodedoptions). + +Moving forward, express-fileupload is considered a "multipart" solution only. + # Install ```bash # With NPM @@ -113,10 +120,6 @@ Option | Acceptable Values | Details --- | --- | --- safeFileNames | | Strips characters from the upload's filename. You can use custom regex to determine what to strip. If set to `true`, non-alphanumeric characters _except_ dashes and underscores will be stripped. This option is off by default.

**Example #1 (strip slashes from file names):** `app.use(fileUpload({ safeFileNames: /\\/g }))`
**Example #2:** `app.use(fileUpload({ safeFileNames: true }))` -# Known Bugs -##### If you're using bodyParser middleware -Add `app.use(fileUpload())` *AFTER* `app.use(bodyParser.json)` and any other `bodyParser` middlewares! This limitation will be investigated in an upcoming release. - # Help Wanted Pull Requests are welcomed! diff --git a/lib/index.js b/lib/index.js index 5849af0..5bb8bd5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,95 +1,169 @@ -var busboy = require('connect-busboy'); +var Busboy = require('busboy'); var fs = require('fs-extra'); var streamifier = require('streamifier'); +var ACCEPTABLE_MIME = /^(?:multipart\/.+)$/i; +var UNACCEPTABLE_METHODS = [ + 'GET', + 'HEAD' +]; + module.exports = function(options) { options = options || {}; return function(req, res, next) { - return busboy(options)(req, res, function() { - - // If no busboy req obj, then no uploads are taking place - if (!req.busboy) + if (!hasBody(req) || !hasAcceptableMethod(req) || !hasAcceptableMime(req)) return next(); - req.files = null; - - req.busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) { - req.body = req.body || {}; - - var prev = req.body[fieldname]; - - if (!prev) - return req.body[fieldname] = val; - - if (Array.isArray(prev)) - return prev.push(val); - - req.body[fieldname] = [prev, val]; - }); - - req.busboy.on('file', function(fieldname, file, filename, encoding, mimetype) { - var buf = new Buffer(0); - var safeFileNameRegex = /[^\w-]/g; - - file.on('data', function(data) { - buf = Buffer.concat([buf, data]); - - if (options.debug) - return console.log('Uploading %s -> %s', fieldname, filename); - }); - - file.on('end', function() { - if (!req.files) - req.files = {}; - - // see: https://github.com/richardgirges/express-fileupload/issues/14 - // firefox uploads empty file in case of cache miss when f5ing page. - // resulting in unexpected behavior. if there is no file data, the file is invalid. - if(!buf.length) - return; - - if (options.safeFileNames) { - if (typeof options.safeFileNames === 'object') - safeFileNameRegex = options.safeFileNames; - - filename = filename.replace(safeFileNameRegex, ''); - } - - var newFile = { - name: filename, - data: buf, - encoding: encoding, - mimetype: mimetype, - mv: function(path, callback) { - var fstream; - fstream = fs.createWriteStream(path); - streamifier.createReadStream(buf).pipe(fstream); - fstream.on('error', function(error) { - if (callback) - callback(error); - }); - fstream.on('close', function() { - if (callback) - callback(null); - }); - } - }; - - if (!req.files.hasOwnProperty(fieldname)) { - req.files[fieldname] = newFile; - } else { - if (req.files[fieldname] instanceof Array) - req.files[fieldname].push(newFile); - else - req.files[fieldname] = [req.files[fieldname], newFile]; - } - }); - }); - - req.busboy.on('finish', next); - - req.pipe(req.busboy); - }); + processMultipart(options, req, res, next); }; }; + + +/** + * Processes multipart request + * Builds a req.body object for fields + * Builds a req.files object for files + * @param {Object} options expressFileupload and Busboy options + * @param {Object} req Express request object + * @param {Object} res Express response object + * @param {Function} next Express next method + * @return {void} + */ +function processMultipart(options, req, res, next) { + var busboyOptions = {}; + var busboy; + + req.files = null; + + // Build busboy config + for (var k in options) { + busboyOptions[k] = options[k]; + } + + // Attach request headers to busboy config + busboyOptions.headers = req.headers; + + // Init busboy instance + busboy = new Busboy(busboyOptions); + + // Build multipart req.body fields + busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mime) { + req.body = req.body || {}; + + var prev = req.body[fieldname]; + + if (!prev) + return req.body[fieldname] = val; + + if (Array.isArray(prev)) + return prev.push(val); + + req.body[fieldname] = [prev, val]; + }); + + // Build req.files fields + busboy.on('file', function(fieldname, file, filename, encoding, mime) { + var buf = new Buffer(0); + var safeFileNameRegex = /[^\w-]/g; + + file.on('data', function(data) { + buf = Buffer.concat([buf, data]); + + if (options.debug) + return console.log('Uploading %s -> %s', fieldname, filename); + }); + + file.on('end', function() { + if (!req.files) + req.files = {}; + + // see: https://github.com/richardgirges/express-fileupload/issues/14 + // firefox uploads empty file in case of cache miss when f5ing page. + // resulting in unexpected behavior. if there is no file data, the file is invalid. + if(!buf.length) + return; + + if (options.safeFileNames) { + if (typeof options.safeFileNames === 'object') + safeFileNameRegex = options.safeFileNames; + + filename = filename.replace(safeFileNameRegex, ''); + } + + var newFile = { + name: filename, + data: buf, + encoding: encoding, + mimetype: mime, + mv: function(path, callback) { + var fstream = fs.createWriteStream(path); + + streamifier.createReadStream(buf).pipe(fstream); + + fstream.on('error', function(error) { + if (callback) + callback(error); + }); + + fstream.on('close', function() { + if (callback) + callback(null); + }); + } + }; + + // Non-array fields + if (!req.files.hasOwnProperty(fieldname)) { + req.files[fieldname] = newFile; + } else { + // Array fields + if (req.files[fieldname] instanceof Array) + req.files[fieldname].push(newFile); + else + req.files[fieldname] = [req.files[fieldname], newFile]; + } + }); + }); + + busboy.on('finish', next); + + req.pipe(busboy); +} + + +/************************************************************** + * Methods below were copied from, or heavily inspired by + * the Connect and connect-busboy packages + **************************************************************/ + +/** + * Ensures the request is not using a non-compliant multipart method + * such as GET or HEAD + * @param {Object} req Express req object + * @return {Boolean} + */ +function hasAcceptableMethod(req) { + return (UNACCEPTABLE_METHODS.indexOf(req.method) < 0); +} + +/** + * Ensures that only multipart requests are processed by express-fileupload + * @param {Object} req Express req object + * @return {Boolean} + */ +function hasAcceptableMime(req) { + var str = (req.headers['content-type'] || '').split(';')[0]; + + return ACCEPTABLE_MIME.test(str); +} + +/** + * Ensures the request contains a content body + * @param {Object} req Express req object + * @return {Boolean} + */ +function hasBody(req) { + return ('transfer-encoding' in req.headers) || + ('content-length' in req.headers && req.headers['content-length'] !== '0'); +} \ No newline at end of file diff --git a/package.json b/package.json index d27a84b..cacd4d7 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { "name": "express-fileupload", - "version": "0.0.7", + "version": "0.1.0-beta", "author": "Richard Girges ", - "description": "Simple express file upload middleware that wraps around connect-busboy", + "description": "Simple express file upload middleware that wraps around Busboy", "main": "./lib/index", "dependencies": { "busboy": "^0.2.14", - "connect-busboy": "0.0.2", "fs-extra": "^0.22.1", "streamifier": "^0.1.1" }, @@ -20,7 +19,7 @@ "forms", "multipart", "files", - "connect-busboy", + "busboy", "middleware" ], "licenses": [ diff --git a/yarn.lock b/yarn.lock index ad68e56..fbcc366 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,7 +24,7 @@ brace-expansion@^1.0.0: balanced-match "^0.4.1" concat-map "0.0.1" -busboy@*, busboy@^0.2.14: +busboy@^0.2.14: version "0.2.14" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" dependencies: @@ -35,12 +35,6 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -connect-busboy@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/connect-busboy/-/connect-busboy-0.0.2.tgz#ac5c9c96672171885e576c66b2bfd95d3bb11097" - dependencies: - busboy "*" - content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"