Allow backup and restore of a single collection

-Both the backup and restore scripts now support a new optional parameter that allows backing up and restoring a single collection only
-Added new verbiage and examples to the backup and restore script sections of the README
-Removed extra "the"'s from some of the restore database examples in the README
This commit is contained in:
Joe Uhren
2025-03-04 20:40:00 -07:00
parent 76a4b5e218
commit d9e0e54dec
3 changed files with 264 additions and 128 deletions
+29 -7
View File
@@ -873,34 +873,52 @@ cd /path/to/explorer && /path/to/node ./scripts/update_explorer.js "dependencies
#### Backup Database Script #### Backup Database Script
Make a complete backup of an eIquidus mongo database and save to compressed file. A built-in locking mechanism prevents data from being updated or changed while a backup is in process. Backups can be safely created while the explorer is actively running and/or while the explorer is turned off. The following backup scenarios are supported: Make a complete backup of an eIquidus mongo database or single collection and save to compressed file. A built-in locking mechanism prevents data from being updated or changed while a backup is in process. Backups can be safely created while the explorer is actively running and/or while the explorer is turned off.
Parameters:
1. Backup path or filename (optional)
2. Collection name (optional) **NOTE:** This parameter is useful for backing up a single database collection such as the `claimaddresses` or plugin-related collections that can later be restored into an existing database without affecting any other database collections.
The following backup scenarios are supported:
**Backup Database (No filename specified)** **Backup Database (No filename specified)**
`npm run create-backup`: Backs up to the explorer/backups directory by default with the current date as the filename in the format yyyy-MMM-dd.bak `npm run create-backup`: Backs up to the explorer/backups directory by default with the current date as the filename in the format yyyy-MMM-dd.bak
**Backup Database (Partial filename specified)** **Backup Database (Partial filename specified)**
`npm run create-backup test`: Backs up the the explorer/backups directory by default with the filename test.bak `npm run create-backup test`: Backs up the explorer/backups directory by default with the filename test.bak
**Backup Database (Full filename specified)** **Backup Database (Full filename specified)**
`npm run create-backup today.bak`: Backs up the the explorer/backups directory by default with the filename today.bak `npm run create-backup today.bak`: Backs up the explorer/backups directory by default with the filename today.bak
**Backup Database (Full path with partial filename specified)** **Backup Database (Full path with partial filename specified)**
`npm run create-backup /usr/local/bin/abc`: Backs up the the /usr/local/bin directory with the filename abc.bak `npm run create-backup /usr/local/bin/abc`: Backs up the /usr/local/bin directory with the filename abc.bak
**Backup Database (Full path and filename specified)** **Backup Database (Full path and filename specified)**
`npm run create-backup ~/new.bak`: Backs up the the users home directory with the filename new.bak `npm run create-backup ~/new.bak`: Backs up the users home directory with the filename new.bak
**Backup Database (Filename and collection both specified)**
`npm run create-backup test claimaddresses`: Backs up only the `claimaddresses` collection to the explorer/backups directory by default with the filename test.bak
**Backup Database (No filename specified, and backup a single collection only)**
`npm run create-backup "" masternodes`: Backs up only the `masternodes` collection to the explorer/backups directory by default with the current date as the filename in the format yyyy-MMM-dd.bak
#### Restore Database Script #### Restore Database Script
Restore a previously saved eIquidus mongo database backup. :warning: **WARNING:** This will completely overwrite your existing eIquidus mongo database, so be sure to make a full backup before proceeding. A built-in locking mechanism prevents data from being updated or changed while a backup is being restored. Backups can be safely restored while the explorer is actively running and/or while the explorer is turned off. Restore a previously saved eIquidus mongo database backup. :warning: **WARNING:** Unless a single collection name is specified, this will completely overwrite your existing eIquidus mongo database, so be sure to make a full backup before proceeding. A built-in locking mechanism prevents data from being updated or changed while a backup is being restored. Backups can be safely restored while the explorer is actively running and/or while the explorer is turned off.
**NOTE:** Older v1.x eIquidus database backups were compressed into tar.gz files. These older tar.gz backups can still be restored, but you must specifically add the .tar.gz suffix. Example: `npm run restore-backup /path/to/old_backup.tar.gz` **NOTE:** Older v1.x eIquidus database backups were compressed into tar.gz files. These older tar.gz backups can still be restored, but you must specifically add the .tar.gz suffix. Example: `npm run restore-backup /path/to/old_backup.tar.gz`
Parameters:
1. Backup path or filename (optional)
2. Collection name (optional) **NOTE:** This parameter is useful for restoring a single database collection such as the `claimaddresses` or plugin-related collections without affecting any other database collections. This option can be used with a single collection backup or full database backup and will restore only the specified collection.
The following restore scenarios are supported: The following restore scenarios are supported:
**Restore Database (Partial filename specified)** **Restore Database (Partial filename specified)**
@@ -919,6 +937,10 @@ The following restore scenarios are supported:
`npm run restore-backup ~/archive.bak`: Restores the ~/archive.bak file `npm run restore-backup ~/archive.bak`: Restores the ~/archive.bak file
**Restore Database (Filename and collection both specified)**
`npm run restore-backup test claimaddresses`: Restores only the `claimaddresses` collection from the explorer/scripts/backups/test.bak file
#### Delete Database Script #### Delete Database Script
Wipe the eIquidus mongo database clean to start again from scratch. :warning: **WARNING:** This will completely destroy all data in your existing eIquidus mongo database, so be sure to make a full backup before proceeding. A built-in locking mechanism prevents data from being updated or changed while the database is being deleted. The process to delete the database can be executed while the explorer is actively running and/or while the explorer is turned off. Wipe the eIquidus mongo database clean to start again from scratch. :warning: **WARNING:** This will completely destroy all data in your existing eIquidus mongo database, so be sure to make a full backup before proceeding. A built-in locking mechanism prevents data from being updated or changed while the database is being deleted. The process to delete the database can be executed while the explorer is actively running and/or while the explorer is turned off.
+78 -24
View File
@@ -6,6 +6,7 @@ const backupLockName = 'backup';
const settings = require('../lib/settings'); const settings = require('../lib/settings');
let backupPath = path.join(path.dirname(__dirname), 'backups'); let backupPath = path.join(path.dirname(__dirname), 'backups');
let backupFilename; let backupFilename;
let singleCollection = '';
let lockCreated = false; let lockCreated = false;
// exit function used to cleanup lock before finishing script // exit function used to cleanup lock before finishing script
@@ -20,6 +21,42 @@ function exit(exitCode) {
} }
} }
// verify that the collection exists
function verify_collection_exists(cb) {
// check if the backup will be for a single collection
if (singleCollection != null && singleCollection != '') {
const mongoose = require('mongoose');
const dbString = `mongodb://${encodeURIComponent(settings.dbsettings.user)}:${encodeURIComponent(settings.dbsettings.password)}@${settings.dbsettings.address}:${settings.dbsettings.port}/${settings.dbsettings.database}`;
console.log('Connecting to database..');
mongoose.set('strictQuery', true);
// connect to mongo database
mongoose.connect(dbString).then(() => {
// lookup the collection in the list of collections
mongoose.connection.db.listCollections({ name: singleCollection }).toArray().then((collections) => {
// check if the collection exists
if (collections.length > 0) {
// collection exists
return cb(false);
} else {
// collection not found
return cb(true);
}
}).catch((err) => {
console.log(err);
return cb(true);
});
}).catch((err) => {
console.log('Error: Unable to connect to database: %s', err);
exit(999);
return cb(true);
});
} else
return cb(false);
}
// check if a backup filename was passed into the script // check if a backup filename was passed into the script
if (process.argv[2] != null && process.argv[2] != '') { if (process.argv[2] != null && process.argv[2] != '') {
// use the backup filename passed into this script // use the backup filename passed into this script
@@ -32,6 +69,12 @@ if (process.argv[2] != null && process.argv[2] != '') {
backupFilename = `${systemDate.getFullYear()}-${monthName[systemDate.getMonth()]}-${systemDate.getDate()}`; backupFilename = `${systemDate.getFullYear()}-${monthName[systemDate.getMonth()]}-${systemDate.getDate()}`;
} }
// check if a collection name was passed into the script
if (process.argv[3] != null && process.argv[3] != '') {
// save the collection name to a variable for later use
singleCollection = process.argv[3];
}
// check if backup filename has the archive suffix already // check if backup filename has the archive suffix already
if (backupFilename.endsWith(archiveSuffix)) { if (backupFilename.endsWith(archiveSuffix)) {
// remove the archive suffix from the backup filename // remove the archive suffix from the backup filename
@@ -58,41 +101,52 @@ if (!fs.existsSync(path.join(backupPath, `${backupFilename}${archiveSuffix}`)))
if (lib.is_locked([backupLockName]) == false) { if (lib.is_locked([backupLockName]) == false) {
// create a new backup lock before checking the rest of the locks to minimize problems with running scripts at the same time // create a new backup lock before checking the rest of the locks to minimize problems with running scripts at the same time
lib.create_lock(backupLockName); lib.create_lock(backupLockName);
// ensure the lock will be deleted on exit // ensure the lock will be deleted on exit
lockCreated = true; lockCreated = true;
// check all other possible locks since backups should not run at the same time that data is being changed // check all other possible locks since backups should not run at the same time that data is being changed
if (lib.is_locked(['restore', 'delete', 'index', 'markets', 'peers', 'masternodes', 'plugin']) == false) { if (lib.is_locked(['restore', 'delete', 'index', 'markets', 'peers', 'masternodes', 'plugin']) == false) {
// all tests passed. OK to run backup // check if the collection name exists
console.log(`${settings.localization.script_launched }: ${process.pid}`); verify_collection_exists(function(collection_error) {
// check if there was an error finding the collection by name
if (!collection_error) {
// all tests passed. OK to run backup
console.log(`${settings.localization.script_launched }: ${process.pid}`);
const { exec } = require('child_process'); const { exec } = require('child_process');
const randomDirectoryName = Math.random().toString(36).substring(2, 15) + Math.random().toString(23).substring(2, 5);
// execute backup // execute backup
const backupProcess = exec(`mongodump --host="${settings.dbsettings.address}" --port="${settings.dbsettings.port}" --username="${settings.dbsettings.user}" --password="${settings.dbsettings.password}" --db="${settings.dbsettings.database}" --archive="${path.join(backupPath, backupFilename + archiveSuffix)}" --gzip`); const backupProcess = exec(`mongodump --host="${settings.dbsettings.address}" --port="${settings.dbsettings.port}" --username="${settings.dbsettings.user}" --password="${settings.dbsettings.password}" --db="${settings.dbsettings.database}" --archive="${path.join(backupPath, backupFilename + archiveSuffix)}" --gzip${singleCollection == null || singleCollection == '' ? '' : ` --collection ${singleCollection}`}`);
backupProcess.stdout.on('data', (data) => { backupProcess.stdout.on('data', (data) => {
console.log(data); console.log(data);
}); });
backupProcess.stderr.on('data', (data) => { backupProcess.stderr.on('data', (data) => {
console.log(Buffer.from(data).toString()); console.log(Buffer.from(data).toString());
}); });
backupProcess.on('error', (error) => { backupProcess.on('error', (error) => {
console.log(error); console.log(error);
}); });
backupProcess.on('exit', (code, signal) => { backupProcess.on('exit', (code, signal) => {
if (code) { if (code) {
console.log(`Process exit with code: ${code}`); console.log(`Process exit with code: ${code}`);
exit(code); exit(code);
} else if (signal) { } else if (signal) {
console.log(`Process killed with signal: ${signal}`); console.log(`Process killed with signal: ${signal}`);
exit(1); exit(1);
} else {
console.log(`Backup saved successfully to ${path.join(backupPath, backupFilename + archiveSuffix)}`);
exit(0);
}
});
} else { } else {
console.log(`Backup saved successfully to ${path.join(backupPath, backupFilename + archiveSuffix)}`); // the collection does not exist
exit(0); console.log(`Collection "${singleCollection}" does not exist`);
exit(2);
} }
}); });
} else { } else {
+157 -97
View File
@@ -7,7 +7,8 @@ const restoreLockName = 'restore';
const tarModule = 'tar'; const tarModule = 'tar';
const defaultBackupPath = path.join(path.dirname(__dirname), 'backups'); const defaultBackupPath = path.join(path.dirname(__dirname), 'backups');
const settings = require('../lib/settings'); const settings = require('../lib/settings');
var lockCreated = false; let singleCollection = '';
let lockCreated = false;
// exit function used to cleanup lock before finishing script // exit function used to cleanup lock before finishing script
function exit(mongoose, exitCode) { function exit(mongoose, exitCode) {
@@ -83,19 +84,13 @@ function drop_collection(mongoose, colName, cb) {
} }
function delete_database(mongoose, cb) { function delete_database(mongoose, cb) {
const dbString = `mongodb://${encodeURIComponent(settings.dbsettings.user)}:${encodeURIComponent(settings.dbsettings.password)}@${settings.dbsettings.address}:${settings.dbsettings.port}/${settings.dbsettings.database}`; // check if a single collection is being restored
if (singleCollection != null && singleCollection == '') {
console.log('Connecting to database..');
mongoose.set('strictQuery', true);
// connect to mongo database
mongoose.connect(dbString).then(() => {
// get the list of collections // get the list of collections
mongoose.connection.db.listCollections().toArray().then((collections) => { mongoose.connection.db.listCollections().toArray().then((collections) => {
// check if there are any collections // check if there are any collections
if (collections.length > 0) { if (collections.length > 0) {
var counter = 0; let counter = 0;
// loop through all collections // loop through all collections
collections.forEach((collection) => { collections.forEach((collection) => {
@@ -122,10 +117,10 @@ function delete_database(mongoose, cb) {
console.log(err); console.log(err);
return cb(true); return cb(true);
}); });
}).catch((err) => { } else {
console.log('Error: Unable to connect to database: %s', err); // do not delete any collections since mongorestore will drop the collection automatically
exit(mongoose, 999); return cb(true);
}); }
} }
function restore_backup(mongoose, backupPath, extractedPath, gZip) { function restore_backup(mongoose, backupPath, extractedPath, gZip) {
@@ -134,7 +129,7 @@ function restore_backup(mongoose, backupPath, extractedPath, gZip) {
console.log(`${settings.localization.restoring_backup}.. ${settings.localization.please_wait}..`); console.log(`${settings.localization.restoring_backup}.. ${settings.localization.please_wait}..`);
// restore mongo database from backup // restore mongo database from backup
const restoreProcess = exec(`mongorestore --host="${settings.dbsettings.address}" --port="${settings.dbsettings.port}" --username="${settings.dbsettings.user}" --password="${settings.dbsettings.password}" --authenticationDatabase="${settings.dbsettings.database}" ${(gZip ? `--gzip --archive="${backupPath}"` : `"${extractedPath}"`)}`); const restoreProcess = exec(`mongorestore --host="${settings.dbsettings.address}" --port="${settings.dbsettings.port}" --username="${settings.dbsettings.user}" --password="${settings.dbsettings.password}" --authenticationDatabase="${settings.dbsettings.database}" ${(gZip ? `--gzip --archive="${backupPath}"` : `"${extractedPath}"`)}${singleCollection == null || singleCollection == '' ? '' : ` --drop --db explorerdb --collection ${singleCollection}`}`);
restoreProcess.stdout.on('data', (data) => { restoreProcess.stdout.on('data', (data) => {
console.log(data); console.log(data);
@@ -173,9 +168,73 @@ function restore_backup(mongoose, backupPath, extractedPath, gZip) {
}); });
} }
function delete_prompt(cb) {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// check if a single collection is being restored
if (singleCollection != null && singleCollection != '')
console.log(`You are about to delete and restore the "${singleCollection}" collection from backup.`);
else
console.log('You are about to delete the current eIquidus database and restore from backup.');
// prompt for restoring explorer database
rl.question(`${settings.localization.are_you_sure}: `, function (restoreAnswer) {
// stop prompting
rl.close();
// return the answer
return cb(restoreAnswer);
});
}
// verify that the collection exists
function verify_collection_exists(mongoose, cb) {
const dbString = `mongodb://${encodeURIComponent(settings.dbsettings.user)}:${encodeURIComponent(settings.dbsettings.password)}@${settings.dbsettings.address}:${settings.dbsettings.port}/${settings.dbsettings.database}`;
console.log('Connecting to database..');
mongoose.set('strictQuery', true);
// connect to mongo database
mongoose.connect(dbString).then(() => {
// check if the restore will be for a single collection
if (singleCollection != null && singleCollection != '') {
// lookup the collection in the list of collections
mongoose.connection.db.listCollections({ name: singleCollection }).toArray().then((collections) => {
// check if the collection exists
if (collections.length > 0) {
// collection exists
return cb(false);
} else {
// collection not found
return cb(true);
}
}).catch((err) => {
console.log(err);
return cb(true);
});
} else
return cb(false);
}).catch((err) => {
console.log('Error: Unable to connect to database: %s', err);
exit(999);
return cb(true);
});
}
// check if a backup filename was passed into the script // check if a backup filename was passed into the script
if (process.argv[2] != null && process.argv[2] != '') { if (process.argv[2] != null && process.argv[2] != '') {
var backupPath = process.argv[2]; let backupPath = process.argv[2];
// check if a collection name was passed into the script
if (process.argv[3] != null && process.argv[3] != '') {
// save the collection name to a variable for later use
singleCollection = process.argv[3];
}
// check if the backup filename already has a path // check if the backup filename already has a path
if (!fs.existsSync(`${backupPath}`)) { if (!fs.existsSync(`${backupPath}`)) {
@@ -197,99 +256,100 @@ if (process.argv[2] != null && process.argv[2] != '') {
// check for the backup file (last time) // check for the backup file (last time)
if (fs.existsSync(`${backupPath}`)) { if (fs.existsSync(`${backupPath}`)) {
const readline = require('readline'); const mongoose = require('mongoose');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('You are about to delete the current eIquidus database and restore from backup.'); // check if the collection name exists
verify_collection_exists(mongoose, function(collection_error) {
// check if there was an error finding the collection by name
if (!collection_error) {
// prompt for deleting and restoring the database
delete_prompt(function(restoreAnswer) {
// determine if the explorer database should be restored
switch ((restoreAnswer == null ? '' : restoreAnswer).toLowerCase()) {
case settings.localization.short_yes:
case settings.localization.long_yes:
// check if the "restore backup" process is already running
if (lib.is_locked([restoreLockName]) == false) {
// create a new restore lock before checking the rest of the locks to minimize problems with running scripts at the same time
lib.create_lock(restoreLockName);
// prompt for restoring explorer database // ensure the lock will be deleted on exit
rl.question(`${settings.localization.are_you_sure}: `, function (restoreAnswer) { lockCreated = true;
// stop prompting
rl.close();
// determine if the explorer database should be restored // check all other possible locks since restoring backups should not run at the same time that data is being changed
switch ((restoreAnswer == null ? '' : restoreAnswer).toLowerCase()) { if (lib.is_locked(['backup', 'delete', 'index', 'markets', 'peers', 'masternodes', 'plugin']) == false) {
case settings.localization.short_yes: // all tests passed. OK to run restore
case settings.localization.long_yes: console.log(`${settings.localization.script_launched }: ${process.pid}`);
// check if the "restore backup" process is already running
if (lib.is_locked([restoreLockName]) == false) {
// create a new restore lock before checking the rest of the locks to minimize problems with running scripts at the same time
lib.create_lock(restoreLockName);
// ensure the lock will be deleted on exit
lockCreated = true;
// check all other possible locks since restoring backups should not run at the same time that data is being changed
if (lib.is_locked(['backup', 'delete', 'index', 'markets', 'peers', 'masternodes', 'plugin']) == false) {
// all tests passed. OK to run restore
console.log(`${settings.localization.script_launched }: ${process.pid}`);
// check if this is a tar.gz (older explorer backup format) // check if this is a tar.gz (older explorer backup format)
if (!backupPath.endsWith(oldArchiveSuffix)) { if (!backupPath.endsWith(oldArchiveSuffix)) {
const mongoose = require('mongoose'); const mongoose = require('mongoose');
// newer backup format (.bak) // newer backup format (.bak)
// delete all collections from existing database // delete all collections from existing database
delete_database(mongoose, function(retVal) { delete_database(mongoose, function(retVal) {
if (retVal) { if (retVal) {
// move on to the restore process // move on to the restore process
restore_backup(mongoose, backupPath, backupPath, true); restore_backup(mongoose, backupPath, backupPath, true);
} }
}); });
} else { } else {
// older backup format (.tar.gz) // older backup format (.tar.gz)
// check if the tar module is already installed // check if the tar module is already installed
check_module_directory_exists(tarModule, function(retVal) { check_module_directory_exists(tarModule, function(retVal) {
const tar = require(tarModule); const tar = require(tarModule);
console.log(`${settings.localization.extracting_backup_files}.. ${settings.localization.please_wait}..`); console.log(`${settings.localization.extracting_backup_files}.. ${settings.localization.please_wait}..`);
// extract the backup archive // extract the backup archive
tar.x({ file: backupPath, cwd: defaultBackupPath, gzip: true }, function() { tar.x({ file: backupPath, cwd: defaultBackupPath, gzip: true }, function() {
var extractedPath = path.join(defaultBackupPath, path.basename(backupPath).replace(oldArchiveSuffix, '')); var extractedPath = path.join(defaultBackupPath, path.basename(backupPath).replace(oldArchiveSuffix, ''));
// check if this is a valid backup archive now that the files have been extracted // check if this is a valid backup archive now that the files have been extracted
if (fs.existsSync(`${path.join(extractedPath, settings.dbsettings.database)}`)) { if (fs.existsSync(`${path.join(extractedPath, settings.dbsettings.database)}`)) {
const mongoose = require('mongoose'); // delete all collections from existing database
delete_database(mongoose, function(retVal) {
// delete all collections from existing database if (retVal) {
delete_database(mongoose, function(retVal) { // move on to the restore process
if (retVal) { restore_backup(mongoose, backupPath, extractedPath, false);
// move on to the restore process }
restore_backup(mongoose, backupPath, extractedPath, false); });
} else {
// backup file is not a valid mongo database backup
// try to remove the backup directory
try {
fs.rmSync(extractedPath, { recursive: true });
} catch {
// do nothing
} finally {
console.log(`${path.basename(backupPath)} is not a valid backup file`);
exit(mongoose, 1);
}
} }
}); });
} else { });
// backup file is not a valid mongo database backup }
// try to remove the backup directory } else {
try { // another script process is currently running
fs.rmSync(extractedPath, { recursive: true }); console.log("Restore aborted");
} catch { exit(mongoose, 2);
// do nothing }
} finally { } else {
console.log(`${path.basename(backupPath)} is not a valid backup file`); // restore process is already running
exit(null, 1); console.log("Restore aborted");
} exit(mongoose, 2);
}
});
});
} }
} else {
// another script process is currently running
console.log("Restore aborted");
exit(null, 2);
}
} else {
// restore process is already running
console.log("Restore aborted");
exit(null, 2);
}
break; break;
default: default:
console.log(`${settings.localization.process_aborted}. ${settings.localization.nothing_was_restored}.`); console.log(`${settings.localization.process_aborted}. ${settings.localization.nothing_was_restored}.`);
exit(null, 2); exit(mongoose, 2);
}
});
} else {
// the collection does not exist
console.log(`Collection "${singleCollection}" does not exist`);
exit(mongoose, 2);
} }
}); });
} else { } else {