s3sync 0.3.4 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,103 @@
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
@@ -0,0 +1,707 @@
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