fun_with_files 0.0.15 → 0.0.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.markdown +15 -3
  3. data/Gemfile +17 -7
  4. data/{README.rdoc → README.markdown} +11 -10
  5. data/VERSION +1 -1
  6. data/lib/fun_with/files/bootstrapper.rb +87 -0
  7. data/lib/fun_with/files/digest_methods.rb +30 -16
  8. data/lib/fun_with/files/directory_builder.rb +4 -0
  9. data/lib/fun_with/files/downloader.rb +3 -19
  10. data/lib/fun_with/files/errors.rb +9 -1
  11. data/lib/fun_with/files/file_manipulation_methods.rb +25 -15
  12. data/lib/fun_with/files/file_path.rb +147 -150
  13. data/lib/fun_with/files/file_path_class_methods.rb +23 -2
  14. data/lib/fun_with/files/file_permission_methods.rb +18 -7
  15. data/lib/fun_with/files/file_requirements.rb +63 -7
  16. data/lib/fun_with/files/requirements/manager.rb +104 -0
  17. data/lib/fun_with/files/root_path.rb +3 -3
  18. data/lib/fun_with/files/stat_methods.rb +33 -0
  19. data/lib/fun_with/files/string_behavior.rb +6 -2
  20. data/lib/fun_with/files/utils/byte_size.rb +143 -0
  21. data/lib/fun_with/files/utils/opts.rb +26 -0
  22. data/lib/fun_with/files/utils/succession.rb +47 -0
  23. data/lib/fun_with/files/utils/timestamp.rb +47 -0
  24. data/lib/fun_with/files/utils/timestamp_format.rb +31 -0
  25. data/lib/fun_with/files/watcher.rb +157 -0
  26. data/lib/fun_with/files/watchers/directory_watcher.rb +67 -0
  27. data/lib/fun_with/files/watchers/file_watcher.rb +45 -0
  28. data/lib/fun_with/files/watchers/missing_watcher.rb +23 -0
  29. data/lib/fun_with/files/watchers/node_watcher.rb +44 -0
  30. data/lib/fun_with/testing/assertions/fun_with_files.rb +91 -0
  31. data/lib/fun_with/testing/test_case_extensions.rb +12 -0
  32. data/lib/fun_with_files.rb +5 -75
  33. data/test/helper.rb +13 -5
  34. data/test/test_core_extensions.rb +5 -0
  35. data/test/test_directory_builder.rb +29 -10
  36. data/test/test_extension_methods.rb +62 -0
  37. data/test/test_file_manipulation.rb +2 -2
  38. data/test/test_file_path.rb +18 -39
  39. data/test/test_file_requirements.rb +36 -0
  40. data/test/test_fun_with_files.rb +1 -1
  41. data/test/test_fwf_assertions.rb +62 -0
  42. data/test/test_moving_files.rb +111 -0
  43. data/test/test_permission_methods.rb +22 -0
  44. data/test/test_root_path.rb +9 -0
  45. data/test/test_stat_methods.rb +17 -0
  46. data/test/test_timestamping.rb +74 -0
  47. data/test/test_utils_bytesize.rb +71 -0
  48. data/test/test_utils_succession.rb +30 -0
  49. data/test/test_watchers.rb +196 -0
  50. metadata +54 -16
@@ -0,0 +1,104 @@
1
+ module FunWith
2
+ module Files
3
+ module Requirements
4
+ class Manager
5
+ def self.require_files( path )
6
+ self.new( path ).require_files
7
+ end
8
+
9
+ def initialize( path )
10
+ case path
11
+ when Array
12
+ @required_files = path
13
+ when FilePath
14
+ if path.directory?
15
+ @required_files = path.glob( :recursive => true, :ext => "rb" )
16
+ else
17
+ @required_files = [path]
18
+ end
19
+ end
20
+
21
+ @required_files.map!{|f| f.expand.gsub( /\.rb$/, '' ) }
22
+ @successfully_required = []
23
+ @missing_constants = {}
24
+ end
25
+
26
+ def require_files
27
+ while @required_files.length > 0
28
+ file = @required_files.shift
29
+
30
+ if try_requiring_file( file )
31
+ check_for_needed_constants
32
+ end
33
+ end
34
+
35
+ # Ran into a situation where it was failing because the missing constant was incorrectly being guessed
36
+ # to be M1::M2::M3::M4 instead of M1::M2::M4. Had the file been required, it would have gone through.
37
+ # So I'm adding a last-chance round, using the ugly old approach of simply trying to require everything
38
+ # over and over again until it's clear no progress ins being made.
39
+ unless @missing_constants.fwf_blank?
40
+ unless require_files_messily( @missing_constants.values.flatten )
41
+ raise NameError.new( "The following constants could not be defined: #{@missing_constants.inspect}")
42
+ end
43
+ end
44
+ end
45
+
46
+ # If it's not the sort of error we're looking for, re-raise the error
47
+ def uninitialized_constant_error( e, &block )
48
+ if e.message =~ /^uninitialized constant/
49
+ yield
50
+ else
51
+ raise e
52
+ end
53
+ end
54
+
55
+ def try_requiring_file( file )
56
+ begin
57
+ require file
58
+ @successfully_required << file
59
+ true
60
+ rescue NameError => e
61
+ uninitialized_constant_error( e ) do
62
+ konst = e.message.split.last
63
+
64
+ @missing_constants[konst] ||= []
65
+ @missing_constants[konst] << file
66
+ false
67
+ end
68
+ end
69
+ end
70
+
71
+ def check_for_needed_constants
72
+ for konst, files in @missing_constants
73
+ if Object.const_defined?( konst )
74
+ @required_files = files + @required_files
75
+ @missing_constants.delete( konst )
76
+ end
77
+ end
78
+ end
79
+
80
+ # returns true if all the files given got required
81
+ def require_files_messily( files )
82
+ while true
83
+ files_remaining = files.length
84
+ return true if files_remaining == 0
85
+
86
+ files.length.times do
87
+ begin
88
+ file = files.shift
89
+ require file
90
+ @successfully_required << file
91
+ rescue NameError => e
92
+ uninitialized_constant_error( e ) do
93
+ files.push( file )
94
+ end
95
+ end
96
+ end
97
+
98
+ return false if files.length == files_remaining
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,12 +1,12 @@
1
1
  module FunWith
2
2
  module Files
3
3
  module RootPathExtensions
4
- def root( *args )
4
+ def root( *args, &block )
5
5
  if args.length > 0
6
6
  args.unshift( @root_path )
7
- FilePath.new( *args )
7
+ FilePath.new( *args, &block )
8
8
  else
9
- FilePath.new( @root_path )
9
+ FilePath.new( @root_path, &block )
10
10
  end
11
11
  end
12
12
 
@@ -0,0 +1,33 @@
1
+ module FunWith
2
+ module Files
3
+ module StatMethods
4
+ def stat
5
+ File.stat( self )
6
+ end
7
+
8
+ def inode
9
+ self.stat.ino
10
+ end
11
+
12
+ # def older_than?( time, &block )
13
+ # end
14
+ #
15
+ # def newer_than?( time, &block )
16
+ # end
17
+ #
18
+ # def bigger_than?( sz, units = :B, &block )
19
+ # end
20
+ #
21
+ # def smaller_than?( sz, units = :B, &block )
22
+ # end
23
+ #
24
+ # def modified_before?( time, &block )
25
+ # end
26
+ #
27
+ # def modified_since?( time, &block )
28
+ # end
29
+
30
+
31
+ end
32
+ end
33
+ end
@@ -3,8 +3,12 @@
3
3
  module FunWith
4
4
  module Files
5
5
  module StringBehavior
6
- def =~( rval )
7
- @path =~ rval
6
+ def =~( rhs )
7
+ @path =~ rhs
8
+ end
9
+
10
+ def !~( rhs )
11
+ @path !~ rhs
8
12
  end
9
13
 
10
14
  def match( *args )
@@ -0,0 +1,143 @@
1
+ module FunWith
2
+ module Files
3
+ module Utils
4
+ module ByteSize
5
+ "ideas"
6
+ "format: %u - units (lowercase), %k - units (uppercase), %b - units (lower, with b), %B"
7
+ " %3 - humanized value, three sigfigs, meaning that if it's under 10, you get '7.32'"
8
+ " but if it's over 100, rounds to the nearest full number. 10-99, it goes"
9
+ " down to the tenths prare"
10
+ "or just give an example: 7.32 kb"
11
+
12
+ # format: "your filesize is (%n.nn %u)"
13
+
14
+
15
+ UNITS = {
16
+ :B => 1,
17
+ :KB => 1_000,
18
+ :MB => 1_000_000,
19
+ :GB => 1_000_000_000,
20
+ :TB => 1_000_000_000_000,
21
+ :PB => 1_000_000_000_000_000,
22
+ :EB => 1_000_000_000_000_000_000,
23
+ :ZB => 1_000_000_000_000_000_000_000
24
+ }
25
+
26
+ UNIT_STANDARDIZERS = { "" => :B, "B" => :B, "b" => :B }
27
+
28
+ for s in %w(K M G T P E Z KB MB GB TB PB EB ZB)
29
+ unit_sym = s.length == 1 ? :"#{s}B" : :"#{s}"
30
+ UNIT_STANDARDIZERS[s] = unit_sym
31
+ UNIT_STANDARDIZERS[s.downcase] = unit_sym
32
+ UNIT_STANDARDIZERS[s.to_sym] = unit_sym
33
+ end
34
+
35
+ def convert( expr, units = :B )
36
+ to_units( to_bytes( expr ), units )
37
+ end
38
+
39
+ # Takes a string of the form "<NUMBER><UNIT>"
40
+ # and returns the number of bytes represented.
41
+ # See UNITS constant for valid constants
42
+ def to_bytes( expr )
43
+ regexp = /^\s*(?<num>\d+(\.\d+)?)\s*(?<unit>(k|m|g|t|p|z|)b?)\s*$/i
44
+
45
+ if m = expr.upcase.match( regexp )
46
+ num = m[:num].to_f
47
+ units = standardize_unit( m[:unit] )
48
+ # units = case units.length
49
+ # when 0
50
+ # :B
51
+ # when 1
52
+ # (units == "B" ? units : units + "B").to_sym
53
+ # when 2
54
+ # units.to_sym
55
+ # end
56
+ debugger unless UNITS.has_key?(units)
57
+ (num * UNITS[units]).to_i
58
+ else
59
+ raise ArgumentError.new( "#{expr} is not in a format that to_bytes recognizes")
60
+ end
61
+ end
62
+
63
+ # Looking for a human-friendly vibe more than accuracy.
64
+ # At most one unit of post-decimal precision, and only
65
+ # for small numbers. If the tenths place is a zero,
66
+ # the trailing zero is dropped.
67
+ def to_units( byte_count, unit )
68
+ num = byte_count.to_f / UNITS[standardize_unit(unit)]
69
+ # the first comparison gets rid of leading zeros
70
+ # the second comparison prevents the decimal from being printed
71
+ # when it doesn't make a big difference
72
+ if num == num.to_i || num >= 100 # 9.9k 10k
73
+ num_str = num.to_i.to_s
74
+ else
75
+ num_str = sprintf( "%0.01f", num )
76
+ end
77
+
78
+ num_str = num_str[0..-3] if num_str[-2..-1] == ".0"
79
+
80
+ num_str + unit.to_s
81
+ end
82
+
83
+ def standardize_unit( unit )
84
+ # So the caller can add a space if desired, but ultimately it might be
85
+ # better to offer more flexible formatting options.
86
+ unit = unit.strip if unit.respond_to?(:strip)
87
+
88
+ if UNIT_STANDARDIZERS.has_key?( unit )
89
+ UNIT_STANDARDIZERS[unit]
90
+ else
91
+ raise ArgumentError.new( "ByteSize.to_units doesn't understand the unit #{unit.inspect}(unit.class)" )
92
+ end
93
+ end
94
+
95
+
96
+ def humanize_bytes( bytes )
97
+ return "?" unless bytes.is_a?( Integer ) && bytes >= 0
98
+
99
+ bytes = bytes.to_f
100
+
101
+ if bytes > 1_000_000_000
102
+ exp = "G"
103
+ amt = bytes / 1_000_000_000
104
+ elsif bytes > 1_000_000
105
+ exp = "M"
106
+ amt = bytes / 1_000_000
107
+ elsif bytes > 1_000
108
+ exp = "K"
109
+ amt = bytes / 1_000
110
+ else
111
+ exp = "B"
112
+ amt = bytes
113
+ end
114
+
115
+ if amt > 10
116
+ digits = 0
117
+ elsif amt > 1
118
+ digits = 1
119
+ end
120
+
121
+ sprintf( "%0.#{digits}f", amt ) + exp
122
+ end
123
+
124
+ # returns a string of numbers, representing the float
125
+ # d - number of figures after the zero (max)
126
+ def limited_precision_value( f, d )
127
+ # 4, 1234.5 -> 1234
128
+ # 4, 123.45 -> 123.4
129
+ # 4, 12.3423 -> 12.34
130
+ # 4, 0.0001 -> 0.0001 -> - 1234.5, 123.45, 12.345, 1.2345 0.1234 0.0123
131
+ # 2 - 1234, 123, 12.3, 1.23, 0.12, 0.01
132
+ # 1 - 12.3, 12, 1
133
+ # 0 - 12, 1, 0
134
+ #
135
+ #
136
+ #
137
+ #
138
+ #
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,26 @@
1
+ module FunWith
2
+ module Files
3
+ module Utils
4
+ class Opts
5
+ # It's tradition to pass an options hash as the last argument (creaky old tradition, named variables getting more popular)
6
+ # Separates out that last configuration hash, if it's been given.
7
+ def self.extract_opts_from_args( args )
8
+ if args.last.is_a?( Hash )
9
+ [args[0..-2], args.last ]
10
+ else
11
+ [args, {}]
12
+ end
13
+ end
14
+
15
+ # Given a hash and a list of keys, return a hash that only includes the keys listed.
16
+ def self.narrow_options( opts, keys )
17
+ opts.keep_if{ |k,v| keys.include?( k ) }
18
+ end
19
+
20
+ def self.narrow_file_utils_options( opts, cmd )
21
+ self.narrow_options( opts, FileUtils::OPT_TABLE[ cmd.to_s ] )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ module FunWith
2
+ module Files
3
+ module Utils
4
+ class Succession
5
+ def self.get_successor_name( basename, digit_count )
6
+ pieces = basename.to_s.split(".")
7
+
8
+ if pieces.length == 0
9
+ pieces = [ self.format_counter( 0, digit_count ) ]
10
+ elsif is_counter?( pieces.last, digit_count )
11
+ pieces = self.increment_position( pieces, pieces.length - 1 )
12
+ elsif is_counter?( pieces[-2], digit_count )
13
+ pieces = self.increment_position( pieces, pieces.length - 2 )
14
+ else
15
+ pieces = self.install_counter( pieces, digit_count )
16
+ end
17
+
18
+ pieces.join(".")
19
+ end
20
+
21
+ def self.is_counter?( str, digit_count )
22
+ return false if str.nil?
23
+ (str =~ /^\d{#{digit_count}}$/) != nil
24
+ end
25
+
26
+ def self.format_counter( i, len )
27
+ sprintf( "%0#{len}i", i )
28
+ end
29
+
30
+ def self.increment_position( arr, pos_to_increment )
31
+ arr.map.each_with_index do |str, i|
32
+ if i == pos_to_increment
33
+ self.format_counter( str.to_i + 1, str.length )
34
+ else
35
+ str
36
+ end
37
+ end
38
+ end
39
+
40
+ def self.install_counter( arr, count )
41
+ counter = self.format_counter( 0, count )
42
+ arr[0..-2] + [counter] + [arr[-1]]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ module FunWith
2
+ module Files
3
+ module Utils
4
+ class Timestamp
5
+ # Currently exactly one format is supported. Laaaaame!
6
+
7
+ def self.format( key )
8
+ @formats ||= {
9
+ :default => TimestampFormat.new.recognizer( /^\d{17}$/ ).strftime("%Y%m%d%H%M%S%L"),
10
+ :ymd => TimestampFormat.new.recognizer( /^\d{4}-\d{2}-\d{2}$/ ).strftime("%Y-%m-%d"),
11
+ :ym => TimestampFormat.new.recognizer( /^\d{4}-\d{2}$/ ).strftime("%Y-%m"),
12
+ :y => TimestampFormat.new.recognizer( /^\d{4}$/ ).strftime("%Y"),
13
+
14
+ # UNIX timestamp
15
+ :s => TimestampFormat.new.recognizer( /^\d{10}$/ ).strftime("%s")
16
+ }
17
+
18
+ if @formats.has_key?(key)
19
+ @formats[key]
20
+ else
21
+ raise Errors::TimestampFormatUnrecognized.new( "Unrecognized timestamp format (#{key.inspect}). Choose from #{@formats.keys.inspect}" )
22
+ end
23
+ end
24
+
25
+ def self.timestamp( basename, format: :default, splitter: ".", time: Time.now )
26
+ filename_chunks = basename.to_s.split( splitter )
27
+ format = format.is_a?( TimestampFormat ) ? format : self.format( format )
28
+ new_timestamp = format.format_time( time )
29
+
30
+ timestamp_index = filename_chunks.map.each_with_index{ |str,i|
31
+ format.matches?( str ) ? i : nil
32
+ }.compact.last
33
+
34
+ if timestamp_index
35
+ filename_chunks[timestamp_index] = new_timestamp
36
+ elsif filename_chunks.length == 1
37
+ filename_chunks << new_timestamp
38
+ else
39
+ filename_chunks.insert( -2, new_timestamp )
40
+ end
41
+
42
+ filename_chunks.join( splitter )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ module FunWith
2
+ module Files
3
+ module Utils
4
+ class TimestampFormat
5
+ # the timestamp identifies a chunk of the filename
6
+ # to be its kind of timestamp by checking it against a
7
+ # regular expression.
8
+ def recognizer( regex )
9
+ @recognizer = regex
10
+ self
11
+ end
12
+
13
+ # The strftime format used to output the timestamp
14
+ def strftime( s )
15
+ @strftime_format = s
16
+ self
17
+ end
18
+
19
+ def format_time( t )
20
+ t.strftime( @strftime_format )
21
+ end
22
+
23
+ # does the given chunk look like a timestamp using this format?
24
+ # returns true or false.
25
+ def matches?( str, &block )
26
+ @recognizer.match( str ) != nil
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,157 @@
1
+ # You want to watch a list of files or directories for changes
2
+ # (files created, deleted, or modified), and then return a... list?
3
+ # of changes.
4
+ #
5
+ # Tricky part is, what constitutes a "change" for a directory.
6
+ # or a file, for that matter.
7
+ #
8
+ # Cool expansion: customize the sorts of changes that trigger actions.
9
+ # You can already do that to some extent with the user-code that the
10
+ # change list is delivered to
11
+ #
12
+ # usr/ (modified, watched)
13
+ # bin/ (modified)
14
+ # new_directory/ (created)
15
+ # README.txt (created)
16
+ # bash (unchanged)
17
+ #
18
+ # lib/ (modified)
19
+ # libgpg.so (unchanged)
20
+ # libmysql.so (deleted)
21
+ # libzip.so (modified)
22
+ # libjpeg.so (file_created)
23
+ # you_have_been_hacked_by_chinese.txt (file_created)
24
+ # cache/ (deleted)
25
+ # firefox/ (deleted)
26
+ # cached_item.jpg (deleted)
27
+ # cached_folder/ (deleted)
28
+ # cache_file.csv (deleted)
29
+ #
30
+ # When you find a change in a subdirector/file, the :modified status
31
+ # propagates up the tree.
32
+ # Feels like a job for a visitor.
33
+ #
34
+ # If you create the top-level watcher, it could create any sub-watchers
35
+ # for the files and folders. It asks its watchers to update their
36
+ # statuses and report back.
37
+ #
38
+ # But the top-level one should know that it's the top level, so it
39
+ # shouldn't be deleting its watchers that might be, for example,
40
+ # waiting for a file to come into being.
41
+ #
42
+ # Hence, anyone using this code probably ought to stick to using the main Watcher
43
+ # class, and not worry about the ones it uses in the background
44
+ #
45
+ # Filters can be added
46
+ module FunWith
47
+ module Files
48
+ class Watcher
49
+ def self.watch( *paths, interval: 1.0, notice: [], ignore: [], &block )
50
+ watcher = self.new( paths ).sleep_interval( interval ).filter( notice: notice, ignore: ignore )
51
+
52
+ if block_given?
53
+ watcher.watch( &block )
54
+ else
55
+ watcher
56
+ end
57
+ end
58
+
59
+ def self.factory( path )
60
+ path = path.fwf_filepath
61
+
62
+ if path.exist?
63
+ if path.directory?
64
+ Watchers::DirectoryWatcher.new( path )
65
+ elsif path.file?
66
+ Watchers::FileWatcher.new( path )
67
+ end
68
+ else
69
+ Watchers::MissingWatcher.new( path )
70
+ end
71
+ end
72
+
73
+ def initialize( paths )
74
+ @sleep_interval = 1.0
75
+ @notice_filters = []
76
+ @ignore_filters = []
77
+
78
+ # Create a watcher for every single thing that we're
79
+ # asking it to watch
80
+ @watchers = paths.inject({}) do |watchers, path|
81
+ watchers[path.fwf_filepath] = self.class.factory( path )
82
+ watchers
83
+ end
84
+ end
85
+
86
+ def sleep_interval( i )
87
+ @sleep_interval = i
88
+ self
89
+ end
90
+
91
+ def watch( &block )
92
+ while true
93
+ sleep( @sleep_interval )
94
+ yield self.update
95
+ end
96
+ end
97
+
98
+ def filter( notice: [], ignore: [] )
99
+ @notice_filters += [notice].flatten
100
+ @ignore_filters += [ignore].flatten
101
+
102
+ self
103
+ end
104
+
105
+ # returns a hash of the changes that have happened in the file system being monitored,
106
+ def update
107
+ {}.tap do |changes|
108
+ for path, watcher in @watchers
109
+ changes.merge!( watcher.update )
110
+ replace_watcher( path, changes[path] ) # a DirectoryWatcher might need to be replaced with a MissingWatcher, for example, or vice-versa
111
+
112
+ # corner case: if a directory is created, everything created under the directory
113
+ # is deemed to have also been created at the same time
114
+ if path.directory? && changes[path] == :created
115
+ changes.merge!( path.glob(:all).inject({}){ |memo,path| memo[path] = :created ; memo } )
116
+ end
117
+ end
118
+
119
+ apply_filters( changes )
120
+ end
121
+ end
122
+
123
+ def replace_watcher( path, change )
124
+ case change
125
+ when nil
126
+ # didn't change
127
+ when :deleted, :created
128
+ @watchers[path] = self.class.factory( path )
129
+ end
130
+ end
131
+
132
+ #
133
+ def apply_filters( changes )
134
+ apply_notice_filters( changes )
135
+ apply_ignore_filters( changes )
136
+ changes
137
+ end
138
+
139
+ def apply_notice_filters( changes )
140
+ for filter in @notice_filters
141
+ for path in changes.keys
142
+ changes.delete( path ) if path !~ filter
143
+ end
144
+ end
145
+ end
146
+
147
+ def apply_ignore_filters( changes )
148
+ for filter in @ignore_filters
149
+ for path in changes.keys
150
+ changes.delete( path ) if path =~ filter
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+
@@ -0,0 +1,67 @@
1
+ module FunWith
2
+ module Files
3
+ module Watchers
4
+ class DirectoryWatcher < NodeWatcher
5
+ def initialize( path )
6
+ set_path( path )
7
+ @watchers = create_watchers( self.path.entries )
8
+ end
9
+
10
+
11
+ # returns a hash of changes
12
+ def update
13
+ new_changeset do
14
+ if self.path.exist?
15
+ update_existing_files
16
+ find_new_files
17
+ else
18
+ # If the directory is gone, you can assume the same is true
19
+ # for all the files it held.
20
+ report_files( self.all_paths, :deleted )
21
+ end
22
+ end
23
+ end
24
+
25
+ def update_existing_files
26
+ # first, check on the files we're supposed to be keeping track of
27
+ for path, watcher in @watchers
28
+ @changes[path] = :deleted unless path.exist?
29
+ @changes.merge!( watcher.update )
30
+
31
+ # While the main Watcher will continue to monitor the places it's
32
+ # been told, even if they're missing, the subwatchers just disappear
33
+ # when the files they're watching do.
34
+ @watchers.delete( path ) unless path.exist?
35
+ end
36
+ end
37
+
38
+ def find_new_files
39
+ # next, get the updated list of files/folders beneath this directory
40
+ current_paths = self.path.entries
41
+
42
+ for path in current_paths
43
+ unless @watchers.has_key?( path )
44
+ w = Watcher.factory( path )
45
+
46
+ report_files( w.all_paths, :created )
47
+
48
+ @watchers[path] = w
49
+ end
50
+ end
51
+ end
52
+
53
+ # modify the current list of changes by adding "deleted" for
54
+ # every file/folder below this one.
55
+ def report_files( paths, status )
56
+ for path in paths
57
+ @changes[path] = status
58
+ end
59
+ end
60
+
61
+ def all_paths
62
+ @watchers.map{|path, watcher| watcher.all_paths }.flatten + [self.path]
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end