Switch from multipart to busboy

Fixes #1227

- Removed deprecated `multipart` references.
- Setup `busboy` to pass along file streams and do a naive parse of form
values.
- Updated logic in file storage and db import to handle file streams
instead of the temporary files created by `multipart`.
This commit is contained in:
William Dibbern 2013-12-03 20:47:39 -06:00
parent 96f246533b
commit bf7692b151
8 changed files with 196 additions and 121 deletions

1
.gitignore vendored
View File

@ -41,6 +41,7 @@ projectFilesBackup
/core/server/data/export/exported* /core/server/data/export/exported*
/docs /docs
/_site /_site
/content/tmp/*
/content/data/* /content/data/*
/content/plugins/**/* /content/plugins/**/*
/content/themes/**/* /content/themes/**/*

View File

@ -239,6 +239,10 @@ var path = require('path'),
] ]
}, },
storage: {
src: ['core/test/unit/**/storage*_spec.js']
},
integration: { integration: {
src: ['core/test/integration/**/model*_spec.js'] src: ['core/test/integration/**/model*_spec.js']
}, },

View File

@ -1,6 +1,7 @@
var dataExport = require('../data/export'), var dataExport = require('../data/export'),
dataImport = require('../data/import'), dataImport = require('../data/import'),
api = require('../api'), apiNotifications = require('./notifications'),
apiSettings = require('./settings'),
fs = require('fs-extra'), fs = require('fs-extra'),
path = require('path'), path = require('path'),
when = require('when'), when = require('when'),
@ -27,57 +28,58 @@ db = {
res.download(exportedFilePath, 'GhostData.json'); res.download(exportedFilePath, 'GhostData.json');
}).otherwise(function (error) { }).otherwise(function (error) {
// Notify of an error if it occurs // Notify of an error if it occurs
return api.notification.browse().then(function (notifications) { return apiNotifications.browse().then(function (notifications) {
var notification = { var notification = {
type: 'error', type: 'error',
message: error.message || error, message: error.message || error,
status: 'persistent', status: 'persistent',
id: 'per-' + (notifications.length + 1) id: 'per-' + (notifications.length + 1)
}; };
return api.notifications.add(notification).then(function () {
return apiNotifications.add(notification).then(function () {
res.redirect(debugPath); res.redirect(debugPath);
}); });
}); });
}); });
}, },
'import': function (req, res) { 'import': function (req, res) {
var notification; var notification,
databaseVersion;
if (!req.files.importfile || req.files.importfile.size === 0 || req.files.importfile.name.indexOf('json') === -1) { if (!req.files.importfile || !req.files.importfile.path || req.files.importfile.name.indexOf('json') === -1) {
/** /**
* Notify of an error if it occurs * Notify of an error if it occurs
* *
* - If there's no file (although if you don't select anything, the input is still submitted, so * - If there's no file (although if you don't select anything, the input is still submitted, so
* !req.files.importfile will always be false) * !req.files.importfile will always be false)
* - If the size is 0 * - If there is no path
* - If the name doesn't have json in it * - If the name doesn't have json in it
*/ */
return api.notification.browse().then(function (notifications) { return apiNotifications.browse().then(function (notifications) {
notification = { notification = {
type: 'error', type: 'error',
message: "Must select a .json file to import", message: "Must select a .json file to import",
status: 'persistent', status: 'persistent',
id: 'per-' + (notifications.length + 1) id: 'per-' + (notifications.length + 1)
}; };
return api.notifications.add(notification).then(function () {
return apiNotifications.add(notification).then(function () {
res.redirect(debugPath); res.redirect(debugPath);
}); });
}); });
} }
// Get the current version for importing apiSettings.read({ key: 'databaseVersion' }).then(function (setting) {
api.settings.read({ key: 'databaseVersion' })
.then(function (setting) {
return when(setting.value); return when(setting.value);
}, function () { }, function () {
return when('001'); return when('001');
}) }).then(function (version) {
.then(function (databaseVersion) { databaseVersion = version;
// Read the file contents // Read the file contents
return nodefn.call(fs.readFile, req.files.importfile.path) return nodefn.call(fs.readFile, req.files.importfile.path);
.then(function (fileContents) { }).then(function (fileContents) {
var importData, var importData,
error = "", error = '',
tableKeys = _.keys(schema); tableKeys = _.keys(schema);
// Parse the json data // Parse the json data
@ -124,10 +126,9 @@ db = {
} }
// Import for the current version // Import for the current version
return dataImport(databaseVersion, importData); return dataImport(databaseVersion, importData);
}); }).then(function importSuccess() {
}) return apiNotifications.browse();
.then(function importSuccess() { }).then(function (notifications) {
return api.notification.browse().then(function (notifications) {
notification = { notification = {
type: 'success', type: 'success',
message: "Data imported. Log in with the user details you imported", message: "Data imported. Log in with the user details you imported",
@ -135,16 +136,15 @@ db = {
id: 'per-' + (notifications.length + 1) id: 'per-' + (notifications.length + 1)
}; };
return api.notifications.add(notification).then(function () { return apiNotifications.add(notification).then(function () {
req.session.destroy(); req.session.destroy();
res.set({ res.set({
"X-Cache-Invalidate": "/*" "X-Cache-Invalidate": "/*"
}); });
res.redirect(config.paths().webroot + '/ghost/signin/'); res.redirect(config.paths().webroot + '/ghost/signin/');
}); });
}); }).otherwise(function importFailure(error) {
}, function importFailure(error) { return apiNotifications.browse().then(function (notifications) {
return api.notification.browse().then(function (notifications) {
// Notify of an error if it occurs // Notify of an error if it occurs
notification = { notification = {
type: 'error', type: 'error',
@ -153,7 +153,7 @@ db = {
id: 'per-' + (notifications.length + 1) id: 'per-' + (notifications.length + 1)
}; };
return api.notifications.add(notification).then(function () { return apiNotifications.add(notification).then(function () {
res.redirect(debugPath); res.redirect(debugPath);
}); });
}); });

View File

@ -0,0 +1,66 @@
var BusBoy = require('busboy'),
fs = require('fs-extra'),
path = require('path'),
os = require('os');
// ### ghostBusboy
// Process multipart file streams and copies them to a memory stream to be
// processed later.
function ghostBusBoy(req, res, next) {
var busboy,
tmpDir,
hasError = false;
if (req.method && req.method.match(/get/i)) {
return next();
}
busboy = new BusBoy({ headers: req.headers });
tmpDir = os.tmpdir();
req.files = req.files || {};
req.body = req.body || {};
busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
var filePath;
// If the filename is invalid, mark an error
if (!filename) {
hasError = true;
}
// If we've flagged any errors, do not process any streams
if (hasError) {
return file.emit('end');
}
filePath = path.join(tmpDir, filename || 'temp.tmp');
file.on('end', function () {
req.files[fieldname] = {
type: mimetype,
encoding: encoding,
name: filename,
path: filePath
};
});
busboy.on('limit', function () {
hasError = true;
res.send(413, { errorCode: 413, message: 'File size limit breached.' });
});
file.pipe(fs.createWriteStream(filePath));
});
busboy.on('field', function (fieldname, val) {
req.body[fieldname] = val;
});
busboy.on('end', function () {
next();
});
req.pipe(busboy);
}
module.exports = ghostBusBoy;

View File

@ -237,9 +237,8 @@ module.exports = function (server, dbHash) {
expressServer.use(express.json()); expressServer.use(express.json());
expressServer.use(express.urlencoded()); expressServer.use(express.urlencoded());
expressServer.use(root + '/ghost/upload/', express.multipart()); expressServer.use('/ghost/upload/', middleware.busboy);
expressServer.use(root + '/ghost/upload/', express.multipart({uploadDir: __dirname + '/content/images'})); expressServer.use('/ghost/api/v0.1/db/', middleware.busboy);
expressServer.use(root + '/ghost/api/v0.1/db/', express.multipart());
// Session handling // Session handling
expressServer.use(express.cookieParser()); expressServer.use(express.cookieParser());

View File

@ -4,6 +4,7 @@
var _ = require('underscore'), var _ = require('underscore'),
express = require('express'), express = require('express'),
busboy = require('./ghost-busboy'),
config = require('../config'), config = require('../config'),
path = require('path'), path = require('path'),
api = require('../api'), api = require('../api'),
@ -139,7 +140,9 @@ var middleware = {
return; return;
} }
next(); next();
} },
busboy: busboy
}; };
module.exports = middleware; module.exports = middleware;

View File

@ -20,24 +20,25 @@ localFileStore = _.extend(baseStore, {
// - returns a promise which ultimately returns the full url to the uploaded image // - returns a promise which ultimately returns the full url to the uploaded image
'save': function (image) { 'save': function (image) {
var saved = when.defer(), var saved = when.defer(),
targetDir = this.getTargetDir(config.paths().imagesRelPath); targetDir = this.getTargetDir(config.paths().imagesRelPath),
targetFilename;
this.getUniqueFileName(this, image, targetDir).then(function (filename) { this.getUniqueFileName(this, image, targetDir).then(function (filename) {
nodefn.call(fs.mkdirs, targetDir).then(function () { targetFilename = filename;
return nodefn.call(fs.copy, image.path, filename); return nodefn.call(fs.mkdirs, targetDir);
}).then(function () {
return nodefn.call(fs.copy, image.path, targetFilename);
}).then(function () { }).then(function () {
// we should remove the temporary image
return nodefn.call(fs.unlink, image.path).otherwise(errors.logError); return nodefn.call(fs.unlink, image.path).otherwise(errors.logError);
}).then(function () { }).then(function () {
// The src for the image must be in URI format, not a file system path, which in Windows uses \ // The src for the image must be in URI format, not a file system path, which in Windows uses \
// For local file system storage can use relative path so add a slash // For local file system storage can use relative path so add a slash
var fullUrl = ('/' + filename).replace(new RegExp('\\' + path.sep, 'g'), '/'); var fullUrl = ('/' + targetFilename).replace(new RegExp('\\' + path.sep, 'g'), '/');
return saved.resolve(fullUrl); return saved.resolve(fullUrl);
}).otherwise(function (e) { }).otherwise(function (e) {
errors.logError(e); errors.logError(e);
return saved.reject(e); return saved.reject(e);
}); });
}).otherwise(errors.logError);
return saved.promise; return saved.promise;
}, },

View File

@ -34,6 +34,7 @@
"dependencies": { "dependencies": {
"bcryptjs": "0.7.10", "bcryptjs": "0.7.10",
"bookshelf": "0.6.1", "bookshelf": "0.6.1",
"busboy": "0.0.12",
"colors": "0.6.2", "colors": "0.6.2",
"connect-slashes": "1.0.2", "connect-slashes": "1.0.2",
"downsize": "0.0.4", "downsize": "0.0.4",