fs-transform
Advanced tools
Comparing version 1.0.1 to 2.0.0
'use strict'; | ||
var Transformer = require('transformer.js'); | ||
var Transformer = require('./lib/transformer.js'); | ||
@@ -5,0 +5,0 @@ /** |
@@ -6,2 +6,8 @@ 'use strict'; | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
var isString = require('101/is-string'); | ||
var last = require('101/last'); | ||
var async = require('async'); | ||
var debug = require('debug'); | ||
var execLog = debug('fs-transform:exec'); | ||
@@ -32,10 +38,20 @@ /** | ||
if (last(root) === '/') { | ||
this.root = root.substr(0, root.length - 1); | ||
this.root = path.resolve(root.substr(0, root.length - 1)); | ||
} | ||
else { | ||
this.root = root; | ||
this.root = path.resolve(root); | ||
} | ||
this.working = null; | ||
} | ||
/** | ||
* Commands required to use the `FsDriver` class. | ||
* @type {array} | ||
*/ | ||
Object.defineProperty(FsDriver, "commands", { | ||
value: ['cp', 'mv', 'grep', 'sed', 'diff', 'rm'], | ||
writable: false | ||
}); | ||
/** | ||
* Makes a relative path absolute to the root directory of the driver. Returns | ||
@@ -57,9 +73,96 @@ * the original path if it is already absolute. | ||
FsDriver.prototype.absolutePath = function (path) { | ||
if (!isString(path)) { | ||
return null; | ||
} | ||
if (path.charAt(0) === '/') { | ||
return path; | ||
} | ||
return this.root + '/' + path; | ||
} | ||
return !this.working ? | ||
this.root + '/' + path : | ||
this.working + '/' + path; | ||
}; | ||
/** | ||
* Escapes a string for use in a command. | ||
* @param {string} str String to escape. | ||
* @return {string} The command line escaped string. | ||
*/ | ||
FsDriver.prototype.escape = function (str) { | ||
// Note: I tried alternatives to this, but none worked | ||
// in both osx and linux :( | ||
return str.replace(/(['/\\])/g, '\\$1'); | ||
}; | ||
/** | ||
* Logs and executes a command. | ||
* @param {string} command Command to execute. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after command | ||
* finishes. | ||
*/ | ||
FsDriver.prototype.exec = function (command, cb) { | ||
var self = this; | ||
execLog(command); | ||
childProcess.exec(command, function (err, output) { | ||
var scriptCommand; | ||
if (!self.working) { | ||
scriptCommand = command; | ||
} | ||
else { | ||
scriptCommand = command.replace( | ||
new RegExp(self.working + '([^\\s]*)', 'g'), | ||
self.root + '$1' | ||
); | ||
} | ||
cb(err, output, scriptCommand); | ||
}); | ||
}; | ||
/** | ||
* Callback to invoke after the command run by FsDriver.exec finishes. | ||
* @param {Error} [err] An error, if one occurred when executing the command. | ||
* @param {string} [output] The text result of the command. | ||
* @param {string} scriptCommand The command that was executed, with all | ||
* filenames pathed relative to the root (used for saving script commands). | ||
*/ | ||
/** | ||
* Determines if a given command is installed on the system. | ||
* @param {string} commandName Name of the command to check. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after command | ||
* finishes. | ||
*/ | ||
FsDriver.prototype.hasCommand = function (commandName, cb) { | ||
this.exec('command -v ' + commandName, cb); | ||
}; | ||
/** | ||
* Ensures that all of the commands needed to run the FsDriver are installed | ||
* on the client system. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after command | ||
* finishes. | ||
*/ | ||
FsDriver.prototype.hasAllCommands = function(cb) { | ||
async.each(FsDriver.commands, this.hasCommand.bind(this), cb); | ||
}; | ||
/** | ||
* @returns a shell script that checks for all needed commands. | ||
*/ | ||
FsDriver.prototype.getCommandCheckScript = function () { | ||
var checks = FsDriver.commands.map(function (name) { | ||
return 'command -v ' + name + ' >/dev/null 2>&1 ' + | ||
'|| { echo "' + | ||
'Required command: ' + name + | ||
' -- Please install it before running this script.' + | ||
'"; exit 1; }'; | ||
}).join('\n'); | ||
return [ | ||
'# Check to ensure required commands are available', | ||
checks, | ||
'# END Required command check' | ||
].join('\n') + '\n'; | ||
}; | ||
/** | ||
* Moves a file. | ||
@@ -76,13 +179,11 @@ * | ||
* @param {string} dest Destination file path. | ||
* @param {function} cb Callback to execute with the results of the file move. | ||
* @return {string} The move command to be executed. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after the move | ||
* completes. | ||
*/ | ||
FsDriver.prototype.move = function (source, dest, cb) { | ||
var command = [ | ||
this.exec([ | ||
'mv', | ||
this.absolutePath(source), | ||
this.absolutePath(dest) | ||
].join(' '); | ||
childProcess.exec(command, cb); | ||
return command; | ||
].join(' '), cb); | ||
}; | ||
@@ -101,13 +202,12 @@ | ||
* @param {string} dest Destination file path. | ||
* @param {function} cb Callback to execute with the results of the file copy. | ||
* @return {string} The copy command to be executed. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after the copy | ||
* completes. | ||
*/ | ||
FsDriver.prototype.copy = function (source, dest, cb) { | ||
var command = [ | ||
this.exec([ | ||
'cp', | ||
this.absolutePath(source), | ||
this.absolutePath(dest) | ||
].join(' '); | ||
childProcess.exec(command, cb); | ||
return command; | ||
].join(' '), cb); | ||
}; | ||
@@ -126,13 +226,11 @@ | ||
* @param {string} text Text to search for. | ||
* @param {function} cb Callback to execute after the search has been performed. | ||
* @return {string} The grep command that is to be executed. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after the grep | ||
* completes. | ||
*/ | ||
FsDriver.prototype.grep = function (text, cb) { | ||
var command = [ | ||
this.exec([ | ||
'grep -rn', | ||
text.replace(/\\/, '\\\\'), | ||
this.root | ||
].join(' '); | ||
childProcess.exec(command, cb); | ||
return command; | ||
'\'' + this.escape(text) + '\'', | ||
this.working ? this.working : this.root | ||
].join(' '), cb); | ||
}; | ||
@@ -153,4 +251,4 @@ | ||
* @param {Number} line Line number on which to perform the replacement. | ||
* @param {function} cb Callback to execute when the replacement has been made. | ||
* @return {string} The sed command that is to be executed. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after the sed | ||
* completes. | ||
*/ | ||
@@ -160,4 +258,4 @@ FsDriver.prototype.sed = function (search, replace, name, line, cb) { | ||
line + 's', | ||
search.replace(/\//, '\\/'), | ||
replace.replace(/\//, '\\/'), | ||
this.escape(search), | ||
this.escape(replace), | ||
'g' | ||
@@ -167,9 +265,8 @@ ].join('/'); | ||
var command = [ | ||
'sed -i "" "', pattern, '" ', | ||
'sed -i.last', | ||
'\'' + pattern + '\'', | ||
this.absolutePath(name) | ||
].join(''); | ||
].join(' '); | ||
childProcess.exec(command, cb); | ||
return command; | ||
this.exec(command, cb); | ||
}; | ||
@@ -185,1 +282,102 @@ | ||
}; | ||
/** | ||
* Performs a file diff. | ||
* @param {string} a Name of the first file for the diff. | ||
* @param {string} b Name of the second file. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after the diff | ||
* completes. | ||
*/ | ||
FsDriver.prototype.diff = function (a, b, cb) { | ||
var command = [ | ||
'diff -u -r', | ||
this.absolutePath(a), | ||
this.absolutePath(b) | ||
].join(' '); | ||
this.exec(command, function (err, diff, scriptCommand) { | ||
// `diff` returns 1 when there are differences and > 1 when there is trouble | ||
if (err && err.code > 1) { return cb(err); } | ||
cb(null, diff, scriptCommand); | ||
}); | ||
}; | ||
/** | ||
* Removes a file. | ||
* @param {string} filename Name of the file to remove. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after the remove | ||
* completes. | ||
*/ | ||
FsDriver.prototype.remove = function (filename, cb) { | ||
this.exec([ | ||
'rm', | ||
this.absolutePath(filename) | ||
].join(' '), cb); | ||
}; | ||
/** | ||
* Recursively removes a file or directory. | ||
* @param {string} filename Name of the file to remove. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after the remove | ||
* completes. | ||
*/ | ||
FsDriver.prototype.removeRecursive = function (filename, cb) { | ||
this.exec([ | ||
'rm -rf', | ||
this.absolutePath(filename) | ||
].join(' '), cb); | ||
}; | ||
/** | ||
* Attempts to find an available working directory relative to /tmp. | ||
*/ | ||
FsDriver.prototype.findWorkingDirectory = function() { | ||
var rootName = last(this.root.split('/')); | ||
var workingPath = '/tmp/.' + rootName + '.fs-work'; | ||
var n = Math.random(); | ||
while (this.exists(workingPath + '.' + n)) { | ||
n = Math.random(); | ||
} | ||
return workingPath += '.' + n; | ||
}; | ||
/** | ||
* Creates a temporary working copy of the root directory. This copy is used to | ||
* set the absolute path for given relative file names in various commands (e.g. | ||
* move, copy, grep, sed, rm, etc.) | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after the working | ||
* directory has been created. | ||
*/ | ||
FsDriver.prototype.createWorkingDirectory = function (cb) { | ||
var self = this; | ||
var workingPath = this.findWorkingDirectory(); | ||
var copyCommand = ['cp -r', this.root, workingPath].join(' '); | ||
this.exec(copyCommand, function (err) { | ||
if (err) { return cb(err); } | ||
self.working = workingPath; | ||
cb(); | ||
}); | ||
}; | ||
/** | ||
* Removes the temporary working directory. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after removing the | ||
* temporary working directory. | ||
*/ | ||
FsDriver.prototype.removeWorkingDirectory = function (cb) { | ||
var self = this; | ||
if (!this.working) { return cb(); } | ||
this.removeRecursive(this.working, function (err) { | ||
if (err) { return cb(err); } | ||
self.working = null; | ||
cb(); | ||
}); | ||
}; | ||
/** | ||
* Returns a full diff between the working directory and the root directory. | ||
* @param {fs-driver~ExecCallback} cb Callback to execute after performing the | ||
* diff. | ||
*/ | ||
FsDriver.prototype.workingDiff = function (cb) { | ||
this.diff(this.root, this.working, cb); | ||
}; |
@@ -8,2 +8,4 @@ 'use strict'; | ||
var exists = require('101/exists'); | ||
var debug = require('debug'); | ||
var fullDiffDebug = debug('fs-transform:full-diff'); | ||
@@ -39,6 +41,7 @@ /** | ||
this._ruleActions = {}; | ||
this.currentResult = null; | ||
this.warnings = []; | ||
// Stores a list of the transformative commands that were executed | ||
this.commands = []; | ||
this.results = []; | ||
this.nameChanges = []; | ||
@@ -51,42 +54,26 @@ this.setAction('copy', this.copy); | ||
/** | ||
* Callback executed when transformations have been completed, or if an error | ||
* has occurred. | ||
* @callback fs-transform~Callback | ||
* @param {Error} err An error if something went wrong when processing the | ||
* transformation rules. | ||
* Default postfix to append to backup files after performing find and replace | ||
* transformation operations. | ||
* @type {string} | ||
*/ | ||
Transformer.ORIGINAL_POSTFIX = '.fs-transform.original'; | ||
/** | ||
* Performs a series of filesystem transformations. | ||
* @param {String} root Root directory to run the transformations. | ||
* @param {String|Array} rules An array of transformations, or a JSON string | ||
* that parses into a stream of transformations. | ||
* @param {fs-transform~Callback} cb Callback to execute once the | ||
* transformations have been completed or if an error has occurred. | ||
* Pushes a new result to the result list. | ||
* @param {object} rule Rule that generates the result. | ||
* @return {object} The new result object. | ||
*/ | ||
Transformer.transform = function(root, rules, cb) { | ||
try { | ||
new Transformer(root, rules).execute(cb); | ||
} | ||
catch (err) { | ||
cb(err); | ||
} | ||
Transformer.prototype.pushResult = function (rule) { | ||
this.currentResult = { | ||
rule: rule, | ||
commands: [], | ||
warnings: [], | ||
nameChanges: [], | ||
diffs: {} | ||
}; | ||
this.results.push(this.currentResult); | ||
return this.currentResult; | ||
}; | ||
/** | ||
* Executes the transformations. | ||
* @param {fs-transform~Callback} cb Callback to execute once the | ||
* transformations have been completed or if an error has occurred. | ||
*/ | ||
Transformer.prototype.execute = function (cb) { | ||
var self = this; | ||
this.commands = []; | ||
async.mapSeries(this.rules, function (rule, ruleCallback) { | ||
self.applyRule(rule, ruleCallback); | ||
}, function (err) { | ||
cb(err, self); | ||
}); | ||
}; | ||
/** | ||
* Adds a rule generated warning to the transformer. | ||
@@ -97,30 +84,54 @@ * @param object Object that generated the warning. | ||
Transformer.prototype.addWarning = function (object, msg) { | ||
this.warnings.push(new Warning(object, msg)); | ||
var warning = new Warning(object, msg); | ||
this.warnings.push(warning); | ||
if (exists(this.currentResult)) { | ||
this.currentResult.warnings.push(warning); | ||
} | ||
}; | ||
/** | ||
* Allows the user to override existing rule actions and define new ones. | ||
* Note the action function will be applied within the context of this | ||
* Transformation. | ||
* @param {string} name Name of the action to define or override. | ||
* @param {fs-transform~Rule} fn Function to apply when encountering the rule | ||
* with the given action name. | ||
* Adds a name change result. | ||
* @param {object} nameChange Name change to add. | ||
*/ | ||
Transformer.prototype.setAction = function(name, fn) { | ||
var self = this; | ||
this._ruleActions[name] = function() { | ||
fn.apply(self, arguments); | ||
Transformer.prototype.addNameChange = function (from, to) { | ||
var nameChange = { | ||
from: from, | ||
to: to | ||
}; | ||
this.nameChanges.push(nameChange); | ||
if (exists(this.currentResult)) { | ||
this.currentResult.nameChanges.push(nameChange); | ||
} | ||
}; | ||
/** | ||
* Determine the rule action handler for the action name. | ||
* @param {string} name Name of the action. | ||
* @return {function} The rule handler for the action. | ||
* Adds a diff to the current result. | ||
* @param {string} filename Name of the file for the diff. | ||
* @param {string} Contents of the diff (via diff -u, similar to git diff). | ||
*/ | ||
Transformer.prototype.getAction = function(name) { | ||
return this._ruleActions[name]; | ||
Transformer.prototype.addDiff = function (filename, diff) { | ||
if (!this.currentResult) { return; } | ||
if (!this.currentResult.diffs[filename]) { | ||
this.currentResult.diffs[filename] = []; | ||
} | ||
this.currentResult.diffs[filename].push(diff); | ||
}; | ||
/** | ||
* Saves a command that was executed by the transformer. We do this so we can | ||
* reconstruct all of the transformations in the form of a script. | ||
* @param {string} command Command to save. | ||
* @param {object} rule Rule that generated the command. | ||
*/ | ||
Transformer.prototype.saveCommand = function (command, rule) { | ||
this.commands.push({ | ||
rule: rule, | ||
command: command | ||
}); | ||
if (exists(this.currentResult)) { | ||
this.currentResult.commands.push(command); | ||
} | ||
}; | ||
/** | ||
* @return the commands executed by this transformer in the form of a command | ||
@@ -137,2 +148,4 @@ * line script. | ||
var commandCheck = this.driver.getCommandCheckScript(); | ||
var commands = this.commands.map(function (cmd) { | ||
@@ -143,19 +156,37 @@ return ('# from rule: ' + JSON.stringify(cmd.rule)) + '\n' + | ||
return [preamble, commands, ''].join('\n'); | ||
return [preamble, commandCheck, commands, ''].join('\n'); | ||
}; | ||
/** | ||
* Saves a command that was executed by the transformer. We do this so we can | ||
* reconstruct all of the transformations in the form of a script. | ||
* @param {string} command Command to save. | ||
* @param {object} rule Rule that generated the command. | ||
* @return The full diff after performing all filesystem transforms. | ||
*/ | ||
Transformer.prototype.saveCommand = function (command, rule) { | ||
this.commands.push({ | ||
command: command, | ||
rule: rule | ||
}); | ||
Transformer.prototype.getDiff = function() { | ||
return this._fullDiff; | ||
}; | ||
/** | ||
* Allows the user to override existing rule actions and define new ones. | ||
* Note the action function will be applied within the context of this | ||
* Transformation. | ||
* @param {string} name Name of the action to define or override. | ||
* @param {fs-transform~Rule} fn Function to apply when encountering the rule | ||
* with the given action name. | ||
*/ | ||
Transformer.prototype.setAction = function(name, fn) { | ||
var self = this; | ||
this._ruleActions[name] = function() { | ||
fn.apply(self, arguments); | ||
}; | ||
}; | ||
/** | ||
* Determine the rule action handler for the action name. | ||
* @param {string} name Name of the action. | ||
* @return {fs-transform~Rule} The rule handler for the action. | ||
*/ | ||
Transformer.prototype.getAction = function(name) { | ||
return this._ruleActions[name]; | ||
}; | ||
/** | ||
* Callback for the application of rule actions. | ||
@@ -168,2 +199,133 @@ * @callback fs-transform~Rule | ||
/** | ||
* Performs a series of filesystem transformations. | ||
* @param {String} root Root directory to run the transformations. | ||
* @param {String|Array} rules An array of transformations, or a JSON string | ||
* that parses into a stream of transformations. | ||
* @param {fs-transform~Callback} cb Callback to execute once the | ||
* transformations have been completed or if an error has occurred. | ||
*/ | ||
Transformer.transform = function(root, rules, cb) { | ||
try { | ||
new Transformer(root, rules).transform(cb); | ||
} | ||
catch (err) { | ||
cb(err); | ||
} | ||
}; | ||
/** | ||
* Performs a dry run of the transformation. | ||
* @param {String} root Root directory to run the transformations. | ||
* @param {String|Array} rules An array of transformations, or a JSON string | ||
* that parses into a stream of transformations. | ||
* @param {fs-transform~Callback} cb Callback to execute once the | ||
* transformations have been completed or if an error has occurred. | ||
*/ | ||
Transformer.dry = function(root, rules, cb) { | ||
try { | ||
new Transformer(root, rules).dry(cb); | ||
} | ||
catch (err) { | ||
cb(err); | ||
} | ||
}; | ||
/** | ||
* Callback executed when transformations have been completed, or if an error | ||
* has occurred. | ||
* @callback fs-transform~Callback | ||
* @param {Error} err An error if something went wrong when processing the | ||
* transformation rules. | ||
*/ | ||
/** | ||
* Perform and commit the transformations. | ||
* @param {fs-transform~Callback} cb Callback to execute once the | ||
* transformations have been completed or if an error has occurred. | ||
*/ | ||
Transformer.prototype.transform = function (cb) { | ||
this._execute(true, cb); | ||
}; | ||
/** | ||
* Perform a dry run of of the tranformations. | ||
* @param {fs-transform~Callback} cb Callback to execute once the | ||
* transformations have been completed or if an error has occurred. | ||
*/ | ||
Transformer.prototype.dry = function (cb) { | ||
this._execute(false, cb); | ||
}; | ||
/** | ||
* Executes each of the the tranformation rules and collects their results. | ||
* @param {boolean} commit Whether or not to commit the transformations to the | ||
* original root directory. If true then the changes are applied to the root | ||
* directory, if false then the root directory will remain unchanged. | ||
* @param {fs-transform~Callback} executeCallback Callback to execute once the | ||
* transformations have been completed or if an error has occurred. | ||
*/ | ||
Transformer.prototype._execute = function (commit, executeCallback) { | ||
var self = this; | ||
this.commands = []; | ||
this._fullDiff = ""; | ||
async.series([ | ||
// 0. Check to ensure we have all the required commands | ||
function checkRequiredCommands(cb) { | ||
self.driver.hasAllCommands(cb); | ||
}, | ||
// 1. Create a working directory | ||
function setup(cb) { | ||
self.driver.createWorkingDirectory(cb); | ||
}, | ||
// 2. Apply transformation rules | ||
function applyRules(cb) { | ||
async.mapSeries(self.rules, function (rule, ruleCallback) { | ||
self.applyRule(rule, ruleCallback); | ||
}, function (err) { | ||
cb(err, self); | ||
}); | ||
}, | ||
// 3. Fetch a diff between the working and the original root | ||
function fetchFullDiff(cb) { | ||
self.driver.workingDiff(function (err, diff) { | ||
if (err) { return cb(err); } | ||
fullDiffDebug(diff); | ||
self._fullDiff = diff; | ||
cb(); | ||
}); | ||
}, | ||
// 4. Commit the changes if applicable, otherwise remove the working | ||
// directory. | ||
function cleanup(cleanupCallback) { | ||
if (!commit) { | ||
return self.driver.removeWorkingDirectory(cleanupCallback); | ||
} | ||
var root = self.driver.root; | ||
var backup = root + '.bak'; | ||
var working = self.driver.working; | ||
async.series([ | ||
function backupRoot(cb) { | ||
self.driver.move(root, backup, cb); | ||
}, | ||
function moveWorkingToRoot(cb) { | ||
self.driver.move(working, root, cb); | ||
}, | ||
function removeBackup(cb) { | ||
self.driver.removeRecursive(backup, cb); | ||
} | ||
], cleanupCallback); | ||
} | ||
], function (err) { | ||
executeCallback(err, self); | ||
}); | ||
}; | ||
/** | ||
* Applys a given transformation rule. | ||
@@ -187,2 +349,3 @@ * @param {object} rule Rule to apply. | ||
this.pushResult(rule); | ||
actionMethod(rule, cb); | ||
@@ -213,3 +376,3 @@ }; | ||
return true; | ||
} | ||
}; | ||
@@ -226,5 +389,6 @@ /** | ||
var self = this; | ||
var command = this.driver.copy(rule.source, rule.dest, function (err) { | ||
this.driver.copy(rule.source, rule.dest, function (err, output, command) { | ||
if (err) { return cb(err); } | ||
self.saveCommand(command, rule); | ||
self.addNameChange(rule.source, rule.dest); | ||
cb(); | ||
@@ -244,5 +408,6 @@ }); | ||
var self = this; | ||
var command = this.driver.move(rule.source, rule.dest, function (err) { | ||
this.driver.move(rule.source, rule.dest, function (err, output, command) { | ||
if (err) { return cb(err); } | ||
self.saveCommand(command, rule); | ||
self.addNameChange(rule.source, rule.dest); | ||
cb(); | ||
@@ -310,3 +475,3 @@ }); | ||
exclude.forEach(function(excludeRule, index) { | ||
var name = excludeRule.name; | ||
var name = self.driver.absolutePath(excludeRule.name); | ||
var line = excludeRule.line; | ||
@@ -341,21 +506,65 @@ | ||
if (files.length === 0) { | ||
self.addWarning(rule, 'All results were excluded.') | ||
self.addWarning(rule, 'All results were excluded.'); | ||
return cb(); | ||
} | ||
// 3. Apply in-place `sed` to result set | ||
async.each(files, function (file, cb) { | ||
var command = self.driver.sed( | ||
search, | ||
replace, | ||
file.name, | ||
file.line, | ||
function(err) { | ||
if (err) { return cb(err); } | ||
self.saveCommand(command, rule); | ||
cb(); | ||
} | ||
); | ||
}, cb); | ||
// 3. Perform Search and Replace | ||
// 3.0 Collect the set of filenames for each of the files being changed. | ||
var filenames = []; | ||
files.forEach(function (file) { | ||
if (!~filenames.indexOf(file.name)) { | ||
filenames.push(file.name); | ||
} | ||
}); | ||
async.series([ | ||
// 3.1 Create an temporary copy for each file being changed | ||
function createOriginalCopy(cb) { | ||
async.each(filenames, function (name, copyCallback) { | ||
var copyName = name + Transformer.ORIGINAL_POSTFIX; | ||
self.driver.copy(name, copyName, copyCallback); | ||
}, cb); | ||
}, | ||
// 3.2 Make replacements by using sed | ||
function sedFiles(cb) { | ||
async.each(files, function (file, sedCallback) { | ||
self.driver.sed( | ||
search, | ||
replace, | ||
file.name, | ||
parseInt(file.line), | ||
function(err, output, command) { | ||
if (err) { return sedCallback(err); } | ||
self.saveCommand(command, rule); | ||
sedCallback(); | ||
} | ||
); | ||
}, cb); | ||
}, | ||
// 3.3 Collect diffs for each file | ||
function collectDiffs(cb) { | ||
async.each(filenames, function (name, diffCallback) { | ||
var copyName = name + Transformer.ORIGINAL_POSTFIX; | ||
self.driver.diff(copyName, name, function (err, diff) { | ||
if (err) { return diffCallback(err); } | ||
self.addDiff(name, diff); | ||
diffCallback(); | ||
}); | ||
}, cb); | ||
}, | ||
// 3.4 Cleanup temporary files | ||
function cleanup(cb) { | ||
async.each(filenames, function (name, removeCallback) { | ||
var copyName = name + Transformer.ORIGINAL_POSTFIX; | ||
var dotLastName = name + '.last'; | ||
var names = [copyName, dotLastName].join(' '); | ||
self.driver.remove(names, removeCallback); | ||
}, cb); | ||
} | ||
], cb); | ||
}); | ||
}; |
{ | ||
"name": "fs-transform", | ||
"version": "1.0.1", | ||
"version": "2.0.0", | ||
"description": "Fast, rule based, file system transformations.", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "jshint index.js && lab -v -c test/*", | ||
"test": "jshint index.js lib/ && lab -v -c -a code test/*", | ||
"doc": "jsdoc -d doc index.js lib/" | ||
@@ -34,3 +34,5 @@ }, | ||
"101": "^0.16.1", | ||
"async": "^0.9.0" | ||
"async": "^0.9.0", | ||
"debug": "^2.1.3", | ||
"diff": "^1.3.2" | ||
}, | ||
@@ -37,0 +39,0 @@ "devDependencies": { |
@@ -150,2 +150,21 @@ # fs-transform | ||
## Dry Runs | ||
`fs-transform` performs all of its work in a temporary directory so it can | ||
gracefully fail if an error occurs (leaving the root directory as it was before | ||
the transforms were applied). | ||
For further safety you can perform transformations in "dry run mode". Using it | ||
is rather simple: | ||
```js | ||
Transformer.dry('/root/directory', myRules, function (err, transformer) { | ||
// Two things to not: | ||
// | ||
// 1) Check to see if errors are reported in `err` | ||
// | ||
// 2) The `transformer` now has all the same information as it would during | ||
// an actual run! | ||
}); | ||
``` | ||
## Generating Shell Scripts | ||
@@ -192,2 +211,15 @@ `fs-transform` also has the ability to generate reusable shell scripts. Whenever | ||
## Generating Diffs | ||
`fs-transform` allows you to get a full recursive diff between the root | ||
before transformations were applied, and the root after. Here's an example of | ||
how to get the full text diff: | ||
```js | ||
Transformer.dry('/root/directory', myRules, function (err, transformer) { | ||
// Get and log the diff: | ||
var fullDiff = transformer.getDiff(); | ||
console.log(fullDiff); | ||
}); | ||
``` | ||
## Contributing | ||
@@ -194,0 +226,0 @@ |
@@ -42,5 +42,5 @@ var Lab = require('lab'); | ||
it('should use the empty string if no root was given', function (done) { | ||
it('should use the current working directory if no root was given', function (done) { | ||
var driver = new FsDriver(); | ||
expect(driver.root).to.equal(''); | ||
expect(driver.root).to.equal(process.cwd()); | ||
done(); | ||
@@ -68,9 +68,25 @@ }); | ||
}); | ||
it('should return null if given a non string path', function(done) { | ||
expect(driver.absolutePath()).to.be.null(); | ||
expect(driver.absolutePath(undefined)).to.be.null(); | ||
expect(driver.absolutePath(null)).to.be.null(); | ||
expect(driver.absolutePath({})).to.be.null(); | ||
expect(driver.absolutePath(42)).to.be.null(); | ||
done(); | ||
}); | ||
it('should use the working directory if one is present', function (done) { | ||
expect(driver.absolutePath('foo')).to.equal('/tmp/foo'); | ||
driver.working = '/etc'; | ||
expect(driver.absolutePath('foo')).to.equal('/etc/foo'); | ||
done(); | ||
}) | ||
}); // end 'absolutePath' | ||
describe('file system', function () { | ||
var driver = new FsDriver('/tmp'); | ||
describe('exec', function () { | ||
var driver = new FsDriver('/root/dir'); | ||
beforeEach(function (done) { | ||
sinon.stub(childProcess, 'exec').yields(); | ||
sinon.stub(childProcess, 'exec').yieldsAsync(); | ||
done(); | ||
@@ -84,71 +100,92 @@ }); | ||
describe('move', function() { | ||
it('should return the move command', function(done) { | ||
var command = "mv /tmp/foo /tmp/bar"; | ||
expect(driver.move('foo', 'bar', noop)).to.equal(command); | ||
it('should execute the given command', function(done) { | ||
var command = 'cp wow neat'; | ||
driver.exec(command, function (err) { | ||
expect(childProcess.exec.calledWith(command)).to.be.true(); | ||
done(); | ||
}); | ||
it('should execute system `mv` when moving a file', function (done) { | ||
var source = 'a.txt'; | ||
var dest = 'b.txt'; | ||
driver.move(source, dest, function (err) { | ||
if (err) { return done(err); } | ||
var command = 'mv /tmp/' + source + ' /tmp/' + dest; | ||
expect(childProcess.exec.calledWith(command)) | ||
.to.be.true(); | ||
done(); | ||
}); | ||
}); | ||
it('should provide the command in the callback', function(done) { | ||
var expected = 'cp gnarly brah'; | ||
driver.exec(expected, function (err, output, command) { | ||
expect(command).to.equal(expected); | ||
done(); | ||
}); | ||
}); | ||
it('should yield `exec` errors to the given callback', function (done) { | ||
childProcess.exec.yields(new Error('Error')); | ||
driver.move('foo', 'bar', function (err) { | ||
expect(err).to.exist(); | ||
done(); | ||
}); | ||
it('should replace working paths with root in returned commands', function(done) { | ||
var expected = 'command /root/dir/a /root/dir/b /root/dir/c'; | ||
var execute = 'command /work/dir/a /work/dir/b /work/dir/c' | ||
driver.working = '/work/dir'; | ||
driver.exec(execute, function (err, output, command) { | ||
expect(command).to.equal(expected); | ||
delete driver.working; | ||
done(); | ||
}); | ||
}); // end 'move' | ||
}); | ||
describe('copy', function() { | ||
it('should return the copy command', function(done) { | ||
var command = "cp /tmp/foo /tmp/bar"; | ||
expect(driver.copy('foo', 'bar', noop)).to.equal(command); | ||
it('should yield childProcess.exec errors to the callback', function(done) { | ||
var error = new Error('Some error'); | ||
childProcess.exec.yieldsAsync(error); | ||
driver.exec('whatever', function (err) { | ||
expect(err).to.equal(error); | ||
done(); | ||
}); | ||
}); | ||
}); // end 'exec' | ||
it('should execute system `cp` when copying a file', function (done) { | ||
var source = 'a.txt'; | ||
var dest = 'b.txt'; | ||
driver.copy(source, dest, function (err) { | ||
if (err) { return done(err); } | ||
var command = 'cp /tmp/' + source + ' /tmp/' + dest; | ||
expect(childProcess.exec.calledWith(command)) | ||
.to.be.true(); | ||
done(); | ||
}); | ||
describe('file system', function () { | ||
var driver; | ||
beforeEach(function (done) { | ||
driver = new FsDriver('/tmp'); | ||
sinon.stub(driver, 'exec').yieldsAsync(); | ||
done(); | ||
}); | ||
it('should check for commands using driver.exec', function(done) { | ||
driver.hasCommand('foo', function () { | ||
expect(driver.exec.calledOnce).to.be.true(); | ||
expect(driver.exec.calledWith('command -v foo')).to.be.true(); | ||
done(); | ||
}); | ||
}); | ||
it('should yield `exec` errors to the given callback', function (done) { | ||
childProcess.exec.yields(new Error('Error')); | ||
driver.copy('foo', 'bar', function (err) { | ||
expect(err).to.exist(); | ||
done(); | ||
it('should check for all commands using driver.exec', function(done) { | ||
sinon.stub(driver, 'hasCommand').yieldsAsync(); | ||
driver.hasAllCommands(function () { | ||
expect(driver.hasCommand.callCount).to.equal(FsDriver.commands.length); | ||
FsDriver.commands.forEach(function (name) { | ||
expect(driver.hasCommand.calledWith(name)).to.be.true(); | ||
}); | ||
driver.hasCommand.restore(); | ||
done(); | ||
}); | ||
}); // end 'copy' | ||
}); | ||
describe('grep', function() { | ||
it('should return the grep command', function(done) { | ||
var command = 'grep -rn search /tmp'; | ||
expect(driver.grep('search', noop)).to.equal(command); | ||
it('should use driver.exec to perform file moves', function (done) { | ||
var command = "mv /tmp/foo /tmp/bar"; | ||
driver.move('foo', 'bar', function () { | ||
expect(driver.exec.calledOnce).to.be.true(); | ||
expect(driver.exec.calledWith(command)).to.be.true(); | ||
done(); | ||
}); | ||
}); | ||
it('should execute system `grep`', function (done) { | ||
driver.grep('foo', function (err) { | ||
if (err) { return done(err); } | ||
var command = 'grep -rn foo /tmp'; | ||
expect(childProcess.exec.calledWith(command)) | ||
.to.be.true(); | ||
it('should use driver.exec to perform file copies', function(done) { | ||
var command = "cp /tmp/foo /tmp/bar"; | ||
driver.copy('foo', 'bar', function () { | ||
expect(driver.exec.calledOnce).to.be.true(); | ||
expect(driver.exec.calledWith(command)).to.be.true(); | ||
done(); | ||
}); | ||
}); | ||
describe('grep', function() { | ||
it('should use driver.exec to perform the grep', function(done) { | ||
var command = 'grep -rn \'search\' /tmp'; | ||
driver.grep('search', function () { | ||
expect(driver.exec.calledOnce).to.be.true(); | ||
expect(driver.exec.calledWith(command)).to.be.true(); | ||
done(); | ||
@@ -159,6 +196,6 @@ }); | ||
it('should properly escape search patterns', function(done) { | ||
driver.grep('\\lambda', function (err) { | ||
driver.grep('\\lambda\'', function (err) { | ||
if (err) { return done(err); } | ||
var command = 'grep -rn \\\\lambda /tmp'; | ||
expect(childProcess.exec.calledWith(command)) | ||
var command = 'grep -rn \'\\\\lambda\\\'\' /tmp'; | ||
expect(driver.exec.calledWith(command)) | ||
.to.be.true(); | ||
@@ -169,8 +206,11 @@ done(); | ||
it('should yield `exec` errors to the given callback', function (done) { | ||
childProcess.exec.yields(new Error('Error')); | ||
driver.grep('foo', function (err) { | ||
expect(err).to.exist(); | ||
done(); | ||
it('should use the working directory when one is supplied', function(done) { | ||
var command = 'grep -rn \'search\' /working'; | ||
driver.working = '/working'; | ||
driver.grep('search', function (err) { | ||
if (err) { return done(err); } | ||
expect(driver.exec.calledWith(command)).to.be.true(); | ||
driver.exec.restore(); | ||
}); | ||
done(); | ||
}); | ||
@@ -180,15 +220,7 @@ }); // end 'grep' | ||
describe('sed', function() { | ||
it('should return the correct sed command', function(done) { | ||
var command = 'sed -i "" "1337s/bar/baz/g" /tmp/file1.txt'; | ||
expect(driver.sed('bar', 'baz', 'file1.txt', 1337, noop)) | ||
.to.equal(command); | ||
done(); | ||
}); | ||
it('should execute system `sed`', function(done) { | ||
driver.sed('foo', 'bar', 'example.txt', 20, function (err) { | ||
if (err) { return done(err); } | ||
var command = 'sed -i "" "20s/foo/bar/g" /tmp/example.txt'; | ||
expect(childProcess.exec.calledWith(command)) | ||
.to.be.true(); | ||
it('should use driver.exec to perform the sed', function(done) { | ||
var command = 'sed -i.last \'1337s/bar/baz/g\' /tmp/file1.txt'; | ||
driver.sed('bar', 'baz', 'file1.txt', 1337, function () { | ||
expect(driver.exec.calledOnce).to.be.true(); | ||
expect(driver.exec.calledWith(command)).to.be.true(); | ||
done(); | ||
@@ -201,4 +233,4 @@ }); | ||
if (err) { return done(err); } | ||
var command = 'sed -i "" "17s/\\/foo/\\/bar/g" /tmp/example.txt'; | ||
expect(childProcess.exec.calledWith(command)) | ||
var command = 'sed -i.last \'17s/\\/foo/\\/bar/g\' /tmp/example.txt'; | ||
expect(driver.exec.calledWith(command)) | ||
.to.be.true(); | ||
@@ -208,10 +240,2 @@ done(); | ||
}); | ||
it('should yield `exec` errors to the given callback', function(done) { | ||
childProcess.exec.yields(new Error('Error')); | ||
driver.sed('foo', 'bar', 'awesome.txt', 28, function (err) { | ||
expect(err).to.exist(); | ||
done(); | ||
}); | ||
}); | ||
}); // end 'sed' | ||
@@ -223,4 +247,4 @@ | ||
driver.exists('example'); | ||
expect(stub.calledOnce); | ||
expect(stub.calledWith('/tmp/example')); | ||
expect(stub.calledOnce).to.be.true(); | ||
expect(stub.calledWith('/tmp/example')).to.be.true(); | ||
fs.existsSync.restore(); | ||
@@ -239,4 +263,169 @@ done(); | ||
}); | ||
}); // end 'exists' | ||
describe('diff', function() { | ||
it('should use driver.exec to perform the diff', function(done) { | ||
var command = 'diff -u -r /tmp/a /tmp/b'; | ||
driver.diff('/tmp/a', '/tmp/b', function (err) { | ||
expect(driver.exec.calledOnce).to.be.true(); | ||
expect(driver.exec.calledWith(command)).to.be.true(); | ||
done(); | ||
}); | ||
}); | ||
it('should ignore errors with code 1 (indicated differences)', function (done) { | ||
var error = new Error('diff error'); | ||
error.code = 1; | ||
driver.exec.yields(error); | ||
driver.diff('a', 'b', function (err) { | ||
expect(err).to.be.null(); | ||
done(); | ||
}); | ||
}); | ||
it('should yield errors with code > 1 to the given callback', function (done) { | ||
var error = new Error('diff error'); | ||
error.code = 2; | ||
driver.exec.yields(error); | ||
driver.diff('a', 'b', function (err) { | ||
expect(err).to.equal(error); | ||
done(); | ||
}); | ||
}); | ||
}); // end 'diff' | ||
it('should use driver.exec to remove files', function(done) { | ||
var command = 'rm /tmp/file1.txt'; | ||
driver.remove('file1.txt', function () { | ||
expect(driver.exec.calledOnce).to.be.true(); | ||
expect(driver.exec.calledWith(command)).to.be.true(); | ||
done(); | ||
}); | ||
}); | ||
it('should use driver.exec to recursively remove files', function(done) { | ||
var command = 'rm -rf /tmp/dir/'; | ||
driver.removeRecursive('dir/', function () { | ||
expect(driver.exec.calledOnce).to.be.true(); | ||
expect(driver.exec.calledWith(command)).to.be.true(); | ||
done(); | ||
}); | ||
}); | ||
describe('findWorkingDirectory', function() { | ||
beforeEach(function (done) { | ||
driver.root = '/etc/init.d'; | ||
sinon.stub(driver, 'exists'); | ||
sinon.stub(Math, 'random') | ||
.onFirstCall().returns(0) | ||
.onSecondCall().returns(1) | ||
.onThirdCall().returns(2); | ||
done(); | ||
}); | ||
afterEach(function (done) { | ||
Math.random.restore(); | ||
done(); | ||
}); | ||
it('should find a new working directory', function(done) { | ||
driver.exists.returns(false); | ||
var working = driver.findWorkingDirectory(); | ||
expect(Math.random.calledOnce).to.be.true(); | ||
expect(working).to.equal('/tmp/.init.d.fs-work.0'); | ||
done(); | ||
}); | ||
it('should not attempt to use a working directory that already exists', function(done) { | ||
driver.exists | ||
.onFirstCall().returns(true) | ||
.onSecondCall().returns(true) | ||
.onThirdCall().returns(false); | ||
expect(driver.findWorkingDirectory()) | ||
.to.equal('/tmp/.init.d.fs-work.2'); | ||
done(); | ||
}); | ||
}); | ||
describe('createWorkingDirectory', function() { | ||
beforeEach(function (done) { | ||
driver.root = '/etc/init.d'; | ||
sinon.stub(driver, 'exists').returns(false); | ||
sinon.stub(Math, 'random').returns(0); | ||
done(); | ||
}); | ||
afterEach(function (done) { | ||
Math.random.restore(); | ||
done(); | ||
}); | ||
it('should set the working directory', function(done) { | ||
driver.createWorkingDirectory(function (err) { | ||
if (err) { return done(err); } | ||
expect(driver.working).to.equal('/tmp/.init.d.fs-work.0'); | ||
done(); | ||
}); | ||
}); | ||
it('should create the working directory with driver.exec', function (done) { | ||
var command = 'cp -r /etc/init.d /tmp/.init.d.fs-work.0'; | ||
driver.createWorkingDirectory(function (err) { | ||
if (err) { return done(err); } | ||
expect(driver.exec.calledWith(command)).to.be.true(); | ||
done(); | ||
}); | ||
}); | ||
it('should yield system errors to the supplied callback', function(done) { | ||
var error = new Error('Errorz'); | ||
driver.exec.yields(error); | ||
driver.createWorkingDirectory(function (err) { | ||
expect(err).to.equal(error); | ||
done(); | ||
}); | ||
}); | ||
}); // end 'createWorkingDirectory' | ||
describe('removeWorkingDirectory', function() { | ||
beforeEach(function (done) { | ||
sinon.stub(driver, 'removeRecursive').yieldsAsync(); | ||
done(); | ||
}); | ||
afterEach(function (done) { | ||
driver.removeRecursive.restore(); | ||
done(); | ||
}); | ||
it('should remove the working directory', function(done) { | ||
driver.working = '/tmp/x'; | ||
driver.removeWorkingDirectory(function (err) { | ||
if (err) { return done(err); } | ||
expect(driver.removeRecursive.calledWith('/tmp/x')).to.be.true(); | ||
expect(driver.working).to.be.null(); | ||
done(); | ||
}); | ||
}); | ||
it('should not remove a working directory if none exists', function(done) { | ||
driver.working = null; | ||
driver.removeWorkingDirectory(function (err) { | ||
if (err) { return done(err); } | ||
expect(driver.removeRecursive.callCount).to.equal(0); | ||
done(); | ||
}); | ||
}); | ||
it('should yield system errors to the supplied callback', function(done) { | ||
var error = new Error('Remove error'); | ||
driver.removeRecursive.yields(error); | ||
driver.working = '/woot/sauce'; | ||
driver.removeWorkingDirectory(function (err) { | ||
expect(err).to.equal(error); | ||
done(); | ||
}); | ||
}); | ||
}); // end 'removeWorkingDirectory' | ||
}); // end 'file system' | ||
}); // end 'fs-driver' |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
109157
26
2725
239
4
3
9
+ Addeddebug@^2.1.3
+ Addeddiff@^1.3.2
+ Addeddebug@2.6.9(transitive)
+ Addeddiff@1.4.0(transitive)
+ Addedms@2.0.0(transitive)