fun_with_files 0.0.14 → 0.0.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.markdown +15 -3
  3. data/Gemfile +17 -7
  4. data/{README.rdoc → README.markdown} +12 -11
  5. data/Rakefile +3 -3
  6. data/VERSION +1 -1
  7. data/lib/fun_with/files/bootstrapper.rb +87 -0
  8. data/lib/fun_with/files/core_extensions/file.rb +15 -3
  9. data/lib/fun_with/files/core_extensions/set.rb +12 -0
  10. data/lib/fun_with/files/core_extensions/true_class.rb +11 -0
  11. data/lib/fun_with/files/digest_methods.rb +50 -17
  12. data/lib/fun_with/files/directory_builder.rb +9 -4
  13. data/lib/fun_with/files/downloader.rb +29 -16
  14. data/lib/fun_with/files/errors.rb +9 -1
  15. data/lib/fun_with/files/file_manipulation_methods.rb +25 -15
  16. data/lib/fun_with/files/file_orderer.rb +2 -0
  17. data/lib/fun_with/files/file_path.rb +242 -156
  18. data/lib/fun_with/files/file_path_class_methods.rb +23 -2
  19. data/lib/fun_with/files/file_permission_methods.rb +18 -7
  20. data/lib/fun_with/files/file_requirements.rb +63 -7
  21. data/lib/fun_with/files/requirements/manager.rb +104 -0
  22. data/lib/fun_with/files/root_path.rb +3 -3
  23. data/lib/fun_with/files/stat_methods.rb +33 -0
  24. data/lib/fun_with/files/string_behavior.rb +6 -2
  25. data/lib/fun_with/files/utils/byte_size.rb +143 -0
  26. data/lib/fun_with/files/utils/opts.rb +26 -0
  27. data/lib/fun_with/files/utils/succession.rb +47 -0
  28. data/lib/fun_with/files/utils/timestamp.rb +47 -0
  29. data/lib/fun_with/files/utils/timestamp_format.rb +31 -0
  30. data/lib/fun_with/files/watcher.rb +157 -0
  31. data/lib/fun_with/files/watchers/directory_watcher.rb +67 -0
  32. data/lib/fun_with/files/watchers/file_watcher.rb +45 -0
  33. data/lib/fun_with/files/watchers/missing_watcher.rb +23 -0
  34. data/lib/fun_with/files/watchers/node_watcher.rb +44 -0
  35. data/lib/fun_with/testing/assertions/fun_with_files.rb +91 -0
  36. data/lib/fun_with/testing/test_case_extensions.rb +12 -0
  37. data/lib/fun_with_files.rb +5 -31
  38. data/test/helper.rb +13 -5
  39. data/test/test_core_extensions.rb +6 -0
  40. data/test/test_descent.rb +2 -2
  41. data/test/test_directory_builder.rb +29 -10
  42. data/test/test_extension_methods.rb +62 -0
  43. data/test/test_file_manipulation.rb +4 -4
  44. data/test/test_file_path.rb +83 -56
  45. data/test/test_file_requirements.rb +36 -0
  46. data/test/test_fun_with_files.rb +1 -1
  47. data/test/test_fwf_assertions.rb +62 -0
  48. data/test/test_moving_files.rb +111 -0
  49. data/test/test_permission_methods.rb +22 -0
  50. data/test/test_root_path.rb +9 -0
  51. data/test/test_stat_methods.rb +17 -0
  52. data/test/test_timestamping.rb +74 -0
  53. data/test/test_utils_bytesize.rb +71 -0
  54. data/test/test_utils_succession.rb +30 -0
  55. data/test/test_watchers.rb +196 -0
  56. metadata +59 -13
  57. /data/lib/fun_with/files/core_extensions/{false.rb → false_class.rb} +0 -0
  58. /data/lib/fun_with/files/core_extensions/{nil.rb → nil_class.rb} +0 -0
@@ -1,22 +1,78 @@
1
1
  module FunWith
2
2
  module Files
3
3
  module FileRequirements
4
- def _raise_error_if_not test, msg, error_class
5
- if test
6
- raise error_class.new( msg + "(file: #{self})" )
7
- end
4
+ def needs_to_exist error_msg = "Path does not exist"
5
+ _raise_error_if_not self.exist?, error_msg, Errno::ENOENT
8
6
  end
9
7
 
10
8
  def needs_to_be_a_file error_msg = "Path is not a file"
9
+ self.needs_to_exist( error_msg + " (does not exist)" )
11
10
  _raise_error_if_not self.file?, error_msg, Errno::ENOENT
12
11
  end
13
12
 
13
+ def needs_to_be_readable error_msg = "Path is not readable"
14
+ self.needs_to_exist( error_msg + " (does not exist)" )
15
+ _raise_error_if_not self.writable?, error_msg, Errno::EPERM
16
+ end
17
+
14
18
  def needs_to_be_writable error_msg = "Path is not writable"
15
- _raise_error_if_not self.writable?, error_msg, Errno::ENOENT
19
+ self.needs_to_exist( error_msg + " (does not exist)" )
20
+ _raise_error_if_not self.writable?, error_msg, Errno::EPERM
16
21
  end
17
22
 
18
- def needs_to_be_empty error_msg = "Path needs to point to"
19
- _raise_error_if_not self.empty?, error_msg, Errno::ENOENT
23
+ def needs_to_be_executable error_msg = "Path is not executable"
24
+ self.needs_to_exist( error_msg + " (does not exist)" )
25
+ _raise_error_if_not self.executable?, error_msg, Errno::ENOEXEC
26
+ end
27
+
28
+ # returns a different code depending on whether the path is a file
29
+ # or a directory.
30
+ def needs_to_be_empty error_msg = "Path needs to be empty"
31
+ self.needs_to_exist( error_msg + " (does not exist)" )
32
+ error_class = Errno::EOWNERDEAD # it's as good a code as any
33
+ error_class = Errno::ENOTEMPTY if self.directory?
34
+ error_class = Errors::FileNotEmpty if self.file? # there's no libc error for "file oughta be empty"
35
+
36
+ _raise_error_if_not self.empty?, error_msg, error_class
37
+ end
38
+
39
+ def needs_to_be_a_directory error_msg = "Path is not a directory"
40
+ self.needs_to_exist( error_msg + " (does not exist)" )
41
+ _raise_error_if_not self.directory?, error_msg, Errno::ENOTDIR
42
+ end
43
+
44
+ def needs_to_be( *requirements )
45
+ for requirement in requirements
46
+ case requirement
47
+ when :exist
48
+ self.needs_to_exist
49
+ when :readable
50
+ self.needs_to_be_readable
51
+ when :writable
52
+ self.needs_to_be_writable
53
+ when :executable
54
+ self.needs_to_be_executable
55
+ when :empty
56
+ self.needs_to_be_empty
57
+ when :directory
58
+ self.needs_to_be_a_directory
59
+ when :file
60
+ self.needs_to_be_a_file
61
+ else
62
+ warn "Did not understand file.needs_to_be constraint: #{arg}"
63
+ end
64
+ end
65
+ end
66
+
67
+ protected
68
+ def _raise_error_if test, msg, error_class
69
+ if test
70
+ raise error_class.new( msg + "(file: #{self})" )
71
+ end
72
+ end
73
+
74
+ def _raise_error_if_not test, msg, error_class
75
+ _raise_error_if !test, msg, error_class
20
76
  end
21
77
  end
22
78
  end
@@ -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