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.
- data/History.txt +10 -0
- data/lib/awsbase/right_awsbase.rb +7 -1
- data/lib/awsbase/version.rb +1 -1
- data/lib/ec2/right_ec2.rb +14 -2
- data/lib/ec2/right_ec2_ebs.rb +35 -2
- data/lib/ec2/right_ec2_instances.rb +21 -12
- data/lib/emr/right_emr_interface.rb +1 -0
- data/lib/s3/right_s3.rb +28 -5
- data/lib/s3/right_s3_interface.rb +184 -4
- data/right_aws.gemspec +1 -0
- data/test/s3/test_right_s3.rb +79 -5
- metadata +8 -17
data/History.txt
CHANGED
@@ -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
|
-
#
|
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
|
data/lib/awsbase/version.rb
CHANGED
data/lib/ec2/right_ec2.rb
CHANGED
@@ -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
|
#-----------------------------------------------------------------
|
data/lib/ec2/right_ec2_ebs.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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, :
|
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, :
|
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
|
-
|
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'
|
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'
|
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'
|
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'
|
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'
|
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',
|
data/lib/s3/right_s3.rb
CHANGED
@@ -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'
|
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
|
#-----------------------------------------------------------------
|
data/right_aws.gemspec
CHANGED
@@ -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
|
|
data/test/s3/test_right_s3.rb
CHANGED
@@ -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,
|
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,
|
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
|
-
|
5
|
-
prerelease:
|
4
|
+
prerelease: false
|
6
5
|
segments:
|
7
6
|
- 3
|
8
7
|
- 0
|
9
|
-
-
|
10
|
-
version: 3.0.
|
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:
|
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.
|
214
|
+
rubygems_version: 1.3.6
|
224
215
|
signing_key:
|
225
216
|
specification_version: 3
|
226
|
-
summary:
|
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
|