diff --git a/lib/commands/import.js b/lib/commands/import.js index be28fd4e3..2a8039896 100644 --- a/lib/commands/import.js +++ b/lib/commands/import.js @@ -3,7 +3,7 @@ const Command = require('../command'); class ImportCommand extends Command { async run(argv) { const semver = require('semver'); - const importTask = require('../tasks/import'); + const {importTask} = require('../tasks/import'); const {SystemError} = require('../errors'); const instance = this.system.getInstance(); diff --git a/lib/commands/setup.js b/lib/commands/setup.js index 6ea872bca..e9525f16c 100644 --- a/lib/commands/setup.js +++ b/lib/commands/setup.js @@ -51,7 +51,7 @@ class SetupCommand extends Command { const linux = require('../tasks/linux'); const migrator = require('../tasks/migrator'); const configure = require('../tasks/configure'); - const importTask = require('../tasks/import'); + const {importTask} = require('../tasks/import'); // This is used so we can denote built-in setup steps // and disable the "do you wish to set up x?" prompt diff --git a/lib/tasks/import/api.js b/lib/tasks/import/api.js index d645b9d41..4632ae11d 100644 --- a/lib/tasks/import/api.js +++ b/lib/tasks/import/api.js @@ -1,10 +1,14 @@ const fs = require('fs-extra'); const got = require('got'); const get = require('lodash/get'); +const util = require('util'); +const stream = require('stream'); const semver = require('semver'); const FormData = require('form-data'); const {Cookie} = require('tough-cookie'); +const finished = util.promisify(stream.finished); + const {SystemError} = require('../../errors'); const bases = { @@ -96,9 +100,19 @@ async function runImport(version, url, auth, exportFile) { await got.post('/db/', {...authOpts, body}); } +async function downloadExport(version, url, auth, outputFile) { + const authOpts = await getAuthOpts(version, url, auth); + + const ws = fs.createWriteStream(outputFile); + const resp = got.stream('/db/', {...authOpts}).pipe(ws); + + await finished(resp); +} + module.exports = { getBaseUrl, isSetup, setup, - runImport + runImport, + downloadExport }; diff --git a/lib/tasks/import/index.js b/lib/tasks/import/index.js index ba846f011..e2fd38317 100644 --- a/lib/tasks/import/index.js +++ b/lib/tasks/import/index.js @@ -1,41 +1,7 @@ -const validator = require('validator'); - +const {importTask} = require('./tasks'); const parseExport = require('./parse-export'); -const {isSetup, setup, runImport} = require('./api'); - -async function task(ui, instance, exportFile) { - const {data} = parseExport(exportFile); - const url = instance.config.get('url'); - - const prompts = [{ - type: 'password', - name: 'password', - message: 'Enter your Ghost administrator password', - validate: val => validator.isLength(`${val}`, {min: 10}) || 'Password must be at least 10 characters long' - }]; - - const blogIsSetup = await isSetup(instance.version, url); - if (blogIsSetup) { - prompts.unshift({ - type: 'string', - name: 'username', - message: 'Enter your Ghost administrator email address', - validate: val => validator.isEmail(`${val}`) || 'You must specify a valid email' - }); - } - - const {username, password} = await ui.prompt(prompts); - const importUsername = username || data.email; - - return ui.listr([{ - title: 'Running blog setup', - task: () => setup(instance.version, url, {...data, password}), - enabled: () => !blogIsSetup - }, { - title: 'Running blog import', - task: () => runImport(instance.version, url, {username: importUsername, password}, exportFile) - }], false); -} -module.exports = task; -module.exports.parseExport = parseExport; +module.exports = { + importTask, + parseExport +}; diff --git a/lib/tasks/import/tasks.js b/lib/tasks/import/tasks.js new file mode 100644 index 000000000..cd6b5cffc --- /dev/null +++ b/lib/tasks/import/tasks.js @@ -0,0 +1,42 @@ +const validator = require('validator'); + +const parseExport = require('./parse-export'); +const {isSetup, setup, runImport} = require('./api'); + +async function importTask(ui, instance, exportFile) { + const {data} = parseExport(exportFile); + const url = instance.config.get('url'); + + const prompts = [{ + type: 'password', + name: 'password', + message: 'Enter your Ghost administrator password', + validate: val => validator.isLength(`${val}`, {min: 10}) || 'Password must be at least 10 characters long' + }]; + + const blogIsSetup = await isSetup(instance.version, url); + if (blogIsSetup) { + prompts.unshift({ + type: 'string', + name: 'username', + message: 'Enter your Ghost administrator email address', + validate: val => validator.isEmail(`${val}`) || 'You must specify a valid email' + }); + } + + const {username, password} = await ui.prompt(prompts); + const importUsername = username || data.email; + + return ui.listr([{ + title: 'Running blog setup', + task: () => setup(instance.version, url, {...data, password}), + enabled: () => !blogIsSetup + }, { + title: 'Running blog import', + task: () => runImport(instance.version, url, {username: importUsername, password}, exportFile) + }], false); +} + +module.exports = { + importTask +}; diff --git a/test/unit/commands/setup-spec.js b/test/unit/commands/setup-spec.js index d5dc73834..7c538d590 100644 --- a/test/unit/commands/setup-spec.js +++ b/test/unit/commands/setup-spec.js @@ -262,7 +262,7 @@ describe('Unit: Commands > Setup', function () { it('import', function () { const importTaskStub = sinon.stub(); - const {tasks} = getTasks({'../tasks/import': importTaskStub}); + const {tasks} = getTasks({'../tasks/import': {importTask: importTaskStub}}); const [,,,,,importTask] = tasks; expect(importTask.id).to.equal('import'); diff --git a/test/unit/tasks/import/api-spec.js b/test/unit/tasks/import/api-spec.js index 2af24719c..632f541e7 100644 --- a/test/unit/tasks/import/api-spec.js +++ b/test/unit/tasks/import/api-spec.js @@ -1,9 +1,11 @@ const {expect} = require('chai'); const nock = require('nock'); const path = require('path'); +const tmp = require('tmp'); +const fs = require('fs-extra'); const {SystemError} = require('../../../../lib/errors'); -const {getBaseUrl, isSetup, setup, runImport} = require('../../../../lib/tasks/import/api'); +const {getBaseUrl, isSetup, setup, runImport, downloadExport} = require('../../../../lib/tasks/import/api'); const testUrl = 'http://localhost:2368'; @@ -169,4 +171,153 @@ describe('Unit > Tasks > Import > setup', function () { expect(importScope.isDone()).to.be.true; }); }); + + describe('downloadExport', function () { + it('1.x', async function () { + const clientId = 'client-id'; + const clientSecret = 'client-secret'; + const configBody = { + configuration: [{ + clientId, clientSecret + }] + }; + + const tokenRequestBody = { + grant_type: 'password', + client_id: clientId, + client_secret: clientSecret, + username: 'test@example.com', + password: 'password' + }; + + const tokenResponseBody = { + access_token: 'access-token' + }; + + const configScope = nock(testUrl) + .get('/ghost/api/v0.1/configuration/') + .reply(200, configBody); + + const tokenScope = nock(testUrl) + .post('/ghost/api/v0.1/authentication/token/', tokenRequestBody) + .reply(201, tokenResponseBody); + + const exportData = { + db: [{ + meta: { + version: '1.0.0' + }, + data: { + users: [] + } + }] + }; + const exportScope = nock(testUrl, { + reqheaders: { + Authorization: 'Bearer access-token' + } + }).get('/ghost/api/v0.1/db/').reply(200, exportData); + + const tmpDir = tmp.dirSync(); + const outputFile = path.join(tmpDir.name, '1.x.json'); + + await downloadExport('1.0.0', testUrl, { + username: 'test@example.com', + password: 'password' + }, outputFile); + + expect(configScope.isDone()).to.be.true; + expect(tokenScope.isDone()).to.be.true; + expect(exportScope.isDone()).to.be.true; + expect(fs.readJsonSync(outputFile)).to.deep.equal(exportData); + }); + + it('2.x', async function () { + const sessionScope = nock(testUrl, { + reqheaders: { + Origin: testUrl + } + }).post('/ghost/api/v2/admin/session/', { + username: 'test@example.com', + password: 'password' + }).reply(201, 'Success', { + 'Set-Cookie': 'ghost-admin-api-session=test-session-data; Path=/ghost; HttpOnly; Secure; Expires=Tue, 31 Dec 2099 23:59:59 GMT;' + }); + + const exportData = { + db: [{ + meta: { + version: '2.0.0' + }, + data: { + users: [] + } + }] + }; + const exportScope = nock(testUrl, { + reqheaders: { + cookie: [ + 'ghost-admin-api-session=test-session-data' + ], + origin: testUrl + } + }).get('/ghost/api/v2/admin/db/').reply(200, exportData); + + const tmpDir = tmp.dirSync(); + const outputFile = path.join(tmpDir.name, '2.x.json'); + + await downloadExport('2.0.0', 'http://localhost:2368', { + username: 'test@example.com', + password: 'password' + }, outputFile); + + expect(sessionScope.isDone()).to.be.true; + expect(exportScope.isDone()).to.be.true; + expect(fs.readJsonSync(outputFile)).to.deep.equal(exportData); + }); + + it('3.x', async function () { + const sessionScope = nock(testUrl, { + reqheaders: { + Origin: testUrl + } + }).post('/ghost/api/v3/admin/session/', { + username: 'test@example.com', + password: 'password' + }).reply(201, 'Success', { + 'Set-Cookie': 'ghost-admin-api-session=test-session-data; Path=/ghost; HttpOnly; Secure; Expires=Tue, 31 Dec 2099 23:59:59 GMT;' + }); + + const exportData = { + db: [{ + meta: { + version: '3.0.0' + }, + data: { + users: [] + } + }] + }; + const exportScope = nock(testUrl, { + reqheaders: { + cookie: [ + 'ghost-admin-api-session=test-session-data' + ], + origin: testUrl + } + }).get('/ghost/api/v3/admin/db/').reply(200, exportData); + + const tmpDir = tmp.dirSync(); + const outputFile = path.join(tmpDir.name, '3.x.json'); + + await downloadExport('3.0.0', 'http://localhost:2368', { + username: 'test@example.com', + password: 'password' + }, outputFile); + + expect(sessionScope.isDone()).to.be.true; + expect(exportScope.isDone()).to.be.true; + expect(fs.readJsonSync(outputFile)).to.deep.equal(exportData); + }); + }); }); diff --git a/test/unit/tasks/import/index-spec.js b/test/unit/tasks/import/tasks-spec.js similarity index 90% rename from test/unit/tasks/import/index-spec.js rename to test/unit/tasks/import/tasks-spec.js index 98979cdf2..6e40e6969 100644 --- a/test/unit/tasks/import/index-spec.js +++ b/test/unit/tasks/import/tasks-spec.js @@ -4,16 +4,16 @@ const proxyquire = require('proxyquire').noCallThru(); const Promise = require('bluebird'); const createConfigStub = require('../../../utils/config-stub'); -const modulePath = '../../../../lib/tasks/import'; +const modulePath = '../../../../lib/tasks/import/tasks'; -describe('Unit: Tasks > Import', function () { +describe('Unit: Tasks > Import > Import task', function () { it('works with already set up blog', async function () { const parseExport = sinon.stub().returns({data: {name: 'test', email: 'test@example.com', blogTitle: 'test'}}); const isSetup = sinon.stub().resolves(true); const setup = sinon.stub().resolves(); const runImport = sinon.stub().resolves(); - const task = proxyquire(modulePath, { + const {importTask} = proxyquire(modulePath, { './parse-export': parseExport, './api': {isSetup, setup, runImport} }); @@ -29,7 +29,7 @@ describe('Unit: Tasks > Import', function () { const config = createConfigStub(); config.get.withArgs('url').returns('http://localhost:2368'); - await task({prompt, listr}, {config, version: '1.0.0'}, 'test-export.json'); + await importTask({prompt, listr}, {config, version: '1.0.0'}, 'test-export.json'); expect(parseExport.calledOnceWithExactly('test-export.json')).to.be.true; expect(isSetup.calledOnceWithExactly('1.0.0', 'http://localhost:2368')).to.be.true; @@ -58,7 +58,7 @@ describe('Unit: Tasks > Import', function () { const setup = sinon.stub().resolves(); const runImport = sinon.stub().resolves(); - const task = proxyquire(modulePath, { + const {importTask} = proxyquire(modulePath, { './parse-export': parseExport, './api': {isSetup, setup, runImport} }); @@ -74,7 +74,7 @@ describe('Unit: Tasks > Import', function () { const config = createConfigStub(); config.get.withArgs('url').returns('http://localhost:2368'); - await task({prompt, listr}, {config, version: '1.0.0'}, 'test-export.json'); + await importTask({prompt, listr}, {config, version: '1.0.0'}, 'test-export.json'); expect(parseExport.calledOnceWithExactly('test-export.json')).to.be.true; expect(isSetup.calledOnceWithExactly('1.0.0', 'http://localhost:2368')).to.be.true;