@cloudant/couchbackup 2.10.0-SNAPSHOT.199 → 2.10.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/README.md +10 -0
- package/app.js +253 -268
- package/bin/couchbackup.bin.js +1 -3
- package/bin/couchrestore.bin.js +2 -4
- package/includes/allDocsGenerator.js +53 -0
- package/includes/backup.js +103 -247
- package/includes/backupMappings.js +260 -0
- package/includes/config.js +10 -9
- package/includes/error.js +42 -44
- package/includes/liner.js +134 -23
- package/includes/logfilegetbatches.js +25 -60
- package/includes/logfilesummary.js +41 -71
- package/includes/parser.js +3 -3
- package/includes/request.js +95 -106
- package/includes/restore.js +45 -14
- package/includes/restoreMappings.js +141 -0
- package/includes/spoolchanges.js +57 -79
- package/includes/transforms.js +378 -0
- package/package.json +5 -8
- package/includes/change.js +0 -41
- package/includes/shallowbackup.js +0 -80
- package/includes/writer.js +0 -164
package/README.md
CHANGED
|
@@ -118,6 +118,10 @@ You may also specify the name of the output file, rather than directing the back
|
|
|
118
118
|
couchbackup --db animaldb --log animaldb.log --resume true --output animaldb.txt
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
### Compatibility note
|
|
122
|
+
|
|
123
|
+
When using `--resume` use the same version of couchbackup that started the backup.
|
|
124
|
+
|
|
121
125
|
## Restore
|
|
122
126
|
|
|
123
127
|
Now that we have our backup text file, we can restore it to a new, empty, existing database using the `couchrestore`:
|
|
@@ -132,6 +136,12 @@ or specifying the database name on the command-line:
|
|
|
132
136
|
cat animaldb.txt | couchrestore --db animaldb2
|
|
133
137
|
```
|
|
134
138
|
|
|
139
|
+
### Compatibility note
|
|
140
|
+
|
|
141
|
+
**Do not use an older version of couchbackup to restore a backup created with a newer version.**
|
|
142
|
+
|
|
143
|
+
Newer versions of couchbackup can restore backups created by older versions within the same major version.
|
|
144
|
+
|
|
135
145
|
## Compressed backups
|
|
136
146
|
|
|
137
147
|
If we want to compress the backup data before storing to disk, we can pipe the contents through `gzip`:
|
package/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright © 2017,
|
|
1
|
+
// Copyright © 2017, 2024 IBM Corp. All rights reserved.
|
|
2
2
|
//
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
// you may not use this file except in compliance with the License.
|
|
@@ -19,27 +19,27 @@
|
|
|
19
19
|
* @see module:couchbackup
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const events = require('node:events');
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const URL = require('node:url').URL;
|
|
25
|
+
const backup = require('./includes/backup.js');
|
|
23
26
|
const defaults = require('./includes/config.js').apiDefaults;
|
|
24
|
-
const
|
|
25
|
-
const
|
|
27
|
+
const { convertError, BackupError, OptionError } = require('./includes/error.js');
|
|
28
|
+
const { newClient } = require('./includes/request.js');
|
|
26
29
|
const restoreInternal = require('./includes/restore.js');
|
|
27
|
-
const backupShallow = require('./includes/shallowbackup.js');
|
|
28
30
|
const debug = require('debug')('couchbackup:app');
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const URL = require('url').URL;
|
|
31
|
+
const pkg = require('./package.json');
|
|
32
|
+
const { RESUME_COMMENT } = require('./includes/restoreMappings.js');
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Test for a positive, safe integer.
|
|
35
36
|
*
|
|
36
|
-
* @param {
|
|
37
|
+
* @param {any} x - Object under test.
|
|
37
38
|
*/
|
|
38
39
|
function isSafePositiveInteger(x) {
|
|
39
40
|
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
|
|
40
41
|
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991;
|
|
41
|
-
|
|
42
|
-
return Object.prototype.toString.call(x) === '[object Number]' &&
|
|
42
|
+
return (typeof x === 'number' || typeof x === 'bigint') &&
|
|
43
43
|
// Is it an integer?
|
|
44
44
|
x % 1 === 0 &&
|
|
45
45
|
// Is it positive?
|
|
@@ -49,162 +49,235 @@ function isSafePositiveInteger(x) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Validate
|
|
52
|
+
* Validate URL.
|
|
53
53
|
*
|
|
54
|
-
* @param {
|
|
55
|
-
* @param {
|
|
56
|
-
* @
|
|
54
|
+
* @param {string} url - URL of database.
|
|
55
|
+
* @param {boolean} isIAM - A flag if IAM authentication been used.
|
|
56
|
+
* @returns Boolean true if all checks are passing.
|
|
57
57
|
*/
|
|
58
|
-
function
|
|
58
|
+
async function validateURL(url, isIAM) {
|
|
59
59
|
if (typeof url !== 'string') {
|
|
60
|
-
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (opts && typeof opts.bufferSize !== 'undefined' && !isSafePositiveInteger(opts.bufferSize)) {
|
|
64
|
-
cb(new error.BackupError('InvalidOption', 'Invalid buffer size option, must be a positive integer in the range (0, MAX_SAFE_INTEGER]'), null);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (opts && typeof opts.iamApiKey !== 'undefined' && typeof opts.iamApiKey !== 'string') {
|
|
68
|
-
cb(new error.BackupError('InvalidOption', 'Invalid iamApiKey option, must be type string'), null);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (opts && typeof opts.log !== 'undefined' && typeof opts.log !== 'string') {
|
|
72
|
-
cb(new error.BackupError('InvalidOption', 'Invalid log option, must be type string'), null);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (opts && typeof opts.mode !== 'undefined' && ['full', 'shallow'].indexOf(opts.mode) === -1) {
|
|
76
|
-
cb(new error.BackupError('InvalidOption', 'Invalid mode option, must be either "full" or "shallow"'), null);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (opts && typeof opts.output !== 'undefined' && typeof opts.output !== 'string') {
|
|
80
|
-
cb(new error.BackupError('InvalidOption', 'Invalid output option, must be type string'), null);
|
|
81
|
-
return;
|
|
60
|
+
throw new OptionError('Invalid URL, must be type string');
|
|
82
61
|
}
|
|
83
|
-
if (opts && typeof opts.parallelism !== 'undefined' && !isSafePositiveInteger(opts.parallelism)) {
|
|
84
|
-
cb(new error.BackupError('InvalidOption', 'Invalid parallelism option, must be a positive integer in the range (0, MAX_SAFE_INTEGER]'), null);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (opts && typeof opts.requestTimeout !== 'undefined' && !isSafePositiveInteger(opts.requestTimeout)) {
|
|
88
|
-
cb(new error.BackupError('InvalidOption', 'Invalid request timeout option, must be a positive integer in the range (0, MAX_SAFE_INTEGER]'), null);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (opts && typeof opts.resume !== 'undefined' && typeof opts.resume !== 'boolean') {
|
|
92
|
-
cb(new error.BackupError('InvalidOption', 'Invalid resume option, must be type boolean'), null);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
62
|
// Validate URL and ensure no auth if using key
|
|
97
63
|
try {
|
|
98
64
|
const urlObject = new URL(url);
|
|
99
65
|
// We require a protocol, host and path (for db), fail if any is missing.
|
|
100
66
|
if (urlObject.protocol !== 'https:' && urlObject.protocol !== 'http:') {
|
|
101
|
-
|
|
102
|
-
return;
|
|
67
|
+
throw new OptionError('Invalid URL protocol.');
|
|
103
68
|
}
|
|
104
69
|
if (!urlObject.pathname || urlObject.pathname === '/') {
|
|
105
|
-
|
|
106
|
-
return;
|
|
70
|
+
throw new OptionError('Invalid URL, missing path element (no database).');
|
|
107
71
|
}
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
return;
|
|
72
|
+
if (isIAM && (urlObject.username || urlObject.password)) {
|
|
73
|
+
throw new OptionError('URL user information must not be supplied when using IAM API key.');
|
|
111
74
|
}
|
|
112
75
|
} catch (err) {
|
|
113
|
-
|
|
114
|
-
return;
|
|
76
|
+
throw convertError(err);
|
|
115
77
|
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
116
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Validate options.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} opts - Options.
|
|
85
|
+
* @returns Boolean true if all checks are passing.
|
|
86
|
+
*/
|
|
87
|
+
async function validateOptions(opts) {
|
|
88
|
+
// if we don't have opts then we'll be using defaults
|
|
89
|
+
if (!opts) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
const rules = [
|
|
93
|
+
{ key: 'iamApiKey', type: 'string' },
|
|
94
|
+
{ key: 'log', type: 'string' },
|
|
95
|
+
{ key: 'output', type: 'string' },
|
|
96
|
+
{ key: 'bufferSize', type: 'number' },
|
|
97
|
+
{ key: 'parallelism', type: 'number' },
|
|
98
|
+
{ key: 'requestTimeout', type: 'number' },
|
|
99
|
+
{ key: 'mode', type: 'enum', values: ['full', 'shallow'] },
|
|
100
|
+
{ key: 'resume', type: 'boolean' }
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const rule of rules) {
|
|
104
|
+
const val = opts[rule.key];
|
|
105
|
+
switch (rule.type) {
|
|
106
|
+
case 'string':
|
|
107
|
+
if (typeof val !== 'undefined' && typeof val !== 'string') {
|
|
108
|
+
throw new OptionError(`Invalid ${rule.key} option, must be type string`);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case 'number':
|
|
112
|
+
if (typeof val !== 'undefined' && !isSafePositiveInteger(val)) {
|
|
113
|
+
const humanized = rule.key.replace(/[A-Z]/g, l => ` ${l.toLowerCase()}`);
|
|
114
|
+
throw new OptionError(`Invalid ${humanized} option, must be a positive integer in the range (0, MAX_SAFE_INTEGER]`);
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
case 'enum':
|
|
118
|
+
if (typeof val !== 'undefined' && rule.values.indexOf(val) === -1) {
|
|
119
|
+
const humanized = rule.values
|
|
120
|
+
.map(w => `"${w}"`)
|
|
121
|
+
.reduce((acc, w, i, arr) => {
|
|
122
|
+
return acc + (i < arr.length - 1 ? ', ' : ' or ') + w;
|
|
123
|
+
});
|
|
124
|
+
throw new OptionError(`Invalid mode option, must be either ${humanized}`);
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
case 'boolean':
|
|
128
|
+
if (typeof val !== 'undefined' && typeof val !== 'boolean') {
|
|
129
|
+
throw new OptionError(`Invalid ${rule.key} option, must be type boolean`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Show warning on invalid params in shallow mode.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} opts - Options.
|
|
140
|
+
*/
|
|
141
|
+
async function shallowModeWarnings(opts) {
|
|
142
|
+
if (!opts || opts.mode !== 'shallow') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
117
145
|
// Perform validation of invalid options for shallow mode and WARN
|
|
118
146
|
// We don't error for backwards compatibility with scripts that may have been
|
|
119
147
|
// written passing complete sets of options through
|
|
120
|
-
if (opts
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
console.warn('WARNING: the option "parallelism" has no effect when using shallow mode.');
|
|
126
|
-
}
|
|
148
|
+
if (opts.log || opts.resume) {
|
|
149
|
+
console.warn('WARNING: the options "log" and "resume" are invalid when using shallow mode.');
|
|
150
|
+
}
|
|
151
|
+
if (opts.parallelism) {
|
|
152
|
+
console.warn('WARNING: the option "parallelism" has no effect when using shallow mode.');
|
|
127
153
|
}
|
|
154
|
+
}
|
|
128
155
|
|
|
129
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Additional checks for log on resume.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} opts - Options.
|
|
160
|
+
* @returns Boolean true if all checks are passing.
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
async function validateLogOnResume(opts) {
|
|
164
|
+
const logFileExists = opts && opts.log && fs.existsSync(opts.log);
|
|
165
|
+
if (!opts || opts.mode === 'shallow') {
|
|
166
|
+
// No opts specified, defaults will be populated.
|
|
167
|
+
// In shallow mode log/resume are irrelevant and we'll have warned already.
|
|
168
|
+
return true;
|
|
169
|
+
} else if (opts.resume) {
|
|
170
|
+
// Expecting to resume
|
|
130
171
|
if (!opts.log) {
|
|
131
172
|
// This is the second place we check for the presence of the log option in conjunction with resume
|
|
132
173
|
// It has to be here for the API case
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
174
|
+
throw new BackupError('NoLogFileName', 'To resume a backup, a log file must be specified');
|
|
175
|
+
} else if (!logFileExists) {
|
|
176
|
+
throw new BackupError('LogDoesNotExist', 'To resume a backup, the log file must exist');
|
|
177
|
+
}
|
|
178
|
+
if (opts.bufferSize) {
|
|
179
|
+
// Warn that the bufferSize is already fixed
|
|
180
|
+
console.warn('WARNING: the original backup "bufferSize" applies when resuming a backup.');
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// Not resuming
|
|
184
|
+
if (logFileExists) {
|
|
185
|
+
throw new BackupError('LogFileExists', `The log file ${opts.log} exists. ` +
|
|
186
|
+
'Use the resume option if you want to resume a backup from an existing log file.');
|
|
138
187
|
}
|
|
139
188
|
}
|
|
140
189
|
return true;
|
|
141
190
|
}
|
|
142
191
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Validate arguments.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} url - URL of database.
|
|
196
|
+
* @param {object} opts - Options.
|
|
197
|
+
* @param {boolean} backup - true for backup, false for restore
|
|
198
|
+
* @returns Boolean true if all checks are passing.
|
|
199
|
+
*/
|
|
200
|
+
async function validateArgs(url, opts, isBackup = true) {
|
|
201
|
+
const isIAM = opts && typeof opts.iamApiKey === 'string';
|
|
202
|
+
const validations = [
|
|
203
|
+
validateURL(url, isIAM),
|
|
204
|
+
validateOptions(opts)
|
|
205
|
+
];
|
|
206
|
+
if (isBackup) {
|
|
207
|
+
validations.push(
|
|
208
|
+
shallowModeWarnings(opts),
|
|
209
|
+
validateLogOnResume(opts)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return Promise.all(validations);
|
|
150
213
|
}
|
|
151
214
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Check the backup database exists and that the credentials used have
|
|
217
|
+
* visibility. Throw a fatal error if there is a problem with the DB.
|
|
218
|
+
*
|
|
219
|
+
* @param {object} dbClient - database client object
|
|
220
|
+
* @returns Passed in database client object
|
|
221
|
+
*/
|
|
222
|
+
async function validateBackupDb(dbClient) {
|
|
223
|
+
try {
|
|
224
|
+
await dbClient.service.headDatabase({ db: dbClient.dbName });
|
|
225
|
+
return dbClient;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const e = parseDbResponseError(dbClient, err);
|
|
228
|
+
if (e.name === 'DatabaseNotFound') {
|
|
229
|
+
e.message = `${err.message} Ensure the backup source database exists.`;
|
|
230
|
+
}
|
|
231
|
+
// maybe convert it to HTTPError
|
|
232
|
+
throw convertError(e);
|
|
233
|
+
}
|
|
163
234
|
}
|
|
164
235
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Check that the restore database exists, is new and is empty. Also verify that the credentials used have
|
|
238
|
+
* visibility. Callback with a fatal error if there is a problem with the DB.
|
|
239
|
+
*
|
|
240
|
+
* @param {object} dbClient - database client object
|
|
241
|
+
* @returns Passed in database client object
|
|
242
|
+
*/
|
|
243
|
+
async function validateRestoreDb(dbClient) {
|
|
244
|
+
try {
|
|
245
|
+
const response = await dbClient.service.getDatabaseInformation({ db: dbClient.dbName });
|
|
173
246
|
const { doc_count: docCount, doc_del_count: deletedDocCount } = response.result;
|
|
174
247
|
// The system databases can have a validation ddoc(s) injected in them on creation.
|
|
175
248
|
// This sets the doc count off, so we just complitely exclude the system databases from this check.
|
|
176
249
|
// The assumption here is that users restoring system databases know what they are doing.
|
|
177
|
-
if (!
|
|
178
|
-
|
|
179
|
-
notEmptyDBErr.name = 'DatabaseNotEmpty';
|
|
180
|
-
callback(notEmptyDBErr);
|
|
181
|
-
} else {
|
|
182
|
-
callback();
|
|
250
|
+
if (!dbClient.dbName.startsWith('_') && (docCount !== 0 || deletedDocCount !== 0)) {
|
|
251
|
+
throw new BackupError('DatabaseNotEmpty', `Target database ${dbClient.url}${dbClient.dbName} is not empty. A target database must be a new and empty database.`);
|
|
183
252
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
253
|
+
// good to use
|
|
254
|
+
return dbClient;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
const e = parseDbResponseError(dbClient, err);
|
|
257
|
+
if (e.name === 'DatabaseNotFound') {
|
|
258
|
+
e.message = `${e.message} Create the target database before restoring.`;
|
|
259
|
+
}
|
|
260
|
+
// maybe convert it to HTTPError
|
|
261
|
+
throw convertError(e);
|
|
262
|
+
}
|
|
188
263
|
}
|
|
189
264
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Convert the database validation response error to a special DatabaseNotFound error
|
|
267
|
+
* in case the database is missing. Otherwise returns an original error.
|
|
268
|
+
* @param {object} dbClient - database client object
|
|
269
|
+
* @param {object} err - HTTP response error
|
|
270
|
+
* @returns {Error} - DatabaseNotFound error or passed in err
|
|
271
|
+
*/
|
|
272
|
+
function parseDbResponseError(dbClient, err) {
|
|
197
273
|
if (err && err.status === 404) {
|
|
198
274
|
// Override the error type and message for the DB not found case
|
|
199
|
-
const msg = `Database ${
|
|
200
|
-
`${
|
|
275
|
+
const msg = `Database ${dbClient.url}` +
|
|
276
|
+
`${dbClient.dbName} does not exist. ` +
|
|
201
277
|
'Check the URL and database name have been specified correctly.';
|
|
202
|
-
|
|
203
|
-
noDBErr.name = 'DatabaseNotFound';
|
|
204
|
-
return noDBErr;
|
|
278
|
+
return new BackupError('DatabaseNotFound', msg);
|
|
205
279
|
}
|
|
206
|
-
|
|
207
|
-
return error.convertResponseError(err);
|
|
280
|
+
return err;
|
|
208
281
|
}
|
|
209
282
|
|
|
210
283
|
module.exports = {
|
|
@@ -225,109 +298,52 @@ module.exports = {
|
|
|
225
298
|
* @param {backupRestoreCallback} callback - Called on completion.
|
|
226
299
|
*/
|
|
227
300
|
backup: function(srcUrl, targetStream, opts, callback) {
|
|
228
|
-
const listenerErrorIndicator = { errored: false };
|
|
229
301
|
if (typeof callback === 'undefined' && typeof opts === 'function') {
|
|
230
302
|
callback = opts;
|
|
231
303
|
opts = {};
|
|
232
304
|
}
|
|
233
|
-
if (!validateArgs(srcUrl, opts, callback)) {
|
|
234
|
-
// bad args, bail
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// if there is an error writing to the stream, call the completion
|
|
239
|
-
// callback with the error set
|
|
240
|
-
addEventListener(listenerErrorIndicator, targetStream, 'error', function(err) {
|
|
241
|
-
debug('Error ' + JSON.stringify(err));
|
|
242
|
-
if (callback) callback(err);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
opts = Object.assign({}, defaults(), opts);
|
|
246
305
|
|
|
247
306
|
const ee = new events.EventEmitter();
|
|
248
307
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// If resuming write a newline as it's possible one would be missing from
|
|
270
|
-
// an interruption of the previous backup. If the backup was clean this
|
|
271
|
-
// will cause an empty line that will be gracefully handled by the restore.
|
|
272
|
-
if (opts.resume) {
|
|
273
|
-
targetStream.write('\n');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Get the event emitter from the backup process so we can handle events
|
|
277
|
-
// before passing them on to the app's event emitter if needed.
|
|
278
|
-
const internalEE = backup(backupDB, opts);
|
|
279
|
-
addEventListener(listenerErrorIndicator, internalEE, 'changes', function(batch) {
|
|
280
|
-
ee.emit('changes', batch);
|
|
281
|
-
});
|
|
282
|
-
addEventListener(listenerErrorIndicator, internalEE, 'received', function(obj, q, logCompletedBatch) {
|
|
283
|
-
// this may be too verbose to have as well as the "backed up" message
|
|
284
|
-
// debug(' received batch', obj.batch, ' docs: ', obj.total, 'Time', obj.time);
|
|
285
|
-
// Callback to emit the written event when the content is flushed
|
|
286
|
-
function writeFlushed() {
|
|
287
|
-
ee.emit('written', { total: obj.total, time: obj.time, batch: obj.batch });
|
|
288
|
-
if (logCompletedBatch) {
|
|
289
|
-
logCompletedBatch(obj.batch);
|
|
290
|
-
}
|
|
291
|
-
debug(' backed up batch', obj.batch, ' docs: ', obj.total, 'Time', obj.time);
|
|
292
|
-
}
|
|
293
|
-
// Write the received content to the targetStream
|
|
294
|
-
const continueWriting = targetStream.write(JSON.stringify(obj.data) + '\n',
|
|
295
|
-
'utf8',
|
|
296
|
-
writeFlushed);
|
|
297
|
-
if (!continueWriting) {
|
|
298
|
-
// The buffer was full, pause the queue to stop the writes until we
|
|
299
|
-
// get a drain event
|
|
300
|
-
if (q && !q.paused) {
|
|
301
|
-
q.pause();
|
|
302
|
-
targetStream.once('drain', function() {
|
|
303
|
-
q.resume();
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
// For errors we expect, may or may not be fatal
|
|
309
|
-
addEventListener(listenerErrorIndicator, internalEE, 'error', function(err) {
|
|
310
|
-
debug('Error ' + JSON.stringify(err));
|
|
311
|
-
callback(err);
|
|
312
|
-
});
|
|
313
|
-
addEventListener(listenerErrorIndicator, internalEE, 'finished', function(obj) {
|
|
314
|
-
function emitFinished() {
|
|
315
|
-
debug('Backup complete - written ' + JSON.stringify(obj));
|
|
316
|
-
const summary = { total: obj.total };
|
|
317
|
-
ee.emit('finished', summary);
|
|
318
|
-
if (callback) callback(null, summary);
|
|
319
|
-
}
|
|
320
|
-
if (targetStream === process.stdout) {
|
|
321
|
-
// stdout cannot emit a finish event so use a final write + callback
|
|
322
|
-
targetStream.write('', 'utf8', emitFinished);
|
|
308
|
+
validateArgs(srcUrl, opts)
|
|
309
|
+
// Set up the DB client
|
|
310
|
+
.then(() => {
|
|
311
|
+
opts = Object.assign({}, defaults(), opts);
|
|
312
|
+
return newClient(srcUrl, opts);
|
|
313
|
+
})
|
|
314
|
+
// Validate the DB exists, before proceeding to backup
|
|
315
|
+
.then(backupDbClient => validateBackupDb(backupDbClient))
|
|
316
|
+
.then(backupDbClient => {
|
|
317
|
+
// Write either a file header or a resume marker.
|
|
318
|
+
let metadataToWrite;
|
|
319
|
+
if (opts.mode === 'full' && opts.resume) {
|
|
320
|
+
// resume is valid in full mode only
|
|
321
|
+
// Write the resume marker and a newline as it's possible one would be missing from
|
|
322
|
+
// an interruption of the previous backup. If the backup was clean this
|
|
323
|
+
// will cause an empty line that will be gracefully handled by the restore.
|
|
324
|
+
debug('Will write resume marker.');
|
|
325
|
+
metadataToWrite = `${RESUME_COMMENT}\n`;
|
|
323
326
|
} else {
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
targetStream.end('', 'utf8', emitFinished);
|
|
327
|
+
// Write a file header including the name, version and mode
|
|
328
|
+
debug('Will write backup file header.');
|
|
329
|
+
metadataToWrite = `${JSON.stringify({ name: pkg.name, version: pkg.version, mode: opts.mode })}\n`;
|
|
328
330
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
+
return new Promise((resolve, reject) => {
|
|
332
|
+
targetStream.write(metadataToWrite, 'utf-8', (err) => {
|
|
333
|
+
if (err) { reject(err); } else { resolve(backupDbClient); }
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
})
|
|
337
|
+
.then((backupDbClient) =>
|
|
338
|
+
backup(backupDbClient, opts, targetStream, ee)
|
|
339
|
+
)
|
|
340
|
+
.then((total) => {
|
|
341
|
+
debug(`Finished backup with total doc count of ${total}`);
|
|
342
|
+
ee.emit('finished', total);
|
|
343
|
+
callback(null, total);
|
|
344
|
+
})
|
|
345
|
+
.catch(e => callback(convertError(e)));
|
|
346
|
+
|
|
331
347
|
return ee;
|
|
332
348
|
},
|
|
333
349
|
|
|
@@ -344,64 +360,33 @@ module.exports = {
|
|
|
344
360
|
* @param {backupRestoreCallback} callback - Called on completion.
|
|
345
361
|
*/
|
|
346
362
|
restore: function(srcStream, targetUrl, opts, callback) {
|
|
347
|
-
const listenerErrorIndicator = { errored: false };
|
|
348
363
|
if (typeof callback === 'undefined' && typeof opts === 'function') {
|
|
349
364
|
callback = opts;
|
|
350
365
|
opts = {};
|
|
351
366
|
}
|
|
352
|
-
validateArgs(targetUrl, opts, callback);
|
|
353
|
-
opts = Object.assign({}, defaults(), opts);
|
|
354
367
|
|
|
355
368
|
const ee = new events.EventEmitter();
|
|
356
369
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
ee,
|
|
378
|
-
function(err, writer) {
|
|
379
|
-
if (err) {
|
|
380
|
-
callback(err, null);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
if (writer != null) {
|
|
384
|
-
addEventListener(listenerErrorIndicator, writer, 'restored', function(obj) {
|
|
385
|
-
debug(' restored ', obj.total);
|
|
386
|
-
ee.emit('restored', { documents: obj.documents, total: obj.total });
|
|
387
|
-
});
|
|
388
|
-
addEventListener(listenerErrorIndicator, writer, 'error', function(err) {
|
|
389
|
-
debug('Error ' + JSON.stringify(err));
|
|
390
|
-
// Only call destroy if it is available on the stream
|
|
391
|
-
if (srcStream.destroy && srcStream.destroy instanceof Function) {
|
|
392
|
-
srcStream.destroy();
|
|
393
|
-
}
|
|
394
|
-
callback(err);
|
|
395
|
-
});
|
|
396
|
-
addEventListener(listenerErrorIndicator, writer, 'finished', function(obj) {
|
|
397
|
-
debug('restore complete');
|
|
398
|
-
ee.emit('finished', { total: obj.total });
|
|
399
|
-
callback(null, obj);
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
);
|
|
404
|
-
});
|
|
370
|
+
validateArgs(targetUrl, opts, false)
|
|
371
|
+
// Set up the DB client
|
|
372
|
+
.then(() => {
|
|
373
|
+
opts = Object.assign({}, defaults(), opts);
|
|
374
|
+
return newClient(targetUrl, opts);
|
|
375
|
+
})
|
|
376
|
+
// Validate the DB exists, before proceeding to restore
|
|
377
|
+
.then((restoreDbClient) => validateRestoreDb(restoreDbClient))
|
|
378
|
+
.then((restoreDbClient) => {
|
|
379
|
+
return restoreInternal(
|
|
380
|
+
restoreDbClient,
|
|
381
|
+
opts,
|
|
382
|
+
srcStream,
|
|
383
|
+
ee);
|
|
384
|
+
})
|
|
385
|
+
.then((total) => {
|
|
386
|
+
ee.emit('finished', total);
|
|
387
|
+
callback(null, total);
|
|
388
|
+
})
|
|
389
|
+
.catch(e => callback(convertError(e)));
|
|
405
390
|
return ee;
|
|
406
391
|
}
|
|
407
392
|
};
|
package/bin/couchbackup.bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Copyright © 2017,
|
|
2
|
+
// Copyright © 2017, 2024 IBM Corp. All rights reserved.
|
|
3
3
|
//
|
|
4
4
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
// you may not use this file except in compliance with the License.
|
|
@@ -71,8 +71,6 @@ try {
|
|
|
71
71
|
backupBatchDebug('Total batches received:', batch + 1);
|
|
72
72
|
}).on('written', function(obj) {
|
|
73
73
|
backupBatchDebug('Written batch ID:', obj.batch, 'Total document revisions written:', obj.total, 'Time:', obj.time);
|
|
74
|
-
}).on('error', function(e) {
|
|
75
|
-
backupDebug('ERROR', e);
|
|
76
74
|
}).on('finished', function(obj) {
|
|
77
75
|
backupDebug('Finished - Total document revisions written:', obj.total);
|
|
78
76
|
});
|