2014-06-03 14:05:25 +01:00
|
|
|
// # DB API
|
|
|
|
// API for DB operations
|
2013-12-25 01:05:20 +01:00
|
|
|
var dataExport = require('../data/export'),
|
|
|
|
dataImport = require('../data/import'),
|
|
|
|
dataProvider = require('../models'),
|
|
|
|
fs = require('fs-extra'),
|
2014-08-17 06:17:23 +00:00
|
|
|
Promise = require('bluebird'),
|
2014-02-05 00:40:30 -08:00
|
|
|
_ = require('lodash'),
|
2014-07-15 12:40:14 +02:00
|
|
|
path = require('path'),
|
2014-12-10 13:28:16 +00:00
|
|
|
os = require('os'),
|
|
|
|
glob = require('glob'),
|
|
|
|
uuid = require('node-uuid'),
|
|
|
|
extract = require('extract-zip'),
|
2014-05-09 12:11:29 +02:00
|
|
|
errors = require('../../server/errors'),
|
2014-04-08 15:40:33 +02:00
|
|
|
canThis = require('../permissions').canThis,
|
2014-12-10 13:28:16 +00:00
|
|
|
utils = require('./utils'),
|
2013-12-29 23:52:55 +00:00
|
|
|
api = {},
|
2014-12-10 13:28:16 +00:00
|
|
|
db,
|
|
|
|
types = ['application/octet-stream', 'application/json', 'application/zip', 'application/x-zip-compressed'],
|
|
|
|
extensions = ['.json', '.zip'];
|
2013-10-09 10:52:58 +02:00
|
|
|
|
2013-12-29 23:52:55 +00:00
|
|
|
api.settings = require('./settings');
|
|
|
|
|
2014-12-10 13:28:16 +00:00
|
|
|
// TODO refactor this out of here
|
|
|
|
function isJSON(ext) {
|
|
|
|
return ext === '.json';
|
|
|
|
}
|
|
|
|
|
|
|
|
function isZip(ext) {
|
|
|
|
return ext === '.zip';
|
|
|
|
}
|
|
|
|
|
|
|
|
function getJSONFileContents(filepath, ext) {
|
|
|
|
if (isJSON(ext)) {
|
|
|
|
// if it's just a JSON file, read it
|
|
|
|
return Promise.promisify(fs.readFile)(filepath);
|
|
|
|
} else if (isZip(ext)) {
|
|
|
|
var tmpdir = path.join(os.tmpdir(), uuid.v4());
|
|
|
|
|
|
|
|
return Promise.promisify(extract)(filepath, {dir: tmpdir}).then(function () {
|
|
|
|
return Promise.promisify(glob)('**/*.json', {cwd: tmpdir}).then(function (files) {
|
|
|
|
if (files[0]) {
|
|
|
|
// @TODO: handle multiple JSON files
|
|
|
|
return Promise.promisify(fs.readFile)(path.join(tmpdir, files[0]));
|
|
|
|
} else {
|
|
|
|
return Promise.reject(new errors.UnsupportedMediaTypeError(
|
|
|
|
'Zip did not include any content to import.'
|
|
|
|
));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2014-07-15 12:40:14 +02:00
|
|
|
}
|
|
|
|
}
|
2014-12-10 13:28:16 +00:00
|
|
|
|
2014-06-03 14:05:25 +01:00
|
|
|
/**
|
|
|
|
* ## DB API Methods
|
|
|
|
*
|
|
|
|
* **See:** [API Methods](index.js.html#api%20methods)
|
|
|
|
*/
|
2013-10-09 10:52:58 +02:00
|
|
|
db = {
|
2014-06-03 14:05:25 +01:00
|
|
|
/**
|
|
|
|
* ### Export Content
|
|
|
|
* Generate the JSON to export
|
|
|
|
*
|
|
|
|
* @public
|
|
|
|
* @param {{context}} options
|
|
|
|
* @returns {Promise} Ghost Export JSON format
|
|
|
|
*/
|
2014-09-10 00:06:24 -04:00
|
|
|
exportContent: function (options) {
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 13:41:19 +01:00
|
|
|
options = options || {};
|
2014-04-08 15:40:33 +02:00
|
|
|
|
2014-02-27 16:48:38 +01:00
|
|
|
// Export data, otherwise send error 500
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 13:41:19 +01:00
|
|
|
return canThis(options.context).exportContent.db().then(function () {
|
2014-09-10 00:06:24 -04:00
|
|
|
return dataExport().then(function (exportedData) {
|
|
|
|
return {db: [exportedData]};
|
|
|
|
}).catch(function (error) {
|
|
|
|
return Promise.reject(new errors.InternalServerError(error.message || error));
|
2014-04-08 15:40:33 +02:00
|
|
|
});
|
2014-09-10 00:06:24 -04:00
|
|
|
}, function () {
|
2014-09-27 19:05:35 +00:00
|
|
|
return Promise.reject(new errors.NoPermissionError('You do not have permission to export data (no rights).'));
|
2014-09-10 00:06:24 -04:00
|
|
|
});
|
2013-10-09 10:52:58 +02:00
|
|
|
},
|
2014-06-03 14:05:25 +01:00
|
|
|
/**
|
|
|
|
* ### Import Content
|
|
|
|
* Import posts, tags etc from a JSON blob
|
|
|
|
*
|
|
|
|
* @public
|
|
|
|
* @param {{context}} options
|
|
|
|
* @returns {Promise} Success
|
|
|
|
*/
|
2014-09-10 00:06:24 -04:00
|
|
|
importContent: function (options) {
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 13:41:19 +01:00
|
|
|
options = options || {};
|
2014-07-15 12:40:14 +02:00
|
|
|
var databaseVersion,
|
|
|
|
ext,
|
|
|
|
filepath;
|
2013-10-09 10:52:58 +02:00
|
|
|
|
2014-12-10 13:28:16 +00:00
|
|
|
// Check if a file was provided
|
|
|
|
if (!utils.checkFileExists(options, 'importfile')) {
|
|
|
|
return Promise.reject(new errors.NoPermissionError('Please select a file to import.'));
|
|
|
|
}
|
2013-10-09 10:52:58 +02:00
|
|
|
|
2014-12-10 13:28:16 +00:00
|
|
|
// Check if the file is valid
|
|
|
|
if (!utils.checkFileIsValid(options.importfile, types, extensions)) {
|
|
|
|
return Promise.reject(new errors.UnsupportedMediaTypeError(
|
|
|
|
'Please select either a .json or .zip file to import.'
|
|
|
|
));
|
|
|
|
}
|
2014-05-07 01:28:51 -04:00
|
|
|
|
2014-12-10 13:28:16 +00:00
|
|
|
// TODO refactor this out of here
|
|
|
|
filepath = options.importfile.path;
|
|
|
|
ext = path.extname(options.importfile.name).toLowerCase();
|
|
|
|
|
|
|
|
// Permissions check
|
|
|
|
return canThis(options.context).importContent.db().then(function () {
|
|
|
|
return api.settings.read(
|
|
|
|
{key: 'databaseVersion', context: {internal: true}}
|
|
|
|
).then(function (response) {
|
|
|
|
var setting = response.settings[0];
|
2014-07-15 12:40:14 +02:00
|
|
|
|
2014-12-10 13:28:16 +00:00
|
|
|
return setting.value;
|
2014-04-08 15:40:33 +02:00
|
|
|
}).then(function (version) {
|
|
|
|
databaseVersion = version;
|
|
|
|
// Read the file contents
|
2014-12-10 13:28:16 +00:00
|
|
|
return getJSONFileContents(filepath, ext);
|
2014-04-08 15:40:33 +02:00
|
|
|
}).then(function (fileContents) {
|
2014-07-28 22:41:45 +01:00
|
|
|
var importData;
|
2013-10-09 10:52:58 +02:00
|
|
|
|
2014-04-08 15:40:33 +02:00
|
|
|
// Parse the json data
|
|
|
|
try {
|
|
|
|
importData = JSON.parse(fileContents);
|
2014-05-08 20:03:05 -04:00
|
|
|
|
|
|
|
// if importData follows JSON-API format `{ db: [exportedData] }`
|
|
|
|
if (_.keys(importData).length === 1 && Array.isArray(importData.db)) {
|
|
|
|
importData = importData.db[0];
|
|
|
|
}
|
2014-04-08 15:40:33 +02:00
|
|
|
} catch (e) {
|
2014-07-28 22:41:45 +01:00
|
|
|
errors.logError(e, 'API DB import content', 'check that the import file is valid JSON.');
|
2014-09-27 19:05:35 +00:00
|
|
|
return Promise.reject(new errors.BadRequestError('Failed to parse the import JSON file.'));
|
2014-04-08 15:40:33 +02:00
|
|
|
}
|
2013-10-09 10:52:58 +02:00
|
|
|
|
2014-04-08 15:40:33 +02:00
|
|
|
if (!importData.meta || !importData.meta.version) {
|
2014-08-17 06:17:23 +00:00
|
|
|
return Promise.reject(
|
2014-07-28 22:41:45 +01:00
|
|
|
new errors.ValidationError('Import data does not specify version', 'meta.version')
|
|
|
|
);
|
2014-04-08 15:40:33 +02:00
|
|
|
}
|
2013-10-09 10:52:58 +02:00
|
|
|
|
2014-04-08 15:40:33 +02:00
|
|
|
// Import for the current version
|
|
|
|
return dataImport(databaseVersion, importData);
|
2014-08-17 06:17:23 +00:00
|
|
|
}).then(api.settings.updateSettingsCache)
|
2014-09-10 00:06:24 -04:00
|
|
|
.return({db: []})
|
2014-08-17 06:17:23 +00:00
|
|
|
.finally(function () {
|
2014-04-08 15:40:33 +02:00
|
|
|
// Unlink the file after import
|
2014-08-17 06:17:23 +00:00
|
|
|
return Promise.promisify(fs.unlink)(filepath);
|
2014-04-08 15:40:33 +02:00
|
|
|
});
|
|
|
|
}, function () {
|
2014-09-27 19:05:35 +00:00
|
|
|
return Promise.reject(new errors.NoPermissionError('You do not have permission to import data (no rights).'));
|
2013-12-03 20:47:39 -06:00
|
|
|
});
|
2013-12-25 01:05:20 +01:00
|
|
|
},
|
2014-06-03 14:05:25 +01:00
|
|
|
/**
|
|
|
|
* ### Delete All Content
|
|
|
|
* Remove all posts and tags
|
|
|
|
*
|
|
|
|
* @public
|
|
|
|
* @param {{context}} options
|
|
|
|
* @returns {Promise} Success
|
|
|
|
*/
|
2014-09-10 00:06:24 -04:00
|
|
|
deleteAllContent: function (options) {
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 13:41:19 +01:00
|
|
|
options = options || {};
|
2014-04-08 15:40:33 +02:00
|
|
|
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 13:41:19 +01:00
|
|
|
return canThis(options.context).deleteAllContent.db().then(function () {
|
2014-08-17 06:17:23 +00:00
|
|
|
return Promise.resolve(dataProvider.deleteAllContent())
|
2014-09-10 00:06:24 -04:00
|
|
|
.return({db: []})
|
2014-08-17 06:17:23 +00:00
|
|
|
.catch(function (error) {
|
|
|
|
return Promise.reject(new errors.InternalServerError(error.message || error));
|
2014-04-08 15:40:33 +02:00
|
|
|
});
|
|
|
|
}, function () {
|
2014-09-27 19:05:35 +00:00
|
|
|
return Promise.reject(new errors.NoPermissionError('You do not have permission to export data (no rights).'));
|
2014-04-08 15:40:33 +02:00
|
|
|
});
|
2013-11-01 12:12:01 +00:00
|
|
|
}
|
2013-10-09 10:52:58 +02:00
|
|
|
};
|
|
|
|
|
2014-01-28 23:18:00 -05:00
|
|
|
module.exports = db;
|