Skip to content

Commit e404dea

Browse files
committed
feat(export): add export command
refs TryGhost#468 - add export command & export taks
1 parent b2629e5 commit e404dea

File tree

6 files changed

+407
-89
lines changed

6 files changed

+407
-89
lines changed

lib/commands/export.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const Command = require('../command');
2+
3+
class ExportCommand extends Command {
4+
async run(argv) {
5+
const {exportTask} = require('../tasks/import');
6+
const {SystemError} = require('../errors');
7+
8+
const instance = this.system.getInstance();
9+
const isRunning = await instance.isRunning();
10+
11+
if (!isRunning) {
12+
const shouldStart = await this.ui.confirm('Ghost instance is not currently running. Would you like to start it?', true);
13+
14+
if (!shouldStart) {
15+
throw new SystemError('Ghost instance is not currently running');
16+
}
17+
18+
instance.checkEnvironment();
19+
await this.ui.run(() => instance.start(), 'Starting Ghost');
20+
}
21+
22+
await this.ui.run(() => exportTask(this.ui, instance, argv.file), 'Exporting content');
23+
this.ui.log(`Content exported to ${argv.file}`, 'green');
24+
}
25+
}
26+
27+
ExportCommand.description = 'Export content from a blog';
28+
ExportCommand.params = '[file]';
29+
30+
module.exports = ExportCommand;

lib/tasks/import/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
const {importTask} = require('./tasks');
1+
const {importTask, exportTask} = require('./tasks');
22
const parseExport = require('./parse-export');
33

44
module.exports = {
55
importTask,
6+
exportTask,
67
parseExport
78
};

lib/tasks/import/tasks.js

+31-15
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
const validator = require('validator');
22

3+
const {SystemError} = require('../../errors');
34
const parseExport = require('./parse-export');
4-
const {isSetup, setup, runImport} = require('./api');
5+
const {isSetup, setup, runImport, downloadExport} = require('./api');
6+
7+
const authPrompts = [{
8+
type: 'string',
9+
name: 'username',
10+
message: 'Enter your Ghost administrator email address',
11+
validate: val => validator.isEmail(`${val}`) || 'You must specify a valid email'
12+
}, {
13+
type: 'password',
14+
name: 'password',
15+
message: 'Enter your Ghost administrator password',
16+
validate: val => validator.isLength(`${val}`, {min: 10}) || 'Password must be at least 10 characters long'
17+
}];
518

619
async function importTask(ui, instance, exportFile) {
720
const {data} = parseExport(exportFile);
821
const url = instance.config.get('url');
922

10-
const prompts = [{
11-
type: 'password',
12-
name: 'password',
13-
message: 'Enter your Ghost administrator password',
14-
validate: val => validator.isLength(`${val}`, {min: 10}) || 'Password must be at least 10 characters long'
15-
}];
23+
let prompts = authPrompts;
1624

1725
const blogIsSetup = await isSetup(instance.version, url);
18-
if (blogIsSetup) {
19-
prompts.unshift({
20-
type: 'string',
21-
name: 'username',
22-
message: 'Enter your Ghost administrator email address',
23-
validate: val => validator.isEmail(`${val}`) || 'You must specify a valid email'
24-
});
26+
if (!blogIsSetup) {
27+
prompts = authPrompts.slice(1);
2528
}
2629

2730
const {username, password} = await ui.prompt(prompts);
@@ -37,6 +40,19 @@ async function importTask(ui, instance, exportFile) {
3740
}], false);
3841
}
3942

43+
async function exportTask(ui, instance, exportFile) {
44+
const url = instance.config.get('url');
45+
46+
const blogIsSetup = await isSetup(instance.version, url);
47+
if (!blogIsSetup) {
48+
throw new SystemError('Cannot export content from a blog that hasn\'t been set up.');
49+
}
50+
51+
const authData = await ui.prompt(authPrompts);
52+
await downloadExport(instance.version, url, authData, exportFile);
53+
}
54+
4055
module.exports = {
41-
importTask
56+
importTask,
57+
exportTask
4258
};

test/unit/commands/export-spec.js

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const {expect} = require('chai');
2+
const sinon = require('sinon');
3+
const proxyquire = require('proxyquire').noCallThru();
4+
5+
const {SystemError} = require('../../../lib/errors');
6+
7+
const modulePath = '../../../lib/commands/export';
8+
9+
describe('Unit: Commands > export', function () {
10+
it('runs export task if instance is running', async function () {
11+
const exportTask = sinon.stub().resolves();
12+
const instance = {
13+
isRunning: sinon.stub().resolves(true)
14+
};
15+
const ui = {
16+
run: sinon.stub().callsFake(fn => fn()),
17+
log: sinon.stub()
18+
};
19+
const getInstance = sinon.stub().returns(instance);
20+
21+
const ExportCommand = proxyquire(modulePath, {'../tasks/import': {exportTask}});
22+
const cmd = new ExportCommand(ui, {getInstance});
23+
24+
await cmd.run({file: 'test-output.json'});
25+
expect(getInstance.calledOnce).to.be.true;
26+
expect(instance.isRunning.calledOnce).to.be.true;
27+
expect(ui.run.calledOnce).to.be.true;
28+
expect(exportTask.calledOnceWithExactly(ui, instance, 'test-output.json')).to.be.true;
29+
expect(ui.log.calledOnce).to.be.true;
30+
});
31+
32+
it('prompts to start if not running and throws if not confirmed', async function () {
33+
const exportTask = sinon.stub().resolves();
34+
const instance = {
35+
isRunning: sinon.stub().resolves(false)
36+
};
37+
const ui = {
38+
confirm: sinon.stub().resolves(false),
39+
run: sinon.stub().callsFake(fn => fn()),
40+
log: sinon.stub()
41+
};
42+
const getInstance = sinon.stub().returns(instance);
43+
44+
const ExportCommand = proxyquire(modulePath, {'../tasks/import': {exportTask}});
45+
const cmd = new ExportCommand(ui, {getInstance});
46+
47+
try {
48+
await cmd.run({file: 'test-output.json'});
49+
} catch (error) {
50+
expect(error).to.be.an.instanceof(SystemError);
51+
expect(error.message).to.include('not currently running');
52+
expect(getInstance.calledOnce).to.be.true;
53+
expect(instance.isRunning.calledOnce).to.be.true;
54+
expect(ui.confirm.calledOnce).to.be.true;
55+
expect(ui.run.called).to.be.false;
56+
expect(exportTask.called).to.be.false;
57+
expect(ui.log.called).to.be.false;
58+
return;
59+
}
60+
61+
expect.fail('run should have errored');
62+
});
63+
64+
it('prompts to start if not running and starts if confirmed', async function () {
65+
const exportTask = sinon.stub().resolves();
66+
const instance = {
67+
isRunning: sinon.stub().resolves(false),
68+
start: sinon.stub().resolves(),
69+
checkEnvironment: sinon.stub()
70+
};
71+
const ui = {
72+
confirm: sinon.stub().resolves(true),
73+
run: sinon.stub().callsFake(fn => fn()),
74+
log: sinon.stub()
75+
};
76+
const getInstance = sinon.stub().returns(instance);
77+
78+
const ExportCommand = proxyquire(modulePath, {'../tasks/import': {exportTask}});
79+
const cmd = new ExportCommand(ui, {getInstance});
80+
81+
await cmd.run({file: 'test-output.json'});
82+
expect(getInstance.calledOnce).to.be.true;
83+
expect(instance.isRunning.calledOnce).to.be.true;
84+
expect(ui.confirm.calledOnce).to.be.true;
85+
expect(instance.checkEnvironment.calledOnce).to.be.true;
86+
expect(ui.run.calledTwice).to.be.true;
87+
expect(instance.start.calledOnce).to.be.true;
88+
expect(exportTask.calledOnceWithExactly(ui, instance, 'test-output.json')).to.be.true;
89+
expect(ui.log.calledOnce).to.be.true;
90+
});
91+
});

test/unit/commands/import-spec.js

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
const {expect} = require('chai');
2+
const sinon = require('sinon');
3+
const proxyquire = require('proxyquire').noCallThru();
4+
5+
const {SystemError} = require('../../../lib/errors');
6+
7+
const modulePath = '../../../lib/commands/import';
8+
9+
describe('Unit: Commands > import', function () {
10+
it('throws error if importing a 0.x import into a > 1.x blog', async function () {
11+
const parseExport = sinon.stub().returns({version: '0.11.14'});
12+
const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport}});
13+
const getInstance = sinon.stub().returns({version: '3.0.0'});
14+
15+
const cmd = new ImportCommand({}, {getInstance});
16+
17+
try {
18+
await cmd.run({file: 'test-output.json'});
19+
} catch (error) {
20+
expect(error).to.be.an.instanceof(SystemError);
21+
expect(error.message).to.include('can only be imported by Ghost v1.x versions');
22+
expect(getInstance.calledOnce).to.be.true;
23+
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
24+
return;
25+
}
26+
27+
expect.fail('expected run to error');
28+
});
29+
30+
it('runs import task from v0.x to 1.x if blog is running', async function () {
31+
const parseExport = sinon.stub().returns({version: '0.11.14'});
32+
const run = sinon.stub().resolves();
33+
const importTask = sinon.stub().resolves({run});
34+
const instance = {
35+
isRunning: sinon.stub().resolves(true),
36+
version: '1.0.0'
37+
};
38+
39+
const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport, importTask}});
40+
const getInstance = sinon.stub().returns(instance);
41+
42+
const cmd = new ImportCommand({ui: true}, {getInstance});
43+
44+
await cmd.run({file: 'test-output.json'});
45+
expect(getInstance.calledOnce).to.be.true;
46+
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
47+
expect(instance.isRunning.calledOnce).to.be.true;
48+
expect(importTask.calledOnceWithExactly({ui: true}, instance, 'test-output.json')).to.be.true;
49+
expect(run.calledOnce).to.be.true;
50+
});
51+
52+
it('runs import task from v1.x to any', async function () {
53+
const parseExport = sinon.stub().returns({version: '1.0.0'});
54+
const run = sinon.stub().resolves();
55+
const importTask = sinon.stub().resolves({run});
56+
const instance = {
57+
isRunning: sinon.stub().resolves(true),
58+
version: '3.0.0'
59+
};
60+
61+
const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport, importTask}});
62+
const getInstance = sinon.stub().returns(instance);
63+
64+
const cmd = new ImportCommand({ui: true}, {getInstance});
65+
66+
await cmd.run({file: 'test-output.json'});
67+
expect(getInstance.calledOnce).to.be.true;
68+
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
69+
expect(instance.isRunning.calledOnce).to.be.true;
70+
expect(importTask.calledOnceWithExactly({ui: true}, instance, 'test-output.json')).to.be.true;
71+
expect(run.calledOnce).to.be.true;
72+
});
73+
74+
it('prompts to start if not running and throws if not confirmed', async function () {
75+
const parseExport = sinon.stub().returns({version: '1.0.0'});
76+
const instance = {
77+
isRunning: sinon.stub().resolves(false),
78+
version: '3.0.0'
79+
};
80+
const confirm = sinon.stub().resolves(false);
81+
82+
const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport}});
83+
const getInstance = sinon.stub().returns(instance);
84+
85+
const cmd = new ImportCommand({confirm}, {getInstance});
86+
87+
try {
88+
await cmd.run({file: 'test-output.json'});
89+
} catch (error) {
90+
expect(error).to.be.an.instanceof(SystemError);
91+
expect(error.message).to.include('not currently running');
92+
expect(getInstance.calledOnce).to.be.true;
93+
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
94+
expect(instance.isRunning.calledOnce).to.be.true;
95+
expect(confirm.calledOnce).to.be.true;
96+
return;
97+
}
98+
99+
expect.fail('run should have errored');
100+
});
101+
102+
it('prompts to start if not running and starts if confirmed', async function () {
103+
const parseExport = sinon.stub().returns({version: '1.0.0'});
104+
const runImport = sinon.stub().resolves();
105+
const importTask = sinon.stub().resolves({run: runImport});
106+
const instance = {
107+
isRunning: sinon.stub().resolves(false),
108+
checkEnvironment: sinon.stub(),
109+
start: sinon.stub().resolves(),
110+
version: '3.0.0'
111+
};
112+
const confirm = sinon.stub().resolves(true);
113+
const run = sinon.stub().callsFake(fn => fn());
114+
115+
const ImportCommand = proxyquire(modulePath, {'../tasks/import': {parseExport, importTask}});
116+
const getInstance = sinon.stub().returns(instance);
117+
118+
const cmd = new ImportCommand({confirm, run}, {getInstance});
119+
120+
await cmd.run({file: 'test-output.json'});
121+
122+
expect(getInstance.calledOnce).to.be.true;
123+
expect(parseExport.calledOnceWithExactly('test-output.json')).to.be.true;
124+
expect(instance.isRunning.calledOnce).to.be.true;
125+
expect(confirm.calledOnce).to.be.true;
126+
expect(instance.checkEnvironment.calledOnce).to.be.true;
127+
expect(run.calledOnce).to.be.true;
128+
expect(instance.start.calledOnce).to.be.true;
129+
expect(importTask.calledOnceWithExactly({confirm, run}, instance, 'test-output.json')).to.be.true;
130+
expect(runImport.calledOnce).to.be.true;
131+
});
132+
});

0 commit comments

Comments
 (0)