ghost 4.37.0 → 4.39.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/.c8rc.json +1 -1
- package/README.md +26 -18
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/casper/assets/built/global.css +1 -1
- package/content/themes/casper/assets/built/global.css.map +1 -1
- 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/global.css +14 -6
- package/content/themes/casper/assets/css/screen.css +9 -1
- package/content/themes/casper/package.json +2 -2
- package/content/themes/casper/partials/post-card.hbs +1 -1
- package/content/themes/casper/post.hbs +18 -19
- package/content/themes/casper/yarn.lock +186 -217
- package/core/built/assets/{chunk.3.4906cf0b01d6d8e33374.js → chunk.3.6e2ed2d00856e12bd81a.js} +19 -19
- package/core/built/assets/ghost-dark-498ff8339a89bb68c3f78f59bee4146e.css +1 -0
- package/core/built/assets/ghost.min-77b93478f83b0def6ddc5a4f23ce963e.css +1 -0
- package/core/built/assets/{ghost.min-c1938f6ee696bf08bd6bf93cac341ea2.js → ghost.min-e6559d901897066aa6a6d4145e3728ed.js} +466 -413
- package/core/built/assets/icons/{event-changed-subscription.svg → event-subscriptions.svg} +0 -1
- package/core/built/assets/icons/eye.svg +4 -1
- package/core/built/assets/icons/member-add.svg +3 -0
- package/core/built/assets/icons/member.svg +3 -0
- package/core/built/assets/icons/pin.svg +4 -1
- package/core/built/assets/{vendor.min-6dc30be68238b5c55df0cdc1f2dc8b8d.js → vendor.min-c39476bced9adb98ee2b292d01c7a8f4.js} +2303 -1372
- package/core/frontend/apps/private-blogging/lib/middleware.js +1 -1
- package/core/frontend/apps/private-blogging/lib/views/private.hbs +9 -10
- package/core/frontend/helpers/get.js +4 -0
- package/core/frontend/helpers/match.js +12 -0
- package/core/frontend/helpers/prev_post.js +11 -1
- package/core/frontend/helpers/tiers.js +59 -0
- package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
- package/core/frontend/public/ghost.css +205 -143
- package/core/frontend/services/routing/router-manager.js +1 -1
- package/core/frontend/views/unsubscribe.hbs +28 -33
- package/core/frontend/web/middleware/error-handler.js +2 -2
- package/core/frontend/web/site.js +10 -0
- package/core/server/api/canary/authentication.js +7 -0
- package/core/server/api/canary/index.js +4 -0
- package/core/server/api/canary/members.js +9 -2
- package/core/server/api/canary/products.js +3 -6
- package/core/server/api/canary/tiers-public.js +34 -0
- package/core/server/api/canary/tiers.js +6 -7
- package/core/server/api/canary/utils/serializers/input/pages.js +1 -1
- package/core/server/api/canary/utils/serializers/input/posts.js +1 -1
- package/core/server/api/canary/utils/serializers/output/email-posts.js +7 -1
- package/core/server/api/canary/utils/serializers/output/pages.js +9 -2
- package/core/server/api/canary/utils/serializers/output/posts.js +8 -2
- package/core/server/api/canary/utils/serializers/output/preview.js +7 -1
- package/core/server/api/canary/utils/serializers/output/products.js +3 -1
- package/core/server/api/canary/utils/serializers/output/tiers.js +4 -2
- package/core/server/api/canary/utils/serializers/output/utils/mapper.js +17 -7
- package/core/server/api/shared/serializers/handle.js +2 -2
- package/core/server/api/v2/utils/serializers/input/pages.js +1 -1
- package/core/server/api/v2/utils/serializers/input/posts.js +1 -1
- package/core/server/api/v3/utils/serializers/input/pages.js +1 -1
- package/core/server/api/v3/utils/serializers/input/posts.js +1 -1
- package/core/server/data/db/connection.js +3 -2
- package/core/server/data/importer/import-manager.js +152 -113
- package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -1
- package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -1
- package/core/server/data/migrations/versions/3.29/03-remove-orphaned-customers.js +2 -1
- package/core/server/data/migrations/versions/3.29/04-remove-orphaned-subscriptions.js +2 -1
- package/core/server/data/migrations/versions/3.29/05-add-member-constraints.js +3 -2
- package/core/server/data/migrations/versions/3.39/06-add-email-recipient-index.js +4 -3
- package/core/server/data/migrations/versions/4.0/14-remove-orphaned-stripe-records.js +2 -1
- package/core/server/data/migrations/versions/4.0/26-add-cascade-on-delete.js +2 -1
- package/core/server/data/migrations/versions/4.0/29-fix-foreign-key-for-members-stripe-customers-subscriptions.js +2 -1
- package/core/server/data/migrations/versions/4.1/02-add-unique-constraint-for-member-stripe-tables.js +2 -1
- package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +3 -2
- package/core/server/data/migrations/versions/4.33/2022-01-14-11-51-add-default-free-tier.js +3 -0
- package/core/server/data/migrations/versions/4.33/2022-01-18-09-07-remove-duplicate-offer-redemptions.js +2 -2
- package/core/server/data/migrations/versions/4.35/2022-02-01-11-48-update-email-recipient-filter-column-type.js +2 -1
- package/core/server/data/migrations/versions/4.35/2022-02-01-12-03-update-recipient-filter-column-type.js +2 -1
- package/core/server/data/migrations/versions/4.37/2022-02-21-09-53-backfill-members-last-seen-at-column.js +3 -2
- package/core/server/data/migrations/versions/4.38/2022-03-01-08-46-add-visibility-to-tiers.js +11 -0
- package/core/server/data/migrations/versions/4.38/2022-03-03-16-12-add-visibility-to-tiers.js +8 -0
- package/core/server/data/migrations/versions/4.38/2022-03-03-16-17-drop-tiers-visible-column.js +7 -0
- package/core/server/data/migrations/versions/4.39/2022-03-07-10-57-update-free-products-visibility-column.js +66 -0
- package/core/server/data/migrations/versions/4.39/2022-03-07-10-57-update-products-visibility-column.js +36 -0
- package/core/server/data/schema/clients/index.js +1 -1
- package/core/server/data/schema/clients/mysql.js +4 -4
- package/core/server/data/schema/commands.js +42 -50
- package/core/server/data/schema/default-settings/default-settings.json +2 -2
- package/core/server/data/schema/fixtures/fixtures.json +18 -161
- package/core/server/data/schema/schema.js +7 -0
- package/core/server/frontend/ghost.min.css +1 -1
- package/core/server/lib/image/image-size.js +12 -4
- package/core/server/models/base/plugins/generate-slug.js +13 -1
- package/core/server/models/base/plugins/raw-knex.js +1 -1
- package/core/server/models/post.js +16 -6
- package/core/server/models/product.js +2 -1
- package/core/server/models/user.js +1 -1
- package/core/server/services/auth/api-key/admin.js +15 -6
- package/core/server/services/auth/setup.js +34 -13
- package/core/server/services/email-analytics/lib/event-processor.js +18 -1
- package/core/server/services/mega/mega.js +4 -4
- package/core/server/services/mega/template.js +1 -1
- package/core/server/services/members/content-gating.js +1 -1
- package/core/server/services/members/middleware.js +4 -0
- package/core/server/services/members/service.js +13 -1
- package/core/server/services/posts/posts-service.js +1 -1
- package/core/server/services/url/UrlGenerator.js +1 -1
- package/core/server/services/webhooks/webhooks-service.js +2 -0
- package/core/server/views/maintenance.html +2 -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/api/app.js +3 -0
- package/core/server/web/api/canary/admin/middleware.js +2 -0
- package/core/server/web/api/canary/content/routes.js +1 -0
- package/core/server/web/members/app.js +1 -1
- package/core/server/web/parent/backend.js +2 -1
- package/core/server/web/shared/middleware/uncapitalise.js +3 -2
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/utils.js +5 -1
- package/core/shared/labs.js +8 -9
- package/core/shared/url-utils.js +4 -1
- package/package.json +56 -52
- package/yarn.lock +809 -607
- package/core/built/assets/ghost-dark-d54723f7267e66fa2595f897076e86c2.css +0 -1
- package/core/built/assets/ghost.min-02a5f8954bd85fe28817b8c8b111b8aa.css +0 -1
- package/core/built/assets/icons/event-started-subscription.svg +0 -6
- package/core/built/assets/icons/locked-email-back.svg +0 -1
- package/core/built/assets/icons/locked-email-front.svg +0 -1
- package/core/built/assets/icons/locked-email-lock.svg +0 -1
- package/core/built/assets/img/ghost-logo-de2acf283f53ba1fd1149928faeaaa74.png +0 -0
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
-
const Promise = require('bluebird');
|
|
3
2
|
const fs = require('fs-extra');
|
|
4
3
|
const path = require('path');
|
|
5
4
|
const os = require('os');
|
|
6
5
|
const glob = require('glob');
|
|
7
6
|
const uuid = require('uuid');
|
|
8
7
|
const {extract} = require('@tryghost/zip');
|
|
9
|
-
const {pipeline, sequence} = require('@tryghost/promise');
|
|
10
8
|
const tpl = require('@tryghost/tpl');
|
|
11
9
|
const logging = require('@tryghost/logging');
|
|
12
10
|
const errors = require('@tryghost/errors');
|
|
@@ -21,7 +19,6 @@ const messages = {
|
|
|
21
19
|
error: 'Import could not clean up file ',
|
|
22
20
|
context: 'Your site will continue to work as expected'
|
|
23
21
|
},
|
|
24
|
-
unsupportedRoonExport: 'Your zip file looks like an old format Roon export, please re-export your Roon blog and try again.',
|
|
25
22
|
noContentToImport: 'Zip did not include any content to import.',
|
|
26
23
|
invalidZipStructure: 'Invalid zip file structure.',
|
|
27
24
|
invalidZipFileBaseDirectory: 'Invalid zip file: base directory read failed',
|
|
@@ -41,10 +38,20 @@ let defaults = {
|
|
|
41
38
|
|
|
42
39
|
class ImportManager {
|
|
43
40
|
constructor() {
|
|
41
|
+
/**
|
|
42
|
+
* @type {Importer[]} importers
|
|
43
|
+
*/
|
|
44
44
|
this.importers = [ImageImporter, DataImporter];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @type {Handler[]}
|
|
48
|
+
*/
|
|
45
49
|
this.handlers = [ImageHandler, JSONHandler, MarkdownHandler];
|
|
46
50
|
|
|
47
51
|
// Keep track of file to cleanup at the end
|
|
52
|
+
/**
|
|
53
|
+
* @type {?string}
|
|
54
|
+
*/
|
|
48
55
|
this.fileToDelete = null;
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -53,7 +60,7 @@ class ImportManager {
|
|
|
53
60
|
* @returns {string[]}
|
|
54
61
|
*/
|
|
55
62
|
getExtensions() {
|
|
56
|
-
return _.
|
|
63
|
+
return _.union(_.flatMap(this.handlers, 'extensions'), defaults.extensions);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
/**
|
|
@@ -61,7 +68,7 @@ class ImportManager {
|
|
|
61
68
|
* @returns {string[]}
|
|
62
69
|
*/
|
|
63
70
|
getContentTypes() {
|
|
64
|
-
return _.
|
|
71
|
+
return _.union(_.flatMap(this.handlers, 'contentTypes'), defaults.contentTypes);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/**
|
|
@@ -69,7 +76,7 @@ class ImportManager {
|
|
|
69
76
|
* @returns {string[]}
|
|
70
77
|
*/
|
|
71
78
|
getDirectories() {
|
|
72
|
-
return _.
|
|
79
|
+
return _.union(_.flatMap(this.handlers, 'directories'), defaults.directories);
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
/**
|
|
@@ -85,7 +92,7 @@ class ImportManager {
|
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
94
|
* @param {String[]} extensions
|
|
88
|
-
* @param {Number} level
|
|
95
|
+
* @param {Number} [level]
|
|
89
96
|
* @returns {String}
|
|
90
97
|
*/
|
|
91
98
|
getExtensionGlob(extensions, level) {
|
|
@@ -98,7 +105,7 @@ class ImportManager {
|
|
|
98
105
|
/**
|
|
99
106
|
*
|
|
100
107
|
* @param {String[]} directories
|
|
101
|
-
* @param {Number} level
|
|
108
|
+
* @param {Number} [level]
|
|
102
109
|
* @returns {String}
|
|
103
110
|
*/
|
|
104
111
|
getDirectoryGlob(directories, level) {
|
|
@@ -110,26 +117,24 @@ class ImportManager {
|
|
|
110
117
|
|
|
111
118
|
/**
|
|
112
119
|
* Remove files after we're done (abstracted into a function for easier testing)
|
|
113
|
-
* @returns {
|
|
120
|
+
* @returns {Promise<void>}
|
|
114
121
|
*/
|
|
115
|
-
cleanUp() {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (self.fileToDelete === null) {
|
|
122
|
+
async cleanUp() {
|
|
123
|
+
if (this.fileToDelete === null) {
|
|
119
124
|
return;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
127
|
+
try {
|
|
128
|
+
await fs.remove(this.fileToDelete);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
logging.error(new errors.InternalServerError({
|
|
131
|
+
err: err,
|
|
132
|
+
context: tpl(messages.couldNotCleanUpFile.error),
|
|
133
|
+
help: tpl(messages.couldNotCleanUpFile.context)
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
130
136
|
|
|
131
|
-
|
|
132
|
-
});
|
|
137
|
+
this.fileToDelete = null;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
/**
|
|
@@ -146,28 +151,20 @@ class ImportManager {
|
|
|
146
151
|
* Importable content must be found either in the root, or inside one base directory
|
|
147
152
|
*
|
|
148
153
|
* @param {String} directory
|
|
149
|
-
* @returns {
|
|
154
|
+
* @returns {boolean}
|
|
150
155
|
*/
|
|
151
156
|
isValidZip(directory) {
|
|
152
157
|
// Globs match content in the root or inside a single directory
|
|
153
|
-
const extMatchesBase = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_OR_SINGLE_DIR), {cwd: directory});
|
|
158
|
+
const extMatchesBase = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_OR_SINGLE_DIR), {cwd: directory, nocase: true});
|
|
154
159
|
|
|
155
160
|
const extMatchesAll = glob.sync(
|
|
156
|
-
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
|
|
161
|
+
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory, nocase: true}
|
|
157
162
|
);
|
|
158
163
|
|
|
159
164
|
const dirMatches = glob.sync(
|
|
160
165
|
this.getDirectoryGlob(this.getDirectories(), ROOT_OR_SINGLE_DIR), {cwd: directory}
|
|
161
166
|
);
|
|
162
167
|
|
|
163
|
-
const oldRoonMatches = glob.sync(this.getDirectoryGlob(['drafts', 'published', 'deleted'], ROOT_OR_SINGLE_DIR),
|
|
164
|
-
{cwd: directory});
|
|
165
|
-
|
|
166
|
-
// This is a temporary extra message for the old format roon export which doesn't work with Ghost
|
|
167
|
-
if (oldRoonMatches.length > 0) {
|
|
168
|
-
throw new errors.UnsupportedMediaTypeError({message: tpl(messages.unsupportedRoonExport)});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
168
|
// If this folder contains importable files or a content or images directory
|
|
172
169
|
if (extMatchesBase.length > 0 || (dirMatches.length > 0 && extMatchesAll.length > 0)) {
|
|
173
170
|
return true;
|
|
@@ -182,8 +179,8 @@ class ImportManager {
|
|
|
182
179
|
|
|
183
180
|
/**
|
|
184
181
|
* Use the extract module to extract the given zip file to a temp directory & return the temp directory path
|
|
185
|
-
* @param {
|
|
186
|
-
* @returns {Promise
|
|
182
|
+
* @param {string} filePath
|
|
183
|
+
* @returns {Promise<string>} full path to the extracted folder
|
|
187
184
|
*/
|
|
188
185
|
extractZip(filePath) {
|
|
189
186
|
const tmpDir = path.join(os.tmpdir(), uuid.v4());
|
|
@@ -199,11 +196,11 @@ class ImportManager {
|
|
|
199
196
|
* are relevant to the given handler, and return them as a name and path combo
|
|
200
197
|
* @param {Object} handler
|
|
201
198
|
* @param {String} directory
|
|
202
|
-
* @returns [] Files
|
|
199
|
+
* @returns {File[]} Files
|
|
203
200
|
*/
|
|
204
201
|
getFilesFromZip(handler, directory) {
|
|
205
202
|
const globPattern = this.getExtensionGlob(handler.extensions, ALL_DIRS);
|
|
206
|
-
return _.map(glob.sync(globPattern, {cwd: directory}), function (file) {
|
|
203
|
+
return _.map(glob.sync(globPattern, {cwd: directory, nocase: true}), function (file) {
|
|
207
204
|
return {name: file, path: path.join(directory, file)};
|
|
208
205
|
});
|
|
209
206
|
}
|
|
@@ -215,9 +212,9 @@ class ImportManager {
|
|
|
215
212
|
*/
|
|
216
213
|
getBaseDirectory(directory) {
|
|
217
214
|
// Globs match root level only
|
|
218
|
-
const extMatches = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_ONLY), {cwd: directory});
|
|
215
|
+
const extMatches = glob.sync(this.getExtensionGlob(this.getExtensions(), ROOT_ONLY), {cwd: directory, nocase: true});
|
|
219
216
|
|
|
220
|
-
const dirMatches = glob.sync(this.getDirectoryGlob(this.getDirectories(), ROOT_ONLY), {cwd: directory});
|
|
217
|
+
const dirMatches = glob.sync(this.getDirectoryGlob(this.getDirectories(), ROOT_ONLY), {cwd: directory, nocase: true});
|
|
221
218
|
let extMatchesAll;
|
|
222
219
|
|
|
223
220
|
// There is no base directory
|
|
@@ -226,9 +223,9 @@ class ImportManager {
|
|
|
226
223
|
}
|
|
227
224
|
// There is a base directory, grab it from any ext match
|
|
228
225
|
extMatchesAll = glob.sync(
|
|
229
|
-
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory}
|
|
226
|
+
this.getExtensionGlob(this.getExtensions(), ALL_DIRS), {cwd: directory, nocase: true}
|
|
230
227
|
);
|
|
231
|
-
if (extMatchesAll.length < 1 || extMatchesAll[0].split('/') < 1) {
|
|
228
|
+
if (extMatchesAll.length < 1 || extMatchesAll[0].split('/').length < 1) {
|
|
232
229
|
throw new errors.ValidationError({message: tpl(messages.invalidZipFileBaseDirectory)});
|
|
233
230
|
}
|
|
234
231
|
|
|
@@ -242,48 +239,42 @@ class ImportManager {
|
|
|
242
239
|
* The data key contains JSON representing any data that should be imported
|
|
243
240
|
* The image key contains references to images that will be stored (and where they will be stored)
|
|
244
241
|
* @param {File} file
|
|
245
|
-
* @returns {Promise
|
|
242
|
+
* @returns {Promise<ImportData>}
|
|
246
243
|
*/
|
|
247
|
-
processZip(file) {
|
|
248
|
-
const
|
|
244
|
+
async processZip(file) {
|
|
245
|
+
const zipDirectory = await this.extractZip(file.path);
|
|
249
246
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
247
|
+
/**
|
|
248
|
+
* @type {ImportData}
|
|
249
|
+
*/
|
|
250
|
+
const importData = {};
|
|
251
|
+
|
|
252
|
+
this.isValidZip(zipDirectory);
|
|
253
|
+
const baseDir = this.getBaseDirectory(zipDirectory);
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
for (const handler of this.handlers) {
|
|
256
|
+
const files = this.getFilesFromZip(handler, zipDirectory);
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
if (files.length > 0) {
|
|
259
259
|
if (Object.prototype.hasOwnProperty.call(importData, handler.type)) {
|
|
260
260
|
// This limitation is here to reduce the complexity of the importer for now
|
|
261
|
-
|
|
261
|
+
throw new errors.UnsupportedMediaTypeError({
|
|
262
262
|
message: tpl(messages.zipContainsMultipleDataFormats)
|
|
263
|
-
}));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const files = self.getFilesFromZip(handler, zipDirectory);
|
|
267
|
-
|
|
268
|
-
if (files.length > 0) {
|
|
269
|
-
ops.push(function () {
|
|
270
|
-
return handler.loadFile(files, baseDir).then(function (data) {
|
|
271
|
-
importData[handler.type] = data;
|
|
272
|
-
});
|
|
273
263
|
});
|
|
274
264
|
}
|
|
275
|
-
});
|
|
276
265
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
message: tpl(messages.noContentToImport)
|
|
280
|
-
}));
|
|
266
|
+
const data = await handler.loadFile(files, baseDir);
|
|
267
|
+
importData[handler.type] = data;
|
|
281
268
|
}
|
|
269
|
+
}
|
|
282
270
|
|
|
283
|
-
|
|
284
|
-
|
|
271
|
+
if (Object.keys(importData).length === 0) {
|
|
272
|
+
throw new errors.UnsupportedMediaTypeError({
|
|
273
|
+
message: tpl(messages.noContentToImport)
|
|
285
274
|
});
|
|
286
|
-
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return importData;
|
|
287
278
|
}
|
|
288
279
|
|
|
289
280
|
/**
|
|
@@ -293,7 +284,7 @@ class ImportManager {
|
|
|
293
284
|
* The data key contains JSON representing any data that should be imported
|
|
294
285
|
* The image key contains references to images that will be stored (and where they will be stored)
|
|
295
286
|
* @param {File} file
|
|
296
|
-
* @returns {Promise
|
|
287
|
+
* @returns {Promise<ImportData>}
|
|
297
288
|
*/
|
|
298
289
|
processFile(file, ext) {
|
|
299
290
|
const fileHandler = _.find(this.handlers, function (handler) {
|
|
@@ -313,7 +304,7 @@ class ImportManager {
|
|
|
313
304
|
* Load the given file into usable importData in the format: {data: {}, images: []}, regardless of
|
|
314
305
|
* whether the file is a single importable file like a JSON file, or a zip file containing loads of files.
|
|
315
306
|
* @param {File} file
|
|
316
|
-
* @returns {Promise}
|
|
307
|
+
* @returns {Promise<ImportData>}
|
|
317
308
|
*/
|
|
318
309
|
loadFile(file) {
|
|
319
310
|
const self = this;
|
|
@@ -326,17 +317,14 @@ class ImportManager {
|
|
|
326
317
|
* Pass the prepared importData through the preProcess function of the various importers, so that the importers can
|
|
327
318
|
* make any adjustments to the data based on relationships between it
|
|
328
319
|
* @param {ImportData} importData
|
|
329
|
-
* @returns {Promise
|
|
320
|
+
* @returns {Promise<ImportData>}
|
|
330
321
|
*/
|
|
331
|
-
preProcess(importData) {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return importer.preProcess(importData);
|
|
336
|
-
});
|
|
337
|
-
});
|
|
322
|
+
async preProcess(importData) {
|
|
323
|
+
for (const importer of this.importers) {
|
|
324
|
+
importData = importer.preProcess(importData);
|
|
325
|
+
}
|
|
338
326
|
|
|
339
|
-
return
|
|
327
|
+
return Promise.resolve(importData);
|
|
340
328
|
}
|
|
341
329
|
|
|
342
330
|
/**
|
|
@@ -344,65 +332,116 @@ class ImportManager {
|
|
|
344
332
|
* Each importer gets passed the data from importData which has the key matching its type - i.e. it only gets the
|
|
345
333
|
* data that it should import. Each importer then handles actually importing that data into Ghost
|
|
346
334
|
* @param {ImportData} importData
|
|
347
|
-
* @param {
|
|
348
|
-
* @returns {Promise<
|
|
335
|
+
* @param {ImportOptions} [importOptions] to allow override of certain import features such as locking a user
|
|
336
|
+
* @returns {Promise<ImportResult[]>} importResults
|
|
349
337
|
*/
|
|
350
|
-
doImport(importData, importOptions) {
|
|
338
|
+
async doImport(importData, importOptions) {
|
|
351
339
|
importOptions = importOptions || {};
|
|
352
|
-
const
|
|
353
|
-
|
|
340
|
+
const importResults = [];
|
|
341
|
+
|
|
342
|
+
for (const importer of this.importers) {
|
|
354
343
|
if (Object.prototype.hasOwnProperty.call(importData, importer.type)) {
|
|
355
|
-
|
|
356
|
-
return importer.doImport(importData[importer.type], importOptions);
|
|
357
|
-
});
|
|
344
|
+
importResults.push(await importer.doImport(importData[importer.type], importOptions));
|
|
358
345
|
}
|
|
359
|
-
}
|
|
346
|
+
}
|
|
360
347
|
|
|
361
|
-
return
|
|
362
|
-
return importResult;
|
|
363
|
-
});
|
|
348
|
+
return importResults;
|
|
364
349
|
}
|
|
365
350
|
|
|
366
351
|
/**
|
|
367
352
|
* Import Step 4:
|
|
368
353
|
* Report on what was imported, currently a no-op
|
|
369
|
-
* @param {
|
|
370
|
-
* @returns {Promise<
|
|
354
|
+
* @param {ImportResult[]} importResults
|
|
355
|
+
* @returns {Promise<ImportResult[]>} importResults
|
|
371
356
|
*/
|
|
372
|
-
generateReport(
|
|
373
|
-
return Promise.resolve(
|
|
357
|
+
async generateReport(importResults) {
|
|
358
|
+
return Promise.resolve(importResults);
|
|
374
359
|
}
|
|
375
360
|
|
|
376
361
|
/**
|
|
377
362
|
* Import From File
|
|
378
363
|
* The main method of the ImportManager, call this to kick everything off!
|
|
379
364
|
* @param {File} file
|
|
380
|
-
* @param {
|
|
381
|
-
* @returns {Promise}
|
|
365
|
+
* @param {ImportOptions} importOptions to allow override of certain import features such as locking a user
|
|
366
|
+
* @returns {Promise<ImportResult[]>}
|
|
382
367
|
*/
|
|
383
|
-
importFromFile(file, importOptions = {}) {
|
|
384
|
-
|
|
368
|
+
async importFromFile(file, importOptions = {}) {
|
|
369
|
+
try {
|
|
370
|
+
// Step 1: Handle converting the file to usable data
|
|
371
|
+
let importData = await this.loadFile(file);
|
|
385
372
|
|
|
386
|
-
// Step 1: Handle converting the file to usable data
|
|
387
|
-
return this.loadFile(file).then(function (importData) {
|
|
388
373
|
// Step 2: Let the importers pre-process the data
|
|
389
|
-
|
|
390
|
-
|
|
374
|
+
importData = await this.preProcess(importData);
|
|
375
|
+
|
|
391
376
|
// Step 3: Actually do the import
|
|
392
377
|
// @TODO: It would be cool to have some sort of dry run flag here
|
|
393
|
-
|
|
394
|
-
|
|
378
|
+
let importResult = await this.doImport(importData, importOptions);
|
|
379
|
+
|
|
395
380
|
// Step 4: Report on the import
|
|
396
|
-
return
|
|
397
|
-
}
|
|
381
|
+
return await this.generateReport(importResult);
|
|
382
|
+
} finally {
|
|
383
|
+
// Step 5: Cleanup any files
|
|
384
|
+
this.cleanUp();
|
|
385
|
+
}
|
|
398
386
|
}
|
|
399
387
|
}
|
|
400
388
|
|
|
401
389
|
/**
|
|
402
|
-
*
|
|
390
|
+
* @typedef {object} ImportOptions
|
|
391
|
+
* @property {boolean} [returnImportedData]
|
|
392
|
+
* @property {boolean} [importPersistUser]
|
|
393
|
+
*/
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @typedef {object} Importer
|
|
397
|
+
* @property {"images"|"data"} type
|
|
398
|
+
* @property {PreProcessMethod} preProcess
|
|
399
|
+
* @property {DoImportMethod} doImport
|
|
400
|
+
*/
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @callback PreProcessMethod
|
|
404
|
+
* @param {ImportData} importData
|
|
405
|
+
* @returns {ImportData}
|
|
406
|
+
*/
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @callback DoImportMethod
|
|
410
|
+
* @param {object|object[]} importData
|
|
411
|
+
* @param {ImportOptions} importOptions
|
|
412
|
+
* @returns {Promise<ImportResult>} import result
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @typedef {object} Handler
|
|
417
|
+
* @property {"images"|"data"} type
|
|
418
|
+
* @property {string[]} extensions
|
|
419
|
+
* @property {string[]} contentTypes
|
|
420
|
+
* @property {string[]} directories
|
|
421
|
+
* @property {LoadFileMethod} loadFile
|
|
422
|
+
*/
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @callback LoadFileMethod
|
|
426
|
+
* @param {File[]} files
|
|
427
|
+
* @param {string} [baseDir]
|
|
428
|
+
* @returns {Promise<object[]|object>} data
|
|
429
|
+
*/
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* File object
|
|
433
|
+
* @typedef {Object} File
|
|
434
|
+
* @property {string} name
|
|
435
|
+
* @property {string} path
|
|
436
|
+
*/
|
|
437
|
+
|
|
438
|
+
/**
|
|
403
439
|
* @typedef {Object} ImportData
|
|
404
|
-
* @property
|
|
405
|
-
* @property
|
|
440
|
+
* @property {Object} [data]
|
|
441
|
+
* @property {Array} [images]
|
|
406
442
|
*/
|
|
407
443
|
|
|
444
|
+
/**
|
|
445
|
+
* @typedef {Object} ImportResult
|
|
446
|
+
*/
|
|
408
447
|
module.exports = new ImportManager();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
2
3
|
|
|
3
4
|
module.exports = {
|
|
4
5
|
config: {
|
|
@@ -6,7 +7,7 @@ module.exports = {
|
|
|
6
7
|
},
|
|
7
8
|
|
|
8
9
|
async up({transacting: knex}) {
|
|
9
|
-
if (knex
|
|
10
|
+
if (!DatabaseInfo.isMySQL(knex)) {
|
|
10
11
|
logging.warn('Skipping cleanup of duplicate subscriptions - database is not MySQL');
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
2
3
|
|
|
3
4
|
module.exports = {
|
|
4
5
|
config: {
|
|
@@ -6,7 +7,7 @@ module.exports = {
|
|
|
6
7
|
},
|
|
7
8
|
|
|
8
9
|
async up({transacting: knex}) {
|
|
9
|
-
if (knex
|
|
10
|
+
if (!DatabaseInfo.isMySQL(knex)) {
|
|
10
11
|
logging.warn('Skipping cleanup of duplicate customers - database is not MySQL');
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
2
3
|
|
|
3
4
|
module.exports = {
|
|
4
5
|
config: {
|
|
@@ -6,7 +7,7 @@ module.exports = {
|
|
|
6
7
|
},
|
|
7
8
|
|
|
8
9
|
async up({transacting: knex}) {
|
|
9
|
-
if (knex
|
|
10
|
+
if (!DatabaseInfo.isMySQL(knex)) {
|
|
10
11
|
logging.warn('Skipping cleanup of orphaned customers - database is not MySQL');
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
2
3
|
|
|
3
4
|
module.exports = {
|
|
4
5
|
config: {
|
|
@@ -6,7 +7,7 @@ module.exports = {
|
|
|
6
7
|
},
|
|
7
8
|
|
|
8
9
|
async up({transacting: knex}) {
|
|
9
|
-
if (knex
|
|
10
|
+
if (!DatabaseInfo.isMySQL(knex)) {
|
|
10
11
|
logging.warn('Skipping cleanup of orphaned subscriptions - database is not MySQL');
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
2
3
|
|
|
3
4
|
module.exports = {
|
|
4
5
|
config: {
|
|
@@ -6,7 +7,7 @@ module.exports = {
|
|
|
6
7
|
},
|
|
7
8
|
|
|
8
9
|
async up({transacting: knex}) {
|
|
9
|
-
if (knex
|
|
10
|
+
if (!DatabaseInfo.isMySQL(knex)) {
|
|
10
11
|
return logging.warn('Skipping member tables index creation - database is not MySQL');
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -91,7 +92,7 @@ module.exports = {
|
|
|
91
92
|
},
|
|
92
93
|
|
|
93
94
|
async down({transacting: knex}) {
|
|
94
|
-
if (knex
|
|
95
|
+
if (!DatabaseInfo.isMySQL(knex)) {
|
|
95
96
|
return logging.warn('Skipping member tables index removal - database is not MySQL');
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
2
|
const {createNonTransactionalMigration} = require('../../utils');
|
|
3
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
3
4
|
|
|
4
5
|
module.exports = createNonTransactionalMigration(
|
|
5
6
|
async function up(knex) {
|
|
6
7
|
let hasIndex = false;
|
|
7
8
|
|
|
8
|
-
if (knex
|
|
9
|
+
if (DatabaseInfo.isSQLite(knex)) {
|
|
9
10
|
const result = await knex.raw(`select * from sqlite_master where type = 'index' and tbl_name = 'email_recipients' and name = 'email_recipients_email_id_member_email_index'`);
|
|
10
11
|
hasIndex = result.length !== 0;
|
|
11
12
|
} else {
|
|
@@ -27,7 +28,7 @@ module.exports = createNonTransactionalMigration(
|
|
|
27
28
|
async function down(knex) {
|
|
28
29
|
let missingIndex = false;
|
|
29
30
|
|
|
30
|
-
if (knex
|
|
31
|
+
if (DatabaseInfo.isSQLite(knex)) {
|
|
31
32
|
const result = await knex.raw(`select * from sqlite_master where type = 'index' and tbl_name = 'email_recipients' and name = 'email_recipients_email_id_member_email_index'`);
|
|
32
33
|
missingIndex = result.length === 0;
|
|
33
34
|
} else {
|
|
@@ -42,7 +43,7 @@ module.exports = createNonTransactionalMigration(
|
|
|
42
43
|
|
|
43
44
|
logging.info('Dropping composite index on email_recipients for [email_id, member_email]');
|
|
44
45
|
|
|
45
|
-
if (knex
|
|
46
|
+
if (DatabaseInfo.isMySQL(knex)) {
|
|
46
47
|
await knex.schema.table('email_recipients', (table) => {
|
|
47
48
|
table.dropForeign('email_id');
|
|
48
49
|
table.dropIndex(['email_id', 'member_email']);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const {createIrreversibleMigration} = require('../../utils');
|
|
2
2
|
const logging = require('@tryghost/logging');
|
|
3
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
3
4
|
|
|
4
5
|
module.exports = createIrreversibleMigration(async function up(connection) {
|
|
5
|
-
if (connection
|
|
6
|
+
if (DatabaseInfo.isMySQL(connection)) {
|
|
6
7
|
logging.info('Skipping removal of orphaned stripe records for MySQL');
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
2
|
const {createIrreversibleMigration} = require('../../utils');
|
|
3
3
|
const {addForeign, dropForeign} = require('../../../schema/commands');
|
|
4
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
4
5
|
|
|
5
6
|
module.exports = createIrreversibleMigration(async (knex) => {
|
|
6
|
-
if (knex
|
|
7
|
+
if (!DatabaseInfo.isSQLite(knex)) {
|
|
7
8
|
return logging.warn('Skipping adding "on delete cascade" - database is not SQLite3');
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
2
|
const {createIrreversibleMigration} = require('../../utils');
|
|
3
3
|
const {addForeign, dropForeign} = require('../../../schema/commands');
|
|
4
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
4
5
|
|
|
5
6
|
module.exports = createIrreversibleMigration(async (knex) => {
|
|
6
|
-
if (knex
|
|
7
|
+
if (!DatabaseInfo.isSQLite(knex)) {
|
|
7
8
|
return logging.warn('Skipping fixing foreign key for members_stripe_customers_subscriptions - database is not SQLite3');
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
2
|
const {createTransactionalMigration} = require('../../utils');
|
|
3
3
|
const {addUnique} = require('../../../schema/commands');
|
|
4
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
4
5
|
|
|
5
6
|
module.exports = createTransactionalMigration(
|
|
6
7
|
async function up(connection) {
|
|
7
|
-
if (connection
|
|
8
|
+
if (!DatabaseInfo.isSQLite(connection)) {
|
|
8
9
|
return logging.warn('Skipping adding unique constraint for members_stripe_customers_subscriptions and members_stripe_customers - database is not SQLite3');
|
|
9
10
|
}
|
|
10
11
|
|
package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const logging = require('@tryghost/logging');
|
|
2
2
|
const {createNonTransactionalMigration} = require('../../utils');
|
|
3
3
|
const {addUnique} = require('../../../schema/commands');
|
|
4
|
+
const DatabaseInfo = require('@tryghost/database-info');
|
|
4
5
|
|
|
5
6
|
module.exports = createNonTransactionalMigration(
|
|
6
7
|
async function up(knex) {
|
|
@@ -14,7 +15,7 @@ module.exports = createNonTransactionalMigration(
|
|
|
14
15
|
table.string('portal_title', 191).nullable();
|
|
15
16
|
});
|
|
16
17
|
|
|
17
|
-
if (knex
|
|
18
|
+
if (DatabaseInfo.isSQLite(knex)) {
|
|
18
19
|
// eslint-disable-next-line no-restricted-syntax
|
|
19
20
|
for (const column of ['name', 'code', 'stripe_coupon_id']) {
|
|
20
21
|
await addUnique('offers', column, knex);
|
|
@@ -32,7 +33,7 @@ module.exports = createNonTransactionalMigration(
|
|
|
32
33
|
table.string('portal_title', 191).notNullable();
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
if (knex
|
|
36
|
+
if (DatabaseInfo.isSQLite(knex)) {
|
|
36
37
|
// eslint-disable-next-line no-restricted-syntax
|
|
37
38
|
for (const column of ['name', 'code', 'stripe_coupon_id']) {
|
|
38
39
|
await addUnique('offers', column, knex);
|