mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-25 02:31:59 -05:00
Merge pull request #6659 from jaswilli/multer
Replace busboy upload middleware with multer
This commit is contained in:
commit
7ff74010fd
12 changed files with 54 additions and 136 deletions
|
@ -66,13 +66,16 @@ db = {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
function validate(options) {
|
function validate(options) {
|
||||||
|
options.name = options.originalname;
|
||||||
|
options.type = options.mimetype;
|
||||||
|
|
||||||
// Check if a file was provided
|
// Check if a file was provided
|
||||||
if (!utils.checkFileExists(options, 'importfile')) {
|
if (!utils.checkFileExists(options)) {
|
||||||
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.db.selectFileToImport')));
|
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.db.selectFileToImport')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the file is valid
|
// Check if the file is valid
|
||||||
if (!utils.checkFileIsValid(options.importfile, importer.getTypes(), importer.getExtensions())) {
|
if (!utils.checkFileIsValid(options, importer.getTypes(), importer.getExtensions())) {
|
||||||
return Promise.reject(new errors.UnsupportedMediaTypeError(
|
return Promise.reject(new errors.UnsupportedMediaTypeError(
|
||||||
i18n.t('errors.api.db.unsupportedFile') +
|
i18n.t('errors.api.db.unsupportedFile') +
|
||||||
_.reduce(importer.getExtensions(), function (memo, ext) {
|
_.reduce(importer.getExtensions(), function (memo, ext) {
|
||||||
|
@ -85,7 +88,7 @@ db = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function importContent(options) {
|
function importContent(options) {
|
||||||
return importer.importFromFile(options.importfile)
|
return importer.importFromFile(options)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
api.settings.updateSettingsCache();
|
api.settings.updateSettingsCache();
|
||||||
})
|
})
|
||||||
|
|
|
@ -192,7 +192,7 @@ http = function http(apiMethod) {
|
||||||
return function apiHandler(req, res, next) {
|
return function apiHandler(req, res, next) {
|
||||||
// We define 2 properties for using as arguments in API calls:
|
// We define 2 properties for using as arguments in API calls:
|
||||||
var object = req.body,
|
var object = req.body,
|
||||||
options = _.extend({}, req.files, req.query, req.params, {
|
options = _.extend({}, req.file, req.query, req.params, {
|
||||||
context: {
|
context: {
|
||||||
user: (req.user && req.user.id) ? req.user.id : null
|
user: (req.user && req.user.id) ? req.user.id : null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
var config = require('../config'),
|
var config = require('../config'),
|
||||||
Promise = require('bluebird'),
|
Promise = require('bluebird'),
|
||||||
fs = require('fs-extra'),
|
fs = require('fs-extra'),
|
||||||
|
pUnlink = Promise.promisify(fs.unlink),
|
||||||
storage = require('../storage'),
|
storage = require('../storage'),
|
||||||
errors = require('../errors'),
|
errors = require('../errors'),
|
||||||
utils = require('./utils'),
|
utils = require('./utils'),
|
||||||
|
@ -20,31 +21,31 @@ upload = {
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* @param {{context}} options
|
* @param {{context}} options
|
||||||
* @returns {Promise} Success
|
* @returns {Promise<String>} location of uploaded file
|
||||||
*/
|
*/
|
||||||
add: function (options) {
|
add: Promise.method(function (options) {
|
||||||
var store = storage.getStorage(),
|
var store = storage.getStorage();
|
||||||
filepath;
|
|
||||||
|
// Public interface of the storage module's `save` method requires
|
||||||
|
// the file's name to be on the .name property.
|
||||||
|
options.name = options.originalname;
|
||||||
|
options.type = options.mimetype;
|
||||||
|
|
||||||
// Check if a file was provided
|
// Check if a file was provided
|
||||||
if (!utils.checkFileExists(options, 'uploadimage')) {
|
if (!utils.checkFileExists(options)) {
|
||||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.upload.pleaseSelectImage')));
|
throw new errors.NoPermissionError(i18n.t('errors.api.upload.pleaseSelectImage'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the file is valid
|
// Check if the file is valid
|
||||||
if (!utils.checkFileIsValid(options.uploadimage, config.uploads.contentTypes, config.uploads.extensions)) {
|
if (!utils.checkFileIsValid(options, config.uploads.contentTypes, config.uploads.extensions)) {
|
||||||
return Promise.reject(new errors.UnsupportedMediaTypeError(i18n.t('errors.api.upload.pleaseSelectValidImage')));
|
throw new errors.UnsupportedMediaTypeError(i18n.t('errors.api.upload.pleaseSelectValidImage'));
|
||||||
}
|
}
|
||||||
|
|
||||||
filepath = options.uploadimage.path;
|
return store.save(options).finally(function () {
|
||||||
|
|
||||||
return store.save(options.uploadimage).then(function (url) {
|
|
||||||
return url;
|
|
||||||
}).finally(function () {
|
|
||||||
// Remove uploaded file from tmp location
|
// Remove uploaded file from tmp location
|
||||||
return Promise.promisify(fs.unlink)(filepath);
|
return pUnlink(options.path);
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = upload;
|
module.exports = upload;
|
||||||
|
|
|
@ -289,12 +289,12 @@ utils = {
|
||||||
|
|
||||||
return Promise.resolve(object);
|
return Promise.resolve(object);
|
||||||
},
|
},
|
||||||
checkFileExists: function (options, filename) {
|
checkFileExists: function (fileData) {
|
||||||
return !!(options[filename] && options[filename].type && options[filename].path);
|
return !!(fileData.mimetype && fileData.path);
|
||||||
},
|
},
|
||||||
checkFileIsValid: function (file, types, extensions) {
|
checkFileIsValid: function (fileData, types, extensions) {
|
||||||
var type = file.type,
|
var type = fileData.mimetype,
|
||||||
ext = path.extname(file.name).toLowerCase();
|
ext = path.extname(fileData.name).toLowerCase();
|
||||||
|
|
||||||
if (_.contains(types, type) && _.contains(extensions, ext)) {
|
if (_.contains(types, type) && _.contains(extensions, ext)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
var BusBoy = require('busboy'),
|
|
||||||
fs = require('fs-extra'),
|
|
||||||
path = require('path'),
|
|
||||||
os = require('os'),
|
|
||||||
i18n = require('../i18n'),
|
|
||||||
crypto = require('crypto');
|
|
||||||
|
|
||||||
// ### ghostBusboy
|
|
||||||
// Process multipart file streams
|
|
||||||
function ghostBusBoy(req, res, next) {
|
|
||||||
var busboy,
|
|
||||||
stream,
|
|
||||||
tmpDir;
|
|
||||||
|
|
||||||
// busboy is only used for POST requests
|
|
||||||
if (req.method && !/post/i.test(req.method)) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
busboy = new BusBoy({headers: req.headers});
|
|
||||||
tmpDir = os.tmpdir();
|
|
||||||
|
|
||||||
req.files = req.files || {};
|
|
||||||
req.body = req.body || {};
|
|
||||||
|
|
||||||
busboy.on('file', function onFile(fieldname, file, filename, encoding, mimetype) {
|
|
||||||
var filePath,
|
|
||||||
tmpFileName,
|
|
||||||
md5 = crypto.createHash('md5');
|
|
||||||
|
|
||||||
// If the filename is invalid, skip the stream
|
|
||||||
if (!filename) {
|
|
||||||
return file.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an MD5 hash of original filename
|
|
||||||
md5.update(filename, 'utf8');
|
|
||||||
|
|
||||||
tmpFileName = (new Date()).getTime() + md5.digest('hex');
|
|
||||||
|
|
||||||
filePath = path.join(tmpDir, tmpFileName || 'temp.tmp');
|
|
||||||
|
|
||||||
file.on('end', function end() {
|
|
||||||
req.files[fieldname] = {
|
|
||||||
type: mimetype,
|
|
||||||
encoding: encoding,
|
|
||||||
name: filename,
|
|
||||||
path: filePath
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
file.on('error', function onError(error) {
|
|
||||||
console.log('Error', i18n.t('errors.middleware.ghostbusboy.fileUploadingError'), error);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream = fs.createWriteStream(filePath);
|
|
||||||
|
|
||||||
stream.on('error', function onError(error) {
|
|
||||||
console.log('Error', i18n.t('errors.middleware.ghostbusboy.fileUploadingError'), error);
|
|
||||||
});
|
|
||||||
|
|
||||||
file.pipe(stream);
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on('error', function onError(error) {
|
|
||||||
console.log('Error', i18n.t('errors.middleware.ghostbusboy.somethingWentWrong'), error);
|
|
||||||
res.status(500).send({code: 500, message: i18n.t('errors.middleware.ghostbusboy.couldNotParseUpload')});
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on('field', function onField(fieldname, val) {
|
|
||||||
req.body[fieldname] = val;
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on('finish', function onFinish() {
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
req.pipe(busboy);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ghostBusBoy;
|
|
|
@ -10,9 +10,9 @@ var bodyParser = require('body-parser'),
|
||||||
passport = require('passport'),
|
passport = require('passport'),
|
||||||
utils = require('../utils'),
|
utils = require('../utils'),
|
||||||
sitemapHandler = require('../data/xml/sitemap/handler'),
|
sitemapHandler = require('../data/xml/sitemap/handler'),
|
||||||
|
multer = require('multer'),
|
||||||
|
tmpdir = require('os').tmpdir,
|
||||||
authStrategies = require('./auth-strategies'),
|
authStrategies = require('./auth-strategies'),
|
||||||
busboy = require('./ghost-busboy'),
|
|
||||||
auth = require('./auth'),
|
auth = require('./auth'),
|
||||||
cacheControl = require('./cache-control'),
|
cacheControl = require('./cache-control'),
|
||||||
checkSSL = require('./check-ssl'),
|
checkSSL = require('./check-ssl'),
|
||||||
|
@ -34,7 +34,7 @@ var bodyParser = require('body-parser'),
|
||||||
setupMiddleware;
|
setupMiddleware;
|
||||||
|
|
||||||
middleware = {
|
middleware = {
|
||||||
busboy: busboy,
|
upload: multer({dest: tmpdir()}),
|
||||||
cacheControl: cacheControl,
|
cacheControl: cacheControl,
|
||||||
spamPrevention: spamPrevention,
|
spamPrevention: spamPrevention,
|
||||||
privateBlogging: privateBlogging,
|
privateBlogging: privateBlogging,
|
||||||
|
|
|
@ -85,7 +85,7 @@ apiRoutes = function apiRoutes(middleware) {
|
||||||
|
|
||||||
// ## DB
|
// ## DB
|
||||||
router.get('/db', authenticatePrivate, api.http(api.db.exportContent));
|
router.get('/db', authenticatePrivate, api.http(api.db.exportContent));
|
||||||
router.post('/db', authenticatePrivate, middleware.busboy, api.http(api.db.importContent));
|
router.post('/db', authenticatePrivate, middleware.upload.single('importfile'), api.http(api.db.importContent));
|
||||||
router.del('/db', authenticatePrivate, api.http(api.db.deleteAllContent));
|
router.del('/db', authenticatePrivate, api.http(api.db.deleteAllContent));
|
||||||
|
|
||||||
// ## Mail
|
// ## Mail
|
||||||
|
@ -111,7 +111,7 @@ apiRoutes = function apiRoutes(middleware) {
|
||||||
router.post('/authentication/revoke', authenticatePrivate, api.http(api.authentication.revoke));
|
router.post('/authentication/revoke', authenticatePrivate, api.http(api.authentication.revoke));
|
||||||
|
|
||||||
// ## Uploads
|
// ## Uploads
|
||||||
router.post('/uploads', authenticatePrivate, middleware.busboy, api.http(api.uploads.add));
|
router.post('/uploads', authenticatePrivate, middleware.upload.single('uploadimage'), api.http(api.uploads.add));
|
||||||
|
|
||||||
// API Router middleware
|
// API Router middleware
|
||||||
router.use(middleware.api.errorHandler);
|
router.use(middleware.api.errorHandler);
|
||||||
|
|
|
@ -70,11 +70,6 @@
|
||||||
"accessDenied": "Access denied.",
|
"accessDenied": "Access denied.",
|
||||||
"pleaseSignIn": "Please Sign In"
|
"pleaseSignIn": "Please Sign In"
|
||||||
},
|
},
|
||||||
"ghostbusboy": {
|
|
||||||
"fileUploadingError": "Something went wrong uploading the file",
|
|
||||||
"somethingWentWrong": "Something went wrong parsing the form",
|
|
||||||
"couldNotParseUpload": "Could not parse upload completely."
|
|
||||||
},
|
|
||||||
"oauth": {
|
"oauth": {
|
||||||
"invalidClient": "Invalid client.",
|
"invalidClient": "Invalid client.",
|
||||||
"invalidRefreshToken": "Invalid refresh token.",
|
"invalidRefreshToken": "Invalid refresh token.",
|
||||||
|
|
|
@ -93,11 +93,11 @@ describe('DB API', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('import content is denied (editor, author & without authentication)', function (done) {
|
it('import content is denied (editor, author & without authentication)', function (done) {
|
||||||
var file = {importfile: {
|
var file = {
|
||||||
name: 'myFile.json',
|
originalname: 'myFile.json',
|
||||||
path: '/my/path/myFile.json',
|
path: '/my/path/myFile.json',
|
||||||
type: 'application/json'
|
mimetype: 'application/json'
|
||||||
}};
|
};
|
||||||
|
|
||||||
return dbAPI.importContent(_.extend(testUtils.context.editor, file)).then(function () {
|
return dbAPI.importContent(_.extend(testUtils.context.editor, file)).then(function () {
|
||||||
done(new Error('Import content is not denied for editor.'));
|
done(new Error('Import content is not denied for editor.'));
|
||||||
|
@ -124,7 +124,7 @@ describe('DB API', function () {
|
||||||
error.errorType.should.eql('ValidationError');
|
error.errorType.should.eql('ValidationError');
|
||||||
|
|
||||||
var context = _.extend(testUtils.context.admin, {
|
var context = _.extend(testUtils.context.admin, {
|
||||||
importfile: {name: 'myFile.docx', path: '/my/path/myFile.docx', type: 'application/docx'}
|
originalname: 'myFile.docx', path: '/my/path/myFile.docx', mimetype: 'application/docx'
|
||||||
});
|
});
|
||||||
|
|
||||||
return dbAPI.importContent(context);
|
return dbAPI.importContent(context);
|
||||||
|
|
|
@ -8,8 +8,8 @@ var tmp = require('tmp'),
|
||||||
// Stuff we are testing
|
// Stuff we are testing
|
||||||
UploadAPI = require('../../../server/api/upload'),
|
UploadAPI = require('../../../server/api/upload'),
|
||||||
uploadimage = {
|
uploadimage = {
|
||||||
name: '',
|
originalname: '',
|
||||||
type: '',
|
mimetype: '',
|
||||||
path: ''
|
path: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ function setupAndTestUpload(filename, mimeType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadimage.path = path;
|
uploadimage.path = path;
|
||||||
uploadimage.name = filename;
|
uploadimage.originalname = filename;
|
||||||
uploadimage.type = mimeType;
|
uploadimage.mimetype = mimeType;
|
||||||
|
|
||||||
// create a temp directory (the directory that the API saves the file to)
|
// create a temp directory (the directory that the API saves the file to)
|
||||||
tmp.dir({keep: true, unsafeCleanup: true}, function (err, path, cleanupDir) {
|
tmp.dir({keep: true, unsafeCleanup: true}, function (err, path, cleanupDir) {
|
||||||
|
@ -37,7 +37,7 @@ function setupAndTestUpload(filename, mimeType) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
UploadAPI.add({uploadimage: uploadimage})
|
UploadAPI.add(uploadimage)
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
.catch(reject)
|
.catch(reject)
|
||||||
.finally(function () {
|
.finally(function () {
|
||||||
|
@ -65,8 +65,8 @@ function testResult(filename, result) {
|
||||||
describe('Upload API', function () {
|
describe('Upload API', function () {
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
uploadimage = {
|
uploadimage = {
|
||||||
name: '',
|
originalname: '',
|
||||||
type: 'application/octet-stream',
|
mimetype: 'application/octet-stream',
|
||||||
path: ''
|
path: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -374,31 +374,31 @@ describe('API Utils', function () {
|
||||||
|
|
||||||
describe('checkFileExists', function () {
|
describe('checkFileExists', function () {
|
||||||
it('should return true if file exists in input', function () {
|
it('should return true if file exists in input', function () {
|
||||||
apiUtils.checkFileExists({test: {type: 'file', path: 'path'}}, 'test').should.be.true();
|
apiUtils.checkFileExists({mimetype: 'file', path: 'path'}).should.be.true();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if file does not exist in input', function () {
|
it('should return false if file does not exist in input', function () {
|
||||||
apiUtils.checkFileExists({test: {type: 'file', path: 'path'}}, 'notthere').should.be.false();
|
apiUtils.checkFileExists({}).should.be.false();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if file is incorrectly structured', function () {
|
it('should return false if file is incorrectly structured', function () {
|
||||||
apiUtils.checkFileExists({test: 'notafile'}, 'test').should.be.false();
|
apiUtils.checkFileExists({type: 'file'}).should.be.false();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkFileIsValid', function () {
|
describe('checkFileIsValid', function () {
|
||||||
it('returns true if file has valid extension and type', function () {
|
it('returns true if file has valid extension and type', function () {
|
||||||
apiUtils.checkFileIsValid({name: 'test.txt', type: 'text'}, ['text'], ['.txt']).should.be.true();
|
apiUtils.checkFileIsValid({name: 'test.txt', mimetype: 'text'}, ['text'], ['.txt']).should.be.true();
|
||||||
apiUtils.checkFileIsValid({name: 'test.jpg', type: 'jpeg'}, ['text', 'jpeg'], ['.txt', '.jpg']).should.be.true();
|
apiUtils.checkFileIsValid({name: 'test.jpg', mimetype: 'jpeg'}, ['text', 'jpeg'], ['.txt', '.jpg']).should.be.true();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false if file has invalid extension', function () {
|
it('returns false if file has invalid extension', function () {
|
||||||
apiUtils.checkFileIsValid({name: 'test.txt', type: 'text'}, ['text'], ['.tar']).should.be.false();
|
apiUtils.checkFileIsValid({name: 'test.txt', mimetype: 'text'}, ['text'], ['.tar']).should.be.false();
|
||||||
apiUtils.checkFileIsValid({name: 'test', type: 'text'}, ['text'], ['.txt']).should.be.false();
|
apiUtils.checkFileIsValid({name: 'test', mimetype: 'text'}, ['text'], ['.txt']).should.be.false();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false if file has invalid type', function () {
|
it('returns false if file has invalid type', function () {
|
||||||
apiUtils.checkFileIsValid({name: 'test.txt', type: 'text'}, ['archive'], ['.txt']).should.be.false();
|
apiUtils.checkFileIsValid({name: 'test.txt', mimetype: 'text'}, ['archive'], ['.txt']).should.be.false();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
"bluebird": "3.3.3",
|
"bluebird": "3.3.3",
|
||||||
"body-parser": "1.14.2",
|
"body-parser": "1.14.2",
|
||||||
"bookshelf": "0.9.2",
|
"bookshelf": "0.9.2",
|
||||||
"busboy": "0.2.12",
|
|
||||||
"chalk": "1.1.1",
|
"chalk": "1.1.1",
|
||||||
"cheerio": "0.20.0",
|
"cheerio": "0.20.0",
|
||||||
"compression": "1.6.1",
|
"compression": "1.6.1",
|
||||||
|
@ -52,6 +51,7 @@
|
||||||
"lodash": "3.10.1",
|
"lodash": "3.10.1",
|
||||||
"moment": "2.11.2",
|
"moment": "2.11.2",
|
||||||
"morgan": "1.6.1",
|
"morgan": "1.6.1",
|
||||||
|
"multer": "1.1.0",
|
||||||
"node-uuid": "1.4.7",
|
"node-uuid": "1.4.7",
|
||||||
"nodemailer": "0.7.1",
|
"nodemailer": "0.7.1",
|
||||||
"oauth2orize": "1.2.2",
|
"oauth2orize": "1.2.2",
|
||||||
|
|
Loading…
Add table
Reference in a new issue