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.
- checksums.yaml +5 -5
- data/CHANGELOG.markdown +15 -3
- data/Gemfile +17 -7
- data/{README.rdoc → README.markdown} +11 -10
- data/VERSION +1 -1
- data/lib/fun_with/files/bootstrapper.rb +87 -0
- data/lib/fun_with/files/digest_methods.rb +30 -16
- data/lib/fun_with/files/directory_builder.rb +4 -0
- data/lib/fun_with/files/downloader.rb +3 -19
- data/lib/fun_with/files/errors.rb +9 -1
- data/lib/fun_with/files/file_manipulation_methods.rb +25 -15
- data/lib/fun_with/files/file_path.rb +147 -150
- data/lib/fun_with/files/file_path_class_methods.rb +23 -2
- data/lib/fun_with/files/file_permission_methods.rb +18 -7
- data/lib/fun_with/files/file_requirements.rb +63 -7
- data/lib/fun_with/files/requirements/manager.rb +104 -0
- data/lib/fun_with/files/root_path.rb +3 -3
- data/lib/fun_with/files/stat_methods.rb +33 -0
- data/lib/fun_with/files/string_behavior.rb +6 -2
- data/lib/fun_with/files/utils/byte_size.rb +143 -0
- data/lib/fun_with/files/utils/opts.rb +26 -0
- data/lib/fun_with/files/utils/succession.rb +47 -0
- data/lib/fun_with/files/utils/timestamp.rb +47 -0
- data/lib/fun_with/files/utils/timestamp_format.rb +31 -0
- data/lib/fun_with/files/watcher.rb +157 -0
- data/lib/fun_with/files/watchers/directory_watcher.rb +67 -0
- data/lib/fun_with/files/watchers/file_watcher.rb +45 -0
- data/lib/fun_with/files/watchers/missing_watcher.rb +23 -0
- data/lib/fun_with/files/watchers/node_watcher.rb +44 -0
- data/lib/fun_with/testing/assertions/fun_with_files.rb +91 -0
- data/lib/fun_with/testing/test_case_extensions.rb +12 -0
- data/lib/fun_with_files.rb +5 -75
- data/test/helper.rb +13 -5
- data/test/test_core_extensions.rb +5 -0
- data/test/test_directory_builder.rb +29 -10
- data/test/test_extension_methods.rb +62 -0
- data/test/test_file_manipulation.rb +2 -2
- data/test/test_file_path.rb +18 -39
- data/test/test_file_requirements.rb +36 -0
- data/test/test_fun_with_files.rb +1 -1
- data/test/test_fwf_assertions.rb +62 -0
- data/test/test_moving_files.rb +111 -0
- data/test/test_permission_methods.rb +22 -0
- data/test/test_root_path.rb +9 -0
- data/test/test_stat_methods.rb +17 -0
- data/test/test_timestamping.rb +74 -0
- data/test/test_utils_bytesize.rb +71 -0
- data/test/test_utils_succession.rb +30 -0
- data/test/test_watchers.rb +196 -0
- 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
|
@@ -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
|