s3cp 0.1.14 → 0.1.15

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.
data/History.txt CHANGED
@@ -1,4 +1,12 @@
1
- === 0.1.15 / (Pending)
1
+ === 0.1.16 / (Pending)
2
+
3
+ === 0.1.15 / (2012-02-17)
4
+
5
+ * Added: s3cp now automatically checks MD5 checksums during download/upload
6
+ and retries up to 5 times by default if the checksum fails.
7
+ The number of attempts may be configured using --max-attempts,
8
+ the retry delay may be changed with --retry-delay and the check
9
+ may be disabled completely using --no-checksum.
2
10
 
3
11
  === 0.1.14 / (2012-02-09)
4
12
 
data/lib/s3cp/s3cp.rb CHANGED
@@ -5,6 +5,7 @@ require 'optparse'
5
5
  require 'date'
6
6
  require 'highline/import'
7
7
  require 'fileutils'
8
+ require 'digest'
8
9
 
9
10
  require 's3cp/utils'
10
11
 
@@ -13,6 +14,9 @@ options = {}
13
14
  options[:verbose] = $stdout.isatty ? true : false
14
15
  options[:headers] = []
15
16
  options[:overwrite] = true
17
+ options[:checksum] = true
18
+ options[:retries] = 5
19
+ options[:retry_delay] = 1
16
20
 
17
21
  op = OptionParser.new do |opts|
18
22
  opts.banner = <<-BANNER
@@ -45,6 +49,18 @@ op = OptionParser.new do |opts|
45
49
  options[:overwrite] = false
46
50
  end
47
51
 
52
+ opts.on("--max-attempts N", "Number of attempts to upload/download until checksum matches (default #{options[:retries]})") do |attempts|
53
+ options[:max_attempts] = attempts.to_i
54
+ end
55
+
56
+ opts.on("--retry-delay SECONDS", "Time to wait (in seconds) between retries (default #{options[:retry_delay]})") do |delay|
57
+ options[:retry_delay] = delay.to_i
58
+ end
59
+
60
+ opts.on("--no-checksum", "Disable checksum checking") do
61
+ options[:checksum] = false
62
+ end
63
+
48
64
  opts.separator ""
49
65
 
50
66
  opts.on('--headers \'Header1: Header1Value\',\'Header2: Header2Value\'', Array, "Headers to set on the item in S3." ) do |h|
@@ -144,6 +160,19 @@ def with_headers(msg)
144
160
  msg
145
161
  end
146
162
 
163
+ def md5(filename)
164
+ digest = Digest::MD5.new()
165
+ file = File.open(filename, 'r')
166
+ begin
167
+ file.each_line do |line|
168
+ digest << line
169
+ end
170
+ ensure
171
+ file.close()
172
+ end
173
+ digest.hexdigest
174
+ end
175
+
147
176
  def s3_to_s3(bucket_from, key, bucket_to, dest)
148
177
  log(with_headers("Copy s3://#{bucket_from}/#{key} to s3://#{bucket_to}/#{dest}"))
149
178
  if @headers.empty?
@@ -153,26 +182,65 @@ def s3_to_s3(bucket_from, key, bucket_to, dest)
153
182
  end
154
183
  end
155
184
 
156
- def local_to_s3(bucket_to, key, file)
185
+ def local_to_s3(bucket_to, key, file, options = {})
157
186
  log(with_headers("Copy #{file} to s3://#{bucket_to}/#{key}"))
158
- f = File.open(file)
159
- begin
160
- @s3.interface.put(bucket_to, key, f, @headers)
161
- ensure
162
- f.close()
187
+ if options[:checksum]
188
+ expected_md5 = md5(file)
163
189
  end
190
+ retries = 0
191
+ begin
192
+ if retries == options[:max_attempts]
193
+ fail "Unable to upload to s3://#{bucket_from}/#{key_from} after #{retries} attempts."
194
+ end
195
+ sleep options[:retry_delay] if retries > 0
196
+
197
+ f = File.open(file)
198
+ begin
199
+ meta = @s3.interface.put(bucket_to, key, f, @headers)
200
+
201
+ if options[:checksum]
202
+ metadata = @s3.interface.head(bucket_to, key)
203
+ actual_md5 = metadata["etag"] or fail "Unable to get etag/md5 for #{bucket_to}:#{key}"
204
+ actual_md5 = actual_md5.sub(/^"/, "").sub(/"$/, "") # strip beginning and trailing quotes
205
+ end
206
+ rescue => e
207
+ raise e unless options[:checksum]
208
+ STDERR.puts e
209
+ ensure
210
+ f.close()
211
+ end
212
+ retries += 1
213
+ end until options[:checksum] == false || expected_md5 == actual_md5
164
214
  end
165
215
 
166
- def s3_to_local(bucket_from, key_from, dest)
216
+ def s3_to_local(bucket_from, key_from, dest, options = {})
167
217
  log("Copy s3://#{bucket_from}/#{key_from} to #{dest}")
168
- f = File.new(dest, "wb")
218
+ retries = 0
169
219
  begin
170
- @s3.interface.get(bucket_from, key_from) do |chunk|
171
- f.write(chunk)
220
+ if retries == options[:max_attempts]
221
+ File.delete(dest) if File.exist?(dest)
222
+ fail "Unable to download s3://#{bucket_from}/#{key_from} after #{retries} attempts."
172
223
  end
173
- ensure
174
- f.close()
175
- end
224
+ sleep options[:retry_delay] if retries > 0
225
+
226
+ f = File.new(dest, "wb")
227
+ begin
228
+ if options[:checksum]
229
+ metadata = @s3.interface.head(bucket_from, key_from)
230
+ expected_md5 = metadata["etag"] or fail "Unable to get etag/md5 for #{bucket_from}:#{key_from}"
231
+ expected_md5 = expected_md5.sub(/^"/, "").sub(/"$/, "") # strip beginning and trailing quotes
232
+ end
233
+ @s3.interface.get(bucket_from, key_from) do |chunk|
234
+ f.write(chunk)
235
+ end
236
+ rescue => e
237
+ raise e unless options[:checksum]
238
+ STDERR.puts e
239
+ ensure
240
+ f.close()
241
+ end
242
+ retries += 1
243
+ end until options[:checksum] == false || md5(dest) == expected_md5
176
244
  end
177
245
 
178
246
  def s3_exist?(bucket, key)
@@ -209,10 +277,10 @@ def copy(from, to, options)
209
277
  files.each do |f|
210
278
  f = File.expand_path(f)
211
279
  key = no_slash(key_to) + '/' + relative(from, f)
212
- local_to_s3(bucket_to, key, f) unless !options[:overwrite] && s3_exist?(bucket_to, key)
280
+ local_to_s3(bucket_to, key, f, options) unless !options[:overwrite] && s3_exist?(bucket_to, key)
213
281
  end
214
282
  else
215
- local_to_s3(bucket_to, key_to, File.expand_path(from)) unless !options[:overwrite] && s3_exist?(bucket_to, key_to)
283
+ local_to_s3(bucket_to, key_to, File.expand_path(from), options) unless !options[:overwrite] && s3_exist?(bucket_to, key_to)
216
284
  end
217
285
  when :s3_to_local
218
286
  if options[:recursive]
@@ -226,12 +294,12 @@ def copy(from, to, options)
226
294
  dir = File.dirname(dest)
227
295
  FileUtils.mkdir_p dir unless File.exist? dir
228
296
  fail "Destination path is not a directory: #{dir}" unless File.directory?(dir)
229
- s3_to_local(bucket_from, key, dest) unless !options[:overwrite] && File.exist?(dest)
297
+ s3_to_local(bucket_from, key, dest, options) unless !options[:overwrite] && File.exist?(dest)
230
298
  end
231
299
  else
232
300
  dest = File.expand_path(to)
233
301
  dest = File.join(dest, File.basename(key_from)) if File.directory?(dest)
234
- s3_to_local(bucket_from, key_from, dest) unless !options[:overwrite] && File.exist?(dest)
302
+ s3_to_local(bucket_from, key_from, dest, options) unless !options[:overwrite] && File.exist?(dest)
235
303
  end
236
304
  when :local_to_local
237
305
  if options[:recursive]
data/lib/s3cp/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
 
2
2
  module S3CP
3
- VERSION = "0.1.14"
3
+ VERSION = "0.1.15"
4
4
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: s3cp
3
3
  version: !ruby/object:Gem::Version
4
- hash: 7
4
+ hash: 5
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 14
10
- version: 0.1.14
9
+ - 15
10
+ version: 0.1.15
11
11
  platform: ruby
12
12
  authors:
13
13
  - Alex Boisvert
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-02-10 00:00:00 Z
18
+ date: 2012-02-18 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  prerelease: false