mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-23 11:55:01 +03:00
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:
parent
96f246533b
commit
bf7692b151
1
.gitignore
vendored
1
.gitignore
vendored
@ -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/**/*
|
||||||
|
@ -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']
|
||||||
},
|
},
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
var dataExport = require('../data/export'),
|
var dataExport = require('../data/export'),
|
||||||
dataImport = require('../data/import'),
|
dataImport = require('../data/import'),
|
||||||
api = require('../api'),
|
apiNotifications = require('./notifications'),
|
||||||
fs = require('fs-extra'),
|
apiSettings = require('./settings'),
|
||||||
path = require('path'),
|
fs = require('fs-extra'),
|
||||||
when = require('when'),
|
path = require('path'),
|
||||||
nodefn = require('when/node/function'),
|
when = require('when'),
|
||||||
_ = require('underscore'),
|
nodefn = require('when/node/function'),
|
||||||
schema = require('../data/schema'),
|
_ = require('underscore'),
|
||||||
config = require('../config'),
|
schema = require('../data/schema'),
|
||||||
debugPath = config.paths().webroot + '/ghost/debug/',
|
config = require('../config'),
|
||||||
|
debugPath = config.paths().webroot + '/ghost/debug/',
|
||||||
|
|
||||||
db;
|
db;
|
||||||
|
|
||||||
@ -27,137 +28,136 @@ 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' })
|
return when(setting.value);
|
||||||
.then(function (setting) {
|
}, function () {
|
||||||
return when(setting.value);
|
return when('001');
|
||||||
}, function () {
|
}).then(function (version) {
|
||||||
return when('001');
|
databaseVersion = version;
|
||||||
})
|
// Read the file contents
|
||||||
.then(function (databaseVersion) {
|
return nodefn.call(fs.readFile, req.files.importfile.path);
|
||||||
// Read the file contents
|
}).then(function (fileContents) {
|
||||||
return nodefn.call(fs.readFile, req.files.importfile.path)
|
var importData,
|
||||||
.then(function (fileContents) {
|
error = '',
|
||||||
var importData,
|
tableKeys = _.keys(schema);
|
||||||
error = "",
|
|
||||||
tableKeys = _.keys(schema);
|
|
||||||
|
|
||||||
// Parse the json data
|
// Parse the json data
|
||||||
try {
|
try {
|
||||||
importData = JSON.parse(fileContents);
|
importData = JSON.parse(fileContents);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return when.reject(new Error("Failed to parse the import file"));
|
return when.reject(new Error("Failed to parse the import file"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!importData.meta || !importData.meta.version) {
|
if (!importData.meta || !importData.meta.version) {
|
||||||
return when.reject(new Error("Import data does not specify version"));
|
return when.reject(new Error("Import data does not specify version"));
|
||||||
}
|
}
|
||||||
|
|
||||||
_.each(tableKeys, function (constkey) {
|
_.each(tableKeys, function (constkey) {
|
||||||
_.each(importData.data[constkey], function (elem) {
|
_.each(importData.data[constkey], function (elem) {
|
||||||
var prop;
|
var prop;
|
||||||
for (prop in elem) {
|
for (prop in elem) {
|
||||||
if (elem.hasOwnProperty(prop)) {
|
if (elem.hasOwnProperty(prop)) {
|
||||||
if (schema[constkey].hasOwnProperty(prop)) {
|
if (schema[constkey].hasOwnProperty(prop)) {
|
||||||
if (elem.hasOwnProperty(prop)) {
|
if (elem.hasOwnProperty(prop)) {
|
||||||
if (!_.isNull(elem[prop])) {
|
if (!_.isNull(elem[prop])) {
|
||||||
if (elem[prop].length > schema[constkey][prop].maxlength) {
|
if (elem[prop].length > schema[constkey][prop].maxlength) {
|
||||||
error += error !== "" ? "<br>" : "";
|
|
||||||
error += "Property '" + prop + "' exceeds maximum length of " + schema[constkey][prop].maxlength + " (element:" + constkey + " / id:" + elem.id + ")";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!schema[constkey][prop].nullable) {
|
|
||||||
error += error !== "" ? "<br>" : "";
|
|
||||||
error += "Property '" + prop + "' is not nullable (element:" + constkey + " / id:" + elem.id + ")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error += error !== "" ? "<br>" : "";
|
error += error !== "" ? "<br>" : "";
|
||||||
error += "Property '" + prop + "' is not allowed (element:" + constkey + " / id:" + elem.id + ")";
|
error += "Property '" + prop + "' exceeds maximum length of " + schema[constkey][prop].maxlength + " (element:" + constkey + " / id:" + elem.id + ")";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!schema[constkey][prop].nullable) {
|
||||||
|
error += error !== "" ? "<br>" : "";
|
||||||
|
error += "Property '" + prop + "' is not nullable (element:" + constkey + " / id:" + elem.id + ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
});
|
error += error !== "" ? "<br>" : "";
|
||||||
|
error += "Property '" + prop + "' is not allowed (element:" + constkey + " / id:" + elem.id + ")";
|
||||||
if (error !== "") {
|
}
|
||||||
return when.reject(new Error(error));
|
|
||||||
}
|
}
|
||||||
// Import for the current version
|
}
|
||||||
return dataImport(databaseVersion, importData);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(function importSuccess() {
|
|
||||||
return api.notification.browse().then(function (notifications) {
|
|
||||||
notification = {
|
|
||||||
type: 'success',
|
|
||||||
message: "Data imported. Log in with the user details you imported",
|
|
||||||
status: 'persistent',
|
|
||||||
id: 'per-' + (notifications.length + 1)
|
|
||||||
};
|
|
||||||
|
|
||||||
return api.notifications.add(notification).then(function () {
|
|
||||||
req.session.destroy();
|
|
||||||
res.set({
|
|
||||||
"X-Cache-Invalidate": "/*"
|
|
||||||
});
|
|
||||||
res.redirect(config.paths().webroot + '/ghost/signin/');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, function importFailure(error) {
|
|
||||||
return api.notification.browse().then(function (notifications) {
|
|
||||||
// Notify of an error if it occurs
|
|
||||||
notification = {
|
|
||||||
type: 'error',
|
|
||||||
message: error.message || error,
|
|
||||||
status: 'persistent',
|
|
||||||
id: 'per-' + (notifications.length + 1)
|
|
||||||
};
|
|
||||||
|
|
||||||
return api.notifications.add(notification).then(function () {
|
|
||||||
res.redirect(debugPath);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (error !== "") {
|
||||||
|
return when.reject(new Error(error));
|
||||||
|
}
|
||||||
|
// Import for the current version
|
||||||
|
return dataImport(databaseVersion, importData);
|
||||||
|
}).then(function importSuccess() {
|
||||||
|
return apiNotifications.browse();
|
||||||
|
}).then(function (notifications) {
|
||||||
|
notification = {
|
||||||
|
type: 'success',
|
||||||
|
message: "Data imported. Log in with the user details you imported",
|
||||||
|
status: 'persistent',
|
||||||
|
id: 'per-' + (notifications.length + 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
return apiNotifications.add(notification).then(function () {
|
||||||
|
req.session.destroy();
|
||||||
|
res.set({
|
||||||
|
"X-Cache-Invalidate": "/*"
|
||||||
|
});
|
||||||
|
res.redirect(config.paths().webroot + '/ghost/signin/');
|
||||||
|
});
|
||||||
|
}).otherwise(function importFailure(error) {
|
||||||
|
return apiNotifications.browse().then(function (notifications) {
|
||||||
|
// Notify of an error if it occurs
|
||||||
|
notification = {
|
||||||
|
type: 'error',
|
||||||
|
message: error.message || error,
|
||||||
|
status: 'persistent',
|
||||||
|
id: 'per-' + (notifications.length + 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
return apiNotifications.add(notification).then(function () {
|
||||||
|
res.redirect(debugPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
66
core/server/middleware/ghost-busboy.js
Normal file
66
core/server/middleware/ghost-busboy.js
Normal 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;
|
@ -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());
|
||||||
|
@ -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;
|
||||||
|
@ -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 () {
|
}).then(function () {
|
||||||
// we should remove the temporary image
|
return nodefn.call(fs.copy, image.path, targetFilename);
|
||||||
return nodefn.call(fs.unlink, image.path).otherwise(errors.logError);
|
}).then(function () {
|
||||||
}).then(function () {
|
return nodefn.call(fs.unlink, image.path).otherwise(errors.logError);
|
||||||
// The src for the image must be in URI format, not a file system path, which in Windows uses \
|
}).then(function () {
|
||||||
// For local file system storage can use relative path so add a slash
|
// The src for the image must be in URI format, not a file system path, which in Windows uses \
|
||||||
var fullUrl = ('/' + filename).replace(new RegExp('\\' + path.sep, 'g'), '/');
|
// For local file system storage can use relative path so add a slash
|
||||||
return saved.resolve(fullUrl);
|
var fullUrl = ('/' + targetFilename).replace(new RegExp('\\' + path.sep, 'g'), '/');
|
||||||
}).otherwise(function (e) {
|
return saved.resolve(fullUrl);
|
||||||
errors.logError(e);
|
}).otherwise(function (e) {
|
||||||
return saved.reject(e);
|
errors.logError(e);
|
||||||
});
|
return saved.reject(e);
|
||||||
}).otherwise(errors.logError);
|
});
|
||||||
|
|
||||||
return saved.promise;
|
return saved.promise;
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user