right_aws 3.0.4 → 3.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -346,3 +346,13 @@ the source key.
346
346
  - Fixed:
347
347
  - #125 - fixes redirect bug in file PUT requests (Cary)
348
348
  - some other minor fixes
349
+
350
+ === 3.0.5
351
+ Release Notes:
352
+ - Added: API '2012-06-15' support for CreateVolume and DescribeVolumes API calls (to support IOPS)
353
+ - Fixed:
354
+ - Single-threaded multipart upload support (https://github.com/rightscale/right_aws/pull/116)
355
+ - S3 multi object delete (https://github.com/rightscale/right_aws/pull/106)
356
+ - Support for "ami_version" added in emr interface (https://github.com/rightscale/right_aws/pull/129)
357
+ - S3: Added block references to several methods (https://github.com/rightscale/right_aws/pull/130)
358
+ - Some other minor changes
@@ -43,7 +43,12 @@ module RightAws
43
43
  Base64.encode64(OpenSSL::HMAC.digest(@@digest1, aws_secret_access_key, auth_string)).strip
44
44
  end
45
45
 
46
- # Escape a string accordingly Amazon rules
46
+ # Calculates 'Content-MD5' header value for some content
47
+ def self.content_md5(content)
48
+ Base64.encode64(Digest::MD5::new.update(content).digest).strip
49
+ end
50
+
51
+ # Escape a string accordingly Amazon rulles
47
52
  # http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
48
53
  def self.amz_escape(param)
49
54
  param = param.flatten.join('') if param.is_a?(Array) # ruby 1.9.x Array#to_s fix
@@ -443,6 +448,7 @@ module RightAws
443
448
  "AWSAccessKeyId" => @aws_access_key_id,
444
449
  "Version" => custom_options[:api_version] || @params[:api_version] }
445
450
  service_hash.merge!(options)
451
+ service_hash["SecurityToken"] = @params[:token] if @params[:token]
446
452
  # Sign request options
447
453
  service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:host_to_sign], @params[:service])
448
454
  # Use POST if the length of the query string is too large
@@ -2,7 +2,7 @@ module RightAws #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 3 unless defined?(MAJOR)
4
4
  MINOR = 0 unless defined?(MINOR)
5
- TINY = 4 unless defined?(TINY)
5
+ TINY = 5 unless defined?(TINY)
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.') unless defined?(STRING)
8
8
  end
@@ -91,9 +91,12 @@ module RightAws
91
91
  'm2.xlarge' ,
92
92
  'm2.2xlarge',
93
93
  'm2.4xlarge',
94
+ 'm3.xlarge' ,
95
+ 'm3.2xlarge',
94
96
  'cc1.4xlarge',
95
97
  'cg1.4xlarge',
96
- 'cc2.8xlarge']
98
+ 'cc2.8xlarge',
99
+ 'hi1.4xlarge' ]
97
100
 
98
101
  @@bench = AwsBenchmarkingBlock.new
99
102
  def self.bench_xml
@@ -120,6 +123,7 @@ module RightAws
120
123
  # * <tt>:logger</tt>: for log messages, default: RAILS_DEFAULT_LOGGER else STDOUT
121
124
  # * <tt>:signature_version</tt>: The signature version : '0','1' or '2'(default)
122
125
  # * <tt>:cache</tt>: true/false: caching for: ec2_describe_images, describe_instances,
126
+ # * <tt>:token</tt>: Option SecurityToken for temporary credentials
123
127
  # describe_images_by_owner, describe_images_by_executable_by, describe_availability_zones,
124
128
  # describe_security_groups, describe_key_pairs, describe_addresses,
125
129
  # describe_volumes, describe_snapshots methods, default: false.
@@ -154,12 +158,15 @@ module RightAws
154
158
  # 'RemoteFunctionName' -> :remote_funtion_name
155
159
  cache_name = remote_function_name.right_underscore.to_sym
156
160
  list, options = AwsUtils::split_items_and_params(list_and_options)
161
+ custom_options = {}
157
162
  # Resource IDs to fetch
158
163
  request_hash = amazonize_list(remote_item_name, list)
159
164
  # Other custom options
160
165
  options.each do |key, values|
161
166
  next if values.right_blank?
162
167
  case key
168
+ when :options
169
+ custom_options = values
163
170
  when :filters then
164
171
  request_hash.merge!(amazonize_list(['Filter.?.Name', 'Filter.?.Value.?'], values))
165
172
  else
@@ -167,12 +174,17 @@ module RightAws
167
174
  end
168
175
  end
169
176
  cache_for = (list.right_blank? && options.right_blank?) ? cache_name : nil
170
- link = generate_request(remote_function_name, request_hash)
177
+ link = generate_request(remote_function_name, request_hash, custom_options)
171
178
  request_cache_or_info(cache_for, link, parser_class, @@bench, cache_for, &block)
172
179
  rescue Exception
173
180
  on_exception
174
181
  end
175
182
 
183
+ def merge_new_options_into_list_and_options(list_and_options, new_options)
184
+ list, options = AwsUtils::split_items_and_params(list_and_options)
185
+ list << options.merge(new_options)
186
+ end
187
+
176
188
  #-----------------------------------------------------------------
177
189
  # Keys
178
190
  #-----------------------------------------------------------------
@@ -29,6 +29,9 @@ module RightAws
29
29
  # EBS: Volumes
30
30
  #-----------------------------------------------------------------
31
31
 
32
+ VOLUME_API_VERSION = (API_VERSION > '2012-06-15') ? API_VERSION : '2012-06-15'
33
+ VOLUME_TYPES = ['standard', 'io1']
34
+
32
35
  # Describe EBS volumes.
33
36
  #
34
37
  # Accepts a list of volumes and/or a set of filters as the last parameter.
@@ -47,6 +50,14 @@ module RightAws
47
50
  # :aws_id => "vol-60957009",
48
51
  # :aws_created_at => "2008-06-18T08:19:20.000Z",
49
52
  # :aws_instance_id => "i-c014c0a9"},
53
+ # {:aws_id => "vol-71de8b1f",
54
+ # :aws_size => 5,
55
+ # :snapshot_id => nil,
56
+ # :zone => "us-east-1a",
57
+ # :aws_status => "available",
58
+ # :aws_created_at => "2012-06-21T18:47:34.000Z",
59
+ # :volume_type => "io1",
60
+ # :iop => "5"},#
50
61
  # {:aws_size => 1,
51
62
  # :zone => "merlot",
52
63
  # :snapshot_id => nil,
@@ -59,6 +70,7 @@ module RightAws
59
70
  # P.S. filters: http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVolumes.html
60
71
  #
61
72
  def describe_volumes(*list_and_options)
73
+ list_and_options = merge_new_options_into_list_and_options(list_and_options, :options => {:api_version => VOLUME_API_VERSION})
62
74
  describe_resources_with_list_and_options('DescribeVolumes', 'VolumeId', QEc2DescribeVolumesParser, list_and_options)
63
75
  end
64
76
 
@@ -73,12 +85,29 @@ module RightAws
73
85
  # :aws_created_at => "2008-06-24T18:13:32.000Z",
74
86
  # :aws_size => 94}
75
87
  #
76
- def create_volume(snapshot_id, size, zone)
88
+ # ec2.create_volume(nil, 5, 'us-east-1a', :iops => '5', :volume_type => 'io1') #=>
89
+ # {:aws_id=>"vol-71de8b1f",
90
+ # :aws_size=>5,
91
+ # :snapshot_id=>nil,
92
+ # :zone=>"us-east-1a",
93
+ # :aws_status=>"creating",
94
+ # :aws_created_at=>"2012-06-21T18:47:34.000Z",
95
+ # :volume_type=>"io1",
96
+ # :iops=>"5"}
97
+ #
98
+ def create_volume(snapshot_id, size, zone, options={})
77
99
  hash = { "Size" => size.to_s,
78
100
  "AvailabilityZone" => zone.to_s }
79
101
  # Get rig of empty snapshot: e8s guys do not like it
80
102
  hash["SnapshotId"] = snapshot_id.to_s unless snapshot_id.right_blank?
81
- link = generate_request("CreateVolume", hash )
103
+ # Add IOPS support (default behavior) but skip it when an old API version call is requested
104
+ options[:options] ||= {}
105
+ options[:options][:api_version] ||= VOLUME_API_VERSION
106
+ if options[:options][:api_version] >= VOLUME_API_VERSION
107
+ hash["VolumeType"] = options[:volume_type] unless options[:volume_type].right_blank?
108
+ hash["Iops"] = options[:iops] unless options[:iops].right_blank?
109
+ end
110
+ link = generate_request("CreateVolume", hash, options[:options])
82
111
  request_info(link, QEc2CreateVolumeParser.new(:logger => @logger))
83
112
  rescue Exception
84
113
  on_exception
@@ -364,6 +393,8 @@ module RightAws
364
393
  when 'size' then @result[:aws_size] = @text.to_i ###
365
394
  when 'snapshotId' then @result[:snapshot_id] = @text.right_blank? ? nil : @text ###
366
395
  when 'availabilityZone' then @result[:zone] = @text ###
396
+ when 'volumeType' then @result[:volume_type] = @text
397
+ when 'iops' then @result[:iops] = @text
367
398
  end
368
399
  end
369
400
  def reset
@@ -403,6 +434,8 @@ module RightAws
403
434
  when 'snapshotId' then @item[:snapshot_id] = @text.right_blank? ? nil : @text
404
435
  when 'availabilityZone' then @item[:zone] = @text
405
436
  when 'deleteOnTermination' then @item[:delete_on_termination] = (@text == 'true')
437
+ when 'volumeType' then @item[:volume_type] = @text
438
+ when 'iops' then @item[:iops] = @text
406
439
  else
407
440
  case full_tag_name
408
441
  when %r{/volumeSet/item/volumeId$} then @item[:aws_id] = @text
@@ -25,6 +25,8 @@ module RightAws
25
25
 
26
26
  class Ec2
27
27
 
28
+ INSTANCE_API_VERSION = (API_VERSION > '2012-07-20') ? API_VERSION : '2012-07-20'
29
+
28
30
  #-----------------------------------------------------------------
29
31
  # Instances
30
32
  #-----------------------------------------------------------------
@@ -108,6 +110,7 @@ module RightAws
108
110
  # P.S. filters: http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeInstances.html
109
111
  #
110
112
  def describe_instances(*list_and_options)
113
+ list_and_options = merge_new_options_into_list_and_options(list_and_options, :options => {:api_version => INSTANCE_API_VERSION})
111
114
  describe_resources_with_list_and_options('DescribeInstances', 'InstanceId', QEc2DescribeInstancesParser, list_and_options) do |parser|
112
115
  get_desc_instances(parser.result)
113
116
  end
@@ -158,7 +161,6 @@ module RightAws
158
161
  :group_names => group_names,
159
162
  :key_name => key_name,
160
163
  :instance_type => instance_type,
161
- :addressing_type => addressing_type,
162
164
  :kernel_id => kernel_id,
163
165
  :ramdisk_id => ramdisk_id,
164
166
  :availability_zone => availability_zone,
@@ -174,9 +176,10 @@ module RightAws
174
176
 
175
177
  # Launch new EC2 instances.
176
178
  #
177
- # Options: :image_id, :addressing_type, :min_count, max_count, :key_name, :kernel_id, :ramdisk_id,
179
+ # Options: :image_id, :min_count, max_count, :key_name, :kernel_id, :ramdisk_id,
178
180
  # :availability_zone, :monitoring_enabled, :subnet_id, :disable_api_termination, :instance_initiated_shutdown_behavior,
179
- # :block_device_mappings, :placement_group_name, :license_pool, :group_ids, :group_names, :private_ip_address
181
+ # :block_device_mappings, :placement_group_name, :license_pool, :group_ids, :group_names, :private_ip_address,
182
+ # :ebs_optimized
180
183
  #
181
184
  # Returns a list of launched instances or an exception.
182
185
  #
@@ -223,9 +226,9 @@ module RightAws
223
226
  def launch_instances(image_id, options={})
224
227
  options[:user_data] = options[:user_data].to_s
225
228
  params = map_api_keys_and_values( options,
226
- :key_name, :addressing_type, :kernel_id,
229
+ :key_name, :kernel_id,
227
230
  :ramdisk_id, :subnet_id, :instance_initiated_shutdown_behavior,
228
- :private_ip_address, :additional_info, :license_pool,
231
+ :private_ip_address, :additional_info, :license_pool, :ebs_optimized,
229
232
  :image_id => { :value => image_id },
230
233
  :min_count => { :value => options[:min_count] || 1 },
231
234
  :max_count => { :value => options[:max_count] || options[:min_count] || 1 },
@@ -243,7 +246,12 @@ module RightAws
243
246
  :value => Proc.new{ options[:monitoring_enabled] && options[:monitoring_enabled].to_s }})
244
247
  # Log debug information
245
248
  @logger.info("Launching instance of image #{image_id}. Options: #{params.inspect}")
246
- link = generate_request("RunInstances", params)
249
+ # Add IOPS support (default behavior) but skip it when an old API version call is requested
250
+ options[:options] ||= {}
251
+ options[:options][:api_version] ||= INSTANCE_API_VERSION
252
+ params.delete("EbsOptimized") if options[:options][:api_version] < INSTANCE_API_VERSION
253
+ #
254
+ link = generate_request("RunInstances", params, options[:options])
247
255
  instances = request_info(link, QEc2DescribeInstancesParser.new(:logger => @logger))
248
256
  get_desc_instances(instances)
249
257
  rescue Exception
@@ -383,7 +391,7 @@ module RightAws
383
391
  when 'GroupId' then request_hash.merge!(amazonize_list('GroupId', value))
384
392
  else request_hash["#{attribute}.Value"] = value
385
393
  end
386
- link = generate_request('ModifyInstanceAttribute', request_hash)
394
+ link = generate_request('ModifyInstanceAttribute', request_hash, :api_version => INSTANCE_API_VERSION)
387
395
  request_info(link, RightBoolResponseParser.new(:logger => @logger))
388
396
  rescue Exception
389
397
  on_exception
@@ -588,9 +596,10 @@ module RightAws
588
596
  when 'requesterId' then @item[:requester_id] = @text
589
597
  when 'virtualizationType' then @item[:virtualization_type] = @text
590
598
  when 'clientToken' then @item[:client_token] = @text
591
- when 'sourceDestCheck' then @item[:source_dest_check] = @text == 'true' ? true : false
599
+ when 'sourceDestCheck' then @item[:source_dest_check] = @text == 'true'
592
600
  when 'tenancy' then @item[:placement_tenancy] = @text
593
601
  when 'hypervisor' then @item[:hypervisor] = @text
602
+ when 'ebsOptimized' then @item[:ebs_optimized] = @text == 'true'
594
603
  else
595
604
  case full_tag_name
596
605
  # EC2 Groups
@@ -618,7 +627,7 @@ module RightAws
618
627
  when 'volumeId' then @block_device_mapping[:ebs_volume_id] = @text
619
628
  when 'status' then @block_device_mapping[:ebs_status] = @text
620
629
  when 'attachTime' then @block_device_mapping[:ebs_attach_time] = @text
621
- when 'deleteOnTermination' then @block_device_mapping[:ebs_delete_on_termination] = @text == 'true' ? true : false
630
+ when 'deleteOnTermination' then @block_device_mapping[:ebs_delete_on_termination] = @text == 'true'
622
631
  when 'item' then @item[:block_device_mappings] << @block_device_mapping
623
632
  end
624
633
  when %r{/instancesSet/item$} then @reservation[:instances_set] << @item
@@ -675,9 +684,9 @@ module RightAws
675
684
  when %r{/ramdisk/value$} then @result = @text
676
685
  when %r{/userData/value$} then @result = @text
677
686
  when %r{/rootDeviceName/value$} then @result = @text
678
- when %r{/disableApiTermination/value} then @result = @text == 'true' ? true : false
687
+ when %r{/disableApiTermination/value} then @result = @text == 'true'
679
688
  when %r{/instanceInitiatedShutdownBehavior/value$} then @result = @text
680
- when %r{/sourceDestCheck/value$} then @result = @text == 'true' ? true : false
689
+ when %r{/sourceDestCheck/value$} then @result = @text == 'true'
681
690
  when %r{/groupSet/item} # no trailing $
682
691
  case name
683
692
  when 'groupId' then @group[:group_id] = @text
@@ -692,7 +701,7 @@ module RightAws
692
701
  when 'volumeId' then @block_device_mapping[:ebs_volume_id] = @text
693
702
  when 'status' then @block_device_mapping[:ebs_status] = @text
694
703
  when 'attachTime' then @block_device_mapping[:ebs_attach_time] = @text
695
- when 'deleteOnTermination' then @block_device_mapping[:ebs_delete_on_termination] = @text == 'true' ? true : false
704
+ when 'deleteOnTermination' then @block_device_mapping[:ebs_delete_on_termination] = @text == 'true'
696
705
  when 'item' then @result << @block_device_mapping
697
706
  end
698
707
  end
@@ -124,6 +124,7 @@ module RightAws
124
124
  :additional_info => 'AdditionalInfo',
125
125
  :log_uri => 'LogUri',
126
126
  :name => 'Name',
127
+ :ami_version => 'AmiVersion',
127
128
  # JobFlowInstancesConfig
128
129
  :ec2_key_name => 'Instances.Ec2KeyName',
129
130
  :hadoop_version => 'Instances.HadoopVersion',
@@ -259,11 +259,12 @@ module RightAws
259
259
  # key = RightAws::S3::Key.create(bucket, 'logs/today/1.log')
260
260
  # key.head
261
261
  #
262
- def key(key_name, head=false)
262
+ def key(key_name, head=false, &blck)
263
263
  raise 'Key name can not be empty.' if key_name.right_blank?
264
264
  key_instance = nil
265
265
  # if this key exists - find it ....
266
266
  keys({'prefix'=>key_name}, head).each do |key|
267
+ blck.call if block_given?
267
268
  if key.name == key_name.to_s
268
269
  key_instance = key
269
270
  break
@@ -282,9 +283,9 @@ module RightAws
282
283
  #
283
284
  # bucket.put('logs/today/1.log', 'Olala!') #=> true
284
285
  #
285
- def put(key, data=nil, meta_headers={}, perms=nil, headers={})
286
+ def put(key, data=nil, meta_headers={}, perms=nil, headers={}, &blck)
286
287
  key = Key.create(self, key.to_s, data, meta_headers) unless key.is_a?(Key)
287
- key.put(data, perms, headers)
288
+ key.put(data, perms, headers, &blck)
288
289
  end
289
290
 
290
291
  # Retrieve data object from Amazon.
@@ -518,11 +519,33 @@ module RightAws
518
519
  # ...
519
520
  # key.put('Olala!') #=> true
520
521
  #
521
- def put(data=nil, perms=nil, headers={})
522
+ def put(data=nil, perms=nil, headers={}, &blck)
522
523
  headers['x-amz-acl'] = perms if perms
523
524
  @data = data || @data
524
525
  meta = self.class.add_meta_prefix(@meta_headers)
525
- @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})
526
549
  end
527
550
 
528
551
  # Rename an object. Returns new object name.
@@ -26,6 +26,8 @@ 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
 
@@ -41,15 +43,20 @@ module RightAws
41
43
  S3_REQUEST_PARAMETERS = [ 'acl',
42
44
  'location',
43
45
  'logging', # this one is beta, no support for now
46
+ 'partNumber',
44
47
  'response-content-type',
45
48
  'response-content-language',
46
49
  'response-expires',
47
50
  'response-cache-control',
48
51
  'response-content-disposition',
49
52
  'response-content-encoding',
50
- 'torrent' ].sort
51
-
53
+ 'torrent',
54
+ 'uploadId',
55
+ 'uploads',
56
+ 'delete'].sort
57
+ MULTI_OBJECT_DELETE_MAX_KEYS = 1000
52
58
 
59
+
53
60
  @@bench = AwsBenchmarkingBlock.new
54
61
  def self.bench_xml
55
62
  @@bench.xml
@@ -428,7 +435,7 @@ module RightAws
428
435
  # mode.
429
436
  #
430
437
 
431
- def put(bucket, key, data=nil, headers={})
438
+ def put(bucket, key, data=nil, headers={}, &blck)
432
439
  # On Windows, if someone opens a file in text mode, we must reset it so
433
440
  # to binary mode for streaming to work properly
434
441
  if(data.respond_to?(:binmode))
@@ -439,7 +446,7 @@ module RightAws
439
446
  headers['expect'] = '100-continue'
440
447
  end
441
448
  req_hash = generate_rest_request('PUT', headers.merge(:url=>"#{bucket}/#{CGI::escape key}", :data=>data))
442
- request_info(req_hash, RightHttp2xxParser.new)
449
+ request_info(req_hash, RightHttp2xxParser.new, &blck)
443
450
  rescue
444
451
  on_exception
445
452
  end
@@ -524,6 +531,134 @@ module RightAws
524
531
  r[:verified_md5] ? (return r) : (raise AwsError.new("Uploaded object failed MD5 checksum verification: #{r.inspect}"))
525
532
  end
526
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
+
527
662
  # Retrieves object data from Amazon. Returns a +hash+ or an exception.
528
663
  #
529
664
  # s3.get('my_awesome_bucket', 'log/curent/1.log') #=>
@@ -655,6 +790,34 @@ module RightAws
655
790
  on_exception
656
791
  end
657
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
+
658
821
  # Copy an object.
659
822
  # directive: :copy - copy meta-headers from source (default value)
660
823
  # :replace - replace meta-headers by passed ones
@@ -1021,6 +1184,23 @@ module RightAws
1021
1184
  on_exception
1022
1185
  end
1023
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
+
1024
1204
  #-----------------------------------------------------------------
1025
1205
  # PARSERS:
1026
1206
  #-----------------------------------------------------------------
@@ -43,6 +43,7 @@ Gem::Specification.new do |spec|
43
43
  spec.add_development_dependency('rake')
44
44
  spec.add_development_dependency('rcov')
45
45
 
46
+ spec.summary = 'The RightScale AWS gems have been designed to provide a robust, fast, and secure interface to Amazon EC2, EBS, S3, SQS, SDB, and CloudFront.'
46
47
  spec.description = <<-EOF
47
48
  == DESCRIPTION:
48
49
 
@@ -13,6 +13,8 @@ class TestS3 < Test::Unit::TestCase
13
13
  @key1 = 'test/woohoo1/'
14
14
  @key2 = 'test1/key/woohoo2'
15
15
  @key3 = 'test2/A%B@C_D&E?F+G=H"I'
16
+ @key4 = 'test/large_multipart_file_string'
17
+ @key5 = 'test/large_multipart_file_stream'
16
18
  @key1_copy = 'test/woohoo1_2'
17
19
  @key1_new_name = 'test/woohoo1_3'
18
20
  @key2_new_name = 'test1/key/woohoo2_new'
@@ -41,6 +43,26 @@ class TestS3 < Test::Unit::TestCase
41
43
  assert @s3.put(@bucket, @key3, RIGHT_OBJECT_TEXT, 'x-amz-meta-family'=>'Woohoo3!'), 'Put bucket fail'
42
44
  end
43
45
 
46
+ def test_04_put_multipart_string
47
+ test_text = ""
48
+ for i in 1..100000
49
+ test_text << "Testing test text #{i}\n"
50
+ end
51
+ assert @s3.store_object_multipart({:bucket => @bucket, :key => @key4, :data => StringIO.new(test_text)}), 'Put bucket multipart fail'
52
+ end
53
+
54
+ def test_04b_store_object_multipart_stream
55
+ rd, wr = IO.pipe
56
+ producer = Thread.new(wr) do |out|
57
+ for i in 1..100000
58
+ out.write("Testing stream text #{i}\n")
59
+ end
60
+ out.close
61
+ end
62
+ assert @s3.store_object_multipart({:bucket => @bucket, :key => @key5, :data => rd , :part_size => (20*1024*1024)}), 'Put bucket multipart fail'
63
+ rd.close
64
+ end
65
+
44
66
  def test_05_get_and_get_object
45
67
  assert_raise(Rightscale::AwsError) { @s3.get(@bucket, 'undefined/key') }
46
68
  data1 = @s3.get(@bucket, @key1)
@@ -74,10 +96,12 @@ class TestS3 < Test::Unit::TestCase
74
96
 
75
97
  def test_08_keys
76
98
  keys = @s3.list_bucket(@bucket).map{|b| b[:key]}
77
- assert_equal keys.size, 3, "There should be 3 keys"
99
+ assert_equal keys.size, 5, "There should be 5 keys"
78
100
  assert(keys.include?(@key1))
79
101
  assert(keys.include?(@key2))
80
102
  assert(keys.include?(@key3))
103
+ assert(keys.include?(@key4))
104
+ assert(keys.include?(@key5))
81
105
  end
82
106
 
83
107
  def test_09_copy_key
@@ -144,6 +168,7 @@ class TestS3 < Test::Unit::TestCase
144
168
 
145
169
 
146
170
 
171
+
147
172
  #---------------------------
148
173
  # Rightscale::S3 classes
149
174
  #---------------------------
@@ -408,7 +433,7 @@ class TestS3 < Test::Unit::TestCase
408
433
  sleep 10
409
434
 
410
435
  assert_equal({:enabled => true, :targetbucket => @bucket2, :targetprefix => "loggylogs/"}, bucket.logging_info)
411
-
436
+
412
437
  assert bucket.disable_logging
413
438
 
414
439
  # check 'Drop' method
@@ -457,14 +482,63 @@ class TestS3 < Test::Unit::TestCase
457
482
  end
458
483
  end
459
484
 
485
+ def test_44_delete_multiple
486
+ bucket = RightAws::S3::Bucket.create(@s, @bucket, true)
487
+
488
+ key1 = Rightscale::S3::Key.create(bucket, @key1)
489
+ key2 = Rightscale::S3::Key.create(bucket, @key2)
490
+ key3 = Rightscale::S3::Key.create(bucket, @key3)
491
+
492
+ assert @s3.put(@bucket, @key1, RIGHT_OBJECT_TEXT), 'Put bucket fail'
493
+ assert @s3.put(@bucket, @key2, RIGHT_OBJECT_TEXT), 'Put bucket fail'
494
+ assert @s3.put(@bucket, @key3, RIGHT_OBJECT_TEXT), 'Put bucket fail'
495
+
496
+ key1.refresh
497
+ key2.refresh
498
+ key3.refresh
499
+
500
+ assert key1.exists?
501
+ assert key2.exists?
502
+ assert key3.exists?
503
+
504
+ result = @s3.delete_multiple(@bucket, [@key1, @key2, @key3])
505
+ assert result.empty?
506
+
507
+ key1.refresh
508
+ key2.refresh
509
+ key3.refresh
510
+
511
+ assert !key1.exists?
512
+ assert !key2.exists?
513
+ assert !key3.exists?
514
+ end
515
+
516
+ def test_45_delete_multiple_more_than_1000_objects
517
+ n = 1200
518
+ keys = (1..n).map { |i| "key-#{i}"}
519
+
520
+ keys.each do |key|
521
+ assert @s3.put(@bucket, key, RIGHT_OBJECT_TEXT), 'Put bucket fail'
522
+ end
523
+
524
+ result = @s3.delete_multiple(@bucket, keys)
525
+ assert result.empty?
526
+
527
+ keys_after = @s3.list_bucket(@bucket).map { |obj| obj[:key] }
528
+
529
+ keys.each do |key|
530
+ assert !keys_after.include?(key)
531
+ end
532
+ end
533
+
460
534
  private
461
535
 
462
536
  def request( uri )
463
537
  url = URI.parse( uri )
464
538
 
465
- http = Net::HTTP.new(url.host, 443)
466
- http.use_ssl = true
467
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
539
+ http = Net::HTTP.new(url.host, 80)
540
+ # http.use_ssl = true
541
+ # http.verify_mode = OpenSSL::SSL::VERIFY_PEER
468
542
  http.request(Net::HTTP::Get.new( url.request_uri ))
469
543
  end
470
544
 
metadata CHANGED
@@ -1,13 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: right_aws
3
3
  version: !ruby/object:Gem::Version
4
- hash: 15
5
- prerelease:
4
+ prerelease: false
6
5
  segments:
7
6
  - 3
8
7
  - 0
9
- - 4
10
- version: 3.0.4
8
+ - 5
9
+ version: 3.0.5
11
10
  platform: ruby
12
11
  authors:
13
12
  - RightScale, Inc.
@@ -15,17 +14,16 @@ autorequire:
15
14
  bindir: bin
16
15
  cert_chain: []
17
16
 
18
- date: 2012-04-10 00:00:00 Z
17
+ date: 2013-03-05 00:00:00 -08:00
18
+ default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: right_http_connection
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
- none: false
25
24
  requirements:
26
25
  - - ">="
27
26
  - !ruby/object:Gem::Version
28
- hash: 21
29
27
  segments:
30
28
  - 1
31
29
  - 2
@@ -37,11 +35,9 @@ dependencies:
37
35
  name: rake
38
36
  prerelease: false
39
37
  requirement: &id002 !ruby/object:Gem::Requirement
40
- none: false
41
38
  requirements:
42
39
  - - ">="
43
40
  - !ruby/object:Gem::Version
44
- hash: 3
45
41
  segments:
46
42
  - 0
47
43
  version: "0"
@@ -51,11 +47,9 @@ dependencies:
51
47
  name: rcov
52
48
  prerelease: false
53
49
  requirement: &id003 !ruby/object:Gem::Requirement
54
- none: false
55
50
  requirements:
56
51
  - - ">="
57
52
  - !ruby/object:Gem::Version
58
- hash: 3
59
53
  segments:
60
54
  - 0
61
55
  version: "0"
@@ -186,6 +180,7 @@ files:
186
180
  - test/sqs/test_right_sqs_gen2.rb
187
181
  - test/test_credentials.rb
188
182
  - test/ts_right_aws.rb
183
+ has_rdoc: true
189
184
  homepage:
190
185
  licenses: []
191
186
 
@@ -198,32 +193,28 @@ rdoc_options:
198
193
  require_paths:
199
194
  - lib
200
195
  required_ruby_version: !ruby/object:Gem::Requirement
201
- none: false
202
196
  requirements:
203
197
  - - ">="
204
198
  - !ruby/object:Gem::Version
205
- hash: 57
206
199
  segments:
207
200
  - 1
208
201
  - 8
209
202
  - 7
210
203
  version: 1.8.7
211
204
  required_rubygems_version: !ruby/object:Gem::Requirement
212
- none: false
213
205
  requirements:
214
206
  - - ">="
215
207
  - !ruby/object:Gem::Version
216
- hash: 3
217
208
  segments:
218
209
  - 0
219
210
  version: "0"
220
211
  requirements:
221
212
  - libxml-ruby >= 0.5.2.0 is encouraged
222
213
  rubyforge_project: rightaws
223
- rubygems_version: 1.8.17
214
+ rubygems_version: 1.3.6
224
215
  signing_key:
225
216
  specification_version: 3
226
- summary: Interface classes for the Amazon EC2, SQS, and S3 Web Services
217
+ summary: The RightScale AWS gems have been designed to provide a robust, fast, and secure interface to Amazon EC2, EBS, S3, SQS, SDB, and CloudFront.
227
218
  test_files:
228
219
  - test/acf/test_helper.rb
229
220
  - test/acf/test_right_acf.rb