right_aws 1.9.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/History.txt +164 -13
  2. data/Manifest.txt +28 -1
  3. data/README.txt +12 -10
  4. data/Rakefile +56 -29
  5. data/lib/acf/right_acf_interface.rb +343 -172
  6. data/lib/acf/right_acf_invalidations.rb +144 -0
  7. data/lib/acf/right_acf_origin_access_identities.rb +230 -0
  8. data/lib/acf/right_acf_streaming_interface.rb +229 -0
  9. data/lib/acw/right_acw_interface.rb +248 -0
  10. data/lib/as/right_as_interface.rb +698 -0
  11. data/lib/awsbase/right_awsbase.rb +755 -115
  12. data/lib/awsbase/support.rb +2 -78
  13. data/lib/awsbase/version.rb +9 -0
  14. data/lib/ec2/right_ec2.rb +274 -1294
  15. data/lib/ec2/right_ec2_ebs.rb +514 -0
  16. data/lib/ec2/right_ec2_images.rb +444 -0
  17. data/lib/ec2/right_ec2_instances.rb +797 -0
  18. data/lib/ec2/right_ec2_monitoring.rb +70 -0
  19. data/lib/ec2/right_ec2_placement_groups.rb +108 -0
  20. data/lib/ec2/right_ec2_reserved_instances.rb +243 -0
  21. data/lib/ec2/right_ec2_security_groups.rb +496 -0
  22. data/lib/ec2/right_ec2_spot_instances.rb +422 -0
  23. data/lib/ec2/right_ec2_tags.rb +139 -0
  24. data/lib/ec2/right_ec2_vpc.rb +598 -0
  25. data/lib/ec2/right_ec2_vpc2.rb +382 -0
  26. data/lib/ec2/right_ec2_windows_mobility.rb +84 -0
  27. data/lib/elb/right_elb_interface.rb +573 -0
  28. data/lib/emr/right_emr_interface.rb +728 -0
  29. data/lib/iam/right_iam_access_keys.rb +71 -0
  30. data/lib/iam/right_iam_groups.rb +195 -0
  31. data/lib/iam/right_iam_interface.rb +341 -0
  32. data/lib/iam/right_iam_mfa_devices.rb +67 -0
  33. data/lib/iam/right_iam_users.rb +251 -0
  34. data/lib/rds/right_rds_interface.rb +1657 -0
  35. data/lib/right_aws.rb +30 -13
  36. data/lib/route_53/right_route_53_interface.rb +641 -0
  37. data/lib/s3/right_s3.rb +108 -41
  38. data/lib/s3/right_s3_interface.rb +349 -118
  39. data/lib/sdb/active_sdb.rb +388 -54
  40. data/lib/sdb/right_sdb_interface.rb +323 -64
  41. data/lib/sns/right_sns_interface.rb +286 -0
  42. data/lib/sqs/right_sqs.rb +1 -2
  43. data/lib/sqs/right_sqs_gen2.rb +73 -17
  44. data/lib/sqs/right_sqs_gen2_interface.rb +146 -73
  45. data/lib/sqs/right_sqs_interface.rb +12 -22
  46. data/right_aws.gemspec +91 -0
  47. data/test/README.mdown +39 -0
  48. data/test/acf/test_right_acf.rb +11 -19
  49. data/test/awsbase/test_helper.rb +2 -0
  50. data/test/awsbase/test_right_awsbase.rb +11 -0
  51. data/test/ec2/test_right_ec2.rb +32 -1
  52. data/test/elb/test_helper.rb +2 -0
  53. data/test/elb/test_right_elb.rb +43 -0
  54. data/test/rds/test_helper.rb +2 -0
  55. data/test/rds/test_right_rds.rb +120 -0
  56. data/test/route_53/fixtures/a_record.xml +18 -0
  57. data/test/route_53/fixtures/alias_record.xml +18 -0
  58. data/test/route_53/test_helper.rb +2 -0
  59. data/test/route_53/test_right_route_53.rb +141 -0
  60. data/test/s3/test_right_s3.rb +176 -42
  61. data/test/s3/test_right_s3_stubbed.rb +6 -4
  62. data/test/sdb/test_active_sdb.rb +120 -19
  63. data/test/sdb/test_batch_put_attributes.rb +54 -0
  64. data/test/sdb/test_right_sdb.rb +71 -16
  65. data/test/sns/test_helper.rb +2 -0
  66. data/test/sns/test_right_sns.rb +153 -0
  67. data/test/sqs/test_right_sqs.rb +0 -6
  68. data/test/sqs/test_right_sqs_gen2.rb +104 -49
  69. data/test/ts_right_aws.rb +1 -0
  70. metadata +181 -22
data/lib/s3/right_s3.rb CHANGED
@@ -59,7 +59,6 @@ module RightAws
59
59
  # {:server => 's3.amazonaws.com' # Amazon service host: 's3.amazonaws.com'(default)
60
60
  # :port => 443 # Amazon service port: 80 or 443(default)
61
61
  # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
62
- # :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
63
62
  # :logger => Logger Object} # Logger instance: logs to STDOUT if omitted }
64
63
  def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
65
64
  @interface = S3Interface.new(aws_access_key_id, aws_secret_access_key, params)
@@ -98,10 +97,24 @@ module RightAws
98
97
  # (section: Canned Access Policies)
99
98
  #
100
99
  def bucket(name, create=false, perms=nil, headers={})
101
- headers['x-amz-acl'] = perms if perms
102
- @interface.create_bucket(name, headers) if create
103
- buckets.each { |bucket| return bucket if bucket.name == name }
104
- nil
100
+ result = nil
101
+ if create
102
+ headers['x-amz-acl'] = perms if perms
103
+ @interface.create_bucket(name, headers)
104
+ end
105
+ begin
106
+ buckets.each do |bucket|
107
+ if bucket.name == name
108
+ result = bucket
109
+ break
110
+ end
111
+ end
112
+ rescue RightAws::AwsError => e
113
+ # With non root creds one can use bucket(s) but can't list them
114
+ raise e unless e.message['AccessDenied']
115
+ result = Bucket::new(self, name)
116
+ end
117
+ result
105
118
  end
106
119
 
107
120
 
@@ -217,25 +230,23 @@ module RightAws
217
230
  #
218
231
  # keys, service = bucket.keys_and_service({'max-keys'=> 2, 'prefix' => 'logs'})
219
232
  # p keys #=> # 2 keys array
220
- # p service #=> {"max-keys"=>"2", "prefix"=>"logs", "name"=>"my_awesome_bucket", "marker"=>"", "is_truncated"=>true}
233
+ # p service #=> {"max-keys"=>"2", "prefix"=>"logs", "name"=>"my_awesome_bucket", "marker"=>"", "is_truncated"=>true, :common_prefixes=>[]}
221
234
  #
222
235
  def keys_and_service(options={}, head=false)
223
236
  opt = {}; options.each{ |key, value| opt[key.to_s] = value }
224
- service_data = {}
225
- thislist = {}
226
- list = []
227
- @s3.interface.incrementally_list_bucket(@name, opt) do |thislist|
228
- thislist[:contents].each do |entry|
237
+ service = {}
238
+ keys = []
239
+ @s3.interface.incrementally_list_bucket(@name, opt) do |_service|
240
+ service = _service
241
+ service[:contents].each do |entry|
229
242
  owner = Owner.new(entry[:owner_id], entry[:owner_display_name])
230
243
  key = Key.new(self, entry[:key], nil, {}, {}, entry[:last_modified], entry[:e_tag], entry[:size], entry[:storage_class], owner)
231
244
  key.head if head
232
- list << key
245
+ keys << key
233
246
  end
234
247
  end
235
- thislist.each_key do |key|
236
- service_data[key] = thislist[key] unless (key == :contents || key == :common_prefixes)
237
- end
238
- [list, service_data]
248
+ service.delete(:contents)
249
+ [keys, service]
239
250
  end
240
251
 
241
252
  # Retrieve key information from Amazon.
@@ -248,11 +259,12 @@ module RightAws
248
259
  # key = RightAws::S3::Key.create(bucket, 'logs/today/1.log')
249
260
  # key.head
250
261
  #
251
- def key(key_name, head=false)
252
- raise 'Key name can not be empty.' if key_name.blank?
262
+ def key(key_name, head=false, &blck)
263
+ raise 'Key name can not be empty.' if key_name.right_blank?
253
264
  key_instance = nil
254
265
  # if this key exists - find it ....
255
266
  keys({'prefix'=>key_name}, head).each do |key|
267
+ blck.call if block_given?
256
268
  if key.name == key_name.to_s
257
269
  key_instance = key
258
270
  break
@@ -271,17 +283,17 @@ module RightAws
271
283
  #
272
284
  # bucket.put('logs/today/1.log', 'Olala!') #=> true
273
285
  #
274
- def put(key, data=nil, meta_headers={}, perms=nil, headers={})
286
+ def put(key, data=nil, meta_headers={}, perms=nil, headers={}, &blck)
275
287
  key = Key.create(self, key.to_s, data, meta_headers) unless key.is_a?(Key)
276
- key.put(data, perms, headers)
288
+ key.put(data, perms, headers, &blck)
277
289
  end
278
290
 
279
- # Retrieve object data from Amazon.
291
+ # Retrieve data object from Amazon.
280
292
  # The +key+ is a +String+ or Key.
281
- # Returns Key instance.
293
+ # Returns String instance.
282
294
  #
283
- # key = bucket.get('logs/today/1.log') #=>
284
- # puts key.data #=> 'sasfasfasdf'
295
+ # data = bucket.get('logs/today/1.log') #=>
296
+ # puts data #=> 'sasfasfasdf'
285
297
  #
286
298
  def get(key, headers={})
287
299
  key = Key.create(self, key.to_s) unless key.is_a?(Key)
@@ -346,9 +358,10 @@ module RightAws
346
358
  # If +force+ is set, clears and deletes the bucket.
347
359
  # Returns +true+.
348
360
  #
349
- # bucket.delete(true) #=> true
361
+ # bucket.delete(:force => true) #=> true
350
362
  #
351
- def delete(force=false)
363
+ def delete(options={})
364
+ force = options.is_a?(Hash) && options[:force]==true
352
365
  force ? @s3.interface.force_delete_bucket(@name) : @s3.interface.delete_bucket(@name)
353
366
  end
354
367
 
@@ -460,6 +473,30 @@ module RightAws
460
473
  get if !@data and exists?
461
474
  @data
462
475
  end
476
+
477
+ # Getter for the 'content-type' metadata
478
+ def content_type
479
+ @headers['content-type'] if @headers
480
+ end
481
+
482
+ # Helper to get and URI-decode a header metadata.
483
+ # Metadata have to be HTTP encoded (rfc2616) as we use the Amazon S3 REST api
484
+ # see http://docs.amazonwebservices.com/AmazonS3/latest/index.html?UsingMetadata.html
485
+ def decoded_meta_headers(key = nil)
486
+ if key
487
+ # Get one metadata value by its key
488
+ URI.decode(@meta_headers[key.to_s])
489
+ else
490
+ # Get a hash of all metadata with a decoded value
491
+ @decoded_meta_headers ||= begin
492
+ metadata = {}
493
+ @meta_headers.each do |key, value|
494
+ metadata[key.to_sym] = URI.decode(value)
495
+ end
496
+ metadata
497
+ end
498
+ end
499
+ end
463
500
 
464
501
  # Retrieve object data and attributes from Amazon.
465
502
  # Returns a +String+.
@@ -482,11 +519,33 @@ module RightAws
482
519
  # ...
483
520
  # key.put('Olala!') #=> true
484
521
  #
485
- def put(data=nil, perms=nil, headers={})
522
+ def put(data=nil, perms=nil, headers={}, &blck)
486
523
  headers['x-amz-acl'] = perms if perms
487
524
  @data = data || @data
488
525
  meta = self.class.add_meta_prefix(@meta_headers)
489
- @bucket.s3.interface.put(@bucket.name, @name, @data, meta.merge(headers))
526
+ @bucket.s3.interface.put(@bucket.name, @name, @data, meta.merge(headers), &blck)
527
+ end
528
+
529
+ # Store object data on S3 using the Multipart Upload API. This is useful if you do not know the file size
530
+ # upfront (for example reading from pipe or socket) or if you are transmitting data over an unreliable network.
531
+ #
532
+ # Parameter +data+ is an object which responds to :read or an object which can be converted to a String prior to upload.
533
+ # Parameter +part_size+ determines the size of each part sent (must be > 5MB per Amazon's API requirements)
534
+ #
535
+ # If data is a stream the caller is responsible for calling close() on the stream after this methods returns
536
+ #
537
+ # Returns +true+.
538
+ #
539
+ # upload_data = StringIO.new('My sample data')
540
+ # key = RightAws::S3::Key.create(bucket, 'logs/today/1.log')
541
+ # key.data = upload_data
542
+ # key.put_multipart(:part_size => 5*1024*1024) #=> true
543
+ #
544
+ def put_multipart(data=nil, perms=nil, headers={}, part_size=nil)
545
+ headers['x-amz-acl'] = perms if perms
546
+ @data = data || @data
547
+ meta = self.class.add_meta_prefix(@meta_headers)
548
+ @bucket.s3.interface.store_object_multipart({:bucket => @bucket.name, :key => @name, :data => @data, :headers => meta.merge(headers), :part_size => part_size})
490
549
  end
491
550
 
492
551
  # Rename an object. Returns new object name.
@@ -626,7 +685,7 @@ module RightAws
626
685
  # key.delete #=> true
627
686
  #
628
687
  def delete
629
- raise 'Key name must be specified.' if @name.blank?
688
+ raise 'Key name must be specified.' if @name.right_blank?
630
689
  @bucket.s3.interface.delete(@bucket, @name)
631
690
  end
632
691
 
@@ -759,11 +818,11 @@ module RightAws
759
818
  @thing = thing
760
819
  @id = id
761
820
  @name = name
762
- @perms = perms.to_a
821
+ @perms = Array(perms)
763
822
  case action
764
- when :apply: apply
765
- when :refresh: refresh
766
- when :apply_and_refresh: apply; refresh
823
+ when :apply then apply
824
+ when :refresh then refresh
825
+ when :apply_and_refresh then apply; refresh
767
826
  end
768
827
  end
769
828
 
@@ -775,9 +834,13 @@ module RightAws
775
834
  false
776
835
  end
777
836
 
778
- # Return Grantee type (+String+): "Group" or "CanonicalUser".
837
+ # Return Grantee type (+String+): "Group", "AmazonCustomerByEmail" or "CanonicalUser".
779
838
  def type
780
- @id[/^http:/] ? "Group" : "CanonicalUser"
839
+ case @id
840
+ when /^http:/ then "Group"
841
+ when /@/ then "AmazonCustomerByEmail"
842
+ else "CanonicalUser"
843
+ end
781
844
  end
782
845
 
783
846
  # Return a name or an id.
@@ -872,7 +935,11 @@ module RightAws
872
935
  end
873
936
 
874
937
  def to_xml # :nodoc:
875
- id_str = @id[/^http/] ? "<URI>#{@id}</URI>" : "<ID>#{@id}</ID>"
938
+ id_str = case @id
939
+ when /^http/ then "<URI>#{@id}</URI>"
940
+ when /@/ then "<EmailAddress>#{@id}</EmailAddress>"
941
+ else "<ID>#{@id}</ID>"
942
+ end
876
943
  grants = ''
877
944
  @perms.each do |perm|
878
945
  grants << "<Grant>" +
@@ -1012,8 +1079,8 @@ module RightAws
1012
1079
  #
1013
1080
  # bucket.get('logs/today/1.log', 1.hour)
1014
1081
  #
1015
- def get(key, expires=nil, headers={})
1016
- @s3.interface.get_link(@name, key.to_s, expires, headers)
1082
+ def get(key, expires=nil, headers={}, response_params={})
1083
+ @s3.interface.get_link(@name, key.to_s, expires, headers, response_params)
1017
1084
  end
1018
1085
 
1019
1086
  # Generate link to delete bucket.
@@ -1054,7 +1121,7 @@ module RightAws
1054
1121
  @bucket = bucket
1055
1122
  @name = name.to_s
1056
1123
  @meta_headers = meta_headers
1057
- raise 'Key name can not be empty.' if @name.blank?
1124
+ raise 'Key name can not be empty.' if @name.right_blank?
1058
1125
  end
1059
1126
 
1060
1127
  # Generate link to PUT key data.
@@ -1069,8 +1136,8 @@ module RightAws
1069
1136
  #
1070
1137
  # bucket.get('logs/today/1.log', 1.hour) #=> https://s3.amazonaws.com:443/my_awesome_bucket/logs%2Ftoday%2F1.log?Signature=h...M%3D&Expires=1180820032&AWSAccessKeyId=1...2
1071
1138
  #
1072
- def get(expires=nil, headers={})
1073
- @bucket.s3.interface.get_link(@bucket.to_s, @name, expires, headers)
1139
+ def get(expires=nil, headers={}, response_params={})
1140
+ @bucket.s3.interface.get_link(@bucket.to_s, @name, expires, headers, response_params)
1074
1141
  end
1075
1142
 
1076
1143
  # Generate link to delete key.
@@ -26,18 +26,37 @@ module RightAws
26
26
  class S3Interface < RightAwsBase
27
27
 
28
28
  USE_100_CONTINUE_PUT_SIZE = 1_000_000
29
+ MINIMUM_PART_SIZE = 5 * 1024 * 1024
30
+ DEFAULT_RETRY_COUNT = 5
29
31
 
30
32
  include RightAwsBaseInterface
31
33
 
32
34
  DEFAULT_HOST = 's3.amazonaws.com'
33
35
  DEFAULT_PORT = 443
34
36
  DEFAULT_PROTOCOL = 'https'
37
+ DEFAULT_SERVICE = '/'
35
38
  REQUEST_TTL = 30
36
39
  DEFAULT_EXPIRES_AFTER = 1 * 24 * 60 * 60 # One day's worth of seconds
37
40
  ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60
38
41
  AMAZON_HEADER_PREFIX = 'x-amz-'
39
42
  AMAZON_METADATA_PREFIX = 'x-amz-meta-'
43
+ S3_REQUEST_PARAMETERS = [ 'acl',
44
+ 'location',
45
+ 'logging', # this one is beta, no support for now
46
+ 'partNumber',
47
+ 'response-content-type',
48
+ 'response-content-language',
49
+ 'response-expires',
50
+ 'response-cache-control',
51
+ 'response-content-disposition',
52
+ 'response-content-encoding',
53
+ 'torrent',
54
+ 'uploadId',
55
+ 'uploads',
56
+ 'delete'].sort
57
+ MULTI_OBJECT_DELETE_MAX_KEYS = 1000
40
58
 
59
+
41
60
  @@bench = AwsBenchmarkingBlock.new
42
61
  def self.bench_xml
43
62
  @@bench.xml
@@ -46,23 +65,37 @@ module RightAws
46
65
  @@bench.service
47
66
  end
48
67
 
68
+ # Params supported:
69
+ # :no_subdomains => true # do not use bucket as a part of domain name but as a part of path
70
+ @@params = {}
71
+ def self.params
72
+ @@params
73
+ end
74
+
75
+ # get custom option
76
+ def param(name)
77
+ # - check explicitly defined param (@params)
78
+ # - otherwise check implicitly defined one (@@params)
79
+ @params.has_key?(name) ? @params[name] : @@params[name]
80
+ end
49
81
 
50
82
  # Creates new RightS3 instance.
51
83
  #
52
- # s3 = RightAws::S3Interface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', {:multi_thread => true, :logger => Logger.new('/tmp/x.log')}) #=> #<RightAws::S3Interface:0xb7b3c27c>
84
+ # s3 = RightAws::S3Interface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', {:logger => Logger.new('/tmp/x.log')}) #=> #<RightAws::S3Interface:0xb7b3c27c>
53
85
  #
54
86
  # Params is a hash:
55
87
  #
56
- # {:server => 's3.amazonaws.com' # Amazon service host: 's3.amazonaws.com'(default)
57
- # :port => 443 # Amazon service port: 80 or 443(default)
58
- # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
59
- # :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
60
- # :logger => Logger Object} # Logger instance: logs to STDOUT if omitted }
88
+ # {:server => 's3.amazonaws.com' # Amazon service host: 's3.amazonaws.com'(default)
89
+ # :port => 443 # Amazon service port: 80 or 443(default)
90
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
91
+ # :logger => Logger Object # Logger instance: logs to STDOUT if omitted
92
+ # :no_subdomains => true} # Force placing bucket name into path instead of domain name
61
93
  #
62
94
  def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
63
95
  init({ :name => 'S3',
64
96
  :default_host => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).host : DEFAULT_HOST,
65
- :default_port => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).port : DEFAULT_PORT,
97
+ :default_port => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).port : DEFAULT_PORT,
98
+ :default_service => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).path : DEFAULT_SERVICE,
66
99
  :default_protocol => ENV['S3_URL'] ? URI.parse(ENV['S3_URL']).scheme : DEFAULT_PROTOCOL },
67
100
  aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
68
101
  aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
@@ -78,7 +111,11 @@ module RightAws
78
111
  s3_headers = {}
79
112
  headers.each do |key, value|
80
113
  key = key.downcase
81
- s3_headers[key] = value.to_s.strip if key[/^#{AMAZON_HEADER_PREFIX}|^content-md5$|^content-type$|^date$/o]
114
+ value = case
115
+ when value.is_a?(Array) then value.join('')
116
+ else value.to_s
117
+ end
118
+ s3_headers[key] = value.strip if key[/^#{AMAZON_HEADER_PREFIX}|^content-md5$|^content-type$|^date$/o]
82
119
  end
83
120
  s3_headers['content-type'] ||= ''
84
121
  s3_headers['content-md5'] ||= ''
@@ -89,13 +126,20 @@ module RightAws
89
126
  s3_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
90
127
  out_string << (key[/^#{AMAZON_HEADER_PREFIX}/o] ? "#{key}:#{value}\n" : "#{value}\n")
91
128
  end
92
- # ignore everything after the question mark...
129
+ # ignore everything after the question mark by default...
93
130
  out_string << path.gsub(/\?.*$/, '')
94
- # ...unless there is an acl or torrent parameter
95
- out_string << '?acl' if path[/[&?]acl($|&|=)/]
96
- out_string << '?torrent' if path[/[&?]torrent($|&|=)/]
97
- out_string << '?location' if path[/[&?]location($|&|=)/]
98
- out_string << '?logging' if path[/[&?]logging($|&|=)/] # this one is beta, no support for now
131
+ # ... unless there is a parameter that we care about.
132
+ S3_REQUEST_PARAMETERS.each do |parameter|
133
+ if path[/[&?]#{parameter}(=[^&]*)?($|&)/]
134
+ if $1
135
+ value = CGI::unescape($1)
136
+ else
137
+ value = ''
138
+ end
139
+ out_string << (out_string[/[?]/] ? "&#{parameter}#{value}" : "?#{parameter}#{value}")
140
+ end
141
+ end
142
+
99
143
  out_string
100
144
  end
101
145
 
@@ -108,37 +152,43 @@ module RightAws
108
152
  end
109
153
  true
110
154
  end
111
-
112
- # Generates request hash for REST API.
113
- # Assumes that headers[:url] is URL encoded (use CGI::escape)
114
- def generate_rest_request(method, headers) # :nodoc:
155
+
156
+ def fetch_request_params(headers) #:nodoc:
115
157
  # default server to use
116
- server = @params[:server]
117
- # fix path
118
- path_to_sign = headers[:url]
119
- path_to_sign = "/#{path_to_sign}" unless path_to_sign[/^\//]
158
+ server = @params[:server]
159
+ service = @params[:service].to_s
160
+ service.chop! if service[%r{/$}] # remove trailing '/' from service
120
161
  # extract bucket name and check it's dns compartibility
121
- path_to_sign[%r{^/([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
162
+ headers[:url].to_s[%r{^([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
122
163
  bucket_name, key_path, params_list = $1, $2, $3
164
+ key_path = key_path.gsub( '%2F', '/' ) if key_path
123
165
  # select request model
124
- if is_dns_bucket?(bucket_name)
125
- # add backet to a server name
166
+ if !param(:no_subdomains) && is_dns_bucket?(bucket_name)
167
+ # fix a path
126
168
  server = "#{bucket_name}.#{server}"
127
- # remove bucket from the path
128
- path = "#{key_path || '/'}#{params_list}"
129
- # refactor the path (add '/' before params_list if the key is empty)
130
- path_to_sign = "/#{bucket_name}#{path}"
169
+ key_path ||= '/'
170
+ path = "#{service}#{key_path}#{params_list}"
131
171
  else
132
- path = path_to_sign
172
+ path = "#{service}/#{bucket_name}#{key_path}#{params_list}"
133
173
  end
174
+ path_to_sign = "#{service}/#{bucket_name}#{key_path}#{params_list}"
175
+ # path_to_sign = "/#{bucket_name}#{key_path}#{params_list}"
176
+ [ server, path, path_to_sign ]
177
+ end
178
+
179
+ # Generates request hash for REST API.
180
+ # Assumes that headers[:url] is URL encoded (use CGI::escape)
181
+ def generate_rest_request(method, headers) # :nodoc:
182
+ # calculate request data
183
+ server, path, path_to_sign = fetch_request_params(headers)
134
184
  data = headers[:data]
135
- # remove unset(==optional) and symbolyc keys
136
- headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
185
+ # make sure headers are downcased strings
186
+ headers = AwsUtils::fix_headers(headers)
137
187
  #
138
188
  headers['content-type'] ||= ''
139
189
  headers['date'] = Time.now.httpdate
140
190
  # create request
141
- request = "Net::HTTP::#{method.capitalize}".constantize.new(path)
191
+ request = "Net::HTTP::#{method.capitalize}".right_constantize.new(path)
142
192
  request.body = data if data
143
193
  # set request headers and meta headers
144
194
  headers.each { |key, value| request[key.to_s] = value }
@@ -157,12 +207,9 @@ module RightAws
157
207
  # Sends request to Amazon and parses the response.
158
208
  # Raises AwsError if any banana happened.
159
209
  def request_info(request, parser, &block) # :nodoc:
160
- thread = @params[:multi_thread] ? Thread.current : Thread.main
161
- thread[:s3_connection] ||= Rightscale::HttpConnection.new(:exception => RightAws::AwsError, :logger => @logger)
162
- request_info_impl(thread[:s3_connection], @@bench, request, parser, &block)
210
+ request_info_impl(:s3_connection, @@bench, request, parser, &block)
163
211
  end
164
212
 
165
-
166
213
  # Returns an array of customer's buckets. Each item is a +hash+.
167
214
  #
168
215
  # s3.list_all_my_buckets #=>
@@ -187,8 +234,14 @@ module RightAws
187
234
  #
188
235
  def create_bucket(bucket, headers={})
189
236
  data = nil
190
- unless headers[:location].blank?
191
- data = "<CreateBucketConfiguration><LocationConstraint>#{headers[:location].to_s.upcase}</LocationConstraint></CreateBucketConfiguration>"
237
+ location = case headers[:location].to_s
238
+ when 'us','US' then ''
239
+ when 'eu' then 'EU'
240
+ else headers[:location].to_s
241
+ end
242
+
243
+ unless location.right_blank?
244
+ data = "<CreateBucketConfiguration><LocationConstraint>#{location}</LocationConstraint></CreateBucketConfiguration>"
192
245
  end
193
246
  req_hash = generate_rest_request('PUT', headers.merge(:url=>bucket, :data => data))
194
247
  request_info(req_hash, RightHttp2xxParser.new)
@@ -238,7 +291,7 @@ module RightAws
238
291
  AwsUtils.allow_only([:bucket,:xmldoc, :headers], params)
239
292
  params[:headers] = {} unless params[:headers]
240
293
  req_hash = generate_rest_request('PUT', params[:headers].merge(:url=>"#{params[:bucket]}?logging", :data => params[:xmldoc]))
241
- request_info(req_hash, S3TrueParser.new)
294
+ request_info(req_hash, RightHttp2xxParser.new)
242
295
  rescue
243
296
  on_exception
244
297
  end
@@ -273,7 +326,7 @@ module RightAws
273
326
  # 'max-keys' => "5"}, ..., {...}]
274
327
  #
275
328
  def list_bucket(bucket, options={}, headers={})
276
- bucket += '?'+options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless options.blank?
329
+ bucket += '?'+options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless options.right_blank?
277
330
  req_hash = generate_rest_request('GET', headers.merge(:url=>bucket))
278
331
  request_info(req_hash, S3ListBucketParser.new(:logger => @logger))
279
332
  rescue
@@ -308,10 +361,10 @@ module RightAws
308
361
  # ]
309
362
  # }
310
363
  def incrementally_list_bucket(bucket, options={}, headers={}, &block)
311
- internal_options = options.symbolize_keys
364
+ internal_options = options.right_symbolize_keys
312
365
  begin
313
366
  internal_bucket = bucket.dup
314
- internal_bucket += '?'+internal_options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless internal_options.blank?
367
+ internal_bucket += '?'+internal_options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless internal_options.right_blank?
315
368
  req_hash = generate_rest_request('GET', headers.merge(:url=>internal_bucket))
316
369
  response = request_info(req_hash, S3ImprovedListBucketParser.new(:logger => @logger))
317
370
  there_are_more_keys = response[:is_truncated]
@@ -382,7 +435,7 @@ module RightAws
382
435
  # mode.
383
436
  #
384
437
 
385
- def put(bucket, key, data=nil, headers={})
438
+ def put(bucket, key, data=nil, headers={}, &blck)
386
439
  # On Windows, if someone opens a file in text mode, we must reset it so
387
440
  # to binary mode for streaming to work properly
388
441
  if(data.respond_to?(:binmode))
@@ -393,7 +446,7 @@ module RightAws
393
446
  headers['expect'] = '100-continue'
394
447
  end
395
448
  req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data))
396
- request_info(req_hash, RightHttp2xxParser.new)
449
+ request_info(req_hash, RightHttp2xxParser.new, &blck)
397
450
  rescue
398
451
  on_exception
399
452
  end
@@ -478,6 +531,134 @@ module RightAws
478
531
  r[:verified_md5] ? (return r) : (raise AwsError.new("Uploaded object failed MD5 checksum verification: #{r.inspect}"))
479
532
  end
480
533
 
534
+ # New experimental API for uploading objects using the multipart upload API.
535
+ # store_object_multipart is similar in function to the store_object method, but breaks the input into parts and transmits each
536
+ # part separately. The multipart upload API has the benefit of being be able to retransmit a part in isolation without needing to
537
+ # restart the entire upload. This makes it ideal for uploading large files over unreliable networks. It also does not
538
+ # require the file size to be known before starting the upload, making it useful for stream data as it is created (say via reading a pipe or socket).
539
+ # The hash of the response headers contains useful information like the location (the URI for the newly created object), bucket, key, and etag).
540
+ #
541
+ # The optional argument of :headers allows the caller to specify arbitrary request header values.
542
+ #
543
+ # s3.store_object_multipart(:bucket => "foobucket", :key => "foo", :data => "polemonium" )
544
+ # => {:location=>"https://s3.amazonaws.com/right_s3_awesome_test_bucket_000B1_officedrop/test%2Flarge_multipart_file",
545
+ # :e_tag=>"\"72b81ac08aed4d4d1055c11f56c2a258-1\"",
546
+ # :key=>"test/large_multipart_file",
547
+ # :bucket=>"right_s3_awesome_test_bucket_000B1_officedrop"}
548
+ #
549
+ # f = File.new("some_file", "r")
550
+ # s3.store_object_multipart(:bucket => "foobucket", :key => "foo", :data => f )
551
+ # => {:location=>"https://s3.amazonaws.com/right_s3_awesome_test_bucket_000B1_officedrop/test%2Flarge_multipart_file",
552
+ # :e_tag=>"\"72b81ac08aed4d4d1055c11f56c2a258-1\"",
553
+ # :key=>"test/large_multipart_file",
554
+ # :bucket=>"right_s3_awesome_test_bucket_000B1_officedrop"}
555
+ def store_object_multipart(params)
556
+ AwsUtils.allow_only([:bucket, :key, :data, :headers, :part_size, :retry_count], params)
557
+ AwsUtils.mandatory_arguments([:bucket, :key, :data], params)
558
+ params[:headers] = {} unless params[:headers]
559
+
560
+ params[:data].binmode if(params[:data].respond_to?(:binmode)) # On Windows, if someone opens a file in text mode, we must reset it to binary mode for streaming to work properly
561
+
562
+ # detect whether we are using straight read or converting to string first
563
+ unless(params[:data].respond_to?(:read))
564
+ params[:data] = StringIO.new(params[:data].to_s)
565
+ end
566
+
567
+ # make sure part size is > 5 MB minimum
568
+ params[:part_size] ||= MINIMUM_PART_SIZE
569
+ if params[:part_size] < MINIMUM_PART_SIZE
570
+ raise AwsError.new("Part size for a multipart upload must be greater than or equal to #{5 * 1024 * 1024} bytes. #{params[:part_size]} bytes was provided.")
571
+ end
572
+
573
+ # make sure retry_count is positive
574
+ params[:retry_count] ||= DEFAULT_RETRY_COUNT
575
+ if params[:retry_count] < 0
576
+ raise AwsError.new("Retry count must be positive. #{params[:retry_count]} bytes was provided.")
577
+ end
578
+
579
+ # Set 100-continue for large part sizes
580
+ if (params[:part_size] >= USE_100_CONTINUE_PUT_SIZE)
581
+ params[:headers]['expect'] = '100-continue'
582
+ end
583
+
584
+ # initiate upload
585
+ initiate_hash = generate_rest_request('POST', params[:headers].merge(:url=>"#{params[:bucket]}/#{CGI::escape params[:key]}?uploads"))
586
+ initiate_resp = request_info(initiate_hash, S3MultipartUploadInitiateResponseParser.new)
587
+ upload_id = initiate_resp[:upload_id]
588
+
589
+ # split into parts and upload each one, re-trying if necessary
590
+ # upload occurs serially at this time.
591
+ part_etags = []
592
+ part_data = ""
593
+ index = 1
594
+ until params[:data].eof?
595
+ part_data = params[:data].read(params[:part_size])
596
+ unless part_data.size == 0
597
+ retry_attempts = 1
598
+ while true
599
+ begin
600
+ send_part_hash = generate_rest_request('PUT', params[:headers].merge({ :url=>"#{params[:bucket]}/#{CGI::escape params[:key]}?partNumber=#{index}&uploadId=#{upload_id}", :data=>part_data } ))
601
+ send_part_resp = request_info(send_part_hash, S3HttpResponseHeadParser.new)
602
+ part_etags << {:part_num => index, :etag => send_part_resp['etag']}
603
+ index += 1
604
+ break # successful, can move to next part
605
+ rescue AwsError => e
606
+ if retry_attempts >= params[:retry_count]
607
+ raise e
608
+ else
609
+ #Hit an error attempting to transmit part, retry until retry_attemts have been exhausted
610
+ retry_attempts += 1
611
+ end
612
+ end
613
+ end
614
+ end
615
+ end
616
+
617
+ # assemble complete upload message
618
+ complete_body = "<CompleteMultipartUpload>"
619
+ part_etags.each do |part_hash|
620
+ complete_body << "<Part><PartNumber>#{part_hash[:part_num]}</PartNumber><ETag>#{part_hash[:etag]}</ETag></Part>"
621
+ end
622
+ complete_body << "</CompleteMultipartUpload>"
623
+ complete_req_hash = generate_rest_request('POST', {:url=>"#{params[:bucket]}/#{CGI::escape params[:key]}?uploadId=#{upload_id}", :data => complete_body})
624
+ return request_info(complete_req_hash, S3CompleteMultipartParser.new)
625
+ rescue
626
+ on_exception
627
+ end
628
+
629
+ class S3MultipartUploadInitiateResponseParser < RightAWSParser
630
+ def reset
631
+ @result = {}
632
+ end
633
+ def headers_to_string(headers)
634
+ result = {}
635
+ headers.each do |key, value|
636
+ value = value.first if value.is_a?(Array) && value.size<2
637
+ result[key] = value
638
+ end
639
+ result
640
+ end
641
+ def tagend(name)
642
+ case name
643
+ when 'UploadId' then @result[:upload_id] = @text
644
+ end
645
+ end
646
+ end
647
+
648
+ class S3CompleteMultipartParser < RightAWSParser # :nodoc:
649
+ def reset
650
+ @result = {}
651
+ end
652
+ def tagend(name)
653
+ case name
654
+ when 'Location' then @result[:location] = @text
655
+ when 'Bucket' then @result[:bucket] = @text
656
+ when 'Key' then @result[:key] = @text
657
+ when 'ETag' then @result[:e_tag] = @text
658
+ end
659
+ end
660
+ end
661
+
481
662
  # Retrieves object data from Amazon. Returns a +hash+ or an exception.
482
663
  #
483
664
  # s3.get('my_awesome_bucket', 'log/curent/1.log') #=>
@@ -609,6 +790,34 @@ module RightAws
609
790
  on_exception
610
791
  end
611
792
 
793
+ # Deletes multiple keys. Returns an array with errors, if any.
794
+ #
795
+ # s3.delete_multiple('my_awesome_bucket', ['key1', 'key2', ...)
796
+ # #=> [ { :key => 'key2', :code => 'AccessDenied', :message => "Access Denied" } ]
797
+ #
798
+ def delete_multiple(bucket, keys=[], headers={})
799
+ errors = []
800
+ keys = Array.new(keys)
801
+ while keys.length > 0
802
+ data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
803
+ data += "<Delete>\n<Quiet>true</Quiet>\n"
804
+ keys.take(MULTI_OBJECT_DELETE_MAX_KEYS).each do |key|
805
+ data += "<Object><Key>#{AwsUtils::xml_escape(key)}</Key></Object>\n"
806
+ end
807
+ data += "</Delete>"
808
+ req_hash = generate_rest_request('POST', headers.merge(
809
+ :url => "#{bucket}?delete",
810
+ :data => data,
811
+ 'content-md5' => AwsUtils::content_md5(data)
812
+ ))
813
+ errors += request_info(req_hash, S3DeleteMultipleParser.new)
814
+ keys = keys.drop(MULTI_OBJECT_DELETE_MAX_KEYS)
815
+ end
816
+ errors
817
+ rescue
818
+ on_exception
819
+ end
820
+
612
821
  # Copy an object.
613
822
  # directive: :copy - copy meta-headers from source (default value)
614
823
  # :replace - replace meta-headers by passed ones
@@ -674,7 +883,7 @@ module RightAws
674
883
  # <Permission>FULL_CONTROL</Permission></Grant></AccessControlList></AccessControlPolicy>" }
675
884
  #
676
885
  def get_acl(bucket, key='', headers={})
677
- key = key.blank? ? '' : "/#{CGI::escape key}"
886
+ key = key.right_blank? ? '' : "/#{CGI::escape key}"
678
887
  req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
679
888
  request_info(req_hash, S3HttpResponseBodyParser.new)
680
889
  rescue
@@ -704,7 +913,7 @@ module RightAws
704
913
  # :display_name=>"root"}}
705
914
  #
706
915
  def get_acl_parse(bucket, key='', headers={})
707
- key = key.blank? ? '' : "/#{CGI::escape key}"
916
+ key = key.right_blank? ? '' : "/#{CGI::escape key}"
708
917
  req_hash = generate_rest_request('GET', headers.merge(:url=>"#{bucket}#{key}?acl"))
709
918
  acl = request_info(req_hash, S3AclParser.new(:logger => @logger))
710
919
  result = {}
@@ -717,7 +926,7 @@ module RightAws
717
926
  else
718
927
  result[:grantees][key] =
719
928
  { :display_name => grantee[:display_name] || grantee[:uri].to_s[/[^\/]*$/],
720
- :permissions => grantee[:permissions].to_a,
929
+ :permissions => Array(grantee[:permissions]),
721
930
  :attributes => grantee[:attributes] }
722
931
  end
723
932
  end
@@ -728,7 +937,7 @@ module RightAws
728
937
 
729
938
  # Sets the ACL on a bucket or object.
730
939
  def put_acl(bucket, key, acl_xml_doc, headers={})
731
- key = key.blank? ? '' : "/#{CGI::escape key}"
940
+ key = key.right_blank? ? '' : "/#{CGI::escape key}"
732
941
  req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}#{key}?acl", :data=>acl_xml_doc))
733
942
  request_info(req_hash, S3HttpResponseBodyParser.new)
734
943
  rescue
@@ -806,36 +1015,25 @@ module RightAws
806
1015
  # Query API: Links
807
1016
  #-----------------------------------------------------------------
808
1017
 
1018
+ def s3_link_escape(text)
1019
+ #CGI::escape(text.to_s).gsub(/[+]/, '%20')
1020
+ AwsUtils::amz_escape(text.to_s)
1021
+ end
1022
+
809
1023
  # Generates link for QUERY API
810
1024
  def generate_link(method, headers={}, expires=nil) #:nodoc:
811
- # default server to use
812
- server = @params[:server]
813
- # fix path
814
- path_to_sign = headers[:url]
815
- path_to_sign = "/#{path_to_sign}" unless path_to_sign[/^\//]
816
- # extract bucket name and check it's dns compartibility
817
- path_to_sign[%r{^/([a-z0-9._-]*)(/[^?]*)?(\?.+)?}i]
818
- bucket_name, key_path, params_list = $1, $2, $3
819
- # select request model
820
- if is_dns_bucket?(bucket_name)
821
- # add backet to a server name
822
- server = "#{bucket_name}.#{server}"
823
- # remove bucket from the path
824
- path = "#{key_path || '/'}#{params_list}"
825
- # refactor the path (add '/' before params_list if the key is empty)
826
- path_to_sign = "/#{bucket_name}#{path}"
827
- else
828
- path = path_to_sign
829
- end
830
- # expiration time
1025
+ # calculate request data
1026
+ server, path, path_to_sign = fetch_request_params(headers)
1027
+
1028
+ # expiration time
831
1029
  expires ||= DEFAULT_EXPIRES_AFTER
832
1030
  expires = Time.now.utc + expires if expires.is_a?(Fixnum) && (expires < ONE_YEAR_IN_SECONDS)
833
1031
  expires = expires.to_i
834
- # remove unset(==optional) and symbolyc keys
835
- headers.each{ |key, value| headers.delete(key) if (value.nil? || key.is_a?(Symbol)) }
1032
+ # make sure headers are downcased strings
1033
+ headers = AwsUtils::fix_headers(headers)
836
1034
  #generate auth strings
837
1035
  auth_string = canonical_string(method, path_to_sign, headers, expires)
838
- signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new("sha1"), @aws_secret_access_key, auth_string)).strip)
1036
+ signature = CGI::escape(AwsUtils::sign( @aws_secret_access_key, auth_string))
839
1037
  # path building
840
1038
  addon = "Signature=#{signature}&Expires=#{expires}&AWSAccessKeyId=#{@aws_access_key_id}"
841
1039
  path += path[/\?/] ? "&#{addon}" : "?#{addon}"
@@ -879,7 +1077,7 @@ module RightAws
879
1077
  # s3.list_bucket_link('my_awesome_bucket') #=> url string
880
1078
  #
881
1079
  def list_bucket_link(bucket, options=nil, expires=nil, headers={})
882
- bucket += '?' + options.map{|k, v| "#{k.to_s}=#{CGI::escape v.to_s}"}.join('&') unless options.blank?
1080
+ bucket += '?' + options.map{|k, v| "#{k.to_s}=#{s3_link_escape(v)}"}.join('&') unless options.right_blank?
883
1081
  generate_link('GET', headers.merge(:url=>bucket), expires)
884
1082
  rescue
885
1083
  on_exception
@@ -890,7 +1088,7 @@ module RightAws
890
1088
  # s3.put_link('my_awesome_bucket',key, object) #=> url string
891
1089
  #
892
1090
  def put_link(bucket, key, data=nil, expires=nil, headers={})
893
- generate_link('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data), expires)
1091
+ generate_link('PUT', headers.merge(:url=>"#{bucket}/#{s3_link_escape(key)}", :data=>data), expires)
894
1092
  rescue
895
1093
  on_exception
896
1094
  end
@@ -907,8 +1105,20 @@ module RightAws
907
1105
  # s3.get_link('my_awesome_bucket',key) #=> https://s3.amazonaws.com:443/my_awesome_bucket/asia%2Fcustomers?Signature=QAO...
908
1106
  #
909
1107
  # see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html
910
- def get_link(bucket, key, expires=nil, headers={})
911
- generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
1108
+ #
1109
+ # To specify +response+-* parameters, define them in the response_params hash:
1110
+ #
1111
+ # s3.get_link('my_awesome_bucket',key,nil,{},{ "response-content-disposition" => "attachment; filename=caf�.png", "response-content-type" => "image/png"})
1112
+ #
1113
+ # #=> https://s3.amazonaws.com:443/my_awesome_bucket/asia%2Fcustomers?response-content-disposition=attachment%3B%20filename%3Dcaf%25C3%25A9.png&response-content-type=image%2Fpng&Signature=wio...
1114
+ #
1115
+ def get_link(bucket, key, expires=nil, headers={}, response_params={})
1116
+ if response_params.size > 0
1117
+ response_params = '?' + response_params.map { |k, v| "#{k}=#{s3_link_escape(v)}" }.join('&')
1118
+ else
1119
+ response_params = ''
1120
+ end
1121
+ generate_link('GET', headers.merge(:url=>"#{bucket}/#{s3_link_escape(key)}#{response_params}"), expires)
912
1122
  rescue
913
1123
  on_exception
914
1124
  end
@@ -918,7 +1128,7 @@ module RightAws
918
1128
  # s3.head_link('my_awesome_bucket',key) #=> url string
919
1129
  #
920
1130
  def head_link(bucket, key, expires=nil, headers={})
921
- generate_link('HEAD', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
1131
+ generate_link('HEAD', headers.merge(:url=>"#{bucket}/#{s3_link_escape(key)}"), expires)
922
1132
  rescue
923
1133
  on_exception
924
1134
  end
@@ -928,7 +1138,7 @@ module RightAws
928
1138
  # s3.delete_link('my_awesome_bucket',key) #=> url string
929
1139
  #
930
1140
  def delete_link(bucket, key, expires=nil, headers={})
931
- generate_link('DELETE', headers.merge(:url=>"#{bucket}/#{CGI::escape key}"), expires)
1141
+ generate_link('DELETE', headers.merge(:url=>"#{bucket}/#{s3_link_escape(key)}"), expires)
932
1142
  rescue
933
1143
  on_exception
934
1144
  end
@@ -939,7 +1149,7 @@ module RightAws
939
1149
  # s3.get_acl_link('my_awesome_bucket',key) #=> url string
940
1150
  #
941
1151
  def get_acl_link(bucket, key='', headers={})
942
- return generate_link('GET', headers.merge(:url=>"#{bucket}/#{CGI::escape key}?acl"))
1152
+ return generate_link('GET', headers.merge(:url=>"#{bucket}/#{s3_link_escape(key)}?acl"))
943
1153
  rescue
944
1154
  on_exception
945
1155
  end
@@ -949,7 +1159,7 @@ module RightAws
949
1159
  # s3.put_acl_link('my_awesome_bucket',key) #=> url string
950
1160
  #
951
1161
  def put_acl_link(bucket, key='', headers={})
952
- return generate_link('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}?acl"))
1162
+ return generate_link('PUT', headers.merge(:url=>"#{bucket}/#{s3_link_escape(key)}?acl"))
953
1163
  rescue
954
1164
  on_exception
955
1165
  end
@@ -974,6 +1184,23 @@ module RightAws
974
1184
  on_exception
975
1185
  end
976
1186
 
1187
+ class S3DeleteMultipleParser < RightAWSParser # :nodoc:
1188
+ def reset
1189
+ @result = []
1190
+ end
1191
+ def tagstart(name, attributes)
1192
+ @error = {} if name == 'Error'
1193
+ end
1194
+ def tagend(name)
1195
+ case name
1196
+ when 'Key' then @error[:key] = @text
1197
+ when 'Code' then @error[:code] = @text
1198
+ when 'Message' then @error[:message] = @text
1199
+ when 'Error' then @result << @error
1200
+ end
1201
+ end
1202
+ end
1203
+
977
1204
  #-----------------------------------------------------------------
978
1205
  # PARSERS:
979
1206
  #-----------------------------------------------------------------
@@ -988,11 +1215,11 @@ module RightAws
988
1215
  end
989
1216
  def tagend(name)
990
1217
  case name
991
- when 'ID' ; @owner[:owner_id] = @text
992
- when 'DisplayName' ; @owner[:owner_display_name] = @text
993
- when 'Name' ; @current_bucket[:name] = @text
994
- when 'CreationDate'; @current_bucket[:creation_date] = @text
995
- when 'Bucket' ; @result << @current_bucket.merge(@owner)
1218
+ when 'ID' then @owner[:owner_id] = @text
1219
+ when 'DisplayName' then @owner[:owner_display_name] = @text
1220
+ when 'Name' then @current_bucket[:name] = @text
1221
+ when 'CreationDate'then @current_bucket[:creation_date] = @text
1222
+ when 'Bucket' then @result << @current_bucket.merge(@owner)
996
1223
  end
997
1224
  end
998
1225
  end
@@ -1009,21 +1236,23 @@ module RightAws
1009
1236
  def tagend(name)
1010
1237
  case name
1011
1238
  # service info
1012
- when 'Name' ; @service['name'] = @text
1013
- when 'Prefix' ; @service['prefix'] = @text
1014
- when 'Marker' ; @service['marker'] = @text
1015
- when 'MaxKeys' ; @service['max-keys'] = @text
1016
- when 'Delimiter' ; @service['delimiter'] = @text
1017
- when 'IsTruncated' ; @service['is_truncated'] = (@text =~ /false/ ? false : true)
1239
+ when 'Name' then @service['name'] = @text
1240
+ when 'Prefix' then @service['prefix'] = @text
1241
+ when 'Marker' then @service['marker'] = @text
1242
+ when 'MaxKeys' then @service['max-keys'] = @text
1243
+ when 'Delimiter' then @service['delimiter'] = @text
1244
+ when 'IsTruncated' then @service['is_truncated'] = (@text =~ /false/ ? false : true)
1018
1245
  # key data
1019
- when 'Key' ; @current_key[:key] = @text
1020
- when 'LastModified'; @current_key[:last_modified] = @text
1021
- when 'ETag' ; @current_key[:e_tag] = @text
1022
- when 'Size' ; @current_key[:size] = @text.to_i
1023
- when 'StorageClass'; @current_key[:storage_class] = @text
1024
- when 'ID' ; @current_key[:owner_id] = @text
1025
- when 'DisplayName' ; @current_key[:owner_display_name] = @text
1026
- when 'Contents' ; @current_key[:service] = @service; @result << @current_key
1246
+ when 'Key' then @current_key[:key] = @text
1247
+ when 'LastModified'then @current_key[:last_modified] = @text
1248
+ when 'ETag' then @current_key[:e_tag] = @text
1249
+ when 'Size' then @current_key[:size] = @text.to_i
1250
+ when 'StorageClass'then @current_key[:storage_class] = @text
1251
+ when 'ID' then @current_key[:owner_id] = @text
1252
+ when 'DisplayName' then @current_key[:owner_display_name] = @text
1253
+ when 'Contents'
1254
+ @current_key[:service] = @service
1255
+ @result << @current_key
1027
1256
  end
1028
1257
  end
1029
1258
  end
@@ -1045,27 +1274,29 @@ module RightAws
1045
1274
  def tagend(name)
1046
1275
  case name
1047
1276
  # service info
1048
- when 'Name' ; @result[:name] = @text
1277
+ when 'Name' then @result[:name] = @text
1049
1278
  # Amazon uses the same tag for the search prefix and for the entries
1050
1279
  # in common prefix...so use our simple flag to see which element
1051
1280
  # we are parsing
1052
- when 'Prefix' ; @in_common_prefixes ? @common_prefixes << @text : @result[:prefix] = @text
1053
- when 'Marker' ; @result[:marker] = @text
1054
- when 'MaxKeys' ; @result[:max_keys] = @text
1055
- when 'Delimiter' ; @result[:delimiter] = @text
1056
- when 'IsTruncated' ; @result[:is_truncated] = (@text =~ /false/ ? false : true)
1057
- when 'NextMarker' ; @result[:next_marker] = @text
1281
+ when 'Prefix' then @in_common_prefixes ? @common_prefixes << @text : @result[:prefix] = @text
1282
+ when 'Marker' then @result[:marker] = @text
1283
+ when 'MaxKeys' then @result[:max_keys] = @text
1284
+ when 'Delimiter' then @result[:delimiter] = @text
1285
+ when 'IsTruncated' then @result[:is_truncated] = (@text =~ /false/ ? false : true)
1286
+ when 'NextMarker' then @result[:next_marker] = @text
1058
1287
  # key data
1059
- when 'Key' ; @current_key[:key] = @text
1060
- when 'LastModified'; @current_key[:last_modified] = @text
1061
- when 'ETag' ; @current_key[:e_tag] = @text
1062
- when 'Size' ; @current_key[:size] = @text.to_i
1063
- when 'StorageClass'; @current_key[:storage_class] = @text
1064
- when 'ID' ; @current_key[:owner_id] = @text
1065
- when 'DisplayName' ; @current_key[:owner_display_name] = @text
1066
- when 'Contents' ; @result[:contents] << @current_key
1288
+ when 'Key' then @current_key[:key] = @text
1289
+ when 'LastModified'then @current_key[:last_modified] = @text
1290
+ when 'ETag' then @current_key[:e_tag] = @text
1291
+ when 'Size' then @current_key[:size] = @text.to_i
1292
+ when 'StorageClass'then @current_key[:storage_class] = @text
1293
+ when 'ID' then @current_key[:owner_id] = @text
1294
+ when 'DisplayName' then @current_key[:owner_display_name] = @text
1295
+ when 'Contents' then @result[:contents] << @current_key
1067
1296
  # Common Prefix stuff
1068
- when 'CommonPrefixes' ; @result[:common_prefixes] = @common_prefixes; @in_common_prefixes = false
1297
+ when 'CommonPrefixes'
1298
+ @result[:common_prefixes] = @common_prefixes
1299
+ @in_common_prefixes = false
1069
1300
  end
1070
1301
  end
1071
1302
  end
@@ -1140,8 +1371,8 @@ module RightAws
1140
1371
  end
1141
1372
  def tagend(name)
1142
1373
  case name
1143
- when 'LastModified' : @result[:last_modified] = @text
1144
- when 'ETag' : @result[:e_tag] = @text
1374
+ when 'LastModified' then @result[:last_modified] = @text
1375
+ when 'ETag' then @result[:e_tag] = @text
1145
1376
  end
1146
1377
  end
1147
1378
  end
@@ -1158,7 +1389,7 @@ module RightAws
1158
1389
  def headers_to_string(headers)
1159
1390
  result = {}
1160
1391
  headers.each do |key, value|
1161
- value = value.to_s if value.is_a?(Array) && value.size<2
1392
+ value = value.first if value.is_a?(Array) && value.size<2
1162
1393
  result[key] = value
1163
1394
  end
1164
1395
  result