The directory record stores information about a directory and keeps track of changes to the structure of its childs.
Defines the used precision based on the type of mtime returned by the system (whether its in milliseconds or just seconds)
Data structure used to save meta data about a path
Creates the ignoring patterns from the default ignored directories and extensions. It memoizes the generated patterns to avoid unnecessary computation.
# File lib/listen/directory_record.rb, line 35 def generate_default_ignoring_patterns @@default_ignoring_patterns ||= Array.new.tap do |default_patterns| # Add directories ignored_directories = DEFAULT_IGNORED_DIRECTORIES.map { |d| Regexp.escape(d) } default_patterns << %r{^(?:#{ignored_directories.join('|')})/} # Add extensions ignored_extensions = DEFAULT_IGNORED_EXTENSIONS.map { |e| Regexp.escape(e) } default_patterns << %r{(?:#{ignored_extensions.join('|')})$} end end
Initializes a directory record.
@option [String] directory the directory to keep track of
# File lib/listen/directory_record.rb, line 52 def initialize(directory) raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(directory) @directory = directory @ignoring_patterns = Set.new @filtering_patterns = Set.new @sha1_checksums = Hash.new @ignoring_patterns.merge(DirectoryRecord.generate_default_ignoring_patterns) end
Finds the paths that should be stored and adds them to the paths' hash.
# File lib/listen/directory_record.rb, line 130 def build @paths = Hash.new { |h, k| h[k] = Hash.new } important_paths { |path| insert_path(path) } end
Detects changes in the passed directories, updates the record with the new changes and returns the changes
@param [Array] directories the list of directories scan for changes @param [Hash] options @option options [Boolean] recursive scan all sub-directories recursively @option options [Boolean] relative_paths whether or not to use relative paths for changes
@return [Hash<Array>] the changes
# File lib/listen/directory_record.rb, line 145 def fetch_changes(directories, options = {}) @changes = { :modified => [], :added => [], :removed => [] } directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first directories.each do |directory| next unless directory[@directory] # Path is or inside directory detect_modifications_and_removals(directory, options) detect_additions(directory, options) end @changes end
Adds filtering patterns to the listener.
@example Filter some files
ignore /\.txt$/, /.*\.zip/
@param [Regexp] regexp a pattern for filtering paths
# File lib/listen/directory_record.rb, line 98 def filter(*regexps) @filtering_patterns.merge(regexps) end
Returns whether a path should be filtered or not.
@param [String] path the path to test.
@return [Boolean]
# File lib/listen/directory_record.rb, line 119 def filtered?(path) # When no filtering patterns are set, ALL files are stored. return true if @filtering_patterns.empty? path = relative_to_base(path) @filtering_patterns.any? { |pattern| pattern =~ path } end
Returns the filtering patterns used in the record to know which paths should be stored.
@return [Array<Regexp>] the filtering patterns
# File lib/listen/directory_record.rb, line 76 def filtering_patterns @filtering_patterns.to_a end
Adds ignoring patterns to the record.
@example Ignore some paths
ignore %r{^ignored/path/}, /man/
@param [Regexp] regexp a pattern for ignoring paths
# File lib/listen/directory_record.rb, line 87 def ignore(*regexps) @ignoring_patterns.merge(regexps) end
Returns whether a path should be ignored or not.
@param [String] path the path to test.
@return [Boolean]
# File lib/listen/directory_record.rb, line 108 def ignored?(path) path = relative_to_base(path) @ignoring_patterns.any? { |pattern| pattern =~ path } end
Returns the ignoring patterns in the record
@return [Array<Regexp>] the ignoring patterns
# File lib/listen/directory_record.rb, line 67 def ignoring_patterns @ignoring_patterns.to_a end
Converts an absolute path to a path that's relative to the base directory.
@param [String] path the path to convert
@return [String] the relative path
# File lib/listen/directory_record.rb, line 164 def relative_to_base(path) return nil unless path[@directory] path.sub(%r{^#{Regexp.quote(@directory)}#{File::SEPARATOR}?}, '') end
Returns whether or not a file's content has been modified by comparing the SHA1-checksum to a stored one.
@param [String] path the file path
# File lib/listen/directory_record.rb, line 253 def content_modified?(path) sha1_checksum = Digest::SHA1.file(path).to_s return false if @sha1_checksums[path] == sha1_checksum @sha1_checksums.key?(path) rescue Errno::EACCES, Errno::ENOENT false ensure @sha1_checksums[path] = sha1_checksum if sha1_checksum end
Detects additions in a directory.
@param [String] directory the path to analyze @param [Hash] options @option options [Boolean] recursive scan all sub-directories recursively @option options [Boolean] relative_paths whether or not to use relative paths for changes
# File lib/listen/directory_record.rb, line 224 def detect_additions(directory, options = {}) # Don't process removed directories return unless File.exist?(directory) Find.find(directory) do |path| next if path == @directory if File.directory?(path) # Add a trailing slash to directories when checking if a directory is # ignored to optimize finding them as Find.find doesn't. if ignored?(path + File::SEPARATOR) || (directory != path && (!options[:recursive] && existing_path?(path))) Find.prune # Don't look any further into this directory. else insert_path(path) end elsif !ignored?(path) && filtered?(path) && !existing_path?(path) if File.file?(path) @changes[:added] << (options[:relative_paths] ? relative_to_base(path) : path) insert_path(path) end end end end
Detects modifications and removals recursively in a directory.
@note Modifications detection begins by checking the modification time (mtime)
of files and then by checking content changes (using SHA1-checksum) when the mtime of files is not changed.
@param [String] directory the path to analyze @param [Hash] options @option options [Boolean] recursive scan all sub-directories recursively @option options [Boolean] relative_paths whether or not to use relative paths for changes
# File lib/listen/directory_record.rb, line 182 def detect_modifications_and_removals(directory, options = {}) @paths[directory].each do |basename, meta_data| path = File.join(directory, basename) case meta_data.type when 'Dir' if File.directory?(path) detect_modifications_and_removals(path, options) if options[:recursive] else detect_modifications_and_removals(path, { :recursive => true }.merge(options)) @paths[directory].delete(basename) @paths.delete("#{directory}/#{basename}") end when 'File' if File.exist?(path) new_mtime = mtime_of(path) # First check if we are in the same second (to update checksums) # before checking the time difference if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime # Update the meta data of the files meta_data.mtime = new_mtime @paths[directory][basename] = meta_data @changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path) end else @paths[directory].delete(basename) @sha1_checksums.delete(path) @changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path) end end end end
Returns whether or not a path exists in the paths hash.
@param [String] path the path to check
@return [Boolean]
# File lib/listen/directory_record.rb, line 304 def existing_path?(path) @paths[File.dirname(path)][File.basename(path)] != nil end
Traverses the base directory looking for paths that should be stored; thus paths that are filters or not ignored.
@yield [path] an important path
# File lib/listen/directory_record.rb, line 268 def important_paths Find.find(@directory) do |path| next if path == @directory if File.directory?(path) # Add a trailing slash to directories when checking if a directory is # ignored to optimize finding them as Find.find doesn't. if ignored?(path + File::SEPARATOR) Find.prune # Don't look any further into this directory. else yield(path) end elsif !ignored?(path) && filtered?(path) yield(path) end end end
Inserts a path with its type (Dir or File) in paths hash.
@param [String] path the path to insert in @paths.
# File lib/listen/directory_record.rb, line 290 def insert_path(path) meta_data = MetaData.new meta_data.type = File.directory?(path) ? 'Dir' : 'File' meta_data.mtime = mtime_of(path) unless meta_data.type == 'Dir' # mtimes of dirs are not used yet @paths[File.dirname(path)][File.basename(path)] = meta_data rescue Errno::ENOENT end
Returns the modification time of a file based on the precision defined by the system
@param [String] file the file for which the mtime must be returned
@return [Fixnum, Float] the mtime of the file
# File lib/listen/directory_record.rb, line 314 def mtime_of(file) File.lstat(file).mtime.send(HIGH_PRECISION_SUPPORTED ? :to_f : :to_i) end