UNPKG

watch-tree-maintained

Version:

Yet another library for watching FS trees. Includes a JSON-on-stdout command-line tool and {filePreexisted,allPreexistingFilesReported} events.

162 lines (117 loc) 3.75 kB
fs = require 'fs' events = require 'events' assert = require 'assert' async = require 'async' class Paths constructor: () -> @items = [] @itemsDict = {} @numItems = 0 @pos = 0 add: (x) -> assert.ok not @contains x @items.push x @numItems += 1 @itemsDict[x] = true contains: (x) -> @itemsDict[x]? next: () -> return null if @items.length == 0 @pos = (@pos + 1) % @numItems @items[@pos] exports.StatWatcher = class StatWatcher extends events.EventEmitter constructor: (top, opt) -> events.EventEmitter.call this options = opt or {} @ignore = if opt.ignore? then new RegExp opt.ignore else null @match = if opt.match? then new RegExp opt.match else null @sampleRate = if opt['sample-rate']? then (1 * opt['sample-rate']) else 5 @maxStatsPending = 10 # Does not apply to the initial scan @paths = new Paths() @paths.add top @path_mtime = {} @numStatsPending = 0 @preexistingPathsToReport = {} @numPreexistingPathsToReport = 0 pathsIn top, (paths) => for path in paths if (not @paths.contains path) and (not @ignore or not path.match @ignore) and (not @match or path.match @match) @preexistingPathsToReport[path] = true @numPreexistingPathsToReport++ @paths.add path @statPath path @intervalId = setInterval (() => @tick()), @sampleRate end: () -> clearInterval @intervalId tick: () -> if @numStatsPending <= @maxStatsPending path = @paths.next() if path @statPath path statPath: (path) -> @numStatsPending++ fs.stat path, (err, stats) => @numStatsPending-- last_mtime = @path_mtime[path] or null if err # file deleted if err.code is 'ENOENT' if last_mtime @emit 'fileDeleted', path delete @path_mtime[path] # error else throw err else @path_mtime[path] = stats.mtime # (new or modified) dir if stats.isDirectory() if (not last_mtime) or (stats.mtime > last_mtime) @scanDir path else # new file if not last_mtime eventName = 'fileCreated' if @preexistingPathsToReport[path] eventName = 'filePreexisted' delete @preexistingPathsToReport[path] @numPreexistingPathsToReport-- @emit eventName, path, stats # modified file else if stats.mtime > last_mtime @emit 'fileModified', path, stats if @numPreexistingPathsToReport == 0 @emit 'allPreexistingFilesReported' @numPreexistingPathsToReport = -1 scanDir: (path) -> fs.readdir path, (err, files) => for file in files path2 = "#{path}/#{file}" if (not @paths.contains path2) and (not @ignore or not path2.match @ignore) and (not @match or path2.match @match) @paths.add path2 @statPath path2 _pathsIn = (path, paths, callback) -> fs.readdir path, (err, files) -> # Case: file if err and err.code is 'ENOTDIR' paths.push path return callback() # Case: error throw err if err # Case: dir async.forEach( files, ((file, cb) -> _pathsIn("#{path}/#{file}", paths, cb)), ((err) -> throw err if err callback()) ) pathsIn = (dir, callback) -> paths = [] _pathsIn dir, paths, () -> callback paths