right_aws 1.9.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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