ghost 4.15.1 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +1 -1
- package/content/themes/casper/default.hbs +2 -2
- package/content/themes/casper/package.json +1 -1
- package/content/themes/casper/page.hbs +28 -26
- package/content/themes/casper/partials/post-card.hbs +2 -2
- package/content/themes/casper/post.hbs +67 -65
- package/content/themes/casper/tag.hbs +2 -2
- package/core/built/assets/ghost-dark-bb2831fc27fcb02893ed0a761207dc63.css +1 -0
- package/core/built/assets/{ghost.min-e35cfee26d942c364166f57f3dcc9e75.js → ghost.min-d1d99f3ed6e0f427874b2a11e7078475.js} +228 -187
- package/core/built/assets/ghost.min-e7612edfa72b0fe2c201b387923e6fc7.css +1 -0
- package/core/built/assets/icons/discount-bubble.svg +1 -0
- package/core/built/assets/{vendor.min-ca33abc718f21a51327841d58f8875d0.js → vendor.min-3660ec7864887f1496fe7a27fd23ab76.js} +44 -42
- package/core/frontend/helpers/ghost_head.js +7 -1
- package/core/frontend/services/settings/loader.js +2 -2
- package/core/frontend/services/theme-engine/middleware.js +4 -1
- package/core/server/api/canary/settings.js +13 -144
- package/core/server/api/canary/utils/validators/input/settings.js +23 -1
- package/core/server/api/v3/settings.js +13 -132
- package/core/server/api/v3/utils/validators/input/settings.js +23 -1
- package/core/server/data/exporter/table-lists.js +1 -0
- package/core/server/data/importer/import-manager.js +398 -0
- package/core/server/data/importer/importers/data/data-importer.js +162 -0
- package/core/server/data/importer/importers/data/index.js +1 -162
- package/core/server/data/importer/index.js +1 -379
- package/core/server/data/migrations/versions/4.16/01-add-custom-theme-settings-table.js +9 -0
- package/core/server/data/schema/schema.js +16 -0
- package/core/server/models/custom-theme-setting.js +9 -0
- package/core/server/models/index.js +2 -0
- package/core/server/services/custom-theme-settings.js +8 -0
- package/core/server/services/members/api.js +1 -0
- package/core/server/services/settings/index.js +13 -16
- package/core/server/services/settings/settings-bread-service.js +188 -0
- package/core/server/services/settings/settings-utils.js +32 -0
- package/core/server/services/themes/ThemeStorage.js +5 -4
- package/core/server/services/themes/activation-bridge.js +14 -0
- package/core/server/services/themes/validate.js +5 -2
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/members/app.js +2 -0
- package/core/shared/custom-theme-settings-cache.js +3 -0
- package/core/shared/labs.js +2 -1
- package/package.json +28 -27
- package/yarn.lock +806 -795
- package/core/built/assets/ghost-dark-faf931d90e92535e6c03ca16793cbe7b.css +0 -1
- package/core/built/assets/ghost.min-7aa074ad556a8455155ac88ceaca03ab.css +0 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const Promise = require('bluebird');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const glob = require('glob');
|
|
7
|
+
const uuid = require('uuid');
|
|
8
|
+
const {extract} = require('@tryghost/zip');
|
|
9
|
+
const {pipeline, sequence} = require('@tryghost/promise');
|
|
10
|
+
const i18n = require('../../../shared/i18n');
|
|
11
|
+
const logging = require('@tryghost/logging');
|
|
12
|
+
const errors = require('@tryghost/errors');
|
|
13
|
+
const ImageHandler = require('./handlers/image');
|
|
14
|
+
const JSONHandler = require('./handlers/json');
|
|
15
|
+
const MarkdownHandler = require('./handlers/markdown');
|
|
16
|
+
const ImageImporter = require('./importers/image');
|
|
17
|
+
const DataImporter = require('./importers/data');
|
|
18
|
+
|
|
19
|
+
// Glob levels
|
|
20
|
+
const ROOT_ONLY = 0;
|
|
21
|
+
|
|
22
|
+
const ROOT_OR_SINGLE_DIR = 1;
|
|
23
|
+
const ALL_DIRS = 2;
|
|
24
|
+
let defaults;
|
|
25
|
+
|
|
26
|
+
defaults = {
|
|
27
|
+
extensions: ['.zip'],
|
|
28
|
+
contentTypes: ['application/zip', 'application/x-zip-compressed'],
|
|
29
|
+
directories: []
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
class ImportManager {
|
|
33
|
+
constructor() {
|
|
34
|
+
this.importers = [ImageImporter, DataImporter];
|
|
35
|
+
this.handlers = [ImageHandler, JSONHandler, MarkdownHandler];
|
|
36
|
+
|
|
37
|
+
// Keep track of file to cleanup at the end
|
|
38
|
+
this.fileToDelete = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get an array of all the file extensions for which we have handlers
|
|
43
|
+
* @returns {string[]}
|
|
44
|
+
*/
|
|
45
|
+
getExtensions() {
|
|
46
|
+
return _.flatten(_.union(_.map(this.handlers, 'extensions'), defaults.extensions));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get an array of all the mime types for which we have handlers
|
|
51
|
+
* @returns {string[]}
|
|
52
|
+
*/
|
|
53
|
+
getContentTypes() {
|
|
54
|
+
return _.flatten(_.union(_.map(this.handlers, 'contentTypes'), defaults.contentTypes));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get an array of directories for which we have handlers
|
|
59
|
+
* @returns {string[]}
|
|
60
|
+
*/
|
|
61
|
+
getDirectories() {
|
|
62
|
+
return _.flatten(_.union(_.map(this.handlers, 'directories'), defaults.directories));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert items into a glob string
|
|
67
|
+
* @param {String[]} items
|
|
68
|
+
* @returns {String}
|
|
69
|
+
*/
|
|
70
|
+
getGlobPattern(items) {
|
|
71
|
+
return '+(' + _.reduce(items, function (memo, ext) {
|
|
72
|
+
return memo !== '' ? memo + '|' + ext : ext;
|
|
73
|
+
}, '') + ')';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {String[]} extensions
|
|
78
|
+
* @param {Number} level
|
|
79
|
+
* @returns {String}
|
|
80
|
+
*/
|
|
81
|
+
getExtensionGlob(extensions, level) {
|
|
82
|
+
const prefix = level === ALL_DIRS ? '**/*' :
|
|
83
|
+
(level === ROOT_OR_SINGLE_DIR ? '{*/*,*}' : '*');
|
|
84
|
+
|
|
85
|
+
return prefix + this.getGlobPattern(extensions);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
*
|
|
90
|
+
* @param {String[]} directories
|
|
91
|
+
* @param {Number} level
|
|
92
|
+
* @returns {String}
|
|
93
|
+
*/
|
|
94
|
+
getDirectoryGlob(directories, level) {
|
|
95
|
+
const prefix = level === ALL_DIRS ? '**/' :
|
|
96
|
+
(level === ROOT_OR_SINGLE_DIR ? '{*/,}' : '');
|
|
97
|
+
|
|
98
|
+
return prefix + this.getGlobPattern(directories);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Remove files after we're done (abstracted into a function for easier testing)
|
|
103
|
+
* @returns {Function}
|
|
104
|
+
*/
|
|
105
|
+
cleanUp() {
|
|
106
|
+
const self = this;
|
|
107
|
+
|
|
108
|
+
if (self.fileToDelete === null) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fs.remove(self.fileToDelete, function (err) {
|
|
113
|
+
if (err) {
|
|
114
|
+
logging.error(new errors.GhostError({
|
|
115
|
+
err: err,
|
|
116
|
+
context: i18n.t('errors.data.importer.index.couldNotCleanUpFile.error'),
|
|
117
|
+
help: i18n.t('errors.data.importer.index.couldNotCleanUpFile.context')
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
self.fileToDelete = null;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Return true if the given file is a Zip
|
|
127
|
+
* @returns Boolean
|
|
128
|
+
*/
|
|
129
|
+
isZip(ext) {
|
|
130
|
+
return _.includes(defaults.extensions, ext);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Checks the content of a zip folder to see if it is valid.
|
|
135
|
+
* Importable content includes any files or directories which the handlers can process
|
|
136
|
+
* Importable content must be found either in the root, or inside one base directory
|
|
137
|
+
*
|
|
138
|
+
* @param {String} directory
|
|
139
|
+
* @returns {Promise}
|
|
140
|
+
*/
|
|
141
|
+
isValidZip(directory) {
|
|
142
|
+
// Globs match content in the root or inside a single directory
|
|
143
|
+
const extMatchesBase = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_OR_SINGLE_DIR), {cwd: directory});
|
|
144
|
+
|
|
145
|
+
const extMatchesAll = glob.sync(
|
|
146
|
+
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const dirMatches = glob.sync(
|
|
150
|
+
this.getDirectoryGlob(this.getDirectories(), ROOT_OR_SINGLE_DIR), {cwd: directory}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const oldRoonMatches = glob.sync(this.getDirectoryGlob(['drafts', 'published', 'deleted'], ROOT_OR_SINGLE_DIR),
|
|
154
|
+
{cwd: directory});
|
|
155
|
+
|
|
156
|
+
// This is a temporary extra message for the old format roon export which doesn't work with Ghost
|
|
157
|
+
if (oldRoonMatches.length > 0) {
|
|
158
|
+
throw new errors.UnsupportedMediaTypeError({message: i18n.t('errors.data.importer.index.unsupportedRoonExport')});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If this folder contains importable files or a content or images directory
|
|
162
|
+
if (extMatchesBase.length > 0 || (dirMatches.length > 0 && extMatchesAll.length > 0)) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (extMatchesAll.length < 1) {
|
|
167
|
+
throw new errors.UnsupportedMediaTypeError({message: i18n.t('errors.data.importer.index.noContentToImport')});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new errors.UnsupportedMediaTypeError({message: i18n.t('errors.data.importer.index.invalidZipStructure')});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Use the extract module to extract the given zip file to a temp directory & return the temp directory path
|
|
175
|
+
* @param {String} filePath
|
|
176
|
+
* @returns {Promise[]} Files
|
|
177
|
+
*/
|
|
178
|
+
extractZip(filePath) {
|
|
179
|
+
const tmpDir = path.join(os.tmpdir(), uuid.v4());
|
|
180
|
+
this.fileToDelete = tmpDir;
|
|
181
|
+
|
|
182
|
+
return extract(filePath, tmpDir).then(function () {
|
|
183
|
+
return tmpDir;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Use the handler extensions to get a globbing pattern, then use that to fetch all the files from the zip which
|
|
189
|
+
* are relevant to the given handler, and return them as a name and path combo
|
|
190
|
+
* @param {Object} handler
|
|
191
|
+
* @param {String} directory
|
|
192
|
+
* @returns [] Files
|
|
193
|
+
*/
|
|
194
|
+
getFilesFromZip(handler, directory) {
|
|
195
|
+
const globPattern = this.getExtensionGlob(handler.extensions, ALL_DIRS);
|
|
196
|
+
return _.map(glob.sync(globPattern, {cwd: directory}), function (file) {
|
|
197
|
+
return {name: file, path: path.join(directory, file)};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get the name of the single base directory if there is one, else return an empty string
|
|
203
|
+
* @param {String} directory
|
|
204
|
+
* @returns {String}
|
|
205
|
+
*/
|
|
206
|
+
getBaseDirectory(directory) {
|
|
207
|
+
// Globs match root level only
|
|
208
|
+
const extMatches = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_ONLY), {cwd: directory});
|
|
209
|
+
|
|
210
|
+
const dirMatches = glob.sync(this.getDirectoryGlob(this.getDirectories(), ROOT_ONLY), {cwd: directory});
|
|
211
|
+
let extMatchesAll;
|
|
212
|
+
|
|
213
|
+
// There is no base directory
|
|
214
|
+
if (extMatches.length > 0 || dirMatches.length > 0) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// There is a base directory, grab it from any ext match
|
|
218
|
+
extMatchesAll = glob.sync(
|
|
219
|
+
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
|
|
220
|
+
);
|
|
221
|
+
if (extMatchesAll.length < 1 || extMatchesAll[0].split('/') < 1) {
|
|
222
|
+
throw new errors.ValidationError({message: i18n.t('errors.data.importer.index.invalidZipFileBaseDirectory')});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return extMatchesAll[0].split('/')[0];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Process Zip
|
|
230
|
+
* Takes a reference to a zip file, extracts it, sends any relevant files from inside to the right handler, and
|
|
231
|
+
* returns an object in the importData format: {data: {}, images: []}
|
|
232
|
+
* The data key contains JSON representing any data that should be imported
|
|
233
|
+
* The image key contains references to images that will be stored (and where they will be stored)
|
|
234
|
+
* @param {File} file
|
|
235
|
+
* @returns {Promise(ImportData)}
|
|
236
|
+
*/
|
|
237
|
+
processZip(file) {
|
|
238
|
+
const self = this;
|
|
239
|
+
|
|
240
|
+
return this.extractZip(file.path).then(function (zipDirectory) {
|
|
241
|
+
const ops = [];
|
|
242
|
+
const importData = {};
|
|
243
|
+
let baseDir;
|
|
244
|
+
|
|
245
|
+
self.isValidZip(zipDirectory);
|
|
246
|
+
baseDir = self.getBaseDirectory(zipDirectory);
|
|
247
|
+
|
|
248
|
+
_.each(self.handlers, function (handler) {
|
|
249
|
+
if (Object.prototype.hasOwnProperty.call(importData, handler.type)) {
|
|
250
|
+
// This limitation is here to reduce the complexity of the importer for now
|
|
251
|
+
return Promise.reject(new errors.UnsupportedMediaTypeError({
|
|
252
|
+
message: i18n.t('errors.data.importer.index.zipContainsMultipleDataFormats')
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const files = self.getFilesFromZip(handler, zipDirectory);
|
|
257
|
+
|
|
258
|
+
if (files.length > 0) {
|
|
259
|
+
ops.push(function () {
|
|
260
|
+
return handler.loadFile(files, baseDir).then(function (data) {
|
|
261
|
+
importData[handler.type] = data;
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (ops.length === 0) {
|
|
268
|
+
return Promise.reject(new errors.UnsupportedMediaTypeError({
|
|
269
|
+
message: i18n.t('errors.data.importer.index.noContentToImport')
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return sequence(ops).then(function () {
|
|
274
|
+
return importData;
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Process File
|
|
281
|
+
* Takes a reference to a single file, sends it to the relevant handler to be loaded and returns an object in the
|
|
282
|
+
* importData format: {data: {}, images: []}
|
|
283
|
+
* The data key contains JSON representing any data that should be imported
|
|
284
|
+
* The image key contains references to images that will be stored (and where they will be stored)
|
|
285
|
+
* @param {File} file
|
|
286
|
+
* @returns {Promise(ImportData)}
|
|
287
|
+
*/
|
|
288
|
+
processFile(file, ext) {
|
|
289
|
+
const fileHandler = _.find(this.handlers, function (handler) {
|
|
290
|
+
return _.includes(handler.extensions, ext);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return fileHandler.loadFile([_.pick(file, 'name', 'path')]).then(function (loadedData) {
|
|
294
|
+
// normalize the returned data
|
|
295
|
+
const importData = {};
|
|
296
|
+
importData[fileHandler.type] = loadedData;
|
|
297
|
+
return importData;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Import Step 1:
|
|
303
|
+
* Load the given file into usable importData in the format: {data: {}, images: []}, regardless of
|
|
304
|
+
* whether the file is a single importable file like a JSON file, or a zip file containing loads of files.
|
|
305
|
+
* @param {File} file
|
|
306
|
+
* @returns {Promise}
|
|
307
|
+
*/
|
|
308
|
+
loadFile(file) {
|
|
309
|
+
const self = this;
|
|
310
|
+
const ext = path.extname(file.name).toLowerCase();
|
|
311
|
+
return this.isZip(ext) ? self.processZip(file) : self.processFile(file, ext);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Import Step 2:
|
|
316
|
+
* Pass the prepared importData through the preProcess function of the various importers, so that the importers can
|
|
317
|
+
* make any adjustments to the data based on relationships between it
|
|
318
|
+
* @param {ImportData} importData
|
|
319
|
+
* @returns {Promise(ImportData)}
|
|
320
|
+
*/
|
|
321
|
+
preProcess(importData) {
|
|
322
|
+
const ops = [];
|
|
323
|
+
_.each(this.importers, function (importer) {
|
|
324
|
+
ops.push(function () {
|
|
325
|
+
return importer.preProcess(importData);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
return pipeline(ops);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Import Step 3:
|
|
334
|
+
* Each importer gets passed the data from importData which has the key matching its type - i.e. it only gets the
|
|
335
|
+
* data that it should import. Each importer then handles actually importing that data into Ghost
|
|
336
|
+
* @param {ImportData} importData
|
|
337
|
+
* @param {Object} importOptions to allow override of certain import features such as locking a user
|
|
338
|
+
* @returns {Promise<any>}
|
|
339
|
+
*/
|
|
340
|
+
doImport(importData, importOptions) {
|
|
341
|
+
importOptions = importOptions || {};
|
|
342
|
+
const ops = [];
|
|
343
|
+
_.each(this.importers, function (importer) {
|
|
344
|
+
if (Object.prototype.hasOwnProperty.call(importData, importer.type)) {
|
|
345
|
+
ops.push(function () {
|
|
346
|
+
return importer.doImport(importData[importer.type], importOptions);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
return sequence(ops).then(function (importResult) {
|
|
352
|
+
return importResult;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Import Step 4:
|
|
358
|
+
* Report on what was imported, currently a no-op
|
|
359
|
+
* @param {ImportData} importData
|
|
360
|
+
* @returns {Promise<ImportData>}
|
|
361
|
+
*/
|
|
362
|
+
generateReport(importData) {
|
|
363
|
+
return Promise.resolve(importData);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Import From File
|
|
368
|
+
* The main method of the ImportManager, call this to kick everything off!
|
|
369
|
+
* @param {File} file
|
|
370
|
+
* @param {Object} importOptions to allow override of certain import features such as locking a user
|
|
371
|
+
* @returns {Promise}
|
|
372
|
+
*/
|
|
373
|
+
importFromFile(file, importOptions = {}) {
|
|
374
|
+
const self = this;
|
|
375
|
+
|
|
376
|
+
// Step 1: Handle converting the file to usable data
|
|
377
|
+
return this.loadFile(file).then(function (importData) {
|
|
378
|
+
// Step 2: Let the importers pre-process the data
|
|
379
|
+
return self.preProcess(importData);
|
|
380
|
+
}).then(function (importData) {
|
|
381
|
+
// Step 3: Actually do the import
|
|
382
|
+
// @TODO: It would be cool to have some sort of dry run flag here
|
|
383
|
+
return self.doImport(importData, importOptions);
|
|
384
|
+
}).then(function (importData) {
|
|
385
|
+
// Step 4: Report on the import
|
|
386
|
+
return self.generateReport(importData);
|
|
387
|
+
}).finally(() => self.cleanUp()); // Step 5: Cleanup any files
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A number, or a string containing a number.
|
|
393
|
+
* @typedef {Object} ImportData
|
|
394
|
+
* @property [Object] data
|
|
395
|
+
* @property [Array] images
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
module.exports = new ImportManager();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const Promise = require('bluebird');
|
|
3
|
+
const semver = require('semver');
|
|
4
|
+
const {IncorrectUsageError} = require('@tryghost/errors');
|
|
5
|
+
const debug = require('@tryghost/debug')('importer:data');
|
|
6
|
+
const {sequence} = require('@tryghost/promise');
|
|
7
|
+
const models = require('../../../../models');
|
|
8
|
+
const PostsImporter = require('./posts');
|
|
9
|
+
const TagsImporter = require('./tags');
|
|
10
|
+
const SettingsImporter = require('./settings');
|
|
11
|
+
const UsersImporter = require('./users');
|
|
12
|
+
const RolesImporter = require('./roles');
|
|
13
|
+
let importers = {};
|
|
14
|
+
let DataImporter;
|
|
15
|
+
|
|
16
|
+
DataImporter = {
|
|
17
|
+
type: 'data',
|
|
18
|
+
|
|
19
|
+
preProcess: function preProcess(importData) {
|
|
20
|
+
importData.preProcessedByData = true;
|
|
21
|
+
return importData;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
init: function init(importData) {
|
|
25
|
+
importers.users = new UsersImporter(importData.data);
|
|
26
|
+
importers.roles = new RolesImporter(importData.data);
|
|
27
|
+
importers.tags = new TagsImporter(importData.data);
|
|
28
|
+
importers.posts = new PostsImporter(importData.data);
|
|
29
|
+
importers.settings = new SettingsImporter(importData.data);
|
|
30
|
+
|
|
31
|
+
return importData;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Allow importing with an options object that is passed through the importer
|
|
35
|
+
doImport: function doImport(importData, importOptions) {
|
|
36
|
+
importOptions = importOptions || {};
|
|
37
|
+
|
|
38
|
+
const ops = [];
|
|
39
|
+
let errors = [];
|
|
40
|
+
let results = [];
|
|
41
|
+
|
|
42
|
+
const modelOptions = {
|
|
43
|
+
importing: true,
|
|
44
|
+
context: {
|
|
45
|
+
internal: true
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (!Object.prototype.hasOwnProperty.call(importOptions, 'returnImportedData')) {
|
|
50
|
+
importOptions.returnImportedData = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (importOptions.importPersistUser) {
|
|
54
|
+
modelOptions.importPersistUser = importOptions.importPersistUser;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!importData.meta) {
|
|
58
|
+
return Promise.reject(new IncorrectUsageError({
|
|
59
|
+
message: 'Wrong importer structure. `meta` is missing.',
|
|
60
|
+
help: 'https://ghost.org/docs/migration/custom/'
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!importData.meta.version) {
|
|
65
|
+
return Promise.reject(new IncorrectUsageError({
|
|
66
|
+
message: 'Wrong importer structure. `meta.version` is missing.',
|
|
67
|
+
help: 'https://ghost.org/docs/migration/custom/'
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// CASE: We deny LTS imports, because these are major version jumps. Only imports from v1 until the latest are supported.
|
|
72
|
+
// We can detect a wrong structure by checking the meta version field. Ghost v0 doesn't use semver compliant versions.
|
|
73
|
+
if (!semver.valid(importData.meta.version)) {
|
|
74
|
+
return Promise.reject(new IncorrectUsageError({
|
|
75
|
+
message: 'Detected unsupported file structure.',
|
|
76
|
+
help: 'Please install Ghost 1.0, import the file and then update your blog to the latest Ghost version.\nVisit https://ghost.org/docs/update/ or ask for help in our https://forum.ghost.org.'
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.init(importData);
|
|
81
|
+
|
|
82
|
+
return models.Base.transaction(function (transacting) {
|
|
83
|
+
modelOptions.transacting = transacting;
|
|
84
|
+
|
|
85
|
+
_.each(importers, function (importer) {
|
|
86
|
+
ops.push(function doModelImport() {
|
|
87
|
+
return importer.fetchExisting(modelOptions, importOptions)
|
|
88
|
+
.then(function () {
|
|
89
|
+
return importer.beforeImport(modelOptions, importOptions);
|
|
90
|
+
})
|
|
91
|
+
.then(function () {
|
|
92
|
+
if (importer.options.requiredImportedData.length) {
|
|
93
|
+
_.each(importer.options.requiredImportedData, (key) => {
|
|
94
|
+
importer.requiredImportedData[key] = importers[key].importedData;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (importer.options.requiredExistingData.length) {
|
|
99
|
+
_.each(importer.options.requiredExistingData, (key) => {
|
|
100
|
+
importer.requiredExistingData[key] = importers[key].existingData;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return importer.replaceIdentifiers(modelOptions, importOptions);
|
|
105
|
+
})
|
|
106
|
+
.then(function () {
|
|
107
|
+
return importer.doImport(modelOptions, importOptions)
|
|
108
|
+
.then(function (_results) {
|
|
109
|
+
results = results.concat(_results);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
sequence(ops)
|
|
116
|
+
.then(function () {
|
|
117
|
+
results.forEach(function (promise) {
|
|
118
|
+
if (!promise.isFulfilled()) {
|
|
119
|
+
errors = errors.concat(promise.reason());
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (errors.length === 0) {
|
|
124
|
+
transacting.commit();
|
|
125
|
+
} else {
|
|
126
|
+
transacting.rollback(errors);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}).then(function () {
|
|
130
|
+
/**
|
|
131
|
+
* data: imported data
|
|
132
|
+
* originalData: data from the json file
|
|
133
|
+
* problems: warnings
|
|
134
|
+
*/
|
|
135
|
+
const toReturn = {
|
|
136
|
+
data: {},
|
|
137
|
+
originalData: importData.data,
|
|
138
|
+
problems: []
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
_.each(importers, function (importer) {
|
|
142
|
+
toReturn.problems = toReturn.problems.concat(importer.problems);
|
|
143
|
+
|
|
144
|
+
if (importOptions.returnImportedData) {
|
|
145
|
+
toReturn.data[importer.dataKeyToImport] = importer.importedDataToReturn;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return toReturn;
|
|
150
|
+
}).catch(function (err) {
|
|
151
|
+
debug(err);
|
|
152
|
+
return Promise.reject(err);
|
|
153
|
+
}).finally(() => {
|
|
154
|
+
// release memory
|
|
155
|
+
importers = {};
|
|
156
|
+
results = null;
|
|
157
|
+
importData = null;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
module.exports = DataImporter;
|