right_aws 3.0.4 → 3.0.5

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.
@@ -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