listen 1.3.1 → 2.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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