file_evented_update_checker.rb 3.9 KB
Newer Older
1
require 'listen'
2 3
require 'set'
require 'pathname'
4 5

module ActiveSupport
6
  class FileEventedUpdateChecker #:nodoc: all
7
    def initialize(files, dirs={}, &block)
8 9
      @ph    = PathHelper.new
      @files = files.map {|f| @ph.xpath(f)}.to_set
10 11 12

      @dirs = {}
      dirs.each do |dir, exts|
13
        @dirs[@ph.xpath(dir)] = Array(exts).map {|ext| @ph.normalize_extension(ext)}
14
      end
15

X
Xavier Noria 已提交
16 17 18
      @block   = block
      @updated = false
      @lcsp    = @ph.longest_common_subpath(@dirs.keys)
19

20 21
      if (dtw = directories_to_watch).any?
        Listen.to(*dtw, &method(:changed)).start
22
      end
23 24 25
    end

    def updated?
X
Xavier Noria 已提交
26
      @updated
27 28 29 30 31
    end

    def execute
      @block.call
    ensure
X
Xavier Noria 已提交
32
      @updated = false
33 34 35 36 37 38 39 40 41 42 43 44
    end

    def execute_if_updated
      if updated?
        execute
        true
      end
    end

    private

    def changed(modified, added, removed)
45
      unless updated?
X
Xavier Noria 已提交
46
        @updated = (modified + added + removed).any? {|f| watching?(f)}
47 48 49
      end
    end

50
    def watching?(file)
51
      file = @ph.xpath(file)
52

53
      return true  if @files.member?(file)
54 55
      return false if file.directory?

56
      ext = @ph.normalize_extension(file.extname)
57 58 59
      dir = file.dirname

      loop do
60
        if @dirs.fetch(dir, []).include?(ext)
61 62
          break true
        else
63 64 65 66
          if @lcsp
            break false if dir == @lcsp
          else
            break false if dir.root?
67
          end
68

69 70 71 72 73
          dir = dir.parent
        end
      end
    end

74 75 76 77 78 79 80 81 82
    def directories_to_watch
      bd = []

      bd.concat @files.map {|f| @ph.existing_parent(f.dirname)}
      bd.concat @dirs.keys.map {|dir| @ph.existing_parent(dir)}
      bd.compact!
      bd.uniq!

      @ph.filter_out_descendants(bd)
83 84
    end

85 86 87 88
    class PathHelper
      def xpath(path)
        Pathname.new(path).expand_path
      end
89

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
      def normalize_extension(ext)
        ext.to_s.sub(/\A\./, '')
      end

      # Given a collection of Pathname objects returns the longest subpath
      # common to all of them, or +nil+ if there is none.
      def longest_common_subpath(paths)
        return if paths.empty?

        csp = Pathname.new(paths[0])

        paths[1..-1].each do |path|
          loop do
            break if path.ascend do |ascendant|
              break true if ascendant == csp
            end

            if csp.root?
              # A root directory is not an ascendant of path. This may happen
              # if there are paths in different drives on Windows.
              return
            else
              csp = csp.parent
            end
          end
        end

        csp
      end

      # Returns the deepest existing ascendant, which could be the argument itself.
      def existing_parent(dir)
        loop do
          if dir.directory?
            break dir
125
          else
126 127 128 129 130 131 132 133
            if dir.root?
              # Edge case in which not even the root exists. For example, Windows
              # paths could have a non-existing drive letter. Since the parent of
              # root is root, we need to break to prevent an infinite loop.
              break
            else
              dir = dir.parent
            end
134 135 136
          end
        end
      end
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161

      # Filters out directories which are descendants of others in the collection (stable).
      def filter_out_descendants(directories)
        return directories if directories.length < 2

        sorted      = directories.sort_by {|dir| dir.each_filename.to_a.length}
        descendants = []

        until sorted.empty?
          directory = sorted.shift

          sorted.each do |candidate_to_descendant|
            if candidate_to_descendant.to_path.start_with?(directory.to_path)
              dparts = directory.each_filename.to_a
              cparts = candidate_to_descendant.each_filename.to_a

              if cparts[0, dparts.length] == dparts
                descendants << candidate_to_descendant
              end
            end
          end
        end

        directories - descendants
      end
162 163 164
    end
  end
end