fix merge conflicts; split the library up into separate files to promote easier testability

dev
Richard Girges 6 years ago
commit aa57dc43aa
  1. 1
      .eslintrc
  2. 63
      lib/fileFactory.js
  3. 254
      lib/index.js
  4. 44
      lib/isEligibleRequest.js
  5. 147
      lib/processMultipart.js
  6. 78
      test/fileFactory.spec.js
  7. 1
      test/server.js

@ -16,6 +16,7 @@
"code": 100,
"tabWidth": 2
}],
"semi": 2,
"keyword-spacing": 2,
"indent": [2, 2, { "SwitchCase": 1 }]
}

@ -0,0 +1,63 @@
'use strict';
const fs = require('fs');
const path = require('path');
const streamifier = require('streamifier');
const md5 = require('md5');
module.exports = function(options, fileUploadOptions = null) {
return {
name: options.name,
data: options.buffer,
encoding: options.encoding,
truncated: options.truncated,
mimetype: options.mimetype,
md5: md5(options.buffer),
mv: function(filePath, 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) {
if (fileUploadOptions && fileUploadOptions.createParentPath) {
const parentPath = path.dirname(filePath);
if (!fs.existsSync(parentPath)) {
fs.mkdirSync(parentPath);
}
}
const fstream = fs.createWriteStream(filePath);
streamifier.createReadStream(options.buffer).pipe(fstream);
fstream.on('error', function(error) {
errorFunc(error);
});
fstream.on('close', function() {
successFunc();
});
}
}
};
};

@ -1,250 +1,30 @@
'use strict';
const Busboy = require('busboy');
const fs = require('fs');
const path = require('path');
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'
];
module.exports = function(options) {
options = options || {};
/**
* Expose the file upload middleware
*/
module.exports = function(fileUploadOptions) {
fileUploadOptions = Object.assign({}, fileUploadOptions || {}, {
safeFileNames: false,
preserveExtension: false,
abortOnLimit: false,
createParentPath: false
});
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);
};
};
/**
* 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) {
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) {
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); // eslint-disable-line
}
});
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(filePath, 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) {
if (options.createParentPath) {
const parentPath = path.dirname(filePath);
if (!fs.existsSync(parentPath)) {
fs.mkdirSync(parentPath);
}
}
const fstream = fs.createWriteStream(filePath);
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}
* Quietly expose fileFactory; useful for testing
*/
function hasBody(req) {
return ('transfer-encoding' in req.headers) ||
('content-length' in req.headers && req.headers['content-length'] !== '0');
}
module.exports.fileFactory = fileFactory;

@ -0,0 +1,44 @@
const ACCEPTABLE_CONTENT_TYPE = /^(?:multipart\/.+)$/i;
const UNACCEPTABLE_METHODS = [
'GET',
'HEAD'
];
/**
* Ensures that the request in question is eligible for file uploads
* @param {Object} req Express req object
*/
module.exports = function(req) {
return hasBody(req) && hasAcceptableMethod(req) && hasAcceptableContentType(req);
};
/**
* 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 hasAcceptableContentType(req) {
let str = (req.headers['content-type'] || '').split(';')[0];
return ACCEPTABLE_CONTENT_TYPE.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');
}

@ -0,0 +1,147 @@
const Busboy = require('busboy');
const fileFactory = require('./fileFactory');
/**
* 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}
*/
module.exports = 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) {
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); // eslint-disable-line
}
});
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);
}
const newFile = fileFactory({
name: filename,
buffer: buf,
encoding: encoding,
truncated: file.truncated,
mimetype: mime
});
// 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);
};

@ -0,0 +1,78 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const md5 = require('md5');
const path = require('path');
const fileFactory = require('../lib').fileFactory;
const server = require('./server');
const mockBuffer = fs.readFileSync(path.join(server.fileDir, 'basketball.png'));
describe('Test of the fileFactory factory', function() {
beforeEach(function() {
server.clearUploadsDir();
});
it('return a file object', function() {
assert.ok(fileFactory({
name: 'basketball.png',
buffer: mockBuffer
}));
});
describe('File object behavior', function() {
const file = fileFactory({
name: 'basketball.png',
buffer: mockBuffer
});
it('move the file to the specified folder', function(done) {
file.mv(path.join(server.uploadDir, 'basketball.png'), function(err) {
assert.ifError(err);
done();
});
});
it('reject the mv if the destination does not exists', function(done) {
file.mv(path.join(server.uploadDir, 'unknown', 'basketball.png'), function(err) {
assert.ok(err);
done();
});
});
});
describe('Properties', function() {
it('contains the name property', function() {
assert.equal(fileFactory({
name: 'basketball.png',
buffer: mockBuffer
}).name, 'basketball.png');
});
it('contains the data property', function() {
assert.ok(fileFactory({
name: 'basketball.png',
buffer: mockBuffer
}).data);
});
it('contains the encoding property', function() {
assert.equal(fileFactory({
name: 'basketball.png',
buffer: mockBuffer,
encoding: 'utf-8'
}).encoding, 'utf-8');
});
it('contains the mimetype property', function() {
assert.equal(fileFactory({
name: 'basketball.png',
buffer: mockBuffer,
mimetype: 'image/png'
}).mimetype, 'image/png');
});
it('contains the md5 property', function() {
const mockMd5 = md5(mockBuffer);
assert.equal(fileFactory({
name: 'basketball.png',
buffer: mockBuffer
}).md5, mockMd5);
});
});
});

@ -1,4 +1,5 @@
'use strict';
const path = require('path');
const fileDir = path.join(__dirname, 'files');
const uploadDir = path.join(__dirname, 'uploads');

Loading…
Cancel
Save