s3cp 0.1.14 → 0.1.15

Sign up to get free protection for your applications and to get access to all the features.
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