s3sync 1.2.5 → 2.0.0
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/bin/s3sync +67 -726
- data/lib/s3sync.rb +2 -0
- data/lib/s3sync/cli.rb +475 -0
- data/lib/s3sync/config.rb +98 -0
- data/lib/s3sync/exceptions.rb +55 -0
- data/lib/s3sync/sync.rb +371 -0
- data/lib/s3sync/util.rb +29 -0
- data/lib/s3sync/version.rb +27 -0
- metadata +177 -54
- data/CHANGELOG +0 -175
- data/README +0 -401
- data/README_s3cmd +0 -172
- data/Rakefile +0 -35
- data/bin/s3cmd +0 -245
- data/lib/HTTPStreaming.rb +0 -103
- data/lib/S3.rb +0 -707
- data/lib/S3_s3sync_mod.rb +0 -143
- data/lib/S3encoder.rb +0 -50
- data/lib/s3config.rb +0 -27
- data/lib/s3try.rb +0 -161
- data/lib/thread_generator.rb +0 -383
- data/lib/version.rb +0 -9
- data/setup.rb +0 -1585
data/Rakefile
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'rake'
|
3
|
-
require 'rake/clean'
|
4
|
-
require 'rake/testtask'
|
5
|
-
require 'rake/packagetask'
|
6
|
-
require 'rake/gempackagetask'
|
7
|
-
require 'rake/rdoctask'
|
8
|
-
require File.join(File.dirname(__FILE__), 'lib', 'version')
|
9
|
-
|
10
|
-
Gem::manage_gems
|
11
|
-
|
12
|
-
readmes = ["README","README_s3cmd"]
|
13
|
-
|
14
|
-
spec = Gem::Specification.new do |s|
|
15
|
-
s.platform = Gem::Platform::RUBY
|
16
|
-
s.name = "s3sync"
|
17
|
-
s.version = S3sync::VERSION::STRING
|
18
|
-
s.author = ""
|
19
|
-
s.email = ""
|
20
|
-
s.homepage = "http://s3sync.net/"
|
21
|
-
s.rubyforge_project = "s3sync"
|
22
|
-
s.summary = "rsync-like client for backing up to Amazons S3"
|
23
|
-
s.files = Dir.glob("{bin,lib,docs}/**/*.rb") + ["Rakefile", "setup.rb", "CHANGELOG"] + readmes
|
24
|
-
s.require_path = "lib"
|
25
|
-
s.executables = ['s3sync','s3cmd']
|
26
|
-
s.has_rdoc = true
|
27
|
-
s.extra_rdoc_files = readmes
|
28
|
-
end
|
29
|
-
Rake::GemPackageTask.new(spec) do |pkg|
|
30
|
-
pkg.need_zip = true
|
31
|
-
pkg.need_tar = true
|
32
|
-
end
|
33
|
-
task :default => "pkg/#{spec.name}-#{spec.version}.gem" do
|
34
|
-
puts "generated latest version"
|
35
|
-
end
|
data/bin/s3cmd
DELETED
@@ -1,245 +0,0 @@
|
|
1
|
-
#! /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby
|
2
|
-
# This software code is made available "AS IS" without warranties of any
|
3
|
-
# kind. You may copy, display, modify and redistribute the software
|
4
|
-
# code either by itself or as incorporated into your code; provided that
|
5
|
-
# you do not remove any proprietary notices. Your use of this software
|
6
|
-
# code is at your own risk and you waive any claim against the author
|
7
|
-
# with respect to your use of this software code.
|
8
|
-
# (c) 2007 s3sync.net
|
9
|
-
#
|
10
|
-
|
11
|
-
module S3sync
|
12
|
-
|
13
|
-
# always look "here" for include files (thanks aktxyz)
|
14
|
-
$LOAD_PATH << File.expand_path(File.dirname(__FILE__))
|
15
|
-
|
16
|
-
require 's3try'
|
17
|
-
|
18
|
-
$S3CMD_VERSION = '1.2.5'
|
19
|
-
|
20
|
-
require 'getoptlong'
|
21
|
-
|
22
|
-
# after other mods, so we don't overwrite yaml vals with defaults
|
23
|
-
require 's3config'
|
24
|
-
include S3Config
|
25
|
-
|
26
|
-
def S3sync.s3cmdMain
|
27
|
-
# ---------- OPTIONS PROCESSING ---------- #
|
28
|
-
|
29
|
-
$S3syncOptions = Hash.new
|
30
|
-
optionsParser = GetoptLong.new(
|
31
|
-
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
|
32
|
-
[ '--ssl', '-s', GetoptLong::NO_ARGUMENT ],
|
33
|
-
[ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
|
34
|
-
[ '--dryrun', '-n', GetoptLong::NO_ARGUMENT ],
|
35
|
-
[ '--debug', '-d', GetoptLong::NO_ARGUMENT ],
|
36
|
-
[ '--progress', GetoptLong::NO_ARGUMENT ],
|
37
|
-
[ '--expires-in', GetoptLong::REQUIRED_ARGUMENT ]
|
38
|
-
)
|
39
|
-
|
40
|
-
def S3sync.s3cmdUsage(message = nil)
|
41
|
-
$stderr.puts message if message
|
42
|
-
name = $0.split('/').last
|
43
|
-
$stderr.puts <<"ENDUSAGE"
|
44
|
-
#{name} [options] <command> [arg(s)]\t\tversion #{$S3CMD_VERSION}
|
45
|
-
--help -h --verbose -v --dryrun -n
|
46
|
-
--ssl -s --debug -d --progress
|
47
|
-
--expires-in=( <# of seconds> | [#d|#h|#m|#s] )
|
48
|
-
|
49
|
-
Commands:
|
50
|
-
#{name} listbuckets [headers]
|
51
|
-
#{name} createbucket <bucket> [constraint (i.e. EU)]
|
52
|
-
#{name} deletebucket <bucket> [headers]
|
53
|
-
#{name} list <bucket>[:prefix] [max/page] [delimiter] [headers]
|
54
|
-
#{name} location <bucket> [headers]
|
55
|
-
#{name} delete <bucket>:key [headers]
|
56
|
-
#{name} deleteall <bucket>[:prefix] [headers]
|
57
|
-
#{name} get|put <bucket>:key <file> [headers]
|
58
|
-
ENDUSAGE
|
59
|
-
exit
|
60
|
-
end #usage
|
61
|
-
|
62
|
-
begin
|
63
|
-
optionsParser.each {|opt, arg| $S3syncOptions[opt] = (arg || true)}
|
64
|
-
rescue StandardError
|
65
|
-
s3cmdUsage # the parser already printed an error message
|
66
|
-
end
|
67
|
-
s3cmdUsage if $S3syncOptions['--help']
|
68
|
-
$S3syncOptions['--verbose'] = true if $S3syncOptions['--dryrun'] or $S3syncOptions['--debug'] or $S3syncOptions['--progress']
|
69
|
-
$S3syncOptions['--ssl'] = true if $S3syncOptions['--ssl'] # change from "" to true to appease s3 port chooser
|
70
|
-
|
71
|
-
if $S3syncOptions['--expires-in'] =~ /d|h|m|s/
|
72
|
-
e = $S3syncOptions['--expires-in']
|
73
|
-
days = (e =~ /(\d+)d/)? (/(\d+)d/.match(e))[1].to_i : 0
|
74
|
-
hours = (e =~ /(\d+)h/)? (/(\d+)h/.match(e))[1].to_i : 0
|
75
|
-
minutes = (e =~ /(\d+)m/)? (/(\d+)m/.match(e))[1].to_i : 0
|
76
|
-
seconds = (e =~ /(\d+)s/)? (/(\d+)s/.match(e))[1].to_i : 0
|
77
|
-
$S3syncOptions['--expires-in'] = seconds + 60 * ( minutes + 60 * ( hours + 24 * ( days ) ) )
|
78
|
-
end
|
79
|
-
|
80
|
-
# ---------- CONNECT ---------- #
|
81
|
-
S3sync::s3trySetup
|
82
|
-
# ---------- COMMAND PROCESSING ---------- #
|
83
|
-
|
84
|
-
command, path, file = ARGV
|
85
|
-
|
86
|
-
s3cmdUsage("You didn't set up your environment variables; see README.txt") if not($AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY)
|
87
|
-
s3cmdUsage("Need a command (etc)") if not command
|
88
|
-
|
89
|
-
path = '' unless path
|
90
|
-
path = path.dup # modifiable
|
91
|
-
path += ':' unless path.match(':')
|
92
|
-
bucket = (/^(.*?):/.match(path))[1]
|
93
|
-
path.replace((/:(.*)$/.match(path))[1])
|
94
|
-
|
95
|
-
case command
|
96
|
-
when "delete"
|
97
|
-
s3cmdUsage("Need a bucket") if bucket == ''
|
98
|
-
s3cmdUsage("Need a key") if path == ''
|
99
|
-
headers = hashPairs(ARGV[2...ARGV.length])
|
100
|
-
$stderr.puts "delete #{bucket}:#{path} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
|
101
|
-
S3try(:delete, bucket, path) unless $S3syncOptions['--dryrun']
|
102
|
-
when "deleteall"
|
103
|
-
s3cmdUsage("Need a bucket") if bucket == ''
|
104
|
-
headers = hashPairs(ARGV[2...ARGV.length])
|
105
|
-
$stderr.puts "delete ALL entries in #{bucket}:#{path} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
|
106
|
-
more = true
|
107
|
-
marker = nil
|
108
|
-
while more do
|
109
|
-
res = s3cmdList(bucket, path, nil, nil, marker)
|
110
|
-
res.entries.each do |item|
|
111
|
-
# the s3 commands (with my modified UTF-8 conversion) expect native char encoding input
|
112
|
-
key = Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", item.key).join
|
113
|
-
$stderr.puts "delete #{bucket}:#{key} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
|
114
|
-
S3try(:delete, bucket, key) unless $S3syncOptions['--dryrun']
|
115
|
-
end
|
116
|
-
more = res.properties.is_truncated
|
117
|
-
marker = (res.properties.next_marker)? res.properties.next_marker : ((res.entries.length > 0) ? res.entries.last.key : nil)
|
118
|
-
# get this into local charset; when we pass it to s3 that is what's expected
|
119
|
-
marker = Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", marker).join if marker
|
120
|
-
end
|
121
|
-
when "list"
|
122
|
-
s3cmdUsage("Need a bucket") if bucket == ''
|
123
|
-
max, delim = ARGV[2..3]
|
124
|
-
headers = hashPairs(ARGV[4...ARGV.length])
|
125
|
-
$stderr.puts "list #{bucket}:#{path} #{max} #{delim} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
|
126
|
-
puts "--------------------"
|
127
|
-
|
128
|
-
more = true
|
129
|
-
marker = nil
|
130
|
-
while more do
|
131
|
-
res = s3cmdList(bucket, path, max, delim, marker, headers)
|
132
|
-
if delim
|
133
|
-
res.common_prefix_entries.each do |item|
|
134
|
-
|
135
|
-
puts "dir: " + Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", item.prefix).join
|
136
|
-
end
|
137
|
-
puts "--------------------"
|
138
|
-
end
|
139
|
-
res.entries.each do |item|
|
140
|
-
puts Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", item.key).join
|
141
|
-
end
|
142
|
-
if res.properties.is_truncated
|
143
|
-
printf "More? Y/n: "
|
144
|
-
more = (STDIN.gets.match('^[Yy]?$'))
|
145
|
-
marker = (res.properties.next_marker)? res.properties.next_marker : ((res.entries.length > 0) ? res.entries.last.key : nil)
|
146
|
-
# get this into local charset; when we pass it to s3 that is what's expected
|
147
|
-
marker = Iconv.iconv($S3SYNC_NATIVE_CHARSET, "UTF-8", marker).join if marker
|
148
|
-
|
149
|
-
else
|
150
|
-
more = false
|
151
|
-
end
|
152
|
-
end # more
|
153
|
-
when "listbuckets"
|
154
|
-
headers = hashPairs(ARGV[1...ARGV.length])
|
155
|
-
$stderr.puts "list all buckets #{headers.inspect if headers}" if $S3syncOptions['--verbose']
|
156
|
-
if $S3syncOptions['--expires-in']
|
157
|
-
$stdout.puts S3url(:list_all_my_buckets, headers)
|
158
|
-
else
|
159
|
-
res = S3try(:list_all_my_buckets, headers)
|
160
|
-
res.entries.each do |item|
|
161
|
-
puts item.name
|
162
|
-
end
|
163
|
-
end
|
164
|
-
when "createbucket"
|
165
|
-
s3cmdUsage("Need a bucket") if bucket == ''
|
166
|
-
lc = ''
|
167
|
-
if(ARGV.length > 2)
|
168
|
-
lc = '<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01"><LocationConstraint>' + ARGV[2] + '</LocationConstraint></CreateBucketConfiguration>'
|
169
|
-
end
|
170
|
-
$stderr.puts "create bucket #{bucket} #{lc}" if $S3syncOptions['--verbose']
|
171
|
-
S3try(:create_bucket, bucket, lc) unless $S3syncOptions['--dryrun']
|
172
|
-
when "deletebucket"
|
173
|
-
s3cmdUsage("Need a bucket") if bucket == ''
|
174
|
-
headers = hashPairs(ARGV[2...ARGV.length])
|
175
|
-
$stderr.puts "delete bucket #{bucket} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
|
176
|
-
S3try(:delete_bucket, bucket, headers) unless $S3syncOptions['--dryrun']
|
177
|
-
when "location"
|
178
|
-
s3cmdUsage("Need a bucket") if bucket == ''
|
179
|
-
headers = hashPairs(ARGV[2...ARGV.length])
|
180
|
-
query = Hash.new
|
181
|
-
query['location'] = 'location'
|
182
|
-
$stderr.puts "location request bucket #{bucket} #{query.inspect} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
|
183
|
-
S3try(:get_query_stream, bucket, '', query, headers, $stdout) unless $S3syncOptions['--dryrun']
|
184
|
-
when "get"
|
185
|
-
s3cmdUsage("Need a bucket") if bucket == ''
|
186
|
-
s3cmdUsage("Need a key") if path == ''
|
187
|
-
s3cmdUsage("Need a file") if file == ''
|
188
|
-
headers = hashPairs(ARGV[3...ARGV.length])
|
189
|
-
$stderr.puts "get from key #{bucket}:#{path} into #{file} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
|
190
|
-
unless $S3syncOptions['--dryrun']
|
191
|
-
if $S3syncOptions['--expires-in']
|
192
|
-
$stdout.puts S3url(:get, bucket, path, headers)
|
193
|
-
else
|
194
|
-
outStream = File.open(file, 'wb')
|
195
|
-
outStream = ProgressStream.new(outStream) if $S3syncOptions['--progress']
|
196
|
-
S3try(:get_stream, bucket, path, headers, outStream)
|
197
|
-
outStream.close
|
198
|
-
end
|
199
|
-
end
|
200
|
-
when "put"
|
201
|
-
s3cmdUsage("Need a bucket") if bucket == ''
|
202
|
-
s3cmdUsage("Need a key") if path == ''
|
203
|
-
s3cmdUsage("Need a file") if file == ''
|
204
|
-
headers = hashPairs(ARGV[3...ARGV.length])
|
205
|
-
stream = File.open(file, 'rb')
|
206
|
-
stream = ProgressStream.new(stream, File.stat(file).size) if $S3syncOptions['--progress']
|
207
|
-
s3o = S3::S3Object.new(stream, {}) # support meta later?
|
208
|
-
headers['Content-Length'] = FileTest.size(file).to_s
|
209
|
-
$stderr.puts "put to key #{bucket}:#{path} from #{file} #{headers.inspect if headers}" if $S3syncOptions['--verbose']
|
210
|
-
S3try(:put, bucket, path, s3o, headers) unless $S3syncOptions['--dryrun']
|
211
|
-
stream.close
|
212
|
-
else
|
213
|
-
s3cmdUsage
|
214
|
-
end
|
215
|
-
|
216
|
-
end #main
|
217
|
-
def S3sync.s3cmdList(bucket, path, max=nil, delim=nil, marker=nil, headers={})
|
218
|
-
debug(max)
|
219
|
-
options = Hash.new
|
220
|
-
options['prefix'] = path # start at the right depth
|
221
|
-
options['max-keys'] = max ? max.to_s : 100
|
222
|
-
options['delimiter'] = delim if delim
|
223
|
-
options['marker'] = marker if marker
|
224
|
-
S3try(:list_bucket, bucket, options, headers)
|
225
|
-
end
|
226
|
-
|
227
|
-
# turn an array into a hash of pairs
|
228
|
-
def S3sync.hashPairs(ar)
|
229
|
-
ret = Hash.new
|
230
|
-
ar.each do |item|
|
231
|
-
name = (/^(.*?):/.match(item))[1]
|
232
|
-
item = (/^.*?:(.*)$/.match(item))[1]
|
233
|
-
ret[name] = item
|
234
|
-
end if ar
|
235
|
-
ret
|
236
|
-
end
|
237
|
-
end #module
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
def debug(str)
|
242
|
-
$stderr.puts str if $S3syncOptions['--debug']
|
243
|
-
end
|
244
|
-
|
245
|
-
S3sync::s3cmdMain #go!
|
data/lib/HTTPStreaming.rb
DELETED
@@ -1,103 +0,0 @@
|
|
1
|
-
# This software code is made available "AS IS" without warranties of any
|
2
|
-
# kind. You may copy, display, modify and redistribute the software
|
3
|
-
# code either by itself or as incorporated into your code; provided that
|
4
|
-
# you do not remove any proprietary notices. Your use of this software
|
5
|
-
# code is at your own risk and you waive any claim against the author
|
6
|
-
# with respect to your use of this software code.
|
7
|
-
# (c) 2007 s3sync.net
|
8
|
-
#
|
9
|
-
|
10
|
-
# The purpose of this file is to overlay the net/http library
|
11
|
-
# to add some functionality
|
12
|
-
# (without changing the file itself or requiring a specific version)
|
13
|
-
# It still isn't perfectly robust, i.e. if radical changes are made
|
14
|
-
# to the underlying lib this stuff will need updating.
|
15
|
-
|
16
|
-
require 'net/http'
|
17
|
-
|
18
|
-
module Net
|
19
|
-
|
20
|
-
$HTTPStreamingDebug = false
|
21
|
-
|
22
|
-
# Allow request body to be an IO stream
|
23
|
-
# Allow an IO stream argument to stream the response body out
|
24
|
-
class HTTP
|
25
|
-
alias _HTTPStreaming_request request
|
26
|
-
|
27
|
-
def request(req, body = nil, streamResponseBodyTo = nil, &block)
|
28
|
-
if not block_given? and streamResponseBodyTo and streamResponseBodyTo.respond_to?(:write)
|
29
|
-
$stderr.puts "Response using streaming" if $HTTPStreamingDebug
|
30
|
-
# this might be a retry, we should make sure the stream is at its beginning
|
31
|
-
streamResponseBodyTo.rewind if streamResponseBodyTo.respond_to?(:rewind) and streamResponseBodyTo != $stdout
|
32
|
-
block = proc do |res|
|
33
|
-
res.read_body do |chunk|
|
34
|
-
streamResponseBodyTo.write(chunk)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
if body != nil && body.respond_to?(:read)
|
39
|
-
$stderr.puts "Request using streaming" if $HTTPStreamingDebug
|
40
|
-
# this might be a retry, we should make sure the stream is at its beginning
|
41
|
-
body.rewind if body.respond_to?(:rewind)
|
42
|
-
req.body_stream = body
|
43
|
-
return _HTTPStreaming_request(req, nil, &block)
|
44
|
-
else
|
45
|
-
return _HTTPStreaming_request(req, body, &block)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
end #module
|
51
|
-
|
52
|
-
module S3sync
|
53
|
-
class ProgressStream < SimpleDelegator
|
54
|
-
def initialize(s, size=0)
|
55
|
-
@start = @last = Time.new
|
56
|
-
@total = size
|
57
|
-
@transferred = 0
|
58
|
-
@closed = false
|
59
|
-
@printed = false
|
60
|
-
@innerStream = s
|
61
|
-
super(@innerStream)
|
62
|
-
__setobj__(@innerStream)
|
63
|
-
end
|
64
|
-
# need to catch reads and writes so we can count what's being transferred
|
65
|
-
def read(i)
|
66
|
-
res = @innerStream.read(i)
|
67
|
-
@transferred += res.respond_to?(:length) ? res.length : 0
|
68
|
-
now = Time.new
|
69
|
-
if(now - @last > 1) # don't do this oftener than once per second
|
70
|
-
@printed = true
|
71
|
-
$stdout.printf("\rProgress: %db %db/s %s ", @transferred, (@transferred/(now - @start)).floor,
|
72
|
-
@total > 0? (100 * @transferred/@total).floor.to_s + "%" : ""
|
73
|
-
)
|
74
|
-
$stdout.flush
|
75
|
-
@last = now
|
76
|
-
end
|
77
|
-
res
|
78
|
-
end
|
79
|
-
def write(s)
|
80
|
-
@transferred += s.length
|
81
|
-
res = @innerStream.write(s)
|
82
|
-
now = Time.new
|
83
|
-
if(now -@last > 1) # don't do this oftener than once per second
|
84
|
-
@printed = true
|
85
|
-
$stdout.printf("\rProgress: %db %db/s %s ", @transferred, (@transferred/(now - @start)).floor,
|
86
|
-
@total > 0? (100 * @transferred/@total).floor.to_s + "%" : ""
|
87
|
-
)
|
88
|
-
$stdout.flush
|
89
|
-
@last = now
|
90
|
-
end
|
91
|
-
res
|
92
|
-
end
|
93
|
-
def rewind()
|
94
|
-
@transferred = 0
|
95
|
-
@innerStream.rewind if @innerStream.respond_to?(:rewind)
|
96
|
-
end
|
97
|
-
def close()
|
98
|
-
$stdout.printf("\n") if @printed and not @closed
|
99
|
-
@closed = true
|
100
|
-
@innerStream.close
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end #module
|
data/lib/S3.rb
DELETED
@@ -1,707 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
# This software code is made available "AS IS" without warranties of any
|
4
|
-
# kind. You may copy, display, modify and redistribute the software
|
5
|
-
# code either by itself or as incorporated into your code; provided that
|
6
|
-
# you do not remove any proprietary notices. Your use of this software
|
7
|
-
# code is at your own risk and you waive any claim against Amazon
|
8
|
-
# Digital Services, Inc. or its affiliates with respect to your use of
|
9
|
-
# this software code. (c) 2006 Amazon Digital Services, Inc. or its
|
10
|
-
# affiliates.
|
11
|
-
|
12
|
-
require 'base64'
|
13
|
-
require 'cgi'
|
14
|
-
require 'openssl'
|
15
|
-
require 'digest/sha1'
|
16
|
-
require 'net/https'
|
17
|
-
require 'rexml/document'
|
18
|
-
require 'time'
|
19
|
-
|
20
|
-
# this wasn't added until v 1.8.3
|
21
|
-
if (RUBY_VERSION < '1.8.3')
|
22
|
-
class Net::HTTP::Delete < Net::HTTPRequest
|
23
|
-
METHOD = 'DELETE'
|
24
|
-
REQUEST_HAS_BODY = false
|
25
|
-
RESPONSE_HAS_BODY = true
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# this module has two big classes: AWSAuthConnection and
|
30
|
-
# QueryStringAuthGenerator. both use identical apis, but the first actually
|
31
|
-
# performs the operation, while the second simply outputs urls with the
|
32
|
-
# appropriate authentication query string parameters, which could be used
|
33
|
-
# in another tool (such as your web browser for GETs).
|
34
|
-
module S3
|
35
|
-
DEFAULT_HOST = 's3.amazonaws.com'
|
36
|
-
PORTS_BY_SECURITY = { true => 443, false => 80 }
|
37
|
-
METADATA_PREFIX = 'x-amz-meta-'
|
38
|
-
AMAZON_HEADER_PREFIX = 'x-amz-'
|
39
|
-
|
40
|
-
# builds the canonical string for signing.
|
41
|
-
def S3.canonical_string(method, bucket="", path="", path_args={}, headers={}, expires=nil)
|
42
|
-
interesting_headers = {}
|
43
|
-
headers.each do |key, value|
|
44
|
-
lk = key.downcase
|
45
|
-
if (lk == 'content-md5' or
|
46
|
-
lk == 'content-type' or
|
47
|
-
lk == 'date' or
|
48
|
-
lk =~ /^#{AMAZON_HEADER_PREFIX}/o)
|
49
|
-
interesting_headers[lk] = value.to_s.strip
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
# these fields get empty strings if they don't exist.
|
54
|
-
interesting_headers['content-type'] ||= ''
|
55
|
-
interesting_headers['content-md5'] ||= ''
|
56
|
-
|
57
|
-
# just in case someone used this. it's not necessary in this lib.
|
58
|
-
if interesting_headers.has_key? 'x-amz-date'
|
59
|
-
interesting_headers['date'] = ''
|
60
|
-
end
|
61
|
-
|
62
|
-
# if you're using expires for query string auth, then it trumps date
|
63
|
-
# (and x-amz-date)
|
64
|
-
if not expires.nil?
|
65
|
-
interesting_headers['date'] = expires
|
66
|
-
end
|
67
|
-
|
68
|
-
buf = "#{method}\n"
|
69
|
-
interesting_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
|
70
|
-
if key =~ /^#{AMAZON_HEADER_PREFIX}/o
|
71
|
-
buf << "#{key}:#{value}\n"
|
72
|
-
else
|
73
|
-
buf << "#{value}\n"
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# build the path using the bucket and key
|
78
|
-
if not bucket.empty?
|
79
|
-
buf << "/#{bucket}"
|
80
|
-
end
|
81
|
-
# append the key (it might be empty string)
|
82
|
-
# append a slash regardless
|
83
|
-
buf << "/#{path}"
|
84
|
-
|
85
|
-
# if there is an acl, logging, or torrent parameter
|
86
|
-
# add them to the string
|
87
|
-
if path_args.has_key?('acl')
|
88
|
-
buf << '?acl'
|
89
|
-
elsif path_args.has_key?('torrent')
|
90
|
-
buf << '?torrent'
|
91
|
-
elsif path_args.has_key?('location')
|
92
|
-
buf << '?location'
|
93
|
-
elsif path_args.has_key?('logging')
|
94
|
-
buf << '?logging'
|
95
|
-
end
|
96
|
-
|
97
|
-
return buf
|
98
|
-
end
|
99
|
-
|
100
|
-
# encodes the given string with the aws_secret_access_key, by taking the
|
101
|
-
# hmac-sha1 sum, and then base64 encoding it. optionally, it will also
|
102
|
-
# url encode the result of that to protect the string if it's going to
|
103
|
-
# be used as a query string parameter.
|
104
|
-
def S3.encode(aws_secret_access_key, str, urlencode=false)
|
105
|
-
digest = OpenSSL::Digest::Digest.new('sha1')
|
106
|
-
b64_hmac =
|
107
|
-
Base64.encode64(
|
108
|
-
OpenSSL::HMAC.digest(digest, aws_secret_access_key, str)).strip
|
109
|
-
|
110
|
-
if urlencode
|
111
|
-
return CGI::escape(b64_hmac)
|
112
|
-
else
|
113
|
-
return b64_hmac
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# build the path_argument string
|
118
|
-
def S3.path_args_hash_to_string(path_args={})
|
119
|
-
arg_string = ''
|
120
|
-
path_args.each { |k, v|
|
121
|
-
arg_string << k
|
122
|
-
if not v.nil?
|
123
|
-
arg_string << "=#{CGI::escape(v)}"
|
124
|
-
end
|
125
|
-
arg_string << '&'
|
126
|
-
}
|
127
|
-
return arg_string
|
128
|
-
end
|
129
|
-
|
130
|
-
|
131
|
-
# uses Net::HTTP to interface with S3. note that this interface should only
|
132
|
-
# be used for smaller objects, as it does not stream the data. if you were
|
133
|
-
# to download a 1gb file, it would require 1gb of memory. also, this class
|
134
|
-
# creates a new http connection each time. it would be greatly improved with
|
135
|
-
# some connection pooling.
|
136
|
-
class AWSAuthConnection
|
137
|
-
attr_accessor :calling_format
|
138
|
-
|
139
|
-
def initialize(aws_access_key_id, aws_secret_access_key, is_secure=true,
|
140
|
-
server=DEFAULT_HOST, port=PORTS_BY_SECURITY[is_secure],
|
141
|
-
calling_format=CallingFormat::REGULAR)
|
142
|
-
@aws_access_key_id = aws_access_key_id
|
143
|
-
@aws_secret_access_key = aws_secret_access_key
|
144
|
-
@server = server
|
145
|
-
@is_secure = is_secure
|
146
|
-
@calling_format = calling_format
|
147
|
-
@port = port
|
148
|
-
end
|
149
|
-
|
150
|
-
def create_bucket(bucket, headers={})
|
151
|
-
return Response.new(make_request('PUT', bucket, '', {}, headers))
|
152
|
-
end
|
153
|
-
|
154
|
-
# takes options :prefix, :marker, :max_keys, and :delimiter
|
155
|
-
def list_bucket(bucket, options={}, headers={})
|
156
|
-
path_args = {}
|
157
|
-
options.each { |k, v|
|
158
|
-
path_args[k] = v.to_s
|
159
|
-
}
|
160
|
-
|
161
|
-
return ListBucketResponse.new(make_request('GET', bucket, '', path_args, headers))
|
162
|
-
end
|
163
|
-
|
164
|
-
def delete_bucket(bucket, headers={})
|
165
|
-
return Response.new(make_request('DELETE', bucket, '', {}, headers))
|
166
|
-
end
|
167
|
-
|
168
|
-
def put(bucket, key, object, headers={})
|
169
|
-
object = S3Object.new(object) if not object.instance_of? S3Object
|
170
|
-
|
171
|
-
return Response.new(
|
172
|
-
make_request('PUT', bucket, CGI::escape(key), {}, headers, object.data, object.metadata)
|
173
|
-
)
|
174
|
-
end
|
175
|
-
|
176
|
-
def get(bucket, key, headers={})
|
177
|
-
return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {}, headers))
|
178
|
-
end
|
179
|
-
|
180
|
-
def delete(bucket, key, headers={})
|
181
|
-
return Response.new(make_request('DELETE', bucket, CGI::escape(key), {}, headers))
|
182
|
-
end
|
183
|
-
|
184
|
-
def head(bucket, key, headers={})
|
185
|
-
return GetResponse.new(make_request('HEAD', bucket, CGI::escape(key), {}, headers))
|
186
|
-
end
|
187
|
-
|
188
|
-
def get_bucket_logging(bucket, headers={})
|
189
|
-
return GetResponse.new(make_request('GET', bucket, '', {'logging' => nil}, headers))
|
190
|
-
end
|
191
|
-
|
192
|
-
def put_bucket_logging(bucket, logging_xml_doc, headers={})
|
193
|
-
return Response.new(make_request('PUT', bucket, '', {'logging' => nil}, headers, logging_xml_doc))
|
194
|
-
end
|
195
|
-
|
196
|
-
def get_bucket_acl(bucket, headers={})
|
197
|
-
return get_acl(bucket, '', headers)
|
198
|
-
end
|
199
|
-
|
200
|
-
# returns an xml document representing the access control list.
|
201
|
-
# this could be parsed into an object.
|
202
|
-
def get_acl(bucket, key, headers={})
|
203
|
-
return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {'acl' => nil}, headers))
|
204
|
-
end
|
205
|
-
|
206
|
-
def put_bucket_acl(bucket, acl_xml_doc, headers={})
|
207
|
-
return put_acl(bucket, '', acl_xml_doc, headers)
|
208
|
-
end
|
209
|
-
|
210
|
-
# sets the access control policy for the given resource. acl_xml_doc must
|
211
|
-
# be a string in the acl xml format.
|
212
|
-
def put_acl(bucket, key, acl_xml_doc, headers={})
|
213
|
-
return Response.new(
|
214
|
-
make_request('PUT', bucket, CGI::escape(key), {'acl' => nil}, headers, acl_xml_doc, {})
|
215
|
-
)
|
216
|
-
end
|
217
|
-
|
218
|
-
def list_all_my_buckets(headers={})
|
219
|
-
return ListAllMyBucketsResponse.new(make_request('GET', '', '', {}, headers))
|
220
|
-
end
|
221
|
-
|
222
|
-
private
|
223
|
-
def make_request(method, bucket='', key='', path_args={}, headers={}, data='', metadata={})
|
224
|
-
|
225
|
-
# build the domain based on the calling format
|
226
|
-
server = ''
|
227
|
-
if bucket.empty?
|
228
|
-
# for a bucketless request (i.e. list all buckets)
|
229
|
-
# revert to regular domain case since this operation
|
230
|
-
# does not make sense for vanity domains
|
231
|
-
server = @server
|
232
|
-
elsif @calling_format == CallingFormat::SUBDOMAIN
|
233
|
-
server = "#{bucket}.#{@server}"
|
234
|
-
elsif @calling_format == CallingFormat::VANITY
|
235
|
-
server = bucket
|
236
|
-
else
|
237
|
-
server = @server
|
238
|
-
end
|
239
|
-
|
240
|
-
# build the path based on the calling format
|
241
|
-
path = ''
|
242
|
-
if (not bucket.empty?) and (@calling_format == CallingFormat::REGULAR)
|
243
|
-
path << "/#{bucket}"
|
244
|
-
end
|
245
|
-
# add the slash after the bucket regardless
|
246
|
-
# the key will be appended if it is non-empty
|
247
|
-
path << "/#{key}"
|
248
|
-
|
249
|
-
# build the path_argument string
|
250
|
-
# add the ? in all cases since
|
251
|
-
# signature and credentials follow path args
|
252
|
-
path << '?'
|
253
|
-
path << S3.path_args_hash_to_string(path_args)
|
254
|
-
|
255
|
-
http = Net::HTTP.new(server, @port)
|
256
|
-
http.use_ssl = @is_secure
|
257
|
-
http.start do
|
258
|
-
req = method_to_request_class(method).new("#{path}")
|
259
|
-
|
260
|
-
set_headers(req, headers)
|
261
|
-
set_headers(req, metadata, METADATA_PREFIX)
|
262
|
-
|
263
|
-
set_aws_auth_header(req, @aws_access_key_id, @aws_secret_access_key, bucket, key, path_args)
|
264
|
-
if req.request_body_permitted?
|
265
|
-
return http.request(req, data)
|
266
|
-
else
|
267
|
-
return http.request(req)
|
268
|
-
end
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def method_to_request_class(method)
|
273
|
-
case method
|
274
|
-
when 'GET'
|
275
|
-
return Net::HTTP::Get
|
276
|
-
when 'PUT'
|
277
|
-
return Net::HTTP::Put
|
278
|
-
when 'DELETE'
|
279
|
-
return Net::HTTP::Delete
|
280
|
-
when 'HEAD'
|
281
|
-
return Net::HTTP::Head
|
282
|
-
else
|
283
|
-
raise "Unsupported method #{method}"
|
284
|
-
end
|
285
|
-
end
|
286
|
-
|
287
|
-
# set the Authorization header using AWS signed header authentication
|
288
|
-
def set_aws_auth_header(request, aws_access_key_id, aws_secret_access_key, bucket='', key='', path_args={})
|
289
|
-
# we want to fix the date here if it's not already been done.
|
290
|
-
request['Date'] ||= Time.now.httpdate
|
291
|
-
|
292
|
-
# ruby will automatically add a random content-type on some verbs, so
|
293
|
-
# here we add a dummy one to 'supress' it. change this logic if having
|
294
|
-
# an empty content-type header becomes semantically meaningful for any
|
295
|
-
# other verb.
|
296
|
-
request['Content-Type'] ||= ''
|
297
|
-
|
298
|
-
canonical_string =
|
299
|
-
S3.canonical_string(request.method, bucket, key, path_args, request.to_hash, nil)
|
300
|
-
encoded_canonical = S3.encode(aws_secret_access_key, canonical_string)
|
301
|
-
|
302
|
-
request['Authorization'] = "AWS #{aws_access_key_id}:#{encoded_canonical}"
|
303
|
-
end
|
304
|
-
|
305
|
-
def set_headers(request, headers, prefix='')
|
306
|
-
headers.each do |key, value|
|
307
|
-
request[prefix + key] = value
|
308
|
-
end
|
309
|
-
end
|
310
|
-
end
|
311
|
-
|
312
|
-
|
313
|
-
# This interface mirrors the AWSAuthConnection class above, but instead
|
314
|
-
# of performing the operations, this class simply returns a url that can
|
315
|
-
# be used to perform the operation with the query string authentication
|
316
|
-
# parameters set.
|
317
|
-
class QueryStringAuthGenerator
|
318
|
-
attr_accessor :calling_format
|
319
|
-
attr_accessor :expires
|
320
|
-
attr_accessor :expires_in
|
321
|
-
attr_reader :server
|
322
|
-
attr_reader :port
|
323
|
-
|
324
|
-
# by default, expire in 1 minute
|
325
|
-
DEFAULT_EXPIRES_IN = 60
|
326
|
-
|
327
|
-
def initialize(aws_access_key_id, aws_secret_access_key, is_secure=true,
|
328
|
-
server=DEFAULT_HOST, port=PORTS_BY_SECURITY[is_secure],
|
329
|
-
format=CallingFormat::REGULAR)
|
330
|
-
@aws_access_key_id = aws_access_key_id
|
331
|
-
@aws_secret_access_key = aws_secret_access_key
|
332
|
-
@protocol = is_secure ? 'https' : 'http'
|
333
|
-
@server = server
|
334
|
-
@port = port
|
335
|
-
@calling_format = format
|
336
|
-
# by default expire
|
337
|
-
@expires_in = DEFAULT_EXPIRES_IN
|
338
|
-
end
|
339
|
-
|
340
|
-
# set the expires value to be a fixed time. the argument can
|
341
|
-
# be either a Time object or else seconds since epoch.
|
342
|
-
def expires=(value)
|
343
|
-
@expires = value
|
344
|
-
@expires_in = nil
|
345
|
-
end
|
346
|
-
|
347
|
-
# set the expires value to expire at some point in the future
|
348
|
-
# relative to when the url is generated. value is in seconds.
|
349
|
-
def expires_in=(value)
|
350
|
-
@expires_in = value
|
351
|
-
@expires = nil
|
352
|
-
end
|
353
|
-
|
354
|
-
def create_bucket(bucket, headers={})
|
355
|
-
return generate_url('PUT', bucket, '', {}, headers)
|
356
|
-
end
|
357
|
-
|
358
|
-
# takes options :prefix, :marker, :max_keys, and :delimiter
|
359
|
-
def list_bucket(bucket, options={}, headers={})
|
360
|
-
path_args = {}
|
361
|
-
options.each { |k, v|
|
362
|
-
path_args[k] = v.to_s
|
363
|
-
}
|
364
|
-
return generate_url('GET', bucket, '', path_args, headers)
|
365
|
-
end
|
366
|
-
|
367
|
-
def delete_bucket(bucket, headers={})
|
368
|
-
return generate_url('DELETE', bucket, '', {}, headers)
|
369
|
-
end
|
370
|
-
|
371
|
-
# don't really care what object data is. it's just for conformance with the
|
372
|
-
# other interface. If this doesn't work, check tcpdump to see if the client is
|
373
|
-
# putting a Content-Type header on the wire.
|
374
|
-
def put(bucket, key, object=nil, headers={})
|
375
|
-
object = S3Object.new(object) if not object.instance_of? S3Object
|
376
|
-
return generate_url('PUT', bucket, CGI::escape(key), {}, merge_meta(headers, object))
|
377
|
-
end
|
378
|
-
|
379
|
-
def get(bucket, key, headers={})
|
380
|
-
return generate_url('GET', bucket, CGI::escape(key), {}, headers)
|
381
|
-
end
|
382
|
-
|
383
|
-
def delete(bucket, key, headers={})
|
384
|
-
return generate_url('DELETE', bucket, CGI::escape(key), {}, headers)
|
385
|
-
end
|
386
|
-
|
387
|
-
def get_bucket_logging(bucket, headers={})
|
388
|
-
return generate_url('GET', bucket, '', {'logging' => nil}, headers)
|
389
|
-
end
|
390
|
-
|
391
|
-
def put_bucket_logging(bucket, logging_xml_doc, headers={})
|
392
|
-
return generate_url('PUT', bucket, '', {'logging' => nil}, headers)
|
393
|
-
end
|
394
|
-
|
395
|
-
def get_acl(bucket, key='', headers={})
|
396
|
-
return generate_url('GET', bucket, CGI::escape(key), {'acl' => nil}, headers)
|
397
|
-
end
|
398
|
-
|
399
|
-
def get_bucket_acl(bucket, headers={})
|
400
|
-
return get_acl(bucket, '', headers)
|
401
|
-
end
|
402
|
-
|
403
|
-
# don't really care what acl_xml_doc is.
|
404
|
-
# again, check the wire for Content-Type if this fails.
|
405
|
-
def put_acl(bucket, key, acl_xml_doc, headers={})
|
406
|
-
return generate_url('PUT', bucket, CGI::escape(key), {'acl' => nil}, headers)
|
407
|
-
end
|
408
|
-
|
409
|
-
def put_bucket_acl(bucket, acl_xml_doc, headers={})
|
410
|
-
return put_acl(bucket, '', acl_xml_doc, headers)
|
411
|
-
end
|
412
|
-
|
413
|
-
def list_all_my_buckets(headers={})
|
414
|
-
return generate_url('GET', '', '', {}, headers)
|
415
|
-
end
|
416
|
-
|
417
|
-
|
418
|
-
private
|
419
|
-
# generate a url with the appropriate query string authentication
|
420
|
-
# parameters set.
|
421
|
-
def generate_url(method, bucket="", key="", path_args={}, headers={})
|
422
|
-
expires = 0
|
423
|
-
if not @expires_in.nil?
|
424
|
-
expires = Time.now.to_i + @expires_in
|
425
|
-
elsif not @expires.nil?
|
426
|
-
expires = @expires
|
427
|
-
else
|
428
|
-
raise "invalid expires state"
|
429
|
-
end
|
430
|
-
|
431
|
-
canonical_string =
|
432
|
-
S3::canonical_string(method, bucket, key, path_args, headers, expires)
|
433
|
-
encoded_canonical =
|
434
|
-
S3::encode(@aws_secret_access_key, canonical_string)
|
435
|
-
|
436
|
-
url = CallingFormat.build_url_base(@protocol, @server, @port, bucket, @calling_format)
|
437
|
-
|
438
|
-
path_args["Signature"] = encoded_canonical.to_s
|
439
|
-
path_args["Expires"] = expires.to_s
|
440
|
-
path_args["AWSAccessKeyId"] = @aws_access_key_id.to_s
|
441
|
-
arg_string = S3.path_args_hash_to_string(path_args)
|
442
|
-
|
443
|
-
return "#{url}/#{key}?#{arg_string}"
|
444
|
-
end
|
445
|
-
|
446
|
-
def merge_meta(headers, object)
|
447
|
-
final_headers = headers.clone
|
448
|
-
if not object.nil? and not object.metadata.nil?
|
449
|
-
object.metadata.each do |k, v|
|
450
|
-
final_headers[METADATA_PREFIX + k] = v
|
451
|
-
end
|
452
|
-
end
|
453
|
-
return final_headers
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
|
-
class S3Object
|
458
|
-
attr_accessor :data
|
459
|
-
attr_accessor :metadata
|
460
|
-
def initialize(data, metadata={})
|
461
|
-
@data, @metadata = data, metadata
|
462
|
-
end
|
463
|
-
end
|
464
|
-
|
465
|
-
# class for storing calling format constants
|
466
|
-
module CallingFormat
|
467
|
-
REGULAR = 0 # http://s3.amazonaws.com/bucket/key
|
468
|
-
SUBDOMAIN = 1 # http://bucket.s3.amazonaws.com/key
|
469
|
-
VANITY = 2 # http://<vanity_domain>/key -- vanity_domain resolves to s3.amazonaws.com
|
470
|
-
|
471
|
-
# build the url based on the calling format, and bucket
|
472
|
-
def CallingFormat.build_url_base(protocol, server, port, bucket, format)
|
473
|
-
build_url_base = "#{protocol}://"
|
474
|
-
if bucket.empty?
|
475
|
-
build_url_base << "#{server}:#{port}"
|
476
|
-
elsif format == SUBDOMAIN
|
477
|
-
build_url_base << "#{bucket}.#{server}:#{port}"
|
478
|
-
elsif format == VANITY
|
479
|
-
build_url_base << "#{bucket}:#{port}"
|
480
|
-
else
|
481
|
-
build_url_base << "#{server}:#{port}/#{bucket}"
|
482
|
-
end
|
483
|
-
return build_url_base
|
484
|
-
end
|
485
|
-
end
|
486
|
-
|
487
|
-
class Owner
|
488
|
-
attr_accessor :id
|
489
|
-
attr_accessor :display_name
|
490
|
-
end
|
491
|
-
|
492
|
-
class ListEntry
|
493
|
-
attr_accessor :key
|
494
|
-
attr_accessor :last_modified
|
495
|
-
attr_accessor :etag
|
496
|
-
attr_accessor :size
|
497
|
-
attr_accessor :storage_class
|
498
|
-
attr_accessor :owner
|
499
|
-
end
|
500
|
-
|
501
|
-
class ListProperties
|
502
|
-
attr_accessor :name
|
503
|
-
attr_accessor :prefix
|
504
|
-
attr_accessor :marker
|
505
|
-
attr_accessor :max_keys
|
506
|
-
attr_accessor :delimiter
|
507
|
-
attr_accessor :is_truncated
|
508
|
-
attr_accessor :next_marker
|
509
|
-
end
|
510
|
-
|
511
|
-
class CommonPrefixEntry
|
512
|
-
attr_accessor :prefix
|
513
|
-
end
|
514
|
-
|
515
|
-
# Parses the list bucket output into a list of ListEntry objects, and
|
516
|
-
# a list of CommonPrefixEntry objects if applicable.
|
517
|
-
class ListBucketParser
|
518
|
-
attr_reader :properties
|
519
|
-
attr_reader :entries
|
520
|
-
attr_reader :common_prefixes
|
521
|
-
|
522
|
-
def initialize
|
523
|
-
reset
|
524
|
-
end
|
525
|
-
|
526
|
-
def tag_start(name, attributes)
|
527
|
-
if name == 'ListBucketResult'
|
528
|
-
@properties = ListProperties.new
|
529
|
-
elsif name == 'Contents'
|
530
|
-
@curr_entry = ListEntry.new
|
531
|
-
elsif name == 'Owner'
|
532
|
-
@curr_entry.owner = Owner.new
|
533
|
-
elsif name == 'CommonPrefixes'
|
534
|
-
@common_prefix_entry = CommonPrefixEntry.new
|
535
|
-
end
|
536
|
-
end
|
537
|
-
|
538
|
-
# we have one, add him to the entries list
|
539
|
-
def tag_end(name)
|
540
|
-
# this prefix is the one we echo back from the request
|
541
|
-
if name == 'Name'
|
542
|
-
@properties.name = @curr_text
|
543
|
-
elsif name == 'Prefix' and @is_echoed_prefix
|
544
|
-
@properties.prefix = @curr_text
|
545
|
-
@is_echoed_prefix = nil
|
546
|
-
elsif name == 'Marker'
|
547
|
-
@properties.marker = @curr_text
|
548
|
-
elsif name == 'MaxKeys'
|
549
|
-
@properties.max_keys = @curr_text.to_i
|
550
|
-
elsif name == 'Delimiter'
|
551
|
-
@properties.delimiter = @curr_text
|
552
|
-
elsif name == 'IsTruncated'
|
553
|
-
@properties.is_truncated = @curr_text == 'true'
|
554
|
-
elsif name == 'NextMarker'
|
555
|
-
@properties.next_marker = @curr_text
|
556
|
-
elsif name == 'Contents'
|
557
|
-
@entries << @curr_entry
|
558
|
-
elsif name == 'Key'
|
559
|
-
@curr_entry.key = @curr_text
|
560
|
-
elsif name == 'LastModified'
|
561
|
-
@curr_entry.last_modified = @curr_text
|
562
|
-
elsif name == 'ETag'
|
563
|
-
@curr_entry.etag = @curr_text
|
564
|
-
elsif name == 'Size'
|
565
|
-
@curr_entry.size = @curr_text.to_i
|
566
|
-
elsif name == 'StorageClass'
|
567
|
-
@curr_entry.storage_class = @curr_text
|
568
|
-
elsif name == 'ID'
|
569
|
-
@curr_entry.owner.id = @curr_text
|
570
|
-
elsif name == 'DisplayName'
|
571
|
-
@curr_entry.owner.display_name = @curr_text
|
572
|
-
elsif name == 'CommonPrefixes'
|
573
|
-
@common_prefixes << @common_prefix_entry
|
574
|
-
elsif name == 'Prefix'
|
575
|
-
# this is the common prefix for keys that match up to the delimiter
|
576
|
-
@common_prefix_entry.prefix = @curr_text
|
577
|
-
end
|
578
|
-
@curr_text = ''
|
579
|
-
end
|
580
|
-
|
581
|
-
def text(text)
|
582
|
-
@curr_text += text
|
583
|
-
end
|
584
|
-
|
585
|
-
def xmldecl(version, encoding, standalone)
|
586
|
-
# ignore
|
587
|
-
end
|
588
|
-
|
589
|
-
# get ready for another parse
|
590
|
-
def reset
|
591
|
-
@is_echoed_prefix = true;
|
592
|
-
@entries = []
|
593
|
-
@curr_entry = nil
|
594
|
-
@common_prefixes = []
|
595
|
-
@common_prefix_entry = nil
|
596
|
-
@curr_text = ''
|
597
|
-
end
|
598
|
-
end
|
599
|
-
|
600
|
-
class Bucket
|
601
|
-
attr_accessor :name
|
602
|
-
attr_accessor :creation_date
|
603
|
-
end
|
604
|
-
|
605
|
-
class ListAllMyBucketsParser
|
606
|
-
attr_reader :entries
|
607
|
-
|
608
|
-
def initialize
|
609
|
-
reset
|
610
|
-
end
|
611
|
-
|
612
|
-
def tag_start(name, attributes)
|
613
|
-
if name == 'Bucket'
|
614
|
-
@curr_bucket = Bucket.new
|
615
|
-
end
|
616
|
-
end
|
617
|
-
|
618
|
-
# we have one, add him to the entries list
|
619
|
-
def tag_end(name)
|
620
|
-
if name == 'Bucket'
|
621
|
-
@entries << @curr_bucket
|
622
|
-
elsif name == 'Name'
|
623
|
-
@curr_bucket.name = @curr_text
|
624
|
-
elsif name == 'CreationDate'
|
625
|
-
@curr_bucket.creation_date = @curr_text
|
626
|
-
end
|
627
|
-
@curr_text = ''
|
628
|
-
end
|
629
|
-
|
630
|
-
def text(text)
|
631
|
-
@curr_text += text
|
632
|
-
end
|
633
|
-
|
634
|
-
def xmldecl(version, encoding, standalone)
|
635
|
-
# ignore
|
636
|
-
end
|
637
|
-
|
638
|
-
# get ready for another parse
|
639
|
-
def reset
|
640
|
-
@entries = []
|
641
|
-
@owner = nil
|
642
|
-
@curr_bucket = nil
|
643
|
-
@curr_text = ''
|
644
|
-
end
|
645
|
-
end
|
646
|
-
|
647
|
-
class Response
|
648
|
-
attr_reader :http_response
|
649
|
-
def initialize(response)
|
650
|
-
@http_response = response
|
651
|
-
end
|
652
|
-
end
|
653
|
-
|
654
|
-
class GetResponse < Response
|
655
|
-
attr_reader :object
|
656
|
-
def initialize(response)
|
657
|
-
super(response)
|
658
|
-
metadata = get_aws_metadata(response)
|
659
|
-
data = response.body
|
660
|
-
@object = S3Object.new(data, metadata)
|
661
|
-
end
|
662
|
-
|
663
|
-
# parses the request headers and pulls out the s3 metadata into a hash
|
664
|
-
def get_aws_metadata(response)
|
665
|
-
metadata = {}
|
666
|
-
response.each do |key, value|
|
667
|
-
if key =~ /^#{METADATA_PREFIX}(.*)$/oi
|
668
|
-
metadata[$1] = value
|
669
|
-
end
|
670
|
-
end
|
671
|
-
return metadata
|
672
|
-
end
|
673
|
-
end
|
674
|
-
|
675
|
-
class ListBucketResponse < Response
|
676
|
-
attr_reader :properties
|
677
|
-
attr_reader :entries
|
678
|
-
attr_reader :common_prefix_entries
|
679
|
-
|
680
|
-
def initialize(response)
|
681
|
-
super(response)
|
682
|
-
if response.is_a? Net::HTTPSuccess
|
683
|
-
parser = ListBucketParser.new
|
684
|
-
REXML::Document.parse_stream(response.body, parser)
|
685
|
-
@properties = parser.properties
|
686
|
-
@entries = parser.entries
|
687
|
-
@common_prefix_entries = parser.common_prefixes
|
688
|
-
else
|
689
|
-
@entries = []
|
690
|
-
end
|
691
|
-
end
|
692
|
-
end
|
693
|
-
|
694
|
-
class ListAllMyBucketsResponse < Response
|
695
|
-
attr_reader :entries
|
696
|
-
def initialize(response)
|
697
|
-
super(response)
|
698
|
-
if response.is_a? Net::HTTPSuccess
|
699
|
-
parser = ListAllMyBucketsParser.new
|
700
|
-
REXML::Document.parse_stream(response.body, parser)
|
701
|
-
@entries = parser.entries
|
702
|
-
else
|
703
|
-
@entries = []
|
704
|
-
end
|
705
|
-
end
|
706
|
-
end
|
707
|
-
end
|