file_evented_update_checker.rb 3.2 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

      if (watch_dirs = base_directories).any?
21
        Listen.to(*watch_dirs, &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 74
          dir = dir.parent
        end
      end
    end

    # TODO: Better return a list of non-nested directories.
75
    def base_directories
76
      [].tap do |bd|
77 78
        bd.concat @files.map {|f| @ph.existing_parent(f.dirname)}
        bd.concat @dirs.keys.map {|dir| @ph.existing_parent(dir)}
79 80 81
        bd.compact!
        bd.uniq!
      end
82 83
    end

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

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
      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
124
          else
125 126 127 128 129 130 131 132
            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
133 134 135
          end
        end
      end
136 137 138
    end
  end
end