@automattic/vip 3.19.2 → 3.20.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/dist/bin/vip-dev-env-sync-sql.js +23 -2
- package/dist/bin/vip-export-sql.js +24 -2
- package/dist/bin/vip-import-sql.js +58 -5
- package/dist/commands/dev-env-sync-sql.js +10 -12
- package/dist/commands/export-sql.js +140 -71
- package/dist/lib/backup-storage-availability/backup-storage-availability.js +22 -33
- package/dist/lib/cli/exit.js +4 -4
- package/dist/lib/dev-environment/dev-environment-core.js +5 -14
- package/dist/lib/dev-environment/dev-environment-lando.js +2 -4
- package/dist/lib/http/download-file.js +46 -0
- package/dist/lib/live-backup-copy.js +149 -0
- package/dist/lib/search-and-replace.js +2 -2
- package/dist/lib/utils.js +13 -13
- package/dist/lib/xdg-data.js +15 -0
- package/npm-shrinkwrap.json +3648 -1094
- package/package.json +10 -21
- package/tsconfig.json +2 -2
|
@@ -5,6 +5,7 @@ var _devEnvSyncSql = require("../commands/dev-env-sync-sql");
|
|
|
5
5
|
var _command = _interopRequireDefault(require("../lib/cli/command"));
|
|
6
6
|
var _devEnvironmentCli = require("../lib/dev-environment/dev-environment-cli");
|
|
7
7
|
var _devEnvironmentLando = require("../lib/dev-environment/dev-environment-lando");
|
|
8
|
+
var _liveBackupCopy = require("../lib/live-backup-copy");
|
|
8
9
|
var _tracker = require("../lib/tracker");
|
|
9
10
|
var _userError = _interopRequireDefault(require("../lib/user-error"));
|
|
10
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -12,6 +13,21 @@ const usage = 'vip dev-env sync sql';
|
|
|
12
13
|
const examples = [{
|
|
13
14
|
usage: `vip @example-app.develop dev-env sync sql --slug=example-site`,
|
|
14
15
|
description: 'Sync the database of the develop environment in the "example-app" application to a local environment named "example-site".'
|
|
16
|
+
}, {
|
|
17
|
+
usage: `vip @example-app.develop dev-env sync sql --slug=example-site --table=wp_posts --table=wp_comments`,
|
|
18
|
+
description: 'Sync only the wp_posts and wp_comments tables from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
|
|
19
|
+
}, {
|
|
20
|
+
usage: `vip @example-app.develop dev-env sync sql --slug=example-site --table=wp_posts,wp_comments`,
|
|
21
|
+
description: 'Sync only the wp_posts and wp_comments tables using comma-separated syntax from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
|
|
22
|
+
}, {
|
|
23
|
+
usage: `vip @example-app.develop dev-env sync sql --slug=example-site --subsite-id=2 --subsite-id=3`,
|
|
24
|
+
description: 'Sync only the tables for the subsites with IDs 2 and 3 from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
|
|
25
|
+
}, {
|
|
26
|
+
usage: `vip @example-app.develop dev-env sync sql --slug=example-site --subsite-id=2,3`,
|
|
27
|
+
description: 'Sync only the tables for the subsites with IDs 2 and 3 using comma-separated syntax from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
|
|
28
|
+
}, {
|
|
29
|
+
usage: `vip @example-app.develop dev-env sync sql --slug=example-site --config-file=~/dev-env-sync-config.json`,
|
|
30
|
+
description: 'Use the specified config file to determine what to sync from the database of the develop environment in the "example-app" application to a local environment named "example-site".'
|
|
15
31
|
}];
|
|
16
32
|
const appQuery = `
|
|
17
33
|
id,
|
|
@@ -42,12 +58,17 @@ const appQuery = `
|
|
|
42
58
|
requiredArgs: 0,
|
|
43
59
|
module: 'dev-env-sync-sql',
|
|
44
60
|
usage
|
|
45
|
-
}).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('force', 'Skip validations.', undefined, _devEnvironmentCli.processBooleanOption).examples(examples).argv(process.argv, async (arg, opt) => {
|
|
61
|
+
}).option('slug', 'A unique name for a local environment. Default is "vip-local".', undefined, _devEnvironmentCli.processSlug).option('table', 'A table to sync from the remote environment to the local environment. Multiple tables can be specified with multiple --table flags or as a comma-separated list.').option('subsite-id', 'The ID of a subsite/network site to sync from the remote environment to the local environment. Multiple subsite IDs can be specified with multiple --subsite-id flags or as a comma-separated list.').option('wpcli-command', 'The WP-CLI command to run on the remote environment to retrieve the database export configuration.').option('config-file', 'The backup copy config file to use for the sync.', undefined).option('force', 'Skip validations.', undefined, _devEnvironmentCli.processBooleanOption).examples(examples).argv(process.argv, async (arg, opt) => {
|
|
46
62
|
const {
|
|
47
63
|
app,
|
|
48
64
|
env,
|
|
65
|
+
configFile,
|
|
66
|
+
table,
|
|
67
|
+
subsiteId,
|
|
68
|
+
wpcliCommand,
|
|
49
69
|
...optRest
|
|
50
70
|
} = opt;
|
|
71
|
+
const liveBackupCopyCLIOptions = (0, _liveBackupCopy.parseLiveBackupCopyCLIOptions)(configFile, table, subsiteId, wpcliCommand);
|
|
51
72
|
const slug = await (0, _devEnvironmentCli.getEnvironmentName)(optRest);
|
|
52
73
|
const trackerFn = (0, _tracker.makeCommandTracker)('dev_env_sync_sql', {
|
|
53
74
|
app: app.id,
|
|
@@ -64,7 +85,7 @@ const appQuery = `
|
|
|
64
85
|
});
|
|
65
86
|
throw new _userError.default('Environment needs to be started first');
|
|
66
87
|
}
|
|
67
|
-
const cmd = new _devEnvSyncSql.DevEnvSyncSQLCommand(app, env, slug, lando, trackerFn);
|
|
88
|
+
const cmd = new _devEnvSyncSql.DevEnvSyncSQLCommand(app, env, slug, lando, trackerFn, liveBackupCopyCLIOptions);
|
|
68
89
|
// TODO: There's a function called handleCLIException for dev-env that handles exceptions but DevEnvSyncSQLCommand has its own implementation.
|
|
69
90
|
// We should probably use handleCLIException instead?
|
|
70
91
|
const didCommandRun = await cmd.run();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
var _exportSql = require("../commands/export-sql");
|
|
5
5
|
var _command = _interopRequireDefault(require("../lib/cli/command"));
|
|
6
|
+
var _liveBackupCopy = require("../lib/live-backup-copy");
|
|
6
7
|
var _tracker = require("../lib/tracker");
|
|
7
8
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
9
|
const examples = [{
|
|
@@ -14,6 +15,21 @@ const examples = [{
|
|
|
14
15
|
}, {
|
|
15
16
|
usage: 'vip @example-app.develop export sql --generate-backup',
|
|
16
17
|
description: 'Generate a fresh database backup for an environment and download a copy of that backup.'
|
|
18
|
+
}, {
|
|
19
|
+
usage: 'vip @example-app.develop export sql --table=wp_posts --table=wp_comments',
|
|
20
|
+
description: 'Generate a database backup including only the wp_posts and wp_comments tables, and download a copy of that backup.'
|
|
21
|
+
}, {
|
|
22
|
+
usage: 'vip @example-app.develop export sql --table=wp_posts,wp_comments',
|
|
23
|
+
description: 'Generate a database backup including only the wp_posts and wp_comments tables using comma-separated syntax, and download a copy of that backup.'
|
|
24
|
+
}, {
|
|
25
|
+
usage: 'vip @example-app.develop export sql --subsite-id=2 --subsite-id=3',
|
|
26
|
+
description: 'Generate a database backup including only the tables related to the subsites with IDs 2 and 3, and download a copy of that backup.'
|
|
27
|
+
}, {
|
|
28
|
+
usage: 'vip @example-app.develop export sql --subsite-id=2,3',
|
|
29
|
+
description: 'Generate a database backup including only the tables related to the subsites with IDs 2 and 3 using comma-separated syntax, and download a copy of that backup.'
|
|
30
|
+
}, {
|
|
31
|
+
usage: 'vip @example-app.develop export sql --config-file=~/db-export-config.json',
|
|
32
|
+
description: 'Generate a database backup using the specified config file, and download a copy of that backup.'
|
|
17
33
|
}];
|
|
18
34
|
const appQuery = `
|
|
19
35
|
id,
|
|
@@ -36,10 +52,14 @@ const appQuery = `
|
|
|
36
52
|
module: 'export-sql',
|
|
37
53
|
requiredArgs: 0,
|
|
38
54
|
usage: 'vip export sql'
|
|
39
|
-
}).option('output', 'Download the file to a specific local directory path with a custom file name.').option('generate-backup', 'Generate a fresh database backup and export a copy of that backup.').examples(examples).argv(process.argv, async (arg, {
|
|
55
|
+
}).option('output', 'Download the file to a specific local directory path with a custom file name.').option('table', 'A table to export from the remote environment. Multiple tables can be specified with multiple --table flags or as a comma-separated list.').option('subsite-id', 'The ID of a subsite/network site to export from the remote environment. Multiple subsite IDs can be specified with multiple --subsite-id flags or as a comma-separated list.').option('wpcli-command', 'The WP-CLI command to run on the remote environment to retrieve the database export configuration.').option('config-file', 'The backup copy config file to use for the export.', undefined).option('generate-backup', 'Generate a fresh database backup and export a copy of that backup.').examples(examples).argv(process.argv, async (arg, {
|
|
40
56
|
app,
|
|
41
57
|
env,
|
|
42
58
|
output,
|
|
59
|
+
configFile,
|
|
60
|
+
table,
|
|
61
|
+
subsiteId,
|
|
62
|
+
wpcliCommand,
|
|
43
63
|
generateBackup
|
|
44
64
|
}) => {
|
|
45
65
|
const trackerFn = (0, _tracker.makeCommandTracker)('export_sql', {
|
|
@@ -48,9 +68,11 @@ const appQuery = `
|
|
|
48
68
|
generate_backup: generateBackup
|
|
49
69
|
});
|
|
50
70
|
await trackerFn('execute');
|
|
71
|
+
const liveBackupCopyCLIOptions = (0, _liveBackupCopy.parseLiveBackupCopyCLIOptions)(configFile, table, subsiteId, wpcliCommand);
|
|
51
72
|
const exportCommand = new _exportSql.ExportSQLCommand(app, env, {
|
|
52
73
|
outputFile: output,
|
|
53
|
-
generateBackup
|
|
74
|
+
generateBackup,
|
|
75
|
+
liveBackupCopyCLIOptions
|
|
54
76
|
}, trackerFn);
|
|
55
77
|
await exportCommand.run();
|
|
56
78
|
await trackerFn('success');
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
exports.__esModule = true;
|
|
5
5
|
exports.gates = gates;
|
|
6
|
+
exports.parseHeaders = parseHeaders;
|
|
6
7
|
exports.promptToContinue = void 0;
|
|
7
8
|
exports.validateAndGetTableNames = validateAndGetTableNames;
|
|
8
9
|
var _chalk = _interopRequireDefault(require("chalk"));
|
|
@@ -86,8 +87,8 @@ const SQL_IMPORT_PREFLIGHT_PROGRESS_STEPS = [{
|
|
|
86
87
|
*/
|
|
87
88
|
function isValidUrl(str) {
|
|
88
89
|
try {
|
|
89
|
-
new URL(str);
|
|
90
|
-
return
|
|
90
|
+
const url = new URL(str);
|
|
91
|
+
return !/^[a-z]:$/iu.test(url.protocol);
|
|
91
92
|
} catch {
|
|
92
93
|
return false;
|
|
93
94
|
}
|
|
@@ -102,6 +103,35 @@ function isValidMd5(md5) {
|
|
|
102
103
|
return /^[a-f0-9]{32}$/i.test(md5);
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Parse and validate headers from CLI input
|
|
108
|
+
* @param {string|string[]} headers - Header(s) in "Name: Value" format
|
|
109
|
+
* @returns {Array<{name: string, value: string}>} Parsed headers
|
|
110
|
+
*/
|
|
111
|
+
function parseHeaders(headers) {
|
|
112
|
+
if (!headers) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
const headerArray = Array.isArray(headers) ? headers : [headers];
|
|
116
|
+
const parsedHeaders = [];
|
|
117
|
+
for (const header of headerArray) {
|
|
118
|
+
const colonIndex = header.indexOf(':');
|
|
119
|
+
if (colonIndex === -1) {
|
|
120
|
+
exit.withError(`Invalid header format: "${header}". Expected format: "Name: Value"`);
|
|
121
|
+
}
|
|
122
|
+
const name = header.substring(0, colonIndex).trim();
|
|
123
|
+
const value = header.substring(colonIndex + 1).trim();
|
|
124
|
+
if (!name) {
|
|
125
|
+
exit.withError(`Invalid header format: "${header}". Header name cannot be empty.`);
|
|
126
|
+
}
|
|
127
|
+
parsedHeaders.push({
|
|
128
|
+
name,
|
|
129
|
+
value
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return parsedHeaders;
|
|
133
|
+
}
|
|
134
|
+
|
|
105
135
|
/**
|
|
106
136
|
* @param {import('../lib/site-import/db-file-import').AppForImport} app
|
|
107
137
|
* @param {import('../lib/site-import/db-file-import').EnvForImport} env
|
|
@@ -226,6 +256,11 @@ const examples = [
|
|
|
226
256
|
usage: 'vip @example-app.develop import sql https://example.org/file.sql',
|
|
227
257
|
description: 'Import a remote SQL database backup file from the URL with MD5 hash verification to the develop environment of the "example-app" application.'
|
|
228
258
|
},
|
|
259
|
+
// URL import with HTTP Basic Auth
|
|
260
|
+
{
|
|
261
|
+
usage: 'vip @example-app.develop import sql https://username:password@example.org/file.sql',
|
|
262
|
+
description: 'Import a remote SQL database backup file from a URL that requires HTTP Basic Authentication.'
|
|
263
|
+
},
|
|
229
264
|
// `search-replace` flag
|
|
230
265
|
{
|
|
231
266
|
usage: 'vip @example-app.develop import sql file.sql --search-replace="from.example.com,to.example.com" --search-replace="example.com/from,example.com/to"',
|
|
@@ -241,6 +276,16 @@ const examples = [
|
|
|
241
276
|
usage: 'vip @example-app.develop import sql file.sql --search-replace="https://from.example.com,https://to.example.com" --output="updated-file.sql"',
|
|
242
277
|
description: 'Create a copy of the imported file with the completed search and replace operations and save it locally to a file named "updated-file.sql".'
|
|
243
278
|
},
|
|
279
|
+
// URL import with headers
|
|
280
|
+
{
|
|
281
|
+
usage: 'vip @example-app.develop import sql https://example.org/file.sql --header "Authorization: Bearer token"',
|
|
282
|
+
description: 'Import a remote SQL database backup file from a URL with custom authorization header.'
|
|
283
|
+
},
|
|
284
|
+
// URL import with multiple headers
|
|
285
|
+
{
|
|
286
|
+
usage: 'vip @example-app.develop import sql https://example.org/file.sql --header "Authorization: Bearer token" --header "User-Agent: VIP CLI"',
|
|
287
|
+
description: 'Import a remote SQL database backup file from a URL with multiple custom headers.'
|
|
288
|
+
},
|
|
244
289
|
// `sql status` subcommand
|
|
245
290
|
{
|
|
246
291
|
usage: 'vip @example-app.develop import sql status',
|
|
@@ -374,7 +419,7 @@ const displayPlaybook = ({
|
|
|
374
419
|
requiredArgs: 1,
|
|
375
420
|
module: 'import-sql',
|
|
376
421
|
usage
|
|
377
|
-
}).command('status', 'Check the status of a SQL database import currently in progress.').option('skip-validate', 'Do not perform file validation prior to import. If the file contains unsupported entries, the import is likely to fail.').option('search-replace', 'Search for a string in the SQL file and replace it with a new string. Separate the values by a comma only; no spaces (e.g. --search-replace="from,to").').option('in-place', 'Overwrite the local input file with the results of the search and replace operation prior to import.').option('output', 'The local file path to save a copy of the results from the search and replace operation when the --search-replace option is passed. Ignored when used with the --in-place option.').option('skip-maintenance-mode', 'Imports data without putting the environment in maintenance mode. Available only for unlaunched environments. Caution: This may cause site instability during import.').option('md5', 'MD5 hash of the remote SQL file for verification. If not provided, the verification will not be performed.').examples(examples)
|
|
422
|
+
}).command('status', 'Check the status of a SQL database import currently in progress.').option('skip-validate', 'Do not perform file validation prior to import. If the file contains unsupported entries, the import is likely to fail.').option('search-replace', 'Search for a string in the SQL file and replace it with a new string. Separate the values by a comma only; no spaces (e.g. --search-replace="from,to").').option('in-place', 'Overwrite the local input file with the results of the search and replace operation prior to import.').option('output', 'The local file path to save a copy of the results from the search and replace operation when the --search-replace option is passed. Ignored when used with the --in-place option.').option('skip-maintenance-mode', 'Imports data without putting the environment in maintenance mode. Available only for unlaunched environments. Caution: This may cause site instability during import.').option('md5', 'MD5 hash of the remote SQL file for verification. If not provided, the verification will not be performed.').option('header', 'Add a header to the request when downloading from a URL. Format: "Name: Value". Can be used multiple times.').examples(examples)
|
|
378
423
|
// eslint-disable-next-line complexity
|
|
379
424
|
.argv(process.argv, async (arg, opts) => {
|
|
380
425
|
const {
|
|
@@ -385,7 +430,8 @@ const displayPlaybook = ({
|
|
|
385
430
|
skipValidate,
|
|
386
431
|
searchReplace,
|
|
387
432
|
skipMaintenanceMode,
|
|
388
|
-
md5
|
|
433
|
+
md5,
|
|
434
|
+
header
|
|
389
435
|
} = opts;
|
|
390
436
|
const {
|
|
391
437
|
id: envId,
|
|
@@ -394,6 +440,12 @@ const displayPlaybook = ({
|
|
|
394
440
|
const [fileNameOrURL] = arg;
|
|
395
441
|
const isMultiSite = await (0, _isMultiSite.isMultiSiteInSiteMeta)(appId, envId);
|
|
396
442
|
const isUrl = isValidUrl(fileNameOrURL);
|
|
443
|
+
|
|
444
|
+
// Parse and validate headers
|
|
445
|
+
const headers = parseHeaders(header);
|
|
446
|
+
if (!isUrl && headers.length > 0) {
|
|
447
|
+
console.log(_chalk.default.yellowBright('The --header parameter is only used for URL imports. It will be ignored for local file imports.'));
|
|
448
|
+
}
|
|
397
449
|
if (isUrl && opts.inPlace) {
|
|
398
450
|
console.log(_chalk.default.yellowBright('The --in-place option is not supported when importing from a URL. This option will be ignored.'));
|
|
399
451
|
opts.inPlace = false;
|
|
@@ -527,7 +579,8 @@ Processing the SQL import for your environment...
|
|
|
527
579
|
...startImportVariables.input,
|
|
528
580
|
url: fileNameOrURL,
|
|
529
581
|
searchReplace: [],
|
|
530
|
-
md5
|
|
582
|
+
md5,
|
|
583
|
+
urlHeaders: headers
|
|
531
584
|
};
|
|
532
585
|
} else {
|
|
533
586
|
progressTracker.stepRunning('upload');
|
|
@@ -87,30 +87,27 @@ class DevEnvSyncSQLCommand {
|
|
|
87
87
|
tmpDir;
|
|
88
88
|
siteUrls = [];
|
|
89
89
|
searchReplaceMap = {};
|
|
90
|
+
liveBackupCopyCLIOptions;
|
|
90
91
|
_track;
|
|
91
92
|
_sqlDumpType;
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* Creates a new instance of the command
|
|
95
|
-
*
|
|
96
|
-
* @param app The app object
|
|
97
|
-
* @param env The environment object
|
|
98
|
-
* @param slug The site slug
|
|
99
|
-
* @param lando The lando object
|
|
100
|
-
* @param trackerFn Function to call for tracking
|
|
101
96
|
*/
|
|
102
|
-
constructor(app, env, slug, lando, trackerFn = () => {}) {
|
|
97
|
+
constructor(app, env, slug, lando, trackerFn = () => {}, liveBackupCopyCLIOptions) {
|
|
103
98
|
this.app = app;
|
|
104
99
|
this.env = env;
|
|
105
100
|
this.slug = slug;
|
|
106
101
|
this.lando = lando;
|
|
107
102
|
this._track = trackerFn;
|
|
108
103
|
this.tmpDir = (0, _utils.makeTempDir)();
|
|
104
|
+
this.liveBackupCopyCLIOptions = liveBackupCopyCLIOptions;
|
|
109
105
|
}
|
|
110
106
|
track(name, eventProps) {
|
|
111
107
|
return this._track(name, {
|
|
112
108
|
...eventProps,
|
|
113
|
-
sqldump_type: this._sqlDumpType
|
|
109
|
+
sqldump_type: this._sqlDumpType,
|
|
110
|
+
live_backup_copy: this.liveBackupCopyCLIOptions?.useLiveBackupCopy
|
|
114
111
|
});
|
|
115
112
|
}
|
|
116
113
|
get landoDomain() {
|
|
@@ -132,9 +129,9 @@ class DevEnvSyncSQLCommand {
|
|
|
132
129
|
const dumpDetails = await (0, _database.getSqlDumpDetails)(this.sqlFile);
|
|
133
130
|
this._sqlDumpType = dumpDetails.type;
|
|
134
131
|
}
|
|
135
|
-
async confirmEnoughStorage(
|
|
136
|
-
const storageAvailability = _backupStorageAvailability.BackupStorageAvailability
|
|
137
|
-
return await storageAvailability.validateAndPromptDiskSpaceWarningForDevEnvBackupImport();
|
|
132
|
+
async confirmEnoughStorage(archiveSize) {
|
|
133
|
+
const storageAvailability = new _backupStorageAvailability.BackupStorageAvailability();
|
|
134
|
+
return await storageAvailability.validateAndPromptDiskSpaceWarningForDevEnvBackupImport(archiveSize);
|
|
138
135
|
}
|
|
139
136
|
|
|
140
137
|
/**
|
|
@@ -144,7 +141,8 @@ class DevEnvSyncSQLCommand {
|
|
|
144
141
|
async generateExport() {
|
|
145
142
|
const exportCommand = new _exportSql.ExportSQLCommand(this.app, this.env, {
|
|
146
143
|
outputFile: this.gzFile,
|
|
147
|
-
confirmEnoughStorageHook: this.confirmEnoughStorage.bind(this)
|
|
144
|
+
confirmEnoughStorageHook: this.confirmEnoughStorage.bind(this),
|
|
145
|
+
liveBackupCopyCLIOptions: this.liveBackupCopyCLIOptions
|
|
148
146
|
}, this.track.bind(this));
|
|
149
147
|
await exportCommand.run();
|
|
150
148
|
}
|
|
@@ -5,7 +5,6 @@ exports.GENERATE_DOWNLOAD_LINK_MUTATION = exports.ExportSQLCommand = exports.CRE
|
|
|
5
5
|
var _chalk = _interopRequireDefault(require("chalk"));
|
|
6
6
|
var _fs = _interopRequireDefault(require("fs"));
|
|
7
7
|
var _graphqlTag = _interopRequireDefault(require("graphql-tag"));
|
|
8
|
-
var _https = _interopRequireDefault(require("https"));
|
|
9
8
|
var _path = _interopRequireDefault(require("path"));
|
|
10
9
|
var _backupDb = require("./backup-db");
|
|
11
10
|
var _api = _interopRequireWildcard(require("../lib/api"));
|
|
@@ -13,6 +12,9 @@ var _backupStorageAvailability = require("../lib/backup-storage-availability/bac
|
|
|
13
12
|
var exit = _interopRequireWildcard(require("../lib/cli/exit"));
|
|
14
13
|
var _format = require("../lib/cli/format");
|
|
15
14
|
var _progress = require("../lib/cli/progress");
|
|
15
|
+
var _downloadFile = require("../lib/http/download-file");
|
|
16
|
+
var _liveBackupCopy = _interopRequireWildcard(require("../lib/live-backup-copy"));
|
|
17
|
+
var liveBackupCopy = _liveBackupCopy;
|
|
16
18
|
var _retry = require("../lib/retry");
|
|
17
19
|
var _utils = require("../lib/utils");
|
|
18
20
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
@@ -189,6 +191,8 @@ class ExportSQLCommand {
|
|
|
189
191
|
DOWNLOAD: 'download'
|
|
190
192
|
};
|
|
191
193
|
track;
|
|
194
|
+
liveBackupCopyCLIOptions;
|
|
195
|
+
showMyDumperWarning;
|
|
192
196
|
|
|
193
197
|
/**
|
|
194
198
|
* Creates an instance of SQLExportCommand
|
|
@@ -221,6 +225,8 @@ class ExportSQLCommand {
|
|
|
221
225
|
name: 'Downloading file'
|
|
222
226
|
}]);
|
|
223
227
|
this.track = trackerFn;
|
|
228
|
+
this.liveBackupCopyCLIOptions = options.liveBackupCopyCLIOptions;
|
|
229
|
+
this.showMyDumperWarning = options.showMyDumperWarning === undefined ? true : options.showMyDumperWarning;
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
/**
|
|
@@ -258,39 +264,6 @@ class ExportSQLCommand {
|
|
|
258
264
|
return metadata?.value?.split('/')[1];
|
|
259
265
|
}
|
|
260
266
|
|
|
261
|
-
/**
|
|
262
|
-
* Downloads the exported file
|
|
263
|
-
*
|
|
264
|
-
* @param {string} url The download URL
|
|
265
|
-
* @return {Promise} A promise which resolves to the path of the downloaded file
|
|
266
|
-
* @throws {Error} Throws an error if the download fails
|
|
267
|
-
*/
|
|
268
|
-
async downloadExportedFile(url) {
|
|
269
|
-
const filename = this.outputFile || (await this.getExportedFileName()) || 'exported.sql.gz';
|
|
270
|
-
const file = _fs.default.createWriteStream(filename);
|
|
271
|
-
return new Promise((resolve, reject) => {
|
|
272
|
-
_https.default.get(url, response => {
|
|
273
|
-
response.pipe(file);
|
|
274
|
-
const total = parseInt(response.headers['content-length'], 10);
|
|
275
|
-
let current = 0;
|
|
276
|
-
file.on('finish', () => {
|
|
277
|
-
file.close();
|
|
278
|
-
resolve(_path.default.resolve(file.path));
|
|
279
|
-
});
|
|
280
|
-
file.on('error', err => {
|
|
281
|
-
// TODO: fs.unlink runs in the background so there's a chance that the app dies before it finishes.
|
|
282
|
-
// This needs fixing.
|
|
283
|
-
_fs.default.unlink(filename, () => null);
|
|
284
|
-
reject(err);
|
|
285
|
-
});
|
|
286
|
-
response.on('data', chunk => {
|
|
287
|
-
current += chunk.length;
|
|
288
|
-
this.progressTracker.setProgress(`- ${(100 * current / total).toFixed(2)}% (${(0, _format.formatBytes)(current)}/${(0, _format.formatBytes)(total)})`);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
267
|
/**
|
|
295
268
|
* Checks if the export job's preflight step is successful
|
|
296
269
|
*
|
|
@@ -328,15 +301,12 @@ class ExportSQLCommand {
|
|
|
328
301
|
console.log(noticeMessage);
|
|
329
302
|
await cmd.run(false);
|
|
330
303
|
}
|
|
331
|
-
async confirmEnoughStorage(
|
|
332
|
-
if (!job) {
|
|
333
|
-
throw new Error('confirmEnoughStorage: job is missing');
|
|
334
|
-
}
|
|
304
|
+
async confirmEnoughStorage(archiveSize) {
|
|
335
305
|
if (this.confirmEnoughStorageHook) {
|
|
336
|
-
return await this.confirmEnoughStorageHook(
|
|
306
|
+
return await this.confirmEnoughStorageHook(archiveSize);
|
|
337
307
|
}
|
|
338
|
-
const storageAvailability = _backupStorageAvailability.BackupStorageAvailability
|
|
339
|
-
return await storageAvailability.validateAndPromptDiskSpaceWarningForBackupImport();
|
|
308
|
+
const storageAvailability = new _backupStorageAvailability.BackupStorageAvailability();
|
|
309
|
+
return await storageAvailability.validateAndPromptDiskSpaceWarningForBackupImport(archiveSize);
|
|
340
310
|
}
|
|
341
311
|
|
|
342
312
|
/**
|
|
@@ -357,6 +327,66 @@ class ExportSQLCommand {
|
|
|
357
327
|
exit.withError(`Cannot write to the specified path: ${error?.message}`);
|
|
358
328
|
}
|
|
359
329
|
}
|
|
330
|
+
const filename = this.outputFile || 'exported.sql.gz';
|
|
331
|
+
this.progressTracker.stepRunning(this.steps.PREPARE);
|
|
332
|
+
this.progressTracker.startPrinting();
|
|
333
|
+
let url;
|
|
334
|
+
let size;
|
|
335
|
+
if (this.liveBackupCopyCLIOptions?.useLiveBackupCopy) {
|
|
336
|
+
const result = await this.generateLiveBackupCopy();
|
|
337
|
+
url = result.url;
|
|
338
|
+
size = result.size;
|
|
339
|
+
} else {
|
|
340
|
+
url = await this.runBackup();
|
|
341
|
+
const exportJob = await this.getExportJob();
|
|
342
|
+
if (!exportJob) {
|
|
343
|
+
throw new Error('Export job not found');
|
|
344
|
+
}
|
|
345
|
+
const bytesWrittenMeta = exportJob.metadata?.find(meta => meta?.name === 'bytesWritten');
|
|
346
|
+
if (!bytesWrittenMeta?.value) {
|
|
347
|
+
throw new Error('Export job metadata does not contain bytesWritten');
|
|
348
|
+
}
|
|
349
|
+
size = Number(bytesWrittenMeta.value);
|
|
350
|
+
}
|
|
351
|
+
const storageConfirmed = await this.progressTracker.handleContinuePrompt(async setPromptShown => {
|
|
352
|
+
const status = await this.confirmEnoughStorage(size);
|
|
353
|
+
if (status.isPromptShown) {
|
|
354
|
+
setPromptShown();
|
|
355
|
+
}
|
|
356
|
+
return status.continue;
|
|
357
|
+
});
|
|
358
|
+
if (storageConfirmed) {
|
|
359
|
+
this.progressTracker.stepSuccess(this.steps.CONFIRM_ENOUGH_STORAGE);
|
|
360
|
+
} else {
|
|
361
|
+
this.progressTracker.stepFailed(this.steps.CONFIRM_ENOUGH_STORAGE);
|
|
362
|
+
this.stopProgressTracker();
|
|
363
|
+
exit.withError('Command canceled by user.');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// The export file is prepared. Let's download it
|
|
367
|
+
try {
|
|
368
|
+
const onProgressCallback = (current, total) => {
|
|
369
|
+
if (total) {
|
|
370
|
+
this.progressTracker.setProgress(`- ${(100 * current / total).toFixed(2)}% (${(0, _format.formatBytes)(current)}/${(0, _format.formatBytes)(total)})`);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
await (0, _downloadFile.downloadFile)(url, filename, onProgressCallback);
|
|
374
|
+
this.progressTracker.stepSuccess(this.steps.DOWNLOAD);
|
|
375
|
+
this.stopProgressTracker();
|
|
376
|
+
console.log(`File saved to ${filename}`);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
const error = err;
|
|
379
|
+
this.progressTracker.stepFailed(this.steps.DOWNLOAD);
|
|
380
|
+
this.stopProgressTracker();
|
|
381
|
+
await this.track('error', {
|
|
382
|
+
error_type: 'download_failed',
|
|
383
|
+
error_message: error.message,
|
|
384
|
+
stack: error.stack
|
|
385
|
+
});
|
|
386
|
+
exit.withError(`Error downloading exported file: ${error.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async runBackup() {
|
|
360
390
|
if (this.generateBackup) {
|
|
361
391
|
await this.runBackupJob();
|
|
362
392
|
}
|
|
@@ -376,7 +406,7 @@ class ExportSQLCommand {
|
|
|
376
406
|
} else {
|
|
377
407
|
console.log(`${(0, _format.getGlyphForStatus)('success')} Backup created with timestamp ${latestBackup.createdAt}`);
|
|
378
408
|
}
|
|
379
|
-
const showMyDumperWarning = (latestBackup.sqlDumpTool ?? envSqlDumpTool) === 'mydumper';
|
|
409
|
+
const showMyDumperWarning = this.showMyDumperWarning && (latestBackup.sqlDumpTool ?? envSqlDumpTool) === 'mydumper';
|
|
380
410
|
if (showMyDumperWarning) {
|
|
381
411
|
console.warn(_chalk.default.yellow.bold('WARNING:'), _chalk.default.yellow('This is a large or complex database. The backup file for this database is generated with MyDumper. ' + 'The file can only be loaded with MyLoader. ' + 'For more information: https://github.com/mydumper/mydumper'));
|
|
382
412
|
}
|
|
@@ -406,45 +436,84 @@ class ExportSQLCommand {
|
|
|
406
436
|
exit.withError(`Error creating export job: ${error.message}`);
|
|
407
437
|
}
|
|
408
438
|
}
|
|
409
|
-
this.progressTracker.stepRunning(this.steps.PREPARE);
|
|
410
|
-
this.progressTracker.startPrinting();
|
|
411
439
|
await (0, _utils.pollUntil)(this.getExportJob.bind(this), EXPORT_SQL_PROGRESS_POLL_INTERVAL, this.isPrepared.bind(this));
|
|
412
440
|
this.progressTracker.stepSuccess(this.steps.PREPARE);
|
|
413
441
|
await (0, _utils.pollUntil)(this.getExportJob.bind(this), EXPORT_SQL_PROGRESS_POLL_INTERVAL, this.isCreated.bind(this));
|
|
414
442
|
this.progressTracker.stepSuccess(this.steps.CREATE);
|
|
415
|
-
const storageConfirmed = await this.progressTracker.handleContinuePrompt(async setPromptShown => {
|
|
416
|
-
const status = await this.confirmEnoughStorage(await this.getExportJob());
|
|
417
|
-
if (status.isPromptShown) {
|
|
418
|
-
setPromptShown();
|
|
419
|
-
}
|
|
420
|
-
return status.continue;
|
|
421
|
-
});
|
|
422
|
-
if (storageConfirmed) {
|
|
423
|
-
this.progressTracker.stepSuccess(this.steps.CONFIRM_ENOUGH_STORAGE);
|
|
424
|
-
} else {
|
|
425
|
-
this.progressTracker.stepFailed(this.steps.CONFIRM_ENOUGH_STORAGE);
|
|
426
|
-
this.stopProgressTracker();
|
|
427
|
-
exit.withError('Command canceled by user.');
|
|
428
|
-
}
|
|
429
443
|
const url = await generateDownloadLink(this.app.id, this.env.id, latestBackup.id);
|
|
430
444
|
this.progressTracker.stepSuccess(this.steps.DOWNLOAD_LINK);
|
|
431
|
-
|
|
432
|
-
|
|
445
|
+
return url;
|
|
446
|
+
}
|
|
447
|
+
async generateLiveBackupCopy() {
|
|
448
|
+
if (!this.liveBackupCopyCLIOptions) {
|
|
449
|
+
throw new Error('Configuration file is required for live backup copy.');
|
|
450
|
+
}
|
|
451
|
+
if (!this.app.id || !this.env.id) {
|
|
452
|
+
throw new Error('App ID and Environment ID are required to start live backup copy.');
|
|
453
|
+
}
|
|
454
|
+
const config = this.getLiveBackupConfigFromCLIOptions();
|
|
433
455
|
try {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
456
|
+
const copyId = await liveBackupCopy.startLiveBackupCopy({
|
|
457
|
+
appId: this.app.id,
|
|
458
|
+
environmentId: this.env.id,
|
|
459
|
+
config
|
|
460
|
+
});
|
|
461
|
+
this.progressTracker.stepSuccess(this.steps.PREPARE);
|
|
462
|
+
this.progressTracker.stepRunning(this.steps.CREATE);
|
|
463
|
+
const result = await liveBackupCopy.getDownloadURL({
|
|
464
|
+
appId: this.app.id,
|
|
465
|
+
environmentId: this.env.id,
|
|
466
|
+
copyId
|
|
467
|
+
});
|
|
468
|
+
this.progressTracker.stepSuccess(this.steps.CREATE);
|
|
469
|
+
this.progressTracker.stepSuccess(this.steps.DOWNLOAD_LINK);
|
|
470
|
+
return result;
|
|
438
471
|
} catch (err) {
|
|
439
|
-
const
|
|
440
|
-
this.progressTracker.stepFailed(this.steps.DOWNLOAD);
|
|
441
|
-
this.stopProgressTracker();
|
|
472
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
442
473
|
await this.track('error', {
|
|
443
|
-
error_type: '
|
|
444
|
-
error_message:
|
|
445
|
-
stack:
|
|
474
|
+
error_type: 'live_backup_copy',
|
|
475
|
+
error_message: message,
|
|
476
|
+
stack: err instanceof Error ? err.stack : undefined
|
|
446
477
|
});
|
|
447
|
-
exit.withError(`Error
|
|
478
|
+
exit.withError(`Error creating live backup copy: ${message}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
getLiveBackupConfigFromCLIOptions() {
|
|
482
|
+
if (this.liveBackupCopyCLIOptions?.configFile) {
|
|
483
|
+
return this.loadLiveBackupCopyConfig(this.liveBackupCopyCLIOptions?.configFile);
|
|
484
|
+
}
|
|
485
|
+
let type = _liveBackupCopy.BackupLiveCopyType.TABLES;
|
|
486
|
+
let tables;
|
|
487
|
+
let subsiteIds;
|
|
488
|
+
if (this.liveBackupCopyCLIOptions?.tables) {
|
|
489
|
+
tables = Object.fromEntries(this.liveBackupCopyCLIOptions?.tables.map(key => [key, {}]));
|
|
490
|
+
}
|
|
491
|
+
if (this.liveBackupCopyCLIOptions?.subsiteIds) {
|
|
492
|
+
type = _liveBackupCopy.BackupLiveCopyType.SUBSITE_IDS;
|
|
493
|
+
subsiteIds = this.liveBackupCopyCLIOptions?.subsiteIds.map(id => Number(id));
|
|
494
|
+
}
|
|
495
|
+
if (this.liveBackupCopyCLIOptions?.wpcliCommand) {
|
|
496
|
+
type = _liveBackupCopy.BackupLiveCopyType.WP_CLI_COMMAND;
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
type,
|
|
500
|
+
tables,
|
|
501
|
+
subsite_ids: subsiteIds,
|
|
502
|
+
wpcli_command: this.liveBackupCopyCLIOptions?.wpcliCommand
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
loadLiveBackupCopyConfig(configFile) {
|
|
506
|
+
if (!_fs.default.existsSync(configFile)) {
|
|
507
|
+
throw new Error(`Configuration file not found: ${configFile}`);
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
return JSON.parse(_fs.default.readFileSync(configFile, 'utf-8'));
|
|
511
|
+
} catch (err) {
|
|
512
|
+
const errMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
513
|
+
if (err instanceof SyntaxError) {
|
|
514
|
+
throw new Error(`Invalid JSON in configuration file: ${configFile} - ${errMessage}`);
|
|
515
|
+
}
|
|
516
|
+
throw new Error(`Error reading configuration file: ${configFile} - ${errMessage}`);
|
|
448
517
|
}
|
|
449
518
|
}
|
|
450
519
|
}
|