diff --git a/README.md b/README.md index 58df403..b00aa9f 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,20 @@ Rsync Does **NOT** Ensure Consistency | Rsync **May** Ensure Integrity - Enabling this flag will incur a performance penalty as many more checksums may be generated - `--accurateProgress` - Recurse all directories before transferring any files to generate a more accurate file tree - - *Note: This will increase memory usage substantially* + - *Note: This will increase memory usage substantially (10x increase is possible)* ##### Snapshot Management - `--maxSnapshots NUMBER` - Maximum number of snapshots - Once number is exceeded, oldest snapshots will be deleted until the condition is met +##### Script Hooks + Script Hooks can be used to run scripts before or after backup on the client while using the same log file as the backup process. Script hooks are not run in parallel. +- `--runBefore EXECUTABLE` *Can be used multiple times* + - Script to run on client before backup (file will be executed directly and output will be logged) + - Can be useful for taking backups of data that requires consistency (ex: running pg_dump) and putting it in a folder that will be transfered by Rsync in the backup +- `--runAfter EXECUTABLE` *Can be used multiple times* + - Script to run on client after backup + - Hook will only trigger if backup is successful + - Can be useful for deleting temporary data after it is successfully transferred ##### Logging - `--logFormat FORMAT` *Default:* `text` - Format used to log output diff --git a/index.js b/index.js index ba4056f..e4a5ccb 100644 --- a/index.js +++ b/index.js @@ -89,6 +89,21 @@ let backup = async () => { logger.logger.log('stdout')({msgType: 'progress', status: 'No Previous Snapshots Detected, Creating Full Backup'}); } + //Configure Script Before Backup Hooks + let runBefore = []; + if(argv.runBefore !== undefined){ + runBefore = argv.runBefore; + if(!Array.isArray(argv.runBefore)) + runBefore = [argv.runBefore]; + } + + if(runBefore.length){ + logger.logStateChange('Executing Pre Backup Hooks'); + for(let executablePath of runBefore){ + await incrementer.executeScriptHook(executablePath); + } + } + //Execute Rsync debug('Executing command: '+rsync.command()); rsyncPid = logger.startRsync(rsync); @@ -98,11 +113,34 @@ let backup = async () => { let finalized = await incrementer.finalizeBackup(); if(finalized) { logger.setFinalDestination(incrementer.finalDest); - let deleted = await incrementer.deleteOldSnapshots(); - if(deleted) - logger.logStateChange('Deleted Oldest Snapshots'); + await incrementer.deleteOldSnapshots(); } }); + + //Configure Script After Backup Hooks + let runAfter = []; + if(argv.runAfter !== undefined){ + runAfter = argv.runAfter; + if(!Array.isArray(argv.runAfter)) + runAfter = [argv.runAfter]; + } + + if(runAfter.length){ + logger.addSuccessCallback(() => { + logger.logStateChange('Executing Post Backup Hooks'); + }); + + runAfter.forEach((executablePath) => { + logger.addSuccessCallback(async () => { + await incrementer.executeScriptHook(executablePath); + }); + }); + } + + //Success Message + logger.addSuccessCallback(() => { + logger.logStateChange('Backup Finalized') + }); }; function quit () { //Handle killing rsync process diff --git a/lib/Incrementer.js b/lib/Incrementer.js index 6486f98..3930b0d 100644 --- a/lib/Incrementer.js +++ b/lib/Incrementer.js @@ -2,6 +2,7 @@ const debug = require('debug')('RsyncBackup:lib:Incrementer'); const spawn = require('child_process').spawn; +const execFile = require('child_process').execFile; const path = require('path'); class Incrementer{ @@ -70,8 +71,8 @@ class Incrementer{ let hasError = false; let errorCallback = (error) => { error = error.toString(); - this.generator.logger.stderr('An error occurred connecting to server while deleting old snapshots:'); - this.generator.logger.stderr(error); + this.generator.logger.log('stderr')('An error occurred connecting to server while deleting old snapshots:'); + this.generator.logger.log('stderr')(error); hasError = true; }; @@ -96,8 +97,8 @@ class Incrementer{ let hasError = false; let ssh = this.runCommand(bashCommand, (error) => { error = error.toString(); - this.generator.logger.stderr('An error occurred connecting to server while preparing for backup:'); - this.generator.logger.stderr(error); + this.generator.logger.log('stderr')('An error occurred connecting to server while preparing for backup:'); + this.generator.logger.log('stderr')(error); hasError = true; }, (output) => { //Output is list of snapshots newest to oldest, pick the newest for linking output = output.toString().split('\n'); @@ -109,7 +110,7 @@ class Incrementer{ try { this.snapshotCount = Number(line.trim()); } catch(e){ - this.generator.logger.stderr(`Error determining number of snapshots, found: ${line}`); + this.generator.logger.log('stderr')(`Error determining number of snapshots, found: ${line}`); } } else if(line.match(/^\d{4}(-\d{2}){2}.(-?\d{2}){3}(?!.incomplete$)/g)){ //If folder matches the naming format (and is not incomplete) this.linkDest = path.join(this.remoteDirectory, line); @@ -135,8 +136,8 @@ class Incrementer{ let hasError = false; let errorCallback = (error) => { error = error.toString(); - this.generator.logger.stderr('An error occurred connecting to server while finalizing backup:'); - this.generator.logger.stderr(error); + this.generator.logger.log('stderr')('An error occurred connecting to server while finalizing backup:'); + this.generator.logger.log('stderr')(error); hasError = true; }; @@ -152,6 +153,52 @@ class Incrementer{ }); } + executeScriptHook(executablePath){ + return new Promise((resolve, reject) => { + executablePath = path.resolve(executablePath); + let scriptName = path.basename(executablePath); + + this.generator.logger.log('stdout')({msgType: 'progress', status: `${scriptName} Started`}); + + let proc; + try { + proc = execFile(executablePath); + + proc.stdout.on('data', (data) => { + let str = data.toString(); + let lines = str.split('\n'); + lines.forEach((line) => { + this.generator.logger.log('stdout')({msgType: 'progress', status: line}); + }); + }); + + proc.stderr.on('data', (data) => { + let str = data.toString(); + let lines = str.split('\n'); + lines.forEach((line) => { + this.generator.logger.log('stderr')(line); + }); + }); + + proc.on('exit', (code) => { + if (code === 0) { + this.generator.logger.log('stdout')({msgType: 'progress', status: `${scriptName} Exited`}); + } else { + this.generator.logger.log('stderr')(`${scriptName} exited with code ${code}`) + } + + resolve(code); + }); + } catch(err){ //Failed to start process + delete err.stack; + this.generator.logger.log('stderr')(err); + + this.generator.logger.log('stderr')(`${scriptName} Failed`); + resolve(-1); + } + }); + } + leadingZero(digit, length){ digit = digit+''; while(digit.length < length){ diff --git a/lib/LogGenerator.js b/lib/LogGenerator.js index 07b6bbb..7302d26 100644 --- a/lib/LogGenerator.js +++ b/lib/LogGenerator.js @@ -98,7 +98,7 @@ class LogGenerator { } startRsync(rsync){ - let newState = `\tStarting Backup - ${this.tempDir}`; + let newState = `Starting Backup - ${this.tempDir}`; if(this.linkDir) newState+= ` - Increment From ${this.linkDir}`; this.logger.stateChange(newState); diff --git a/lib/Logger.js b/lib/Logger.js index 944847b..88df6d8 100644 --- a/lib/Logger.js +++ b/lib/Logger.js @@ -43,28 +43,30 @@ class Logger { return (data, exitCode) => { //ExitCode only used by callback //Data can be a Buffer, string, JSON or Error(only if stderr) let json = undefined; - let str = undefined; //Convert data to string if it is a buffer if(Buffer.isBuffer(data)) { - str = data.toString(); + data = data.toString(); } else if(data instanceof Error){ - json = [{error: data.stack}]; + if(data.stack) + json = [{error: data.stack}]; + else + json = [{error: data}]; } else if(typeof data === 'object') { json = [data]; } //Convert data to JSON if possible - if(typeof str === 'string' && jsonFn){ - json = jsonFn.bind(this)(str); + if(typeof data === 'string' && jsonFn){ + json = jsonFn.bind(this)(data); } let print; //Bind Logger Context and transform to logger type if(type === 'callback') - print = fn.bind(this.generator.logger)(str, exitCode); + print = fn.bind(this.generator.logger)(data, exitCode); else - print = fn.bind(this.generator.logger)(str, json); + print = fn.bind(this.generator.logger)(data, json); if(!Array.isArray(print)) print = [print]; @@ -112,7 +114,7 @@ class Logger { } callback(error, exitCode){ //Called on process completion - return this.outputParser.callback(error, exitCode); + this.outputParser.callback(error, exitCode); //Don't return because we don't want it output } stateChange(newState){ //States are only logged to output file diff --git a/lib/OutputParser.js b/lib/OutputParser.js index 48dcd96..11b224d 100644 --- a/lib/OutputParser.js +++ b/lib/OutputParser.js @@ -70,7 +70,7 @@ class OutputParser{ return output; } - callback(error, exitCode){ + async callback(error, exitCode){ if(exitCode !== 0 && exitCode !== 24){ //Some error occurred during process execution (Code 24 is files vanished which is to be expected) this.generator.logger.log('stderr')(`Backup Failed, rsync exited with code ${exitCode}`); @@ -81,7 +81,7 @@ class OutputParser{ this.generator.logger.log('stdout')(this.summary); for (let callback of this.generator.callbacks) - callback(); + await callback(); this.generator.callbacks = []; } }