@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.
@@ -12,6 +12,8 @@ proxy:
12
12
  mailpit:
13
13
  - <%= siteSlug %>-mailpit.<%= domain %>:8025
14
14
 
15
+ keys: false
16
+
15
17
  services:
16
18
  devtools:
17
19
  type: compose
@@ -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
- (0, _command.default)({
13
- requiredArgs: 1,
14
- format: true
15
- }).example('vip import validate-files <file>', 'Run the import validation against the file').argv(process.argv, async arg => {
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
- let folderValidation;
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 file extension validation
60
- *
61
- * Ensure that prohibited media file types are not used
73
+ * Get Media Import configuration
62
74
  */
63
-
64
- // Collect invalid files for error logging
65
- let intermediateImagesTotal = 0;
66
- const errorFileTypes = [];
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
- if (errorFileTypes.length > 0) {
128
- (0, _vipImportValidateFiles.logErrorsForInvalidFileTypes)(errorFileTypes);
129
- }
130
- if (errorFileNames.length > 0) {
131
- (0, _vipImportValidateFiles.logErrorsForInvalidFilenames)(errorFileNames);
132
- }
133
- if (Object.keys(intermediateImages).length > 0) {
134
- (0, _vipImportValidateFiles.logErrorsForIntermediateImages)(intermediateImages);
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 (err) {
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 {string} sql A line in a SQL file
24
- * @return {string} Site home url. null if not found
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 {string} sqlFile Path to the SQL file
36
- * @return {Promise<string[]>} List of site urls
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 {string} app The app object
70
- * @param {string} env The environment object
71
- * @param {string} slug The site slug
72
- * @param {Object} lando The lando object
73
- * @param {Function} trackerFn Function to call for tracking
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.nodes;
131
+ const networkSites = this.env.wpSitesSDS?.nodes;
136
132
  if (!networkSites) return;
137
133
  for (const site of networkSites) {
138
- if (!site.blogId || site.blogId === 1) continue;
139
- const url = site.homeUrl.replace(/https?:\/\//, '');
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 {Promise<void>} Promise that resolves to true when the commands are complete. It will return false if the user did not continue during validation prompts.
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: err?.message,
186
- stack: err?.stack
182
+ error_message: error.message,
183
+ stack: error.stack
187
184
  });
188
- exit.withError(`Error exporting SQL backup: ${err?.message}`);
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: err?.message,
198
- stack: err?.stack
195
+ error_message: error.message,
196
+ stack: error.stack
199
197
  });
200
- exit.withError(`Error extracting the SQL export: ${err.message}`);
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: err?.message,
209
- stack: err?.stack
207
+ error_message: error.message,
208
+ stack: error.stack
210
209
  });
211
- exit.withError(`Error extracting site URLs: ${err?.message}`);
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: err?.message,
226
- stack: err?.stack
225
+ error_message: error.message,
226
+ stack: error.stack
227
227
  });
228
- exit.withError(`Error replacing domains: ${err?.message}`);
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: err?.message,
239
- stack: err?.stack
239
+ error_message: error.message,
240
+ stack: error.stack
240
241
  });
241
- exit.withError(`Error importing SQL file: ${err?.message}`);
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
- data: {
99
- app: {
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
- const {
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 null if job creation succeeds
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.name === 'backupId');
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 {Promise} A promise which resolves to the filename
247
+ * @return A promise which resolves to the filename
252
248
  */
253
249
  async getExportedFileName() {
254
250
  const job = await this.getExportJob();
255
- const metadata = job.metadata.find(md => md.name === 'uploadPath');
256
- return metadata?.value.split('/')[1];
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
- _fs.default.unlink(filename);
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 {any} job The export job
294
- * @return {boolean} True if the preflight step is successful
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.steps.find(st => st.id === 'preflight');
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 {any} job The export job
305
- * @return {boolean} True if the upload step is successful
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.steps.find(st => st.id === 'upload_backup');
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: ${err?.message}`,
349
- stack: err?.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: ${err?.message}`);
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
- if (!latestBackup) {
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 (err?.message.includes('Backup Copy already in progress')) {
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: err?.message,
385
- stack: err?.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: err?.message,
392
- stack: err?.stack
394
+ error_message: error.message,
395
+ stack: error.stack
393
396
  });
394
397
  }
395
- exit.withError(`Error creating export job: ${err?.message}`);
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: err?.message,
433
- stack: err?.stack
436
+ error_message: error.message,
437
+ stack: error.stack
434
438
  });
435
- exit.withError(`Error downloading exported file: ${err?.message}`);
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.0';
35
+ const DEV_ENVIRONMENT_VERSION = exports.DEV_ENVIRONMENT_VERSION = '2.1.1';