cprobert-s3sync 1.3.6

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/lib/s3sync/S3.rb ADDED
@@ -0,0 +1,714 @@
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.join.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=nil, headers={})
169
+ if object == nil
170
+ req = make_request('PUT', bucket, CGI::escape(key), {}, headers)
171
+ else
172
+ if not object.instance_of? S3Object
173
+ object = S3Object.new(object)
174
+ end
175
+ req = make_request('PUT', bucket, CGI::escape(key), {}, headers, object.data, object.metadata)
176
+ end
177
+
178
+ return Response.new(req)
179
+ end
180
+
181
+ def get(bucket, key, headers={})
182
+ return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {}, headers))
183
+ end
184
+
185
+ def delete(bucket, key, headers={})
186
+ return Response.new(make_request('DELETE', bucket, CGI::escape(key), {}, headers))
187
+ end
188
+
189
+ def head(bucket, key, headers={})
190
+ return GetResponse.new(make_request('HEAD', bucket, CGI::escape(key), {}, headers))
191
+ end
192
+
193
+ def get_bucket_logging(bucket, headers={})
194
+ return GetResponse.new(make_request('GET', bucket, '', {'logging' => nil}, headers))
195
+ end
196
+
197
+ def put_bucket_logging(bucket, logging_xml_doc, headers={})
198
+ return Response.new(make_request('PUT', bucket, '', {'logging' => nil}, headers, logging_xml_doc))
199
+ end
200
+
201
+ def get_bucket_acl(bucket, headers={})
202
+ return get_acl(bucket, '', headers)
203
+ end
204
+
205
+ # returns an xml document representing the access control list.
206
+ # this could be parsed into an object.
207
+ def get_acl(bucket, key, headers={})
208
+ return GetResponse.new(make_request('GET', bucket, CGI::escape(key), {'acl' => nil}, headers))
209
+ end
210
+
211
+ def put_bucket_acl(bucket, acl_xml_doc, headers={})
212
+ return put_acl(bucket, '', acl_xml_doc, headers)
213
+ end
214
+
215
+ # sets the access control policy for the given resource. acl_xml_doc must
216
+ # be a string in the acl xml format.
217
+ def put_acl(bucket, key, acl_xml_doc, headers={})
218
+ return Response.new(
219
+ make_request('PUT', bucket, CGI::escape(key), {'acl' => nil}, headers, acl_xml_doc, {})
220
+ )
221
+ end
222
+
223
+ def list_all_my_buckets(headers={})
224
+ return ListAllMyBucketsResponse.new(make_request('GET', '', '', {}, headers))
225
+ end
226
+
227
+ private
228
+ def make_request(method, bucket='', key='', path_args={}, headers={}, data='', metadata={})
229
+
230
+ # build the domain based on the calling format
231
+ server = ''
232
+ if bucket.empty?
233
+ # for a bucketless request (i.e. list all buckets)
234
+ # revert to regular domain case since this operation
235
+ # does not make sense for vanity domains
236
+ server = @server
237
+ elsif @calling_format == CallingFormat::SUBDOMAIN
238
+ server = "#{bucket}.#{@server}"
239
+ elsif @calling_format == CallingFormat::VANITY
240
+ server = bucket
241
+ else
242
+ server = @server
243
+ end
244
+
245
+ # build the path based on the calling format
246
+ path = ''
247
+ if (not bucket.empty?) and (@calling_format == CallingFormat::REGULAR)
248
+ path << "/#{bucket}"
249
+ end
250
+ # add the slash after the bucket regardless
251
+ # the key will be appended if it is non-empty
252
+ path << "/#{key}"
253
+
254
+ # build the path_argument string
255
+ # add the ? in all cases since
256
+ # signature and credentials follow path args
257
+ path << '?'
258
+ path << S3.path_args_hash_to_string(path_args)
259
+
260
+ http = Net::HTTP.new(server, @port)
261
+ http.use_ssl = @is_secure
262
+ http.start do
263
+ req = method_to_request_class(method).new("#{path}")
264
+
265
+ set_headers(req, headers)
266
+ set_headers(req, metadata, METADATA_PREFIX)
267
+
268
+ set_aws_auth_header(req, @aws_access_key_id, @aws_secret_access_key, bucket, key, path_args)
269
+ if req.request_body_permitted?
270
+ return http.request(req, data)
271
+ else
272
+ return http.request(req)
273
+ end
274
+ end
275
+ end
276
+
277
+ def method_to_request_class(method)
278
+ case method
279
+ when 'GET'
280
+ return Net::HTTP::Get
281
+ when 'PUT'
282
+ return Net::HTTP::Put
283
+ when 'DELETE'
284
+ return Net::HTTP::Delete
285
+ when 'HEAD'
286
+ return Net::HTTP::Head
287
+ else
288
+ raise "Unsupported method #{method}"
289
+ end
290
+ end
291
+
292
+ # set the Authorization header using AWS signed header authentication
293
+ def set_aws_auth_header(request, aws_access_key_id, aws_secret_access_key, bucket='', key='', path_args={})
294
+ # we want to fix the date here if it's not already been done.
295
+ request['Date'] ||= Time.now.httpdate
296
+
297
+ # ruby will automatically add a random content-type on some verbs, so
298
+ # here we add a dummy one to 'supress' it. change this logic if having
299
+ # an empty content-type header becomes semantically meaningful for any
300
+ # other verb.
301
+ request['Content-Type'] ||= ''
302
+
303
+ canonical_string =
304
+ S3.canonical_string(request.method, bucket, key, path_args, request.to_hash, nil)
305
+ encoded_canonical = S3.encode(aws_secret_access_key, canonical_string)
306
+
307
+ request['Authorization'] = "AWS #{aws_access_key_id}:#{encoded_canonical}"
308
+ end
309
+
310
+ def set_headers(request, headers, prefix='')
311
+ headers.each do |key, value|
312
+ request[prefix + key] = value
313
+ end
314
+ end
315
+ end
316
+
317
+
318
+ # This interface mirrors the AWSAuthConnection class above, but instead
319
+ # of performing the operations, this class simply returns a url that can
320
+ # be used to perform the operation with the query string authentication
321
+ # parameters set.
322
+ class QueryStringAuthGenerator
323
+ attr_accessor :calling_format
324
+ attr_accessor :expires
325
+ attr_accessor :expires_in
326
+ attr_reader :server
327
+ attr_reader :port
328
+
329
+ # by default, expire in 1 minute
330
+ DEFAULT_EXPIRES_IN = 60
331
+
332
+ def initialize(aws_access_key_id, aws_secret_access_key, is_secure=true,
333
+ server=DEFAULT_HOST, port=PORTS_BY_SECURITY[is_secure],
334
+ format=CallingFormat::REGULAR)
335
+ @aws_access_key_id = aws_access_key_id
336
+ @aws_secret_access_key = aws_secret_access_key
337
+ @protocol = is_secure ? 'https' : 'http'
338
+ @server = server
339
+ @port = port
340
+ @calling_format = format
341
+ # by default expire
342
+ @expires_in = DEFAULT_EXPIRES_IN
343
+ end
344
+
345
+ # set the expires value to be a fixed time. the argument can
346
+ # be either a Time object or else seconds since epoch.
347
+ def expires=(value)
348
+ @expires = value
349
+ @expires_in = nil
350
+ end
351
+
352
+ # set the expires value to expire at some point in the future
353
+ # relative to when the url is generated. value is in seconds.
354
+ def expires_in=(value)
355
+ @expires_in = value
356
+ @expires = nil
357
+ end
358
+
359
+ def create_bucket(bucket, headers={})
360
+ return generate_url('PUT', bucket, '', {}, headers)
361
+ end
362
+
363
+ # takes options :prefix, :marker, :max_keys, and :delimiter
364
+ def list_bucket(bucket, options={}, headers={})
365
+ path_args = {}
366
+ options.each { |k, v|
367
+ path_args[k] = v.to_s
368
+ }
369
+ return generate_url('GET', bucket, '', path_args, headers)
370
+ end
371
+
372
+ def delete_bucket(bucket, headers={})
373
+ return generate_url('DELETE', bucket, '', {}, headers)
374
+ end
375
+
376
+ # don't really care what object data is. it's just for conformance with the
377
+ # other interface. If this doesn't work, check tcpdump to see if the client is
378
+ # putting a Content-Type header on the wire.
379
+ def put(bucket, key, object=nil, headers={})
380
+ object = S3Object.new(object) if not object.instance_of? S3Object
381
+ return generate_url('PUT', bucket, CGI::escape(key), {}, merge_meta(headers, object))
382
+ end
383
+
384
+ def get(bucket, key, headers={})
385
+ return generate_url('GET', bucket, CGI::escape(key), {}, headers)
386
+ end
387
+
388
+ def delete(bucket, key, headers={})
389
+ return generate_url('DELETE', bucket, CGI::escape(key), {}, headers)
390
+ end
391
+
392
+ def get_bucket_logging(bucket, headers={})
393
+ return generate_url('GET', bucket, '', {'logging' => nil}, headers)
394
+ end
395
+
396
+ def put_bucket_logging(bucket, logging_xml_doc, headers={})
397
+ return generate_url('PUT', bucket, '', {'logging' => nil}, headers)
398
+ end
399
+
400
+ def get_acl(bucket, key='', headers={})
401
+ return generate_url('GET', bucket, CGI::escape(key), {'acl' => nil}, headers)
402
+ end
403
+
404
+ def get_bucket_acl(bucket, headers={})
405
+ return get_acl(bucket, '', headers)
406
+ end
407
+
408
+ # don't really care what acl_xml_doc is.
409
+ # again, check the wire for Content-Type if this fails.
410
+ def put_acl(bucket, key, acl_xml_doc, headers={})
411
+ return generate_url('PUT', bucket, CGI::escape(key), {'acl' => nil}, headers)
412
+ end
413
+
414
+ def put_bucket_acl(bucket, acl_xml_doc, headers={})
415
+ return put_acl(bucket, '', acl_xml_doc, headers)
416
+ end
417
+
418
+ def list_all_my_buckets(headers={})
419
+ return generate_url('GET', '', '', {}, headers)
420
+ end
421
+
422
+
423
+ private
424
+ # generate a url with the appropriate query string authentication
425
+ # parameters set.
426
+ def generate_url(method, bucket="", key="", path_args={}, headers={})
427
+ expires = 0
428
+ if not @expires_in.nil?
429
+ expires = Time.now.to_i + @expires_in
430
+ elsif not @expires.nil?
431
+ expires = @expires
432
+ else
433
+ raise "invalid expires state"
434
+ end
435
+
436
+ canonical_string =
437
+ S3::canonical_string(method, bucket, key, path_args, headers, expires)
438
+ encoded_canonical =
439
+ S3::encode(@aws_secret_access_key, canonical_string)
440
+
441
+ url = CallingFormat.build_url_base(@protocol, @server, @port, bucket, @calling_format)
442
+
443
+ path_args["Signature"] = encoded_canonical.to_s
444
+ path_args["Expires"] = expires.to_s
445
+ path_args["AWSAccessKeyId"] = @aws_access_key_id.to_s
446
+ arg_string = S3.path_args_hash_to_string(path_args)
447
+
448
+ return "#{url}/#{key}?#{arg_string}"
449
+ end
450
+
451
+ def merge_meta(headers, object)
452
+ final_headers = headers.clone
453
+ if not object.nil? and not object.metadata.nil?
454
+ object.metadata.each do |k, v|
455
+ final_headers[METADATA_PREFIX + k] = v
456
+ end
457
+ end
458
+ return final_headers
459
+ end
460
+ end
461
+
462
+ class S3Object
463
+ attr_accessor :data
464
+ attr_accessor :metadata
465
+ def initialize(data, metadata={})
466
+ @data, @metadata = data, metadata
467
+ end
468
+ end
469
+
470
+ # class for storing calling format constants
471
+ module CallingFormat
472
+ REGULAR = 0 # http://s3.amazonaws.com/bucket/key
473
+ SUBDOMAIN = 1 # http://bucket.s3.amazonaws.com/key
474
+ VANITY = 2 # http://<vanity_domain>/key -- vanity_domain resolves to s3.amazonaws.com
475
+
476
+ # build the url based on the calling format, and bucket
477
+ def CallingFormat.build_url_base(protocol, server, port, bucket, format)
478
+ build_url_base = "#{protocol}://"
479
+ if bucket.empty?
480
+ build_url_base << "#{server}:#{port}"
481
+ elsif format == SUBDOMAIN
482
+ build_url_base << "#{bucket}.#{server}:#{port}"
483
+ elsif format == VANITY
484
+ build_url_base << "#{bucket}:#{port}"
485
+ else
486
+ build_url_base << "#{server}:#{port}/#{bucket}"
487
+ end
488
+ return build_url_base
489
+ end
490
+ end
491
+
492
+ class Owner
493
+ attr_accessor :id
494
+ attr_accessor :display_name
495
+ end
496
+
497
+ class ListEntry
498
+ attr_accessor :key
499
+ attr_accessor :last_modified
500
+ attr_accessor :etag
501
+ attr_accessor :size
502
+ attr_accessor :storage_class
503
+ attr_accessor :owner
504
+ end
505
+
506
+ class ListProperties
507
+ attr_accessor :name
508
+ attr_accessor :prefix
509
+ attr_accessor :marker
510
+ attr_accessor :max_keys
511
+ attr_accessor :delimiter
512
+ attr_accessor :is_truncated
513
+ attr_accessor :next_marker
514
+ end
515
+
516
+ class CommonPrefixEntry
517
+ attr_accessor :prefix
518
+ end
519
+
520
+ # Parses the list bucket output into a list of ListEntry objects, and
521
+ # a list of CommonPrefixEntry objects if applicable.
522
+ class ListBucketParser
523
+ attr_reader :properties
524
+ attr_reader :entries
525
+ attr_reader :common_prefixes
526
+
527
+ def initialize
528
+ reset
529
+ end
530
+
531
+ def tag_start(name, attributes)
532
+ if name == 'ListBucketResult'
533
+ @properties = ListProperties.new
534
+ elsif name == 'Contents'
535
+ @curr_entry = ListEntry.new
536
+ elsif name == 'Owner'
537
+ @curr_entry.owner = Owner.new
538
+ elsif name == 'CommonPrefixes'
539
+ @common_prefix_entry = CommonPrefixEntry.new
540
+ end
541
+ end
542
+
543
+ # we have one, add him to the entries list
544
+ def tag_end(name)
545
+ # this prefix is the one we echo back from the request
546
+ if name == 'Name'
547
+ @properties.name = @curr_text
548
+ elsif name == 'Prefix' and @is_echoed_prefix
549
+ @properties.prefix = @curr_text
550
+ @is_echoed_prefix = nil
551
+ elsif name == 'Marker'
552
+ @properties.marker = @curr_text
553
+ elsif name == 'MaxKeys'
554
+ @properties.max_keys = @curr_text.to_i
555
+ elsif name == 'Delimiter'
556
+ @properties.delimiter = @curr_text
557
+ elsif name == 'IsTruncated'
558
+ @properties.is_truncated = @curr_text == 'true'
559
+ elsif name == 'NextMarker'
560
+ @properties.next_marker = @curr_text
561
+ elsif name == 'Contents'
562
+ @entries << @curr_entry
563
+ elsif name == 'Key'
564
+ @curr_entry.key = @curr_text
565
+ elsif name == 'LastModified'
566
+ @curr_entry.last_modified = @curr_text
567
+ elsif name == 'ETag'
568
+ @curr_entry.etag = @curr_text
569
+ elsif name == 'Size'
570
+ @curr_entry.size = @curr_text.to_i
571
+ elsif name == 'StorageClass'
572
+ @curr_entry.storage_class = @curr_text
573
+ elsif name == 'ID'
574
+ @curr_entry.owner.id = @curr_text
575
+ elsif name == 'DisplayName'
576
+ @curr_entry.owner.display_name = @curr_text
577
+ elsif name == 'CommonPrefixes'
578
+ @common_prefixes << @common_prefix_entry
579
+ elsif name == 'Prefix'
580
+ # this is the common prefix for keys that match up to the delimiter
581
+ @common_prefix_entry.prefix = @curr_text
582
+ end
583
+ @curr_text = ''
584
+ end
585
+
586
+ def text(text)
587
+ @curr_text += text
588
+ end
589
+
590
+ def xmldecl(version, encoding, standalone)
591
+ # ignore
592
+ end
593
+
594
+ # get ready for another parse
595
+ def reset
596
+ @is_echoed_prefix = true;
597
+ @entries = []
598
+ @curr_entry = nil
599
+ @common_prefixes = []
600
+ @common_prefix_entry = nil
601
+ @curr_text = ''
602
+ end
603
+ end
604
+
605
+ class Bucket
606
+ attr_accessor :name
607
+ attr_accessor :creation_date
608
+ end
609
+
610
+ class ListAllMyBucketsParser
611
+ attr_reader :entries
612
+
613
+ def initialize
614
+ reset
615
+ end
616
+
617
+ def tag_start(name, attributes)
618
+ if name == 'Bucket'
619
+ @curr_bucket = Bucket.new
620
+ end
621
+ end
622
+
623
+ # we have one, add him to the entries list
624
+ def tag_end(name)
625
+ if name == 'Bucket'
626
+ @entries << @curr_bucket
627
+ elsif name == 'Name'
628
+ @curr_bucket.name = @curr_text
629
+ elsif name == 'CreationDate'
630
+ @curr_bucket.creation_date = @curr_text
631
+ end
632
+ @curr_text = ''
633
+ end
634
+
635
+ def text(text)
636
+ @curr_text += text
637
+ end
638
+
639
+ def xmldecl(version, encoding, standalone)
640
+ # ignore
641
+ end
642
+
643
+ # get ready for another parse
644
+ def reset
645
+ @entries = []
646
+ @owner = nil
647
+ @curr_bucket = nil
648
+ @curr_text = ''
649
+ end
650
+ end
651
+
652
+ class Response
653
+ attr_reader :http_response
654
+ def initialize(response)
655
+ @http_response = response
656
+ end
657
+ end
658
+
659
+ class GetResponse < Response
660
+ attr_reader :object
661
+ def initialize(response)
662
+ super(response)
663
+ metadata = get_aws_metadata(response)
664
+ data = response.body
665
+ @object = S3Object.new(data, metadata)
666
+ end
667
+
668
+ # parses the request headers and pulls out the s3 metadata into a hash
669
+ def get_aws_metadata(response)
670
+ metadata = {}
671
+ response.each do |key, value|
672
+ if key =~ /^#{METADATA_PREFIX}(.*)$/oi
673
+ metadata[$1] = value
674
+ else
675
+ metadata[key] = value
676
+ end
677
+ end
678
+ return metadata
679
+ end
680
+ end
681
+
682
+ class ListBucketResponse < Response
683
+ attr_reader :properties
684
+ attr_reader :entries
685
+ attr_reader :common_prefix_entries
686
+
687
+ def initialize(response)
688
+ super(response)
689
+ if response.is_a? Net::HTTPSuccess
690
+ parser = ListBucketParser.new
691
+ REXML::Document.parse_stream(response.body, parser)
692
+ @properties = parser.properties
693
+ @entries = parser.entries
694
+ @common_prefix_entries = parser.common_prefixes
695
+ else
696
+ @entries = []
697
+ end
698
+ end
699
+ end
700
+
701
+ class ListAllMyBucketsResponse < Response
702
+ attr_reader :entries
703
+ def initialize(response)
704
+ super(response)
705
+ if response.is_a? Net::HTTPSuccess
706
+ parser = ListAllMyBucketsParser.new
707
+ REXML::Document.parse_stream(response.body, parser)
708
+ @entries = parser.entries
709
+ else
710
+ @entries = []
711
+ end
712
+ end
713
+ end
714
+ end