├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── simple.js ├── package.json ├── rsync.js └── tests ├── accessors.test.js ├── filters.test.js ├── helpers └── output.js ├── input.test.js ├── inputwin32.test.js ├── options.test.js ├── shorthands.test.js └── z_output.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "iojs" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2016 Mattijs Hoitink 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rsync ![build status](https://travis-ci.org/mattijs/node-rsync.svg?branch=master) 2 | 3 | [![NPM](https://nodei.co/npm/rsync.png?downloads=true)](https://nodei.co/npm/rsync/) 4 | 5 | `Rsync` is a class for building and executing `rsync` commands with Node.js. 6 | 7 | ## Installation 8 | 9 | Installation goes through NPM: 10 | 11 | ``` 12 | $ npm install rsync 13 | ``` 14 | 15 | ## License 16 | 17 | This module is licensed under the MIT License. See the `LICENSE` file for more details. 18 | 19 | ## Simple usage 20 | 21 | ```javascript 22 | var Rsync = require('rsync'); 23 | 24 | // Build the command 25 | var rsync = new Rsync() 26 | .shell('ssh') 27 | .flags('az') 28 | .source('/path/to/source') 29 | .destination('server:/path/to/destination'); 30 | 31 | // Execute the command 32 | rsync.execute(function(error, code, cmd) { 33 | // we're done 34 | }); 35 | ``` 36 | 37 | For more examples see the `examples` directory. 38 | 39 | # API 40 | 41 | * [constructor](#constructor) 42 | * [instance methods](#instance-methods) 43 | * [accessor methods](#accessor-methods) 44 | * [static methods](#static-methods) 45 | 46 | ## constructor 47 | 48 | Construct a new Rsync command instance. The constructor takes no arguments. 49 | 50 | ```javascript 51 | var rsync = new Rsync(); 52 | ``` 53 | 54 | ## instance methods 55 | 56 | ### set(option, value) 57 | 58 | Set an option. This can be any option from the rsync manual. The value is optional and only applies to options that take a value. This is not checked however. Supplying a value for an option that does not take a value will append the value regardless. This may cause errors when the command is executed. 59 | 60 | ```javascript 61 | rsync.set('a') 62 | .set('progress') 63 | .set('list-only') 64 | .set('exclude-from', '/path/to/exclude-file'); 65 | ``` 66 | 67 | Options must be unique and setting the same option twice will override any existing value. For options that can be set multiple times special methods exist (see accessor methods). Any leading dashes (-) are stripped when setting the option. 68 | 69 | The `set` method is chainable. 70 | 71 | ### unset(option) 72 | 73 | Unset an option. Any leading dashes (-) are stripped when unsetting an option. 74 | 75 | ```javascript 76 | rsync.unset('progress') 77 | .unset('quiet'); 78 | ``` 79 | 80 | The `unset` method is chainable. 81 | 82 | ### flags(flags, set) 83 | 84 | Set one or more flags. Flags are single letter options without a value, for example _compress_ (`-z`) or _archive_ (`-a`). 85 | 86 | The `flags` method is a polymorphic function: it can take different arguments to set flags. 87 | Flags can be presented as a single string with multiple flags, multiple strings as arguments, an array containing strings or an object with the flags as keys. 88 | 89 | Whether the presented flags need to be set or unset is determined based on the last argument, if this is a Boolean. When presenting the flags as an Object the value for each key (flag) determines if the flag is set or unset. This version can be used to mix setting and unsetting of flags in one statement. 90 | 91 | ```javascript 92 | // As String 93 | rsync.flags('avz'); // set 94 | rsync.flags('avz', false); // unset 95 | 96 | // As String arguments 97 | rsync.flags('a', 'v', 'z'); // set 98 | rsync.flags('a', 'v', 'z', false); // unset 99 | 100 | // As Array 101 | rsync.flags(['a', 'v', 'z']); // set 102 | rsync.flags(['a', 'z'], false); // unset 103 | 104 | // As Object 105 | rsync.flags({ 106 | 'a': true, // set 107 | 'z': true, // set 108 | 'v': false // unset 109 | }); 110 | ``` 111 | 112 | The `flags` method is chainable. 113 | 114 | ### isSet(option) 115 | 116 | Check if an option is set. 117 | 118 | This method does not check alternate versions for an option. When an option is set as the short version this method will still return `false` when checking for the long version, event though they are the same option. 119 | 120 | ```javascript 121 | rsync.set('quiet'); 122 | rsync.isSet('quiet'); // is TRUE 123 | rsync.isSet('q'); // is FALSE 124 | ``` 125 | 126 | ### option(option) 127 | 128 | Get the value for an option by name. If a valueless option is requested null will be returned. 129 | 130 | ```javascript 131 | rsync.option('rsh'); // returns String value 132 | rsync.option('progress'); // returns NULL 133 | ``` 134 | 135 | ### args() 136 | 137 | Get the arguments list for the command that is going to be executed. Returns an Array with the complete options that will be passed to the command. 138 | 139 | ### command() 140 | 141 | Get the complete command that is going to be executed. 142 | 143 | ```javascript 144 | var rsync = new Rsync() 145 | .shell('ssh') 146 | .flags('az') 147 | .source('/p/t/source') 148 | .destination('server:/p/t/dest'); 149 | 150 | var c = rsync.command(); 151 | // c is "rsync -az --rsh="ssh" /p/t/source server:/p/t/dest 152 | ``` 153 | 154 | ### cwd(path) 155 | 156 | Set or get the value for rsync process cwd. 157 | 158 | ```javascript 159 | rsync.cwd(__dirname); // Set cwd to __dirname 160 | rsync.cwd(); // Get cwd value 161 | ``` 162 | 163 | ### env(envObj) 164 | 165 | Set or get the value for rsync process environment variables. 166 | 167 | Default: process.env 168 | 169 | ```javascript 170 | rsync.env(process.env); // Set env to process.env 171 | rsync.env(); // Get env values 172 | ``` 173 | 174 | ### output(stdoutHandler, stderrHandler) 175 | 176 | Register output handler functions for the commands stdout and stderr output. The handlers will be 177 | called with streaming data from the commands output when it is executed. 178 | 179 | ```javascript 180 | rsync.output( 181 | function(data){ 182 | // do things like parse progress 183 | }, function(data) { 184 | // do things like parse error output 185 | } 186 | ); 187 | ``` 188 | 189 | This method can be called with an array containing one or two functions. These functions will 190 | be treated as the stdoutHandler and stderrHandler arguments. This makes it possible to register 191 | handlers through the `Rsync.build` method by specifying the functions as an array. 192 | 193 | ```javascript 194 | var rsync = Rsync.build({ 195 | // ... 196 | output: [stdoutFunc, stderrFunc] // these are references to functions defined elsewhere 197 | // ... 198 | }); 199 | ``` 200 | 201 | ### execute(callback, stdoutHandler, stderrHandler) 202 | 203 | Execute the command. The callback function is called with an Error object (or null when there 204 | was none), the exit code from the executed command and the executed command as a String. 205 | 206 | When `stdoutHandler` and `stderrHandler` functions are provided they will be used to stream 207 | data from stdout and stderr directly without buffering. Any output handlers that were 208 | defined previously will be overwritten. 209 | 210 | The function returns the child process object, which can be used to kill the 211 | rsync process or clean up if the main program exits early. 212 | 213 | ```javascript 214 | // signal handler function 215 | var quitting = function() { 216 | if (rsyncPid) { 217 | rsyncPid.kill(); 218 | } 219 | process.exit(); 220 | } 221 | process.on("SIGINT", quitting); // run signal handler on CTRL-C 222 | process.on("SIGTERM", quitting); // run signal handler on SIGTERM 223 | process.on("exit", quitting); // run signal handler when main process exits 224 | 225 | // simple execute 226 | var rsyncPid = rsync.execute(function(error, code, cmd) { 227 | // we're done 228 | }); 229 | 230 | // execute with stream callbacks 231 | var rsyncPid = rsync.execute( 232 | function(error, code, cmd) { 233 | // we're done 234 | }, function(data){ 235 | // do things like parse progress 236 | }, function(data) { 237 | // do things like parse error output 238 | } 239 | ); 240 | ``` 241 | 242 | ## option shorthands 243 | 244 | The following option shorthand methods are available: 245 | 246 | - **shell(value)**: `--rsh=SHELL` 247 | - **delete()**: `--delete` 248 | - **progress()**: `--progress` 249 | - **archive()**: `-a` 250 | - **compress()**: `-z` 251 | - **recursive()**: `-r` 252 | - **update()**: `-u` 253 | - **quiet()**: `-q` 254 | - **dirs()**: `-d` 255 | - **links()**: `-l` 256 | - **dry()**: `-n` 257 | - **chmod(value)**: `--chmod=VALUE` (accumulative) 258 | - **hardLinks()**: `-H` 259 | - **perms()**: `-p` 260 | - **executability()**: `-E` 261 | - **owner()**: `-o` 262 | - **group()**: `-g` 263 | - **acls()**: `-A` 264 | - **xattrs()**: `-X` 265 | - **devices()**: `--devices` 266 | - **specials**: `--specials` 267 | - **times()**: `-t` 268 | 269 | 270 | All shorthand methods are chainable as long as options that require a value are provided with one. 271 | 272 | ## accessor methods 273 | 274 | These methods can be used to get or set values in a chainable way. When the methods are called without arguments the current value is returned. When the methods are called with a value this will override the current value and the Rsync instance is returned to provide the chainability. 275 | 276 | ### executable(executable) 277 | 278 | Get or set the executable to use as the rsync command. 279 | 280 | ### executableShell(shell) 281 | 282 | Get or set the shell to use to launch the rsync command on non-Windows (Unix and Mac OS X) systems. The default shell is /bin/sh. 283 | 284 | On some systems (Debian, for example) /bin/sh links to /bin/dash, which does not do proper process control. If you have problems with leftover processes, try a different shell such as /bin/bash. 285 | 286 | ### destination(destination) 287 | 288 | Get or set the destination for the rsync command. 289 | 290 | ### source(source) 291 | 292 | Get or set the source or sources for the rsync command. When this method is called multiple times with a value it is appended to the list of sources. It is also possible to present the list of source as an array where each value will be appended to the list of sources 293 | 294 | ```javascript 295 | // chained 296 | rsync.source('/a/path') 297 | .source('/b/path'); 298 | 299 | // as Array 300 | rsync.source(['/a/path', '/b/path']); 301 | ``` 302 | 303 | In both cases the list of sources will contain two paths. 304 | 305 | ### patterns(patterns) 306 | 307 | Register a list of file patterns to include/exclude in the transfer. Patterns can be registered as 308 | an array of Strings or Objects. 309 | 310 | When registering a pattern as a String it be prefixed with a `+` or `-` sign to 311 | signal include or exclude for the pattern. The sign will be stripped of and the 312 | pattern will be added to the ordered pattern list. 313 | 314 | When registering the pattern as an Object it must contain the `action` and 315 | `pattern` keys where `action` contains the `+` or `-` sign and the `pattern` 316 | key contains the file pattern, without the `+` or `-` sign. 317 | 318 | The order of patterns is important for some rsync commands. The patterns are stored in the order 319 | they are added either through the `patterns` method or the `include` and `exclude` methods. The 320 | `patterns` method can be used with `Rsync.build` to provide an ordered list for the command. 321 | 322 | ```javascript 323 | // on an existing Rsync object 324 | rsync.patterns([ '-.git', { action: '+', pattern: '/some_dir' }); 325 | 326 | // through Rsync.build 327 | var command = Rsync.build({ 328 | // ... 329 | patterns: [ '-.git', { action: '+', pattern: '/some_dir' } ] 330 | // ... 331 | }); 332 | ``` 333 | ### exclude(pattern) 334 | 335 | Exclude a pattern from transfer. When this method is called multiple times with a value it is 336 | appended to the list of patterns. It is also possible to present the list of excluded 337 | patterns as an array where each pattern will be appended to the list. 338 | 339 | ```javascript 340 | // chained 341 | rsync.exclude('.git') 342 | .exclude('.DS_Store'); 343 | 344 | // as Array 345 | rsync.exclude(['.git', '.DS_Store']); 346 | ``` 347 | 348 | ### include(pattern) 349 | 350 | Include a pattern for transfer. When this method is called multiple times with a value it is 351 | appended to the list of patterns. It is also possible to present the list of included patterns as 352 | an array where each pattern will be appended to the list. 353 | 354 | ```javascript 355 | // chained 356 | rsync.include('/a/file') 357 | .include('/b/file'); 358 | 359 | // as Array 360 | rsync.include(['/a/file', '/b/file']); 361 | ``` 362 | 363 | ### debug(flag) 364 | 365 | Get or set the debug flag. This is only used internally and must be a Boolean to set or unset. 366 | 367 | ## static methods 368 | 369 | ### build 370 | 371 | For convenience there is the `build` function on the Rsync contructor. This function can be 372 | used to create a new Rsync command instance from an options object. 373 | 374 | For each key in the options object the corresponding method on the Rsync instance will be 375 | called. When a function for the key does not exist it is ignored. An existing Rsync instance 376 | can optionally be provided. 377 | 378 | ```javascript 379 | var rsync = Rsync.build({ 380 | source: '/path/to/source', 381 | destination: 'server:/path/to/destination', 382 | exclude: ['.git'], 383 | flags: 'avz', 384 | shell: 'ssh' 385 | }); 386 | 387 | rsync.execute(function(error, stdout, stderr) { 388 | // we're done 389 | }); 390 | ``` 391 | 392 | # Development 393 | 394 | If there is something missing (which there probably is) just fork, patch and send a pull request. 395 | 396 | For adding a new shorthand method there are a few simple steps to take: 397 | - Fork 398 | - Add the option through the `exposeShortOption`, `exposeLongOption` or `exposeMultiOption` functions. For examples see the source file. 399 | - Update this README file to list the new shorthand method 400 | - Make a pull request 401 | 402 | When adding a shorthand make sure it does not already exist, it is a sane name and a shorthand is necessary. 403 | 404 | If there is something broken (which there probably is), the same applies: fork, patch, pull request. Opening an issue is also possible. 405 | 406 | # Changelog 407 | 408 | v0.6.1 409 | 410 | - Add support for windows file paths under cygwin (#53) 411 | 412 | v0.6.0 413 | 414 | - Escape dollar signs in filenames (#40) 415 | - Add permission shorthands (#46) 416 | - Added env() option to set the process environment variables (#51) 417 | 418 | v0.5.0 419 | 420 | - Properly treat flags as String 421 | - Differentiate between shell and file arguments (escaping) 422 | - Added a bunch of unit tests 423 | - Added TravisCI setup to run tests on branches and PRs 424 | - Added cwd() option to set the process CWD (#36) 425 | 426 | v0.4.0 427 | 428 | - Child process pid is returned from `execute` (#27) 429 | - Command execution shell is configurable for Unix systems (#27) 430 | - Better escaping for filenames with spaces (#24) 431 | 432 | v0.3.0 433 | 434 | - Launch the command under a shell (#15) 435 | - Typo fix isaArray -> isArray for issue (#14) 436 | - Error: rsync exited with code 14 (#11) 437 | 438 | v0.2.0 439 | 440 | - use spawn instead of exec (#6) 441 | 442 | v0.1.0 443 | 444 | - better support for include/exclude filters 445 | - better support for output handlers 446 | - removed output buffering (#6) 447 | 448 | v0.0.2 449 | 450 | - swapped exclude and include order 451 | - better shell escaping 452 | 453 | v0.0.1 454 | 455 | - initial version (actually the second) 456 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A simple example that builds and executes the following command: 3 | * 4 | * rsync -avz --rsh 'ssh' /path/to/source you@server:/destination/path 5 | * 6 | * The `execute` method receives an Error object when an error ocurred, the 7 | * exit code from the executed command and the executed command as a String. 8 | * 9 | * The `shell` method is a shorthand for using `set('rsh', 'ssh')`. 10 | */ 11 | 12 | var Rsync = require('../rsync'); 13 | var cmd; 14 | 15 | /* 16 | * Set up the command using the fluent interface, starting with an 17 | * empty command wrapper and adding options using methods. 18 | */ 19 | cmd = new Rsync() 20 | .flags('avz') 21 | .shell('ssh') 22 | .source('/path/to/source') 23 | .destination('you@server:/destination/path'); 24 | 25 | cmd.execute(function(error, code, cmd) { 26 | console.log('All done executing', cmd); 27 | }); 28 | 29 | /* 30 | * The same command can be set up by using the build method. 31 | * 32 | * This method takes an Object containing the configuration for the 33 | * Rsync command it returns. 34 | */ 35 | cmd = Rsync.build({ 36 | 'flags': 'avz', 37 | 'shell': 'ssh', 38 | 'source': '/path/tp/source', 39 | 'destination': 'you@server:/destination/path' 40 | }) 41 | 42 | cmd.execute(function(error, code, cmd) { 43 | console.log('All done executing', cmd); 44 | }); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsync", 3 | "version": "0.6.1", 4 | "description": "Rsync cli wrapper", 5 | "main": "rsync.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/mattijs/node-rsync" 9 | }, 10 | "keywords": [ 11 | "rsync", 12 | "wrapper", 13 | "cli", 14 | "command" 15 | ], 16 | "author": "Mattijs Hoitink ", 17 | "license": "MIT", 18 | "readmeFilename": "README.md", 19 | "devDependencies": { 20 | "chai": "^3.5.0", 21 | "mocha": "^2.5.3", 22 | "sinon": "^1.17.4" 23 | }, 24 | "scripts": { 25 | "test": "mocha -R spec tests/*.test.js" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rsync.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | var path = require('path'); 3 | 4 | /** 5 | * Rsync is a wrapper class to configure and execute an `rsync` command 6 | * in a fluent and convenient way. 7 | * 8 | * A new command can be set up by creating a new `Rsync` instance of 9 | * obtaining one through the `build` method. 10 | * 11 | * @example 12 | * // using the constructor 13 | * var rsync = new Rsync() 14 | * .source('/path/to/source') 15 | * .destination('myserver:destination/'); 16 | * 17 | * // using the build method with options 18 | * var rsync = Rsync.build({ 19 | * source: '/path/to/source', 20 | * destination: 'myserver:destination/' 21 | * }); 22 | * 23 | * Executing the command can be done using the `execute` method. The command 24 | * is executed as a child process and three callbacks can be registered. See 25 | * the `execute` method for more details. 26 | * 27 | * @example 28 | * rsync.execute(function(error, code, cmd) { 29 | * // function called when the child process is finished 30 | * }, function(stdoutChunk) { 31 | * // function called when a chunk of text is received on stdout 32 | * }, function stderrChunk) { 33 | * // function called when a chunk of text is received on stderr 34 | * }); 35 | * 36 | * @author Mattijs Hoitink 37 | * @copyright Copyright (c) 2013, Mattijs Hoitink 38 | * @license The MIT License 39 | * 40 | * @constructor 41 | * @param {Object} config Configuration settings for the Rsync wrapper. 42 | */ 43 | function Rsync(config) { 44 | if (!(this instanceof Rsync)) { 45 | return new Rsync(config); 46 | } 47 | 48 | // Parse config 49 | config = config || {}; 50 | if (typeof(config) !== 'object') { 51 | throw new Error('Rsync config must be an Object'); 52 | } 53 | 54 | // executable 55 | this._executable = hasOP(config, 'executable') ? config.executable : 'rsync'; 56 | 57 | // shell 58 | this._executableShell = hasOP(config, 'executableShell') ? config.executableShell : '/bin/sh'; 59 | 60 | // source(s) and destination 61 | this._sources = []; 62 | this._destination = ''; 63 | 64 | // ordered list of file patterns to include/exclude 65 | this._patterns = []; 66 | 67 | // options 68 | this._options = {}; 69 | 70 | // output callbacks 71 | this._outputHandlers = { 72 | stdout: null, 73 | stderr: null 74 | }; 75 | 76 | this._cwd = process.cwd(); 77 | 78 | // Allow child_process.spawn env overriding 79 | this._env = process.env; 80 | 81 | // Debug parameter 82 | this._debug = hasOP(config, 'debug') ? config.debug : false; 83 | } 84 | 85 | /** 86 | * Build a new Rsync command from an options Object. 87 | * @param {Object} options 88 | * @return {Rsync} 89 | */ 90 | Rsync.build = function(options) { 91 | var command = new Rsync(); 92 | 93 | // Process all options 94 | for (var key in options) { 95 | if (hasOP(options, key)) { 96 | var value = options[key]; 97 | 98 | // Only allow calling methods on the Rsync command 99 | if (typeof(command[key]) === 'function') { 100 | command[key](value); 101 | } 102 | } 103 | } 104 | 105 | return command; 106 | }; 107 | 108 | /** 109 | * Set an option. 110 | * @param {String} option 111 | * @param mixed value 112 | * @return Rsync 113 | */ 114 | Rsync.prototype.set = function(option, value) { 115 | option = stripLeadingDashes(option); 116 | if (option && option.length > 0) { 117 | this._options[option] = value || null; 118 | } 119 | return this; 120 | }; 121 | 122 | /** 123 | * Unset an option. 124 | * @param {String} option 125 | * @return Rsync 126 | */ 127 | Rsync.prototype.unset = function(option) { 128 | option = stripLeadingDashes(option); 129 | 130 | if (option && Object.keys(this._options).indexOf(option) >= 0) { 131 | delete this._options[option]; 132 | } 133 | return this; 134 | }; 135 | 136 | /** 137 | * Set or unset one or more flags. A flag is a single letter option without a value. 138 | * 139 | * Flags can be presented as a single String, an Array containing Strings or an Object 140 | * with the flags as keys. 141 | * 142 | * When flags are presented as a String or Array the set or unset method will be determined 143 | * by the second parameter. 144 | * When the flags are presented as an Object the set or unset method will be determined by 145 | * the value corresponding to each flag key. 146 | * 147 | * @param {String|Array|Object} flags 148 | * @param {Boolean} set 149 | * @return Rsync 150 | */ 151 | Rsync.prototype.flags = function(flags, set) { 152 | // Do some argument handling 153 | if (!arguments.length) { 154 | return this; 155 | } 156 | else if (arguments.length === 1) { 157 | set = true; 158 | } 159 | else { 160 | // There are more than 1 arguments, assume flags are presented as strings 161 | flags = Array.prototype.slice.call(arguments); 162 | 163 | // Check if the last argument is a boolean 164 | if (typeof(flags[flags.length - 1]) === 'boolean') { 165 | set = flags.pop(); 166 | } 167 | else { 168 | set = true; 169 | } 170 | 171 | // Join the remainder of the arguments to treat them as flags 172 | flags = flags.join(''); 173 | } 174 | 175 | // Split multiple flags 176 | if (typeof(flags) === 'string') { 177 | flags = stripLeadingDashes(flags).split(''); 178 | } 179 | 180 | // Turn array into an object 181 | if (isArray(flags)) { 182 | var obj = {}; 183 | flags.forEach(function(f) { 184 | obj[f] = set; 185 | }); 186 | flags = obj; 187 | } 188 | 189 | // set/unset each flag 190 | for (var key in flags) { 191 | if (hasOP(flags, key)) { 192 | var method = (flags[key]) ? 'set' : 'unset'; 193 | this[method](stripLeadingDashes(key)); 194 | } 195 | } 196 | 197 | return this; 198 | }; 199 | 200 | /** 201 | * Check if an option is set. 202 | * @param {String} option 203 | * @return {Boolean} 204 | */ 205 | Rsync.prototype.isSet = function(option) { 206 | option = stripLeadingDashes(option); 207 | return Object.keys(this._options).indexOf(option) >= 0; 208 | }; 209 | 210 | /** 211 | * Get an option by name. 212 | * @param {String} name 213 | * @return mixed 214 | */ 215 | Rsync.prototype.option = function(name) { 216 | name = stripLeadingDashes(name); 217 | return this._options[name]; 218 | }; 219 | 220 | /** 221 | * Register a list of file patterns to include/exclude in the transfer. Patterns can be 222 | * registered as an array of Strings or Objects. 223 | * 224 | * When registering a pattern as a String it must be prefixed with a `+` or `-` sign to 225 | * signal include or exclude for the pattern. The sign will be stripped of and the 226 | * pattern will be added to the ordered pattern list. 227 | * 228 | * When registering the pattern as an Object it must contain the `action` and 229 | * `pattern` keys where `action` contains the `+` or `-` sign and the `pattern` 230 | * key contains the file pattern, without the `+` or `-` sign. 231 | * 232 | * @example 233 | * // on an existing rsync object 234 | * rsync.patterns(['-docs', { action: '+', pattern: '/subdir/*.py' }]); 235 | * 236 | * // using Rsync.build for a new rsync object 237 | * rsync = Rsync.build({ 238 | * ... 239 | * patterns: [ '-docs', { action: '+', pattern: '/subdir/*.py' }] 240 | * ... 241 | * }) 242 | * 243 | * @param {Array} patterns 244 | * @return Rsync 245 | */ 246 | Rsync.prototype.patterns = function(patterns) { 247 | if (arguments.length > 1) { 248 | patterns = Array.prototype.slice.call(arguments, 0); 249 | } 250 | if (!isArray(patterns)) { 251 | patterns = [ patterns ]; 252 | } 253 | 254 | patterns.forEach(function(pattern) { 255 | var action = '?'; 256 | if (typeof(pattern) === 'string') { 257 | action = pattern.charAt(0); 258 | pattern = pattern.substring(1); 259 | } 260 | else if ( 261 | typeof(pattern) === 'object' && 262 | hasOP(pattern, 'action') && 263 | hasOP(pattern, 'pattern') 264 | ) { 265 | action = pattern.action; 266 | pattern = pattern.pattern; 267 | } 268 | 269 | // Check if the pattern is an include or exclude 270 | if (action === '-') { 271 | this.exclude(pattern); 272 | } 273 | else if (action === '+') { 274 | this.include(pattern); 275 | } 276 | else { 277 | throw new Error('Invalid pattern: ' + pattern); 278 | } 279 | }, this); 280 | 281 | return this; 282 | }; 283 | 284 | /** 285 | * Exclude a file pattern from transfer. The pattern will be appended to the ordered list 286 | * of patterns for the rsync command. 287 | * 288 | * @param {String|Array} patterns 289 | * @return Rsync 290 | */ 291 | Rsync.prototype.exclude = function(patterns) { 292 | if (arguments.length > 1) { 293 | patterns = Array.prototype.slice.call(arguments, 0); 294 | } 295 | if (!isArray(patterns)) { 296 | patterns = [ patterns ]; 297 | } 298 | 299 | patterns.forEach(function(pattern) { 300 | this._patterns.push({ action: '-', pattern: pattern }); 301 | }, this); 302 | 303 | return this; 304 | }; 305 | 306 | /** 307 | * Include a file pattern for transfer. The pattern will be appended to the ordered list 308 | * of patterns for the rsync command. 309 | * 310 | * @param {String|Array} patterns 311 | * @return Rsync 312 | */ 313 | Rsync.prototype.include = function(patterns) { 314 | if (arguments.length > 1) { 315 | patterns = Array.prototype.slice.call(arguments, 0); 316 | } 317 | if (!isArray(patterns)) { 318 | patterns = [ patterns ]; 319 | } 320 | 321 | patterns.forEach(function(pattern) { 322 | this._patterns.push({ action: '+', pattern: pattern }); 323 | }, this); 324 | 325 | return this; 326 | }; 327 | 328 | /** 329 | * Get the command that is going to be executed. 330 | * @return {String} 331 | */ 332 | Rsync.prototype.command = function() { 333 | return this.executable() + ' ' + this.args().join(' '); 334 | }; 335 | 336 | /** 337 | * String representation of the Rsync command. This is the command that is 338 | * going to be executed when calling Rsync::execute. 339 | * @return {String} 340 | */ 341 | Rsync.prototype.toString = Rsync.prototype.command; 342 | 343 | /** 344 | * Get the arguments for the rsync command. 345 | * @return {Array} 346 | */ 347 | Rsync.prototype.args = function() { 348 | // Gathered arguments 349 | var args = []; 350 | 351 | // Add options. Short options (one letter) without values are gathered together. 352 | // Long options have a value but can also be a single letter. 353 | var short = []; 354 | var long = []; 355 | 356 | // Split long and short options 357 | for (var key in this._options) { 358 | if (hasOP(this._options, key)) { 359 | var value = this._options[key]; 360 | var noval = (value === null || value === undefined); 361 | 362 | // Check for short option (single letter without value) 363 | if (key.length === 1 && noval) { 364 | short.push(key); 365 | } 366 | else { 367 | if (isArray(value)) { 368 | value.forEach(function (val) { 369 | long.push(buildOption(key, val, escapeShellArg)); 370 | }); 371 | } 372 | else { 373 | long.push(buildOption(key, value, escapeShellArg)); 374 | } 375 | } 376 | 377 | } 378 | } 379 | 380 | // Add combined short options if any are present 381 | if (short.length > 0) { 382 | args.push('-' + short.join('')); 383 | } 384 | 385 | // Add long options if any are present 386 | if (long.length > 0) { 387 | args = args.concat(long); 388 | } 389 | 390 | // Add includes/excludes in order 391 | this._patterns.forEach(function(def) { 392 | if (def.action === '-') { 393 | args.push(buildOption('exclude', def.pattern, escapeFileArg)); 394 | } 395 | else if (def.action === '+') { 396 | args.push(buildOption('include', def.pattern, escapeFileArg)); 397 | } 398 | else { 399 | debug(this, 'Unknown pattern action ' + def.action); 400 | } 401 | }); 402 | 403 | // Add sources 404 | if (this.source().length > 0) { 405 | args = args.concat(this.source().map(escapeFileArg)); 406 | } 407 | 408 | // Add destination 409 | if (this.destination()) { 410 | args.push(escapeFileArg(this.destination())); 411 | } 412 | 413 | return args; 414 | }; 415 | 416 | /** 417 | * Get and set rsync process cwd directory. 418 | * 419 | * @param {string} cwd= Directory path relative to current process directory. 420 | * @return {string} Return current _cwd. 421 | */ 422 | Rsync.prototype.cwd = function(cwd) { 423 | if (arguments.length > 0) { 424 | if (typeof cwd !== 'string') { 425 | throw new Error('Directory should be a string'); 426 | } 427 | 428 | this._cwd = path.resolve(cwd); 429 | } 430 | 431 | return this._cwd; 432 | }; 433 | 434 | /** 435 | * Get and set rsync process environment variables 436 | * 437 | * @param {string} env= Environment variables 438 | * @return {string} Return current _env. 439 | */ 440 | Rsync.prototype.env = function(env) { 441 | if (arguments.length > 0) { 442 | if (typeof env !== 'object') { 443 | throw new Error('Environment should be an object'); 444 | } 445 | 446 | this._env = env; 447 | } 448 | 449 | return this._env; 450 | }; 451 | 452 | /** 453 | * Register an output handlers for the commands stdout and stderr streams. 454 | * These functions will be called once data is streamed on one of the output buffers 455 | * when the command is executed using `execute`. 456 | * 457 | * Only one callback function can be registered for each output stream. Previously 458 | * registered callbacks will be overridden. 459 | * 460 | * @param {Function} stdout Callback Function for stdout data 461 | * @param {Function} stderr Callback Function for stderr data 462 | * @return Rsync 463 | */ 464 | Rsync.prototype.output = function(stdout, stderr) { 465 | // Check for single argument so the method can be used with Rsync.build 466 | if (arguments.length === 1 && Array.isArray(stdout)) { 467 | stderr = stdout[1]; 468 | stdout = stdout[0]; 469 | } 470 | 471 | if (typeof(stdout) === 'function') { 472 | this._outputHandlers.stdout = stdout; 473 | } 474 | if (typeof(stderr) === 'function') { 475 | this._outputHandlers.stderr = stdout; 476 | } 477 | 478 | return this; 479 | }; 480 | 481 | /** 482 | * Execute the rsync command. 483 | * 484 | * The callback function is called with an Error object (or null when there was none), 485 | * the exit code from the executed command and the executed command as a String. 486 | * 487 | * When stdoutHandler and stderrHandler functions are provided they will be used to stream 488 | * data from stdout and stderr directly without buffering. 489 | * 490 | * @param {Function} callback Called when rsync finishes (optional) 491 | * @param {Function} stdoutHandler Called on each chunk received from stdout (optional) 492 | * @param {Function} stderrHandler Called on each chunk received from stderr (optional) 493 | */ 494 | Rsync.prototype.execute = function(callback, stdoutHandler, stderrHandler) { 495 | // Register output handlers 496 | this.output(stdoutHandler, stderrHandler); 497 | 498 | // Execute the command as a child process 499 | // see https://github.com/joyent/node/blob/937e2e351b2450cf1e9c4d8b3e1a4e2a2def58bb/lib/child_process.js#L589 500 | var cmdProc; 501 | if ('win32' === process.platform) { 502 | cmdProc = spawn('cmd.exe', ['/s', '/c', '"' + this.command() + '"'], 503 | { stdio: 'pipe', windowsVerbatimArguments: true, cwd: this._cwd, env: this._env }); 504 | } 505 | else { 506 | cmdProc = spawn(this._executableShell, ['-c', this.command()], 507 | { stdio: 'pipe', cwd: this._cwd, env: this._env }); 508 | } 509 | 510 | // Capture stdout and stderr if there are output handlers configured 511 | if (typeof(this._outputHandlers.stdout) === 'function') { 512 | cmdProc.stdout.on('data', this._outputHandlers.stdout); 513 | } 514 | if (typeof(this._outputHandlers.stderr) === 'function') { 515 | cmdProc.stderr.on('data', this._outputHandlers.stderr); 516 | } 517 | 518 | // Wait for the command to finish 519 | cmdProc.on('close', function(code) { 520 | var error = null; 521 | 522 | // Check rsyncs error code 523 | // @see http://bluebones.net/2007/06/rsync-exit-codes/ 524 | if (code !== 0) { 525 | error = new Error('rsync exited with code ' + code); 526 | } 527 | 528 | // Check for callback 529 | if (typeof(callback) === 'function') { 530 | callback(error, code, this.command()); 531 | } 532 | }.bind(this)); 533 | 534 | // Return the child process object so it can be cleaned up 535 | // if the process exits 536 | return(cmdProc); 537 | }; 538 | 539 | /** 540 | * Get or set the debug property. 541 | * 542 | * The property is set to the boolean provided so unsetting the debug 543 | * property has to be done by passing false to this method. 544 | * 545 | * @function 546 | * @name debug 547 | * @memberOf Rsync.prototype 548 | * @param {Boolean} debug the value of the debug property (optional) 549 | * @return {Rsync|Boolean} 550 | */ 551 | createValueAccessor('debug'); 552 | 553 | /** 554 | * Get or set the executable to use for the rsync process. 555 | * 556 | * When setting the executable path the Rsync instance is returned for 557 | * the fluent interface. Otherwise the configured executable path 558 | * is returned. 559 | * 560 | * @function 561 | * @name executable 562 | * @memberOf Rsync.prototype 563 | * @param {String} executable path to the executable (optional) 564 | * @return {Rsync|String} 565 | */ 566 | createValueAccessor('executable'); 567 | 568 | /** 569 | * Get or set the shell to use on non-Windows (Unix or Mac OS X) systems. 570 | * 571 | * When setting the shell the Rsync instance is returned for the 572 | * fluent interface. Otherwise the configured shell is returned. 573 | * 574 | * @function 575 | * @name executableShell 576 | * @memberOf Rsync.prototype 577 | * @param {String} shell to use on non-Windows systems (optional) 578 | * @return {Rsync|String} 579 | */ 580 | createValueAccessor('executableShell'); 581 | 582 | /** 583 | * Get or set the destination for the transfer. 584 | * 585 | * When setting the destination the Rsync instance is returned for 586 | * the fluent interface. Otherwise the configured destination path 587 | * is returned. 588 | * 589 | * @function 590 | * @name destination 591 | * @memberOf Rsync.prototype 592 | * @param {String} destination the destination (optional) 593 | * @return {Rsync|String} 594 | */ 595 | createValueAccessor('destination'); 596 | 597 | /** 598 | * Add one or more sources for the command or get the list of configured 599 | * sources. 600 | * 601 | * The sources are appended to the list of known sources if they were not 602 | * included yet and the Rsync instance is returned for the fluent 603 | * interface. Otherwise the configured list of source is returned. 604 | * 605 | * @function 606 | * @name source 607 | * @memberOf Rsync.prototype 608 | * @param {String|Array} sources the source or list of sources to configure (optional) 609 | * @return {Rsync|Array} 610 | */ 611 | createListAccessor('source', '_sources'); 612 | 613 | /** 614 | * Set the shell to use when logging in on a remote server. 615 | * 616 | * This is the same as setting the `rsh` option. 617 | * 618 | * @function 619 | * @name shell 620 | * @memberOf Rsync.prototype 621 | * @param {String} shell the shell option to use 622 | * @return {Rsync} 623 | */ 624 | exposeLongOption('rsh', 'shell'); 625 | 626 | /** 627 | * Add a chmod instruction to the command. 628 | * 629 | * @function 630 | * @name chmod 631 | * @memberOf Rsync.prototype 632 | * @param {String|Array} 633 | * @return {Rsync|Array} 634 | */ 635 | exposeMultiOption('chmod', 'chmod'); 636 | 637 | /** 638 | * Set the delete flag. 639 | * 640 | * This is the same as setting the `--delete` commandline flag. 641 | * 642 | * @function 643 | * @name delete 644 | * @memberOf Rsync.prototype 645 | * @return {Rsync} 646 | */ 647 | exposeShortOption('delete'); 648 | 649 | /** 650 | * Set the progress flag. 651 | * 652 | * This is the same as setting the `--progress` commandline flag. 653 | * 654 | * @function 655 | * @name progress 656 | * @memberOf Rsync.prototype 657 | * @return {Rsync} 658 | */ 659 | exposeShortOption('progress'); 660 | 661 | /** 662 | * Set the archive flag. 663 | * 664 | * @function 665 | * @name archive 666 | * @memberOf Rsync.prototype 667 | * @return {Rsync} 668 | */ 669 | exposeShortOption('a', 'archive'); 670 | 671 | /** 672 | * Set the compress flag. 673 | * 674 | * @function 675 | * @name compress 676 | * @memberOf Rsync.prototype 677 | * @return {Rsync} 678 | */ 679 | exposeShortOption('z', 'compress'); 680 | 681 | /** 682 | * Set the recursive flag. 683 | * 684 | * @function 685 | * @name recursive 686 | * @memberOf Rsync.prototype 687 | * @return {Rsync} 688 | */ 689 | exposeShortOption('r', 'recursive'); 690 | 691 | /** 692 | * Set the update flag. 693 | * 694 | * @function 695 | * @name update 696 | * @memberOf Rsync.prototype 697 | * @return {Rsync} 698 | */ 699 | exposeShortOption('u', 'update'); 700 | 701 | /** 702 | * Set the quiet flag. 703 | * 704 | * @function 705 | * @name quiet 706 | * @memberOf Rsync.prototype 707 | * @return {Rsync} 708 | */ 709 | exposeShortOption('q', 'quiet'); 710 | 711 | /** 712 | * Set the dirs flag. 713 | * 714 | * @function 715 | * @name dirs 716 | * @memberOf Rsync.prototype 717 | * @return {Rsync} 718 | */ 719 | exposeShortOption('d', 'dirs'); 720 | 721 | /** 722 | * Set the links flag. 723 | * 724 | * @function 725 | * @name links 726 | * @memberOf Rsync.prototype 727 | * @return {Rsync} 728 | */ 729 | exposeShortOption('l', 'links'); 730 | 731 | /** 732 | * Set the dry flag. 733 | * 734 | * @function 735 | * @name dry 736 | * @memberOf Rsync.prototype 737 | * @return {Rsync} 738 | */ 739 | exposeShortOption('n', 'dry'); 740 | 741 | /** 742 | * Set the hard links flag preserving hard links for the files transmitted. 743 | * 744 | * @function 745 | * @name hardLinks 746 | * @memberOf Rsync.prototype 747 | * @return {Rsync} 748 | */ 749 | exposeShortOption('H', 'hardLinks'); 750 | 751 | /** 752 | * Set the perms flag. 753 | * 754 | * @function 755 | * @name perms 756 | * @memberOf Rsync.prototype 757 | * @return {Rsync} 758 | */ 759 | exposeShortOption('p', 'perms'); 760 | 761 | /** 762 | * Set the executability flag to preserve executability for the files 763 | * transmitted. 764 | * 765 | * @function 766 | * @name executability 767 | * @memberOf Rsync.prototype 768 | * @return {Rsync} 769 | */ 770 | exposeShortOption('E', 'executability'); 771 | 772 | /** 773 | * Set the group flag to preserve the group permissions of the files 774 | * transmitted. 775 | * 776 | * @function 777 | * @name group 778 | * @memberOf Rsync.prototype 779 | * @return {Rsync} 780 | */ 781 | exposeShortOption('g', 'group'); 782 | 783 | /** 784 | * Set the owner flag to preserve the owner of the files transmitted. 785 | * 786 | * @function 787 | * @name owner 788 | * @memberOf Rsync.prototype 789 | * @return {Rsync} 790 | */ 791 | exposeShortOption('o', 'owner'); 792 | 793 | /** 794 | * Set the acls flag to preserve the ACLs for the files transmitted. 795 | * 796 | * @function 797 | * @name acls 798 | * @memberOf Rsync.prototype 799 | * @return {Rsync} 800 | */ 801 | exposeShortOption('A', 'acls'); 802 | 803 | /** 804 | * Set the xattrs flag to preserve the extended attributes for the files 805 | * transmitted. 806 | * 807 | * @function 808 | * @name xattrs 809 | * @memberOf Rsync.prototype 810 | * @return {Rsync} 811 | */ 812 | exposeShortOption('X', 'xattrs'); 813 | 814 | /** 815 | * Set the devices flag to preserve device files in the transfer. 816 | * 817 | * @function 818 | * @name devices 819 | * @memberOf Rsync.prototype 820 | * @return {Rsync} 821 | */ 822 | exposeShortOption('devices'); 823 | 824 | /** 825 | * Set the specials flag to preserve special files. 826 | * 827 | * @function 828 | * @name specials 829 | * @memberOf Rsync.prototype 830 | * @return {Rsync} 831 | */ 832 | exposeShortOption('specials'); 833 | 834 | /** 835 | * Set the times flag to preserve times for the files in the transfer. 836 | * 837 | * @function 838 | * @name times 839 | * @memberOf Rsync.prototype 840 | * @return {Rsync} 841 | */ 842 | exposeShortOption('t', 'times'); 843 | 844 | // our awesome export product 845 | module.exports = Rsync; 846 | 847 | /* **** */ 848 | 849 | /** 850 | * Create a chainable function on the Rsync prototype for getting and setting an 851 | * internal value. 852 | * @param {String} name 853 | * @param {String} internal 854 | */ 855 | function createValueAccessor(name, internal) { 856 | var container = internal || '_' + name; 857 | 858 | Rsync.prototype[name] = function(value) { 859 | if (!arguments.length) return this[container]; 860 | this[container] = value; 861 | return this; 862 | }; 863 | } 864 | 865 | /** 866 | * @param {String} name 867 | * @param {String} internal 868 | */ 869 | function createListAccessor(name, internal) { 870 | var container = internal || '_' + name; 871 | 872 | Rsync.prototype[name] = function(value) { 873 | if (!arguments.length) return this[container]; 874 | 875 | if (isArray(value)) { 876 | value.forEach(this[name], this); 877 | } 878 | else if (typeof(value) !== 'string') { 879 | throw new Error('Value for Rsync::' + name + ' must be a String'); 880 | } 881 | else if (this[container].indexOf(value) < 0) { 882 | this[container].push(value); 883 | } 884 | 885 | return this; 886 | }; 887 | } 888 | 889 | /** 890 | * Create a shorthand method on the Rsync prototype for setting and unsetting a simple option. 891 | * @param {String} option 892 | * @param {String} name 893 | */ 894 | function exposeShortOption(option, name) { 895 | name = name || option; 896 | 897 | Rsync.prototype[name] = function(set) { 898 | // When no arguments are passed in assume the option 899 | // needs to be set 900 | if (!arguments.length) set = true; 901 | 902 | var method = (set) ? 'set' : 'unset'; 903 | return this[method](option); 904 | }; 905 | } 906 | 907 | /** 908 | * Create a function for an option that can be set multiple time. The option 909 | * will accumulate all values. 910 | * 911 | * @param {String} option 912 | * @param {[String]} name 913 | */ 914 | function exposeMultiOption(option, name) { 915 | name = name || option; 916 | 917 | Rsync.prototype[name] = function(value) { 918 | // When not arguments are passed in assume the options 919 | // current value is requested 920 | if (!arguments.length) return this.option(option); 921 | 922 | if (!value) { 923 | // Unset the option on falsy 924 | this.unset(option); 925 | } 926 | else if (isArray(value)) { 927 | // Call this method for each array value 928 | value.forEach(this[name], this); 929 | } 930 | else { 931 | // Add the value 932 | var current = this.option(option); 933 | if (!current) { 934 | value = [ value ]; 935 | } 936 | else if (!isArray(current)) { 937 | value = [ current, value ]; 938 | } 939 | else { 940 | value = current.concat(value); 941 | } 942 | 943 | this.set(option, value); 944 | } 945 | 946 | return this; 947 | }; 948 | } 949 | 950 | /** 951 | * Expose an rsync long option on the Rsync prototype. 952 | * @param {String} option The option to expose 953 | * @param {String} name An optional alternative name for the option. 954 | */ 955 | function exposeLongOption(option, name) { 956 | name = name || option; 957 | 958 | Rsync.prototype[name] = function(value) { 959 | // When not arguments are passed in assume the options 960 | // current value is requested 961 | if (!arguments.length) return this.option(option); 962 | 963 | var method = (value) ? 'set' : 'unset'; 964 | return this[method](option, value); 965 | }; 966 | } 967 | 968 | /** 969 | * Build an option for use in a shell command. 970 | * 971 | * @param {String} name 972 | * @param {String} value 973 | * @param {Function|boolean} escapeArg 974 | * @return {String} 975 | */ 976 | function buildOption(name, value, escapeArg) { 977 | if (typeof escapeArg === 'boolean') { 978 | escapeArg = (!escapeArg) ? noop : null; 979 | } 980 | 981 | if (typeof escapeArg !== 'function') { 982 | escapeArg = escapeShellArg; 983 | } 984 | 985 | // Detect single option key 986 | var single = (name.length === 1) ? true : false; 987 | 988 | // Decide on prefix and value glue 989 | var prefix = (single) ? '-' : '--'; 990 | var glue = (single) ? ' ' : '='; 991 | 992 | // Build the option 993 | var option = prefix + name; 994 | if (arguments.length > 1 && value) { 995 | value = escapeArg(String(value)); 996 | option += glue + value; 997 | } 998 | 999 | return option; 1000 | } 1001 | 1002 | /** 1003 | * Escape an argument for use in a shell command when necessary. 1004 | * @param {String} arg 1005 | * @return {String} 1006 | */ 1007 | function escapeShellArg(arg) { 1008 | if (!/(["'`\\$ ])/.test(arg)) { 1009 | return arg; 1010 | } 1011 | return '"' + arg.replace(/(["'`\\$])/g, '\\$1') + '"'; 1012 | } 1013 | 1014 | /** 1015 | * Escape a filename for use in a shell command. 1016 | * @param {String} filename the filename to escape 1017 | * @return {String} the escaped version of the filename 1018 | */ 1019 | function escapeFileArg(filename) { 1020 | filename = filename.replace(/(["'`\s\\\(\)\\$])/g,'\\$1'); 1021 | if (!/(\\\\)/.test(filename)) { 1022 | return filename; 1023 | } 1024 | // Under Windows rsync (with cygwin) and OpenSSH for Windows 1025 | // (http://www.mls-software.com/opensshd.html) are using 1026 | // standard linux directory separator so need to replace it 1027 | if ('win32' === process.platform) { 1028 | filename = filename.replace(/\\\\/g,'/').replace(/^["]?[A-Z]\:\//ig,'/'); 1029 | } 1030 | return filename; 1031 | } 1032 | 1033 | /** 1034 | * Strip the leading dashes from a value. 1035 | * @param {String} value 1036 | * @return {String} 1037 | */ 1038 | function stripLeadingDashes(value) { 1039 | if (typeof(value) === 'string') { 1040 | value = value.replace(/^[\-]*/, ''); 1041 | } 1042 | 1043 | return value; 1044 | } 1045 | 1046 | /** 1047 | * Simple function for checking if a value is an Array. Will use the native 1048 | * Array.isArray method if available. 1049 | * @private 1050 | * @param {Mixed} value 1051 | * @return {Boolean} 1052 | */ 1053 | function isArray(value) { 1054 | if (typeof(Array.isArray) === 'function') { 1055 | return Array.isArray(value); 1056 | } 1057 | else { 1058 | return toString.call(value) == '[object Array]'; 1059 | } 1060 | } 1061 | 1062 | /** 1063 | * Simple hasOwnProperty wrapper. This will call hasOwnProperty on the obj 1064 | * through the Object prototype. 1065 | * @private 1066 | * @param {Object} obj The object to check the property on 1067 | * @param {String} key The name of the property to check 1068 | * @return {Boolean} 1069 | */ 1070 | function hasOP(obj, key) { 1071 | return Object.prototype.hasOwnProperty.call(obj, key); 1072 | } 1073 | 1074 | function noop() {} 1075 | 1076 | /** 1077 | * Simple debug printer. 1078 | * 1079 | * @private 1080 | * @param {Rsync} cmd 1081 | * @param {String} message 1082 | */ 1083 | function debug(cmd, message) { 1084 | if (!cmd._debug) return; 1085 | } 1086 | -------------------------------------------------------------------------------- /tests/accessors.test.js: -------------------------------------------------------------------------------- 1 | /*global describe,it*/ 2 | 'use strict'; 3 | var Rsync = require('../rsync'); 4 | var assert = require('chai').assert; 5 | var assertOutput = require('./helpers/output').assertOutput; 6 | var path = require('path'); 7 | 8 | 9 | describe('accessors', function () { 10 | 11 | describe('#executable', function () { 12 | 13 | it('should set the executable to use', function () { 14 | var rsync = Rsync.build({ 15 | 'source': 'a.txt', 16 | 'destination': 'b.txt', 17 | 'executable': '/usr/local/bin/rsync' 18 | }); 19 | 20 | assert.equal('/usr/local/bin/rsync', rsync.executable(), 'executable was set'); 21 | assertOutput(rsync, '/usr/local/bin/rsync a.txt b.txt'); 22 | }); 23 | 24 | }); 25 | 26 | describe('#executableShell', function () { 27 | 28 | it('should set the the executable shell to use', function () { 29 | var rsync = Rsync.build({ 30 | 'source': 'a.txt', 31 | 'destination': 'b.txt', 32 | 'executableShell': '/bin/zsh' 33 | }); 34 | 35 | assert.equal('/bin/zsh', rsync.executableShell(), 'executableShell was set'); 36 | }); 37 | 38 | }); 39 | 40 | describe('#cwd', function () { 41 | 42 | it('should set the the cwd to use', function () { 43 | var rsync = Rsync.build({ 44 | 'source': 'a.txt', 45 | 'destination': 'b.txt', 46 | 'cwd': __dirname + '/..' 47 | }); 48 | 49 | assert.equal(path.resolve(__dirname, '..'), rsync.cwd(), 'cwd was set'); 50 | }); 51 | 52 | }); 53 | 54 | describe('#env', function () { 55 | 56 | it('should set the the env variables to use', function () { 57 | var rsync = Rsync.build({ 58 | 'source': 'a.txt', 59 | 'destination': 'b.txt', 60 | 'env': {'red': 'blue'} 61 | }); 62 | 63 | assert.equal('blue', rsync.env().red, 'env was set'); 64 | }); 65 | 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /tests/filters.test.js: -------------------------------------------------------------------------------- 1 | /*global describe,it,beforeEach,xdescribe,xit*/ 2 | 'use strict'; 3 | var assert = require('chai').assert; 4 | var assertOutput = require('./helpers/output').assertOutput; 5 | 6 | var Rsync = require('../rsync'); 7 | 8 | describe('filters', function () { 9 | var command; 10 | 11 | beforeEach(function () { 12 | command = Rsync.build({ 13 | 'source': 'SOURCE', 14 | 'destination': 'DESTINATION' 15 | }); 16 | }); 17 | 18 | describe('#patterns', function () { 19 | 20 | it('should interpret the first character', function () { 21 | command.patterns(['-.git', '+/tests/*.test.js']); 22 | assert.lengthOf(command._patterns, 2); 23 | }); 24 | 25 | it('should be able to be set as an Object', function () { 26 | command.patterns([ 27 | { 'action': '+', 'pattern': '.git' }, 28 | { 'action': '-', 'pattern': '/tests/*.test' } 29 | ]); 30 | assert.lengthOf(command._patterns, 2); 31 | }); 32 | 33 | it('should throw an error for invalid patterns', function () { 34 | assert.throw(function () { 35 | command.patterns(['*invalid']); 36 | }, /^invalid pattern:/i); 37 | }); 38 | 39 | it('should add patterns to output in order added', function () { 40 | command.patterns([ 41 | { 'action': '-', 'pattern': '.git' }, 42 | { 'action': '+', 'pattern': '/tests/*.test.js' }, 43 | '-build/*' 44 | ]); 45 | assertOutput(command, 'rsync --exclude=.git --include=/tests/*.test.js --exclude=build/* SOURCE DESTINATION'); 46 | }); 47 | 48 | }); 49 | 50 | describe('#exclude', function () { 51 | 52 | it('should accept patterns as arguments', function () { 53 | command.exclude('.git', '.out'); 54 | assert.lengthOf(command._patterns, 2); 55 | }); 56 | 57 | it ('should accept patterns as an Array', function () { 58 | command.exclude(['.build', 'docs']); 59 | assert.lengthOf(command._patterns, 2); 60 | }); 61 | 62 | it('should add patterns to output in order added', function () { 63 | command.exclude('.git', 'docs', '/tests/*.test.js'); 64 | assertOutput(command, 'rsync --exclude=.git --exclude=docs --exclude=/tests/*.test.js SOURCE DESTINATION'); 65 | }); 66 | 67 | it('should escape filenames', function () { 68 | command.exclude('with space', 'tests/* test.js'); 69 | assertOutput(command, 'rsync --exclude=with\\ space --exclude=tests/*\\ test.js SOURCE DESTINATION'); 70 | }); 71 | 72 | }); 73 | 74 | describe('#include', function () { 75 | 76 | it('should accept patterns as arguments', function () { 77 | command.include('.git', '.out'); 78 | assert.lengthOf(command._patterns, 2); 79 | }); 80 | 81 | it ('should accept patterns as an Array', function () { 82 | command.include(['.build', 'docs']); 83 | assert.lengthOf(command._patterns, 2); 84 | }); 85 | 86 | it('should add patterns to output in order added', function () { 87 | command.include('LICENSE', 'README.md', 'rsync.js'); 88 | assertOutput(command, 'rsync --include=LICENSE --include=README.md --include=rsync.js SOURCE DESTINATION'); 89 | }); 90 | 91 | it('should escape filenames', function () { 92 | command.include('LICENSE FILE', '/tests/* test.js'); 93 | assertOutput(command, 'rsync --include=LICENSE\\ FILE --include=/tests/*\\ test.js SOURCE DESTINATION'); 94 | }); 95 | 96 | }); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /tests/helpers/output.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* jshint strict: true */ 3 | var assert = require('chai').assert; 4 | 5 | var output = module.exports; 6 | 7 | /** 8 | * Assert the exact output of a command against an expectation. 9 | * 10 | * @param {Rsync|Function} command 11 | * @param {String|Function} expectation 12 | * @param {String} message 13 | */ 14 | output.assertOutput = function (command, expectation, message) { 15 | command = isFunction(command) ? command() : command; 16 | expectation = isFunction(expectation) ? expectation() : expectation; 17 | message = message || 'expected |' + command.command() + '| to equal |' + expectation + '|'; 18 | 19 | return assert.strictEqual(command.command(), expectation, message); 20 | }; 21 | output.assertExactOutput = output.assertOutput; 22 | 23 | /** 24 | * Assert the exact output of a command against an expectation. 25 | * 26 | * @param {Rsync|Function} command 27 | * @param {RegExp|Function} expectation 28 | * @param {String} message 29 | */ 30 | output.assertOutputPattern = function (command, expectation, message) { 31 | command = isFunction(command) ? command() : command; 32 | expectation = isFunction(expectation) ? expectation() : expectation; 33 | message = message || 'expected |' + command.command() + '| to match |' + String(expectation) + '|'; 34 | 35 | return assert(expectation.test(command.command()), message); 36 | }; 37 | 38 | 39 | function isFunction(input) { 40 | return typeof input === 'function'; 41 | } 42 | -------------------------------------------------------------------------------- /tests/input.test.js: -------------------------------------------------------------------------------- 1 | /* global describe,it */ 2 | "use strict"; 3 | 4 | var assertOutputPattern = require('./helpers/output').assertOutputPattern; 5 | var Rsync = require('../rsync'); 6 | 7 | describe('input', function () { 8 | 9 | //# sources 10 | describe('#source', function () { 11 | var rsync; 12 | 13 | it('should be able to be set as a single String', function () { 14 | rsync = Rsync.build({ 15 | source: 'afile.txt', 16 | destination: 'some_location.txt' 17 | }); 18 | assertOutputPattern(rsync, /\safile.txt\s/g); 19 | }); 20 | 21 | it('should be able to be set as an Array', function () { 22 | rsync = Rsync.build({ 23 | source: [ 'afile.txt', 'bfile.txt' ], 24 | destination: 'some_location.txt' 25 | }); 26 | assertOutputPattern(rsync, /\safile.txt bfile.txt\s/g); 27 | }); 28 | 29 | it('should not escape regular filenames', function () { 30 | rsync = Rsync.build({ 31 | source: [ 'some_file.txt' ], 32 | destination: 'wherever_we_want.txt' 33 | }); 34 | assertOutputPattern(rsync, /\ssome_file.txt\s/g); 35 | }); 36 | 37 | it('should escape spaced filenames', function () { 38 | rsync = Rsync.build({ 39 | source: [ 'some file.txt' ], 40 | destination: 'wherever_we_want.txt' 41 | }); 42 | assertOutputPattern(rsync, /\ssome\\ file.txt\s/g); 43 | }); 44 | 45 | it('should have quote characters escaped',function () { 46 | rsync = Rsync.build({ 47 | source: [ 'a_quoted\'filename\".txt' ], 48 | destination: 'themoon' 49 | }); 50 | assertOutputPattern(rsync, / a_quoted\\'filename\\".txt /); 51 | }); 52 | 53 | it('should have parentheses escaped', function () { 54 | rsync = Rsync.build({ 55 | source: [ 'a (file) with parantheses.txt' ], 56 | destination: 'themoon' 57 | }); 58 | assertOutputPattern(rsync, /a\\ \\\(file\\\)\\ with\\ parantheses.txt/); 59 | }); 60 | 61 | it('should allow mixed filenames', function () { 62 | rsync = Rsync.build({ 63 | source: [ 64 | 'example file.txt', 'manual.pdf', '\'special_case 1\'.rtf' 65 | ], 66 | destination: 'somewhere_else/' 67 | }); 68 | assertOutputPattern(rsync, / example\\ file.txt manual.pdf \\'special_case\\ 1\\'.rtf/); 69 | }); 70 | 71 | }); 72 | 73 | //# destination 74 | describe('#destination', function () { 75 | var rsync; 76 | 77 | it('should not have regular filenames escaped', function () { 78 | rsync = Rsync.build({ 79 | source: [ 'file1.txt' ], 80 | destination: 'the_destination/' 81 | }); 82 | assertOutputPattern(rsync, /the_destination\/$/); 83 | }); 84 | 85 | it('should have spaced filenames escaped', function () { 86 | rsync = Rsync.build({ 87 | source: [ 'file2.txt' ], 88 | destination: 'whereever we want.txt' 89 | }); 90 | assertOutputPattern(rsync, /whereever\\ we\\ want.txt$/); 91 | }); 92 | 93 | it('should have quote characters escaped', function () { 94 | rsync = Rsync.build({ 95 | source: [ 'space.txt' ], 96 | destination: '\'to infinity and beyond\"/' 97 | }); 98 | assertOutputPattern(rsync, /\\'to\\ infinity\\ and\\ beyond\\"\/$/); 99 | }); 100 | it('should have dollar sign characters escaped', function () { 101 | rsync = Rsync.build({ 102 | source: [ 'file3.txt' ], 103 | destination: '$some_destination/' 104 | }); 105 | assertOutputPattern(rsync, /\$some_destination\/$/); 106 | }); 107 | 108 | 109 | }); 110 | 111 | }); 112 | -------------------------------------------------------------------------------- /tests/inputwin32.test.js: -------------------------------------------------------------------------------- 1 | /* global describe,it */ 2 | "use strict"; 3 | 4 | var assertOutputPattern = require('./helpers/output').assertOutputPattern; 5 | var Rsync = require('../rsync'); 6 | 7 | describe('inputwin32', function () { 8 | before(function(){ 9 | this.originalPlatform = process.platform; 10 | Object.defineProperty(process, 'platform', { 11 | value: 'win32' 12 | }); 13 | }); 14 | 15 | //# sources under windows 16 | describe('#sourcewin32', function () { 17 | var rsync; 18 | 19 | it('should convert windows path under windows',function () { 20 | rsync = Rsync.build({ 21 | source: [ 'C:\\home\\username\\develop\\readme.txt' ], 22 | destination: 'themoon' 23 | }); 24 | assertOutputPattern(rsync, / \/home\/username\/develop\/readme\.txt /); 25 | }); 26 | }); 27 | 28 | //# destination under win32 29 | describe('#destinationwin32', function () { 30 | var rsync; 31 | 32 | it('should convert widows path for destination', function () { 33 | rsync = Rsync.build({ 34 | source: [ 'reame.txt' ], 35 | destination: 'C:\\home\\username\\develop\\' 36 | }); 37 | assertOutputPattern(rsync, /\/home\/username\/develop\//); 38 | }); 39 | 40 | }); 41 | 42 | after(function(){ 43 | Object.defineProperty(process, 'platform', { 44 | value: this.originalPlatform 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/options.test.js: -------------------------------------------------------------------------------- 1 | /* global describe,it,beforeEach,xdescribe,xit */ 2 | 'use strict'; 3 | var assert = require('chai').assert; 4 | var Rsync = require('../rsync'); 5 | 6 | describe('options', function () { 7 | var command; 8 | beforeEach(function () { 9 | command = new Rsync(); 10 | }); 11 | 12 | //# set /////////////////////////////////////////////////////////////////////////////////////////// 13 | describe('#set', function () { 14 | it('should set a an option with a value', function () { 15 | command.set('rsh', 'ssh'); 16 | assert.propertyVal(command._options, 'rsh', 'ssh'); 17 | }); 18 | 19 | it('should enable an option without a value', function () { 20 | command.set('dir'); 21 | assert.property(command._options, 'dir'); 22 | }); 23 | 24 | it('should strip leading dashes', function () { 25 | command.set('--progress'); 26 | command.set('--rsh', 'ssh'); 27 | assert.property(command._options, 'progress'); 28 | assert.propertyVal(command._options, 'rsh', 'ssh'); 29 | }); 30 | 31 | }); 32 | 33 | //# unset ///////////////////////////////////////////////////////////////////////////////////////// 34 | describe('#unset', function () { 35 | 36 | it('should unset an option that has a value', function () { 37 | command.set('rsh', 'ssh'); 38 | assert.propertyVal(command._options, 'rsh', 'ssh'); 39 | 40 | command.unset('rsh'); 41 | assert.lengthOf(Object.keys(command._options), 0); 42 | assert.notProperty(command._options, 'rsh'); 43 | }); 44 | 45 | it('should unset an enabled options', function () { 46 | command.set('progress'); 47 | assert.property(command._options, 'progress'); 48 | 49 | command.unset('progress'); 50 | assert.notProperty(command._options, 'progress'); 51 | }); 52 | 53 | it('should unset an option that was not set', function () { 54 | assert.notProperty(command._options, 'dirs'); 55 | command.unset('dirs'); 56 | assert.notProperty(command._options, 'dirs'); 57 | }); 58 | 59 | }); 60 | 61 | //# isSet ///////////////////////////////////////////////////////////////////////////////////////// 62 | describe('#isSet', function () { 63 | 64 | it('should return if an option is set', function () { 65 | command.set('inplace'); 66 | assert.isTrue(command.isSet('inplace')); 67 | 68 | command.set('b'); 69 | assert.isTrue(command.isSet('b')); 70 | 71 | command.set('max-size', '1009'); 72 | assert.isTrue(command.isSet('max-size')); 73 | }); 74 | 75 | it('should strip leading dashes from option name', function () { 76 | command.set('inplace'); 77 | assert.isTrue(command.isSet('--inplace')); 78 | 79 | command.set('b'); 80 | assert.isTrue(command.isSet('-b')); 81 | 82 | command.set('max-size', '1009'); 83 | assert.isTrue(command.isSet('--max-size')); 84 | }); 85 | 86 | }); 87 | 88 | //# option //////////////////////////////////////////////////////////////////////////////////////// 89 | describe('#option', function () { 90 | 91 | it('should return the value for an option', function () { 92 | command.set('max-size', '1009'); 93 | assert.equal(command.option('max-size'), '1009'); 94 | }); 95 | 96 | 97 | it('should return null for a valueless options', function () { 98 | command.set('progress'); 99 | assert.isNull(command.option('progress')); 100 | }); 101 | 102 | it('should return undefined for an option that is not set', function () { 103 | assert.isUndefined(command.option('random')); 104 | }); 105 | 106 | it('should strip leading dashes from option names', function () { 107 | command.set('progress'); 108 | assert.isNull(command.option('--progress')); 109 | 110 | command.set('g'); 111 | assert.isNull(command.option('-g')); 112 | 113 | command.set('max-size', '2009'); 114 | assert.equal(command.option('--max-size'), '2009'); 115 | 116 | assert.isUndefined(command.option('--random')); 117 | }); 118 | 119 | }); 120 | 121 | //# flags ///////////////////////////////////////////////////////////////////////////////////////// 122 | describe('#flags', function () { 123 | 124 | it('it should set multiple flags from a String', function () { 125 | command.flags('avz'); 126 | assert.sameMembers(Object.keys(command._options), ['a', 'v', 'z']); 127 | }); 128 | 129 | it('should set multiple flags from arguments', function () { 130 | command.flags('v', 'z', 'a'); 131 | assert.sameMembers(Object.keys(command._options), ['a', 'v', 'z']); 132 | }); 133 | 134 | it('should set multiple flags from an array', function () { 135 | command.flags(['z', 'a', 'v']); 136 | assert.sameMembers(Object.keys(command._options), ['a', 'v', 'z']); 137 | }); 138 | 139 | it('should unset multiple flags from a string', function () { 140 | command.flags('avz'); 141 | assert.sameMembers(Object.keys(command._options), ['a', 'v', 'z']); 142 | 143 | command.flags('az', false); 144 | assert.sameMembers(Object.keys(command._options), ['v']); 145 | }); 146 | 147 | it('should set multiple flags from arguments', function () { 148 | command.flags('avz'); 149 | assert.sameMembers(Object.keys(command._options), ['a', 'v', 'z']); 150 | 151 | command.flags('z', 'v', false); 152 | assert.sameMembers(Object.keys(command._options), ['a']); 153 | }); 154 | 155 | it('should set multiple flags from an array', function () { 156 | command.flags('avz'); 157 | assert.sameMembers(Object.keys(command._options), ['a', 'v', 'z']); 158 | 159 | command.flags(['a', 'v'], false); 160 | assert.sameMembers(Object.keys(command._options), ['z']); 161 | }); 162 | 163 | it('should set/unset flags from an Object', function () { 164 | command.flags('flag'); 165 | assert.sameMembers(Object.keys(command._options), ['f', 'l', 'a', 'g']); 166 | 167 | command.flags({ 168 | 'l': false, 169 | 's': false, 170 | 'u': true, 171 | 'w': true, 172 | 'b': true 173 | }); 174 | assert.sameMembers(Object.keys(command._options), ['f', 'u', 'w', 'g', 'a', 'b']); 175 | 176 | }); 177 | 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /tests/shorthands.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* global describe, it */ 3 | var assert = require('chai').assert; 4 | var Rsync = require('../rsync'); 5 | var assertOutput = require('./helpers/output').assertOutput; 6 | var assertOutputPattern = require('./helpers/output').assertOutputPattern; 7 | 8 | describe('shorthands', function () { 9 | var command, output; 10 | 11 | beforeEach(function() { 12 | command = Rsync.build({ 13 | 'source': 'SOURCE', 14 | 'destination': 'DESTINATION' 15 | }); 16 | output = 'rsync SOURCE DESTINATION'; 17 | }); 18 | 19 | //# shell ///////////////////////////////////////////////////////////////////////////////////////// 20 | describe('#shell', function () { 21 | var rsync; 22 | it('should add rsh option', function () { 23 | rsync = Rsync.build({ 24 | 'source': 'source', 25 | 'destination': 'destination', 26 | 'shell': 'ssh' 27 | }); 28 | assertOutput(rsync, 'rsync --rsh=ssh source destination'); 29 | }); 30 | 31 | it('should escape options with spaces', function () { 32 | rsync = Rsync.build({ 33 | 'source': 'source', 34 | 'destination': 'destination', 35 | 'shell': 'ssh -i /home/user/.ssh/rsync.key' 36 | }); 37 | assertOutput(rsync, 'rsync --rsh="ssh -i /home/user/.ssh/rsync.key" source destination'); 38 | }); 39 | }); 40 | 41 | //# chmod ///////////////////////////////////////////////////////////////////////////////////////// 42 | describe('#chmod', function () { 43 | var rsync; 44 | 45 | it('should allow a simple value through build', function () { 46 | rsync = Rsync.build({ 47 | 'source': 'source', 48 | 'destination': 'destination', 49 | 'chmod': 'ug=rwx' 50 | }); 51 | assertOutputPattern(rsync, /chmod=ug=rwx/i); 52 | }); 53 | 54 | it('should allow multiple values through build', function () { 55 | rsync = Rsync.build({ 56 | 'source': 'source', 57 | 'destination': 'destination', 58 | 'chmod': [ 'og=uwx', 'rx=ogw' ] 59 | }); 60 | assertOutputPattern(rsync, /chmod=og=uwx --chmod=rx=ogw/); 61 | }); 62 | 63 | it('should allow multiple values through setter', function () { 64 | rsync = Rsync.build({ 65 | 'source': 'source', 66 | 'destination': 'destination' 67 | }); 68 | rsync.chmod('o=rx'); 69 | rsync.chmod('ug=rwx'); 70 | assertOutputPattern(rsync, /--chmod=o=rx --chmod=ug=rwx/); 71 | }); 72 | 73 | it('should return all the chmod values', function () { 74 | var inputValues = [ 'og=uwx', 'rx=ogw' ]; 75 | rsync = Rsync.build({ 76 | 'source': 'source', 77 | 'destination': 'destination', 78 | 'chmod': inputValues 79 | }); 80 | 81 | var values = rsync.chmod(); 82 | assert.deepEqual(values, inputValues); 83 | }); 84 | }); 85 | 86 | //# delete //////////////////////////////////////////////////////////////////////////////////////// 87 | describe('#delete', function () { 88 | var testSet = function () { 89 | command.delete(); 90 | assertOutputPattern(command, /^rsync --delete/); 91 | return command; 92 | }; 93 | it('should add the delete option', testSet); 94 | it('should be able to be unset', function () { 95 | testSet().delete(false); 96 | assertOutput(command, output); 97 | }); 98 | }); 99 | 100 | //# progress ////////////////////////////////////////////////////////////////////////////////////// 101 | describe('#progress', function () { 102 | var testSet = function () { 103 | command.progress(); 104 | assertOutputPattern(command, /^rsync --progress/); 105 | return command; 106 | }; 107 | it('should add the progress option', testSet); 108 | it('should be able to be unset', function () { 109 | testSet().progress(false); 110 | assertOutput(command, output); 111 | }); 112 | }); 113 | 114 | //# archive /////////////////////////////////////////////////////////////////////////////////////// 115 | describe('#archive', function () { 116 | var testSet = function () { 117 | command.archive(); 118 | assertOutputPattern(command, /^rsync -a/); 119 | return command; 120 | }; 121 | it('should add the archive flag', testSet); 122 | it('should be able to be unset', function () { 123 | testSet().archive(false); 124 | assertOutput(command, output); 125 | }); 126 | }); 127 | 128 | //# compress ////////////////////////////////////////////////////////////////////////////////////// 129 | describe('#compress', function () { 130 | var testSet = function () { 131 | command.compress(); 132 | assertOutputPattern(command, /^rsync -z/); 133 | return command; 134 | }; 135 | it('should add the compress flag', testSet); 136 | it('should be able to be unset', function () { 137 | command = testSet().compress(false); 138 | assertOutput(command, output); 139 | }); 140 | }); 141 | 142 | //# recursive ///////////////////////////////////////////////////////////////////////////////////// 143 | describe('#recursive', function () { 144 | var testSet = function () { 145 | command.recursive(); 146 | assertOutputPattern(command, /^rsync -r/); 147 | return command; 148 | }; 149 | it('should add the recursive flag', testSet); 150 | it('should be able to be unset', function () { 151 | command = testSet().recursive(false); 152 | assertOutput(command, output); 153 | }); 154 | }); 155 | 156 | //# update //////////////////////////////////////////////////////////////////////////////////////// 157 | describe('#update', function () { 158 | var testSet = function () { 159 | command.update(); 160 | assertOutputPattern(command, /^rsync -u/); 161 | return command; 162 | }; 163 | it('should add the update flag', testSet); 164 | it('should be able to be unset', function () { 165 | command = testSet().update(false); 166 | assertOutput(command, output); 167 | }); 168 | }); 169 | 170 | //# quiet ///////////////////////////////////////////////////////////////////////////////////////// 171 | describe('#quiet', function () { 172 | var testSet = function () { 173 | command.quiet(); 174 | assertOutputPattern(command, /^rsync -q/); 175 | return command; 176 | }; 177 | it('should add the quiet flag', testSet); 178 | it('should be able to be unset', function () { 179 | command = testSet().quiet(false); 180 | assertOutput(command, output); 181 | }); 182 | }); 183 | 184 | //# dirs ////////////////////////////////////////////////////////////////////////////////////////// 185 | describe('#dirs', function () { 186 | var testSet = function () { 187 | command.dirs(); 188 | assertOutputPattern(command, /^rsync -d/); 189 | return command; 190 | }; 191 | it('should add the dirs flag', testSet); 192 | it('should be able to be unset', function () { 193 | command = testSet().dirs(false); 194 | assertOutput(command, output); 195 | }); 196 | }); 197 | 198 | //# links ///////////////////////////////////////////////////////////////////////////////////////// 199 | describe('#links', function () { 200 | var testSet = function () { 201 | command.links(); 202 | assertOutputPattern(command, /^rsync -l/); 203 | return command; 204 | }; 205 | it('should add the links flag', testSet); 206 | it('should be able to be unset', function () { 207 | command = testSet().links(false); 208 | assertOutput(command, output); 209 | }); 210 | }); 211 | 212 | //# dry /////////////////////////////////////////////////////////////////////////////////////////// 213 | describe('#dry', function () { 214 | var testSet = function () { 215 | command.dry(); 216 | assertOutputPattern(command, /rsync -n/); 217 | return command; 218 | }; 219 | it('should add the dry flag', testSet); 220 | it('should be able to be unset', function () { 221 | command = testSet().dry(false); 222 | assertOutput(command, output); 223 | }); 224 | }); 225 | 226 | //# hardLinks///////////////////////////////////////////////////////////////////////////////////// 227 | describe('#hardLinks', function () { 228 | 229 | it('should add the hard links flag', function () { 230 | command.hardLinks(); 231 | assertOutputPattern(command, /rsync -H/); 232 | }); 233 | 234 | it('should unset the hard links flag', function () { 235 | command.hardLinks(); 236 | assertOutputPattern(command, /rsync -H/); 237 | command.hardLinks(false); 238 | assertOutput(command, output); 239 | }); 240 | 241 | }); 242 | 243 | //# perms //////////////////////////////////////////////////////////////////////////////////////// 244 | describe('#perms', function () { 245 | 246 | it('should add the perms flag', function () { 247 | command.perms(); 248 | assertOutputPattern(command, /rsync -p/); 249 | }); 250 | 251 | it('should unset the perms flag', function () { 252 | command.perms(); 253 | assertOutputPattern(command, /rsync -p/); 254 | command.perms(false); 255 | assertOutput(command, output); 256 | }); 257 | 258 | }); 259 | 260 | describe('#executability', function () { 261 | 262 | it('should add the executability flag', function () { 263 | command.executability(); 264 | assertOutputPattern(command, /rsync -E/); 265 | }); 266 | 267 | it('should unset the executability flag', function () { 268 | command.executability(); 269 | assertOutputPattern(command, /rsync -E/); 270 | command.executability(false); 271 | assertOutput(command, output); 272 | }); 273 | 274 | }); 275 | 276 | describe('#owner', function () { 277 | 278 | it('should add the owner flag', function () { 279 | command.owner(); 280 | assertOutputPattern(command, /rsync -o/); 281 | }); 282 | 283 | it('should unset the owner flag', function () { 284 | command.owner(); 285 | assertOutputPattern(command, /rsync -o/); 286 | command.owner(false); 287 | assertOutput(command, output); 288 | }); 289 | 290 | }); 291 | 292 | describe('#group', function () { 293 | 294 | it('should add the group flag', function () { 295 | command.group(); 296 | assertOutputPattern(command, /rsync -g/); 297 | }); 298 | 299 | it('should unset the group flag', function () { 300 | command.group(); 301 | assertOutputPattern(command, /rsync -g/); 302 | command.group(false); 303 | assertOutput(command, output); 304 | }); 305 | }); 306 | 307 | describe('#acls', function () { 308 | 309 | it('should set the acls flag', function () { 310 | command.acls(); 311 | assertOutputPattern(command, /rsync -A/); 312 | }); 313 | 314 | it('should unset the acls flag', function () { 315 | command.acls(); 316 | assertOutputPattern(command, /rsync -A/); 317 | command.acls(false); 318 | assertOutput(command, output); 319 | }); 320 | 321 | }); 322 | 323 | describe('#xattrs', function () { 324 | 325 | it('should set the xattrs flag', function () { 326 | command.xattrs(); 327 | assertOutputPattern(command, /rsync -X/); 328 | }); 329 | 330 | it('should unset the xattrs flag', function () { 331 | command.xattrs(); 332 | assertOutputPattern(command, /rsync -X/); 333 | command.xattrs(false); 334 | assertOutput(command, output); 335 | }); 336 | 337 | }); 338 | 339 | describe('#devices', function () { 340 | 341 | it('should set the the devices option', function () { 342 | command.devices(); 343 | assertOutputPattern(command, /rsync --devices/); 344 | }); 345 | 346 | it('should unset the devices option', function () { 347 | command.devices(); 348 | assertOutputPattern(command, /rsync --devices/); 349 | command.devices(false); 350 | assertOutput(command, output); 351 | }); 352 | 353 | }); 354 | 355 | describe('#specials', function () { 356 | 357 | it('should set the the specials option', function () { 358 | command.specials(); 359 | assertOutputPattern(command, /rsync --specials/); 360 | }); 361 | 362 | it('should unset the specials option', function () { 363 | command.specials(); 364 | assertOutputPattern(command, /rsync --specials/); 365 | command.specials(false); 366 | assertOutput(command, output); 367 | }); 368 | 369 | }); 370 | 371 | describe('#times', function () { 372 | 373 | it('should set the the times option', function () { 374 | command.times(); 375 | assertOutputPattern(command, /rsync -t/); 376 | }); 377 | 378 | it('should unset the times option', function () { 379 | command.times(); 380 | assertOutputPattern(command, /rsync -t/); 381 | command.times(false); 382 | assertOutput(command, output); 383 | }); 384 | 385 | }); 386 | 387 | }); 388 | -------------------------------------------------------------------------------- /tests/z_output.test.js: -------------------------------------------------------------------------------- 1 | /*global describe,it*/ 2 | "use strict"; 3 | var Rsync = require('../rsync'); 4 | var assertOutput = require('./helpers/output').assertOutput; 5 | 6 | /** 7 | * Some general and weird test cases for command output. 8 | * 9 | * These tests are meant as a general safeguard to complement 10 | * unit tests. 11 | */ 12 | var testCases = [ 13 | { expect: 'rsync -avz --exclude=no-go.txt --exclude=with\\ space --exclude=.git --exclude=*.tiff path_a/ path_b', 14 | build: function() { 15 | return new Rsync() 16 | .flags('avz') 17 | .source('path_a/') 18 | .exclude('no-go.txt') 19 | .exclude('with space') 20 | .exclude('.git') 21 | .exclude('*.tiff') 22 | .destination('path_b'); 23 | }}, 24 | { expect: 'rsync -rav -f "- .git" test-dir/ test-dir-copy', 25 | build: function() { 26 | return new Rsync() 27 | .flags('rav') 28 | .set('f', '- .git') 29 | .source('test-dir/') 30 | .destination('test-dir-copy'); 31 | }} 32 | ]; 33 | 34 | describe('output tests', function () { 35 | 36 | testCases.forEach(function buildTestCase(testCase, index) { 37 | var message = 'passes case ' + (index + 1); 38 | it(message, function() { 39 | assertOutput(testCase.build(), testCase.expect); 40 | }); 41 | }); 42 | 43 | }); 44 | --------------------------------------------------------------------------------