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 +9 -1
- data/lib/s3cp/s3cp.rb +85 -17
- data/lib/s3cp/version.rb +1 -1
- metadata +4 -4
data/History.txt
CHANGED
@@ -1,4 +1,12 @@
|
|
1
|
-
=== 0.1.
|
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
|
-
|
159
|
-
|
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
|
-
|
218
|
+
retries = 0
|
169
219
|
begin
|
170
|
-
|
171
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
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
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:
|
4
|
+
hash: 5
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
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-
|
18
|
+
date: 2012-02-18 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
prerelease: false
|