1126 lines
26 KiB
JavaScript
1126 lines
26 KiB
JavaScript
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 = []
|
|
|
|
// just a list of exclude only patterns for use elsewhere like a watcher
|
|
this._exclude = []
|
|
|
|
// list of exclude files
|
|
this._excludeFiles = []
|
|
|
|
// 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 ]
|
|
}
|
|
|
|
// save this list for other uses
|
|
this._exclude = [...this._exclude,...patterns]
|
|
|
|
patterns.forEach(function(pattern) {
|
|
this._patterns.push({ action: '-', pattern: pattern })
|
|
}, this)
|
|
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Exclude a file of globs/patterns. The file path will to a list
|
|
* of file paths to be used with --exclude-from.
|
|
* dgk added this
|
|
* @param {String|Array} filePaths
|
|
* @return Rsync
|
|
*/
|
|
Rsync.prototype.excludeFrom = function(filePaths) {
|
|
if (arguments.length > 1) {
|
|
filePaths = Array.prototype.slice.call(arguments, 0)
|
|
}
|
|
if (!isArray(filePaths)) {
|
|
filePaths = [ filePaths ]
|
|
}
|
|
|
|
filePaths.forEach(function(filePath) {
|
|
if (filePath.indexOf('/') < 0) filePath=path.normalize(`${this._sources[0]}/${filePath}`)
|
|
// TODO only add to list if file exsists
|
|
this._excludeFiles.push(filePath)
|
|
}, this)
|
|
|
|
// this._excludeFiles = [...this._excludeFiles,...filePaths]
|
|
|
|
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 exclude-from file paths
|
|
this._excludeFiles.forEach(function(file) {
|
|
args.push(`--exclude-from=${escapeFileArg(file)}`)
|
|
})
|
|
|
|
// 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 = stderr
|
|
}
|
|
|
|
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
|
|
}
|