UNPKG

rsync

Version:
1,086 lines (959 loc) 27.4 kB
var spawn = require('child_process').spawn; var path = require('path'); /** * Rsync is a wrapper class to configure and execute an `rsync` command * in a fluent and convenient way. * * A new command can be set up by creating a new `Rsync` instance of * obtaining one through the `build` method. * * @example * // using the constructor * var rsync = new Rsync() * .source('/path/to/source') * .destination('myserver:destination/'); * * // using the build method with options * var rsync = Rsync.build({ * source: '/path/to/source', * destination: 'myserver:destination/' * }); * * Executing the command can be done using the `execute` method. The command * is executed as a child process and three callbacks can be registered. See * the `execute` method for more details. * * @example * rsync.execute(function(error, code, cmd) { * // function called when the child process is finished * }, function(stdoutChunk) { * // function called when a chunk of text is received on stdout * }, function stderrChunk) { * // function called when a chunk of text is received on stderr * }); * * @author Mattijs Hoitink <mattijs@monkeyandmachine.com> * @copyright Copyright (c) 2013, Mattijs Hoitink <mattijs@monkeyandmachine.com> * @license The MIT License * * @constructor * @param {Object} config Configuration settings for the Rsync wrapper. */ function Rsync(config) { if (!(this instanceof Rsync)) { return new Rsync(config); } // Parse config config = config || {}; if (typeof(config) !== 'object') { throw new Error('Rsync config must be an Object'); } // executable this._executable = hasOP(config, 'executable') ? config.executable : 'rsync'; // shell this._executableShell = hasOP(config, 'executableShell') ? config.executableShell : '/bin/sh'; // source(s) and destination this._sources = []; this._destination = ''; // ordered list of file patterns to include/exclude this._patterns = []; // options this._options = {}; // output callbacks this._outputHandlers = { stdout: null, stderr: null }; this._cwd = process.cwd(); // Allow child_process.spawn env overriding this._env = process.env; // Debug parameter this._debug = hasOP(config, 'debug') ? config.debug : false; } /** * Build a new Rsync command from an options Object. * @param {Object} options * @return {Rsync} */ Rsync.build = function(options) { var command = new Rsync(); // Process all options for (var key in options) { if (hasOP(options, key)) { var value = options[key]; // Only allow calling methods on the Rsync command if (typeof(command[key]) === 'function') { command[key](value); } } } return command; }; /** * Set an option. * @param {String} option * @param mixed value * @return Rsync */ Rsync.prototype.set = function(option, value) { option = stripLeadingDashes(option); if (option && option.length > 0) { this._options[option] = value || null; } return this; }; /** * Unset an option. * @param {String} option * @return Rsync */ Rsync.prototype.unset = function(option) { option = stripLeadingDashes(option); if (option && Object.keys(this._options).indexOf(option) >= 0) { delete this._options[option]; } return this; }; /** * Set or unset one or more flags. A flag is a single letter option without a value. * * Flags can be presented as a single String, an Array containing Strings or an Object * with the flags as keys. * * When flags are presented as a String or Array the set or unset method will be determined * by the second parameter. * When the flags are presented as an Object the set or unset method will be determined by * the value corresponding to each flag key. * * @param {String|Array|Object} flags * @param {Boolean} set * @return Rsync */ Rsync.prototype.flags = function(flags, set) { // Do some argument handling if (!arguments.length) { return this; } else if (arguments.length === 1) { set = true; } else { // There are more than 1 arguments, assume flags are presented as strings flags = Array.prototype.slice.call(arguments); // Check if the last argument is a boolean if (typeof(flags[flags.length - 1]) === 'boolean') { set = flags.pop(); } else { set = true; } // Join the remainder of the arguments to treat them as flags flags = flags.join(''); } // Split multiple flags if (typeof(flags) === 'string') { flags = stripLeadingDashes(flags).split(''); } // Turn array into an object if (isArray(flags)) { var obj = {}; flags.forEach(function(f) { obj[f] = set; }); flags = obj; } // set/unset each flag for (var key in flags) { if (hasOP(flags, key)) { var method = (flags[key]) ? 'set' : 'unset'; this[method](stripLeadingDashes(key)); } } return this; }; /** * Check if an option is set. * @param {String} option * @return {Boolean} */ Rsync.prototype.isSet = function(option) { option = stripLeadingDashes(option); return Object.keys(this._options).indexOf(option) >= 0; }; /** * Get an option by name. * @param {String} name * @return mixed */ Rsync.prototype.option = function(name) { name = stripLeadingDashes(name); return this._options[name]; }; /** * Register a list of file patterns to include/exclude in the transfer. Patterns can be * registered as an array of Strings or Objects. * * When registering a pattern as a String it must be prefixed with a `+` or `-` sign to * signal include or exclude for the pattern. The sign will be stripped of and the * pattern will be added to the ordered pattern list. * * When registering the pattern as an Object it must contain the `action` and * `pattern` keys where `action` contains the `+` or `-` sign and the `pattern` * key contains the file pattern, without the `+` or `-` sign. * * @example * // on an existing rsync object * rsync.patterns(['-docs', { action: '+', pattern: '/subdir/*.py' }]); * * // using Rsync.build for a new rsync object * rsync = Rsync.build({ * ... * patterns: [ '-docs', { action: '+', pattern: '/subdir/*.py' }] * ... * }) * * @param {Array} patterns * @return Rsync */ Rsync.prototype.patterns = function(patterns) { if (arguments.length > 1) { patterns = Array.prototype.slice.call(arguments, 0); } if (!isArray(patterns)) { patterns = [ patterns ]; } patterns.forEach(function(pattern) { var action = '?'; if (typeof(pattern) === 'string') { action = pattern.charAt(0); pattern = pattern.substring(1); } else if ( typeof(pattern) === 'object' && hasOP(pattern, 'action') && hasOP(pattern, 'pattern') ) { action = pattern.action; pattern = pattern.pattern; } // Check if the pattern is an include or exclude if (action === '-') { this.exclude(pattern); } else if (action === '+') { this.include(pattern); } else { throw new Error('Invalid pattern: ' + pattern); } }, this); return this; }; /** * Exclude a file pattern from transfer. The pattern will be appended to the ordered list * of patterns for the rsync command. * * @param {String|Array} patterns * @return Rsync */ Rsync.prototype.exclude = function(patterns) { if (arguments.length > 1) { patterns = Array.prototype.slice.call(arguments, 0); } if (!isArray(patterns)) { patterns = [ patterns ]; } patterns.forEach(function(pattern) { this._patterns.push({ action: '-', pattern: pattern }); }, this); return this; }; /** * Include a file pattern for transfer. The pattern will be appended to the ordered list * of patterns for the rsync command. * * @param {String|Array} patterns * @return Rsync */ Rsync.prototype.include = function(patterns) { if (arguments.length > 1) { patterns = Array.prototype.slice.call(arguments, 0); } if (!isArray(patterns)) { patterns = [ patterns ]; } patterns.forEach(function(pattern) { this._patterns.push({ action: '+', pattern: pattern }); }, this); return this; }; /** * Get the command that is going to be executed. * @return {String} */ Rsync.prototype.command = function() { return this.executable() + ' ' + this.args().join(' '); }; /** * String representation of the Rsync command. This is the command that is * going to be executed when calling Rsync::execute. * @return {String} */ Rsync.prototype.toString = Rsync.prototype.command; /** * Get the arguments for the rsync command. * @return {Array} */ Rsync.prototype.args = function() { // Gathered arguments var args = []; // Add options. Short options (one letter) without values are gathered together. // Long options have a value but can also be a single letter. var short = []; var long = []; // Split long and short options for (var key in this._options) { if (hasOP(this._options, key)) { var value = this._options[key]; var noval = (value === null || value === undefined); // Check for short option (single letter without value) if (key.length === 1 && noval) { short.push(key); } else { if (isArray(value)) { value.forEach(function (val) { long.push(buildOption(key, val, escapeShellArg)); }); } else { long.push(buildOption(key, value, escapeShellArg)); } } } } // Add combined short options if any are present if (short.length > 0) { args.push('-' + short.join('')); } // Add long options if any are present if (long.length > 0) { args = args.concat(long); } // Add includes/excludes in order this._patterns.forEach(function(def) { if (def.action === '-') { args.push(buildOption('exclude', def.pattern, escapeFileArg)); } else if (def.action === '+') { args.push(buildOption('include', def.pattern, escapeFileArg)); } else { debug(this, 'Unknown pattern action ' + def.action); } }); // Add sources if (this.source().length > 0) { args = args.concat(this.source().map(escapeFileArg)); } // Add destination if (this.destination()) { args.push(escapeFileArg(this.destination())); } return args; }; /** * Get and set rsync process cwd directory. * * @param {string} cwd= Directory path relative to current process directory. * @return {string} Return current _cwd. */ Rsync.prototype.cwd = function(cwd) { if (arguments.length > 0) { if (typeof cwd !== 'string') { throw new Error('Directory should be a string'); } this._cwd = path.resolve(cwd); } return this._cwd; }; /** * Get and set rsync process environment variables * * @param {string} env= Environment variables * @return {string} Return current _env. */ Rsync.prototype.env = function(env) { if (arguments.length > 0) { if (typeof env !== 'object') { throw new Error('Environment should be an object'); } this._env = env; } return this._env; }; /** * Register an output handlers for the commands stdout and stderr streams. * These functions will be called once data is streamed on one of the output buffers * when the command is executed using `execute`. * * Only one callback function can be registered for each output stream. Previously * registered callbacks will be overridden. * * @param {Function} stdout Callback Function for stdout data * @param {Function} stderr Callback Function for stderr data * @return Rsync */ Rsync.prototype.output = function(stdout, stderr) { // Check for single argument so the method can be used with Rsync.build if (arguments.length === 1 && Array.isArray(stdout)) { stderr = stdout[1]; stdout = stdout[0]; } if (typeof(stdout) === 'function') { this._outputHandlers.stdout = stdout; } if (typeof(stderr) === 'function') { this._outputHandlers.stderr = stdout; } return this; }; /** * Execute the rsync command. * * The callback function is called with an Error object (or null when there was none), * the exit code from the executed command and the executed command as a String. * * When stdoutHandler and stderrHandler functions are provided they will be used to stream * data from stdout and stderr directly without buffering. * * @param {Function} callback Called when rsync finishes (optional) * @param {Function} stdoutHandler Called on each chunk received from stdout (optional) * @param {Function} stderrHandler Called on each chunk received from stderr (optional) */ Rsync.prototype.execute = function(callback, stdoutHandler, stderrHandler) { // Register output handlers this.output(stdoutHandler, stderrHandler); // Execute the command as a child process // see https://github.com/joyent/node/blob/937e2e351b2450cf1e9c4d8b3e1a4e2a2def58bb/lib/child_process.js#L589 var cmdProc; if ('win32' === process.platform) { cmdProc = spawn('cmd.exe', ['/s', '/c', '"' + this.command() + '"'], { stdio: 'pipe', windowsVerbatimArguments: true, cwd: this._cwd, env: this._env }); } else { cmdProc = spawn(this._executableShell, ['-c', this.command()], { stdio: 'pipe', cwd: this._cwd, env: this._env }); } // Capture stdout and stderr if there are output handlers configured if (typeof(this._outputHandlers.stdout) === 'function') { cmdProc.stdout.on('data', this._outputHandlers.stdout); } if (typeof(this._outputHandlers.stderr) === 'function') { cmdProc.stderr.on('data', this._outputHandlers.stderr); } // Wait for the command to finish cmdProc.on('close', function(code) { var error = null; // Check rsyncs error code // @see http://bluebones.net/2007/06/rsync-exit-codes/ if (code !== 0) { error = new Error('rsync exited with code ' + code); } // Check for callback if (typeof(callback) === 'function') { callback(error, code, this.command()); } }.bind(this)); // Return the child process object so it can be cleaned up // if the process exits return(cmdProc); }; /** * Get or set the debug property. * * The property is set to the boolean provided so unsetting the debug * property has to be done by passing false to this method. * * @function * @name debug * @memberOf Rsync.prototype * @param {Boolean} debug the value of the debug property (optional) * @return {Rsync|Boolean} */ createValueAccessor('debug'); /** * Get or set the executable to use for the rsync process. * * When setting the executable path the Rsync instance is returned for * the fluent interface. Otherwise the configured executable path * is returned. * * @function * @name executable * @memberOf Rsync.prototype * @param {String} executable path to the executable (optional) * @return {Rsync|String} */ createValueAccessor('executable'); /** * Get or set the shell to use on non-Windows (Unix or Mac OS X) systems. * * When setting the shell the Rsync instance is returned for the * fluent interface. Otherwise the configured shell is returned. * * @function * @name executableShell * @memberOf Rsync.prototype * @param {String} shell to use on non-Windows systems (optional) * @return {Rsync|String} */ createValueAccessor('executableShell'); /** * Get or set the destination for the transfer. * * When setting the destination the Rsync instance is returned for * the fluent interface. Otherwise the configured destination path * is returned. * * @function * @name destination * @memberOf Rsync.prototype * @param {String} destination the destination (optional) * @return {Rsync|String} */ createValueAccessor('destination'); /** * Add one or more sources for the command or get the list of configured * sources. * * The sources are appended to the list of known sources if they were not * included yet and the Rsync instance is returned for the fluent * interface. Otherwise the configured list of source is returned. * * @function * @name source * @memberOf Rsync.prototype * @param {String|Array} sources the source or list of sources to configure (optional) * @return {Rsync|Array} */ createListAccessor('source', '_sources'); /** * Set the shell to use when logging in on a remote server. * * This is the same as setting the `rsh` option. * * @function * @name shell * @memberOf Rsync.prototype * @param {String} shell the shell option to use * @return {Rsync} */ exposeLongOption('rsh', 'shell'); /** * Add a chmod instruction to the command. * * @function * @name chmod * @memberOf Rsync.prototype * @param {String|Array} * @return {Rsync|Array} */ exposeMultiOption('chmod', 'chmod'); /** * Set the delete flag. * * This is the same as setting the `--delete` commandline flag. * * @function * @name delete * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('delete'); /** * Set the progress flag. * * This is the same as setting the `--progress` commandline flag. * * @function * @name progress * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('progress'); /** * Set the archive flag. * * @function * @name archive * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('a', 'archive'); /** * Set the compress flag. * * @function * @name compress * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('z', 'compress'); /** * Set the recursive flag. * * @function * @name recursive * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('r', 'recursive'); /** * Set the update flag. * * @function * @name update * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('u', 'update'); /** * Set the quiet flag. * * @function * @name quiet * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('q', 'quiet'); /** * Set the dirs flag. * * @function * @name dirs * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('d', 'dirs'); /** * Set the links flag. * * @function * @name links * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('l', 'links'); /** * Set the dry flag. * * @function * @name dry * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('n', 'dry'); /** * Set the hard links flag preserving hard links for the files transmitted. * * @function * @name hardLinks * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('H', 'hardLinks'); /** * Set the perms flag. * * @function * @name perms * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('p', 'perms'); /** * Set the executability flag to preserve executability for the files * transmitted. * * @function * @name executability * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('E', 'executability'); /** * Set the group flag to preserve the group permissions of the files * transmitted. * * @function * @name group * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('g', 'group'); /** * Set the owner flag to preserve the owner of the files transmitted. * * @function * @name owner * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('o', 'owner'); /** * Set the acls flag to preserve the ACLs for the files transmitted. * * @function * @name acls * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('A', 'acls'); /** * Set the xattrs flag to preserve the extended attributes for the files * transmitted. * * @function * @name xattrs * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('X', 'xattrs'); /** * Set the devices flag to preserve device files in the transfer. * * @function * @name devices * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('devices'); /** * Set the specials flag to preserve special files. * * @function * @name specials * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('specials'); /** * Set the times flag to preserve times for the files in the transfer. * * @function * @name times * @memberOf Rsync.prototype * @return {Rsync} */ exposeShortOption('t', 'times'); // our awesome export product module.exports = Rsync; /* **** */ /** * Create a chainable function on the Rsync prototype for getting and setting an * internal value. * @param {String} name * @param {String} internal */ function createValueAccessor(name, internal) { var container = internal || '_' + name; Rsync.prototype[name] = function(value) { if (!arguments.length) return this[container]; this[container] = value; return this; }; } /** * @param {String} name * @param {String} internal */ function createListAccessor(name, internal) { var container = internal || '_' + name; Rsync.prototype[name] = function(value) { if (!arguments.length) return this[container]; if (isArray(value)) { value.forEach(this[name], this); } else if (typeof(value) !== 'string') { throw new Error('Value for Rsync::' + name + ' must be a String'); } else if (this[container].indexOf(value) < 0) { this[container].push(value); } return this; }; } /** * Create a shorthand method on the Rsync prototype for setting and unsetting a simple option. * @param {String} option * @param {String} name */ function exposeShortOption(option, name) { name = name || option; Rsync.prototype[name] = function(set) { // When no arguments are passed in assume the option // needs to be set if (!arguments.length) set = true; var method = (set) ? 'set' : 'unset'; return this[method](option); }; } /** * Create a function for an option that can be set multiple time. The option * will accumulate all values. * * @param {String} option * @param {[String]} name */ function exposeMultiOption(option, name) { name = name || option; Rsync.prototype[name] = function(value) { // When not arguments are passed in assume the options // current value is requested if (!arguments.length) return this.option(option); if (!value) { // Unset the option on falsy this.unset(option); } else if (isArray(value)) { // Call this method for each array value value.forEach(this[name], this); } else { // Add the value var current = this.option(option); if (!current) { value = [ value ]; } else if (!isArray(current)) { value = [ current, value ]; } else { value = current.concat(value); } this.set(option, value); } return this; }; } /** * Expose an rsync long option on the Rsync prototype. * @param {String} option The option to expose * @param {String} name An optional alternative name for the option. */ function exposeLongOption(option, name) { name = name || option; Rsync.prototype[name] = function(value) { // When not arguments are passed in assume the options // current value is requested if (!arguments.length) return this.option(option); var method = (value) ? 'set' : 'unset'; return this[method](option, value); }; } /** * Build an option for use in a shell command. * * @param {String} name * @param {String} value * @param {Function|boolean} escapeArg * @return {String} */ function buildOption(name, value, escapeArg) { if (typeof escapeArg === 'boolean') { escapeArg = (!escapeArg) ? noop : null; } if (typeof escapeArg !== 'function') { escapeArg = escapeShellArg; } // Detect single option key var single = (name.length === 1) ? true : false; // Decide on prefix and value glue var prefix = (single) ? '-' : '--'; var glue = (single) ? ' ' : '='; // Build the option var option = prefix + name; if (arguments.length > 1 && value) { value = escapeArg(String(value)); option += glue + value; } return option; } /** * Escape an argument for use in a shell command when necessary. * @param {String} arg * @return {String} */ function escapeShellArg(arg) { if (!/(["'`\\$ ])/.test(arg)) { return arg; } return '"' + arg.replace(/(["'`\\$])/g, '\\$1') + '"'; } /** * Escape a filename for use in a shell command. * @param {String} filename the filename to escape * @return {String} the escaped version of the filename */ function escapeFileArg(filename) { filename = filename.replace(/(["'`\s\\\(\)\\$])/g,'\\$1'); if (!/(\\\\)/.test(filename)) { return filename; } // Under Windows rsync (with cygwin) and OpenSSH for Windows // (http://www.mls-software.com/opensshd.html) are using // standard linux directory separator so need to replace it if ('win32' === process.platform) { filename = filename.replace(/\\\\/g,'/').replace(/^["]?[A-Z]\:\//ig,'/'); } return filename; } /** * Strip the leading dashes from a value. * @param {String} value * @return {String} */ function stripLeadingDashes(value) { if (typeof(value) === 'string') { value = value.replace(/^[\-]*/, ''); } return value; } /** * Simple function for checking if a value is an Array. Will use the native * Array.isArray method if available. * @private * @param {Mixed} value * @return {Boolean} */ function isArray(value) { if (typeof(Array.isArray) === 'function') { return Array.isArray(value); } else { return toString.call(value) == '[object Array]'; } } /** * Simple hasOwnProperty wrapper. This will call hasOwnProperty on the obj * through the Object prototype. * @private * @param {Object} obj The object to check the property on * @param {String} key The name of the property to check * @return {Boolean} */ function hasOP(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } function noop() {} /** * Simple debug printer. * * @private * @param {Rsync} cmd * @param {String} message */ function debug(cmd, message) { if (!cmd._debug) return; }