Skip to content

Commit

Permalink
Implement Script Hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
mattlyons0 committed Feb 13, 2018
1 parent 48a290f commit 161eee9
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 22 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 41 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
61 changes: 54 additions & 7 deletions lib/Incrementer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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;
};

Expand All @@ -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');
Expand All @@ -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);
Expand All @@ -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;
};

Expand All @@ -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){
Expand Down
2 changes: 1 addition & 1 deletion lib/LogGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 10 additions & 8 deletions lib/Logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/OutputParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);

Expand All @@ -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 = [];
}
}
Expand Down

0 comments on commit 161eee9

Please sign in to comment.