listen 1.3.1 → 2.0.0.beta.1

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,406 +0,0 @@
1
- require 'set'
2
- require 'find'
3
- require 'digest/sha1'
4
-
5
- module Listen
6
-
7
- # The directory record stores information about
8
- # a directory and keeps track of changes to
9
- # the structure of its childs.
10
- #
11
- class DirectoryRecord
12
- attr_reader :directory, :paths, :sha1_checksums
13
-
14
- # The default list of directories that get ignored by the listener.
15
- DEFAULT_IGNORED_DIRECTORIES = %w[.rbx .bundle .git .svn bundle log tmp vendor]
16
-
17
- # The default list of files that get ignored by the listener.
18
- DEFAULT_IGNORED_EXTENSIONS = %w[.DS_Store]
19
-
20
- # Defines the used precision based on the type of mtime returned by the
21
- # system (whether its in milliseconds or just seconds)
22
- #
23
- begin
24
- HIGH_PRECISION_SUPPORTED = File.mtime(__FILE__).to_f.to_s[-2..-1] != '.0'
25
- rescue
26
- HIGH_PRECISION_SUPPORTED = false
27
- end
28
-
29
- # Data structure used to save meta data about a path
30
- #
31
- MetaData = Struct.new(:type, :mtime)
32
-
33
- # Class methods
34
- #
35
- class << self
36
-
37
- # Creates the ignoring patterns from the default ignored
38
- # directories and extensions. It memoizes the generated patterns
39
- # to avoid unnecessary computation.
40
- #
41
- def generate_default_ignoring_patterns
42
- @@default_ignoring_patterns ||= Array.new.tap do |default_patterns|
43
- # Add directories
44
- ignored_directories = DEFAULT_IGNORED_DIRECTORIES.map { |d| Regexp.escape(d) }
45
- default_patterns << %r{^(?:#{ignored_directories.join('|')})/}
46
-
47
- # Add extensions
48
- ignored_extensions = DEFAULT_IGNORED_EXTENSIONS.map { |e| Regexp.escape(e) }
49
- default_patterns << %r{(?:#{ignored_extensions.join('|')})$}
50
- end
51
- end
52
- end
53
-
54
- # Initializes a directory record.
55
- #
56
- # @option [String] directory the directory to keep track of
57
- #
58
- def initialize(directory)
59
- raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(directory)
60
-
61
- @directory, @sha1_checksums = File.expand_path(directory), Hash.new
62
- @ignoring_patterns, @filtering_patterns = Set.new, Set.new
63
-
64
- @ignoring_patterns.merge(DirectoryRecord.generate_default_ignoring_patterns)
65
- end
66
-
67
- # Returns the ignoring patterns in the record to know
68
- # which paths should be ignored.
69
- #
70
- # @return [Array<Regexp>] the ignoring patterns
71
- #
72
- def ignoring_patterns
73
- @ignoring_patterns.to_a
74
- end
75
-
76
- # Returns the filtering patterns in the record to know
77
- # which paths should be stored.
78
- #
79
- # @return [Array<Regexp>] the filtering patterns
80
- #
81
- def filtering_patterns
82
- @filtering_patterns.to_a
83
- end
84
-
85
- # Adds ignoring patterns to the record.
86
- #
87
- # @example Ignore some paths
88
- # ignore %r{^ignored/path/}, /man/
89
- #
90
- # @param [Regexp] regexps a list of patterns for ignoring paths
91
- #
92
- def ignore(*regexps)
93
- @ignoring_patterns.merge(regexps).reject! { |r| r.nil? }
94
- end
95
-
96
- # Replaces ignoring patterns in the record.
97
- #
98
- # @example Ignore only these paths
99
- # ignore! %r{^ignored/path/}, /man/
100
- #
101
- # @param [Regexp] regexps a list of patterns for ignoring paths
102
- #
103
- def ignore!(*regexps)
104
- @ignoring_patterns.replace(regexps).reject! { |r| r.nil? }
105
- end
106
-
107
- # Adds filtering patterns to the record.
108
- #
109
- # @example Filter some files
110
- # filter /\.txt$/, /.*\.zip/
111
- #
112
- # @param [Regexp] regexps a list of patterns for filtering files
113
- #
114
- def filter(*regexps)
115
- @filtering_patterns.merge(regexps).reject! { |r| r.nil? }
116
- end
117
-
118
- # Replaces filtering patterns in the record.
119
- #
120
- # @example Filter only these files
121
- # filter! /\.txt$/, /.*\.zip/
122
- #
123
- # @param [Regexp] regexps a list of patterns for filtering files
124
- #
125
- def filter!(*regexps)
126
- @filtering_patterns.replace(regexps).reject! { |r| r.nil? }
127
- end
128
-
129
- # Returns whether a path should be ignored or not.
130
- #
131
- # @param [String] path the path to test
132
- #
133
- # @return [Boolean]
134
- #
135
- def ignored?(path)
136
- path = relative_to_base(path)
137
- @ignoring_patterns.any? { |pattern| pattern =~ path }
138
- end
139
-
140
- # Returns whether a path should be filtered or not.
141
- #
142
- # @param [String] path the path to test
143
- #
144
- # @return [Boolean]
145
- #
146
- def filtered?(path)
147
- # When no filtering patterns are set, ALL files are stored.
148
- return true if @filtering_patterns.empty?
149
-
150
- path = relative_to_base(path)
151
- @filtering_patterns.any? { |pattern| pattern =~ path }
152
- end
153
-
154
- # Finds the paths that should be stored and adds them
155
- # to the paths' hash.
156
- #
157
- def build
158
- @paths = Hash.new { |h, k| h[k] = Hash.new }
159
- important_paths { |path| insert_path(path) }
160
- end
161
-
162
- # Detects changes in the passed directories, updates
163
- # the record with the new changes and returns the changes.
164
- #
165
- # @param [Array] directories the list of directories to scan for changes
166
- # @param [Hash] options
167
- # @option options [Boolean] recursive scan all sub-directories recursively
168
- # @option options [Boolean] relative_paths whether or not to use relative paths for changes
169
- #
170
- # @return [Hash<Array>] the changes
171
- #
172
- def fetch_changes(directories, options = {})
173
- @changes = { :modified => [], :added => [], :removed => [] }
174
- directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first
175
-
176
- directories.each do |directory|
177
- next unless directory[@directory] # Path is or inside directory
178
-
179
- detect_modifications_and_removals(directory, options)
180
- detect_additions(directory, options)
181
- end
182
-
183
- @changes
184
- end
185
-
186
- # Converts an absolute path to a path that's relative to the base directory.
187
- #
188
- # @param [String] path the path to convert
189
- #
190
- # @return [String] the relative path
191
- #
192
- def relative_to_base(path)
193
- path = path.dup
194
- regexp = "\\A#{Regexp.quote directory}(#{File::SEPARATOR}|\\z)"
195
- if path.respond_to?(:force_encoding)
196
- path.force_encoding("BINARY")
197
- regexp.force_encoding("BINARY")
198
- end
199
- if path.sub!(Regexp.new(regexp), '')
200
- path
201
- end
202
- end
203
-
204
- private
205
-
206
- # Detects modifications and removals recursively in a directory.
207
- #
208
- # @note Modifications detection begins by checking the modification time (mtime)
209
- # of files and then by checking content changes (using SHA1-checksum)
210
- # when the mtime of files is not changed.
211
- #
212
- # @param [String] directory the path to analyze
213
- # @param [Hash] options
214
- # @option options [Boolean] recursive scan all sub-directories recursively
215
- # @option options [Boolean] relative_paths whether or not to use relative paths for changes
216
- #
217
- def detect_modifications_and_removals(directory, options = {})
218
- paths[directory].each do |basename, meta_data|
219
- path = File.join(directory, basename)
220
- case meta_data.type
221
- when 'Dir'
222
- detect_modification_or_removal_for_dir(path, options)
223
- when 'File'
224
- detect_modification_or_removal_for_file(path, meta_data, options)
225
- end
226
- end
227
- end
228
-
229
- def detect_modification_or_removal_for_dir(path, options)
230
-
231
- # Directory still exists
232
- if File.directory?(path)
233
- detect_modifications_and_removals(path, options) if options[:recursive]
234
-
235
- # Directory has been removed
236
- else
237
- detect_modifications_and_removals(path, options)
238
- @paths[File.dirname(path)].delete(File.basename(path))
239
- @paths.delete("#{File.dirname(path)}/#{File.basename(path)}")
240
- end
241
- end
242
-
243
- def detect_modification_or_removal_for_file(path, meta_data, options)
244
- # File still exists
245
- if File.exist?(path)
246
- detect_modification(path, meta_data, options)
247
-
248
- # File has been removed
249
- else
250
- removal_detected(path, meta_data, options)
251
- end
252
- end
253
-
254
- def detect_modification(path, meta_data, options)
255
- new_mtime = mtime_of(path)
256
-
257
- # First check if we are in the same second (to update checksums)
258
- # before checking the time difference
259
- if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime
260
- modification_detected(path, meta_data, new_mtime, options)
261
- end
262
- end
263
-
264
- def modification_detected(path, meta_data, new_mtime, options)
265
- # Update the sha1 checksum of the file
266
- update_sha1_checksum(path)
267
-
268
- # Update the meta data of the file
269
- meta_data.mtime = new_mtime
270
- @paths[File.dirname(path)][File.basename(path)] = meta_data
271
-
272
- @changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path)
273
- end
274
-
275
- def removal_detected(path, meta_data, options)
276
- @paths[File.dirname(path)].delete(File.basename(path))
277
- @sha1_checksums.delete(path)
278
- @changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path)
279
- end
280
-
281
- # Detects additions in a directory.
282
- #
283
- # @param [String] directory the path to analyze
284
- # @param [Hash] options
285
- # @option options [Boolean] recursive scan all sub-directories recursively
286
- # @option options [Boolean] relative_paths whether or not to use relative paths for changes
287
- #
288
- def detect_additions(directory, options = {})
289
- # Don't process removed directories
290
- return unless File.exist?(directory)
291
-
292
- Find.find(directory) do |path|
293
- next if path == @directory
294
-
295
- if File.directory?(path)
296
- # Add a trailing slash to directories when checking if a directory is
297
- # ignored to optimize finding them as Find.find doesn't.
298
- if ignored?(path + File::SEPARATOR) || (directory != path && (!options[:recursive] && existing_path?(path)))
299
- Find.prune # Don't look any further into this directory.
300
- else
301
- insert_path(path)
302
- end
303
- elsif !ignored?(path) && filtered?(path) && !existing_path?(path)
304
- if File.file?(path)
305
- @changes[:added] << (options[:relative_paths] ? relative_to_base(path) : path)
306
- insert_path(path)
307
- end
308
- end
309
- end
310
- end
311
-
312
- # Returns whether or not a file's content has been modified by
313
- # comparing the SHA1-checksum to a stored one.
314
- # Ensure that the SHA1-checksum is inserted to the sha1_checksums
315
- # array for later comparaison if false.
316
- #
317
- # @param [String] path the file path
318
- #
319
- def content_modified?(path)
320
- return false unless File.ftype(path) == 'file'
321
- @sha1_checksum = sha1_checksum(path)
322
- if sha1_checksums[path] == @sha1_checksum || !sha1_checksums.key?(path)
323
- update_sha1_checksum(path)
324
- false
325
- else
326
- true
327
- end
328
- end
329
-
330
- # Inserts a SHA1-checksum path in @SHA1-checksums hash.
331
- #
332
- # @param [String] path the SHA1-checksum path to insert in @sha1_checksums.
333
- #
334
- def update_sha1_checksum(path)
335
- if @sha1_checksum ||= sha1_checksum(path)
336
- @sha1_checksums[path] = @sha1_checksum
337
- @sha1_checksum = nil
338
- end
339
- end
340
-
341
- # Returns the SHA1-checksum for the file path.
342
- #
343
- # @param [String] path the file path
344
- #
345
- def sha1_checksum(path)
346
- Digest::SHA1.file(path).to_s
347
- rescue
348
- nil
349
- end
350
-
351
- # Traverses the base directory looking for paths that should
352
- # be stored; thus paths that are filtered or not ignored.
353
- #
354
- # @yield [path] an important path
355
- #
356
- def important_paths
357
- Find.find(directory) do |path|
358
- next if path == directory
359
-
360
- if File.directory?(path)
361
- # Add a trailing slash to directories when checking if a directory is
362
- # ignored to optimize finding them as Find.find doesn't.
363
- if ignored?(path + File::SEPARATOR)
364
- Find.prune # Don't look any further into this directory.
365
- else
366
- yield(path)
367
- end
368
- elsif !ignored?(path) && filtered?(path)
369
- yield(path)
370
- end
371
- end
372
- end
373
-
374
- # Inserts a path with its type (Dir or File) in paths hash.
375
- #
376
- # @param [String] path the path to insert in @paths.
377
- #
378
- def insert_path(path)
379
- meta_data = MetaData.new
380
- meta_data.type = File.directory?(path) ? 'Dir' : 'File'
381
- meta_data.mtime = mtime_of(path) unless meta_data.type == 'Dir' # mtimes of dirs are not used yet
382
- @paths[File.dirname(path)][File.basename(path)] = meta_data
383
- rescue Errno::ENOENT
384
- end
385
-
386
- # Returns whether or not a path exists in the paths hash.
387
- #
388
- # @param [String] path the path to check
389
- #
390
- # @return [Boolean]
391
- #
392
- def existing_path?(path)
393
- paths[File.dirname(path)][File.basename(path)] != nil
394
- end
395
-
396
- # Returns the modification time of a file based on the precision defined by the system
397
- #
398
- # @param [String] file the file for which the mtime must be returned
399
- #
400
- # @return [Fixnum, Float] the mtime of the file
401
- #
402
- def mtime_of(file)
403
- File.lstat(file).mtime.send(HIGH_PRECISION_SUPPORTED ? :to_f : :to_i)
404
- end
405
- end
406
- end
@@ -1,32 +0,0 @@
1
- module Listen
2
-
3
- # Allows two threads to wait on eachother.
4
- #
5
- # @note Only two threads can be used with this Turnstile
6
- # because of the current implementation.
7
- class Turnstile
8
- attr_accessor :queue
9
-
10
- # Initialize the turnstile.
11
- #
12
- def initialize
13
- # Until Ruby offers semahpores, only queues can be used
14
- # to implement a turnstile.
15
- @queue = Queue.new
16
- end
17
-
18
- # Blocks the current thread until a signal is received.
19
- #
20
- def wait
21
- queue.pop if queue.num_waiting == 0
22
- end
23
-
24
- # Unblocks the waiting thread if any.
25
- #
26
- def signal
27
- queue.push(:dummy) if queue.num_waiting == 1
28
- end
29
-
30
- end
31
-
32
- end