listen 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,26 +1,18 @@
1
- require 'find'
2
- require 'digest/sha1'
3
-
4
- require 'listen/adapter'
5
- require 'listen/adapters/darwin'
6
- require 'listen/adapters/linux'
7
- require 'listen/adapters/polling'
8
- require 'listen/adapters/windows'
9
-
10
1
  module Listen
11
2
  class Listener
12
- attr_accessor :directory, :ignored_paths, :file_filters, :sha1_checksums, :paths, :adapter, :paused
3
+ attr_reader :directory, :directory_record, :adapter
13
4
 
14
- # Default paths that gets ignored by the listener
15
- DEFAULT_IGNORED_PATHS = %w[.bundle .git .DS_Store log tmp vendor]
5
+ # The default value for using relative paths in the callback.
6
+ DEFAULT_TO_RELATIVE_PATHS = false
16
7
 
17
- # Initialize the file listener.
8
+ # Initializes the directory listener.
18
9
  #
19
- # @param [String, Pathname] directory the directory to watch
10
+ # @param [String] directory the directory to listen to
20
11
  # @param [Hash] options the listen options
21
12
  # @option options [String] ignore a list of paths to ignore
22
13
  # @option options [Regexp] filter a list of regexps file filters
23
14
  # @option options [Float] latency the delay between checking for changes in seconds
15
+ # @option options [Boolean] relative_paths whether or not to use relative-paths in the callback
24
16
  # @option options [Boolean] force_polling whether to force the polling adapter or not
25
17
  # @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
26
18
  #
@@ -29,81 +21,84 @@ module Listen
29
21
  # @yieldparam [Array<String>] added the list of added files
30
22
  # @yieldparam [Array<String>] removed the list of removed files
31
23
  #
32
- # @return [Listen::Listener] the file listener
33
- #
34
24
  def initialize(directory, options = {}, &block)
35
- @directory = directory
36
- @ignored_paths = DEFAULT_IGNORED_PATHS
37
- @file_filters = []
38
- @sha1_checksums = {}
39
- @block = block
40
- @ignored_paths += Array(options.delete(:ignore)) if options[:ignore]
41
- @file_filters += Array(options.delete(:filter)) if options[:filter]
25
+ @block = block
26
+ @directory = directory
27
+ @directory_record = DirectoryRecord.new(directory)
28
+ @use_relative_paths = DEFAULT_TO_RELATIVE_PATHS
29
+
30
+ @use_relative_paths = options.delete(:relative_paths) if options[:relative_paths]
31
+ @directory_record.ignore(*options.delete(:ignore)) if options[:ignore]
32
+ @directory_record.filter(*options.delete(:filter)) if options[:filter]
42
33
 
43
34
  @adapter_options = options
44
35
  end
45
36
 
46
- # Initialize the adapter and the @paths concurrently and start the adapter.
37
+ # Starts the listener by initializing the adapter and building
38
+ # the directory record concurrently, then it starts the adapter to watch
39
+ # for changes.
47
40
  #
48
- def start
49
- Thread.new { @adapter = initialize_adapter }
50
- init_paths
51
- sleep 0.01 while @adapter.nil?
52
- @adapter.start
41
+ # @param [Boolean] blocking whether or not to block the current thread after starting
42
+ #
43
+ def start(blocking = true)
44
+ t = Thread.new { @adapter = initialize_adapter }
45
+ @directory_record.build
46
+ t.join
47
+ @adapter.start(blocking)
53
48
  end
54
49
 
55
- # Stop the adapter.
50
+ # Stops the listener.
56
51
  #
57
52
  def stop
58
53
  @adapter.stop
59
54
  end
60
55
 
61
- # Pause the adapter
56
+ # Pauses the listener.
57
+ #
58
+ # @return [Listen::Listener] the listener
62
59
  #
63
60
  def pause
64
61
  @adapter.paused = true
62
+ self
65
63
  end
66
64
 
67
- # Re-initialize @paths and unpause the adapter
65
+ # Unpauses the listener.
66
+ #
67
+ # @return [Listen::Listener] the listener
68
68
  #
69
69
  def unpause
70
- init_paths
70
+ @directory_record.build
71
71
  @adapter.paused = false
72
+ self
72
73
  end
73
74
 
74
- # Is adapter paused
75
+ # Returns whether the listener is paused or not.
75
76
  #
76
77
  # @return [Boolean] adapter paused status
77
78
  #
78
79
  def paused?
79
- !@adapter.nil? && @adapter.paused == true
80
+ !!@adapter && @adapter.paused == true
80
81
  end
81
82
 
82
- # Add ignored path to the listener.
83
- #
84
- # @example Ignore some paths
85
- # ignore ".git", ".svn"
83
+ # Adds ignored paths to the listener.
86
84
  #
87
- # @param [Array<String>] paths a list of paths to ignore
85
+ # @param (see Listen::DirectoryRecord#ignore)
88
86
  #
89
- # @return [Listen::Listener] the listener itself
87
+ # @return [Listen::Listener] the listener
90
88
  #
91
89
  def ignore(*paths)
92
- @ignored_paths.push(*paths)
90
+ @directory_record.ignore(*paths)
93
91
  self
94
92
  end
95
93
 
96
- # Add file filters to the listener.
94
+ # Adds file filters to the listener.
97
95
  #
98
- # @example Filter some files
99
- # ignore /\.txt$/, /.*\.zip/
96
+ # @param (see Listen::DirectoryRecord#filter)
100
97
  #
101
- # @param [Array<Regexp>] regexps a list of regexps file filters
102
- #
103
- # @return [Listen::Listener] the listener itself
98
+ # @return [Listen::Listener] the listener
104
99
  #
105
100
  def filter(*regexps)
106
- @file_filters.push(*regexps)
101
+ @directory_record.filter(*regexps)
107
102
  self
108
103
  end
109
104
 
@@ -115,7 +110,7 @@ module Listen
115
110
  #
116
111
  # @param [Float] seconds the amount of delay, in seconds
117
112
  #
118
- # @return [Listen::Listener] the listener itself
113
+ # @return [Listen::Listener] the listener
119
114
  #
120
115
  def latency(seconds)
121
116
  @adapter_options[:latency] = seconds
@@ -128,9 +123,9 @@ module Listen
128
123
  # @example Forcing the use of the polling adapter
129
124
  # force_polling true
130
125
  #
131
- # @param [Boolean] value wheather to force the polling adapter or not
126
+ # @param [Boolean] value whether to force the polling adapter or not
132
127
  #
133
- # @return [Listen::Listener] the listener itself
128
+ # @return [Listen::Listener] the listener
134
129
  #
135
130
  def force_polling(value)
136
131
  @adapter_options[:force_polling] = value
@@ -144,212 +139,48 @@ module Listen
144
139
  #
145
140
  # @param [String, Boolean] value to change polling fallback message or remove it
146
141
  #
147
- # @return [Listen::Listener] the listener itself
142
+ # @return [Listen::Listener] the listener
148
143
  #
149
144
  def polling_fallback_message(value)
150
145
  @adapter_options[:polling_fallback_message] = value
151
146
  self
152
147
  end
153
148
 
154
- # Set change callback block to the listener.
149
+ # Sets the callback that gets called on changes.
155
150
  #
156
151
  # @example Assign a callback to be called on changes
157
152
  # callback = lambda { |modified, added, removed| ... }
158
153
  # change &callback
159
154
  #
160
- # @param [Block] block a block callback called on changes
155
+ # @param [Proc] block the callback proc
161
156
  #
162
- # @return [Listen::Listener] the listener itself
157
+ # @return [Listen::Listener] the listener
163
158
  #
164
159
  def change(&block) # modified, added, removed
165
160
  @block = block
166
161
  self
167
162
  end
168
163
 
169
- # Call @block callback when there is a diff in the passed directory.
164
+ # Runs the callback passing it the changes if there are any.
170
165
  #
171
- # @param [Array] directories the list of directories to diff
166
+ # @param (see Listen::DirectoryRecord#fetch_changes)
172
167
  #
173
- def on_change(directories, diff_options = {})
174
- changes = diff(directories, diff_options)
168
+ def on_change(directories, options = {})
169
+ changes = @directory_record.fetch_changes(directories, options.merge(
170
+ :relative_paths => @use_relative_paths
171
+ ))
175
172
  unless changes.values.all? { |paths| paths.empty? }
176
173
  @block.call(changes[:modified],changes[:added],changes[:removed])
177
174
  end
178
175
  end
179
176
 
180
- # Initialize the @paths double levels Hash with all existing paths and set diffed_at.
181
- #
182
- def init_paths
183
- @paths = Hash.new { |h,k| h[k] = {} }
184
- all_existing_paths { |path| insert_path(path) }
185
- @diffed_at = Time.now.to_i
186
- end
187
-
188
- # Detect changes diff in a directory.
189
- #
190
- # @param [Array] directories the list of directories to diff
191
- # @param [Hash] options
192
- # @option options [Boolean] recursive scan all sub-direcoties recursively (true when polling)
193
- # @return [Hash<Array>] the file changes
194
- #
195
- def diff(directories, options = {})
196
- @changes = { :modified => [], :added => [], :removed => [] }
197
- directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first
198
- directories.each do |directory|
199
- detect_modifications_and_removals(directory, options)
200
- detect_additions(directory, options)
201
- end
202
- @diffed_at = Time.now.to_i
203
- @changes
204
- end
205
-
206
- private
177
+ private
207
178
 
208
- # Initialize adapter with the listener callback and the @adapter_options
179
+ # Initializes an adapter passing it the callback and adapters' options.
209
180
  #
210
181
  def initialize_adapter
211
182
  callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) }
212
183
  Adapter.select_and_initialize(@directory, @adapter_options, &callback)
213
184
  end
214
-
215
- # Research all existing paths (directories & files) filtered and without ignored directories paths.
216
- #
217
- # @yield [path] the existing path
218
- #
219
- def all_existing_paths
220
- Find.find(@directory) do |path|
221
- next if @directory == path
222
-
223
- if File.directory?(path)
224
- if ignored_path?(path)
225
- Find.prune # Don't look any further into this directory.
226
- else
227
- yield(path)
228
- end
229
- elsif !ignored_path?(path) && filtered_file?(path)
230
- yield(path)
231
- end
232
- end
233
- end
234
-
235
- # Insert a path with its type (Dir or File) in @paths.
236
- #
237
- # @param [String] path the path to insert in @paths.
238
- #
239
- def insert_path(path)
240
- @paths[File.dirname(path)][File.basename(path)] = File.directory?(path) ? 'Dir' : 'File'
241
- end
242
-
243
- # Find is a path exists in @paths.
244
- #
245
- # @param [String] path the path to find in @paths.
246
- # @return [Boolean]
247
- #
248
- def existing_path?(path)
249
- @paths[File.dirname(path)][File.basename(path)] != nil
250
- end
251
-
252
- # Detect modifications and removals recursivly in a directory.
253
- # Modifications detection are based on mtime first and on checksum when mtime == last diffed_at
254
- #
255
- # @param [String] directory the path to analyze
256
- # @param [Hash] options
257
- # @option options [Boolean] recursive scan all sub-direcoties recursively (when polling)
258
- #
259
- def detect_modifications_and_removals(directory, options = {})
260
- @paths[directory].each do |basename, type|
261
- path = File.join(directory, basename)
262
-
263
- case type
264
- when 'Dir'
265
- if File.directory?(path)
266
- detect_modifications_and_removals(path, options) if options[:recursive]
267
- else
268
- detect_modifications_and_removals(path, :recursive => true)
269
- @paths[directory].delete(basename)
270
- @paths.delete("#{directory}/#{basename}")
271
- end
272
- when 'File'
273
- if File.exist?(path)
274
- new_mtime = File.mtime(path).to_i
275
- if @diffed_at < new_mtime || (@diffed_at == new_mtime && content_modified?(path))
276
- @changes[:modified] << relative_path(path)
277
- end
278
- else
279
- @paths[directory].delete(basename)
280
- @sha1_checksums.delete(path)
281
- @changes[:removed] << relative_path(path)
282
- end
283
- end
284
- end
285
- end
286
-
287
- # Tests if the file content has been modified by
288
- # comparing the SHA1 checksum.
289
- #
290
- # @param [String] path the file path
291
- #
292
- def content_modified?(path)
293
- sha1_checksum = Digest::SHA1.file(path).to_s
294
- if @sha1_checksums[path] != sha1_checksum
295
- @sha1_checksums[path] = sha1_checksum
296
- true
297
- else
298
- false
299
- end
300
- end
301
-
302
- # Detect additions in a directory.
303
- #
304
- # @param [String] directory the path to analyze
305
- # @param [Hash] options
306
- # @option options [Boolean] recursive scan all sub-direcoties recursively (when polling)
307
- #
308
- def detect_additions(directory, options = {})
309
- Find.find(directory) do |path|
310
- next if @directory == path
311
-
312
- if File.directory?(path)
313
- if ignored_path?(path) || (directory != path && (!options[:recursive] && existing_path?(path)))
314
- Find.prune # Don't look any further into this directory.
315
- else
316
- insert_path(path)
317
- end
318
- elsif !ignored_path?(path) && filtered_file?(path) && !existing_path?(path)
319
- @changes[:added] << relative_path(path) if File.file?(path)
320
- insert_path(path)
321
- end
322
- end
323
- end
324
-
325
- # Convert absolute path to a path relative to the listener directory (by default).
326
- #
327
- # @param [String] path the path to convert
328
- # @param [String] directory the directoy path to relative from
329
- # @return [String] the relative converted path
330
- #
331
- def relative_path(path, directory = @directory)
332
- base_dir = directory.sub(/\/$/, '')
333
- path.sub(%r(^#{base_dir}/), '')
334
- end
335
-
336
- # Test if a path should be ignored or not.
337
- #
338
- # @param [String] path the path to test.
339
- # @return [Boolean]
340
- #
341
- def ignored_path?(path)
342
- @ignored_paths.any? { |ignored_path| path =~ /#{ignored_path}$/ }
343
- end
344
-
345
- # Test if a file path should be filtered or not.
346
- #
347
- # @param [String] path the file path to test.
348
- # @return [Boolean]
349
- #
350
- def filtered_file?(path)
351
- @file_filters.empty? || @file_filters.any? { |file_filter| path =~ file_filter }
352
- end
353
-
354
185
  end
355
186
  end
@@ -0,0 +1,121 @@
1
+ module Listen
2
+ class MultiListener < Listener
3
+ attr_reader :directories, :directories_records, :adapter
4
+
5
+ # Initializes the multiple directories listener.
6
+ #
7
+ # @param [String] directories the directories to listen to
8
+ # @param [Hash] options the listen options
9
+ # @option options [String] ignore a list of paths to ignore
10
+ # @option options [Regexp] filter a list of regexps file filters
11
+ # @option options [Float] latency the delay between checking for changes in seconds
12
+ # @option options [Boolean] force_polling whether to force the polling adapter or not
13
+ # @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
14
+ #
15
+ # @yield [modified, added, removed] the changed files
16
+ # @yieldparam [Array<String>] modified the list of modified files
17
+ # @yieldparam [Array<String>] added the list of added files
18
+ # @yieldparam [Array<String>] removed the list of removed files
19
+ #
20
+ def initialize(*args, &block)
21
+ options = args.last.is_a?(Hash) ? args.pop : {}
22
+ directories = args
23
+
24
+ @block = block
25
+ @directories = directories
26
+ @directories_records = directories.map { |d| DirectoryRecord.new(d) }
27
+
28
+ ignore(*options.delete(:ignore)) if options[:ignore]
29
+ filter(*options.delete(:filter)) if options[:filter]
30
+
31
+ @adapter_options = options
32
+ end
33
+
34
+ # Starts the listener by initializing the adapter and building
35
+ # the directory record concurrently, then it starts the adapter to watch
36
+ # for changes.
37
+ #
38
+ # @param [Boolean] blocking whether or not to block the current thread after starting
39
+ #
40
+ def start(blocking = true)
41
+ t = Thread.new { @adapter = initialize_adapter }
42
+ @directories_records.each { |r| r.build }
43
+ t.join
44
+ @adapter.start(blocking)
45
+ end
46
+
47
+ # Unpauses the listener.
48
+ #
49
+ # @return [Listen::Listener] the listener
50
+ #
51
+ def unpause
52
+ @directories_records.each { |r| r.build }
53
+ @adapter.paused = false
54
+ self
55
+ end
56
+
57
+ # Adds ignored paths to the listener.
58
+ #
59
+ # @param (see Listen::DirectoryRecord#ignore)
60
+ #
61
+ # @return [Listen::Listener] the listener
62
+ #
63
+ def ignore(*paths)
64
+ @directories_records.each { |r| r.ignore(*paths) }
65
+ self
66
+ end
67
+
68
+ # Adds file filters to the listener.
69
+ #
70
+ # @param (see Listen::DirectoryRecord#filter)
71
+ #
72
+ # @return [Listen::Listener] the listener
73
+ #
74
+ def filter(*regexps)
75
+ @directories_records.each { |r| r.filter(*regexps) }
76
+ self
77
+ end
78
+
79
+ # Runs the callback passing it the changes if there are any.
80
+ #
81
+ # @param (see Listen::DirectoryRecord#fetch_changes)
82
+ #
83
+ def on_change(directories_to_search, options = {})
84
+ changes = fetch_records_changes(directories_to_search, options)
85
+ unless changes.values.all? { |paths| paths.empty? }
86
+ @block.call(changes[:modified],changes[:added],changes[:removed])
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ # Initializes an adapter passing it the callback and adapters' options.
93
+ #
94
+ def initialize_adapter
95
+ callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) }
96
+ Adapter.select_and_initialize(@directories, @adapter_options, &callback)
97
+ end
98
+
99
+ # Returns the sum of all the changes to the directories records
100
+ #
101
+ # @param (see Listen::DirectoryRecord#fetch_changes)
102
+ #
103
+ # @return [Hash] the changes
104
+ #
105
+ def fetch_records_changes(directories_to_search, options)
106
+ @directories_records.inject({}) do |h, r|
107
+ # directory records skips paths outside their range, so passing the
108
+ # whole `directories` array is not a problem.
109
+ record_changes = r.fetch_changes(directories_to_search, options.merge(:relative_paths => DEFAULT_TO_RELATIVE_PATHS))
110
+
111
+ if h.empty?
112
+ h.merge!(record_changes)
113
+ else
114
+ h.each { |k, v| h[k] += record_changes[k] }
115
+ end
116
+
117
+ h
118
+ end
119
+ end
120
+ end
121
+ end