listen 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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