├── .gitignore ├── .npmignore ├── README.md ├── bin ├── syncrow ├── syncrow-init └── syncrow-run ├── docs └── prezentacja.md ├── housekeeping ├── benchmark-short.sh ├── benchmarks.sh └── results.json ├── index.js ├── package.json ├── src ├── benchmarks │ ├── big_files_benchmark.ts │ ├── save_results.ts │ └── small_files_benchmark.ts ├── cli │ ├── program.ts │ ├── syncrow-init.ts │ ├── syncrow-run.ts │ └── syncrow.ts ├── connection │ ├── authorisation_helper.ts │ ├── callback_helper.ts │ ├── connection_helper.ts │ ├── constant_connector.ts │ ├── constant_server.ts │ ├── dynamic_connector.ts │ ├── dynamic_server.ts │ ├── event_messenger.ts │ └── parse_helper.ts ├── core │ └── engine.ts ├── facade │ ├── client.ts │ └── server.ts ├── fs_helpers │ ├── file_container.ts │ ├── file_meta_queue.ts │ ├── path_helper.ts │ └── read_tree.ts ├── references.ts ├── sync │ ├── generic_commands_action.ts │ ├── no_action.ts │ ├── pull_action.ts │ ├── push_action.ts │ └── sync_actions.ts ├── test │ ├── cli │ │ └── cli_test.ts │ ├── connection │ │ ├── authorisation_helper_test.ts │ │ ├── constant_connector_test.ts │ │ ├── constant_server_test.ts │ │ ├── dynamic_connector_test.ts │ │ └── dynamic_server_test.ts │ ├── engine_test.ts │ ├── fs │ │ ├── file_container_test.ts │ │ ├── path_helper_test.ts │ │ └── read_tree_test.ts │ ├── sync │ │ ├── pull_action_test.ts │ │ └── push_action_test.ts │ └── utils │ │ └── event_counter_test.ts ├── transport │ ├── transfer_actions.ts │ ├── transfer_helper.ts │ └── transfer_queue.ts └── utils │ ├── event_counter.ts │ ├── fs_test_utils.ts │ ├── interfaces.ts │ └── logger.ts ├── tsconfig.json ├── typings.json └── typings ├── globals ├── async │ ├── index.d.ts │ └── typings.json ├── chai │ ├── index.d.ts │ └── typings.json ├── chalk │ ├── index.d.ts │ └── typings.json ├── chokidar │ ├── index.d.ts │ └── typings.json ├── commander │ ├── index.d.ts │ └── typings.json ├── debug │ ├── index.d.ts │ └── typings.json ├── es6-collections │ ├── index.d.ts │ └── typings.json ├── es6-promise │ ├── index.d.ts │ └── typings.json ├── git-rev-sync │ └── index.d.ts ├── inquirer │ ├── index.d.ts │ └── typings.json ├── lodash │ ├── index.d.ts │ └── typings.json ├── mkdirp │ ├── index.d.ts │ └── typings.json ├── mocha │ ├── index.d.ts │ └── typings.json ├── node │ ├── index.d.ts │ └── typings.json ├── rimraf │ ├── index.d.ts │ └── typings.json ├── sinon-chai │ ├── index.d.ts │ └── typings.json └── sinon │ ├── index.d.ts │ └── typings.json └── index.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | .syncrow.json 7 | 8 | src/*.js 9 | src/*.js.map 10 | 11 | src/**/*.js 12 | src/**/*.js.map 13 | .idea 14 | 15 | 16 | testdir 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build 37 | 38 | # Dependency directories 39 | node_modules 40 | jspm_packages 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Created by .filter support plugin (hsz.mobi) 49 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | node_modules 4 | testdir 5 | docs 6 | docker 7 | typings 8 | src 9 | .syncrow.json 10 | tsconfig.json 11 | typings.json 12 | housekeeping 13 | *.tgz 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Syncrow 2 | 3 | Real time directory synchronization using sockets. It can be used as a command line tool, and as a library in other programs. 4 | 5 | **Currently in Beta** 6 | 7 | ## Installation 8 | 9 | `$ npm install -g syncrow` 10 | 11 | ## Configuration 12 | 13 | You need to configure syncrow in directory that you want to synchronize. 14 | 15 | `$ syncrow init` 16 | 17 | This command will run an interactive setup, 18 | similar to `npm init`, it will ask questions and save your answers. 19 | 20 | The result will be a `.syncrow.json` with your setup. 21 | 22 | ## Running 23 | 24 | `$ syncrow run` or just `$ syncrow` 25 | 26 | This command will look for `.syncrow.json` file in current directory, 27 | if the file exists it will start a *syncrow* process using it as configuration. 28 | 29 | ## Connecting two machines 30 | 31 | First install syncrow globally on both machines that you want to connect. 32 | Setup one machine to listen for incoming connections: 33 | *(Your password will be stored as a hash)* 34 | 35 | ``` 36 | user@server $ syncrow init 37 | ? Do you want to listen for connection? Yes 38 | ? On which local port would you like to listen 2510 39 | ? What is your external IP/hostname? 192.168.0.6 40 | ? Please enter comma-separated anymatch patterns for files that should be ignored .git,node_modules 41 | ? What synchronization strategy for every new connection would you like to choose? No Action - existing files will be ignored, only new changes will be synced 42 | ? Please enter password for obtaining connection my_horse_is_amazing 43 | ? Would you like to setup advanced options? No 44 | ``` 45 | 46 | Then configure *syncrow* on second machine that will connect: 47 | 48 | ``` 49 | user@laptop $ syncrow init 50 | ? Do you want to listen for connection? No 51 | ? Connection: remote host 192.168.0.6 52 | ? Connection: remote port 2510 53 | ? Please enter comma-separated anymatch patterns for files that should be ignored .git,.idea,node_modules 54 | ? What synchronization strategy for every new connection would you like to choose? Push - when other party connects all remote files will be overwritten by those local 55 | ? Please enter password for obtaining connection my_horse_is_amazing 56 | ? Would you like to setup advanced options? No 57 | ``` 58 | 59 | Once configured - start *syncrow* on both machines: 60 | 61 | `user@server $ syncrow run` 62 | 63 | and 64 | 65 | `user@laptop $ syncrow run` 66 | 67 | After a connection is obtained *syncrow* will sync existing files. 68 | This will run both *syncrow* instances as a foreground processes. 69 | It is possible to connect multiple connecting *syncrow* instances to single *syncrow* listener 70 | 71 | ## Using as a library 72 | It is possible to use *syncrow* as a part of node program. 73 | 74 | ### Class: Server 75 | Listens for incoming connections. 76 | 77 | #### new Server(params) 78 | ```js 79 | const syncrow = require('syncrow'); 80 | 81 | const server = new syncrow.Server({path: './path/to_watch', localPort: 2510, externalHost: '192.168.0.2'}); 82 | 83 | ``` 84 | params: 85 | * `path` **String** path to watch 86 | * `localPort` **Number** port to listen on 87 | * `externalHost` **String** external domain/IP 88 | * `[initalToken]` **String** optional token that will be used for authentication 89 | * `[watch]` **Boolean** optional, defaults to `true`, if set to `false` server will not watch local files 90 | 91 | 92 | #### server.engine 93 | An instance of `syncrow.Engine` 94 | #### server.start(callback) 95 | starts the server watching the FS and listening for connections. 96 | #### server.shutdown() 97 | Completely stops the server. 98 | ### event: connection 99 | Emitted when remote party connects to the server 100 | 101 | ### Class: Client 102 | Connects to remote server. 103 | #### new Client(params) 104 | ```js 105 | const syncrow = require('syncrow'); 106 | 107 | const client = new syncrow.Client({path: './path/to_watch', remotePort: 2510, remoteHost: '127.0.0.1'}); 108 | ``` 109 | params: 110 | * `path` **String** path to watch 111 | * `remotePort` **Number** port for connection 112 | * `remoteHost` **String** host for connection 113 | * `[initalToken]` **String** optional token that will be used for authentication 114 | * `[watch]` **Boolean** optional, defaults to `true`, if set to `false` server will not watch local files 115 | 116 | 117 | #### client.engine 118 | An instance of `syncrow.Engine` 119 | #### client.start(callback) 120 | starts the watching the path and connects to remote server. 121 | #### client.shutdown() 122 | Disconnects and stops the client. 123 | 124 | ### Class: Engine 125 | Watches local file system and handles messages from remote parties. 126 | It should not be created directly. 127 | 128 | ```js 129 | const server = new syncrow.Server({path: './path/to_watch', localPort: 2510, externalHost: '192.168.0.2'}); 130 | 131 | server.start((err)=>{ 132 | if(err) return console.error(err); 133 | 134 | server.engine.on('newFile', (file)=>console.log(`remote created a new file: ${file}`)); 135 | 136 | server.engine.on('changedFile', (file)=>console.log(`remote changed file: ${file}`)); 137 | 138 | server.engine.on('deletedPath', (path)=>console.log(`remote deleted path (file or directory): ${path}`)); 139 | }); 140 | ``` 141 | 142 | #### event: newFile 143 | emitted when file changed by remote has been downloaded. Params: 144 | * `filePath` **String** 145 | 146 | #### event: changedFile 147 | emitted when file changed by remote has been downloaded. Params: 148 | * `filePath` **String** path of the file that changed 149 | 150 | #### event: deletedPath 151 | emitted when path (file or directory) has been deleted locally. Params: 152 | * `filePath` **String** path of the file/directory deleted 153 | 154 | #### event: newDirectory 155 | emitted when directory created by remote has been created locally. Params: 156 | * `dirPath` **String** path of the directory created 157 | 158 | #### event: error 159 | emitted on error. Params: 160 | * `error` **Error** 161 | 162 | #### event: synced 163 | emitted when synchronization with remote has finished 164 | 165 | ## Major TODOS: 166 | 167 | * Add interval synchronization 168 | * Separate into several repositories 169 | * Integrate with Atom 170 | 171 | ## Licence 172 | MIT 173 | -------------------------------------------------------------------------------- /bin/syncrow: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../build/cli/syncrow.js'); -------------------------------------------------------------------------------- /bin/syncrow-init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../build/cli/syncrow-init.js'); -------------------------------------------------------------------------------- /bin/syncrow-run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../build/cli/syncrow-run.js'); -------------------------------------------------------------------------------- /docs/prezentacja.md: -------------------------------------------------------------------------------- 1 | # Syncrow 2 | 3 | Aplikacja do synchronizacji plików w czasie rzeczywistym. 4 | 5 | ## Główne funkcjonalności 6 | * Wykrywanie zmian w pliku 7 | * Wysyłanie plików do podłączonych klientów 8 | * Wykrywanie najnowszej wersji 9 | 10 | ## Technologie 11 | * Node.js 12 | * TypeScript 13 | * Sockets 14 | 15 | ## Model działania 16 | * Aplikacje połączone przez socket TCP 17 | * Wysyłają do siebie komunikaty o zmianach w File Systemie(PUSH) 18 | * Wysyłanie plików odbywa się na osobnym sockecie - prosto do FS 19 | 20 | ## Trudności 21 | * API filesystemu (OSX / Linux / Windows) 22 | * Synchronizacja po przerwanym połączeniu - rozpoznawanie tego samego pliku w różnych miejscach 23 | * Połączenie przez publiczną sieć 24 | * performance 25 | 26 | ## Interfejsy 27 | * Obecnie tylko Command-Line 28 | 29 | ## Komponenty 30 | ### FileContainer 31 | * wraper na file system 32 | * emituje zdarzenia typu plik powstał/plik zmieniono itp. 33 | * konsumuje i produkuje strumienie z plikami 34 | * blokowanie przetwarzanych plików 35 | 36 | ### FileMetaQueue 37 | * kolejka która produkuje metadane dla pliku 38 | * nazwa pliku 39 | * typ(plik/folder) 40 | * data modyfikacji 41 | * hash zawartości (kosztowne) 42 | 43 | ### Client 44 | * Obserwuje FileContainer i interpretuje zdarzenia 45 | * wysyła komunikaty do innych klientów 46 | 47 | ### TransferQueue 48 | * kolejka do transferu plików 49 | * pozwala ograniczyć ilość otwartych socketów 50 | 51 | 52 | ### Messenger 53 | * wraper na socket 54 | * podajemy do niego komunikat i wysyła go przez socket 55 | * jeżeli przychodzi odpowiedź po sockecie, czeka aż przyjdzie cały komunikat 56 | * emituje zdarzenia typu wiadomość 57 | 58 | ## SynchronizationStrategy 59 | * po połaczeniu notyfikujemy strategię 60 | * strategia decyduje kiedy i jakie pliki pobrać 61 | 62 | # TODO 63 | * ignorowanie plików 64 | * więcej strategii połączenia po przerwaniu 65 | * przenoszenie uprawnień typu read/write/execute 66 | 67 | -------------------------------------------------------------------------------- /housekeeping/benchmark-short.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | node build/benchmarks/big_files_benchmark.js; 4 | 5 | node build/benchmarks/small_files_benchmark.js; 6 | 7 | -------------------------------------------------------------------------------- /housekeeping/benchmarks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | node build/benchmarks/big_files_benchmark.js; 4 | node build/benchmarks/big_files_benchmark.js; 5 | node build/benchmarks/big_files_benchmark.js; 6 | node build/benchmarks/big_files_benchmark.js; 7 | 8 | node build/benchmarks/small_files_benchmark.js; 9 | node build/benchmarks/small_files_benchmark.js; 10 | node build/benchmarks/small_files_benchmark.js; 11 | node build/benchmarks/small_files_benchmark.js; 12 | 13 | -------------------------------------------------------------------------------- /housekeeping/results.json: -------------------------------------------------------------------------------- 1 | { 2 | "big_files 4 files x 200000000B": { 3 | "b407b712ee48cc4bb21076fcbddb38ceafec30d1": [ 4 | 5429, 5 | 5348, 6 | 5931, 7 | 5644, 8 | 5704, 9 | 5178, 10 | 5260, 11 | 7660, 12 | 6820, 13 | 5317, 14 | 5123, 15 | 5153, 16 | 5353, 17 | 6920, 18 | 6168, 19 | 5758, 20 | 5315, 21 | 6071, 22 | 5154, 23 | 5659, 24 | 5278 25 | ], 26 | "b407b712ee48cc4bb21076fcbddb38ceafec30d1-average": 5725.857142857143, 27 | "3c0ef209916be8890fb6cee4f7544b4d49159546": [ 28 | 7709, 29 | 5259, 30 | 5702, 31 | 5243 32 | ], 33 | "3c0ef209916be8890fb6cee4f7544b4d49159546-average": 5978.25, 34 | "908bc824dfa06916713963763ae9d956b8401627": [ 35 | 7731, 36 | 5423, 37 | 5551, 38 | 5556 39 | ], 40 | "908bc824dfa06916713963763ae9d956b8401627-average": 6065.25, 41 | "c83ed56f8f8f915c8f976c0c44098b981a5e7975": [ 42 | 6071, 43 | 5623, 44 | 5395, 45 | 5162, 46 | 5905, 47 | 5177, 48 | 5159, 49 | 5325 50 | ], 51 | "c83ed56f8f8f915c8f976c0c44098b981a5e7975-average": 5477.125, 52 | "036f4e8dd6fa3b68bbbd8c48959a4804cbd53fad": [ 53 | 5149, 54 | 5306, 55 | 5153, 56 | 5142 57 | ], 58 | "036f4e8dd6fa3b68bbbd8c48959a4804cbd53fad-average": 5187.5, 59 | "8a76e8a059147490d6102994c6328e66d26c47af": [ 60 | 22088, 61 | 23094, 62 | 23337, 63 | 21259 64 | ], 65 | "8a76e8a059147490d6102994c6328e66d26c47af-average": 22444.5, 66 | "b25cea30dc485ddb2e6650e59ba371b0b21e4080": [ 67 | 5354, 68 | 5234, 69 | 5178, 70 | 5238 71 | ], 72 | "b25cea30dc485ddb2e6650e59ba371b0b21e4080-average": 5251, 73 | "e9b173df3c0aaa5acda9d41ec4bdfdb940a63b82": [ 74 | 7779, 75 | 7096, 76 | 6825 77 | ], 78 | "e9b173df3c0aaa5acda9d41ec4bdfdb940a63b82-average": 7233.333333333333, 79 | "1af8535fdcd3835e049d57b3cb53d0e2f654395d": [ 80 | 8647 81 | ], 82 | "1af8535fdcd3835e049d57b3cb53d0e2f654395d-average": 8647, 83 | "ce1a39e3fbda5d969c08afd704d705e1b7e17cc0": [ 84 | 6946, 85 | 8268, 86 | 10300, 87 | 13130, 88 | 13578, 89 | 12890 90 | ], 91 | "ce1a39e3fbda5d969c08afd704d705e1b7e17cc0-average": 10852, 92 | "cda4a3b5dbed9690faf141d57e4b714522ce54cc": [ 93 | 12010 94 | ], 95 | "cda4a3b5dbed9690faf141d57e4b714522ce54cc-average": 12010, 96 | "dbf81e4f4e0e53fb7fd0a413ef69eb6311054772": [ 97 | 5134 98 | ], 99 | "dbf81e4f4e0e53fb7fd0a413ef69eb6311054772-average": 5134, 100 | "4f4d17f6ce5a9f30b8dec51ae0fbdd108ad55e8c": [ 101 | 5824, 102 | 5344, 103 | 5069, 104 | 5148, 105 | 5180, 106 | 5251, 107 | 5716, 108 | 6653, 109 | 6459, 110 | 5422, 111 | 5320, 112 | 5890, 113 | 5768, 114 | 7613, 115 | 7205, 116 | 7288, 117 | 6218, 118 | 6457, 119 | 9153, 120 | 6366, 121 | 5659 122 | ], 123 | "4f4d17f6ce5a9f30b8dec51ae0fbdd108ad55e8c-average": 6143 124 | }, 125 | "small_files 1000 files x 20000B": { 126 | "b407b712ee48cc4bb21076fcbddb38ceafec30d1": [ 127 | 2759, 128 | 2703, 129 | 3306, 130 | 4177, 131 | 8136, 132 | 2741, 133 | 2839, 134 | 2943, 135 | 2754, 136 | 2940, 137 | 2682, 138 | 2712, 139 | 2879 140 | ], 141 | "b407b712ee48cc4bb21076fcbddb38ceafec30d1-average": 3351.6153846153848, 142 | "3c0ef209916be8890fb6cee4f7544b4d49159546": [ 143 | 3207, 144 | 3130, 145 | 3521, 146 | 9551, 147 | 10215, 148 | 8595, 149 | 8231 150 | ], 151 | "3c0ef209916be8890fb6cee4f7544b4d49159546-average": 6635.714285714285, 152 | "908bc824dfa06916713963763ae9d956b8401627": [ 153 | 2843, 154 | 2337, 155 | 2424 156 | ], 157 | "908bc824dfa06916713963763ae9d956b8401627-average": 2534.6666666666665, 158 | "c83ed56f8f8f915c8f976c0c44098b981a5e7975": [ 159 | 2611, 160 | 2576, 161 | 2709, 162 | 2526, 163 | 2477, 164 | 2933 165 | ], 166 | "c83ed56f8f8f915c8f976c0c44098b981a5e7975-average": 2638.6666666666665, 167 | "036f4e8dd6fa3b68bbbd8c48959a4804cbd53fad": [ 168 | 2679, 169 | 2854, 170 | 3377, 171 | 3345 172 | ], 173 | "036f4e8dd6fa3b68bbbd8c48959a4804cbd53fad-average": 3063.75, 174 | "8a76e8a059147490d6102994c6328e66d26c47af": [ 175 | 4360, 176 | 4166, 177 | 4614 178 | ], 179 | "8a76e8a059147490d6102994c6328e66d26c47af-average": 4380, 180 | "b25cea30dc485ddb2e6650e59ba371b0b21e4080": [ 181 | 2659, 182 | 2944, 183 | 3085, 184 | 3152 185 | ], 186 | "b25cea30dc485ddb2e6650e59ba371b0b21e4080-average": 2960, 187 | "e9b173df3c0aaa5acda9d41ec4bdfdb940a63b82": [ 188 | 2954, 189 | 2851, 190 | 2746 191 | ], 192 | "e9b173df3c0aaa5acda9d41ec4bdfdb940a63b82-average": 2850.3333333333335, 193 | "ce1a39e3fbda5d969c08afd704d705e1b7e17cc0": [ 194 | 6856, 195 | 7300 196 | ], 197 | "ce1a39e3fbda5d969c08afd704d705e1b7e17cc0-average": 7078, 198 | "dbf81e4f4e0e53fb7fd0a413ef69eb6311054772": [ 199 | 2771 200 | ], 201 | "dbf81e4f4e0e53fb7fd0a413ef69eb6311054772-average": 2771, 202 | "4f4d17f6ce5a9f30b8dec51ae0fbdd108ad55e8c": [ 203 | 2693, 204 | 2713, 205 | 2416, 206 | 2757, 207 | 2603, 208 | 2757, 209 | 2430, 210 | 2499, 211 | 2458 212 | ], 213 | "4f4d17f6ce5a9f30b8dec51ae0fbdd108ad55e8c-average": 2591.777777777778 214 | } 215 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Server: require('./build/facade/server'), 3 | Client: require('./build/facade/client'), 4 | Engine: require('./build/client/engine') 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syncrow", 3 | "version": "0.0.4", 4 | "description": "Real time directory synchronization using sockets", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --require source-map-support/register --recursive build/test", 8 | "compile": "tsc || true", 9 | "build": "npm run clear && npm run compile", 10 | "clear": "rm -Rf build", 11 | "benchmark": "./housekeeping/benchmarks.sh", 12 | "benchmark:short": "./housekeeping/benchmark-short.sh", 13 | "lint": "tslint `find src -name '*.ts*'`", 14 | "clear-maps": "find build -name \"*.js.map\" -type f -delete", 15 | "prepare": "npm run build && npm run clear-maps && npm run test" 16 | }, 17 | "keywords": [ 18 | "cli", 19 | "command-line", 20 | "realtime", 21 | "synchronization", 22 | "synchronisation", 23 | "file", 24 | "directory", 25 | "sync" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/jan-osch/syncrow.git" 30 | }, 31 | "author": "jan-osch", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/jan-osch/syncrow/issues" 35 | }, 36 | "bin": { 37 | "syncrow": "bin/syncrow" 38 | }, 39 | "homepage": "https://github.com/jan-osch/syncrow#readme", 40 | "dependencies": { 41 | "async": "^2.0.0-rc.3", 42 | "chalk": "^1.1.3", 43 | "chokidar": "^1.5.2", 44 | "commander": "^2.9.0", 45 | "debug": "^2.2.0", 46 | "ignore": "^3.1.5", 47 | "inquirer": "^1.1.2", 48 | "lodash": "^4.13.1", 49 | "mkdirp": "^0.5.1", 50 | "rimraf": "^2.5.2", 51 | "upath": "^0.1.7" 52 | }, 53 | "devDependencies": { 54 | "chai": "^3.5.0", 55 | "git-rev-sync": "^1.6.0", 56 | "mocha": "^2.5.3", 57 | "rewire": "^2.5.1", 58 | "sinon": "^1.17.4", 59 | "sinon-chai": "^2.8.0", 60 | "source-map-support": "^0.4.2", 61 | "tslint": "^3.15.1", 62 | "typescript": "^1.8.10" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/benchmarks/big_files_benchmark.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | import {createPathSeries, CreatePathArgument, compareDirectories} from "../utils/fs_test_utils"; 3 | import {Engine} from "../core/engine"; 4 | import * as crypto from "crypto"; 5 | import {EventCounter} from "../utils/event_counter"; 6 | import * as rimraf from "rimraf"; 7 | import {pushAction} from "../sync/push_action"; 8 | import saveResults from "./save_results"; 9 | import SListen from "../facade/server"; 10 | import SConnect from "../facade/client"; 11 | 12 | 13 | const TOKEN = '121cb2897o1289nnjos'; 14 | const PORT = 4321; 15 | const SAMPLE_SIZE = 200000000; // 200 MB 16 | const FILE_NUMBER = 4; 17 | const TIMEOUT = 60 * 1000; //1 minute 18 | 19 | const benchmarkName = `big_files ${FILE_NUMBER} files x ${SAMPLE_SIZE}B`; 20 | 21 | let listeningEngine; 22 | let connectingEngine; 23 | let eventCounter:EventCounter; 24 | let startTime; 25 | let endTime; 26 | 27 | setTimeout(()=> { 28 | throw new Error('Timeout out') 29 | }, TIMEOUT); 30 | 31 | const paths:Array = [ 32 | {path: 'build/benchmark/aaa', directory: true}, 33 | {path: 'build/benchmark/bbb', directory: true} 34 | ]; 35 | 36 | for (let i = 0; i < FILE_NUMBER; i++) { 37 | paths.push({path: `build/benchmark/bbb/big_${i + 1}.txt`, content: crypto.randomBytes(SAMPLE_SIZE)}) 38 | } 39 | 40 | async.waterfall( 41 | [ 42 | (cb)=>rimraf('build/benchmark', cb), 43 | 44 | (cb)=>createPathSeries(paths, cb), 45 | 46 | (cb)=> { 47 | startTime = new Date(); 48 | 49 | listeningEngine = new SListen({ 50 | path: 'build/benchmark/aaa', 51 | localPort: PORT, 52 | authenticate: true, 53 | externalHost: '127.0.0.1', 54 | initialToken: TOKEN, 55 | watch: true 56 | } 57 | ); 58 | 59 | return listeningEngine.start(cb) 60 | }, 61 | 62 | (cb)=> { 63 | connectingEngine = new SConnect({ 64 | path: 'build/benchmark/bbb', 65 | remotePort: PORT, 66 | remoteHost: '127.0.0.1', 67 | authenticate: true, 68 | initialToken: TOKEN, 69 | watch: true, 70 | sync: pushAction 71 | }); 72 | 73 | return connectingEngine.start(cb); 74 | }, 75 | 76 | (cb)=> { 77 | eventCounter = EventCounter.getCounter(connectingEngine.engine, Engine.events.synced, 1); 78 | 79 | return setImmediate(cb); 80 | }, 81 | 82 | (cb)=> { 83 | if (eventCounter.hasFinished()) { 84 | return setImmediate(cb); 85 | } 86 | 87 | eventCounter.once(EventCounter.events.done, cb); 88 | }, 89 | 90 | (cb)=> { 91 | endTime = new Date(); 92 | 93 | return setImmediate(cb); 94 | }, 95 | 96 | (cb)=> { 97 | listeningEngine.shutdown(); 98 | connectingEngine.shutdown(); 99 | return compareDirectories('build/benchmark/aaa', 'build/benchmark/bbb', cb) 100 | }, 101 | 102 | (cb)=>rimraf('build/benchmark', cb), 103 | 104 | (cb)=> { 105 | const difference = endTime.getTime() - startTime.getTime(); 106 | 107 | console.log(`Benchmark with big files took: ${difference} ms`); 108 | 109 | return saveResults(benchmarkName, difference, cb); 110 | } 111 | ], 112 | (err)=> { 113 | if (err) throw err; 114 | return process.exit(0); 115 | } 116 | ); 117 | 118 | -------------------------------------------------------------------------------- /src/benchmarks/save_results.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as git from "git-rev-sync"; 3 | import * as async from "async"; 4 | import {ErrorCallback} from "../utils/interfaces"; 5 | 6 | const RESULTS_FILE = 'housekeeping/results.json'; 7 | 8 | export default function saveResults(benchmarkName:string, value:number, callback:ErrorCallback) { 9 | return async.waterfall( 10 | [ 11 | (cb)=>fs.readFile(RESULTS_FILE, cb), 12 | 13 | (rawFile, cb)=>async.asyncify(JSON.parse)(rawFile, cb), 14 | 15 | (results, cb)=> { 16 | const commit = git.long(); 17 | 18 | if (!results[benchmarkName]) { 19 | results[benchmarkName] = {}; 20 | } 21 | 22 | if (!results[benchmarkName][commit]) { 23 | results[benchmarkName][commit] = []; 24 | } 25 | 26 | results[benchmarkName][commit].push(value); 27 | 28 | const sum = results[benchmarkName][commit].reduce((previous, current)=>previous + current, 0); 29 | results[benchmarkName][commit + '-average'] = sum / results[benchmarkName][commit].length; 30 | 31 | return fs.writeFile(RESULTS_FILE, JSON.stringify(results, null, 2), cb) 32 | } 33 | ], 34 | callback 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/benchmarks/small_files_benchmark.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | import {createPathSeries, compareDirectories} from "../utils/fs_test_utils"; 3 | import {Engine} from "../core/engine"; 4 | import * as crypto from "crypto"; 5 | import {EventCounter} from "../utils/event_counter"; 6 | import * as rimraf from "rimraf"; 7 | import {pushAction} from "../sync/push_action"; 8 | import saveResults from "./save_results"; 9 | import SListen from "../facade/server"; 10 | import SConnect from "../facade/client"; 11 | 12 | 13 | const TOKEN = '121cb2897o1289nnjos'; 14 | const PORT = 4321; 15 | const SAMPLE_SIZE = 20000; // 20 KB 16 | const FILE_NUMBER = 1000; // 1000 * 20 = 20 000 KB = 20MB 17 | const TIMEOUT = 30000; //30 seconds 18 | const AUTH_TIMEOUT = 5000; //5 seconds 19 | 20 | const benchmarkName = `small_files ${FILE_NUMBER} files x ${SAMPLE_SIZE}B`; 21 | 22 | let listeningEngine; 23 | let connectingEngine; 24 | let eventCounter:EventCounter; 25 | let startTime; 26 | let endTime; 27 | 28 | setTimeout(()=> { 29 | throw new Error(`Benchmark timeout out after: ${TIMEOUT} miliseconds`) 30 | }, TIMEOUT); 31 | 32 | 33 | async.waterfall( 34 | [ 35 | (cb)=>rimraf('build/benchmark', cb), 36 | 37 | (cb)=> { 38 | const files:Array = [ 39 | {path: 'build/benchmark/aaa', directory: true}, 40 | {path: 'build/benchmark/bbb', directory: true}, 41 | ]; 42 | 43 | for (let i = 0; i < FILE_NUMBER; i++) { 44 | files.push({path: `build/benchmark/bbb/small_${i}.txt`, content: crypto.randomBytes(SAMPLE_SIZE)}) 45 | } 46 | 47 | return createPathSeries(files, cb) 48 | }, 49 | 50 | (cb)=> { 51 | startTime = new Date(); 52 | 53 | listeningEngine = new SListen({ 54 | path: 'build/benchmark/aaa', 55 | localPort: PORT, 56 | authenticate: true, 57 | externalHost: '127.0.0.1', 58 | authTimeout: AUTH_TIMEOUT, 59 | initialToken: TOKEN, 60 | watch: true 61 | }); 62 | 63 | return listeningEngine.start(cb); 64 | }, 65 | 66 | (cb)=> { 67 | 68 | 69 | connectingEngine = new SConnect({ 70 | path: 'build/benchmark/bbb', 71 | remotePort: PORT, 72 | remoteHost: '127.0.0.1', 73 | authenticate: true, 74 | initialToken: TOKEN, 75 | watch: true, 76 | sync: pushAction 77 | } 78 | ); 79 | eventCounter = EventCounter.getCounter(connectingEngine.engine, Engine.events.synced, 1); 80 | return connectingEngine.start(cb); 81 | }, 82 | 83 | (cb)=> { 84 | if (eventCounter.hasFinished()) { 85 | return setImmediate(cb); 86 | } 87 | 88 | eventCounter.once(EventCounter.events.done, cb); 89 | }, 90 | 91 | (cb)=> { 92 | endTime = new Date(); 93 | 94 | return setImmediate(cb); 95 | }, 96 | 97 | (cb)=> { 98 | listeningEngine.shutdown(); 99 | connectingEngine.shutdown(); 100 | return compareDirectories('build/benchmark/aaa', 'build/benchmark/bbb', cb); 101 | }, 102 | (cb)=>rimraf('build/benchmark', cb), 103 | 104 | (cb)=> { 105 | const difference = endTime.getTime() - startTime.getTime(); 106 | 107 | console.log(`Benchmark with ${FILE_NUMBER} small files took: ${difference} ms`); 108 | 109 | return saveResults(benchmarkName, difference, cb); 110 | } 111 | ], 112 | (err)=> { 113 | if (err) throw err; 114 | 115 | return process.exit(0); 116 | } 117 | ); 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/cli/program.ts: -------------------------------------------------------------------------------- 1 | import {SyncAction} from "../sync/sync_actions"; 2 | import {FilterFunction} from "../fs_helpers/file_container"; 3 | 4 | export interface ProgramOptions { 5 | path:string; //path to watch 6 | listen?:boolean; //Listen or connect 7 | 8 | /** 9 | * For connecting 10 | */ 11 | remoteHost?:string; 12 | remotePort?:number; 13 | 14 | 15 | /** 16 | * For listening 17 | */ 18 | localPort?:number; 19 | externalHost?:string;//This should be external IP/domain hostname 20 | 21 | 22 | sync?:SyncAction; //Action that will be taken on each new connection 23 | rawStrategy?:string //string code that denotes each action 24 | deleteLocal?:boolean; //Flag tor SyncAction - will delete local files when they are missing remotely 25 | deleteRemote?:boolean; //Flag for SyncAction - will delete remote files when they are missing locally 26 | 27 | filter?:FilterFunction; //function that will filter out files that should not be watched/transferred 28 | rawFilter?:Array; //Array of anymatch patterns that will construct filter 29 | 30 | initialToken?:string; //Token for first connection 31 | authenticate?:boolean; //flag that will enable authentication of all sockets 32 | 33 | /** 34 | * Reconnect params 35 | */ 36 | reconnect?:boolean; 37 | times?:number; 38 | interval?:number; 39 | 40 | 41 | watch?:boolean //Watch local filesystem 42 | } 43 | 44 | export const configurationFileName = '.syncrow.json'; 45 | -------------------------------------------------------------------------------- /src/cli/syncrow-init.ts: -------------------------------------------------------------------------------- 1 | import * as inquirer from "inquirer"; 2 | import * as fs from "fs"; 3 | import * as crypto from "crypto"; 4 | import {configurationFileName} from "./program"; 5 | 6 | const noActionText = `No Action - existing files will be ignored, only new changes will be synced`; 7 | const pullActionText = `Pull - when other party connects all files will be overwritten by those remote`; 8 | const pushActionText = 'Push - when other party connects all remote files will be overwritten by those local'; 9 | 10 | const questions = [ 11 | { 12 | type: 'confirm', 13 | name: 'listen', 14 | message: 'Do you want to listen for connection?', 15 | default: true 16 | }, 17 | 18 | { 19 | type: 'input', 20 | name: 'remoteHost', 21 | message: 'Connection: remote host', 22 | when: (answers)=>!answers.listen, 23 | default: '127.0.0.1' 24 | }, 25 | 26 | { 27 | type: 'input', 28 | name: 'remotePort', 29 | message: 'Connection: remote port', 30 | validate: (value)=> { 31 | const valid = !isNaN(parseInt(value, 10)); 32 | return valid || 'Please enter a number'; 33 | }, 34 | filter: Number, 35 | default: 2510, 36 | when: (answers)=>!answers.listen 37 | }, 38 | 39 | { 40 | type: 'input', 41 | name: 'localPort', 42 | message: 'On which local port would you like to listen', 43 | validate: (value)=> { 44 | const valid = !isNaN(parseInt(value, 10)); 45 | return valid || 'Please enter a number'; 46 | }, 47 | filter: Number, 48 | default: 2510, 49 | when: (answers)=>answers.listen 50 | }, 51 | 52 | { 53 | type: 'input', 54 | name: 'externalHost', 55 | message: 'What is your external IP/hostname?', 56 | default: '127.0.0.1', 57 | when: (answers)=>answers.listen 58 | }, 59 | 60 | { 61 | type: 'input', 62 | name: 'rawFilter', 63 | message: 'Please enter comma-separated gitignore like patterns for files that should be ignored', 64 | default: '' 65 | }, 66 | 67 | { 68 | type: 'list', 69 | name: 'rawStrategy', 70 | message: 'What synchronization strategy for every new connection would you like to choose?', 71 | choices: [ 72 | noActionText, 73 | pullActionText, 74 | pushActionText 75 | ], 76 | default: 0 77 | }, 78 | 79 | { 80 | type: 'input', 81 | name: 'initialToken', 82 | message: 'Please enter password for obtaining connection', 83 | }, 84 | 85 | { 86 | type: 'confirm', 87 | name: 'advanced', 88 | message: 'Would you like to setup advanced options?', 89 | default: false, 90 | }, 91 | 92 | /** 93 | * Advanced: 94 | */ 95 | 96 | { 97 | type: 'confirm', 98 | name: 'deleteRemote', 99 | message: 'Would you like to delete remote files on push?', 100 | default: true, 101 | when: (answers)=>answers.advanced && answers.rawStrategy === pushActionText 102 | }, 103 | 104 | { 105 | type: 'confirm', 106 | name: 'deleteLocal', 107 | message: 'Would you like to delete local files on pull?', 108 | default: true, 109 | when: (answers)=>answers.advanced && answers.rawStrategy === pullActionText 110 | }, 111 | 112 | { 113 | type: 'confirm', 114 | name: 'authenticate', 115 | message: 'Would you like to authenticate transport sockets?', 116 | default: true, 117 | when: (answers)=>answers.advanced 118 | }, 119 | 120 | { 121 | type: 'confirm', 122 | name: 'watch', 123 | message: 'Would you like to watch local file system?', 124 | default: true, 125 | when: (answers)=>answers.advanced 126 | }, 127 | 128 | { 129 | type: 'confirm', 130 | name: 'reconnect', 131 | message: 'Would you like to reconnect when connection was lost?', 132 | default: true, 133 | when: (answers)=>answers.advanced && !answers.listen 134 | } 135 | ]; 136 | 137 | inquirer.prompt(questions).then(answers=> { 138 | const hash = crypto.createHash('sha256'); 139 | hash.update(answers.initialToken); 140 | answers.initialToken = hash.digest().toString('hex'); 141 | 142 | if (!answers.advanced) { 143 | answers.reconnct = true; 144 | answers.watch = true; 145 | answers.authenticate = true; 146 | 147 | if (answers.rawStrategy === pullActionText) { 148 | answers.deleteLocal = true; 149 | } 150 | 151 | if (answers.rawStrategy === pushActionText) { 152 | answers.deleteRemote = true; 153 | } 154 | 155 | answers.reconnect = true; 156 | } 157 | 158 | if (answers.reconnect) { 159 | answers.interval = 10000; //10 seconds 160 | answers.times = 18; // 18 * 10s = 180s = 3 minutes 161 | delete answers.reconnect; 162 | } 163 | 164 | if (answers.rawStrategy === pullActionText) { 165 | answers.rawStrategy = 'pull'; 166 | } 167 | if (answers.rawStrategy === pushActionText) { 168 | answers.rawStrategy = 'push'; 169 | } 170 | if (answers.rawStrategy === noActionText) { 171 | answers.rawStrategy = 'no'; 172 | } 173 | 174 | if (answers.externalHost === '') { 175 | delete answers.externalHost; 176 | } 177 | 178 | answers.rawFilter = answers.rawFilter ? answers.rawFilter.split(',') : []; 179 | 180 | fs.writeFileSync(configurationFileName, JSON.stringify(answers, null, 2)); 181 | }); 182 | 183 | -------------------------------------------------------------------------------- /src/cli/syncrow-run.ts: -------------------------------------------------------------------------------- 1 | import {debugFor, loggerFor} from "../utils/logger"; 2 | import {Engine} from "../core/engine"; 3 | import * as fs from "fs"; 4 | import {SyncAction} from "../sync/sync_actions"; 5 | import {pullAction} from "../sync/pull_action"; 6 | import {ProgramOptions, configurationFileName} from "./program"; 7 | import {noAction} from "../sync/no_action"; 8 | import {pushAction} from "../sync/push_action"; 9 | import {PathHelper} from "../fs_helpers/path_helper"; 10 | import SListen from "../facade/server"; 11 | import SConnect from "../facade/client"; 12 | 13 | const logger = loggerFor("syncrow-run"); 14 | const debug = debugFor("syncrow:cli:run"); 15 | 16 | /** 17 | * MAIN 18 | */ 19 | debug('executing syncrow-run'); 20 | 21 | 22 | const savedConfig = loadConfigFromFile(configurationFileName); 23 | const preparedConfig = buildConfig(savedConfig); 24 | 25 | printDebugAboutConfig(preparedConfig); 26 | startEngine(savedConfig); 27 | 28 | 29 | /** 30 | * @param path 31 | */ 32 | function loadConfigFromFile(path:string):ProgramOptions { 33 | try { 34 | const result = JSON.parse(fs.readFileSync(path, 'utf8')); 35 | debug(`found configuration in file`); 36 | 37 | return result; 38 | } catch (e) { 39 | throw new Error('Configuration file not found or invalid - run "syncrow init" to initialize') 40 | } 41 | } 42 | 43 | /** 44 | * @param savedConfig 45 | * @returns {ProgramOptions} 46 | */ 47 | function buildConfig(savedConfig:ProgramOptions):ProgramOptions { 48 | const filterStrings = savedConfig.rawFilter.concat([configurationFileName]); 49 | 50 | savedConfig.filter = PathHelper.createFilterFunction(filterStrings, process.cwd()); 51 | 52 | savedConfig.sync = chooseStrategy(savedConfig.rawStrategy); 53 | 54 | return savedConfig; 55 | } 56 | 57 | /** 58 | * @param chosenConfig 59 | */ 60 | function startEngine(chosenConfig:ProgramOptions) { 61 | chosenConfig.path = process.cwd(); 62 | if (chosenConfig.listen) { 63 | 64 | const listener = new SListen( 65 | { 66 | path: process.cwd(), 67 | localPort: chosenConfig.localPort, 68 | externalHost: chosenConfig.externalHost, 69 | sync: chosenConfig.sync, 70 | watch: chosenConfig.watch, 71 | filter: chosenConfig.filter, 72 | initialToken: chosenConfig.initialToken, 73 | authenticate: chosenConfig.authenticate 74 | }); 75 | 76 | return listener.start((err)=> { 77 | debug(`listening engine started`); 78 | ifErrorThrow(err); 79 | listener.engine.on(Engine.events.error, ifErrorThrow); 80 | }) 81 | } 82 | 83 | const connector = new SConnect({ 84 | path: process.cwd(), 85 | remotePort: chosenConfig.remotePort, 86 | remoteHost: chosenConfig.remoteHost, 87 | sync: chosenConfig.sync, 88 | watch: chosenConfig.watch, 89 | filter: chosenConfig.filter, 90 | initialToken: chosenConfig.initialToken, 91 | authenticate: chosenConfig.authenticate 92 | }); 93 | 94 | connector.start((err)=> { 95 | ifErrorThrow(err); 96 | debug(`engine connected`); 97 | connector.engine.on(Engine.events.error, ifErrorThrow); 98 | }) 99 | } 100 | 101 | function chooseStrategy(key:string):SyncAction { 102 | if (key === 'no') { 103 | return noAction; 104 | } 105 | if (key === 'pull') { 106 | return pullAction; 107 | } 108 | if (key === 'push') { 109 | return pushAction; 110 | } 111 | 112 | throw new Error(`invalid strategy key: ${key}`); 113 | } 114 | 115 | function printDebugAboutConfig(finalConfig:ProgramOptions) { 116 | debug(`final config: ${JSON.stringify(finalConfig, null, 2)}`); 117 | } 118 | 119 | function ifErrorThrow(err?:Error|any) { 120 | if (err) { 121 | if (err.stack) console.error(err.stack); 122 | throw err; 123 | } 124 | } -------------------------------------------------------------------------------- /src/cli/syncrow.ts: -------------------------------------------------------------------------------- 1 | import * as program from "commander"; 2 | 3 | /** 4 | * MAIN 5 | */ 6 | main(); 7 | 8 | function main() { 9 | program 10 | .version('0.0.4') 11 | .description('a real-time file synchronization tool') 12 | .command('init', 'initializes syncrow in current directory') 13 | .alias('i') 14 | .command('run', 'run syncrow in current directory', {isDefault: true}) 15 | .alias('r'); 16 | 17 | program.parse(process.argv); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/connection/authorisation_helper.ts: -------------------------------------------------------------------------------- 1 | import {Socket} from "net"; 2 | import * as async from "async"; 3 | import {debugFor} from "../utils/logger"; 4 | import {ErrorCallback} from "../utils/interfaces"; 5 | import * as crypto from "crypto"; 6 | import * as _ from "lodash"; 7 | 8 | const debug = debugFor('syncrow:con:authorisation_helper'); 9 | 10 | export class AuthorisationHelper { 11 | 12 | 13 | /** 14 | * @param socket 15 | * @param token 16 | * @param options 17 | * @param callback 18 | */ 19 | public static authorizeAsClient(socket:Socket, token:string, options:{timeout:number}, callback:ErrorCallback) { 20 | debug(`#authorizeAsClient with token: ${token} called port: ${socket.remotePort}`); 21 | 22 | callback = _.once(callback); 23 | 24 | try { 25 | socket.write(token); 26 | } catch (e) { 27 | debug(`#authorizeAsClient - failed reason: ${e}`); 28 | return callback(e); 29 | } 30 | 31 | 32 | socket.on('close', ()=> { 33 | if (socket.bytesRead === 0) return callback(new Error(`Socket has been destroyed - authorization failed - remotePort: ${socket.remotePort}`)); 34 | 35 | debug(`#authorizeAsClient - success - token: ${token} remotePort: ${socket.remotePort}`); 36 | return callback(); 37 | }); 38 | 39 | return setTimeout( 40 | ()=> { 41 | if (socket.destroyed && socket.bytesRead === 0) return callback(new Error(`Socket has been destroyed - authorization failed - remotePort: ${socket.remotePort}`)); 42 | 43 | debug(`#authorizeAsClient - success - token: ${token} remotePort: ${socket.remotePort}`); 44 | return callback(); 45 | }, 46 | options.timeout 47 | ) 48 | } 49 | 50 | /** 51 | * @param socket 52 | * @param token 53 | * @param options 54 | * @param callback - if error is passed to callback the socket should be destroyed 55 | */ 56 | public static authorizeAsServer(socket:Socket, token:string, options:{timeout:number}, callback:ErrorCallback) { 57 | 58 | debug(`#authorizeAsServer with token: ${token} port: ${socket.localPort} called`); 59 | 60 | const wrapped = async.timeout( 61 | (cb)=> { 62 | socket.once('data', 63 | (data)=> { 64 | debug(`#authorizeAsServer - got data: ${data} port: ${socket.localPort}`); 65 | 66 | const expectedToken = data.toString(); 67 | 68 | if (expectedToken !== token) { 69 | debug(`#authorizeAsServer - token does not match: ${expectedToken} vs ${token} port: ${socket.localPort}`); 70 | return cb(new Error(`token: ${data} does not match: ${token} port: ${socket.localPort}`)); 71 | } 72 | 73 | return cb(); 74 | } 75 | ); 76 | }, 77 | options.timeout, 78 | new Error(`Authorisation timeout - port: ${socket.localPort}`) 79 | ); 80 | 81 | wrapped((err)=> { 82 | socket.removeAllListeners('data'); 83 | return callback(err); 84 | }); 85 | } 86 | 87 | /** 88 | * Generates new token for authorisation 89 | */ 90 | public static generateToken():string { 91 | return crypto.createHash('sha256') 92 | .update(Math.random().toString()) 93 | .digest('hex'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/connection/callback_helper.ts: -------------------------------------------------------------------------------- 1 | import {debugFor} from "../utils/logger"; 2 | 3 | const debug = debugFor('syncrow:con:callback_helper'); 4 | 5 | export class CallbackHelper { 6 | private callbackMap:Map; 7 | 8 | /** 9 | * Used to exchange commands with callbacks 10 | */ 11 | constructor() { 12 | this.callbackMap = new Map(); 13 | } 14 | 15 | /** 16 | * Returns callback if it exists 17 | * @param id 18 | * @returns {function(Error, Event): any} 19 | */ 20 | public getCallback(id:string):Function { 21 | if (id && this.callbackMap.has(id)) { 22 | debug(`found callback for stored id: ${id}`); 23 | const callback = this.callbackMap.get(id); 24 | this.callbackMap.delete(id); 25 | return callback; 26 | } 27 | debug(`callback not found for id: ${id}`); 28 | } 29 | 30 | /** 31 | * Generates an Id 32 | * @returns {string} 33 | */ 34 | public static generateEventId():string { 35 | return Math.random().toString(); 36 | } 37 | 38 | /** 39 | * Adds a function to map of remembered callbacks 40 | * @throws Error if id already exists 41 | * @param id 42 | * @param callback 43 | */ 44 | public addCallbackWithId(id:string, callback:Function) { 45 | if (this.callbackMap.has(id)) { 46 | throw new Error(`callback id: ${id} already exists`); 47 | } 48 | 49 | debug(`setting a callback for id: ${id}`); 50 | this.callbackMap.set(id, callback); 51 | } 52 | 53 | /** 54 | * Handy function that generates id stores the callback and returns id 55 | * @param callback 56 | * @returns {string} id 57 | */ 58 | public addCallback(callback:Function):string { 59 | const id = CallbackHelper.generateEventId(); 60 | this.addCallbackWithId(id, callback); 61 | return id; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/connection/connection_helper.ts: -------------------------------------------------------------------------------- 1 | import {Socket} from "net"; 2 | import {Closable} from "../utils/interfaces"; 3 | 4 | export interface ConnectionAddress { 5 | remotePort:number; 6 | remoteHost:string; 7 | token?:string; 8 | } 9 | 10 | export interface ListenCallback { 11 | (address:ConnectionAddress):any; 12 | } 13 | 14 | export interface SocketCallback { 15 | (err:Error, socket?:Socket):any; 16 | } 17 | 18 | export interface ConnectionHelper extends Closable { 19 | getNewSocket(params:ConnectionHelperParams, callback:SocketCallback):any; 20 | } 21 | 22 | export interface ConnectionHelperParams { 23 | remotePort?:number; 24 | remoteHost?:string; 25 | localHost?:string; 26 | localPort?:number; 27 | token?:string; 28 | listenCallback?:ListenCallback; 29 | } -------------------------------------------------------------------------------- /src/connection/constant_connector.ts: -------------------------------------------------------------------------------- 1 | import {ConnectionHelper, SocketCallback} from "./connection_helper"; 2 | import * as net from "net"; 3 | import {loggerFor, debugFor} from "../utils/logger"; 4 | import {AuthorisationHelper} from "./authorisation_helper"; 5 | import * as _ from "lodash"; 6 | 7 | const logger = loggerFor('ConstantConnector'); 8 | const debug = debugFor('syncrow:con:constant_connector'); 9 | 10 | export default class ConstantConnector implements ConnectionHelper { 11 | 12 | constructor(private authTimeout:number, private remoteHost:string, private remotePort:number, private constantToken:string) { 13 | } 14 | 15 | public shutdown() { 16 | } 17 | 18 | /** 19 | * @param params 20 | * @param callback 21 | */ 22 | public getNewSocket(params:{}, callback:SocketCallback):any { 23 | callback = _.once(callback); 24 | 25 | debug(`#getNewSocket - connecting to : ${this.remoteHost}:${this.remotePort}`); 26 | 27 | const socket = net.connect( 28 | { 29 | port: this.remotePort, 30 | host: this.remoteHost 31 | }, 32 | (err)=> { 33 | if (err)return callback(err); 34 | 35 | if (!this.constantToken) { 36 | debug(`#getNewSocket - finished without authorisation`); 37 | return callback(null, socket); 38 | } 39 | 40 | debug(`#getNewSocket - starting connecting with token: ${this.constantToken}`); 41 | return AuthorisationHelper.authorizeAsClient(socket, 42 | this.constantToken, 43 | {timeout: this.authTimeout}, 44 | 45 | (err)=> { 46 | if (err)return callback(err); 47 | 48 | debug(`#getNewSocket - finished connecting with token: ${this.constantToken}`); 49 | return callback(null, socket); 50 | } 51 | ) 52 | } 53 | ).on('error', callback); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/connection/constant_server.ts: -------------------------------------------------------------------------------- 1 | import {ConnectionHelper, SocketCallback} from "./connection_helper"; 2 | import * as net from "net"; 3 | import {loggerFor, debugFor} from "../utils/logger"; 4 | import {ErrorCallback} from "../utils/interfaces"; 5 | import {AuthorisationHelper} from "./authorisation_helper"; 6 | 7 | const logger = loggerFor('ConstantServer'); 8 | const debug = debugFor('syncrow:con:constant_server'); 9 | 10 | export default class ConstantServer implements ConnectionHelper { 11 | 12 | private server:net.Server; 13 | private awaitingCallback:SocketCallback; 14 | 15 | constructor(private port:number, private params:{constantToken?:string, authTimeout:number}) { 16 | this.server = net.createServer(); 17 | this.server.on('connection', 18 | (socket:net.Socket)=>this.handleConnection(socket) 19 | ); 20 | } 21 | 22 | /** 23 | * Starts the constant server 24 | * @param callback 25 | */ 26 | public listen(callback:ErrorCallback) { 27 | this.server.listen(this.port, callback) 28 | } 29 | 30 | /** 31 | */ 32 | public shutdown() { 33 | this.awaitingCallback = null; 34 | this.server.close(); 35 | } 36 | 37 | /** 38 | * @param params 39 | * @param callback 40 | * @returns {number} 41 | */ 42 | public getNewSocket(params:{}, callback:SocketCallback):any { 43 | if (this.awaitingCallback) { 44 | return setImmediate(callback, new Error('Callback already awaiting a socket')); 45 | } 46 | 47 | this.awaitingCallback = callback; 48 | } 49 | 50 | private handleConnection(socket:net.Socket) { 51 | if (!this.awaitingCallback) { 52 | logger.error('Got a socket that was not ordered - it will be rejected'); 53 | return socket.destroy(); 54 | } 55 | 56 | const serverCallback = this.awaitingCallback; 57 | this.awaitingCallback = null; 58 | 59 | if (!this.params.constantToken) { 60 | debug(`#handleConnection - finished without authorisation`); 61 | return serverCallback(null, socket); 62 | } 63 | debug(`#handleConnection - starting authorisation with ${this.params.constantToken}`); 64 | 65 | return AuthorisationHelper.authorizeAsServer(socket, 66 | this.params.constantToken, 67 | {timeout: this.params.authTimeout}, 68 | (err)=> { 69 | if (err) { 70 | debug(`#handleConnection - destroying connection`); 71 | socket.destroy(); 72 | return serverCallback(err); 73 | } 74 | 75 | debug(`#handleConnection - finished authorisation with ${this.params.constantToken}`); 76 | return serverCallback(null, socket); 77 | } 78 | ); 79 | } 80 | } -------------------------------------------------------------------------------- /src/connection/dynamic_connector.ts: -------------------------------------------------------------------------------- 1 | import {ConnectionHelper, SocketCallback} from "./connection_helper"; 2 | import * as net from "net"; 3 | import {loggerFor, debugFor} from "../utils/logger"; 4 | import {AuthorisationHelper} from "./authorisation_helper"; 5 | import * as _ from "lodash"; 6 | 7 | const logger = loggerFor('DynamicConnector'); 8 | const debug = debugFor('syncrow:con:dynamic_connector'); 9 | 10 | export default class DynamicConnector implements ConnectionHelper { 11 | 12 | constructor(private authTimeout:number) { 13 | } 14 | 15 | public shutdown() { 16 | } 17 | 18 | /** 19 | * @param params 20 | * @param callback 21 | */ 22 | public getNewSocket(params:{remoteHost:string, remotePort:number, token?:string}, callback:SocketCallback):any { 23 | callback = _.once(callback); 24 | 25 | try { 26 | DynamicConnector.validateParams(params); 27 | } catch (e) { 28 | return setImmediate(callback, e); 29 | } 30 | 31 | debug(`#getNewSocket - starting connecting to: ${params.remoteHost}:${params.remotePort}`); 32 | const socket = net.connect( 33 | { 34 | port: params.remotePort, 35 | host: params.remoteHost 36 | }, 37 | (err)=> { 38 | if (err)return callback(err); 39 | 40 | if (!params.token) { 41 | 42 | debug(`#getNewSocket - finished connecting to: ${params.remoteHost}:${params.remotePort} without authorisation`); 43 | return callback(null, socket); 44 | } 45 | 46 | debug(`#getNewSocket - starting authorisation during connecting to: ${params.remoteHost}:${params.remotePort} with token ${params.token}`); 47 | return AuthorisationHelper.authorizeAsClient(socket, 48 | params.token, 49 | {timeout: this.authTimeout}, 50 | (err)=> { 51 | if (err)return callback(err); 52 | 53 | debug(`#getNewSocket - finished connecting to: ${params.remoteHost}:${params.remotePort} with token ${params.token}`); 54 | return callback(null, socket); 55 | } 56 | ) 57 | } 58 | ).on('error', callback); 59 | } 60 | 61 | private static validateParams(params:{remoteHost:string, remotePort:number, token?:string}) { 62 | if (!params.remoteHost) throw new Error('Remote host is needed for dynamic connection'); 63 | if (!params.remotePort) throw new Error('Remote port is needed for dynamic connection'); 64 | } 65 | } -------------------------------------------------------------------------------- /src/connection/dynamic_server.ts: -------------------------------------------------------------------------------- 1 | import {ConnectionHelper, SocketCallback, ListenCallback} from "./connection_helper"; 2 | import * as net from "net"; 3 | import {debugFor} from "../utils/logger"; 4 | import {AuthorisationHelper} from "./authorisation_helper"; 5 | import * as _ from "lodash"; 6 | 7 | const debug = debugFor('syncrow:con:dynamic_server'); 8 | 9 | export default class DynamicServer implements ConnectionHelper { 10 | 11 | private oneTimeServers:Set; 12 | 13 | constructor(private params:{constantToken?:string, authTimeout?:number, generateToken?:boolean}, 14 | private externalHost:string) { 15 | this.oneTimeServers = new Set(); 16 | } 17 | 18 | /** 19 | */ 20 | public shutdown() { 21 | this.oneTimeServers.forEach(s =>s.close()) 22 | } 23 | 24 | /** 25 | * @param params 26 | * @param callback 27 | * @returns {number} 28 | */ 29 | public getNewSocket(params:{listenCallback:ListenCallback}, callback:SocketCallback):any { 30 | callback = _.once(callback); 31 | 32 | debug('#getNewSocket called'); 33 | 34 | const server = net.createServer(); 35 | this.oneTimeServers.add(server); 36 | 37 | const actualToken = this.getToken(); 38 | 39 | server.on('connection', 40 | (socket)=> this.handleConnection(server, actualToken, socket, callback) 41 | ); 42 | 43 | server.listen(()=> { 44 | params.listenCallback({ 45 | remotePort: server.address().port, 46 | remoteHost: this.externalHost, 47 | token: actualToken 48 | }) 49 | }) 50 | } 51 | 52 | private handleConnection(server:net.Server, actualToken:string, socket:net.Socket, callback:SocketCallback) { 53 | this.oneTimeServers.delete(server); 54 | server.close(); 55 | 56 | if (!actualToken) { 57 | debug(`#handleConnection - finished without authorisation`); 58 | return callback(null, socket); 59 | } 60 | 61 | debug(`#handleConnection - starting authorisation with ${actualToken}`); 62 | return AuthorisationHelper.authorizeAsServer(socket, 63 | actualToken, 64 | {timeout: this.params.authTimeout}, 65 | (err)=> { 66 | 67 | if (err) { 68 | debug(`#handleConnection -destroying socket - reason: ${err}`); 69 | socket.destroy(); 70 | return callback(err); 71 | } 72 | 73 | debug(`#handleConnection - finished authorisation with ${actualToken}`); 74 | return callback(null, socket); 75 | } 76 | ) 77 | } 78 | 79 | private getToken():string { 80 | if (this.params.constantToken) return this.params.constantToken; 81 | 82 | if (this.params.generateToken) return AuthorisationHelper.generateToken(); 83 | } 84 | } -------------------------------------------------------------------------------- /src/connection/event_messenger.ts: -------------------------------------------------------------------------------- 1 | import {debugFor, loggerFor} from "../utils/logger"; 2 | import {Socket} from "net"; 3 | import {ParseHelper} from "./parse_helper"; 4 | import {Closable} from "../utils/interfaces"; 5 | import {EventEmitter} from "events"; 6 | import {CallbackHelper} from "./callback_helper"; 7 | 8 | const debug = debugFor('syncrow:con:evented_messenger'); 9 | const logger = loggerFor('Messenger'); 10 | 11 | export interface Event { 12 | type:string; 13 | body?:any; 14 | id?:string; 15 | } 16 | 17 | export class EventMessenger extends EventEmitter implements Closable { 18 | 19 | private socket:Socket; 20 | private isAlive:boolean; 21 | private parseHelper:ParseHelper; 22 | private callbackHelper:CallbackHelper; 23 | 24 | static response:string = 'eventMessengerResponse'; 25 | 26 | static events = { 27 | message: 'message', 28 | died: 'disconnected', 29 | error: 'error' 30 | }; 31 | 32 | /** 33 | * Enables sending string commands between parties 34 | * @param socket 35 | */ 36 | constructor(socket:Socket) { 37 | super(); 38 | 39 | debug('creating new event messenger'); 40 | 41 | this.socket = socket; 42 | this.socket.on('error', (error)=>this.disconnectAndDestroyCurrentSocket(error)); 43 | this.socket.on('close', (error)=>this.disconnectAndDestroyCurrentSocket(error)); 44 | 45 | this.parseHelper = new ParseHelper(this.socket); 46 | this.parseHelper.on(ParseHelper.events.message, (message)=>this.parseAndEmit(message)); 47 | 48 | this.callbackHelper = new CallbackHelper(); 49 | 50 | this.isAlive = true; 51 | } 52 | 53 | /** 54 | * Removes all helpers to prevent memory leaks 55 | */ 56 | public shutdown() { 57 | delete this.parseHelper; 58 | this.disconnectAndDestroyCurrentSocket(); 59 | } 60 | 61 | /** 62 | * Convenience method 63 | * @param type 64 | * @param body 65 | * @param id 66 | */ 67 | public send(type:string, body?:any, id?:string) { 68 | if (!this.isAlive) { 69 | const err = new Error('Socket connection is closed will not write data'); 70 | return logger.error(`${err.stack}`); 71 | } 72 | 73 | const event:Event = { 74 | type: type, 75 | body: body, 76 | }; 77 | 78 | if (id) event.id = id; 79 | 80 | this.parseHelper.writeMessage(JSON.stringify(event)); 81 | } 82 | 83 | /** 84 | * @param type 85 | * @param body 86 | * @param callback 87 | */ 88 | public sendRequest(type:string, body:any, callback:Function) { 89 | const id = this.callbackHelper.addCallback(callback); 90 | 91 | this.send(type, body, id); 92 | } 93 | 94 | /** 95 | * @param source 96 | * @param payload 97 | * @param error 98 | */ 99 | public sendResponse(source:Event, payload:any, error?:Error) { 100 | if (source.id) { 101 | return this.send(EventMessenger.response, {error: error, payload: payload}, source.id); 102 | } 103 | debug(`no response sent for event: ${source}`); 104 | } 105 | 106 | 107 | private disconnectAndDestroyCurrentSocket(error?:Error) { 108 | if (error)logger.error(`Socket error: ${error}`); 109 | 110 | if (this.socket) { 111 | const socket = this.socket; 112 | delete this.socket; 113 | this.isAlive = false; 114 | socket.removeAllListeners(); 115 | socket.destroy(); 116 | this.emit(EventMessenger.events.died); 117 | } 118 | } 119 | 120 | private parseEvent(message:string):Event { 121 | try { 122 | return JSON.parse(message.toString()); 123 | } catch (e) { 124 | debug(`Sending error: exception during parsing message: ${message}`); 125 | this.send(EventMessenger.events.error, {title: 'Bad event', details: message}); 126 | } 127 | } 128 | 129 | private parseAndEmit(rawMessage:string) { 130 | debug(`got an event: ${rawMessage}`); 131 | 132 | const event = this.parseEvent(rawMessage); 133 | 134 | 135 | if (event.type === EventMessenger.response) { 136 | try { 137 | return this.callbackHelper.getCallback(event.id)(event.body.error, event.body.payload); 138 | } catch (err) { 139 | return this.emit(EventMessenger.events.error, {title: `unknown id: ${event.id}`, details: err.message}); 140 | } 141 | } 142 | 143 | if (this.listenerCount(event.type) == 0) { 144 | return this.emit(EventMessenger.events.error, {title: `Unknown event type: ${event.type}`, details: event}) 145 | } 146 | 147 | debug(`emitting event: ${event.type}`); 148 | return this.emit(event.type, event); 149 | } 150 | } -------------------------------------------------------------------------------- /src/connection/parse_helper.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from "events"; 2 | import {Socket} from "net"; 3 | import {debugFor, loggerFor} from "../utils/logger"; 4 | import {Closable} from "../utils/interfaces"; 5 | 6 | const debug = debugFor('syncrow:con:parse_helper'); 7 | const logger = loggerFor('ParseHelper'); 8 | 9 | export class ParseHelper extends EventEmitter implements Closable { 10 | private messageBuffer:string; 11 | 12 | private expectedLength:number; 13 | private static separator = ':'; 14 | private listener:Function; 15 | private id:number; 16 | static events = { 17 | message: 'message', 18 | }; 19 | 20 | /** 21 | * Enables sending string commands between parties 22 | */ 23 | constructor(private socket:Socket) { 24 | super(); 25 | this.listener = (data)=>this.parseData(data); 26 | this.resetBuffers(); 27 | this.socket.on('data', this.listener); 28 | this.id = Math.floor(Math.random() * 10000) 29 | } 30 | 31 | /** 32 | * @param data 33 | * @returns {string} 34 | */ 35 | public writeMessage(data:string) { 36 | debug(`${this.id} writing message: ${data}`); 37 | this.socket.write(`${data.length}${ParseHelper.separator}${data}`); 38 | } 39 | 40 | /** 41 | * Removes own listener from socket 42 | */ 43 | public shutdown() { 44 | debug(`${this.id} removing listeners`); 45 | const previous = this.socket.listenerCount('data'); 46 | 47 | this.socket.removeListener('data', this.listener); 48 | 49 | if (this.socket.listenerCount('data') >= previous) { 50 | logger.warn('shutdown failed - listener not removed - transfer data leak risk') 51 | } 52 | 53 | } 54 | 55 | private parseData(data:Buffer) { 56 | this.messageBuffer += data.toString(); 57 | if (this.expectedLength === null) { 58 | this.checkIfExpectedLengthArrived(); 59 | } 60 | this.checkIfMessageIsComplete(); 61 | } 62 | 63 | private resetBuffers() { 64 | this.messageBuffer = ''; 65 | this.expectedLength = null; 66 | } 67 | 68 | private checkIfExpectedLengthArrived() { 69 | const indexOfContentLengthHeaderSeparator = this.messageBuffer.indexOf(ParseHelper.separator); 70 | 71 | if (indexOfContentLengthHeaderSeparator !== -1) { 72 | this.expectedLength = parseInt(this.messageBuffer.slice(0, indexOfContentLengthHeaderSeparator)); 73 | this.messageBuffer = this.messageBuffer.slice(indexOfContentLengthHeaderSeparator + 1); 74 | } 75 | } 76 | 77 | private checkIfMessageIsComplete() { 78 | if (this.expectedLength && this.messageBuffer.length >= this.expectedLength) { 79 | const message = this.messageBuffer.slice(0, this.expectedLength); 80 | debug(`${this.id} got message: ${message}`); 81 | this.emit(ParseHelper.events.message, message); 82 | this.restartParsingMessage(this.messageBuffer.slice(this.expectedLength)); 83 | } 84 | } 85 | 86 | private restartParsingMessage(remainder:string) { 87 | this.resetBuffers(); 88 | this.messageBuffer = remainder; 89 | this.checkIfExpectedLengthArrived(); 90 | this.checkIfMessageIsComplete(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/facade/client.ts: -------------------------------------------------------------------------------- 1 | import {FileContainer, FilterFunction} from "../fs_helpers/file_container"; 2 | import {ConnectionHelper} from "../connection/connection_helper"; 3 | import {TransferHelper} from "../transport/transfer_helper"; 4 | import {Engine} from "../core/engine"; 5 | import {SyncAction} from "../sync/sync_actions"; 6 | import {EventMessenger} from "../connection/event_messenger"; 7 | import * as async from "async"; 8 | import {debugFor} from "../utils/logger"; 9 | import ConstantConnector from "../connection/constant_connector"; 10 | import DynamicConnector from "../connection/dynamic_connector"; 11 | import {Closable} from "../utils/interfaces"; 12 | 13 | const debug = debugFor('syncrow:facade:client'); 14 | 15 | const AUTH_TIMEOUT = 10; 16 | 17 | /** 18 | * @param params 19 | * @param callback 20 | */ 21 | export default class Client implements Closable { 22 | private authTimeout:number; 23 | private container:FileContainer; 24 | private connectionHelperEntry:ConnectionHelper; 25 | private connectionHelperForTransfer:ConnectionHelper; 26 | private transferHelper:TransferHelper; 27 | public engine:Engine; 28 | 29 | constructor(private params:{ 30 | path:string, 31 | remotePort:number, 32 | remoteHost:string, 33 | 34 | authTimeout?:number, 35 | filter?:FilterFunction, 36 | initialToken?:string, 37 | authenticate?:boolean, 38 | sync?:SyncAction, 39 | watch?:boolean, 40 | retry?:{ 41 | times:number, 42 | interval:number 43 | }}) { 44 | 45 | this.authTimeout = params.authTimeout ? params.authTimeout : AUTH_TIMEOUT; 46 | this.initializeHelpers(); 47 | } 48 | 49 | public start(callback) { 50 | debug(`starting the connect flow`); 51 | 52 | return async.waterfall( 53 | [ 54 | (cb)=> { 55 | if (this.params.watch) return this.container.beginWatching(cb); 56 | 57 | return setImmediate(cb); 58 | }, 59 | 60 | (cb)=> { 61 | if (this.params.retry) { 62 | return async.retry(this.params.retry, 63 | (retryCallback)=>this.connectionHelperEntry.getNewSocket({}, retryCallback), 64 | cb 65 | ); 66 | } 67 | 68 | return this.connectionHelperEntry.getNewSocket({}, cb) 69 | }, 70 | 71 | 72 | (socket, cb)=> { 73 | debug(`initial connection obtained`); 74 | 75 | const eventMessenger = new EventMessenger(socket); 76 | this.engine.addOtherPartyMessenger(eventMessenger); 77 | 78 | if (this.params.retry) { 79 | this.connectAgainAfterPreviousDied(eventMessenger, this.engine, this.connectionHelperEntry); 80 | } 81 | 82 | return setImmediate(cb); 83 | } 84 | ], 85 | callback 86 | ); 87 | } 88 | 89 | /** 90 | */ 91 | public shutdown() { 92 | this.engine.shutdown(); 93 | this.connectionHelperEntry.shutdown(); 94 | this.connectionHelperForTransfer.shutdown(); 95 | this.container.shutdown(); 96 | } 97 | 98 | private initializeHelpers() { 99 | this.container = new FileContainer(this.params.path, {filter: this.params.filter}); 100 | 101 | this.connectionHelperEntry = new ConstantConnector(this.authTimeout, 102 | this.params.remoteHost, 103 | this.params.remotePort, 104 | this.params.initialToken); 105 | 106 | this.connectionHelperForTransfer = new DynamicConnector(this.authTimeout); 107 | 108 | this.transferHelper = new TransferHelper(this.container, this.connectionHelperForTransfer, { 109 | name: 'ConnectingEngine', 110 | preferConnecting: true 111 | }); 112 | 113 | this.engine = new Engine(this.container, this.transferHelper, {sync: this.params.sync}); 114 | } 115 | 116 | private connectAgainAfterPreviousDied(previousMessenger:EventMessenger, engine:Engine, connectionHelper:ConnectionHelper) { 117 | return previousMessenger.once(EventMessenger.events.died, ()=> { 118 | 119 | debug(`obtaining new socket`); 120 | 121 | return connectionHelper.getNewSocket({}, (err, socket)=> { 122 | if (err) { 123 | return engine.emit(Engine.events.error, err); 124 | } 125 | 126 | const eventMessenger = new EventMessenger(socket); 127 | engine.addOtherPartyMessenger(eventMessenger); 128 | 129 | this.connectAgainAfterPreviousDied(eventMessenger, engine, connectionHelper); 130 | } 131 | ) 132 | } 133 | ) 134 | } 135 | } -------------------------------------------------------------------------------- /src/facade/server.ts: -------------------------------------------------------------------------------- 1 | import {FilterFunction, FileContainer} from "../fs_helpers/file_container"; 2 | import {ConnectionHelper} from "../connection/connection_helper"; 3 | import {TransferHelper} from "../transport/transfer_helper"; 4 | import {SyncAction} from "../sync/sync_actions"; 5 | import {EventMessenger} from "../connection/event_messenger"; 6 | import {EventEmitter} from "events"; 7 | import * as async from "async"; 8 | import ConstantServer from "../connection/constant_server"; 9 | import DynamicServer from "../connection/dynamic_server"; 10 | import {debugFor} from "../utils/logger"; 11 | import {Closable, ErrorCallback} from "../utils/interfaces"; 12 | import {Engine} from "../core/engine"; 13 | 14 | const debug = debugFor('syncrow:facade:server'); 15 | const AUTH_TIMEOUT = 100; 16 | 17 | /** 18 | * @param params 19 | * @param {EngineCallback} callback 20 | */ 21 | export default class Server extends EventEmitter implements Closable { 22 | 23 | public static events = { 24 | /** 25 | * @event emitted when remote party connects 26 | */ 27 | connection: 'connection' 28 | }; 29 | 30 | public engine:Engine; 31 | 32 | private authTimeout:number; 33 | private container:FileContainer; 34 | private connectionHelperEntry:ConstantServer; 35 | private transferHelper:TransferHelper; 36 | private connectionHelperForTransfer:ConnectionHelper; 37 | 38 | constructor(private params:{ 39 | path:string, 40 | localPort:number, 41 | externalHost:string, 42 | 43 | authTimeout?:number, 44 | filter?:FilterFunction 45 | initialToken?:string, 46 | authenticate?:boolean, 47 | sync?:SyncAction, 48 | watch?:boolean}) { 49 | super(); 50 | this.authTimeout = params.authTimeout ? params.authTimeout : AUTH_TIMEOUT; 51 | 52 | this.initializeHelpers(); 53 | } 54 | 55 | public start(callback:ErrorCallback) { 56 | debug(`starting the initialization flow`); 57 | 58 | return async.waterfall( 59 | [ 60 | (cb)=> { 61 | if (this.params.watch)return this.container.beginWatching(cb); 62 | return setImmediate(cb); 63 | }, 64 | 65 | (cb)=>this.connectionHelperEntry.listen(cb), 66 | 67 | (cb)=> { 68 | this.listenForMultipleConnections(this.engine, this.connectionHelperEntry); 69 | return setImmediate(cb); 70 | } 71 | ], 72 | callback 73 | ); 74 | } 75 | 76 | public shutdown() { 77 | this.engine.shutdown(); 78 | this.connectionHelperEntry.shutdown(); 79 | this.connectionHelperForTransfer.shutdown(); 80 | this.container.shutdown(); 81 | }; 82 | 83 | private initializeHelpers() { 84 | this.container = new FileContainer(this.params.path, {filter: this.params.filter}); 85 | 86 | this.connectionHelperEntry = new ConstantServer(this.params.localPort, 87 | { 88 | authTimeout: this.authTimeout, 89 | constantToken: this.params.initialToken 90 | } 91 | ); 92 | 93 | this.connectionHelperForTransfer = new DynamicServer({ 94 | authTimeout: this.authTimeout, 95 | generateToken: this.params.authenticate 96 | }, 97 | this.params.externalHost 98 | ); 99 | 100 | this.transferHelper = new TransferHelper(this.container, this.connectionHelperForTransfer, 101 | { 102 | name: 'ListeningEngine', 103 | preferConnecting: false 104 | } 105 | ); 106 | 107 | this.engine = new Engine(this.container, this.transferHelper, {sync: this.params.sync}); 108 | } 109 | 110 | private listenForMultipleConnections(engine:Engine, helper:ConnectionHelper) { 111 | return async.whilst( 112 | ()=>true, 113 | 114 | (cb)=> { 115 | return helper.getNewSocket({}, (err, socket)=> { 116 | if (err) { 117 | engine.emit(Engine.events.error, err); 118 | return cb(); 119 | } 120 | 121 | const eventMessenger = new EventMessenger(socket); 122 | this.emit(Server.events.connection, eventMessenger); 123 | engine.addOtherPartyMessenger(eventMessenger); 124 | return cb(); 125 | }) 126 | }, 127 | 128 | (err)=> { 129 | if (err) engine.emit(Engine.events.error, err); 130 | } 131 | ) 132 | } 133 | } -------------------------------------------------------------------------------- /src/fs_helpers/file_meta_queue.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | import * as fs from "fs"; 3 | import * as _ from "lodash"; 4 | import * as crypto from "crypto"; 5 | import * as path from "path"; 6 | import {SyncData} from "../sync/sync_actions"; 7 | import {debugFor} from "../utils/logger"; 8 | 9 | const debug = debugFor('syncrow:fs:file_meta_queue'); 10 | 11 | export class FileMetaComputingQueue { 12 | private queue:AsyncQueue; 13 | private basePath:string; 14 | 15 | /** 16 | * Used for computing SyncData 17 | * @param queueSize 18 | * @param basePath 19 | */ 20 | constructor(queueSize:number, basePath:string) { 21 | this.queue = async.queue((task:Function, callback:Function)=>task(callback), queueSize); 22 | this.basePath = basePath; 23 | } 24 | 25 | /** 26 | * @param fileName 27 | * @param doneCallback 28 | */ 29 | public computeFileMeta(fileName:string, doneCallback:(err:Error, syncData?:SyncData)=>any) { 30 | debug(`computing file meta for file: ${fileName}`); 31 | 32 | const job = (callback)=> { 33 | 34 | const result = { 35 | name: fileName, 36 | exists: false, 37 | modified: null, 38 | isDirectory: false, 39 | hashCode: '' 40 | }; 41 | 42 | async.waterfall( 43 | [ 44 | (waterfallCallback)=>this.checkIfExistsAndIsDirectory(result, waterfallCallback), 45 | (partial, waterfallCallback)=>this.computeHashForFile(partial, waterfallCallback) 46 | ], (err, result?:SyncData)=> { 47 | if (err) { 48 | doneCallback(err); 49 | return callback(); 50 | } 51 | 52 | doneCallback(null, result); 53 | return callback(); 54 | } 55 | ); 56 | 57 | }; 58 | 59 | this.queue.push(job); 60 | } 61 | 62 | private checkIfExistsAndIsDirectory(syncData:SyncData, callback:(error, syncData?:SyncData)=>any) { 63 | fs.stat(path.join(this.basePath, syncData.name), (err, stats:fs.Stats)=> { 64 | if (err) { 65 | syncData.exists = false; 66 | return callback(null, syncData); 67 | } 68 | 69 | syncData.exists = true; 70 | syncData.modified = stats.mtime; 71 | if (stats.isDirectory()) { 72 | syncData.isDirectory = true; 73 | } 74 | 75 | return callback(null, syncData); 76 | }); 77 | } 78 | 79 | 80 | private computeHashForFile(syncData:SyncData, callback:(error, syncData?:SyncData)=>any) { 81 | callback = _.once(callback); 82 | 83 | if (!FileMetaComputingQueue.shouldComputeHash(syncData)) { 84 | return callback(null, syncData); 85 | } 86 | 87 | const hash = crypto.createHash('sha256'); 88 | const stream = fs.createReadStream(path.join(this.basePath, syncData.name)).pipe(hash); 89 | 90 | hash.on('error', callback); 91 | 92 | hash.on('finish', ()=> { 93 | syncData.hashCode = hash.read().toString('hex'); 94 | callback(null, syncData); 95 | }); 96 | } 97 | 98 | private static shouldComputeHash(syncData:SyncData) { 99 | return (syncData.exists && !syncData.isDirectory) 100 | } 101 | } -------------------------------------------------------------------------------- /src/fs_helpers/path_helper.ts: -------------------------------------------------------------------------------- 1 | import * as upath from "upath"; 2 | import * as path from "path"; 3 | import * as ignore from "ignore"; 4 | import {FilterFunction} from "./file_container"; 5 | import {debugFor} from "../utils/logger"; 6 | const debug = debugFor('syncrow:fs:path_helper'); 7 | 8 | 9 | export class PathHelper { 10 | 11 | /** 12 | * Returns path that is always in UNIX format regardless of origin OS 13 | * @param suspect 14 | * @returns {string} 15 | */ 16 | public static normalizePath(suspect:string):string { 17 | return upath.normalize(suspect); 18 | } 19 | 20 | /** 21 | * @param filterStrings 22 | * @param baseDir 23 | */ 24 | public static createFilterFunction(filterStrings:Array, baseDir:string):FilterFunction { 25 | const absolute = path.resolve(baseDir); 26 | 27 | debug(`creating a filter function with paths: ${filterStrings} and absolute path: ${absolute}`); 28 | 29 | const filter = ignore().add(filterStrings.map(p=>PathHelper.normalizePath(p))).createFilter(); 30 | 31 | return (s:string, stats?:any) => { 32 | if (s === absolute) { 33 | return false; 34 | } 35 | 36 | const relative = path.relative(absolute, s); 37 | 38 | const result = !filter(relative); 39 | return result; 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /src/fs_helpers/read_tree.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | import * as fs from "fs"; 3 | import {Stats} from "fs"; 4 | import * as path from "path"; 5 | 6 | export interface ReadTreeOptions { 7 | filter?:(s:string)=>boolean; 8 | onlyFiles?:boolean 9 | } 10 | 11 | //TODO add docs 12 | export function readTree(root:string, options:ReadTreeOptions, 13 | callback:(err:Error, result:Array)=>any) { 14 | const filter = options.filter ? options.filter : s=> false; 15 | let results = []; 16 | let stack = [root]; 17 | 18 | async.whilst(shouldFinish, (whilstCallback)=> { 19 | let currentDir = stack.pop(); 20 | 21 | if (filter(currentDir)) { 22 | return whilstCallback(); 23 | } 24 | 25 | if (!options.onlyFiles && currentDir != root) { 26 | addToResults(currentDir); 27 | } 28 | 29 | fs.readdir(currentDir, (err, files)=> { 30 | if (err) return whilstCallback(err); 31 | processListOfFiles(currentDir, files, whilstCallback); 32 | }) 33 | 34 | }, (err)=> { 35 | callback(err, results); 36 | }); 37 | 38 | function shouldFinish() { 39 | return stack.length !== 0; 40 | } 41 | 42 | function addToResults(pathToAdd:string) { 43 | if (!filter(pathToAdd)) { 44 | results.push(path.relative(root, pathToAdd)); 45 | } 46 | } 47 | 48 | function processListOfFiles(currentDir:string, fileList:Array, callback) { 49 | async.mapSeries(fileList, (file, seriesCallback:(err?)=>any)=> { 50 | let suspectFile = connectPaths(currentDir, file); 51 | 52 | fs.stat(suspectFile, (err, stat:Stats)=> { 53 | if (err) return seriesCallback(err); 54 | 55 | if (stat.isDirectory()) { 56 | stack.push(suspectFile); 57 | } else if (suspectFile != root) { 58 | addToResults(suspectFile) 59 | } 60 | 61 | seriesCallback(); 62 | }); 63 | 64 | }, callback) 65 | } 66 | 67 | function connectPaths(directory:string, file:string):string { 68 | return path.join(directory, file) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/references.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /src/sync/generic_commands_action.ts: -------------------------------------------------------------------------------- 1 | import {SyncActionParams, MetaTuple, getFileLists, FileLists, getMetaTupleForFile} from "./sync_actions"; 2 | import * as async from "async"; 3 | import * as _ from "lodash"; 4 | import {debugFor} from "../utils/logger"; 5 | 6 | const debug = debugFor('syncrow:sync'); 7 | 8 | export interface CommandsFunction { 9 | (params:SyncActionParams, metaTuple:MetaTuple, callback:ErrorCallback):any; 10 | } 11 | 12 | const RETRY = 1; 13 | 14 | /** 15 | * Used to create actions that process the synchronization in context of one file 16 | * All files will be synced in parallel 17 | * 18 | * @param params 19 | * @param callback 20 | * @param commandsFunction 21 | */ 22 | export function genericCommandsAction(params:SyncActionParams, callback:ErrorCallback, commandsFunction:CommandsFunction):any { 23 | return async.waterfall( 24 | [ 25 | (cb)=> { 26 | getFileLists(params, cb) 27 | }, 28 | (list, cb)=>processFileLists(params, list, cb, commandsFunction) 29 | ], 30 | 31 | callback 32 | ) 33 | } 34 | 35 | function processFileLists(params:SyncActionParams, lists:FileLists, callback:ErrorCallback, commandsFunction:CommandsFunction) { 36 | debug(`local files: ${lists.localList} remote: ${lists.remoteList}`); 37 | const combined = _.union(lists.localList, lists.remoteList); 38 | 39 | return async.each(combined, 40 | 41 | (file, cb)=>async.retry(RETRY, (innerCallback)=>processFile(params, file, innerCallback, commandsFunction), cb), 42 | 43 | callback 44 | ) 45 | } 46 | 47 | function processFile(params:SyncActionParams, file:string, callback:ErrorCallback, commandsFunction:CommandsFunction) { 48 | return async.waterfall( 49 | [ 50 | (cb)=>async.retry(RETRY, (innerCallback)=>getMetaTupleForFile(params, file, innerCallback), cb), 51 | 52 | (metaTuple, cb)=>async.retry(RETRY, (innerCallback)=>commandsFunction(params, metaTuple, innerCallback), cb) 53 | ], 54 | callback 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/sync/no_action.ts: -------------------------------------------------------------------------------- 1 | import {SyncActionParams} from "./sync_actions"; 2 | import {loggerFor} from "../utils/logger"; 3 | 4 | const logger = loggerFor('NoActionStrategy'); 5 | 6 | /** 7 | * Nothing will happen 8 | * 9 | * @param params 10 | * @param callback 11 | */ 12 | export function noAction(params:SyncActionParams, callback:ErrorCallback) { 13 | logger.info(`no action needed`); 14 | return callback(); 15 | } -------------------------------------------------------------------------------- /src/sync/pull_action.ts: -------------------------------------------------------------------------------- 1 | import {debugFor, loggerFor} from "../utils/logger"; 2 | import {SyncActionParams, MetaTuple} from "./sync_actions"; 3 | import {genericCommandsAction} from "./generic_commands_action"; 4 | 5 | const debug = debugFor('syncrow:sync:pull'); 6 | const logger = loggerFor('PullAction'); 7 | 8 | 9 | /** 10 | * It will download all files from remote. 11 | * 12 | * @param params 13 | * @param callback 14 | */ 15 | export function pullAction(params:SyncActionParams, callback:ErrorCallback):any { 16 | debug(`starting pull action`); 17 | 18 | return genericCommandsAction(params, callback, issueCommands) 19 | } 20 | 21 | 22 | function issueCommands(params:SyncActionParams, metaTuple:MetaTuple, callback:ErrorCallback) { 23 | if (metaTuple.remoteMeta.exists && !metaTuple.localMeta.exists) { 24 | if (metaTuple.remoteMeta.isDirectory) { 25 | return params.container.createDirectory(metaTuple.localMeta.name, callback); 26 | } 27 | 28 | return params.subject.requestRemoteFile(params.remoteParty, metaTuple.localMeta.name, callback); 29 | } 30 | 31 | if (!metaTuple.remoteMeta.exists && metaTuple.localMeta.exists) { 32 | if (params.deleteLocalIfRemoteMissing) { 33 | debug(`File: ${metaTuple.localMeta.name} exists locally but does not remotely - deleted`); 34 | return params.container.deleteFile(metaTuple.localMeta.name, callback); 35 | } 36 | 37 | debug(`File: ${metaTuple.localMeta.name} exists locally but does not remotely - it will be ignored`); 38 | return setImmediate(callback); 39 | } 40 | 41 | if (metaTuple.remoteMeta.exists && metaTuple.localMeta.exists) { 42 | if (metaTuple.localMeta.isDirectory) { 43 | return setImmediate(callback); 44 | } 45 | 46 | if (metaTuple.localMeta.hashCode === metaTuple.remoteMeta.hashCode) { 47 | return setImmediate(callback); 48 | } 49 | 50 | return params.subject.requestRemoteFile(params.remoteParty, metaTuple.localMeta.name, callback); 51 | } 52 | 53 | logger.warn(`File ${metaTuple.localMeta.name} - does not exist locally or remotely`); 54 | 55 | return setImmediate(callback); 56 | } 57 | -------------------------------------------------------------------------------- /src/sync/push_action.ts: -------------------------------------------------------------------------------- 1 | import {SyncActionParams, MetaTuple} from "./sync_actions"; 2 | import {debugFor, loggerFor} from "../utils/logger"; 3 | import {genericCommandsAction} from "./generic_commands_action"; 4 | 5 | const debug = debugFor('syncrow:sync:push'); 6 | const logger = loggerFor('PushAction'); 7 | 8 | 9 | /** 10 | * It will push all files to remote. 11 | * 12 | * @param params 13 | * @param callback 14 | */ 15 | export function pushAction(params:SyncActionParams, callback:ErrorCallback):any { 16 | debug(`starting push action`); 17 | 18 | return genericCommandsAction(params, callback, issueCommands) 19 | } 20 | 21 | 22 | function issueCommands(params:SyncActionParams, metaTuple:MetaTuple, callback:ErrorCallback) { 23 | if (metaTuple.localMeta.exists && !metaTuple.remoteMeta.exists) { 24 | if (metaTuple.localMeta.isDirectory) { 25 | return params.subject.createRemoteDirectory(params.remoteParty, metaTuple.localMeta.name, callback); 26 | } 27 | 28 | return params.subject.pushFileToRemote(params.remoteParty, metaTuple.localMeta.name, callback); 29 | } 30 | 31 | if (!metaTuple.localMeta.exists && metaTuple.remoteMeta.exists) { 32 | if (params.deleteRemoteIfLocalMissing) { 33 | return params.subject.deleteRemoteFile(params.remoteParty, metaTuple.localMeta.name, callback); 34 | } 35 | 36 | debug(`File: ${metaTuple.localMeta.name} exists locally but does not remotely - it will be ignored`); 37 | return setImmediate(callback); 38 | } 39 | 40 | if (metaTuple.remoteMeta.exists && metaTuple.localMeta.exists) { 41 | if (metaTuple.localMeta.isDirectory) { 42 | return setImmediate(callback); 43 | } 44 | 45 | if (metaTuple.localMeta.hashCode === metaTuple.remoteMeta.hashCode) { 46 | return setImmediate(callback); 47 | } 48 | 49 | return params.subject.pushFileToRemote(params.remoteParty, metaTuple.localMeta.name, callback); 50 | } 51 | 52 | logger.warn(`File ${metaTuple.localMeta.name} - does not exist locally or remotely`); 53 | 54 | return setImmediate(callback); 55 | } 56 | -------------------------------------------------------------------------------- /src/sync/sync_actions.ts: -------------------------------------------------------------------------------- 1 | import {FileContainer} from "../fs_helpers/file_container"; 2 | import {EventMessenger} from "../connection/event_messenger"; 3 | import * as async from "async"; 4 | import {debugFor} from "../utils/logger"; 5 | 6 | const debug = debugFor('syncrow:sync_actions'); 7 | 8 | export interface SyncData { 9 | hashCode:string; 10 | modified:Date; 11 | name:string; 12 | isDirectory:boolean; 13 | exists:boolean; 14 | } 15 | 16 | export interface SyncActionSubject { 17 | getRemoteFileMeta(otherParty:EventMessenger, fileName:string, callback:(err:Error, syncData?:SyncData)=>any):any; 18 | getRemoteFileList(otherParty:EventMessenger, callback:(err:Error, fileList?:Array)=>any):any; 19 | requestRemoteFile(otherParty:EventMessenger, fileName:string, callback:ErrorCallback):any; 20 | pushFileToRemote(otherParty:EventMessenger, fileName:string, callback:ErrorCallback):any; 21 | deleteRemoteFile(otherParty:EventMessenger, fileName:string, callback:ErrorCallback):any; 22 | createRemoteDirectory(otherParty:EventMessenger, fileName:string, callback:ErrorCallback):any; 23 | getRemoteParties():Array; 24 | } 25 | 26 | export interface SyncActionParams { 27 | remoteParty:EventMessenger; 28 | container:FileContainer; 29 | subject:SyncActionSubject; 30 | deleteLocalIfRemoteMissing?:boolean; 31 | deleteRemoteIfLocalMissing?:boolean; 32 | } 33 | 34 | export interface SyncAction { 35 | (params:SyncActionParams, callback:ErrorCallback):any; 36 | } 37 | 38 | /** 39 | * @param action 40 | * @returns {(params:SyncActionParams, callback:ErrorCallback)=>any} 41 | */ 42 | export function setDeleteLocalFiles(action:SyncAction):SyncAction { 43 | 44 | return (params:SyncActionParams, callback:ErrorCallback)=> { 45 | params.deleteLocalIfRemoteMissing = true; 46 | return action(params, callback); 47 | } 48 | 49 | } 50 | 51 | /** 52 | * @param action 53 | * @returns {(params:SyncActionParams, callback:ErrorCallback)=>any} 54 | */ 55 | export function setDeleteRemoteFiles(action:SyncAction):SyncAction { 56 | 57 | return (params:SyncActionParams, callback:ErrorCallback)=> { 58 | params.deleteRemoteIfLocalMissing = true; 59 | return action(params, callback); 60 | } 61 | 62 | } 63 | 64 | export interface MetaTuple { 65 | localMeta?:SyncData; 66 | remoteMeta?:SyncData; 67 | } 68 | 69 | /** 70 | * @param params 71 | * @param file 72 | * @param callback 73 | */ 74 | export function getMetaTupleForFile(params:SyncActionParams, file:string, callback:(err:Error, result:MetaTuple)=>any):any { 75 | debug(`getting file meta from both remote and local: ${file}`); 76 | 77 | async.parallel( 78 | { 79 | localMeta: (parallelCallback)=> { 80 | params.container.getFileMeta(file, parallelCallback) 81 | }, 82 | remoteMeta: (parallelCallback)=> { 83 | params.subject.getRemoteFileMeta(params.remoteParty, file, parallelCallback); 84 | } 85 | }, 86 | 87 | callback 88 | ); 89 | } 90 | 91 | export interface FileLists { 92 | localList?:Array; 93 | remoteList?:Array; 94 | } 95 | 96 | 97 | /** 98 | * @param params 99 | * @param callback 100 | */ 101 | export function getFileLists(params:SyncActionParams, callback:(err:Error, result:FileLists)=>any) { 102 | 103 | return async.parallel( 104 | { 105 | localList: (cb)=>params.container.getFileTree(cb), 106 | remoteList: (cb)=>params.subject.getRemoteFileList(params.remoteParty, cb) 107 | }, 108 | 109 | callback 110 | ); 111 | } 112 | 113 | -------------------------------------------------------------------------------- /src/test/cli/cli_test.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | import * as path from "path"; 3 | import * as child_process from "child_process"; 4 | import {createPathSeries, removePath, compareDirectories, getRandomString} from "../../utils/fs_test_utils"; 5 | 6 | const START_TIMEOUT = 1000; //Should not take longer than 1 second 7 | const TEST_DIR = path.join(__dirname, 'cli_test'); 8 | 9 | const configurationClient = { 10 | "listen": false, 11 | "remoteHost": "127.0.0.1", 12 | "remotePort": 2510, 13 | "rawFilter": [], 14 | "rawStrategy": "no", 15 | "initialToken": "fd5be39f9524e746b958d965e1f77bf6105425b017884c88767397c8d90b22f4", 16 | "advanced": true, 17 | "authenticate": true, 18 | "watch": true, 19 | "interval": 10000, 20 | "times": 18 21 | }; 22 | 23 | const configurationServer = { 24 | "listen": true, 25 | "localPort": 2510, 26 | "externalHost": "127.0.0.1", 27 | "rawFilter": [], 28 | "rawStrategy": "pull", 29 | "initialToken": "fd5be39f9524e746b958d965e1f77bf6105425b017884c88767397c8d90b22f4", 30 | "advanced": true, 31 | "deleteLocal": true, 32 | "authenticate": true, 33 | "watch": true 34 | }; 35 | 36 | let client; 37 | let server; 38 | 39 | describe('CLI', function () { 40 | this.timeout(START_TIMEOUT + 1000); 41 | beforeEach((done)=> { 42 | return async.series( 43 | [ 44 | (cb)=>removePath(TEST_DIR, cb), 45 | 46 | (cb)=>createPathSeries( 47 | [ 48 | {path: path.join(TEST_DIR, 'client_dir'), directory: true}, 49 | { 50 | path: path.join(TEST_DIR, 'client_dir', '.syncrow.json'), 51 | content: JSON.stringify(configurationClient) 52 | }, 53 | {path: path.join(TEST_DIR, 'client_dir', 'aaa.txt'), content: getRandomString(4000)}, 54 | {path: path.join(TEST_DIR, 'client_dir', 'bbb.txt'), content: getRandomString(4000)}, 55 | {path: path.join(TEST_DIR, 'client_dir', 'ccc.txt'), content: getRandomString(4000)}, 56 | 57 | {path: path.join(TEST_DIR, 'server_dir'), directory: true}, 58 | { 59 | path: path.join(TEST_DIR, 'server_dir', '.syncrow.json'), 60 | content: JSON.stringify(configurationServer) 61 | }, 62 | ], 63 | cb 64 | ), 65 | 66 | (cb)=> { 67 | server = child_process.spawn('../../../../../bin/syncrow', ['run'], {cwd: path.join(TEST_DIR, 'server_dir')}); 68 | 69 | return setTimeout(cb, 100); //Give server some time to start 70 | }, 71 | 72 | (cb)=> { 73 | client = child_process.spawn('../../../../../bin/syncrow', ['run'], {cwd: path.join(TEST_DIR, 'client_dir')}); 74 | 75 | return setTimeout(cb, START_TIMEOUT); 76 | }, 77 | 78 | (cb)=>removePath(path.join(TEST_DIR, 'client_dir', '.syncrow.json'), cb), 79 | 80 | (cb)=>removePath(path.join(TEST_DIR, 'server_dir', '.syncrow.json'), cb) 81 | ], 82 | done 83 | ) 84 | }); 85 | 86 | after((done)=> { 87 | client.kill('SIGKILL'); 88 | server.kill('SIGKILL'); 89 | 90 | return removePath(TEST_DIR, done); 91 | }); 92 | 93 | it('will start and synchronize two directories', (done)=> { 94 | return compareDirectories(path.join(TEST_DIR, 'client_dir'), path.join(TEST_DIR, 'server_dir'), done) 95 | }); 96 | }); 97 | 98 | -------------------------------------------------------------------------------- /src/test/connection/authorisation_helper_test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import {AuthorisationHelper} from "../../connection/authorisation_helper"; 3 | import {obtainTwoSockets} from "../../utils/fs_test_utils"; 4 | import * as assert from "assert"; 5 | import * as async from "async"; 6 | 7 | 8 | describe('AuthorisationHelper', ()=> { 9 | let result; 10 | let pairedShutDown; 11 | 12 | beforeEach((done)=> { 13 | pairedShutDown = obtainTwoSockets((err, twoSockets)=> { 14 | if (err)return done(err); 15 | 16 | result = twoSockets; 17 | return done(); 18 | }); 19 | }); 20 | 21 | afterEach(()=> { 22 | pairedShutDown(); 23 | }); 24 | 25 | it('if both sides use the same token the authorisation will be positive', (done) => { 26 | const token = '92cu1is9810sajk'; 27 | 28 | return async.parallel( 29 | [ 30 | (cb)=>AuthorisationHelper.authorizeAsServer(result.server, token, {timeout: 100}, cb), 31 | (cb)=>AuthorisationHelper.authorizeAsClient(result.client, token, {timeout: 20}, cb) 32 | ], 33 | (err)=> { 34 | if (err)return done(err); 35 | 36 | assert.equal(result.server.listenerCount('data'), 0, 'all server "data" listeners have been removed'); 37 | assert.equal(result.client.listenerCount('data'), 0, 'no client "data" listener have been created'); 38 | 39 | return done(); 40 | }) 41 | }); 42 | 43 | it('if token does not match - the result will be negative', (done) => { 44 | return async.parallel( 45 | [ 46 | (cb)=>AuthorisationHelper.authorizeAsServer(result.server, 'AAA', {timeout: 100}, cb), 47 | 48 | (cb)=>AuthorisationHelper.authorizeAsClient(result.client, 'BBB', {timeout: 30}, cb) 49 | ], 50 | (err)=> { 51 | assert(err, 'authorization failed due to not matching token'); 52 | 53 | assert.equal(result.server.listenerCount('data'), 0, 'all server "data" listeners have been removed'); 54 | assert.equal(result.client.listenerCount('data'), 0, 'no client "data" listener have been created'); 55 | 56 | return done(); 57 | } 58 | ); 59 | 60 | }); 61 | 62 | it('if token is correct but timeout is exceeded on core side - result will be negative', function (done) { 63 | const token = 'SAME_TOKEN'; 64 | 65 | return async.parallel( 66 | [ 67 | (cb)=>AuthorisationHelper.authorizeAsServer(result.server, token, {timeout: 10}, cb), 68 | 69 | (cb)=> { 70 | return setTimeout( 71 | ()=>AuthorisationHelper.authorizeAsClient(result.client, token, {timeout: 30}, cb), 72 | 30 73 | ) 74 | } 75 | ], 76 | (err)=> { 77 | assert(err, 'authorization failed due to timeout'); 78 | 79 | assert.equal(result.server.listenerCount('data'), 0, 'all server "data" listeners have been removed'); 80 | assert.equal(result.client.listenerCount('data'), 0, 'no client "data" listener have been created'); 81 | 82 | return done(); 83 | } 84 | ); 85 | }); 86 | }); -------------------------------------------------------------------------------- /src/test/connection/constant_connector_test.ts: -------------------------------------------------------------------------------- 1 | import ConstantConnector from "../../connection/constant_connector"; 2 | import * as assert from "assert"; 3 | import * as async from "async"; 4 | import * as net from "net"; 5 | 6 | describe('ConstantConnector', function () { 7 | let serverSocket; 8 | let clientSocket; 9 | let connector; 10 | let server; 11 | 12 | afterEach(()=> { 13 | if (serverSocket)serverSocket.destroy(); 14 | if (connector)connector.shutdown(); 15 | if (server)server.close(); 16 | if (clientSocket)clientSocket.destroy(); 17 | }); 18 | 19 | it('can be instantiated, meets the interface', ()=> { 20 | connector = new ConstantConnector(10, 'localhost', 1234, '123312'); 21 | assert(connector.getNewSocket instanceof Function); 22 | assert(connector.shutdown instanceof Function); 23 | }); 24 | 25 | it('when getNewSocket is called it will connect to the remote address', (done)=> { 26 | const port = 4421; 27 | const token = 'mockToken128391nsanjda'; 28 | 29 | connector = new ConstantConnector(10, 'localhost', port, token); 30 | server = net.createServer(s=> { 31 | serverSocket = s 32 | }); 33 | 34 | return async.series([ 35 | (cb)=>server.listen(port, cb), 36 | 37 | (cb)=>connector.getNewSocket({}, cb) 38 | ], 39 | (err, result)=> { 40 | if (err) return done(err); 41 | 42 | clientSocket = result[1]; 43 | assert(serverSocket, 'should have connected'); 44 | 45 | serverSocket.on('data', (data)=> { 46 | assert.equal(data, token, 'sent token should match'); 47 | done(err); 48 | }); 49 | 50 | } 51 | ) 52 | }); 53 | }); -------------------------------------------------------------------------------- /src/test/connection/constant_server_test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as async from "async"; 3 | import * as net from "net"; 4 | import ConstantServer from "../../connection/constant_server"; 5 | 6 | 7 | describe('ConstantServer', function () { 8 | const authTimeout = 100; 9 | const port = 4421; 10 | const token = 'mockToken128391nsanjda'; 11 | 12 | let serverSocket; 13 | let clientSocket; 14 | let connector; 15 | let server; 16 | 17 | afterEach(()=> { 18 | if (serverSocket)serverSocket.destroy(); 19 | if (connector)connector.shutdown(); 20 | if (server)server.close(); 21 | if (clientSocket)clientSocket.destroy(); 22 | }); 23 | 24 | it('can be instantiated, meets the interface', ()=> { 25 | connector = new ConstantServer(port, {constantToken: token, authTimeout: authTimeout}); 26 | assert(connector.getNewSocket instanceof Function); 27 | assert(connector.shutdown instanceof Function); 28 | }); 29 | 30 | describe('listen', ()=> { 31 | it('will start listening on given port', (done)=> { 32 | connector = new ConstantServer(port, {constantToken: token, authTimeout: authTimeout}); 33 | connector.listen(done); 34 | }) 35 | }); 36 | 37 | describe('getSocket', ()=> { 38 | it('after calling listen it will listen on given port', (done)=> { 39 | connector = new ConstantServer(port, {constantToken: token, authTimeout: authTimeout}); 40 | 41 | return async.series([ 42 | (cb)=>connector.listen(cb), 43 | 44 | (cb)=> { 45 | return async.parallel( 46 | [ 47 | (parallelCallback)=>connector.getNewSocket({}, (err, socket)=> { 48 | serverSocket = socket; 49 | parallelCallback(err); 50 | }), 51 | 52 | (parallelCallback)=> { 53 | clientSocket = net.connect(port, ()=> { 54 | clientSocket.write(token); 55 | parallelCallback(); 56 | }) 57 | } 58 | ], 59 | cb 60 | ) 61 | } 62 | ], 63 | (err)=> { 64 | assert(serverSocket, 'should have connected'); 65 | assert(clientSocket, 'should have connected from client'); 66 | done(err); 67 | } 68 | ) 69 | }); 70 | }); 71 | }); -------------------------------------------------------------------------------- /src/test/connection/dynamic_connector_test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as async from "async"; 3 | import * as net from "net"; 4 | import DynamicConnector from "../../connection/dynamic_connector"; 5 | 6 | describe('DynamicConnector', function () { 7 | let serverSocket; 8 | let clientSocket; 9 | let connector; 10 | let server; 11 | 12 | afterEach(()=> { 13 | if (serverSocket)serverSocket.destroy(); 14 | if (connector)connector.shutdown(); 15 | if (server)server.close(); 16 | if (clientSocket)clientSocket.destroy(); 17 | }); 18 | 19 | it('can be instantiated, meets the interface', ()=> { 20 | connector = new DynamicConnector(10); 21 | assert(connector.getNewSocket instanceof Function); 22 | assert(connector.shutdown instanceof Function); 23 | }); 24 | 25 | it('when getNewSocket is called it will connect to the remote address', (done)=> { 26 | const port = 4421; 27 | const token = 'mockToken128391nsanjda'; 28 | 29 | connector = new DynamicConnector(10); 30 | 31 | server = net.createServer(s=> { 32 | serverSocket = s 33 | }); 34 | 35 | return async.series([ 36 | (cb)=>server.listen(port, cb), 37 | 38 | (cb)=>connector.getNewSocket({remoteHost: '127.0.0.1', remotePort: port, token: token}, cb) 39 | ], 40 | (err, result)=> { 41 | if (err) return done(err); 42 | 43 | clientSocket = result[1]; 44 | assert(serverSocket, 'should have connected'); 45 | 46 | serverSocket.on('data', (data)=> { 47 | assert.equal(data, token, 'sent token should match'); 48 | done(err); 49 | }); 50 | } 51 | ) 52 | }); 53 | }); -------------------------------------------------------------------------------- /src/test/connection/dynamic_server_test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as async from "async"; 3 | import * as net from "net"; 4 | import DynamicServer from "../../connection/dynamic_server"; 5 | 6 | describe('DynamicServer', function () { 7 | let clientSocket; 8 | let connector; 9 | let server; 10 | 11 | afterEach(()=> { 12 | if (connector)connector.shutdown(); 13 | if (server)server.close(); 14 | if (clientSocket)clientSocket.destroy(); 15 | }); 16 | 17 | it('can be instantiated, meets the interface', ()=> { 18 | connector = new DynamicServer({}, 'localhost'); 19 | assert(connector.getNewSocket instanceof Function); 20 | assert(connector.shutdown instanceof Function); 21 | }); 22 | 23 | describe('getNewSocket', function () { 24 | it('it will call the listenCallback with address that it is listening on, and then expect connection', (done)=> { 25 | const token = 'mockToken128391nsanjda'; 26 | 27 | connector = new DynamicServer({constantToken: token, authTimeout: 100}, '127.0.0.1'); 28 | 29 | return async.series([ 30 | (cb)=>connector.getNewSocket({ 31 | listenCallback: (address)=> { 32 | 33 | clientSocket = net.connect({ 34 | host: address.remoteHost, 35 | port: address.remotePort 36 | }, ()=> { 37 | clientSocket.write(token); 38 | }); 39 | 40 | } 41 | }, 42 | cb) 43 | ], 44 | done 45 | ) 46 | }); 47 | 48 | it('will fail for invalid token', (done)=> { 49 | const token = 'mockToken128391nsanjda'; 50 | 51 | connector = new DynamicServer({constantToken: token, authTimeout: 100}, '127.0.0.1'); 52 | 53 | return async.series([ 54 | (cb)=>connector.getNewSocket({ 55 | listenCallback: (address)=> { 56 | 57 | clientSocket = net.connect({ 58 | host: address.remoteHost, 59 | port: address.remotePort 60 | }, ()=> { 61 | clientSocket.write('anotherToken'); 62 | }); 63 | 64 | } 65 | }, 66 | cb) 67 | ], 68 | (err)=> { 69 | assert(err, 'Should be an authorisation error'); 70 | 71 | return done(); 72 | } 73 | ) 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/test/engine_test.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | import { 3 | createPathSeries, 4 | removePath, 5 | getRandomString, 6 | compareDirectories, 7 | createPath, 8 | pathExists 9 | } from "../utils/fs_test_utils"; 10 | import {Engine} from "../core/engine"; 11 | import * as mkdirp from "mkdirp"; 12 | import * as fs from "fs"; 13 | import {EventCounter} from "../utils/event_counter"; 14 | import * as assert from "assert"; 15 | import SListen from "../facade/server"; 16 | import SConnect from "../facade/client"; 17 | import * as path from "path"; 18 | 19 | const FS_TIMEOUT = 400; 20 | const TEST_DIR = path.join(__dirname, 'engine_test'); 21 | 22 | describe('Engine', function () { 23 | 24 | let listeningEngine; 25 | let connectingEngine; 26 | let counter:EventCounter; 27 | 28 | const token = '121cb2897o1289nnjos'; 29 | const port = 4321; 30 | 31 | beforeEach((done)=> { 32 | return async.waterfall( 33 | [ 34 | (cb)=>removePath(TEST_DIR, cb), 35 | 36 | (cb)=>createPathSeries( 37 | [ 38 | {path: `${TEST_DIR}/aaa`, directory: true}, 39 | {path: `${TEST_DIR}/bbb`, directory: true} 40 | ], 41 | cb 42 | ), 43 | 44 | (cb)=> { 45 | listeningEngine = new SListen({ 46 | path: `${TEST_DIR}/aaa`, 47 | localPort: port, 48 | authenticate: true, 49 | externalHost: '127.0.0.1', 50 | initialToken: token, 51 | watch: true 52 | }); 53 | return listeningEngine.start(cb) 54 | }, 55 | 56 | (cb)=> { 57 | connectingEngine = new SConnect({ 58 | path: `${TEST_DIR}/bbb`, 59 | remotePort: port, 60 | remoteHost: '127.0.0.1', 61 | authenticate: true, 62 | initialToken: token, 63 | watch: true, 64 | }); 65 | return connectingEngine.start(cb); 66 | }, 67 | ], 68 | done 69 | ) 70 | }); 71 | 72 | afterEach((done)=> { 73 | if (listeningEngine)listeningEngine.shutdown(); 74 | if (connectingEngine)connectingEngine.shutdown(); 75 | 76 | return removePath(TEST_DIR, done); 77 | }); 78 | 79 | 80 | it('two engines will transfer new file and and create new directory when needed', function (done) { 81 | counter = new EventCounter(connectingEngine.engine, [ 82 | {name: Engine.events.newDirectory, count: 1}, 83 | {name: Engine.events.newFile, count: 1} 84 | ]); 85 | 86 | async.series( 87 | [ 88 | (cb)=> mkdirp(`${TEST_DIR}/aaa/directory`, cb), //TODO cleanup 89 | 90 | (cb)=>createPath(`${TEST_DIR}/aaa/file.txt`, getRandomString(4000), false, cb), 91 | 92 | (cb)=> { 93 | if (counter.hasFinished()) return setImmediate(cb); 94 | 95 | return counter.on(EventCounter.events.done, cb); 96 | } 97 | ], 98 | 99 | (err)=> { 100 | assert(pathExists(`${TEST_DIR}/bbb/file.txt`), 'Path on reflected directory must exist'); 101 | done(err); 102 | } 103 | ) 104 | }); 105 | 106 | it('two engines will transfer handle deleting files', function (done) { 107 | counter = EventCounter.getCounter(listeningEngine.engine, Engine.events.deletedPath, 1); 108 | 109 | async.series( 110 | [ 111 | (cb)=>mkdirp(`${TEST_DIR}/bbb/`, cb), 112 | 113 | (cb)=>fs.writeFile(`${TEST_DIR}/bbb/file_1.txt`, '123123123', 'utf8', cb), 114 | 115 | (cb)=>setTimeout(()=>removePath(`${TEST_DIR}/bbb/file_1.txt`, cb), FS_TIMEOUT), 116 | 117 | (cb)=> { 118 | if (counter.hasFinished()) return setImmediate(cb); 119 | 120 | return counter.on(EventCounter.events.done, cb); 121 | } 122 | ], 123 | 124 | done 125 | ) 126 | }); 127 | 128 | 129 | it('two engines will synchronize multiple files both ways', function (done) { 130 | const listenerCounter = EventCounter.getCounter(listeningEngine.engine, Engine.events.newFile, 4); 131 | const connectingCounter = EventCounter.getCounter(connectingEngine.engine, Engine.events.newFile, 2); 132 | 133 | async.series( 134 | [ 135 | (cb)=> { 136 | return createPathSeries( 137 | [ 138 | {path: `${TEST_DIR}/aaa/a.txt`, content: getRandomString(50000)}, 139 | {path: `${TEST_DIR}/aaa/b.txt`, content: getRandomString(50000)}, 140 | 141 | {path: `${TEST_DIR}/bbb/c.txt`, content: getRandomString(50000)}, 142 | {path: `${TEST_DIR}/bbb/d.txt`, content: getRandomString(500000)}, 143 | {path: `${TEST_DIR}/bbb/e.txt`, content: getRandomString(500)}, 144 | {path: `${TEST_DIR}/bbb/f.txt`, content: getRandomString(500)}, 145 | ], 146 | cb 147 | ) 148 | }, 149 | 150 | (cb)=> { 151 | if (listenerCounter.hasFinished()) return setImmediate(cb); 152 | 153 | return listenerCounter.on(EventCounter.events.done, cb); 154 | }, 155 | 156 | (cb)=> { 157 | if (connectingCounter.hasFinished()) return setImmediate(cb); 158 | 159 | return connectingCounter.on(EventCounter.events.done, cb); 160 | }, 161 | 162 | (cb)=>compareDirectories(`${TEST_DIR}/aaa`, `${TEST_DIR}/bbb`, cb) 163 | ], 164 | 165 | done 166 | ) 167 | 168 | }); 169 | 170 | }); 171 | 172 | -------------------------------------------------------------------------------- /src/test/fs/path_helper_test.ts: -------------------------------------------------------------------------------- 1 | import {PathHelper} from "../../fs_helpers/path_helper"; 2 | import * as path from "path"; 3 | import * as assert from "assert"; 4 | 5 | describe('PathHelper', ()=> { 6 | describe('normalizePath', ()=> { 7 | it('will return a unix version of windows path', ()=> { 8 | const actual = PathHelper.normalizePath('some\\test\\dir'); 9 | 10 | assert.equal(actual, 'some/test/dir'); 11 | }); 12 | 13 | it('will return a unix version of windows path, also with spaces inside', ()=> { 14 | const actual = PathHelper.normalizePath('some\\test with spaces\\new dir'); 15 | 16 | assert.equal(actual, 'some/test with spaces/new dir'); 17 | }); 18 | 19 | it('will return unix path with escaped spaces', ()=> { 20 | const actual = PathHelper.normalizePath('some/strange/.path with/spaces multiple'); 21 | 22 | assert.equal(actual, 'some/strange/.path with/spaces multiple'); 23 | }); 24 | }); 25 | 26 | describe('createFilterFunction', ()=> { 27 | it('should work for simple files', ()=> { 28 | const ignored = ['.hidden.json', 'file.txt', 'name\ with\ spaces']; 29 | const func = PathHelper.createFilterFunction(ignored, '.'); 30 | const current = process.cwd(); 31 | 32 | assert(func(path.join(current, ignored[0])), 'should ignore .hidden.json'); 33 | assert(func(path.join(current, ignored[1])), 'should ignore file.txt'); 34 | assert(func(path.join(current, ignored[2])), 'should ignore "name with spaces"'); 35 | assert.equal(func(path.join(current, 'other_file.txt')), false, 'should not ignore other_file.txt'); 36 | assert.equal(func(path.join(current, 'with\ spaces.txt')), false, 'should not ignore file with similar name'); 37 | assert.equal(func(path.join(current, '.hidden')), false, 'should not ignore file with similar name'); 38 | }); 39 | 40 | it('should ignore subdirectories', ()=> { 41 | const ignored = ['.git', path.join('dir', 'sub'), path.join('dir\ spaces')]; 42 | 43 | const func = PathHelper.createFilterFunction(ignored, '.'); 44 | 45 | const current = process.cwd(); 46 | 47 | assert(func(path.join(current, '.git'))); 48 | assert(func(path.join(current, '.git', 'somefile.txt'))); 49 | assert(func(path.join(current, '.git', 'subdir', 'somefile.txt'))); 50 | 51 | assert.equal(func(path.join(current, 'dir')), false, 'should not ignore parent of an ignored path'); 52 | assert.equal(func(path.join(current, 'dir', 'file.txt')), false); 53 | assert(func(path.join(current, 'dir', 'sub'))); 54 | assert(func(path.join(current, 'dir', 'sub', 'file.txt'))); 55 | assert(func(path.join(current, 'dir', 'sub', 'another.txt'))); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/test/fs/read_tree_test.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | import {createPathSeries, removePath} from "../../utils/fs_test_utils"; 3 | import * as path from "path"; 4 | import {readTree} from "../../fs_helpers/read_tree"; 5 | import * as assert from "assert"; 6 | 7 | const TEST_DIR = path.join(__dirname, 'read_tree_test'); 8 | const CONTENT = 'random text'; 9 | 10 | const allFiles = [ 11 | 'top_level.js', 12 | '.git', 13 | path.join('.git', 'file_1.js'), 14 | 'dir', 15 | path.join('dir', 'a.js.map'), 16 | path.join('dir', 'a.js'), 17 | path.join('dir', 'b.js'), 18 | path.join('dir', 'c.js'), 19 | 'outer_dir', 20 | path.join('outer_dir', 'nested_dir'), 21 | path.join('outer_dir', 'nested_dir', 'file_a.js'), 22 | path.join('outer_dir', 'nested_dir', 'file_b.js'), 23 | path.join('outer_dir', 'nested_dir', 'file_c.js') 24 | ]; 25 | 26 | describe('readTree', ()=> { 27 | beforeEach((done)=> { 28 | async.series( 29 | [ 30 | (cb)=>removePath(TEST_DIR, cb), 31 | 32 | (cb)=>createPathSeries( 33 | [ 34 | {path: TEST_DIR, directory: true}, 35 | 36 | {path: path.join(TEST_DIR, 'top_level.js'), content: CONTENT}, 37 | 38 | {path: path.join(TEST_DIR, '.git'), directory: true}, 39 | {path: path.join(TEST_DIR, '.git', 'file_1.js'), content: CONTENT}, 40 | 41 | {path: path.join(TEST_DIR, 'dir'), directory: true}, 42 | {path: path.join(TEST_DIR, 'dir', 'a.js.map'), content: CONTENT}, 43 | {path: path.join(TEST_DIR, 'dir', 'a.js'), content: CONTENT}, 44 | {path: path.join(TEST_DIR, 'dir', 'b.js'), content: CONTENT}, 45 | {path: path.join(TEST_DIR, 'dir', 'c.js'), content: CONTENT}, 46 | 47 | {path: path.join(TEST_DIR, 'outer_dir'), directory: true}, 48 | {path: path.join(TEST_DIR, 'outer_dir', 'nested_dir'), directory: true}, 49 | {path: path.join(TEST_DIR, 'outer_dir', 'nested_dir', 'file_a.js'), content: CONTENT}, 50 | {path: path.join(TEST_DIR, 'outer_dir', 'nested_dir', 'file_b.js'), content: CONTENT}, 51 | {path: path.join(TEST_DIR, 'outer_dir', 'nested_dir', 'file_c.js'), content: CONTENT} 52 | ], 53 | cb 54 | ) 55 | ], 56 | done 57 | ) 58 | }); 59 | 60 | after((done)=> { 61 | removePath(TEST_DIR, done); 62 | }); 63 | 64 | it('without any filtering it will return the whole file tree', (done)=> { 65 | readTree(TEST_DIR, {}, (err, results)=> { 66 | assert.deepEqual(results.sort(), allFiles.sort()); 67 | done(err); 68 | }) 69 | }); 70 | 71 | it('when only files option is given, it will return only files, not directories', (done)=> { 72 | const onlyFiles = [ 73 | 'top_level.js', 74 | path.join('.git', 'file_1.js'), 75 | path.join('dir', 'a.js.map'), 76 | path.join('dir', 'a.js'), 77 | path.join('dir', 'b.js'), 78 | path.join('dir', 'c.js'), 79 | path.join('outer_dir', 'nested_dir', 'file_a.js'), 80 | path.join('outer_dir', 'nested_dir', 'file_b.js'), 81 | path.join('outer_dir', 'nested_dir', 'file_c.js') 82 | ]; 83 | 84 | readTree(TEST_DIR, {onlyFiles: true}, (err, results)=> { 85 | assert.deepEqual(results.sort(), onlyFiles.sort()); 86 | done(err); 87 | }) 88 | }); 89 | 90 | it('if given a filter function, will not reutrn filtered paths or their subdirectories / children paths', (done)=> { 91 | const expected = [ 92 | 'top_level.js', 93 | 'dir', 94 | path.join('dir', 'a.js.map'), 95 | path.join('dir', 'a.js'), 96 | path.join('dir', 'b.js'), 97 | path.join('dir', 'c.js'), 98 | 'outer_dir' 99 | ]; 100 | 101 | const filterFunction = (suspect)=> { 102 | return [ 103 | path.join(TEST_DIR, '.git'), 104 | path.join(TEST_DIR, 'outer_dir', 'nested_dir') 105 | ].indexOf(suspect) !== -1; 106 | }; 107 | 108 | readTree(TEST_DIR, {filter: filterFunction}, (err, results)=> { 109 | assert.deepEqual(results.sort(), expected.sort()); 110 | done(err); 111 | }) 112 | }); 113 | }); -------------------------------------------------------------------------------- /src/test/sync/pull_action_test.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | import {createPathSeries, removePath, getRandomString, compareDirectories, pathExists} from "../../utils/fs_test_utils"; 3 | import {Engine} from "../../core/engine"; 4 | import {EventCounter} from "../../utils/event_counter"; 5 | import * as assert from "assert"; 6 | import {pullAction} from "../../sync/pull_action"; 7 | import * as sinon from "sinon"; 8 | import SConnect from "../../facade/client"; 9 | import SListen from "../../facade/server"; 10 | import {setDeleteLocalFiles} from "../../sync/sync_actions"; 11 | import * as path from "path"; 12 | 13 | const token = '12897371023o1289nnjos'; 14 | const port = 4321; 15 | const TEST_DIR = path.join(__dirname, 'pull_test'); 16 | 17 | describe('PullAction', function () { 18 | 19 | let listeningEngine; 20 | let connectingEngine; 21 | let sandbox; 22 | 23 | beforeEach((done)=> { 24 | sandbox = sinon.sandbox.create(); 25 | 26 | return async.waterfall( 27 | [ 28 | (cb)=>removePath(TEST_DIR, cb), 29 | (cb)=>createPathSeries( 30 | [ 31 | {path: TEST_DIR, directory: true}, 32 | {path: `${TEST_DIR}/dir_list`, directory: true}, 33 | {path: `${TEST_DIR}/dir_conn`, directory: true} 34 | ], 35 | cb 36 | ), 37 | 38 | ], 39 | done 40 | ) 41 | }); 42 | 43 | afterEach((done)=> { 44 | if (listeningEngine)listeningEngine.shutdown(); 45 | if (connectingEngine)connectingEngine.shutdown(); 46 | sandbox.restore(); 47 | 48 | return removePath(TEST_DIR, done); 49 | }); 50 | 51 | 52 | it('will make both file trees identical, will not synchronize existing files', function (done) { 53 | let counter:EventCounter; 54 | 55 | async.waterfall( 56 | [ 57 | (cb)=> { 58 | const sameContent = getRandomString(500); 59 | 60 | return createPathSeries( 61 | [ 62 | {path: `${TEST_DIR}/dir_conn/a.txt`, content: getRandomString(50000)}, 63 | {path: `${TEST_DIR}/dir_conn/b.txt`, content: getRandomString(50000)}, 64 | {path: `${TEST_DIR}/dir_conn/c.txt`, content: getRandomString(500)}, 65 | {path: `${TEST_DIR}/dir_conn/same.txt`, content: sameContent},// the same file 66 | {path: `${TEST_DIR}/dir_list/same.txt`, content: sameContent} // should not be synced 67 | ], 68 | cb 69 | ) 70 | }, 71 | 72 | (cb)=> { 73 | listeningEngine = new SListen({ 74 | path: `${TEST_DIR}/dir_list`, localPort: port, 75 | authenticate: true, 76 | externalHost: '127.0.0.1', 77 | initialToken: token, 78 | watch: true, 79 | sync: pullAction 80 | }); 81 | sandbox.spy(listeningEngine.engine, 'requestRemoteFile'); 82 | counter = EventCounter.getCounter(listeningEngine.engine, Engine.events.synced, 1); 83 | 84 | return listeningEngine.start(cb); 85 | }, 86 | 87 | (cb)=> { 88 | connectingEngine = new SConnect({ 89 | path: `${TEST_DIR}/dir_conn`, 90 | remotePort: port, 91 | remoteHost: '127.0.0.1', 92 | authenticate: true, 93 | initialToken: token, 94 | watch: true 95 | }); 96 | 97 | return connectingEngine.start(cb) 98 | }, 99 | 100 | (cb)=> { 101 | if (counter.hasFinished()) return setImmediate(cb); 102 | 103 | counter.on(EventCounter.events.done, cb); 104 | }, 105 | 106 | (cb)=>compareDirectories(`${TEST_DIR}/dir_conn`, `${TEST_DIR}/dir_list`, cb), 107 | 108 | (cb)=> { 109 | assert(listeningEngine.engine.requestRemoteFile.neverCalledWithMatch(()=>true, 'same.txt')); 110 | setImmediate(cb); 111 | } 112 | ], 113 | done 114 | ); 115 | }); 116 | 117 | it('with setDeleteLocalFiles with delete any local files that do not exist remotely', function (done) { 118 | let counter:EventCounter; 119 | 120 | async.waterfall( 121 | [ 122 | (cb)=> { 123 | 124 | const sameContent = getRandomString(500); 125 | return createPathSeries( 126 | [ 127 | {path: `${TEST_DIR}/dir_conn/a.txt`, content: getRandomString(50000)}, 128 | {path: `${TEST_DIR}/dir_conn/b.txt`, content: getRandomString(50000)}, 129 | {path: `${TEST_DIR}/dir_conn/same.txt`, content: sameContent}, 130 | {path: `${TEST_DIR}/dir_list/same.txt`, content: sameContent}, 131 | {path: `${TEST_DIR}/dir_list/file_to_delete.txt`, content: getRandomString(500)}, 132 | ], 133 | cb 134 | ) 135 | }, 136 | 137 | (cb)=> { 138 | listeningEngine = new SListen({ 139 | path: `${TEST_DIR}/dir_list`, localPort: port, 140 | authenticate: true, 141 | externalHost: '127.0.0.1', 142 | initialToken: token, 143 | watch: true, 144 | sync: setDeleteLocalFiles(pullAction) 145 | }); 146 | 147 | sandbox.spy(listeningEngine.engine, 'requestRemoteFile'); 148 | counter = EventCounter.getCounter(listeningEngine.engine, Engine.events.synced, 1); 149 | 150 | return listeningEngine.start(cb) 151 | }, 152 | 153 | (cb)=> { 154 | connectingEngine = new SConnect({ 155 | path: `${TEST_DIR}/dir_conn`, 156 | remotePort: port, 157 | remoteHost: '127.0.0.1', 158 | authenticate: true, 159 | initialToken: token, 160 | watch: true 161 | }); 162 | 163 | return connectingEngine.start(cb) 164 | }, 165 | 166 | (cb)=> { 167 | if (counter.hasFinished()) return setImmediate(cb); 168 | 169 | return counter.on(EventCounter.events.done, cb); 170 | }, 171 | 172 | (cb)=>compareDirectories(`${TEST_DIR}/dir_conn`, `${TEST_DIR}/dir_list`, cb), 173 | 174 | (cb)=> { 175 | 176 | assert.equal(pathExists(`${TEST_DIR}/dir_conn/file_to_delete.txt`), false); 177 | assert(listeningEngine.engine.requestRemoteFile.neverCalledWithMatch(()=>true, 'same.txt')); 178 | 179 | return setImmediate(cb); 180 | } 181 | ], 182 | done 183 | ); 184 | }) 185 | }); 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /src/test/sync/push_action_test.ts: -------------------------------------------------------------------------------- 1 | import * as async from "async"; 2 | import {createPathSeries, removePath, getRandomString, compareDirectories, pathExists} from "../../utils/fs_test_utils"; 3 | import {Engine} from "../../core/engine"; 4 | import {EventCounter} from "../../utils/event_counter"; 5 | import {pushAction} from "../../sync/push_action"; 6 | import {setDeleteRemoteFiles} from "../../sync/sync_actions"; 7 | import {expect} from "chai"; 8 | import SListen from "../../facade/server"; 9 | import SConnect from "../../facade/client"; 10 | import * as path from "path"; 11 | 12 | const TEST_DIR = path.join(__dirname, 'push_test'); 13 | const TOKEN = '121cb2897o1289nnjos'; 14 | const PORT = 4321; 15 | 16 | describe('PushAction', function () { 17 | 18 | let listeningEngine; 19 | let connectingEngine; 20 | 21 | 22 | beforeEach((done)=> { 23 | return async.waterfall( 24 | [ 25 | (cb)=>removePath(TEST_DIR, cb), 26 | 27 | (cb)=>createPathSeries( 28 | [ 29 | {path: `${TEST_DIR}/dir_list`, directory: true}, 30 | {path: `${TEST_DIR}/dir_conn`, directory: true} 31 | ], 32 | cb 33 | ), 34 | 35 | ], 36 | done 37 | ) 38 | }); 39 | 40 | afterEach((done)=> { 41 | if (listeningEngine)listeningEngine.shutdown(); 42 | if (connectingEngine)connectingEngine.shutdown(); 43 | 44 | return removePath(TEST_DIR, done); 45 | }); 46 | 47 | 48 | it('will make the both file trees identical', function (done) { 49 | let counter:EventCounter; 50 | 51 | async.waterfall( 52 | [ 53 | (cb)=> { 54 | const sameContent = getRandomString(500); 55 | return createPathSeries( 56 | [ 57 | {path: `${TEST_DIR}/dir_list/c.txt`, content: getRandomString(50000)}, 58 | {path: `${TEST_DIR}/dir_list/d.txt`, content: getRandomString(50000)}, 59 | {path: `${TEST_DIR}/dir_list/e.txt`, content: getRandomString(500)}, 60 | {path: `${TEST_DIR}/dir_list/same_file.txt`, content: sameContent}, 61 | {path: `${TEST_DIR}/dir_conn/same_file.txt`, content: sameContent}, 62 | ], 63 | cb 64 | ) 65 | }, 66 | 67 | (cb)=> { 68 | listeningEngine = new SListen({ 69 | path: `${TEST_DIR}/dir_list`, localPort: PORT, 70 | authenticate: true, 71 | externalHost: '127.0.0.1', 72 | initialToken: TOKEN, 73 | watch: true, 74 | sync: pushAction 75 | }); 76 | 77 | counter = EventCounter.getCounter(listeningEngine.engine, Engine.events.synced, 1); 78 | 79 | return listeningEngine.start(cb) 80 | }, 81 | 82 | (cb)=> { 83 | connectingEngine = new SConnect({ 84 | path: `${TEST_DIR}/dir_conn`, 85 | remotePort: PORT, 86 | remoteHost: '127.0.0.1', 87 | authenticate: true, 88 | initialToken: TOKEN, 89 | watch: true 90 | }); 91 | 92 | return connectingEngine.start(cb) 93 | }, 94 | 95 | (cb)=> { 96 | if (counter.hasFinished()) return setImmediate(cb); 97 | 98 | counter.on(EventCounter.events.done, cb); 99 | }, 100 | 101 | (cb)=>compareDirectories(`${TEST_DIR}/dir_conn`, `${TEST_DIR}/dir_list`, cb) 102 | ], 103 | done 104 | ); 105 | }); 106 | 107 | it('with delete remoteFiles will delete remote files remote ', function (done) { 108 | let counter:EventCounter; 109 | 110 | async.waterfall( 111 | [ 112 | (cb)=> { 113 | const contentF = getRandomString(500); 114 | return createPathSeries( 115 | [ 116 | {path: `${TEST_DIR}/dir_list/c.txt`, content: getRandomString(50000)}, 117 | {path: `${TEST_DIR}/dir_list/d.txt`, content: getRandomString(50000)}, 118 | {path: `${TEST_DIR}/dir_list/e.txt`, content: getRandomString(500)}, 119 | {path: `${TEST_DIR}/dir_list/f.txt`, content: contentF}, 120 | {path: `${TEST_DIR}/dir_conn/file_to_delete.txt`, content: contentF}, 121 | ], 122 | cb 123 | ) 124 | }, 125 | 126 | (cb)=> { 127 | listeningEngine = new SListen({ 128 | path: `${TEST_DIR}/dir_list`, localPort: PORT, 129 | authenticate: true, 130 | externalHost: '127.0.0.1', 131 | initialToken: TOKEN, 132 | watch: true, 133 | sync: setDeleteRemoteFiles(pushAction) 134 | }); 135 | 136 | counter = EventCounter.getCounter(listeningEngine.engine, Engine.events.synced, 1); 137 | 138 | return listeningEngine.start(cb) 139 | }, 140 | 141 | (cb)=> { 142 | connectingEngine = new SConnect({ 143 | path: `${TEST_DIR}/dir_conn`, 144 | remotePort: PORT, 145 | remoteHost: '127.0.0.1', 146 | authenticate: true, 147 | initialToken: TOKEN, 148 | watch: true 149 | }); 150 | 151 | return connectingEngine.start(cb) 152 | }, 153 | 154 | (cb)=> { 155 | if (counter.hasFinished()) return setImmediate(cb); 156 | 157 | counter.on(EventCounter.events.done, cb); 158 | }, 159 | 160 | (cb)=>compareDirectories(`${TEST_DIR}/dir_conn`, `${TEST_DIR}/dir_list`, cb), 161 | 162 | (cb)=> { 163 | 164 | expect(pathExists(`${TEST_DIR}/dir_conn/file_to_delete.txt`)).to.equal(false); 165 | 166 | setImmediate(cb); 167 | } 168 | ], 169 | done 170 | ); 171 | }) 172 | }); 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/test/utils/event_counter_test.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from "events"; 2 | import {EventCounter} from "../../utils/event_counter"; 3 | import * as assert from "assert"; 4 | 5 | describe('EventCounter', function () { 6 | it('will emit done event when given event was emitted count number of times', function (done) { 7 | let number = 5; 8 | 9 | const emitter = new EventEmitter(); 10 | const eventCounter = new EventCounter(emitter, [{name: 'mock', count: number}]); 11 | 12 | eventCounter.on(EventCounter.events.done, done); 13 | 14 | while (number-- > 0) { 15 | emitter.emit('mock'); 16 | } 17 | }); 18 | 19 | it('if there was no listener for done event, the hasFinished method will return true when counter finished', function () { 20 | let number = 5; 21 | 22 | const emitter = new EventEmitter(); 23 | const eventCounter = new EventCounter(emitter, [{name: 'mock', count: number}]); 24 | 25 | while (number-- > 0) { 26 | emitter.emit('mock'); 27 | } 28 | 29 | assert(eventCounter.hasFinished()) 30 | }); 31 | }); -------------------------------------------------------------------------------- /src/transport/transfer_actions.ts: -------------------------------------------------------------------------------- 1 | import {Socket} from "net"; 2 | import {loggerFor, debugFor} from "../utils/logger"; 3 | import {ListenCallback, ConnectionHelper, ConnectionAddress} from "../connection/connection_helper"; 4 | import {Container, ErrorCallback} from "../utils/interfaces"; 5 | import * as _ from "lodash"; 6 | 7 | const debug = debugFor("syncrow:transfer:actions"); 8 | const logger = loggerFor('TransferActions'); 9 | 10 | 11 | export class TransferActions { 12 | 13 | public static events = { 14 | listenAndUpload: 'listenAndUpload', 15 | listenAndDownload: 'listenAndDownload', 16 | 17 | connectAndUpload: 'connectAndUpload', 18 | connectAndDownload: 'connectAndDownload', 19 | }; 20 | 21 | 22 | /** 23 | * Listens for other party to connect, then downloads the file from it 24 | * @param fileName 25 | * @param destinationContainer 26 | * @param connectionHelper 27 | * @param doneCallback 28 | * @param listeningCallback 29 | */ 30 | public static listenAndDownloadFile(fileName:string, 31 | destinationContainer:Container, 32 | connectionHelper:ConnectionHelper, 33 | doneCallback:ErrorCallback, 34 | listeningCallback:ListenCallback) { 35 | 36 | debug(`executing: listenAndDownloadFile - fileName: ${fileName}`); 37 | 38 | return connectionHelper.getNewSocket( 39 | {listenCallback: listeningCallback}, 40 | (err, socket)=> { 41 | if (err)return doneCallback(err); 42 | 43 | return TransferActions.consumeFileFromSocket(socket, fileName, destinationContainer, doneCallback) 44 | } 45 | ); 46 | } 47 | 48 | /** 49 | * Listen for other party to connect, and then send the file to it 50 | * @param fileName 51 | * @param sourceContainer 52 | * @param connectionHelper 53 | * @param doneCallback 54 | * @param listenCallback 55 | */ 56 | public static listenAndUploadFile(fileName:string, 57 | sourceContainer:Container, 58 | connectionHelper:ConnectionHelper, 59 | doneCallback:ErrorCallback, 60 | listenCallback:ListenCallback) { 61 | 62 | doneCallback = _.once(doneCallback); 63 | 64 | debug(`#listenAndUploadFile - started fileName: ${fileName}`); 65 | 66 | return connectionHelper.getNewSocket( 67 | {listenCallback: listenCallback}, 68 | (err, fileTransferSocket)=> { 69 | if (err) { 70 | debug(`#listenAndUploadFile - failed fileName: ${fileName} - reason: ${err}`); 71 | return doneCallback(err); 72 | } 73 | 74 | fileTransferSocket.on('end', doneCallback); 75 | fileTransferSocket.on('error', doneCallback); 76 | sourceContainer.getReadStreamForFile(fileName).pipe(fileTransferSocket); 77 | } 78 | ); 79 | } 80 | 81 | /** 82 | * Connects with other party and sends the file to it 83 | * @param fileName 84 | * @param address 85 | * @param sourceContainer 86 | * @param connectionHelper 87 | * @param doneCallback 88 | */ 89 | public static connectAndUploadFile(fileName:string, 90 | address:ConnectionAddress, 91 | sourceContainer:Container, 92 | connectionHelper:ConnectionHelper, 93 | doneCallback:ErrorCallback) { 94 | 95 | debug(`connectAndUploadFile: connecting to ${address.remoteHost}:${address.remotePort}`); 96 | 97 | doneCallback = _.once(doneCallback); 98 | 99 | connectionHelper.getNewSocket( 100 | address, 101 | (err, socket)=> { 102 | if (err) return doneCallback(err); 103 | 104 | socket.on('end', doneCallback); 105 | socket.on('error', doneCallback); 106 | 107 | return sourceContainer.getReadStreamForFile(fileName).pipe(socket); 108 | } 109 | ); 110 | 111 | } 112 | 113 | /** 114 | * Connects with other party and downloads a file from it 115 | * @param fileName 116 | * @param address 117 | * @param destinationContainer 118 | * @param connectionHelper 119 | * @param doneCallback 120 | */ 121 | public static connectAndDownloadFile(fileName:string, 122 | address:ConnectionAddress, 123 | destinationContainer:Container, 124 | connectionHelper:ConnectionHelper, 125 | doneCallback:ErrorCallback) { 126 | 127 | debug(`#connectAndDownloadFile: connecting to ${address.remoteHost}:${address.remotePort}`); 128 | 129 | connectionHelper.getNewSocket( 130 | address, 131 | (err, socket)=> { 132 | if (err) { 133 | debug(`#connectAndDownloadFile: connecting to ${address.remoteHost}:${address.remotePort} failed: ${err}`); 134 | return doneCallback(err); 135 | } 136 | 137 | TransferActions.consumeFileFromSocket(socket, fileName, destinationContainer, doneCallback) 138 | } 139 | ) 140 | } 141 | 142 | private static consumeFileFromSocket(fileTransferSocket:Socket, fileName:string, destinationContainer:Container, callback:ErrorCallback) { 143 | destinationContainer.consumeFileStream(fileName, fileTransferSocket, callback); 144 | } 145 | } -------------------------------------------------------------------------------- /src/transport/transfer_helper.ts: -------------------------------------------------------------------------------- 1 | import {TransferQueue} from "./transfer_queue"; 2 | import {CallbackHelper} from "../connection/callback_helper"; 3 | import {TransferActions} from "./transfer_actions"; 4 | import {loggerFor, debugFor} from "../utils/logger"; 5 | import {EventMessenger} from "../connection/event_messenger"; 6 | import {ConnectionHelper, ConnectionAddress} from "../connection/connection_helper"; 7 | import {Container, ErrorCallback} from "../utils/interfaces"; 8 | 9 | const TRANSFER_CONCURRENCY = 500; 10 | 11 | export interface TransferHelperOptions { 12 | transferQueueSize?:number, 13 | name:string, 14 | preferConnecting:boolean; 15 | 16 | } 17 | 18 | const debug = debugFor('syncrow:transfer:helper'); 19 | const logger = loggerFor('TransferHelper'); 20 | 21 | /** 22 | * Private commands 23 | */ 24 | interface TransferMessage { 25 | fileName:string; 26 | command:string; 27 | id?:string; 28 | address?:ConnectionAddress; 29 | } 30 | 31 | export class TransferHelper { 32 | 33 | static outerEvent = 'transferEvent'; 34 | 35 | private queue:TransferQueue; 36 | private preferConnecting:boolean; 37 | private container:Container; 38 | private callbackHelper:CallbackHelper; 39 | private callbackMap:Map>; 40 | 41 | constructor(container:Container, private connectionHelper:ConnectionHelper, options:TransferHelperOptions) { 42 | const queueSize = options.transferQueueSize ? options.transferQueueSize : TRANSFER_CONCURRENCY; 43 | 44 | this.queue = new TransferQueue(queueSize, options.name); 45 | this.preferConnecting = options.preferConnecting; 46 | this.container = container; 47 | this.callbackHelper = new CallbackHelper(); 48 | this.callbackMap = new Map >(); 49 | } 50 | 51 | /** 52 | * Used to handle commands passed by caller 53 | * @param transferMessage 54 | * @param otherParty 55 | */ 56 | public consumeMessage(transferMessage:TransferMessage, otherParty:EventMessenger) { 57 | if (transferMessage.command === TransferActions.events.connectAndUpload) { 58 | this.queue.addConnectAndUploadJobToQueue( 59 | transferMessage.fileName, 60 | transferMessage.address, 61 | this.container, 62 | this.connectionHelper, 63 | this.getCallbackForIdOrErrorLogger(transferMessage.id) 64 | ); 65 | 66 | } else if (transferMessage.command === TransferActions.events.connectAndDownload) { 67 | this.queue.addConnectAndDownloadJobToQueue( 68 | transferMessage.fileName, 69 | transferMessage.address, 70 | this.container, 71 | this.connectionHelper, 72 | this.getCallbackForIdOrErrorLogger(transferMessage.id) 73 | ); 74 | 75 | } else if (transferMessage.command === TransferActions.events.listenAndDownload) { 76 | return this.getFileViaListening(transferMessage.fileName, otherParty, {id: transferMessage.id}); 77 | 78 | } else if (transferMessage.command === TransferActions.events.listenAndUpload) { 79 | return this.sendFileViaListening(transferMessage.fileName, otherParty, {id: transferMessage.id}); 80 | } 81 | } 82 | 83 | /** 84 | * Downloads a file from remote 85 | * @param otherParty 86 | * @param fileName 87 | * @param callback 88 | */ 89 | public getFileFromRemote(otherParty:EventMessenger, fileName:string, callback:ErrorCallback) { 90 | 91 | if (this.preferConnecting) { 92 | const id = this.callbackHelper.addCallback(callback); 93 | 94 | const message:TransferMessage = { 95 | fileName: fileName, 96 | id: id, 97 | command: TransferActions.events.listenAndUpload 98 | }; 99 | 100 | return otherParty.send(TransferHelper.outerEvent, message); 101 | } 102 | 103 | return this.getFileViaListening(fileName, otherParty, {callback: callback}); 104 | } 105 | 106 | /** 107 | * Uploads a file to remote 108 | * @param otherParty 109 | * @param fileName 110 | * @param callback 111 | */ 112 | public sendFileToRemote(otherParty:EventMessenger, fileName:string, callback:ErrorCallback) { 113 | 114 | if (this.callbackMap.has(fileName)) { 115 | debug(`sendFileToRemote - file: ${fileName} already being sent`); 116 | return this.callbackMap.get(fileName).push(callback); 117 | } 118 | 119 | this.callbackMap.set(fileName, [callback]); 120 | 121 | const finish = (err?:Error)=>this.finishSendingFile(fileName, err); 122 | 123 | if (this.preferConnecting) { 124 | const id = this.callbackHelper.addCallback(finish); 125 | 126 | const message:TransferMessage = { 127 | command: TransferActions.events.listenAndDownload, 128 | id: id, 129 | fileName: fileName 130 | }; 131 | 132 | return otherParty.send(TransferHelper.outerEvent, message); 133 | } 134 | 135 | return this.sendFileViaListening(fileName, otherParty, {callback: finish}); 136 | } 137 | 138 | private finishSendingFile(fileName:string, error?:Error) { 139 | const callbacks = this.callbackMap.get(fileName); 140 | 141 | callbacks.forEach(c => c(error)); 142 | 143 | this.callbackMap.delete(fileName); 144 | } 145 | 146 | private sendFileViaListening(fileName:string, remote:EventMessenger, optional:{id ?:string, callback?:ErrorCallback }) { 147 | this.queue.addListenAndUploadJobToQueue( 148 | fileName, 149 | this.container, 150 | this.connectionHelper, 151 | 152 | (address)=> { 153 | const message:TransferMessage = { 154 | fileName: fileName, 155 | command: TransferActions.events.connectAndDownload, 156 | address: address, 157 | id: optional.id 158 | }; 159 | 160 | remote.send(TransferHelper.outerEvent, message); 161 | }, 162 | 163 | optional.callback 164 | ); 165 | } 166 | 167 | 168 | private getFileViaListening(fileName:string, remote:EventMessenger, optional:{id?:string, callback?:ErrorCallback}) { 169 | this.queue.addListenAndDownloadJobToQueue( 170 | fileName, 171 | this.container, 172 | this.connectionHelper, 173 | 174 | (address)=> { 175 | const message:TransferMessage = { 176 | fileName: fileName, 177 | command: TransferActions.events.connectAndUpload, 178 | address: address, 179 | id: optional.id 180 | }; 181 | 182 | remote.send(TransferHelper.outerEvent, message); 183 | }, 184 | 185 | optional.callback 186 | ); 187 | } 188 | 189 | private getCallbackForIdOrErrorLogger(id?:string):ErrorCallback { 190 | if (id) return this.callbackHelper.getCallback(id); 191 | 192 | return (err)=> { 193 | logger.error(err) 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /src/transport/transfer_queue.ts: -------------------------------------------------------------------------------- 1 | import {TransferActions} from "./transfer_actions"; 2 | import * as async from "async"; 3 | import {ConnectionAddress, ConnectionHelper, ListenCallback} from "../connection/connection_helper"; 4 | import {Container, ErrorCallback} from "../utils/interfaces"; 5 | import {loggerFor, debugFor} from "../utils/logger"; 6 | 7 | const debug = debugFor("syncrow:transfer:queue"); 8 | 9 | const logger = loggerFor('TransferQueue'); 10 | 11 | export class TransferQueue { 12 | 13 | private queue:AsyncQueue; 14 | private name:string; 15 | 16 | constructor(concurrency:number, name:string = '') { 17 | this.queue = async.queue((job:Function, callback:Function)=>job(callback), concurrency); 18 | this.name = name; 19 | } 20 | 21 | /** 22 | * @param fileName 23 | * @param address 24 | * @param sourceContainer 25 | * @param connectionHelper 26 | * @param doneCallback 27 | */ 28 | public addConnectAndUploadJobToQueue(fileName:string, 29 | address:ConnectionAddress, 30 | sourceContainer:Container, 31 | connectionHelper:ConnectionHelper, 32 | doneCallback:ErrorCallback) { 33 | debug(`adding job: connectAndUploadFile: ${fileName}`); 34 | const job = (uploadingDoneCallback) => { 35 | 36 | const timerId = logger.time(`${this.name} - connecting and uploading file: ${fileName}`); 37 | 38 | return TransferActions.connectAndUploadFile(fileName, address, sourceContainer, connectionHelper, 39 | (err)=> { 40 | logger.timeEnd(timerId); 41 | 42 | return uploadingDoneCallback(err) 43 | } 44 | ); 45 | }; 46 | 47 | this.queue.push(job, doneCallback); 48 | } 49 | 50 | /** 51 | * 52 | * @param address 53 | * @param fileName 54 | * @param destinationContainer 55 | * @param connectionHelper 56 | * @param doneCallback 57 | */ 58 | public addConnectAndDownloadJobToQueue(fileName:string, 59 | address:ConnectionAddress, 60 | destinationContainer:Container, 61 | connectionHelper:ConnectionHelper, 62 | doneCallback?:ErrorCallback) { 63 | 64 | debug(`adding job: connectAndDownloadFile: ${fileName}`); 65 | 66 | 67 | const job = (downloadingDoneCallback)=> { 68 | 69 | const timerId = logger.time(`${this.name} - connecting and downloading file: ${fileName}`); 70 | 71 | return TransferActions.connectAndDownloadFile(fileName, address, destinationContainer, connectionHelper, 72 | (err)=> { 73 | logger.timeEnd(timerId); 74 | 75 | return downloadingDoneCallback(err); 76 | } 77 | ); 78 | }; 79 | 80 | this.queue.push(job, doneCallback); 81 | } 82 | 83 | /** 84 | * 85 | * @param fileName 86 | * @param sourceContainer 87 | * @param connectionHelper 88 | * @param listeningCallback 89 | * @param doneCallback 90 | */ 91 | public addListenAndUploadJobToQueue(fileName:string, 92 | sourceContainer:Container, 93 | connectionHelper:ConnectionHelper, 94 | listeningCallback:ListenCallback, 95 | doneCallback:ErrorCallback) { 96 | 97 | debug(`adding job: listenAndUploadFile ${fileName}`); 98 | 99 | const job = (uploadingDoneCallback)=> { 100 | 101 | const timerId = logger.time(`${this.name} - listening and uploading file: ${fileName}`); 102 | 103 | return TransferActions.listenAndUploadFile(fileName, sourceContainer, connectionHelper, 104 | (err)=> { 105 | logger.timeEnd(timerId); 106 | 107 | return uploadingDoneCallback(err) 108 | }, 109 | listeningCallback 110 | ); 111 | 112 | }; 113 | 114 | this.queue.push(job, doneCallback); 115 | } 116 | 117 | /** 118 | * 119 | * @param fileName 120 | * @param destinationContainer 121 | * @param connectionHelper 122 | * @param doneCallback 123 | * @param listeningCallback 124 | */ 125 | public addListenAndDownloadJobToQueue(fileName:string, 126 | destinationContainer:Container, 127 | connectionHelper:ConnectionHelper, 128 | listeningCallback:ListenCallback, 129 | doneCallback:ErrorCallback) { 130 | 131 | debug(`adding job: listenAndDownloadFile - fileName: ${fileName}`); 132 | 133 | const job = (downloadingDoneCallback)=> { 134 | 135 | const timerId = logger.time(`${this.name} - listening and downloading file: ${fileName}`); 136 | 137 | return TransferActions.listenAndDownloadFile(fileName, destinationContainer, connectionHelper, 138 | 139 | (err)=> { 140 | logger.timeEnd(timerId); 141 | return downloadingDoneCallback(err) 142 | }, 143 | 144 | listeningCallback 145 | ); 146 | }; 147 | 148 | return this.queue.push(job, doneCallback); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/utils/event_counter.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from "events"; 2 | 3 | export class EventCounter extends EventEmitter { 4 | static events = { 5 | done: 'done' 6 | }; 7 | 8 | private numberUnfinishedCounters:number; 9 | private listenerMap:Map; 10 | 11 | constructor(private subject:EventEmitter, eventsAndCounts:Array<{name:string, count:number}>) { 12 | super(); 13 | 14 | this.numberUnfinishedCounters = eventsAndCounts.length; 15 | this.listenerMap = new Map(); 16 | 17 | eventsAndCounts.forEach(event=> { 18 | const listener = this.createCountFunction(event.count); 19 | 20 | this.listenerMap.set(event.name, listener); 21 | 22 | this.subject.on(event.name, listener); 23 | }); 24 | } 25 | 26 | /** 27 | * @returns {boolean} 28 | */ 29 | public hasFinished():boolean { 30 | return this.numberUnfinishedCounters === 0; 31 | } 32 | 33 | /** 34 | * @param subject 35 | * @param eventName 36 | * @param count 37 | * @returns {EventCounter} 38 | */ 39 | public static getCounter(subject:EventEmitter, eventName:string, count:number):EventCounter { 40 | return new EventCounter(subject, [{name: eventName, count: count}]) 41 | } 42 | 43 | private createCountFunction(count:number) { 44 | let emittedSoFar = 0; 45 | 46 | return ()=> { 47 | emittedSoFar++; 48 | 49 | if (emittedSoFar === count) { 50 | this.numberUnfinishedCounters--; 51 | 52 | if (this.hasFinished()) { 53 | this.finish(); 54 | } 55 | } 56 | } 57 | } 58 | 59 | private finish() { 60 | this.listenerMap.forEach( 61 | (listener, name)=>this.subject.removeListener(name, listener) 62 | ); 63 | 64 | this.emit(EventCounter.events.done); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/fs_test_utils.ts: -------------------------------------------------------------------------------- 1 | import * as net from "net"; 2 | import * as mkdirp from "mkdirp"; 3 | import * as async from "async"; 4 | import * as fs from "fs"; 5 | import * as rimraf from "rimraf"; 6 | import * as path from "path"; 7 | import {readTree} from "../fs_helpers/read_tree"; 8 | 9 | /** 10 | * @returns {Function} a cleanup function that will close the server and paired sockets 11 | * @param doneCallback 12 | */ 13 | export function obtainTwoSockets(doneCallback:(err, result?:{client:net.Socket, server:net.Socket})=>any):Function { 14 | let clientSocket; 15 | let server; 16 | 17 | const port = 3124; //A constant PORT is used to ensure that the cleanup function is called 18 | 19 | const listener = (socket)=> { 20 | return doneCallback(null, {client: clientSocket, server: socket}); 21 | }; 22 | 23 | async.series( 24 | [ 25 | (callback)=> { 26 | server = net.createServer(listener).listen(port, callback); 27 | }, 28 | (callback)=> { 29 | clientSocket = net.connect({port: port}, callback) 30 | } 31 | ], 32 | (err)=> { 33 | if (err)return doneCallback(err); 34 | } 35 | ); 36 | 37 | return ()=> { 38 | clientSocket.end(); 39 | server.close(); 40 | } 41 | } 42 | 43 | /** 44 | * @param path 45 | * @param content 46 | * @param directory 47 | * @param doneCallback 48 | */ 49 | export function createPath(path:string, content:string|Buffer, directory:boolean, doneCallback:ErrorCallback) { 50 | if (directory) { 51 | return createDir(path, doneCallback); 52 | } 53 | return fs.writeFile(path, content, doneCallback); 54 | } 55 | 56 | 57 | export interface CreatePathArgument { 58 | path:string; 59 | content?:string|Buffer; 60 | directory?:boolean; 61 | } 62 | 63 | /** 64 | * @param files 65 | * @param callback 66 | */ 67 | export function createPathSeries(files:Array, callback:ErrorCallback) { 68 | return async.eachSeries(files, 69 | 70 | (file, cb)=> createPath(file.path, file.content, file.directory, cb), 71 | 72 | callback 73 | ); 74 | } 75 | 76 | /** 77 | * @param path 78 | * @returns {boolean} 79 | */ 80 | export function pathExists(path):boolean { 81 | try { 82 | fs.accessSync(path); 83 | return true; 84 | } catch (e) { 85 | return false; 86 | } 87 | } 88 | 89 | /** 90 | * @param firstFilePath 91 | * @param secondFilePath 92 | * @param callback 93 | */ 94 | export function compareTwoFiles(firstFilePath:string, secondFilePath:string, callback:ErrorCallback) { 95 | return async.waterfall( 96 | [ 97 | (cb)=>getTwoFiles(firstFilePath, secondFilePath, cb), 98 | 99 | (results, cb)=>compareFileContents(firstFilePath, results.first, secondFilePath, results.second, cb) 100 | ], 101 | callback 102 | ) 103 | } 104 | 105 | /** 106 | * @param dirPath 107 | * @param doneCallback 108 | */ 109 | export function createDir(dirPath:string, doneCallback:ErrorCallback) { 110 | return mkdirp(dirPath, doneCallback); 111 | } 112 | 113 | 114 | /** 115 | * @param path 116 | * @param callback 117 | */ 118 | export function removePath(path:string, callback:ErrorCallback) { 119 | return rimraf(path, callback); 120 | } 121 | 122 | /** 123 | * @param pathA 124 | * @param pathB 125 | * @param callback 126 | */ 127 | export function compareDirectories(pathA:string, pathB:string, callback:ErrorCallback) { 128 | 129 | return async.waterfall( 130 | [ 131 | (seriesCallback)=> { 132 | return async.parallel({ 133 | pathA: (cb)=> readTree(pathA, {}, cb), 134 | pathB: (cb)=> readTree(pathB, {}, cb) 135 | }, 136 | seriesCallback 137 | ) 138 | }, 139 | (results, seriesCallback)=> compareFileTrees(pathA, results.pathA, pathB, results.pathB, seriesCallback) 140 | ], 141 | 142 | callback 143 | ) 144 | } 145 | 146 | /** 147 | * @param length 148 | * @returns {string} 149 | */ 150 | export function getRandomString(length:number):string { 151 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 152 | let result = ''; 153 | 154 | function getRandomChar() { 155 | const position = Math.floor(Math.random() * chars.length); 156 | return chars.charAt(position); 157 | } 158 | 159 | while (length > 0) { 160 | length--; 161 | result += getRandomChar(); 162 | } 163 | 164 | return result; 165 | } 166 | 167 | 168 | function getTwoFiles(firstFilePath:string, secondFilePath:string, doneCallback:(err:Error, result:{first?:string, second?:string})=>any) { 169 | async.parallel( 170 | { 171 | first: callback=>fs.readFile(firstFilePath, callback), 172 | second: callback=>fs.readFile(secondFilePath, callback) 173 | }, 174 | 175 | doneCallback 176 | ); 177 | } 178 | 179 | function compareFileTrees(pathA:string, filesA:Array, pathB:string, filesB:Array, callback:ErrorCallback) { 180 | if (filesA.length !== filesB.length) { 181 | return setImmediate(callback, new Error(`File trees are not matching - ${filesA.length} and ${filesB.length}`)); 182 | } 183 | 184 | return async.each(filesA, 185 | 186 | (file, cb)=> { 187 | const firstFilePath = path.join(pathA, file); 188 | const secondFilePath = path.join(pathB, file); 189 | 190 | return compareTwoFiles(firstFilePath, secondFilePath, cb); 191 | }, 192 | 193 | callback 194 | ) 195 | } 196 | 197 | function compareFileContents(pathA:string, contentA:string|Buffer, pathB:string, contentB:string|Buffer, callback) { 198 | if (Buffer.isBuffer(contentA) && Buffer.isBuffer(contentB)) { 199 | const buffersAreEqual = (contentA.compare(contentB) === 0); 200 | if (!buffersAreEqual) { 201 | return setImmediate(callback, new Error(`File contents ${pathA} and ${pathB} (Buffers) do not match `)); 202 | } 203 | 204 | } else { 205 | const stringsAreEqual = contentA === contentB; 206 | if (!stringsAreEqual) return setImmediate(callback, new Error(`File contents ${pathA} and ${pathB} (Strings) do not match`)) 207 | } 208 | 209 | return setImmediate(callback); 210 | } 211 | -------------------------------------------------------------------------------- /src/utils/interfaces.ts: -------------------------------------------------------------------------------- 1 | import ReadableStream = NodeJS.ReadableStream; 2 | import {Engine} from "../core/engine"; 3 | 4 | export interface Closable { 5 | shutdown:()=>any 6 | } 7 | 8 | export interface Container { 9 | consumeFileStream(fileName:string, readStream:ReadableStream, callback:ErrorCallback) 10 | getReadStreamForFile(fileName:string):ReadableStream 11 | } 12 | 13 | export interface ErrorCallback { 14 | (err?:Error):any 15 | } 16 | 17 | export interface EngineCallback { 18 | (err:Error, engine?:Engine):any 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from "chalk"; 2 | import * as debug from "debug"; 3 | 4 | let silent; 5 | 6 | export class Logger { 7 | private context:string; 8 | private timers:Map; 9 | private keys:Map; 10 | 11 | /** 12 | * Wrapper for console - can be later used to store logs to file 13 | * @param context 14 | */ 15 | constructor(context:string) { 16 | this.context = context; 17 | this.timers = new Map(); 18 | this.keys = new Map(); 19 | } 20 | 21 | /** 22 | * Returns an id to use later 23 | * @param key 24 | * @returns {string} 25 | */ 26 | public time(key:string) { 27 | const id = Logger.generateId(); 28 | 29 | this.keys.set(id, key); 30 | this.timers.set(id, new Date()); 31 | 32 | return id; 33 | } 34 | 35 | /** 36 | * @param id - must be id returned by time 37 | */ 38 | public timeEnd(id:string) { 39 | const time = this.timers.get(id); 40 | const key = this.keys.get(id); 41 | 42 | if (!this.timers.delete(id) || !this.keys.delete(id)) { 43 | return this.error('Id does not exist'); 44 | } 45 | 46 | this.logInColor(`${key} - ${new Date().getTime() - time.getTime()} ms`, 'green'); 47 | } 48 | 49 | /** 50 | * For important commands 51 | * @param message 52 | */ 53 | public info(message?:string) { 54 | this.logInColor(message, 'green'); 55 | } 56 | 57 | /** 58 | * When state is invalid, but finalConfig no error 59 | * @param message 60 | */ 61 | public warn(message?:string) { 62 | this.logInColor(message, 'yellow'); 63 | } 64 | 65 | /** 66 | * Prints errors if present 67 | * @param err 68 | */ 69 | public error(err?:any) { 70 | if (err) this.logInColor(`ERROR: ${err.stack ? err.stack : err}`, 'red'); 71 | } 72 | 73 | private static generateId() { 74 | return Math.random().toString(); 75 | } 76 | 77 | private logInColor(message:string, color:string) { 78 | console.log(this.getFormattedMessageInColor(color, message)); 79 | } 80 | 81 | private getFormattedMessageInColor(color:string, message:string) { 82 | return chalk[color](this.formatMessage(message)); 83 | } 84 | 85 | private formatMessage(message:string) { 86 | return `[unix: ${new Date().getTime()}] ${this.context} - ${message}`; 87 | } 88 | } 89 | 90 | /** 91 | * Convenience function - use instead of console 92 | * @param context 93 | * @returns {Logger} 94 | */ 95 | export function loggerFor(context:string):Logger { 96 | return new Logger(context); 97 | } 98 | 99 | /** 100 | * Convenience function - use for everything that will not be saved 101 | * @param routingKey 102 | * @returns {debug.Debugger|any} 103 | */ 104 | export function debugFor(routingKey:string) { 105 | return debug(routingKey); 106 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "outDir": "./build", 5 | "rootDir": "./src", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noEmitOnError": false, 9 | "noImplicitAny": false, 10 | "removeComments": false, 11 | "preserveConstEnums": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "isolatedModules": false, 16 | "pretty": true, 17 | "sourceMap": true 18 | }, 19 | "exclude": [ 20 | "node_modules", 21 | "testdir", 22 | ".git", 23 | "typings/index.d.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chalk": "registry:npm/chalk#1.0.0+20160211003958", 4 | "moment": "registry:npm/moment#2.10.5+20160211003958", 5 | "request": "registry:npm/request#2.69.0+20160428223725" 6 | }, 7 | "globalDependencies": { 8 | "async": "registry:dt/async#1.4.2+20160602152629", 9 | "body-parser": "registry:dt/body-parser#0.0.0+20160619023215", 10 | "chai": "registry:dt/chai#3.4.0+20160601211834", 11 | "chalk": "registry:dt/chalk#0.4.0+20160317120654", 12 | "chokidar": "registry:dt/chokidar#1.4.3+20160316155526", 13 | "commander": "registry:dt/commander#2.3.0+20160317120654", 14 | "debug": "registry:dt/debug#0.0.0+20160317120654", 15 | "inquirer": "registry:dt/inquirer#0.0.0+20160316155526", 16 | "lodash": "registry:dt/lodash#4.14.0+20160802150749", 17 | "mkdirp": "registry:dt/mkdirp#0.3.0+20160317120654", 18 | "mocha": "registry:dt/mocha#2.2.5+20160619032855", 19 | "node": "registry:dt/node#6.0.0+20160813124416", 20 | "rimraf": "registry:dt/rimraf#0.0.0+20160317120654", 21 | "sinon": "registry:dt/sinon#1.16.0+20160517064723", 22 | "sinon-chai": "registry:dt/sinon-chai#2.7.0+20160317120654" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /typings/globals/async/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/2901b0c3c4e17d7b09d6b97abe12616b68ba0643/async/async.d.ts 3 | interface Dictionary { [key:string]:T; 4 | } 5 | 6 | interface ErrorCallback { (err?:Error):void; 7 | } 8 | interface AsyncResultCallback { (err:Error, result?:T):void; 9 | } 10 | interface AsyncResultArrayCallback { (err:Error, results:T[]):void; 11 | } 12 | interface AsyncResultObjectCallback { (err:Error, results:Dictionary):void; 13 | } 14 | 15 | interface AsyncFunction { (callback:(err?:Error, result?:T) => void):void; 16 | } 17 | interface AsyncIterator { (item:T, callback:ErrorCallback):void; 18 | } 19 | interface AsyncForEachOfIterator { (item:T, key:number|string, callback:ErrorCallback):void; 20 | } 21 | interface AsyncResultIterator { (item:T, callback:AsyncResultCallback):void; 22 | } 23 | interface AsyncMemoIterator { (memo:R, item:T, callback:AsyncResultCallback):void; 24 | } 25 | interface AsyncBooleanIterator { (item:T, callback:(err:string, truthValue:boolean) => void):void; 26 | } 27 | 28 | interface AsyncWorker { (task:T, callback:ErrorCallback):void; 29 | } 30 | interface AsyncVoidFunction { (callback:ErrorCallback):void; 31 | } 32 | 33 | interface AsyncQueue { 34 | length():number; 35 | started:boolean; 36 | running():number; 37 | idle():boolean; 38 | concurrency:number; 39 | push(task:T, callback?:ErrorCallback):void; 40 | push(task:T[], callback?:ErrorCallback):void; 41 | unshift(task:T, callback?:ErrorCallback):void; 42 | unshift(task:T[], callback?:ErrorCallback):void; 43 | saturated:() => any; 44 | empty:() => any; 45 | drain:() => any; 46 | paused:boolean; 47 | pause():void 48 | resume():void; 49 | kill():void; 50 | } 51 | 52 | interface AsyncPriorityQueue { 53 | length():number; 54 | concurrency:number; 55 | started:boolean; 56 | paused:boolean; 57 | push(task:T, priority:number, callback?:AsyncResultArrayCallback):void; 58 | push(task:T[], priority:number, callback?:AsyncResultArrayCallback):void; 59 | saturated:() => any; 60 | empty:() => any; 61 | drain:() => any; 62 | running():number; 63 | idle():boolean; 64 | pause():void; 65 | resume():void; 66 | kill():void; 67 | } 68 | 69 | interface AsyncCargo { 70 | length():number; 71 | payload:number; 72 | push(task:any, callback?:Function):void; 73 | push(task:any[], callback?:Function):void; 74 | saturated():void; 75 | empty():void; 76 | drain():void; 77 | idle():boolean; 78 | pause():void; 79 | resume():void; 80 | kill():void; 81 | } 82 | 83 | interface Async { 84 | 85 | // Collections 86 | each(arr:T[], iterator:AsyncIterator, callback?:ErrorCallback):void; 87 | eachSeries(arr:T[], iterator:AsyncIterator, callback?:ErrorCallback):void; 88 | eachLimit(arr:T[], limit:number, iterator:AsyncIterator, callback?:ErrorCallback):void; 89 | forEachOf(obj:any, iterator:(item:any, key:string|number, callback?:ErrorCallback) => void, callback:ErrorCallback):void; 90 | forEachOf(obj:T[], iterator:AsyncForEachOfIterator, callback?:ErrorCallback):void; 91 | forEachOfSeries(obj:any, iterator:(item:any, key:string|number, callback?:ErrorCallback) => void, callback:ErrorCallback):void; 92 | forEachOfSeries(obj:T[], iterator:AsyncForEachOfIterator, callback?:ErrorCallback):void; 93 | forEachOfLimit(obj:any, limit:number, iterator:(item:any, key:string|number, callback?:ErrorCallback) => void, callback:ErrorCallback):void; 94 | forEachOfLimit(obj:T[], limit:number, iterator:AsyncForEachOfIterator, callback?:ErrorCallback):void; 95 | map(arr:T[], iterator:AsyncResultIterator, callback?:AsyncResultArrayCallback):any; 96 | mapSeries(arr:T[], iterator:AsyncResultIterator, callback?:AsyncResultArrayCallback):any; 97 | mapLimit(arr:T[], limit:number, iterator:AsyncResultIterator, callback?:AsyncResultArrayCallback):any; 98 | filter(arr:T[], iterator:AsyncBooleanIterator, callback?:AsyncResultArrayCallback):any; 99 | select(arr:T[], iterator:AsyncBooleanIterator, callback?:AsyncResultArrayCallback):any; 100 | filterSeries(arr:T[], iterator:AsyncBooleanIterator, callback?:AsyncResultArrayCallback):any; 101 | selectSeries(arr:T[], iterator:AsyncBooleanIterator, callback?:AsyncResultArrayCallback):any; 102 | filterLimit(arr:T[], limit:number, iterator:AsyncBooleanIterator, callback?:AsyncResultArrayCallback):any; 103 | selectLimit(arr:T[], limit:number, iterator:AsyncBooleanIterator, callback?:AsyncResultArrayCallback):any; 104 | reject(arr:T[], iterator:AsyncBooleanIterator, callback?:AsyncResultArrayCallback):any; 105 | rejectSeries(arr:T[], iterator:AsyncBooleanIterator, callback?:AsyncResultArrayCallback):any; 106 | rejectLimit(arr:T[], limit:number, iterator:AsyncBooleanIterator, callback?:AsyncResultArrayCallback):any; 107 | reduce(arr:T[], memo:R, iterator:AsyncMemoIterator, callback?:AsyncResultCallback):any; 108 | inject(arr:T[], memo:R, iterator:AsyncMemoIterator, callback?:AsyncResultCallback):any; 109 | foldl(arr:T[], memo:R, iterator:AsyncMemoIterator, callback?:AsyncResultCallback):any; 110 | reduceRight(arr:T[], memo:R, iterator:AsyncMemoIterator, callback:AsyncResultCallback):any; 111 | foldr(arr:T[], memo:R, iterator:AsyncMemoIterator, callback:AsyncResultCallback):any; 112 | detect(arr:T[], iterator:AsyncBooleanIterator, callback?:AsyncResultCallback):any; 113 | detectSeries(arr:T[], iterator:AsyncBooleanIterator, callback?:AsyncResultCallback):any; 114 | detectLimit(arr:T[], limit:number, iterator:AsyncBooleanIterator, callback?:AsyncResultCallback):any; 115 | sortBy(arr:T[], iterator:AsyncResultIterator, callback?:AsyncResultArrayCallback):any; 116 | some(arr:T[], iterator:AsyncBooleanIterator, callback?:(result:boolean) => void):any; 117 | someLimit(arr:T[], limit:number, iterator:AsyncBooleanIterator, callback?:(result:boolean) => void):any; 118 | any(arr:T[], iterator:AsyncBooleanIterator, callback?:(result:boolean) => void):any; 119 | every(arr:T[], iterator:AsyncBooleanIterator, callback?:(result:boolean) => any):any; 120 | everyLimit(arr:T[], limit:number, iterator:AsyncBooleanIterator, callback?:(result:boolean) => any):any; 121 | all(arr:T[], iterator:AsyncBooleanIterator, callback?:(result:boolean) => any):any; 122 | concat(arr:T[], iterator:AsyncResultIterator, callback?:AsyncResultArrayCallback):any; 123 | concatSeries(arr:T[], iterator:AsyncResultIterator, callback?:AsyncResultArrayCallback):any; 124 | 125 | // Control Flow 126 | series(tasks:AsyncFunction[], callback?:AsyncResultArrayCallback):void; 127 | series(tasks:Dictionary>, callback?:AsyncResultObjectCallback):void; 128 | parallel(tasks:Array>, callback?:AsyncResultArrayCallback):void; 129 | parallel(tasks:Dictionary>, callback?:AsyncResultObjectCallback):void; 130 | parallelLimit(tasks:Array>, limit:number, callback?:AsyncResultArrayCallback):void; 131 | parallelLimit(tasks:Dictionary>, limit:number, callback?:AsyncResultObjectCallback):void; 132 | whilst(test:() => boolean, fn:AsyncVoidFunction, callback:(err:any) => void):void; 133 | doWhilst(fn:AsyncVoidFunction, test:() => boolean, callback:(err:any) => void):void; 134 | until(test:() => boolean, fn:AsyncVoidFunction, callback:(err:any) => void):void; 135 | doUntil(fn:AsyncVoidFunction, test:() => boolean, callback:(err:any) => void):void; 136 | during(test:(testCallback:(error:Error, truth:boolean) => void) => void, fn:AsyncVoidFunction, callback:(err:any) => void):void; 137 | doDuring(fn:AsyncVoidFunction, test:(testCallback:(error:Error, truth:boolean) => void) => void, callback:(err:any) => void):void; 138 | forever(next:(errCallback:(err:Error) => void) => void, errBack:(err:Error) => void):void; 139 | waterfall(tasks:Function[], callback?:(err:Error, results?:any) => void):void; 140 | compose(...fns:Function[]):Function; 141 | seq(...fns:Function[]):Function; 142 | applyEach(fns:Function[], argsAndCallback:any[]):void; // applyEach(fns, args..., callback). TS does not support ... for a middle argument. Callback is optional. 143 | applyEachSeries(fns:Function[], argsAndCallback:any[]):void; // applyEachSeries(fns, args..., callback). TS does not support ... for a middle argument. Callback is optional. 144 | queue(worker:AsyncWorker, concurrency?:number):AsyncQueue; 145 | priorityQueue(worker:AsyncWorker, concurrency:number):AsyncPriorityQueue; 146 | cargo(worker:(tasks:any[], callback:ErrorCallback) => void, payload?:number):AsyncCargo; 147 | auto(tasks:any, callback?:(error:Error, results:any) => void):void; 148 | retry(opts:number, task:(callback:AsyncResultCallback, results:any) => void, callback:(error:Error, results:any) => void):void; 149 | retry(opts:{ times:number, interval:number|((retryCount:number) => number) }, task:(callback:AsyncResultCallback, results:any) => void, callback:(error:Error, results:any) => void):void; 150 | iterator(tasks:Function[]):Function; 151 | apply(fn:Function, ...arguments:any[]):AsyncFunction; 152 | nextTick(callback:Function):void; 153 | setImmediate(callback:Function):void; 154 | 155 | times (n:number, iterator:AsyncResultIterator, callback:AsyncResultArrayCallback):void; 156 | timesSeries(n:number, iterator:AsyncResultIterator, callback:AsyncResultArrayCallback):void; 157 | timesLimit(n:number, limit:number, iterator:AsyncResultIterator, callback:AsyncResultArrayCallback):void; 158 | timeout(n:T, timeout:number, value:any):T; 159 | 160 | // Utils 161 | memoize(fn:Function, hasher?:Function):Function; 162 | unmemoize(fn:Function):Function; 163 | ensureAsync(fn:(...argsAndCallback:any[]) => void):Function; 164 | constant(...values:any[]):Function; 165 | asyncify(fn:Function):Function; 166 | wrapSync(fn:Function):Function; 167 | log(fn:Function, ...arguments:any[]):void; 168 | dir(fn:Function, ...arguments:any[]):void; 169 | noConflict():Async; 170 | } 171 | 172 | declare var async:Async; 173 | 174 | declare module "async" { 175 | export = async; 176 | } 177 | -------------------------------------------------------------------------------- /typings/globals/async/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/2901b0c3c4e17d7b09d6b97abe12616b68ba0643/async/async.d.ts", 5 | "raw": "registry:dt/async#1.4.2+20160602152629", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/2901b0c3c4e17d7b09d6b97abe12616b68ba0643/async/async.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/chai/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7273c57f5b35de28b77649d9160f557906a95c68/chai/chai.d.ts", 5 | "raw": "registry:dt/chai#3.4.0+20160601211834", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7273c57f5b35de28b77649d9160f557906a95c68/chai/chai.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/chalk/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/chalk/chalk.d.ts 3 | declare namespace Chalk { 4 | 5 | export var enabled: boolean; 6 | export var supportsColor: boolean; 7 | export var styles: ChalkStyleMap; 8 | 9 | export function stripColor(value: string): any; 10 | export function hasColor(str: string): boolean; 11 | 12 | export interface ChalkChain extends ChalkStyle { 13 | (...text: string[]): string; 14 | } 15 | 16 | export interface ChalkStyleElement { 17 | open: string; 18 | close: string; 19 | } 20 | 21 | // General 22 | export var reset: ChalkChain; 23 | export var bold: ChalkChain; 24 | export var italic: ChalkChain; 25 | export var underline: ChalkChain; 26 | export var inverse: ChalkChain; 27 | export var strikethrough: ChalkChain; 28 | 29 | // Text colors 30 | export var black: ChalkChain; 31 | export var red: ChalkChain; 32 | export var green: ChalkChain; 33 | export var yellow: ChalkChain; 34 | export var blue: ChalkChain; 35 | export var magenta: ChalkChain; 36 | export var cyan: ChalkChain; 37 | export var white: ChalkChain; 38 | export var gray: ChalkChain; 39 | export var grey: ChalkChain; 40 | 41 | // Background colors 42 | export var bgBlack: ChalkChain; 43 | export var bgRed: ChalkChain; 44 | export var bgGreen: ChalkChain; 45 | export var bgYellow: ChalkChain; 46 | export var bgBlue: ChalkChain; 47 | export var bgMagenta: ChalkChain; 48 | export var bgCyan: ChalkChain; 49 | export var bgWhite: ChalkChain; 50 | 51 | 52 | export interface ChalkStyle { 53 | // General 54 | reset: ChalkChain; 55 | bold: ChalkChain; 56 | italic: ChalkChain; 57 | underline: ChalkChain; 58 | inverse: ChalkChain; 59 | strikethrough: ChalkChain; 60 | 61 | // Text colors 62 | black: ChalkChain; 63 | red: ChalkChain; 64 | green: ChalkChain; 65 | yellow: ChalkChain; 66 | blue: ChalkChain; 67 | magenta: ChalkChain; 68 | cyan: ChalkChain; 69 | white: ChalkChain; 70 | gray: ChalkChain; 71 | grey: ChalkChain; 72 | 73 | // Background colors 74 | bgBlack: ChalkChain; 75 | bgRed: ChalkChain; 76 | bgGreen: ChalkChain; 77 | bgYellow: ChalkChain; 78 | bgBlue: ChalkChain; 79 | bgMagenta: ChalkChain; 80 | bgCyan: ChalkChain; 81 | bgWhite: ChalkChain; 82 | } 83 | 84 | export interface ChalkStyleMap { 85 | // General 86 | reset: ChalkStyleElement; 87 | bold: ChalkStyleElement; 88 | italic: ChalkStyleElement; 89 | underline: ChalkStyleElement; 90 | inverse: ChalkStyleElement; 91 | strikethrough: ChalkStyleElement; 92 | 93 | // Text colors 94 | black: ChalkStyleElement; 95 | red: ChalkStyleElement; 96 | green: ChalkStyleElement; 97 | yellow: ChalkStyleElement; 98 | blue: ChalkStyleElement; 99 | magenta: ChalkStyleElement; 100 | cyan: ChalkStyleElement; 101 | white: ChalkStyleElement; 102 | gray: ChalkStyleElement; 103 | 104 | // Background colors 105 | bgBlack: ChalkStyleElement; 106 | bgRed: ChalkStyleElement; 107 | bgGreen: ChalkStyleElement; 108 | bgYellow: ChalkStyleElement; 109 | bgBlue: ChalkStyleElement; 110 | bgMagenta: ChalkStyleElement; 111 | bgCyan: ChalkStyleElement; 112 | bgWhite: ChalkStyleElement; 113 | } 114 | } 115 | 116 | declare module "chalk" { 117 | export = Chalk; 118 | } 119 | -------------------------------------------------------------------------------- /typings/globals/chalk/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/chalk/chalk.d.ts", 5 | "raw": "registry:dt/chalk#0.4.0+20160317120654", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/chalk/chalk.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/chokidar/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/chokidar/chokidar.d.ts 3 | declare module "fs" 4 | { 5 | interface FSWatcher 6 | { 7 | add(fileDirOrGlob:string):void; 8 | add(filesDirsOrGlobs:Array):void; 9 | unwatch(fileDirOrGlob:string):void; 10 | unwatch(filesDirsOrGlobs:Array):void; 11 | getWatched():any; 12 | } 13 | } 14 | 15 | declare module "chokidar" 16 | { 17 | interface WatchOptions 18 | { 19 | persistent?:boolean; 20 | ignored?:any; 21 | ignoreInitial?:boolean; 22 | followSymlinks?:boolean; 23 | cwd?:string; 24 | usePolling?:boolean; 25 | useFsEvents?:boolean; 26 | alwaysStat?:boolean; 27 | depth?:number; 28 | interval?:number; 29 | binaryInterval?:number; 30 | ignorePermissionErrors?:boolean; 31 | atomic?:boolean; 32 | awaitWriteFinish?:any; 33 | } 34 | 35 | import fs = require("fs"); 36 | 37 | function watch(fileDirOrGlob:string, options?:WatchOptions):fs.FSWatcher; 38 | function watch(filesDirsOrGlobs:Array, options?:WatchOptions):fs.FSWatcher; 39 | } 40 | -------------------------------------------------------------------------------- /typings/globals/chokidar/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/chokidar/chokidar.d.ts", 5 | "raw": "registry:dt/chokidar#1.4.3+20160316155526", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/chokidar/chokidar.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/commander/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/commander/commander.d.ts", 5 | "raw": "registry:dt/commander#2.3.0+20160317120654", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/commander/commander.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/debug/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/debug/debug.d.ts 3 | declare var debug: debug.IDebug; 4 | 5 | // Support AMD require 6 | declare module 'debug' { 7 | export = debug; 8 | } 9 | 10 | declare namespace debug { 11 | export interface IDebug { 12 | (namespace: string): debug.IDebugger, 13 | coerce: (val: any) => any, 14 | disable: () => void, 15 | enable: (namespaces: string) => void, 16 | enabled: (namespaces: string) => boolean, 17 | 18 | names: string[], 19 | skips: string[], 20 | 21 | formatters: IFormatters 22 | } 23 | 24 | export interface IFormatters { 25 | [formatter: string]: Function 26 | } 27 | 28 | export interface IDebugger { 29 | (formatter: any, ...args: any[]): void; 30 | 31 | enabled: boolean; 32 | log: Function; 33 | namespace: string; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /typings/globals/debug/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/debug/debug.d.ts", 5 | "raw": "registry:dt/debug#0.0.0+20160317120654", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/debug/debug.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/es6-collections/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/es6-collections/es6-collections.d.ts 3 | interface IteratorResult { 4 | done: boolean; 5 | value?: T; 6 | } 7 | 8 | interface Iterator { 9 | next(value?: any): IteratorResult; 10 | return?(value?: any): IteratorResult; 11 | throw?(e?: any): IteratorResult; 12 | } 13 | 14 | interface ForEachable { 15 | forEach(callbackfn: (value: T) => void): void; 16 | } 17 | 18 | interface Map { 19 | clear(): void; 20 | delete(key: K): boolean; 21 | forEach(callbackfn: (value: V, index: K, map: Map) => void, thisArg?: any): void; 22 | get(key: K): V; 23 | has(key: K): boolean; 24 | set(key: K, value?: V): Map; 25 | entries(): Iterator<[K, V]>; 26 | keys(): Iterator; 27 | values(): Iterator; 28 | size: number; 29 | } 30 | 31 | interface MapConstructor { 32 | new (): Map; 33 | new (iterable: ForEachable<[K, V]>): Map; 34 | prototype: Map; 35 | } 36 | 37 | declare var Map: MapConstructor; 38 | 39 | interface Set { 40 | add(value: T): Set; 41 | clear(): void; 42 | delete(value: T): boolean; 43 | forEach(callbackfn: (value: T, index: T, set: Set) => void, thisArg?: any): void; 44 | has(value: T): boolean; 45 | entries(): Iterator<[T, T]>; 46 | keys(): Iterator; 47 | values(): Iterator; 48 | size: number; 49 | } 50 | 51 | interface SetConstructor { 52 | new (): Set; 53 | new (iterable: ForEachable): Set; 54 | prototype: Set; 55 | } 56 | 57 | declare var Set: SetConstructor; 58 | 59 | interface WeakMap { 60 | delete(key: K): boolean; 61 | clear(): void; 62 | get(key: K): V; 63 | has(key: K): boolean; 64 | set(key: K, value?: V): WeakMap; 65 | } 66 | 67 | interface WeakMapConstructor { 68 | new (): WeakMap; 69 | new (iterable: ForEachable<[K, V]>): WeakMap; 70 | prototype: WeakMap; 71 | } 72 | 73 | declare var WeakMap: WeakMapConstructor; 74 | 75 | interface WeakSet { 76 | delete(value: T): boolean; 77 | clear(): void; 78 | add(value: T): WeakSet; 79 | has(value: T): boolean; 80 | } 81 | 82 | interface WeakSetConstructor { 83 | new (): WeakSet; 84 | new (iterable: ForEachable): WeakSet; 85 | prototype: WeakSet; 86 | } 87 | 88 | declare var WeakSet: WeakSetConstructor; 89 | 90 | declare module "es6-collections" { 91 | var Map: MapConstructor; 92 | var Set: SetConstructor; 93 | var WeakMap: WeakMapConstructor; 94 | var WeakSet: WeakSetConstructor; 95 | } 96 | -------------------------------------------------------------------------------- /typings/globals/es6-collections/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/es6-collections/es6-collections.d.ts", 5 | "raw": "registry:dt/es6-collections#0.5.1+20160316155526", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/es6-collections/es6-collections.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/es6-promise/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/71c9d2336c0c802f89d530e07563e00b9ac07792/es6-promise/es6-promise.d.ts 3 | interface Thenable { 4 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => U | Thenable): Thenable; 5 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => void): Thenable; 6 | } 7 | 8 | declare class Promise implements Thenable { 9 | /** 10 | * If you call resolve in the body of the callback passed to the constructor, 11 | * your promise is fulfilled with result object passed to resolve. 12 | * If you call reject your promise is rejected with the object passed to reject. 13 | * For consistency and debugging (eg stack traces), obj should be an instanceof Error. 14 | * Any errors thrown in the constructor callback will be implicitly passed to reject(). 15 | */ 16 | constructor(callback: (resolve : (value?: T | Thenable) => void, reject: (error?: any) => void) => void); 17 | 18 | /** 19 | * onFulfilled is called when/if "promise" resolves. onRejected is called when/if "promise" rejects. 20 | * Both are optional, if either/both are omitted the next onFulfilled/onRejected in the chain is called. 21 | * Both callbacks have a single parameter , the fulfillment value or rejection reason. 22 | * "then" returns a new promise equivalent to the value you return from onFulfilled/onRejected after being passed through Promise.resolve. 23 | * If an error is thrown in the callback, the returned promise rejects with that error. 24 | * 25 | * @param onFulfilled called when/if "promise" resolves 26 | * @param onRejected called when/if "promise" rejects 27 | */ 28 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => U | Thenable): Promise; 29 | then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => void): Promise; 30 | 31 | /** 32 | * Sugar for promise.then(undefined, onRejected) 33 | * 34 | * @param onRejected called when/if "promise" rejects 35 | */ 36 | catch(onRejected?: (error: any) => U | Thenable): Promise; 37 | } 38 | 39 | declare namespace Promise { 40 | /** 41 | * Make a new promise from the thenable. 42 | * A thenable is promise-like in as far as it has a "then" method. 43 | */ 44 | function resolve(value?: T | Thenable): Promise; 45 | 46 | /** 47 | * Make a promise that rejects to obj. For consistency and debugging (eg stack traces), obj should be an instanceof Error 48 | */ 49 | function reject(error: any): Promise; 50 | function reject(error: T): Promise; 51 | 52 | /** 53 | * Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects. 54 | * the array passed to all can be a mixture of promise-like objects and other objects. 55 | * The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value. 56 | */ 57 | function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable , T5 | Thenable, T6 | Thenable, T7 | Thenable, T8 | Thenable, T9 | Thenable, T10 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>; 58 | function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable , T5 | Thenable, T6 | Thenable, T7 | Thenable, T8 | Thenable, T9 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9]>; 59 | function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable , T5 | Thenable, T6 | Thenable, T7 | Thenable, T8 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8]>; 60 | function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable , T5 | Thenable, T6 | Thenable, T7 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6, T7]>; 61 | function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable , T5 | Thenable, T6 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6]>; 62 | function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable , T5 | Thenable]): Promise<[T1, T2, T3, T4, T5]>; 63 | function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable ]): Promise<[T1, T2, T3, T4]>; 64 | function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable]): Promise<[T1, T2, T3]>; 65 | function all(values: [T1 | Thenable, T2 | Thenable]): Promise<[T1, T2]>; 66 | function all(values: (T | Thenable)[]): Promise; 67 | 68 | /** 69 | * Make a Promise that fulfills when any item fulfills, and rejects if any item rejects. 70 | */ 71 | function race(promises: (T | Thenable)[]): Promise; 72 | } 73 | 74 | declare module 'es6-promise' { 75 | var foo: typeof Promise; // Temp variable to reference Promise in local context 76 | namespace rsvp { 77 | export var Promise: typeof foo; 78 | export function polyfill(): void; 79 | } 80 | export = rsvp; 81 | } 82 | -------------------------------------------------------------------------------- /typings/globals/es6-promise/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/71c9d2336c0c802f89d530e07563e00b9ac07792/es6-promise/es6-promise.d.ts", 5 | "raw": "registry:dt/es6-promise#0.0.0+20160614011821", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/71c9d2336c0c802f89d530e07563e00b9ac07792/es6-promise/es6-promise.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/git-rev-sync/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'git-rev-sync' { 2 | namespace git { 3 | export function long():string 4 | } 5 | 6 | export = git 7 | } 8 | -------------------------------------------------------------------------------- /typings/globals/inquirer/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/inquirer/inquirer.d.ts", 5 | "raw": "registry:dt/inquirer#0.0.0+20160316155526", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/inquirer/inquirer.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/lodash/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/25b78b03bc815ea2d19b2e1a100988a8303f931e/lodash/lodash.d.ts", 5 | "raw": "registry:dt/lodash#4.14.0+20160802150749", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/25b78b03bc815ea2d19b2e1a100988a8303f931e/lodash/lodash.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/mkdirp/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/mkdirp/mkdirp.d.ts 3 | declare module 'mkdirp' { 4 | 5 | function mkdirp(dir: string, cb: (err: any, made: string) => void): void; 6 | function mkdirp(dir: string, flags: any, cb: (err: any, made: string) => void): void; 7 | 8 | namespace mkdirp { 9 | function sync(dir: string, flags?: any): string; 10 | } 11 | export = mkdirp; 12 | } 13 | -------------------------------------------------------------------------------- /typings/globals/mkdirp/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/mkdirp/mkdirp.d.ts", 5 | "raw": "registry:dt/mkdirp#0.3.0+20160317120654", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/mkdirp/mkdirp.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/mocha/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/b1daff0be8fa53f645365303d8e0145d055370e9/mocha/mocha.d.ts 3 | interface MochaSetupOptions { 4 | //milliseconds to wait before considering a test slow 5 | slow?: number; 6 | 7 | // timeout in milliseconds 8 | timeout?: number; 9 | 10 | // ui name "bdd", "tdd", "exports" etc 11 | ui?: string; 12 | 13 | //array of accepted globals 14 | globals?: any[]; 15 | 16 | // reporter instance (function or string), defaults to `mocha.reporters.Spec` 17 | reporter?: any; 18 | 19 | // bail on the first test failure 20 | bail?: boolean; 21 | 22 | // ignore global leaks 23 | ignoreLeaks?: boolean; 24 | 25 | // grep string or regexp to filter tests with 26 | grep?: any; 27 | } 28 | 29 | interface MochaDone { 30 | (error?: Error): void; 31 | } 32 | 33 | declare var mocha: Mocha; 34 | declare var describe: Mocha.IContextDefinition; 35 | declare var xdescribe: Mocha.IContextDefinition; 36 | // alias for `describe` 37 | declare var context: Mocha.IContextDefinition; 38 | // alias for `describe` 39 | declare var suite: Mocha.IContextDefinition; 40 | declare var it: Mocha.ITestDefinition; 41 | declare var xit: Mocha.ITestDefinition; 42 | // alias for `it` 43 | declare var test: Mocha.ITestDefinition; 44 | declare var specify: Mocha.ITestDefinition; 45 | 46 | declare function before(action: () => void): void; 47 | 48 | declare function before(action: (done: MochaDone) => void): void; 49 | 50 | declare function before(description: string, action: () => void): void; 51 | 52 | declare function before(description: string, action: (done: MochaDone) => void): void; 53 | 54 | declare function setup(action: () => void): void; 55 | 56 | declare function setup(action: (done: MochaDone) => void): void; 57 | 58 | declare function after(action: () => void): void; 59 | 60 | declare function after(action: (done: MochaDone) => void): void; 61 | 62 | declare function after(description: string, action: () => void): void; 63 | 64 | declare function after(description: string, action: (done: MochaDone) => void): void; 65 | 66 | declare function teardown(action: () => void): void; 67 | 68 | declare function teardown(action: (done: MochaDone) => void): void; 69 | 70 | declare function beforeEach(action: () => void): void; 71 | 72 | declare function beforeEach(action: (done: MochaDone) => void): void; 73 | 74 | declare function beforeEach(description: string, action: () => void): void; 75 | 76 | declare function beforeEach(description: string, action: (done: MochaDone) => void): void; 77 | 78 | declare function suiteSetup(action: () => void): void; 79 | 80 | declare function suiteSetup(action: (done: MochaDone) => void): void; 81 | 82 | declare function afterEach(action: () => void): void; 83 | 84 | declare function afterEach(action: (done: MochaDone) => void): void; 85 | 86 | declare function afterEach(description: string, action: () => void): void; 87 | 88 | declare function afterEach(description: string, action: (done: MochaDone) => void): void; 89 | 90 | declare function suiteTeardown(action: () => void): void; 91 | 92 | declare function suiteTeardown(action: (done: MochaDone) => void): void; 93 | 94 | declare class Mocha { 95 | constructor(options?: { 96 | grep?: RegExp; 97 | ui?: string; 98 | reporter?: string; 99 | timeout?: number; 100 | bail?: boolean; 101 | }); 102 | 103 | /** Setup mocha with the given options. */ 104 | setup(options: MochaSetupOptions): Mocha; 105 | bail(value?: boolean): Mocha; 106 | addFile(file: string): Mocha; 107 | /** Sets reporter by name, defaults to "spec". */ 108 | reporter(name: string): Mocha; 109 | /** Sets reporter constructor, defaults to mocha.reporters.Spec. */ 110 | reporter(reporter: (runner: Mocha.IRunner, options: any) => any): Mocha; 111 | ui(value: string): Mocha; 112 | grep(value: string): Mocha; 113 | grep(value: RegExp): Mocha; 114 | invert(): Mocha; 115 | ignoreLeaks(value: boolean): Mocha; 116 | checkLeaks(): Mocha; 117 | /** 118 | * Function to allow assertion libraries to throw errors directly into mocha. 119 | * This is useful when running tests in a browser because window.onerror will 120 | * only receive the 'message' attribute of the Error. 121 | */ 122 | throwError(error: Error): void; 123 | /** Enables growl support. */ 124 | growl(): Mocha; 125 | globals(value: string): Mocha; 126 | globals(values: string[]): Mocha; 127 | useColors(value: boolean): Mocha; 128 | useInlineDiffs(value: boolean): Mocha; 129 | timeout(value: number): Mocha; 130 | slow(value: number): Mocha; 131 | enableTimeouts(value: boolean): Mocha; 132 | asyncOnly(value: boolean): Mocha; 133 | noHighlighting(value: boolean): Mocha; 134 | /** Runs tests and invokes `onComplete()` when finished. */ 135 | run(onComplete?: (failures: number) => void): Mocha.IRunner; 136 | } 137 | 138 | // merge the Mocha class declaration with a module 139 | declare namespace Mocha { 140 | /** Partial interface for Mocha's `Runnable` class. */ 141 | interface IRunnable { 142 | title: string; 143 | fn: Function; 144 | async: boolean; 145 | sync: boolean; 146 | timedOut: boolean; 147 | } 148 | 149 | /** Partial interface for Mocha's `Suite` class. */ 150 | interface ISuite { 151 | parent: ISuite; 152 | title: string; 153 | 154 | fullTitle(): string; 155 | } 156 | 157 | /** Partial interface for Mocha's `Test` class. */ 158 | interface ITest extends IRunnable { 159 | parent: ISuite; 160 | pending: boolean; 161 | 162 | fullTitle(): string; 163 | } 164 | 165 | /** Partial interface for Mocha's `Runner` class. */ 166 | interface IRunner {} 167 | 168 | interface IContextDefinition { 169 | (description: string, spec: () => void): ISuite; 170 | only(description: string, spec: () => void): ISuite; 171 | skip(description: string, spec: () => void): void; 172 | timeout(ms: number): void; 173 | } 174 | 175 | interface ITestDefinition { 176 | (expectation: string, assertion?: () => void): ITest; 177 | (expectation: string, assertion?: (done: MochaDone) => void): ITest; 178 | only(expectation: string, assertion?: () => void): ITest; 179 | only(expectation: string, assertion?: (done: MochaDone) => void): ITest; 180 | skip(expectation: string, assertion?: () => void): void; 181 | skip(expectation: string, assertion?: (done: MochaDone) => void): void; 182 | timeout(ms: number): void; 183 | } 184 | 185 | export module reporters { 186 | export class Base { 187 | stats: { 188 | suites: number; 189 | tests: number; 190 | passes: number; 191 | pending: number; 192 | failures: number; 193 | }; 194 | 195 | constructor(runner: IRunner); 196 | } 197 | 198 | export class Doc extends Base {} 199 | export class Dot extends Base {} 200 | export class HTML extends Base {} 201 | export class HTMLCov extends Base {} 202 | export class JSON extends Base {} 203 | export class JSONCov extends Base {} 204 | export class JSONStream extends Base {} 205 | export class Landing extends Base {} 206 | export class List extends Base {} 207 | export class Markdown extends Base {} 208 | export class Min extends Base {} 209 | export class Nyan extends Base {} 210 | export class Progress extends Base { 211 | /** 212 | * @param options.open String used to indicate the start of the progress bar. 213 | * @param options.complete String used to indicate a complete test on the progress bar. 214 | * @param options.incomplete String used to indicate an incomplete test on the progress bar. 215 | * @param options.close String used to indicate the end of the progress bar. 216 | */ 217 | constructor(runner: IRunner, options?: { 218 | open?: string; 219 | complete?: string; 220 | incomplete?: string; 221 | close?: string; 222 | }); 223 | } 224 | export class Spec extends Base {} 225 | export class TAP extends Base {} 226 | export class XUnit extends Base { 227 | constructor(runner: IRunner, options?: any); 228 | } 229 | } 230 | } 231 | 232 | declare module "mocha" { 233 | export = Mocha; 234 | } 235 | -------------------------------------------------------------------------------- /typings/globals/mocha/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/b1daff0be8fa53f645365303d8e0145d055370e9/mocha/mocha.d.ts", 5 | "raw": "registry:dt/mocha#2.2.5+20160619032855", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/b1daff0be8fa53f645365303d8e0145d055370e9/mocha/mocha.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/node/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/db7a37d18c9ebfdea8076243adf6fd23939f8430/node/node.d.ts", 5 | "raw": "registry:dt/node#6.0.0+20160813124416", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/db7a37d18c9ebfdea8076243adf6fd23939f8430/node/node.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/rimraf/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/rimraf/rimraf.d.ts 3 | declare module "rimraf" { 4 | function rimraf(path: string, callback: (error: Error) => void): void; 5 | namespace rimraf { 6 | export function sync(path: string): void; 7 | export var EMFILE_MAX: number; 8 | export var BUSYTRIES_MAX: number; 9 | } 10 | export = rimraf; 11 | } 12 | -------------------------------------------------------------------------------- /typings/globals/rimraf/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/rimraf/rimraf.d.ts", 5 | "raw": "registry:dt/rimraf#0.0.0+20160317120654", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/rimraf/rimraf.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/sinon-chai/index.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by typings 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/sinon-chai/sinon-chai.d.ts 3 | declare namespace Chai { 4 | 5 | interface LanguageChains { 6 | always: Assertion; 7 | } 8 | 9 | interface Assertion { 10 | /** 11 | * true if the spy was called at least once. 12 | */ 13 | called: Assertion; 14 | /** 15 | * @param count The number of recorded calls. 16 | */ 17 | callCount(count: number): Assertion; 18 | /** 19 | * true if the spy was called exactly once. 20 | */ 21 | calledOnce: Assertion; 22 | /** 23 | * true if the spy was called exactly twice. 24 | */ 25 | calledTwice: Assertion; 26 | /** 27 | * true if the spy was called exactly thrice. 28 | */ 29 | calledThrice: Assertion; 30 | /** 31 | * Returns true if the spy was called before anotherSpy. 32 | */ 33 | calledBefore(anotherSpy: Sinon.SinonSpy): Assertion; 34 | /** 35 | * Returns true if the spy was called after anotherSpy. 36 | */ 37 | calledAfter(anotherSpy: Sinon.SinonSpy): Assertion; 38 | /** 39 | * Returns true if spy/stub was called with the new operator. Beware that 40 | * this is inferred based on the value of the this object and the spy 41 | * function's prototype, so it may give false positives if you actively 42 | * return the right kind of object. 43 | */ 44 | calledWithNew: Assertion; 45 | /** 46 | * Returns true if context was this for this call. 47 | */ 48 | calledOn(context: any): Assertion; 49 | /** 50 | * Returns true if call received provided arguments (and possibly others). 51 | */ 52 | calledWith(...args: any[]): Assertion; 53 | /** 54 | * Returns true if call received provided arguments and no others. 55 | */ 56 | calledWithExactly(...args: any[]): Assertion; 57 | /** 58 | * Returns true if call received matching arguments (and possibly others). 59 | * This behaves the same as spyCall.calledWith(sinon.match(arg1), sinon.match(arg2), ...). 60 | */ 61 | calledWithMatch(...args: any[]): Assertion; 62 | /** 63 | * Returns true if spy returned the provided value at least once. Uses 64 | * deep comparison for objects and arrays. Use spy.returned(sinon.match.same(obj)) 65 | * for strict comparison (see matchers). 66 | */ 67 | returned(obj: any): Assertion; 68 | /** 69 | * Returns true if spy threw the provided exception object at least once. 70 | */ 71 | thrown(obj?: Error|typeof Error|string): Assertion; 72 | } 73 | } 74 | 75 | declare module "sinon-chai" { 76 | function sinonChai(chai: any, utils: any): void; 77 | namespace sinonChai { } 78 | export = sinonChai; 79 | } 80 | -------------------------------------------------------------------------------- /typings/globals/sinon-chai/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/sinon-chai/sinon-chai.d.ts", 5 | "raw": "registry:dt/sinon-chai#2.7.0+20160317120654", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/7de6c3dd94feaeb21f20054b9f30d5dabc5efabd/sinon-chai/sinon-chai.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/globals/sinon/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolution": "main", 3 | "tree": { 4 | "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/6f5c53028aaeb2f9e184561f4bf70eaa22a351c3/sinon/sinon.d.ts", 5 | "raw": "registry:dt/sinon#1.16.0+20160517064723", 6 | "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/6f5c53028aaeb2f9e184561f4bf70eaa22a351c3/sinon/sinon.d.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | --------------------------------------------------------------------------------