class Listen::DirectoryRecord

The directory record stores information about a directory and keeps track of changes to the structure of its childs.

Constants

DEFAULT_IGNORED_DIRECTORIES
DEFAULT_IGNORED_EXTENSIONS
HIGH_PRECISION_SUPPORTED

Defines the used precision based on the type of mtime returned by the system (whether its in milliseconds or just seconds)

MetaData

Data structure used to save meta data about a path

Attributes

directory[R]
paths[R]
sha1_checksums[R]

Public Class Methods

generate_default_ignoring_patterns() click to toggle source

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
new(directory) click to toggle source

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

Public Instance Methods

build() click to toggle source

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
fetch_changes(directories, options = {}) click to toggle source

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
filter(*regexps) click to toggle source

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
filtered?(path) click to toggle source

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
filtering_patterns() click to toggle source

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
ignore(*regexps) click to toggle source

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
ignored?(path) click to toggle source

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
ignoring_patterns() click to toggle source

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
relative_to_base(path) click to toggle source

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

Private Instance Methods

content_modified?(path) click to toggle source

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
detect_additions(directory, options = {}) click to toggle source

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
detect_modifications_and_removals(directory, options = {}) click to toggle source

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
existing_path?(path) click to toggle source

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
important_paths() { |path| ... } click to toggle source

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
insert_path(path) click to toggle source

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
mtime_of(file) click to toggle source

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