@automattic/vip 3.6.0 → 3.7.1
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/assets/dev-env.lando.template.yml.ejs +2 -0
- package/dist/bin/vip-import-validate-files.js +74 -79
- package/dist/commands/dev-env-import-sql.js +2 -4
- package/dist/commands/dev-env-sync-sql.js +36 -35
- package/dist/commands/export-sql.js +60 -56
- package/dist/lib/constants/dev-environment.js +1 -1
- package/dist/lib/media-import/config.js +25 -0
- package/dist/lib/vip-import-validate-files.js +157 -36
- package/docs/CHANGELOG.md +14 -0
- package/npm-shrinkwrap.json +494 -799
- package/package.json +7 -7
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* External dependencies
|
|
5
|
+
*/
|
|
2
6
|
"use strict";
|
|
3
7
|
|
|
8
|
+
exports.__esModule = true;
|
|
9
|
+
exports.vipImportValidateFilesCmd = vipImportValidateFilesCmd;
|
|
4
10
|
var _chalk = _interopRequireDefault(require("chalk"));
|
|
5
|
-
var _fs = _interopRequireDefault(require("fs"));
|
|
6
|
-
var _path = _interopRequireDefault(require("path"));
|
|
7
11
|
var _url = _interopRequireDefault(require("url"));
|
|
8
12
|
var _command = _interopRequireDefault(require("../lib/cli/command"));
|
|
13
|
+
var _config = require("../lib/media-import/config");
|
|
9
14
|
var _tracker = require("../lib/tracker");
|
|
10
15
|
var _vipImportValidateFiles = require("../lib/vip-import-validate-files");
|
|
11
16
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Internal dependencies
|
|
19
|
+
*/
|
|
20
|
+
async function vipImportValidateFilesCmd(arg = []) {
|
|
16
21
|
await (0, _tracker.trackEvent)('import_validate_files_command_execute');
|
|
17
22
|
/**
|
|
18
23
|
* File manipulation
|
|
@@ -23,7 +28,11 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
23
28
|
arg = _url.default.parse(folder); // Then parse the file to its URL parts
|
|
24
29
|
const filePath = arg.path; // Extract the path of the file
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
if (!(await (0, _vipImportValidateFiles.isDirectory)(filePath))) {
|
|
32
|
+
console.error(_chalk.default.red('✕ Error:'), 'The given path is not a directory, please provide a valid directory path.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let folderValidation = [];
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* Folder structure validation
|
|
@@ -33,6 +42,11 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
33
42
|
* Recommended structure: `uploads/year/month` (Single sites)
|
|
34
43
|
*/
|
|
35
44
|
const nestedFiles = (0, _vipImportValidateFiles.findNestedDirectories)(filePath);
|
|
45
|
+
|
|
46
|
+
// Terminates the command here if no nested files found
|
|
47
|
+
if (!nestedFiles) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
36
50
|
const {
|
|
37
51
|
files,
|
|
38
52
|
folderStructureObj
|
|
@@ -56,90 +70,64 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
/**
|
|
59
|
-
* Media
|
|
60
|
-
*
|
|
61
|
-
* Ensure that prohibited media file types are not used
|
|
73
|
+
* Get Media Import configuration
|
|
62
74
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const errorFileNames = [];
|
|
68
|
-
const intermediateImages = {};
|
|
69
|
-
|
|
70
|
-
// Iterate through each file to isolate the extension name
|
|
71
|
-
for (const file of files) {
|
|
72
|
-
// Check if file is a directory
|
|
73
|
-
// eslint-disable-next-line no-await-in-loop
|
|
74
|
-
const stats = await _fs.default.promises.stat(file);
|
|
75
|
-
const isFolder = stats.isDirectory();
|
|
76
|
-
const extension = _path.default.extname(file); // Extract the extension of the file
|
|
77
|
-
const ext = extension.substr(1); // We only want the ext name minus the period (e.g- .jpg -> jpg)
|
|
78
|
-
const extLowerCase = ext.toLowerCase(); // Change any uppercase extensions to lowercase
|
|
79
|
-
|
|
80
|
-
// Check for any invalid file extensions
|
|
81
|
-
// Returns true if ext is valid; false if invalid
|
|
82
|
-
const validExtensions = _vipImportValidateFiles.acceptedExtensions.includes(extLowerCase);
|
|
83
|
-
|
|
84
|
-
// Collect files that have no extension, have invalid extensions,
|
|
85
|
-
// or are directories for error logging
|
|
86
|
-
if (!extension || !validExtensions || isFolder) {
|
|
87
|
-
errorFileTypes.push(file);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Filename validation
|
|
92
|
-
*
|
|
93
|
-
* Ensure that filenames don't contain prohibited characters
|
|
94
|
-
*/
|
|
95
|
-
|
|
96
|
-
// Collect files that have invalid file names for error logging
|
|
97
|
-
if ((0, _vipImportValidateFiles.isFileSanitized)(file)) {
|
|
98
|
-
errorFileNames.push(file);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Intermediate image validation
|
|
103
|
-
*
|
|
104
|
-
* Detect any intermediate images.
|
|
105
|
-
*
|
|
106
|
-
* Intermediate images are copies of images that are resized, so you may have multiples of the same image.
|
|
107
|
-
* You can resize an image directly on VIP so intermediate images are not necessary.
|
|
108
|
-
*/
|
|
109
|
-
const original = (0, _vipImportValidateFiles.doesImageHaveExistingSource)(file);
|
|
110
|
-
|
|
111
|
-
// If an image is an intermediate image, increment the total number and
|
|
112
|
-
// populate key/value pairs of the original image and intermediate image(s)
|
|
113
|
-
if (original) {
|
|
114
|
-
intermediateImagesTotal++;
|
|
115
|
-
if (intermediateImages[original]) {
|
|
116
|
-
// Key: original image, value: intermediate image(s)
|
|
117
|
-
intermediateImages[original] = `${intermediateImages[original]}, ${file}`;
|
|
118
|
-
} else {
|
|
119
|
-
intermediateImages[original] = file;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
75
|
+
const mediaImportConfig = await (0, _config.getMediaImportConfig)();
|
|
76
|
+
if (!mediaImportConfig) {
|
|
77
|
+
console.error(_chalk.default.red('✕ Error:'), 'Could not retrieve validation metadata. Please contact VIP Support.');
|
|
78
|
+
return;
|
|
122
79
|
}
|
|
123
80
|
|
|
81
|
+
/**
|
|
82
|
+
* File Validation
|
|
83
|
+
* Collect all errors from file validation
|
|
84
|
+
*/
|
|
85
|
+
const {
|
|
86
|
+
intermediateImagesTotal,
|
|
87
|
+
errorFileTypes,
|
|
88
|
+
errorFileNames,
|
|
89
|
+
errorFileSizes,
|
|
90
|
+
errorFileNamesCharCount,
|
|
91
|
+
intermediateImages
|
|
92
|
+
} = await (0, _vipImportValidateFiles.validateFiles)(files, mediaImportConfig);
|
|
93
|
+
|
|
124
94
|
/**
|
|
125
95
|
* Error logging
|
|
96
|
+
* Not sure if the changes made to the error logging better
|
|
126
97
|
*/
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
98
|
+
(0, _vipImportValidateFiles.logErrors)({
|
|
99
|
+
errorType: _vipImportValidateFiles.ValidateFilesErrors.INVALID_TYPES,
|
|
100
|
+
invalidFiles: errorFileTypes,
|
|
101
|
+
limit: Object.keys(mediaImportConfig.allowedFileTypes)
|
|
102
|
+
});
|
|
103
|
+
(0, _vipImportValidateFiles.logErrors)({
|
|
104
|
+
errorType: _vipImportValidateFiles.ValidateFilesErrors.INVALID_SIZES,
|
|
105
|
+
invalidFiles: errorFileSizes,
|
|
106
|
+
limit: mediaImportConfig.fileSizeLimitInBytes
|
|
107
|
+
});
|
|
108
|
+
(0, _vipImportValidateFiles.logErrors)({
|
|
109
|
+
errorType: _vipImportValidateFiles.ValidateFilesErrors.INVALID_NAME_CHARACTER_COUNTS,
|
|
110
|
+
invalidFiles: errorFileNamesCharCount,
|
|
111
|
+
limit: mediaImportConfig.fileNameCharCount
|
|
112
|
+
});
|
|
113
|
+
(0, _vipImportValidateFiles.logErrors)({
|
|
114
|
+
errorType: _vipImportValidateFiles.ValidateFilesErrors.INVALID_NAMES,
|
|
115
|
+
invalidFiles: errorFileNames
|
|
116
|
+
});
|
|
117
|
+
(0, _vipImportValidateFiles.logErrors)({
|
|
118
|
+
errorType: _vipImportValidateFiles.ValidateFilesErrors.INTERMEDIATE_IMAGES,
|
|
119
|
+
invalidFiles: Object.keys(intermediateImages),
|
|
120
|
+
invalidFilesObj: intermediateImages
|
|
121
|
+
});
|
|
136
122
|
|
|
137
123
|
// Log a summary of all errors
|
|
138
124
|
(0, _vipImportValidateFiles.summaryLogs)({
|
|
139
125
|
folderErrorsLength: folderValidation.length,
|
|
140
126
|
intImagesErrorsLength: intermediateImagesTotal,
|
|
141
127
|
fileTypeErrorsLength: errorFileTypes.length,
|
|
128
|
+
fileErrorFileSizesLength: errorFileSizes.length,
|
|
142
129
|
filenameErrorsLength: errorFileNames.length,
|
|
130
|
+
fileNameCharCountErrorsLength: errorFileNamesCharCount.length,
|
|
143
131
|
totalFiles: files.length,
|
|
144
132
|
totalFolders: nestedDirectories.length
|
|
145
133
|
});
|
|
@@ -158,4 +146,11 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
158
146
|
/* eslint-enable camelcase */
|
|
159
147
|
|
|
160
148
|
await (0, _tracker.trackEvent)('import_validate_files_command_success', allErrors);
|
|
161
|
-
}
|
|
149
|
+
}
|
|
150
|
+
(0, _command.default)({
|
|
151
|
+
requiredArgs: 1,
|
|
152
|
+
format: true
|
|
153
|
+
}).examples([{
|
|
154
|
+
usage: 'vip import validate-files <folder_name>',
|
|
155
|
+
description: 'Run the import validation against the folder of media files'
|
|
156
|
+
}]).argv(process.argv, vipImportValidateFilesCmd);
|
|
@@ -16,9 +16,6 @@ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return
|
|
|
16
16
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
17
17
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
18
|
class DevEnvImportSQLCommand {
|
|
19
|
-
fileName;
|
|
20
|
-
options;
|
|
21
|
-
slug;
|
|
22
19
|
constructor(fileName, options, slug) {
|
|
23
20
|
this.fileName = fileName;
|
|
24
21
|
this.options = options;
|
|
@@ -43,7 +40,8 @@ class DevEnvImportSQLCommand {
|
|
|
43
40
|
console.log(`${_chalk.default.green('✓')} Extracted to ${sqlFile}`);
|
|
44
41
|
}
|
|
45
42
|
this.fileName = sqlFile;
|
|
46
|
-
} catch (
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const err = error;
|
|
47
45
|
exit.withError(`Error extracting the SQL file: ${err.message}`);
|
|
48
46
|
}
|
|
49
47
|
}
|
|
@@ -20,8 +20,8 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
20
20
|
/**
|
|
21
21
|
* Finds the site home url from the SQL line
|
|
22
22
|
*
|
|
23
|
-
* @param
|
|
24
|
-
* @return
|
|
23
|
+
* @param sql A line in a SQL file
|
|
24
|
+
* @return Site home url. null if not found
|
|
25
25
|
*/
|
|
26
26
|
function findSiteHomeUrl(sql) {
|
|
27
27
|
const regex = "'(siteurl|home)',\\s?'(.*?)'";
|
|
@@ -32,8 +32,8 @@ function findSiteHomeUrl(sql) {
|
|
|
32
32
|
/**
|
|
33
33
|
* Extracts a list of site urls from the SQL file
|
|
34
34
|
*
|
|
35
|
-
* @param
|
|
36
|
-
* @return
|
|
35
|
+
* @param sqlFile Path to the SQL file
|
|
36
|
+
* @return List of site urls
|
|
37
37
|
* @throws {Error} If there is an error reading the file
|
|
38
38
|
*/
|
|
39
39
|
async function extractSiteUrls(sqlFile) {
|
|
@@ -54,23 +54,19 @@ async function extractSiteUrls(sqlFile) {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
class DevEnvSyncSQLCommand {
|
|
57
|
-
app;
|
|
58
|
-
env;
|
|
59
|
-
slug;
|
|
60
|
-
lando;
|
|
61
57
|
tmpDir;
|
|
62
|
-
siteUrls;
|
|
63
|
-
searchReplaceMap;
|
|
58
|
+
siteUrls = [];
|
|
59
|
+
searchReplaceMap = {};
|
|
64
60
|
track;
|
|
65
61
|
|
|
66
62
|
/**
|
|
67
63
|
* Creates a new instance of the command
|
|
68
64
|
*
|
|
69
|
-
* @param
|
|
70
|
-
* @param
|
|
71
|
-
* @param
|
|
72
|
-
* @param
|
|
73
|
-
* @param
|
|
65
|
+
* @param app The app object
|
|
66
|
+
* @param env The environment object
|
|
67
|
+
* @param slug The site slug
|
|
68
|
+
* @param lando The lando object
|
|
69
|
+
* @param trackerFn Function to call for tracking
|
|
74
70
|
*/
|
|
75
71
|
constructor(app, env, slug, lando, trackerFn = () => {}) {
|
|
76
72
|
this.app = app;
|
|
@@ -132,12 +128,12 @@ class DevEnvSyncSQLCommand {
|
|
|
132
128
|
for (const url of this.siteUrls) {
|
|
133
129
|
this.searchReplaceMap[url] = this.landoDomain;
|
|
134
130
|
}
|
|
135
|
-
const networkSites = this.env.wpSitesSDS
|
|
131
|
+
const networkSites = this.env.wpSitesSDS?.nodes;
|
|
136
132
|
if (!networkSites) return;
|
|
137
133
|
for (const site of networkSites) {
|
|
138
|
-
if (!site
|
|
139
|
-
const url = site
|
|
140
|
-
if (!this.searchReplaceMap[url]) continue;
|
|
134
|
+
if (!site?.blogId || site.blogId === 1) continue;
|
|
135
|
+
const url = site?.homeUrl?.replace(/https?:\/\//, '');
|
|
136
|
+
if (!url || !this.searchReplaceMap[url]) continue;
|
|
141
137
|
this.searchReplaceMap[url] = `${this.slugifyDomain(url)}-${site.blogId}.${this.landoDomain}`;
|
|
142
138
|
}
|
|
143
139
|
}
|
|
@@ -171,44 +167,47 @@ class DevEnvSyncSQLCommand {
|
|
|
171
167
|
* Sequentially runs the commands to export, search-replace, and import the SQL file
|
|
172
168
|
* to the local environment
|
|
173
169
|
*
|
|
174
|
-
* @return
|
|
170
|
+
* @return Promise that resolves to true when the commands are complete. It will return false if the user did not continue during validation prompts.
|
|
175
171
|
*/
|
|
176
172
|
async run() {
|
|
177
173
|
try {
|
|
178
174
|
await this.generateExport();
|
|
179
175
|
} catch (err) {
|
|
176
|
+
const error = err;
|
|
180
177
|
// this.generateExport probably catches all exceptions, track the event and runs exit.withError() but if things go really wrong
|
|
181
178
|
// and we have no tracking data, we would at least have it logged here.
|
|
182
179
|
// the following will not get executed if this.generateExport() calls exit.withError() on all exception
|
|
183
180
|
await this.track('error', {
|
|
184
181
|
error_type: 'export_sql_backup',
|
|
185
|
-
error_message:
|
|
186
|
-
stack:
|
|
182
|
+
error_message: error.message,
|
|
183
|
+
stack: error.stack
|
|
187
184
|
});
|
|
188
|
-
exit.withError(`Error exporting SQL backup: ${
|
|
185
|
+
exit.withError(`Error exporting SQL backup: ${error.message}`);
|
|
189
186
|
}
|
|
190
187
|
try {
|
|
191
188
|
console.log(`Extracting the exported file ${this.gzFile}...`);
|
|
192
189
|
await (0, _clientFileUploader.unzipFile)(this.gzFile, this.sqlFile);
|
|
193
190
|
console.log(`${_chalk.default.green('✓')} Extracted to ${this.sqlFile}`);
|
|
194
191
|
} catch (err) {
|
|
192
|
+
const error = err;
|
|
195
193
|
await this.track('error', {
|
|
196
194
|
error_type: 'archive_extraction',
|
|
197
|
-
error_message:
|
|
198
|
-
stack:
|
|
195
|
+
error_message: error.message,
|
|
196
|
+
stack: error.stack
|
|
199
197
|
});
|
|
200
|
-
exit.withError(`Error extracting the SQL export: ${
|
|
198
|
+
exit.withError(`Error extracting the SQL export: ${error.message}`);
|
|
201
199
|
}
|
|
202
200
|
try {
|
|
203
201
|
console.log('Extracting site urls from the SQL file...');
|
|
204
202
|
this.siteUrls = await extractSiteUrls(this.sqlFile);
|
|
205
203
|
} catch (err) {
|
|
204
|
+
const error = err;
|
|
206
205
|
await this.track('error', {
|
|
207
206
|
error_type: 'extract_site_urls',
|
|
208
|
-
error_message:
|
|
209
|
-
stack:
|
|
207
|
+
error_message: error.message,
|
|
208
|
+
stack: error.stack
|
|
210
209
|
});
|
|
211
|
-
exit.withError(`Error extracting site URLs: ${
|
|
210
|
+
exit.withError(`Error extracting site URLs: ${error.message}`);
|
|
212
211
|
}
|
|
213
212
|
console.log('Generating search-replace configuration...');
|
|
214
213
|
this.generateSearchReplaceMap();
|
|
@@ -220,12 +219,13 @@ class DevEnvSyncSQLCommand {
|
|
|
220
219
|
await this.runSearchReplace();
|
|
221
220
|
console.log(`${_chalk.default.green('✓')} Search-replace operation is complete`);
|
|
222
221
|
} catch (err) {
|
|
222
|
+
const error = err;
|
|
223
223
|
await this.track('error', {
|
|
224
224
|
error_type: 'search_replace',
|
|
225
|
-
error_message:
|
|
226
|
-
stack:
|
|
225
|
+
error_message: error.message,
|
|
226
|
+
stack: error.stack
|
|
227
227
|
});
|
|
228
|
-
exit.withError(`Error replacing domains: ${
|
|
228
|
+
exit.withError(`Error replacing domains: ${error.message}`);
|
|
229
229
|
}
|
|
230
230
|
try {
|
|
231
231
|
console.log('Importing the SQL file...');
|
|
@@ -233,12 +233,13 @@ class DevEnvSyncSQLCommand {
|
|
|
233
233
|
console.log(`${_chalk.default.green('✓')} SQL file imported`);
|
|
234
234
|
return true;
|
|
235
235
|
} catch (err) {
|
|
236
|
+
const error = err;
|
|
236
237
|
await this.track('error', {
|
|
237
238
|
error_type: 'import_sql_file',
|
|
238
|
-
error_message:
|
|
239
|
-
stack:
|
|
239
|
+
error_message: error.message,
|
|
240
|
+
stack: error.stack
|
|
240
241
|
});
|
|
241
|
-
exit.withError(`Error importing SQL file: ${
|
|
242
|
+
exit.withError(`Error importing SQL file: ${error.message}`);
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
}
|
|
@@ -94,21 +94,18 @@ async function fetchLatestBackupAndJobStatusBase(appId, envId) {
|
|
|
94
94
|
},
|
|
95
95
|
fetchPolicy: 'network-only'
|
|
96
96
|
});
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
environments
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
} = response;
|
|
104
|
-
const latestBackup = environments[0].latestBackup;
|
|
105
|
-
const jobs = environments[0].jobs;
|
|
97
|
+
const environments = response.data.app?.environments;
|
|
98
|
+
const latestBackup = environments?.[0]?.latestBackup;
|
|
99
|
+
const jobs = environments?.[0]?.jobs || [];
|
|
106
100
|
return {
|
|
107
101
|
latestBackup,
|
|
108
102
|
jobs
|
|
109
103
|
};
|
|
110
104
|
}
|
|
111
105
|
async function fetchLatestBackupAndJobStatus(appId, envId) {
|
|
106
|
+
if (!appId || !envId) {
|
|
107
|
+
throw new Error('App ID and Env ID missing');
|
|
108
|
+
}
|
|
112
109
|
return await (0, _retry.retry)({
|
|
113
110
|
retryOnlyIf: options => {
|
|
114
111
|
return (options.error.message || '').indexOf('Unexpected token < in JSON at position 0') !== -1;
|
|
@@ -125,6 +122,9 @@ async function fetchLatestBackupAndJobStatus(appId, envId) {
|
|
|
125
122
|
* @return {Promise} A promise which resolves to the download link
|
|
126
123
|
*/
|
|
127
124
|
async function generateDownloadLink(appId, envId, backupId) {
|
|
125
|
+
if (!appId || !envId || !backupId) {
|
|
126
|
+
throw new Error('generateDownloadLink: A parameter is missing');
|
|
127
|
+
}
|
|
128
128
|
const api = (0, _api.default)();
|
|
129
129
|
const response = await api.mutate({
|
|
130
130
|
mutation: GENERATE_DOWNLOAD_LINK_MUTATION,
|
|
@@ -136,14 +136,7 @@ async function generateDownloadLink(appId, envId, backupId) {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
});
|
|
139
|
-
|
|
140
|
-
data: {
|
|
141
|
-
generateDBBackupCopyUrl: {
|
|
142
|
-
url
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} = response;
|
|
146
|
-
return url;
|
|
139
|
+
return response.data?.generateDBBackupCopyUrl?.url;
|
|
147
140
|
}
|
|
148
141
|
|
|
149
142
|
/**
|
|
@@ -152,10 +145,14 @@ async function generateDownloadLink(appId, envId, backupId) {
|
|
|
152
145
|
* @param {number} appId Application ID
|
|
153
146
|
* @param {number} envId Environment ID
|
|
154
147
|
* @param {number} backupId Backup ID
|
|
155
|
-
* @return {Promise} A promise which resolves to
|
|
148
|
+
* @return {Promise} A promise which resolves to undefined if job creation succeeds
|
|
156
149
|
* @throws {Error} Throws an error if the job creation fails
|
|
157
150
|
*/
|
|
158
151
|
async function createExportJob(appId, envId, backupId) {
|
|
152
|
+
if (!appId || !envId || !backupId) {
|
|
153
|
+
throw new Error('createExportJob: Some fields are undefined');
|
|
154
|
+
}
|
|
155
|
+
|
|
159
156
|
// Disable global error handling so that we can handle errors ourselves
|
|
160
157
|
(0, _api.disableGlobalGraphQLErrorHandling)();
|
|
161
158
|
const api = (0, _api.default)();
|
|
@@ -173,14 +170,10 @@ async function createExportJob(appId, envId, backupId) {
|
|
|
173
170
|
// Re-enable global error handling
|
|
174
171
|
(0, _api.enableGlobalGraphQLErrorHandling)();
|
|
175
172
|
}
|
|
176
|
-
|
|
177
173
|
/**
|
|
178
174
|
* Class representing an export command workflow
|
|
179
175
|
*/
|
|
180
176
|
class ExportSQLCommand {
|
|
181
|
-
app;
|
|
182
|
-
env;
|
|
183
|
-
downloadLink;
|
|
184
177
|
progressTracker;
|
|
185
178
|
outputFile;
|
|
186
179
|
generateBackup;
|
|
@@ -237,10 +230,13 @@ class ExportSQLCommand {
|
|
|
237
230
|
latestBackup,
|
|
238
231
|
jobs
|
|
239
232
|
} = await fetchLatestBackupAndJobStatus(this.app.id, this.env.id);
|
|
233
|
+
if (!latestBackup) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
240
236
|
|
|
241
237
|
// Find the job that generates the export for the latest backup
|
|
242
238
|
return jobs.find(job => {
|
|
243
|
-
const metadata = job.metadata.find(md => md
|
|
239
|
+
const metadata = (job.metadata || []).find(md => md?.name === 'backupId');
|
|
244
240
|
return metadata && parseInt(metadata.value, 10) === latestBackup.id;
|
|
245
241
|
});
|
|
246
242
|
}
|
|
@@ -248,12 +244,15 @@ class ExportSQLCommand {
|
|
|
248
244
|
/**
|
|
249
245
|
* Fetches the S3 filename of the exported backup
|
|
250
246
|
*
|
|
251
|
-
* @return
|
|
247
|
+
* @return A promise which resolves to the filename
|
|
252
248
|
*/
|
|
253
249
|
async getExportedFileName() {
|
|
254
250
|
const job = await this.getExportJob();
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
if (!job) {
|
|
252
|
+
throw new Error('Job not found');
|
|
253
|
+
}
|
|
254
|
+
const metadata = job.metadata?.find(md => md?.name === 'uploadPath');
|
|
255
|
+
return metadata?.value?.split('/')[1];
|
|
257
256
|
}
|
|
258
257
|
|
|
259
258
|
/**
|
|
@@ -276,7 +275,9 @@ class ExportSQLCommand {
|
|
|
276
275
|
resolve(_path.default.resolve(file.path));
|
|
277
276
|
});
|
|
278
277
|
file.on('error', err => {
|
|
279
|
-
|
|
278
|
+
// TODO: fs.unlink runs in the background so there's a chance that the app dies before it finishes.
|
|
279
|
+
// This needs fixing.
|
|
280
|
+
_fs.default.unlink(filename, () => null);
|
|
280
281
|
reject(err);
|
|
281
282
|
});
|
|
282
283
|
response.on('data', chunk => {
|
|
@@ -290,29 +291,27 @@ class ExportSQLCommand {
|
|
|
290
291
|
/**
|
|
291
292
|
* Checks if the export job's preflight step is successful
|
|
292
293
|
*
|
|
293
|
-
* @param
|
|
294
|
-
* @return
|
|
294
|
+
* @param job The export job
|
|
295
|
+
* @return True if the preflight step is successful
|
|
295
296
|
*/
|
|
296
297
|
isPrepared(job) {
|
|
297
|
-
const step = job?.progress
|
|
298
|
+
const step = job?.progress?.steps?.find(st => st?.id === 'preflight');
|
|
298
299
|
return step?.status === 'success';
|
|
299
300
|
}
|
|
300
301
|
|
|
301
302
|
/**
|
|
302
303
|
* Checks if the export job's S3 upload step is successful
|
|
303
304
|
*
|
|
304
|
-
* @param
|
|
305
|
-
* @return
|
|
305
|
+
* @param job The export job
|
|
306
|
+
* @return True if the upload step is successful
|
|
306
307
|
*/
|
|
307
308
|
isCreated(job) {
|
|
308
|
-
const step = job?.progress
|
|
309
|
+
const step = job?.progress?.steps?.find(st => st?.id === 'upload_backup');
|
|
309
310
|
return step?.status === 'success';
|
|
310
311
|
}
|
|
311
312
|
|
|
312
313
|
/**
|
|
313
314
|
* Stops the progress tracker
|
|
314
|
-
*
|
|
315
|
-
* @return {void}
|
|
316
315
|
*/
|
|
317
316
|
stopProgressTracker() {
|
|
318
317
|
this.progressTracker.print();
|
|
@@ -327,6 +326,9 @@ class ExportSQLCommand {
|
|
|
327
326
|
await cmd.run(false);
|
|
328
327
|
}
|
|
329
328
|
async confirmEnoughStorage(job) {
|
|
329
|
+
if (!job) {
|
|
330
|
+
throw new Error('confirmEnoughStorage: job is missing');
|
|
331
|
+
}
|
|
330
332
|
if (this.confirmEnoughStorageHook) {
|
|
331
333
|
return await this.confirmEnoughStorageHook(job);
|
|
332
334
|
}
|
|
@@ -343,12 +345,13 @@ class ExportSQLCommand {
|
|
|
343
345
|
try {
|
|
344
346
|
_fs.default.accessSync(_path.default.parse(this.outputFile).dir, _fs.default.constants.W_OK);
|
|
345
347
|
} catch (err) {
|
|
348
|
+
const error = err;
|
|
346
349
|
await this.track('error', {
|
|
347
350
|
error_type: 'cannot_write_to_path',
|
|
348
|
-
error_message: `Cannot write to the specified path: ${
|
|
349
|
-
stack:
|
|
351
|
+
error_message: `Cannot write to the specified path: ${error?.message}`,
|
|
352
|
+
stack: error?.stack
|
|
350
353
|
});
|
|
351
|
-
exit.withError(`Cannot write to the specified path: ${
|
|
354
|
+
exit.withError(`Cannot write to the specified path: ${error?.message}`);
|
|
352
355
|
}
|
|
353
356
|
}
|
|
354
357
|
if (this.generateBackup) {
|
|
@@ -357,16 +360,15 @@ class ExportSQLCommand {
|
|
|
357
360
|
const {
|
|
358
361
|
latestBackup
|
|
359
362
|
} = await fetchLatestBackupAndJobStatus(this.app.id, this.env.id);
|
|
363
|
+
if (!latestBackup) {
|
|
364
|
+
await this.track('error', {
|
|
365
|
+
error_type: 'no_backup_found',
|
|
366
|
+
error_message: 'No backup found for the site'
|
|
367
|
+
});
|
|
368
|
+
exit.withError(`No backup found for site ${this.app.name}`);
|
|
369
|
+
}
|
|
360
370
|
if (!this.generateBackup) {
|
|
361
|
-
|
|
362
|
-
await this.track('error', {
|
|
363
|
-
error_type: 'no_backup_found',
|
|
364
|
-
error_message: 'No backup found for the site'
|
|
365
|
-
});
|
|
366
|
-
exit.withError(`No backup found for site ${this.app.name}`);
|
|
367
|
-
} else {
|
|
368
|
-
console.log(`${(0, _format.getGlyphForStatus)('success')} Latest backup found with timestamp ${latestBackup.createdAt}`);
|
|
369
|
-
}
|
|
371
|
+
console.log(`${(0, _format.getGlyphForStatus)('success')} Latest backup found with timestamp ${latestBackup.createdAt}`);
|
|
370
372
|
} else {
|
|
371
373
|
console.log(`${(0, _format.getGlyphForStatus)('success')} Backup created with timestamp ${latestBackup.createdAt}`);
|
|
372
374
|
}
|
|
@@ -377,22 +379,23 @@ class ExportSQLCommand {
|
|
|
377
379
|
try {
|
|
378
380
|
await createExportJob(this.app.id, this.env.id, latestBackup.id);
|
|
379
381
|
} catch (err) {
|
|
382
|
+
const error = err;
|
|
380
383
|
// Todo: match error code instead of message substring
|
|
381
|
-
if (
|
|
384
|
+
if (error.message.includes('Backup Copy already in progress')) {
|
|
382
385
|
await this.track('error', {
|
|
383
386
|
error_type: 'job_already_running',
|
|
384
|
-
error_message:
|
|
385
|
-
stack:
|
|
387
|
+
error_message: error.message,
|
|
388
|
+
stack: error.stack
|
|
386
389
|
});
|
|
387
390
|
exit.withError('There is an export job already running for this environment: ' + `https://dashboard.wpvip.com/apps/${this.app.id}/${this.env.uniqueLabel}/database/backups\n` + 'Currently, we allow only one export job at a time, per site. Please try again later.');
|
|
388
391
|
} else {
|
|
389
392
|
await this.track('error', {
|
|
390
393
|
error_type: 'create_export_job',
|
|
391
|
-
error_message:
|
|
392
|
-
stack:
|
|
394
|
+
error_message: error.message,
|
|
395
|
+
stack: error.stack
|
|
393
396
|
});
|
|
394
397
|
}
|
|
395
|
-
exit.withError(`Error creating export job: ${
|
|
398
|
+
exit.withError(`Error creating export job: ${error.message}`);
|
|
396
399
|
}
|
|
397
400
|
}
|
|
398
401
|
this.progressTracker.stepRunning(this.steps.PREPARE);
|
|
@@ -425,14 +428,15 @@ class ExportSQLCommand {
|
|
|
425
428
|
this.stopProgressTracker();
|
|
426
429
|
console.log(`File saved to ${filepath}`);
|
|
427
430
|
} catch (err) {
|
|
431
|
+
const error = err;
|
|
428
432
|
this.progressTracker.stepFailed(this.steps.DOWNLOAD);
|
|
429
433
|
this.stopProgressTracker();
|
|
430
434
|
await this.track('error', {
|
|
431
435
|
error_type: 'download_failed',
|
|
432
|
-
error_message:
|
|
433
|
-
stack:
|
|
436
|
+
error_message: error.message,
|
|
437
|
+
stack: error.stack
|
|
434
438
|
});
|
|
435
|
-
exit.withError(`Error downloading exported file: ${
|
|
439
|
+
exit.withError(`Error downloading exported file: ${error.message}`);
|
|
436
440
|
}
|
|
437
441
|
}
|
|
438
442
|
}
|
|
@@ -32,4 +32,4 @@ const DEV_ENVIRONMENT_DEFAULTS = exports.DEV_ENVIRONMENT_DEFAULTS = {
|
|
|
32
32
|
multisite: false,
|
|
33
33
|
phpVersion: Object.keys(DEV_ENVIRONMENT_PHP_VERSIONS)[0]
|
|
34
34
|
};
|
|
35
|
-
const DEV_ENVIRONMENT_VERSION = exports.DEV_ENVIRONMENT_VERSION = '2.1.
|
|
35
|
+
const DEV_ENVIRONMENT_VERSION = exports.DEV_ENVIRONMENT_VERSION = '2.1.1';
|