forked from paulmillr/chokidar
/
index.coffee
218 lines (189 loc) · 6.86 KB
/
index.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
'use strict'
{EventEmitter} = require 'events'
fs = require 'fs'
sysPath = require 'path'
isBinary = require './is-binary'
nodeVersion = process.versions.node.substring(0, 3)
# Watches files & directories for changes.
#
# Emitted events: `add`, `change`, `unlink`, `error`.
#
# Examples
#
# var watcher = new FSWatcher()
# .add(directories)
# .on('add', function(path) {console.log('File', path, 'was added');})
# .on('change', function(path) {console.log('File', path, 'was changed');})
# .on('unlink', function(path) {console.log('File', path, 'was removed');})
#
exports.FSWatcher = class FSWatcher extends EventEmitter
constructor: (@options = {}) ->
super
@watched = Object.create(null)
@watchers = []
# Set up default options.
@options.persistent ?= no
@options.ignoreInitial ?= no
@options.ignorePermissionErrors ?= no
@options.interval ?= 100
@options.binaryInterval ?= 100
@enableBinaryInterval = @options.binaryInterval isnt @options.interval
@_ignored = do (ignored = @options.ignored) =>
switch toString.call(ignored)
when '[object RegExp]' then (string) -> ignored.test(string)
when '[object Function]' then ignored
else -> no
# You’re frozen when your heart’s not open.
Object.freeze @options
_getWatchedDir: (directory) =>
dir = directory.replace(/[\\\/]$/, '')
@watched[dir] ?= []
_addToWatchedDir: (directory, file) =>
watchedFiles = @_getWatchedDir directory
watchedFiles.push file
_removeFromWatchedDir: (directory, file) =>
watchedFiles = @_getWatchedDir directory
watchedFiles.some (watchedFile, index) =>
if watchedFile is file
watchedFiles.splice(index, 1)
yes
# Private: Check for read permissions
# Based on this answer on SO: http://stackoverflow.com/a/11781404/1358405
#
# stats - fs.Stats object
#
# Returns Boolean
_hasReadPermissions: (stats) =>
Boolean (4 & parseInt (stats.mode & 0o777).toString(8)[0])
# Private: Handles emitting unlink events for
# files and directories, and via recursion, for
# files and directories within directories that are unlinked
#
# directory - string, directory within which the following item is located
# item - string, base path of item/directory
#
# Returns nothing.
_remove: (directory, item) =>
# if what is being deleted is a directory, get that directory's paths
# for recursive deleting and cleaning of watched object
# if it is not a directory, nestedDirectoryChildren will be empty array
fullPath = sysPath.join(directory, item)
nestedDirectoryChildren = @_getWatchedDir(fullPath).slice()
# Remove directory / file from watched list.
@_removeFromWatchedDir directory, item
# Recursively remove children directories / files.
nestedDirectoryChildren.forEach (nestedItem) =>
@_remove fullPath, nestedItem
fs.unwatchFile fullPath
@emit 'unlink', fullPath
# Private: Watch file for changes with fs.watchFile or fs.watch.
#
# item - string, path to file or directory.
# callback - function that will be executed on fs change.
#
# Returns nothing.
_watch: (item, itemType, callback = (->)) =>
directory = sysPath.dirname(item)
basename = sysPath.basename(item)
parent = @_getWatchedDir directory
options = {persistent: @options.persistent}
# Prevent memory leaks.
return if parent.indexOf(basename) >= 0
@_addToWatchedDir directory, basename
if process.platform is 'win32' and nodeVersion is '0.6'
watcher = fs.watch item, options, (event, path) =>
callback item
@watchers.push watcher
else
options.interval = if @enableBinaryInterval and isBinary basename
@options.binaryInterval
else
@options.interval
fs.watchFile item, options, (curr, prev) =>
callback item if curr.mtime.getTime() isnt prev.mtime.getTime()
# Private: Emit `change` event once and watch file to emit it in the future
# once the file is changed.
#
# file - string, fs path.
#
# Returns nothing.
_handleFile: (file, initialAdd = no) =>
@_watch file, 'file', (file) =>
@emit 'change', file
@emit 'add', file unless initialAdd and @options.ignoreInitial
# Private: Read directory to add / remove files from `@watched` list
# and re-read it on change.
#
# directory - string, fs path.
#
# Returns nothing.
_handleDir: (directory) =>
read = (directory) =>
fs.readdir directory, (error, current) =>
return @emit 'error', error if error?
return unless current
previous = @_getWatchedDir(directory)
# Files that absent in current directory snapshot
# but present in previous emit `remove` event
# and are removed from @watched[directory].
previous
.filter (file) =>
current.indexOf(file) < 0
.forEach (file) =>
@_remove directory, file
# Files that present in current directory snapshot
# but absent in previous are added to watch list and
# emit `add` event.
current
.filter (file) =>
previous.indexOf(file) < 0
.forEach (file) =>
@_handle sysPath.join(directory, file), previous.length is 0
read directory
@_watch directory, 'directory', read
# Private: Handle added file or directory.
# Delegates call to _handleFile / _handleDir after checks.
#
# item - string, path to file or directory.
#
# Returns nothing.
_handle: (item, initialAdd = no) =>
# Don't handle invalid files, dotfiles etc.
return if @_ignored item
# Get the canonicalized absolute pathname.
fs.realpath item, (error, path) =>
return @emit 'error', error if error?
# Get file info, check is it file, directory or something else.
fs.stat path, (error, stats) =>
return @emit 'error', error if error?
if @options.ignorePermissionErrors and (not @_hasReadPermissions stats)
return
@_handleFile item, initialAdd if stats.isFile()
@_handleDir item if stats.isDirectory()
emit: (event, args...) ->
super
super 'all', event, args... if event in ['add', 'change', 'unlink']
# Public: Adds directories / files for tracking.
#
# * files - array of strings (file paths).
#
# Examples
#
# add ['app', 'vendor']
#
# Returns an instance of FSWatcher for chaning.
add: (files) =>
files = [files] unless Array.isArray files
files.forEach @_handle
this
# Public: Remove all listeners from watched files.
# Returns an instance of FSWatcher for chaning.
close: =>
@watchers.forEach (watcher) -> watcher.close()
Object.keys(@watched).forEach (directory) =>
@watched[directory].forEach (file) =>
fs.unwatchFile sysPath.join(directory, file)
@watched = Object.create(null)
this
exports.watch = (files, options) ->
new FSWatcher(options).add(files)