@cloudant/couchbackup 2.9.17-SNAPSHOT.3 → 2.10.0-206

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 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, 2021 IBM Corp. All rights reserved.
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 backupFull = require('./includes/backup.js');
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 error = require('./includes/error.js');
25
- const request = require('./includes/request.js');
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 events = require('events');
30
- const fs = require('fs');
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 {object} x - Object under test.
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
- // Is it a number?
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 arguments.
52
+ * Validate URL.
53
53
  *
54
- * @param {object} url - URL of database.
55
- * @param {object} opts - Options.
56
- * @param {function} cb - Callback to be called on error.
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 validateArgs(url, opts, cb) {
58
+ async function validateURL(url, isIAM) {
59
59
  if (typeof url !== 'string') {
60
- cb(new error.BackupError('InvalidOption', 'Invalid URL, must be type string'), null);
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
- cb(new error.BackupError('InvalidOption', 'Invalid URL protocol.'));
102
- return;
67
+ throw new OptionError('Invalid URL protocol.');
103
68
  }
104
69
  if (!urlObject.pathname || urlObject.pathname === '/') {
105
- cb(new error.BackupError('InvalidOption', 'Invalid URL, missing path element (no database).'));
106
- return;
70
+ throw new OptionError('Invalid URL, missing path element (no database).');
107
71
  }
108
- if (opts && opts.iamApiKey && (urlObject.username || urlObject.password)) {
109
- cb(new error.BackupError('InvalidOption', 'URL user information must not be supplied when using IAM API key.'));
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
- cb(error.wrapPossibleInvalidUrlError(err));
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 && opts.mode === 'shallow') {
121
- if (opts.log || opts.resume) {
122
- console.warn('WARNING: the options "log" and "resume" are invalid when using shallow mode.');
123
- }
124
- if (opts.parallelism) {
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
- if (opts && opts.resume) {
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
- cb(new error.BackupError('NoLogFileName', 'To resume a backup, a log file must be specified'), null);
134
- return;
135
- } else if (!fs.existsSync(opts.log)) {
136
- cb(new error.BackupError('LogDoesNotExist', 'To resume a backup, the log file must exist'), null);
137
- return;
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
- function addEventListener(indicator, emitter, event, f) {
144
- emitter.on(event, function(...args) {
145
- if (!indicator.errored) {
146
- if (event === 'error') indicator.errored = true;
147
- f(...args);
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
- Check the backup database exists and that the credentials used have
154
- visibility. Callback with a fatal error if there is a problem with the DB.
155
- @param {string} db - database object
156
- @param {function(err)} callback - error is undefined if DB exists
157
- */
158
- function proceedIfBackupDbValid(db, callback) {
159
- db.service.headDatabase({ db: db.db }).then(() => callback()).catch(err => {
160
- err = error.convertResponseError(err, err => parseIfDbValidResponseError(db, err));
161
- callback(err);
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
- Check that the restore database exists, is new and is empty. Also verify that the credentials used have
167
- visibility. Callback with a fatal error if there is a problem with the DB.
168
- @param {string} db - database object
169
- @param {function(err)} callback - error is undefined if DB exists, new and empty
170
- */
171
- function proceedIfRestoreDbValid(db, callback) {
172
- db.service.getDatabaseInformation({ db: db.db }).then(response => {
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 (!db.db.startsWith('_') && (docCount !== 0 || deletedDocCount !== 0)) {
178
- const notEmptyDBErr = new Error(`Target database ${db.url}${db.db} is not empty.`);
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
- }).catch(err => {
185
- err = error.convertResponseError(err, err => parseIfDbValidResponseError(db, err));
186
- callback(err);
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
- Convert the database validation response error to a special DatabaseNotFound error
192
- in case the database is missing. Otherwise delegate to the default error factory.
193
- @param {object} db - database object
194
- @param {object} err - HTTP response error
195
- */
196
- function parseIfDbValidResponseError(db, err) {
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 ${db.url}` +
200
- `${db.db} does not exist. ` +
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
- const noDBErr = new Error(msg);
203
- noDBErr.name = 'DatabaseNotFound';
204
- return noDBErr;
278
+ return new BackupError('DatabaseNotFound', msg);
205
279
  }
206
- // Delegate to the default error factory if it wasn't a 404
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
- // Set up the DB client
250
- const backupDB = request.client(srcUrl, opts);
251
-
252
- // Validate the DB exists, before proceeding to backup
253
- proceedIfBackupDbValid(backupDB, function(err) {
254
- if (err) {
255
- if (err.name === 'DatabaseNotFound') {
256
- err.message = `${err.message} Ensure the backup source database exists.`;
257
- }
258
- // Didn't exist, or another fatal error, exit
259
- callback(err);
260
- return;
261
- }
262
- let backup = null;
263
- if (opts.mode === 'shallow') {
264
- backup = backupShallow;
265
- } else { // full mode
266
- backup = backupFull;
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
- // If we're writing to a file, end the writes and register the
325
- // emitFinished function for a callback when the file stream's finish
326
- // event is emitted.
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
- // Set up the DB client
358
- const restoreDB = request.client(targetUrl, opts);
359
-
360
- // Validate the DB exists, before proceeding to restore
361
- proceedIfRestoreDbValid(restoreDB, function(err) {
362
- if (err) {
363
- if (err.name === 'DatabaseNotFound') {
364
- err.message = `${err.message} Create the target database before restoring.`;
365
- } else if (err.name === 'DatabaseNotEmpty') {
366
- err.message = `${err.message} A target database must be a new and empty database.`;
367
- }
368
- // Didn't exist, or another fatal error, exit
369
- callback(err);
370
- return;
371
- }
372
-
373
- restoreInternal(
374
- restoreDB,
375
- opts,
376
- srcStream,
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
  };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Copyright © 2017, 2021 IBM Corp. All rights reserved.
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
  });