├── .gitignore ├── LICENSE ├── README.md ├── hound.js ├── package.json └── spec ├── data ├── file 1.js ├── file 2.js └── subdir 1 │ ├── subdir file 1.js │ ├── subdir file 2.js │ └── subsubdir 1 │ └── subsubdir file 1.js └── hound.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | 16 | node_modules 17 | tmp 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Michael Alexander 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the hound Project. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hound - directory tree watcher for node.js 2 | ============================================= 3 | 4 | Cross platform directory tree watcher that works, even on Windows 5 | ----------------------------------------------------------------- 6 | 7 | The philosophy of hound is: 8 | 9 | * **Be reliable, work on every platform** 10 | * **Be fast** 11 | * **Be simple** 12 | 13 | hound is designed to be very reliable, fast, and simple. There are no runtime 14 | dependencies outside of the standard node.js libraries. There is a development 15 | dependency on [Jasmine](https://jasmine.github.io/), which is required 16 | to run the tests. 17 | 18 | Installation 19 | ------------ 20 | 21 | Install using npm: 22 | 23 | ``` 24 | npm install hound 25 | ``` 26 | 27 | Because hound has no runtime dependencies, it is also possible to download the 28 | library manually and require it directly. 29 | 30 | Usage 31 | ----- 32 | 33 | ```javascript 34 | hound = require('hound') 35 | 36 | // Create a directory tree watcher. 37 | watcher = hound.watch('/tmp') 38 | 39 | // Create a file watcher. 40 | watcher = hound.watch('/tmp/file.txt') 41 | 42 | // Add callbacks for file and directory events. The change event only applies 43 | // to files. 44 | watcher.on('create', function(file, stats) { 45 | console.log(file + ' was created') 46 | }) 47 | watcher.on('change', function(file, stats) { 48 | console.log(file + ' was changed') 49 | }) 50 | watcher.on('delete', function(file) { 51 | console.log(file + ' was deleted') 52 | }) 53 | 54 | // Unwatch specific files or directories. 55 | watcher.unwatch('/tmp/another_file') 56 | 57 | // Unwatch all watched files and directories. 58 | watcher.clear() 59 | ``` 60 | 61 | Testing 62 | ------- 63 | 64 | To run the tests, use `npm test`. The tests work on actual directory trees that 65 | are generated in the tmp directory. 66 | -------------------------------------------------------------------------------- /hound.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , util = require('util') 3 | , events = require('events') 4 | , path = require('path') 5 | 6 | /** 7 | * Watch one or more files or directories for changes. 8 | * 9 | * Options: 10 | * - watchFn: Specify a custom filesystem watch function (eg: `fs.watchFile`) 11 | * 12 | * @param {string|array} src The file or directory to watch. 13 | * @param {array} options 14 | * @return {Hound} 15 | */ 16 | exports.watch = function(src, options) { 17 | var watcher = new Hound(options) 18 | watcher.watch(src) 19 | return watcher 20 | } 21 | 22 | /** 23 | * The Hound class tracks watchers and changes and emits events. 24 | */ 25 | function Hound(options) { 26 | events.EventEmitter.call(this) 27 | this.options = options || {} 28 | } 29 | util.inherits(Hound, events.EventEmitter) 30 | Hound.prototype.watchers = [] 31 | 32 | /** 33 | * Watch a file or directory tree for changes, and fire events when they happen. 34 | * Fires the following events: 35 | * 'create' (file, stats) 36 | * 'change' (file, stats) 37 | * 'delete' (file) 38 | * @param {string} src 39 | * @return {Hound} 40 | */ 41 | Hound.prototype.watch = function(src) { 42 | var self = this 43 | var stats = fs.statSync(src) 44 | var lastChange = null 45 | var watchFn = self.options.watchFn || fs.watch 46 | if (stats.isDirectory()) { 47 | var files = fs.readdirSync(src) 48 | for (var i = 0, len = files.length; i < len; i++) { 49 | self.watch(src + path.sep + files[i]) 50 | } 51 | } 52 | self.watchers[src] = watchFn(src, function(event, filename) { 53 | if (fs.existsSync(src)) { 54 | stats = fs.statSync(src) 55 | if (stats.isFile()) { 56 | if (lastChange === null || stats.mtime.getTime() > lastChange) 57 | self.emit('change', src, stats) 58 | lastChange = stats.mtime.getTime() 59 | } else if (stats.isDirectory()) { 60 | // Check if the dir is new 61 | if (self.watchers[src] === undefined) { 62 | self.emit('create', src, stats) 63 | } 64 | // Check files to see if there are any new files 65 | var dirFiles = fs.readdirSync(src) 66 | for (var i = 0, len = dirFiles.length; i < len; i++) { 67 | var file = src + path.sep + dirFiles[i] 68 | if (self.watchers[file] === undefined) { 69 | self.watch(file) 70 | self.emit('create', file, fs.statSync(file)) 71 | } 72 | } 73 | } 74 | } else { 75 | self.unwatch(src) 76 | self.emit('delete', src) 77 | } 78 | }) 79 | self.emit('watch', src) 80 | } 81 | 82 | /** 83 | * Unwatch a file or directory tree. 84 | * @param {string} src 85 | */ 86 | Hound.prototype.unwatch = function(src) { 87 | var self = this 88 | if (self.watchers[src] !== undefined) { 89 | self.watchers[src].close() 90 | delete self.watchers[src] 91 | } 92 | self.emit('unwatch', src) 93 | } 94 | 95 | /** 96 | * Unwatch all currently watched files and directories in this watcher. 97 | */ 98 | Hound.prototype.clear = function() { 99 | var self = this 100 | for (var file in this.watchers) { 101 | self.unwatch(file) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Michael Alexander (http://michaelalexander.com.au)", 3 | "name": "hound", 4 | "description": "Cross platform directory tree watcher, focussed on reliability, speed, and simplicity.", 5 | "keywords": ["directory", "tree", "watch", "windows", "cross", "platform", "reliable", "simple", "fast"], 6 | "version": "1.0.5", 7 | "homepage": "https://github.com/gforceg/node-hound", 8 | "bugs": "https://github.com/gforceg/node-hound/issues", 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/gforceg/node-hound.git" 12 | }, 13 | "main": "hound", 14 | "scripts": { 15 | "test": "node_modules/.bin/jasmine-node spec" 16 | }, 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "jasmine-node": "*" 20 | }, 21 | "optionalDependencies": {}, 22 | "engines": { 23 | "node": ">=0.7.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spec/data/file 1.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gforceg/node-hound/a918d42d8cf62e48694b62792e36130c70ce4846/spec/data/file 1.js -------------------------------------------------------------------------------- /spec/data/file 2.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gforceg/node-hound/a918d42d8cf62e48694b62792e36130c70ce4846/spec/data/file 2.js -------------------------------------------------------------------------------- /spec/data/subdir 1/subdir file 1.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gforceg/node-hound/a918d42d8cf62e48694b62792e36130c70ce4846/spec/data/subdir 1/subdir file 1.js -------------------------------------------------------------------------------- /spec/data/subdir 1/subdir file 2.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gforceg/node-hound/a918d42d8cf62e48694b62792e36130c70ce4846/spec/data/subdir 1/subdir file 2.js -------------------------------------------------------------------------------- /spec/data/subdir 1/subsubdir 1/subsubdir file 1.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gforceg/node-hound/a918d42d8cf62e48694b62792e36130c70ce4846/spec/data/subdir 1/subsubdir 1/subsubdir file 1.js -------------------------------------------------------------------------------- /spec/hound.spec.js: -------------------------------------------------------------------------------- 1 | var hound = require('../hound') 2 | , fs = require('fs') 3 | , path = require('path') 4 | , util = require('util') 5 | 6 | var testSource = path.normalize(__dirname + '/data') 7 | , testDir = path.normalize(__dirname + '/../tmp') 8 | , testFileCount = 8 9 | 10 | /** 11 | * Deletes a source recursively, used for set up. 12 | * @param {string} src 13 | */ 14 | function deleteFile(src) { 15 | if (path.sep == '\\') src = src.replace(' ', path.sep + ' ') 16 | if (!fs.existsSync(src)) return 17 | var stat = fs.statSync(src) 18 | // if (stat === undefined) return 19 | if (stat.isDirectory()) { 20 | var files = fs.readdirSync(src) 21 | for (var i = 0, len = files.length; i < len; i++) { 22 | deleteFile(src + path.sep + files[i]) 23 | } 24 | fs.rmdirSync(src) 25 | } else { 26 | fs.unlinkSync(src) 27 | } 28 | } 29 | /** 30 | * Copies a source recursively, used for set up. 31 | * @param {string} src 32 | * @param {string} src 33 | */ 34 | function copyFile(src, dest) { 35 | stat = fs.statSync(src) 36 | if (stat.isDirectory()) { 37 | if (!fs.existsSync(dest)) fs.mkdirSync(dest) 38 | var files = fs.readdirSync(src) 39 | for (var i = 0, len = files.length; i < len; i++) { 40 | copyFile(src + path.sep + files[i], dest + path.sep + files[i]) 41 | } 42 | } else { 43 | if (!fs.existsSync(dest)) { 44 | // util.pump(fs.createReadStream(src), fs.createWriteStream(dest)) 45 | fs.createReadStream(src).pipe(fs.createWriteStream(dest)) 46 | } 47 | } 48 | } 49 | 50 | describe('Hound', function() { 51 | /** 52 | * Initialise test area with files. 53 | */ 54 | beforeEach(function() { 55 | deleteFile(testDir) 56 | copyFile(testSource, testDir) 57 | }) 58 | 59 | let watcher; 60 | /** 61 | * Remove any watchers. 62 | */ 63 | afterEach(function() { 64 | if (watcher !== undefined) { 65 | watcher.clear() 66 | delete watcher 67 | } 68 | }) 69 | 70 | it('can watch a file', function() { 71 | var file = testDir + '/file 1.js' 72 | watcher = hound.watch(file) 73 | expect(watcher.watchers[file]).toBeDefined() 74 | }) 75 | 76 | it('can watch a directory', function() { 77 | watcher = hound.watch(testDir, {recurse: false}) 78 | expect(watcher.watchers[testDir]).toBeDefined() 79 | }) 80 | 81 | it('can watch a directory tree', function() { 82 | watcher = hound.watch(testDir) 83 | var watcherCount = 0 84 | for (var i in watcher.watchers) { 85 | watcherCount++ 86 | } 87 | expect(watcherCount).toBe(testFileCount) 88 | }) 89 | 90 | it('can unwatch a file', function(done) { 91 | var file = testDir + '/file 1.js' 92 | watcher = hound.watch(file) 93 | watcher.on('unwatch', function(src) { 94 | expect(src).toBe(file) 95 | expect(watcher.watchers.length).toBe(0) 96 | done() 97 | }) 98 | watcher.unwatch(file) 99 | }) 100 | 101 | it('can detect a change in a file when watching directly', function(done) { 102 | var file = testDir + '/subdir 1/subdir file 1.js' 103 | watcher = hound.watch(file) 104 | watcher.on('change', function(src) { 105 | expect(src).toBe(file) 106 | done() 107 | }) 108 | fs.writeFile(file, 'blah blah new data blah') 109 | }) 110 | 111 | it('can detect deletion of a file when watching directly', function(done) { 112 | var file = testDir + '/subdir 1/subdir file 1.js' 113 | watcher = hound.watch(file) 114 | watcher.on('delete', function(src) { 115 | expect(src).toBe(file) 116 | expect(watcher.watchers[src]).toBeUndefined() 117 | done() 118 | }) 119 | fs.unlink(file) 120 | }) 121 | 122 | it('can detect a new file in a dir', function(done) { 123 | var newFile = testDir + '/new file.js' 124 | watcher = hound.watch(testDir) 125 | watcher.on('create', function(src) { 126 | expect(src).toBe(newFile) 127 | done() 128 | }) 129 | fs.writeFile(newFile, 'blah blah blah') 130 | }) 131 | 132 | it('can detect a deleted file in a dir', function(done) { 133 | var file = testDir + '/subdir 1/subdir file 1.js' 134 | watcher = hound.watch(testDir) 135 | watcher.on('delete', function(src) { 136 | expect(src).toBe(file) 137 | expect(watcher.watchers[src]).toBeUndefined() 138 | done() 139 | }) 140 | fs.unlink(file) 141 | }) 142 | 143 | it('can detect a new file in a new dir', function(done) { 144 | var dir = testDir + '/new dir' 145 | , file = dir + '/new dir file.js' 146 | watcher = hound.watch(testDir) 147 | watcher.once('create', function(src) { 148 | expect(src).toBe(dir) 149 | watcher.once('create', function(src) { 150 | expect(src).toBe(file) 151 | done() 152 | }) 153 | fs.writeFile(file, 'bofbajojsa') 154 | }) 155 | fs.mkdirSync(dir) 156 | }) 157 | 158 | it('can specify custom watch function', function() { 159 | var file = testDir + '/file 1.js' 160 | , works = false 161 | , watchFn = function(src) { works = src } 162 | watcher = hound.watch(file, {watchFn: watchFn}) 163 | expect(works).toBe(file) 164 | }) 165 | 166 | it('shouldn\'t raise two events for one create', function(done) { 167 | var file = testDir + '/subdir 1/subdir file 5.js' 168 | var createCount = 0 169 | watcher = hound.watch(testDir) 170 | watcher.on('create', function(src) { 171 | expect(++createCount).toBeLessThan(2) 172 | setTimeout(done, 500) 173 | }) 174 | fs.writeFile(file, 'blah blah new data blah') 175 | }) 176 | 177 | it('shouldn\'t raise two events for one change', function(done) { 178 | var file = testDir + '/subdir 1/subdir file 1.js' 179 | var changeCount = 0 180 | watcher = hound.watch(testDir) 181 | watcher.on('change', function(src) { 182 | expect(++changeCount).toBeLessThan(2) 183 | setTimeout(done, 500) 184 | }) 185 | fs.writeFile(file, 'blah blah new data blah') 186 | }) 187 | 188 | it('shouldn\'t raise two events for one delete', function(done) { 189 | var file = testDir + '/subdir 1/subdir file 1.js' 190 | var deleteCount = 0 191 | watcher = hound.watch(testDir) 192 | watcher.on('delete', function(src) { 193 | expect(++deleteCount).toBeLessThan(2) 194 | setTimeout(done, 500) 195 | }) 196 | fs.unlink(file) 197 | }) 198 | }) 199 | --------------------------------------------------------------------------------