aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/file_evented_update_checker.rb
blob: 70d38c10bbec7d28a01916117a71bf5188ad1b16 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
require 'listen'
require 'set'
require 'pathname'

module ActiveSupport
  class FileEventedUpdateChecker
    def initialize(files, dirs={}, &block)
      @files = files.map {|f| expand_path(f)}.to_set

      @dirs = {}
      dirs.each do |dir, exts|
        @dirs[expand_path(dir)] = Array(exts).map(&:to_s)
      end

      @block = block
      @modified = false

      if (watch_dirs = base_directories).any?
        Listen.to(*watch_dirs, &method(:changed)).start
      end
    end

    def updated?
      @modified
    end

    def execute
      @block.call
    ensure
      @modified = false
    end

    def execute_if_updated
      if updated?
        execute
        true
      end
    end

    private

    def expand_path(fname)
      File.expand_path(fname)
    end

    def changed(modified, added, removed)
      return if updated?

      if (modified + added + removed).any? {|f| watching?(f)}
        @modified = true
      end
    end

    def watching?(file)
      file = expand_path(file)
      return true if @files.member?(file)

      file = Pathname.new(file)
      return false if file.directory?

      ext = file.extname.sub(/\A\./, '')
      dir = file.dirname

      loop do
        if @dirs.fetch(dir.to_path, []).include?(ext)
          break true
        else
          if dir.root? # TODO: find a common parent directory in initialize
            break false
          end
          dir = dir.parent
        end
      end
    end

    # TODO: Better return a list of non-nested directories.
    def base_directories
      [].tap do |bd|
        bd.concat @files.map {|f| existing_parent(File.dirname(f))}
        bd.concat @dirs.keys.map {|dir| existing_parent(dir)}
        bd.compact!
        bd.uniq!
      end
    end

    def existing_parent(dir)
      dir = Pathname.new(File.expand_path(dir))

      loop do
        if dir.directory?
          break dir.to_path
        else
          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
        end
      end
    end
  end
end