diff --git a/lib/commands/export.js b/lib/commands/export.js new file mode 100644 index 000000000..3585aada7 --- /dev/null +++ b/lib/commands/export.js @@ -0,0 +1,30 @@ +const Command = require('../command'); + +class ExportCommand extends Command { + async run(argv) { + const {exportTask} = require('../tasks/import'); + const {SystemError} = require('../errors'); + + const instance = this.system.getInstance(); + const isRunning = await instance.isRunning(); + + if (!isRunning) { + const shouldStart = await this.ui.confirm('Ghost instance is not currently running. Would you like to start it?', true); + + if (!shouldStart) { + throw new SystemError('Ghost instance is not currently running'); + } + + instance.checkEnvironment(); + await this.ui.run(() => instance.start(), 'Starting Ghost'); + } + + await this.ui.run(() => exportTask(this.ui, instance, argv.file), 'Exporting content'); + this.ui.log(`Content exported to ${argv.file}`, 'green'); + } +} + +ExportCommand.description = 'Export content from a blog'; +ExportCommand.params = '[file]'; + +module.exports = ExportCommand; diff --git a/lib/commands/import.js b/lib/commands/import.js index 2a8039896..de9da10f6 100644 --- a/lib/commands/import.js +++ b/lib/commands/import.js @@ -3,11 +3,11 @@ const Command = require('../command'); class ImportCommand extends Command { async run(argv) { const semver = require('semver'); - const {importTask} = require('../tasks/import'); + const {importTask, parseExport} = require('../tasks/import'); const {SystemError} = require('../errors'); const instance = this.system.getInstance(); - const {version} = importTask.parseExport(argv.file); + const {version} = parseExport(argv.file); if (semver.major(version) === 0 && semver.major(instance.version) > 1) { throw new SystemError(`v0.x export files can only be imported by Ghost v1.x versions. You are running Ghost v${instance.version}.`); diff --git a/lib/tasks/import/index.js b/lib/tasks/import/index.js index e2fd38317..e321e9cbb 100644 --- a/lib/tasks/import/index.js +++ b/lib/tasks/import/index.js @@ -1,7 +1,8 @@ -const {importTask} = require('./tasks'); +const {importTask, exportTask} = require('./tasks'); const parseExport = require('./parse-export'); module.exports = { importTask, + exportTask, parseExport }; diff --git a/lib/tasks/import/tasks.js b/lib/tasks/import/tasks.js index cd6b5cffc..d2753587b 100644 --- a/lib/tasks/import/tasks.js +++ b/lib/tasks/import/tasks.js @@ -1,27 +1,30 @@ const validator = require('validator'); +const {SystemError} = require('../../errors'); const parseExport = require('./parse-export'); -const {isSetup, setup, runImport} = require('./api'); +const {isSetup, setup, runImport, downloadExport} = require('./api'); + +const authPrompts = [{ + type: 'string', + name: 'username', + message: 'Enter your Ghost administrator email address', + validate: val => validator.isEmail(`${val}`) || 'You must specify a valid email' +}, { + 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' +}]; 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' - }]; + let prompts = authPrompts; 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' - }); + if (!blogIsSetup) { + prompts = authPrompts.slice(1); } const {username, password} = await ui.prompt(prompts); @@ -37,6 +40,19 @@ async function importTask(ui, instance, exportFile) { }], false); } +async function exportTask(ui, instance, exportFile) { + const url = instance.config.get('url'); + + const blogIsSetup = await isSetup(instance.version, url); + if (!blogIsSetup) { + throw new SystemError('Cannot export content from a blog that hasn\'t been set up.'); + } + + const authData = await ui.prompt(authPrompts); + await downloadExport(instance.version, url, authData, exportFile); +} + module.exports = { - importTask + importTask, + exportTask }; diff --git a/test/unit/commands/import-spec.js b/test/unit/commands/import-spec.js new file mode 100644 index 000000000..e96a6e385 --- /dev/null +++ b/test/unit/commands/import-spec.js @@ -0,0 +1,10 @@ +const {expect} = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); + +const {SystemError} = require('../../../lib/errors'); + +describe('Unit: Commands > import', function () { + it('throws error if importing a 0.x import into a > 1.x blog', async function () { + }); +}); diff --git a/test/unit/tasks/import/tasks-spec.js b/test/unit/tasks/import/tasks-spec.js index 6e40e6969..aaeabbc32 100644 --- a/test/unit/tasks/import/tasks-spec.js +++ b/test/unit/tasks/import/tasks-spec.js @@ -4,92 +4,140 @@ const proxyquire = require('proxyquire').noCallThru(); const Promise = require('bluebird'); const createConfigStub = require('../../../utils/config-stub'); +const {SystemError} = require('../../../../lib/errors'); + const modulePath = '../../../../lib/tasks/import/tasks'; -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(); +describe('Unit: Tasks > Import > Tasks', function () { + describe('importTask', 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 {importTask} = proxyquire(modulePath, { + './parse-export': parseExport, + './api': {isSetup, setup, runImport} + }); + + const prompt = sinon.stub().resolves({username: 'setup@example.com', password: '1234567890'}); + const listr = sinon.stub().callsFake(tasks => Promise.each(tasks, async (t) => { + if (t.enabled && !t.enabled()) { + return; + } + + await t.task(); + })); + const config = createConfigStub(); + config.get.withArgs('url').returns('http://localhost:2368'); + + 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; + expect(prompt.calledOnce).to.be.true; + expect(prompt.args[0][0]).to.have.length(2); + + const usernamePrompt = prompt.args[0][0][0]; + const passwordPrompt = prompt.args[0][0][1]; + + expect(usernamePrompt.validate('test@example.com')).to.be.true; + expect(usernamePrompt.validate('not an email')).to.include('valid email'); + expect(passwordPrompt.validate('1234567890')).to.be.true; + expect(passwordPrompt.validate('short')).to.include('10 characters long'); + + expect(listr.calledOnce).to.be.true; + expect(setup.called).to.be.false; + expect(runImport.calledOnceWithExactly('1.0.0', 'http://localhost:2368', { + username: 'setup@example.com', + password: '1234567890' + }, 'test-export.json')).to.be.true; + }); - const {importTask} = proxyquire(modulePath, { - './parse-export': parseExport, - './api': {isSetup, setup, runImport} + it('works with not setup blog', async function () { + const parseExport = sinon.stub().returns({data: {name: 'test', email: 'test@example.com', blogTitle: 'test'}}); + const isSetup = sinon.stub().resolves(false); + const setup = sinon.stub().resolves(); + const runImport = sinon.stub().resolves(); + + const {importTask} = proxyquire(modulePath, { + './parse-export': parseExport, + './api': {isSetup, setup, runImport} + }); + + const prompt = sinon.stub().resolves({password: '1234567890'}); + const listr = sinon.stub().callsFake(tasks => Promise.each(tasks, async (t) => { + if (t.enabled && !t.enabled()) { + return; + } + + await t.task(); + })); + const config = createConfigStub(); + config.get.withArgs('url').returns('http://localhost:2368'); + + 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; + expect(prompt.calledOnce).to.be.true; + expect(prompt.args[0][0]).to.have.length(1); + expect(listr.calledOnce).to.be.true; + expect(setup.calledOnceWithExactly('1.0.0', 'http://localhost:2368', { + name: 'test', + email: 'test@example.com', + blogTitle: 'test', + password: '1234567890' + })).to.be.true; + expect(runImport.calledOnceWithExactly('1.0.0', 'http://localhost:2368', { + username: 'test@example.com', + password: '1234567890' + }, 'test-export.json')).to.be.true; }); + }); - const prompt = sinon.stub().resolves({username: 'setup@example.com', password: '1234567890'}); - const listr = sinon.stub().callsFake(tasks => Promise.each(tasks, async (t) => { - if (t.enabled && !t.enabled()) { - return; - } + describe('exportTask', function () { + it('throws error for not set up blog', async function () { + const isSetup = sinon.stub().resolves(false); + const config = createConfigStub(); - await t.task(); - })); - const config = createConfigStub(); - config.get.withArgs('url').returns('http://localhost:2368'); + config.get.withArgs('url').returns('http://localhost:2368'); - await importTask({prompt, listr}, {config, version: '1.0.0'}, 'test-export.json'); + const {exportTask} = proxyquire(modulePath, { + './api': {isSetup} + }); - expect(parseExport.calledOnceWithExactly('test-export.json')).to.be.true; - expect(isSetup.calledOnceWithExactly('1.0.0', 'http://localhost:2368')).to.be.true; - expect(prompt.calledOnce).to.be.true; - expect(prompt.args[0][0]).to.have.length(2); + try { + await exportTask({}, {config, version: '1.0.0'}, 'test-export.json'); + } catch (error) { + expect(error).to.be.an.instanceof(SystemError); + expect(error.message).to.include('Cannot export content'); + expect(isSetup.calledOnceWithExactly('1.0.0', 'http://localhost:2368')).to.be.true; + return; + } - const usernamePrompt = prompt.args[0][0][0]; - const passwordPrompt = prompt.args[0][0][1]; + expect.fail('exportTask should have errored'); + }); - expect(usernamePrompt.validate('test@example.com')).to.be.true; - expect(usernamePrompt.validate('not an email')).to.include('valid email'); - expect(passwordPrompt.validate('1234567890')).to.be.true; - expect(passwordPrompt.validate('short')).to.include('10 characters long'); + it('exports content', async function () { + const isSetup = sinon.stub().resolves(true); + const downloadExport = sinon.stub().resolves(); + const config = createConfigStub(); + const prompt = sinon.stub().resolves({username: 'username', password: 'password'}); - expect(listr.calledOnce).to.be.true; - expect(setup.called).to.be.false; - expect(runImport.calledOnceWithExactly('1.0.0', 'http://localhost:2368', { - username: 'setup@example.com', - password: '1234567890' - }, 'test-export.json')).to.be.true; - }); + config.get.withArgs('url').returns('http://localhost:2368'); - it('works with not setup blog', async function () { - const parseExport = sinon.stub().returns({data: {name: 'test', email: 'test@example.com', blogTitle: 'test'}}); - const isSetup = sinon.stub().resolves(false); - const setup = sinon.stub().resolves(); - const runImport = sinon.stub().resolves(); + const {exportTask} = proxyquire(modulePath, { + './api': {isSetup, downloadExport} + }); - const {importTask} = proxyquire(modulePath, { - './parse-export': parseExport, - './api': {isSetup, setup, runImport} + await exportTask({prompt}, {config, version: '1.0.0'}, 'test-export.json'); + expect(isSetup.calledOnceWithExactly('1.0.0', 'http://localhost:2368')).to.be.true; + expect(prompt.calledOnce).to.be.true; + expect(downloadExport.calledOnceWithExactly('1.0.0', 'http://localhost:2368', { + username: 'username', password: 'password' + }, 'test-export.json')); }); - - const prompt = sinon.stub().resolves({password: '1234567890'}); - const listr = sinon.stub().callsFake(tasks => Promise.each(tasks, async (t) => { - if (t.enabled && !t.enabled()) { - return; - } - - await t.task(); - })); - const config = createConfigStub(); - config.get.withArgs('url').returns('http://localhost:2368'); - - 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; - expect(prompt.calledOnce).to.be.true; - expect(prompt.args[0][0]).to.have.length(1); - expect(listr.calledOnce).to.be.true; - expect(setup.calledOnceWithExactly('1.0.0', 'http://localhost:2368', { - name: 'test', - email: 'test@example.com', - blogTitle: 'test', - password: '1234567890' - })).to.be.true; - expect(runImport.calledOnceWithExactly('1.0.0', 'http://localhost:2368', { - username: 'test@example.com', - password: '1234567890' - }, 'test-export.json')).to.be.true; }); });