fun_with_files 0.0.15 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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