express-fileupload
Advanced tools
Comparing version 0.4.0 to 1.0.0-alpha.1
246
lib/index.js
'use strict'; | ||
const Busboy = require('busboy'); | ||
const fs = require('fs-extra'); | ||
const streamifier = require('streamifier'); | ||
const md5 = require('md5'); | ||
const fileFactory = require('./fileFactory'); | ||
const processMultipart = require('./processMultipart'); | ||
const isEligibleRequest = require('./isEligibleRequest'); | ||
const ACCEPTABLE_MIME = /^(?:multipart\/.+)$/i; | ||
const UNACCEPTABLE_METHODS = [ | ||
'GET', | ||
'HEAD' | ||
]; | ||
const fileUploadOptionsDefaults = { | ||
safeFileNames: false, | ||
preserveExtension: false, | ||
abortOnLimit: false, | ||
createParentPath: false | ||
}; | ||
module.exports = function(options) { | ||
options = options || {}; | ||
/** | ||
* Expose the file upload middleware | ||
*/ | ||
module.exports = function(fileUploadOptions) { | ||
fileUploadOptions = Object.assign({}, fileUploadOptionsDefaults, fileUploadOptions || {}); | ||
return function(req, res, next) { | ||
if (!hasBody(req) || !hasAcceptableMethod(req) || !hasAcceptableMime(req)) { | ||
if (!isEligibleRequest(req)) { | ||
return next(); | ||
} | ||
processMultipart(options, req, res, next); | ||
processMultipart(fileUploadOptions, req, res, next); | ||
}; | ||
@@ -27,217 +30,4 @@ }; | ||
/** | ||
* 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} | ||
* Quietly expose fileFactory; useful for testing | ||
*/ | ||
function processMultipart(options, req, res, next) { | ||
let busboyOptions = {}; | ||
let busboy; | ||
req.files = null; | ||
// Build busboy config | ||
for (let k in options) { | ||
if (Object.prototype.hasOwnProperty.call(options, k)) { | ||
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 || {}; | ||
let 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) { | ||
const buffers = []; | ||
let safeFileNameRegex = /[^\w-]/g; | ||
file.on('limit', () => { | ||
if (options.abortOnLimit) { | ||
res.writeHead(413, {'Connection': 'close'}); | ||
res.end('File size limit has been reached'); | ||
} | ||
}); | ||
file.on('data', function(data) { | ||
buffers.push(data); | ||
if (options.debug) { | ||
return console.log('Uploading %s -> %s', fieldname, filename); | ||
} | ||
}); | ||
file.on('end', function() { | ||
if (!req.files) { | ||
req.files = {}; | ||
} | ||
const buf = Buffer.concat(buffers); | ||
// 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) { | ||
let maxExtensionLength = 3; | ||
let extension = ''; | ||
if (typeof options.safeFileNames === 'object') { | ||
safeFileNameRegex = options.safeFileNames; | ||
} | ||
maxExtensionLength = parseInt(options.preserveExtension); | ||
if (options.preserveExtension || maxExtensionLength === 0) { | ||
if (isNaN(maxExtensionLength)) { | ||
maxExtensionLength = 3; | ||
} else { | ||
maxExtensionLength = Math.abs(maxExtensionLength); | ||
} | ||
let filenameParts = filename.split('.'); | ||
let filenamePartsLen = filenameParts.length; | ||
if (filenamePartsLen > 1) { | ||
extension = filenameParts.pop(); | ||
if (extension.length > maxExtensionLength && maxExtensionLength > 0) { | ||
filenameParts[filenameParts.length - 1] += | ||
'.' + extension.substr(0, extension.length - maxExtensionLength); | ||
extension = extension.substr(-maxExtensionLength); | ||
} | ||
extension = maxExtensionLength ? '.' + extension.replace(safeFileNameRegex, '') : ''; | ||
filename = filenameParts.join('.'); | ||
} | ||
} | ||
filename = filename.replace(safeFileNameRegex, '').concat(extension); | ||
} | ||
let newFile = { | ||
name: filename, | ||
data: buf, | ||
encoding: encoding, | ||
truncated: file.truncated, | ||
mimetype: mime, | ||
md5: md5(buf), | ||
mv: function(path, callback) { | ||
// Callback is passed in, use the callback API | ||
if (callback) { | ||
doMove( | ||
() => { | ||
callback(null); | ||
}, | ||
(error) => { | ||
callback(error); | ||
} | ||
); | ||
// Otherwise, return a promise | ||
} else { | ||
return new Promise((resolve, reject) => { | ||
doMove(resolve, reject); | ||
}); | ||
} | ||
/** | ||
* Local function that moves the file to a different location on the filesystem | ||
* Takes two function arguments to make it compatible w/ Promise or Callback APIs | ||
* @param {Function} successFunc | ||
* @param {Function} errorFunc | ||
*/ | ||
function doMove(successFunc, errorFunc) { | ||
const fstream = fs.createWriteStream(path); | ||
streamifier.createReadStream(buf).pipe(fstream); | ||
fstream.on('error', function(error) { | ||
errorFunc(error); | ||
}); | ||
fstream.on('close', function() { | ||
successFunc(); | ||
}); | ||
} | ||
} | ||
}; | ||
// 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]; | ||
} | ||
} | ||
}); | ||
file.on('error', next); | ||
}); | ||
busboy.on('finish', next); | ||
busboy.on('error', 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) { | ||
let 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'); | ||
} | ||
module.exports.fileFactory = fileFactory; |
{ | ||
"name": "express-fileupload", | ||
"version": "0.4.0", | ||
"version": "1.0.0-alpha.1", | ||
"author": "Richard Girges <richardgirges@gmail.com>", | ||
@@ -14,3 +14,2 @@ "description": "Simple express file upload middleware that wraps around Busboy", | ||
"busboy": "^0.2.14", | ||
"fs-extra": "^4.0.1", | ||
"md5": "^2.2.1", | ||
@@ -35,11 +34,11 @@ "streamifier": "^0.1.1" | ||
"devDependencies": { | ||
"body-parser": "^1.17.2", | ||
"coveralls": "^2.13.1", | ||
"eslint": "^4.5.0", | ||
"eslint-config-google": "^0.9.1", | ||
"express": "^4.15.4", | ||
"body-parser": "^1.18.3", | ||
"coveralls": "^3.0.2", | ||
"eslint": "^5.6.0", | ||
"express": "^4.16.3", | ||
"istanbul": "^0.4.5", | ||
"mocha": "^3.5.0", | ||
"supertest": "^3.0.0" | ||
"mocha": "^5.2.0", | ||
"rimraf": "^2.6.2", | ||
"supertest": "^3.3.0" | ||
} | ||
} |
@@ -0,1 +1,4 @@ | ||
## !! LOOKING FOR ADDITIONAL MAINTAINERS !! | ||
Contact @richardgirges if you're interested. | ||
# express-fileupload | ||
@@ -44,2 +47,3 @@ Simple express middleware for uploading files. | ||
* `req.files.foo.truncated`: A boolean that represents if the file is over the size limit | ||
* `req.files.foo.md5`: A function that returns an MD5 checksum of the uploaded file | ||
@@ -123,2 +127,3 @@ ### Full Example | ||
--- | --- | --- | ||
createParentPath | <ul><li><code>false</code> **(default)**</li><li><code>true</code></ul> | Automatically creates the directory path specified in `.mv(filePathName)` | ||
safeFileNames | <ul><li><code>false</code> **(default)**</li><li><code>true</code></li><li>regex</li></ul> | 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.<br /><br />**Example #1 (strip slashes from file names):** `app.use(fileUpload({ safeFileNames: /\\/g }))`<br />**Example #2:** `app.use(fileUpload({ safeFileNames: true }))` | ||
@@ -125,0 +130,0 @@ preserveExtension | <ul><li><code>false</code> **(default)**</li><li><code>true</code></li><li><code>*Number*</code></li></ul> | Preserves filename extension when using <code>safeFileNames</code> option. If set to <code>true</code>, will default to an extension length of 3. If set to <code>*Number*</code>, this will be the max allowable extension length. If an extension is smaller than the extension length, it remains untouched. If the extension is longer, it is shifted.<br /><br />**Example #1 (true):**<br /><code>app.use(fileUpload({ safeFileNames: true, preserveExtension: true }));</code><br />*myFileName.ext* --> *myFileName.ext*<br /><br />**Example #2 (max extension length 2, extension shifted):**<br /><code>app.use(fileUpload({ safeFileNames: true, preserveExtension: 2 }));</code><br />*myFileName.ext* --> *myFileNamee.xt* |
@@ -48,3 +48,3 @@ 'use strict'; | ||
.expect(200) | ||
.end(function(err, res) { | ||
.end(function(err) { | ||
if (err) { | ||
@@ -68,3 +68,3 @@ return done(err); | ||
.expect(200) | ||
.end(function(err, res) { | ||
.end(function(err) { | ||
if (err) { | ||
@@ -121,3 +121,3 @@ return done(err); | ||
.expect(200) | ||
.end(function(err, res) { | ||
.end(function(err) { | ||
if (err) { | ||
@@ -141,3 +141,3 @@ return done(err); | ||
.expect(200) | ||
.end(function(err, res) { | ||
.end(function(err) { | ||
if (err) { | ||
@@ -194,3 +194,3 @@ return done(err); | ||
.expect(200) | ||
.end(function(err, res) { | ||
.end(function(err) { | ||
if (err) { | ||
@@ -229,3 +229,3 @@ return done(err); | ||
.expect(200) | ||
.end(function(err, res) { | ||
.end(function(err) { | ||
if (err) { | ||
@@ -265,3 +265,3 @@ return done(err); | ||
}, | ||
function(err, res) { | ||
function(err) { | ||
if (err) { | ||
@@ -292,3 +292,3 @@ return done(err); | ||
}, | ||
function(err, res) { | ||
function(err) { | ||
if (err) { | ||
@@ -295,0 +295,0 @@ return done(err); |
@@ -71,3 +71,3 @@ const fs = require('fs'); | ||
function(done) { | ||
const fileUploadOptions = {safeFileNames: /[\$#]/g}; | ||
const fileUploadOptions = {safeFileNames: /[$#]/g}; | ||
const actualFileName = 'my$Invalid#fileName.png123'; | ||
@@ -74,0 +74,0 @@ const expectedFileName = 'myInvalidfileName.png123'; |
'use strict'; | ||
const path = require('path'); | ||
const fileDir = path.join(__dirname, 'files'); | ||
const uploadDir = path.join(__dirname, 'uploads'); | ||
const fs = require('fs-extra'); | ||
const fs = require('fs'); | ||
const rimraf = require('rimraf'); | ||
@@ -11,3 +13,4 @@ const clearUploadsDir = function() { | ||
} else { | ||
fs.emptyDirSync(uploadDir); | ||
rimraf.sync(uploadDir); | ||
fs.mkdirSync(uploadDir); | ||
} | ||
@@ -35,2 +38,3 @@ }; | ||
if (err) { | ||
console.log('ERR', err); // eslint-disable-line | ||
return res.status(500).send(err); | ||
@@ -37,0 +41,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
3
24
1046
136
1163561
6
- Removedfs-extra@^4.0.1
- Removedfs-extra@4.0.3(transitive)
- Removedgraceful-fs@4.2.11(transitive)
- Removedjsonfile@4.0.0(transitive)
- Removeduniversalify@0.1.2(transitive)