fun_with_files 0.0.14 → 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 (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