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.
- checksums.yaml +5 -5
- data/CHANGELOG.markdown +15 -3
- data/Gemfile +17 -7
- data/{README.rdoc → README.markdown} +12 -11
- data/Rakefile +3 -3
- data/VERSION +1 -1
- data/lib/fun_with/files/bootstrapper.rb +87 -0
- data/lib/fun_with/files/core_extensions/file.rb +15 -3
- data/lib/fun_with/files/core_extensions/set.rb +12 -0
- data/lib/fun_with/files/core_extensions/true_class.rb +11 -0
- data/lib/fun_with/files/digest_methods.rb +50 -17
- data/lib/fun_with/files/directory_builder.rb +9 -4
- data/lib/fun_with/files/downloader.rb +29 -16
- 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_orderer.rb +2 -0
- data/lib/fun_with/files/file_path.rb +242 -156
- 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 -31
- data/test/helper.rb +13 -5
- data/test/test_core_extensions.rb +6 -0
- data/test/test_descent.rb +2 -2
- data/test/test_directory_builder.rb +29 -10
- data/test/test_extension_methods.rb +62 -0
- data/test/test_file_manipulation.rb +4 -4
- data/test/test_file_path.rb +83 -56
- 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 +59 -13
- /data/lib/fun_with/files/core_extensions/{false.rb → false_class.rb} +0 -0
- /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
|
5
|
-
|
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
|
-
|
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
|
19
|
-
|
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
|
@@ -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
|