fog-aws 3.6.5 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +71 -0
- data/CHANGELOG.md +77 -2
- data/Gemfile +1 -1
- data/README.md +20 -0
- data/lib/fog/aws.rb +1 -1
- data/lib/fog/aws/credential_fetcher.rb +34 -3
- data/lib/fog/aws/models/compute/flavors.rb +460 -0
- data/lib/fog/aws/models/compute/server.rb +4 -2
- data/lib/fog/aws/models/compute/servers.rb +2 -0
- data/lib/fog/aws/models/compute/vpc.rb +1 -1
- data/lib/fog/aws/models/storage/file.rb +125 -3
- data/lib/fog/aws/models/storage/files.rb +32 -2
- data/lib/fog/aws/parsers/storage/upload_part_copy_object.rb +18 -0
- data/lib/fog/aws/requests/compute/request_spot_instances.rb +1 -1
- data/lib/fog/aws/requests/compute/run_instances.rb +20 -0
- data/lib/fog/aws/requests/compute/stop_instances.rb +11 -3
- data/lib/fog/aws/requests/storage/shared_mock_methods.rb +1 -0
- data/lib/fog/aws/requests/storage/upload_part_copy.rb +119 -0
- data/lib/fog/aws/storage.rb +2 -1
- data/lib/fog/aws/version.rb +1 -1
- data/tests/credentials_tests.rb +42 -0
- data/tests/helpers/succeeds_helper.rb +2 -2
- data/tests/models/storage/files_tests.rb +32 -0
- data/tests/requests/storage/multipart_copy_tests.rb +93 -0
- data/tests/requests/storage/object_tests.rb +7 -0
- metadata +9 -6
@@ -14,6 +14,7 @@ module Fog
|
|
14
14
|
attribute :associate_public_ip, :aliases => 'associatePublicIP'
|
15
15
|
attribute :availability_zone, :aliases => 'availabilityZone'
|
16
16
|
attribute :block_device_mapping, :aliases => 'blockDeviceMapping'
|
17
|
+
attribute :hibernation_options, :aliases => 'hibernationOptions'
|
17
18
|
attribute :network_interfaces, :aliases => 'networkInterfaces'
|
18
19
|
attribute :client_token, :aliases => 'clientToken'
|
19
20
|
attribute :disable_api_termination, :aliases => 'disableApiTermination'
|
@@ -144,6 +145,7 @@ module Fog
|
|
144
145
|
|
145
146
|
options = {
|
146
147
|
'BlockDeviceMapping' => block_device_mapping,
|
148
|
+
'HibernationOptions' => hibernation_options,
|
147
149
|
'NetworkInterfaces' => network_interfaces,
|
148
150
|
'ClientToken' => client_token,
|
149
151
|
'DisableApiTermination' => disable_api_termination,
|
@@ -227,9 +229,9 @@ module Fog
|
|
227
229
|
true
|
228
230
|
end
|
229
231
|
|
230
|
-
def stop(
|
232
|
+
def stop(options = {})
|
231
233
|
requires :id
|
232
|
-
service.stop_instances(id,
|
234
|
+
service.stop_instances(id, options)
|
233
235
|
true
|
234
236
|
end
|
235
237
|
|
@@ -22,6 +22,7 @@ module Fog
|
|
22
22
|
# ami_launch_index=nil,
|
23
23
|
# availability_zone=nil,
|
24
24
|
# block_device_mapping=nil,
|
25
|
+
# hibernation_options=nil,
|
25
26
|
# network_interfaces=nil,
|
26
27
|
# client_token=nil,
|
27
28
|
# dns_name=nil,
|
@@ -119,6 +120,7 @@ module Fog
|
|
119
120
|
# ami_launch_index=0,
|
120
121
|
# availability_zone="us-east-1b",
|
121
122
|
# block_device_mapping=[],
|
123
|
+
# hibernation_options=[],
|
122
124
|
# client_token=nil,
|
123
125
|
# dns_name="ec2-25-2-474-44.compute-1.amazonaws.com",
|
124
126
|
# groups=["default"],
|
@@ -4,6 +4,10 @@ module Fog
|
|
4
4
|
module AWS
|
5
5
|
class Storage
|
6
6
|
class File < Fog::Model
|
7
|
+
MIN_MULTIPART_CHUNK_SIZE = 5242880
|
8
|
+
MAX_SINGLE_PUT_SIZE = 5368709120
|
9
|
+
MULTIPART_COPY_THRESHOLD = 15728640
|
10
|
+
|
7
11
|
# @see AWS Object docs http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectOps.html
|
8
12
|
|
9
13
|
identity :key, :aliases => 'Key'
|
@@ -27,14 +31,54 @@ module Fog
|
|
27
31
|
attribute :kms_key_id, :aliases => 'x-amz-server-side-encryption-aws-kms-key-id'
|
28
32
|
attribute :tags, :aliases => 'x-amz-tagging'
|
29
33
|
|
34
|
+
UploadPartData = Struct.new(:part_number, :upload_options, :etag)
|
35
|
+
|
36
|
+
class PartList
|
37
|
+
def initialize(parts = [])
|
38
|
+
@parts = parts
|
39
|
+
@mutex = Mutex.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def push(part)
|
43
|
+
@mutex.synchronize { @parts.push(part) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def shift
|
47
|
+
@mutex.synchronize { @parts.shift }
|
48
|
+
end
|
49
|
+
|
50
|
+
def clear!
|
51
|
+
@mutex.synchronize { @parts.clear }
|
52
|
+
end
|
53
|
+
|
54
|
+
def size
|
55
|
+
@mutex.synchronize { @parts.size }
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_a
|
59
|
+
@mutex.synchronize { @parts.dup }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
30
63
|
# @note Chunk size to use for multipart uploads.
|
31
64
|
# Use small chunk sizes to minimize memory. E.g. 5242880 = 5mb
|
32
65
|
attr_reader :multipart_chunk_size
|
33
66
|
def multipart_chunk_size=(mp_chunk_size)
|
34
|
-
raise ArgumentError.new("minimum multipart_chunk_size is
|
67
|
+
raise ArgumentError.new("minimum multipart_chunk_size is #{MIN_MULTIPART_CHUNK_SIZE}") if mp_chunk_size < MIN_MULTIPART_CHUNK_SIZE
|
35
68
|
@multipart_chunk_size = mp_chunk_size
|
36
69
|
end
|
37
70
|
|
71
|
+
# @note Number of threads used to copy files.
|
72
|
+
def concurrency=(concurrency)
|
73
|
+
raise ArgumentError.new('minimum concurrency is 1') if concurrency.to_i < 1
|
74
|
+
|
75
|
+
@concurrency = concurrency.to_i
|
76
|
+
end
|
77
|
+
|
78
|
+
def concurrency
|
79
|
+
@concurrency || 1
|
80
|
+
end
|
81
|
+
|
38
82
|
def acl
|
39
83
|
requires :directory, :key
|
40
84
|
service.get_object_acl(directory.key, key).body['AccessControlList']
|
@@ -99,7 +143,18 @@ module Fog
|
|
99
143
|
#
|
100
144
|
def copy(target_directory_key, target_file_key, options = {})
|
101
145
|
requires :directory, :key
|
102
|
-
|
146
|
+
|
147
|
+
# With a single PUT operation you can upload objects up to 5 GB in size. Automatically set MP for larger objects.
|
148
|
+
self.multipart_chunk_size = MIN_MULTIPART_CHUNK_SIZE * 2 if !multipart_chunk_size && self.content_length.to_i > MAX_SINGLE_PUT_SIZE
|
149
|
+
|
150
|
+
if multipart_chunk_size && self.content_length.to_i >= multipart_chunk_size
|
151
|
+
upload_part_options = options.select { |key, _| ALLOWED_UPLOAD_PART_OPTIONS.include?(key.to_sym) }
|
152
|
+
upload_part_options = upload_part_options.merge({ 'x-amz-copy-source' => "#{directory.key}/#{key}" })
|
153
|
+
multipart_copy(options, upload_part_options, target_directory_key, target_file_key)
|
154
|
+
else
|
155
|
+
service.copy_object(directory.key, key, target_directory_key, target_file_key, options)
|
156
|
+
end
|
157
|
+
|
103
158
|
target_directory = service.directories.new(:key => target_directory_key)
|
104
159
|
target_directory.files.head(target_file_key)
|
105
160
|
end
|
@@ -214,7 +269,7 @@ module Fog
|
|
214
269
|
options.merge!(encryption_headers)
|
215
270
|
|
216
271
|
# With a single PUT operation you can upload objects up to 5 GB in size. Automatically set MP for larger objects.
|
217
|
-
self.multipart_chunk_size =
|
272
|
+
self.multipart_chunk_size = MIN_MULTIPART_CHUNK_SIZE if !multipart_chunk_size && Fog::Storage.get_body_size(body) > MAX_SINGLE_PUT_SIZE
|
218
273
|
|
219
274
|
if multipart_chunk_size && Fog::Storage.get_body_size(body) >= multipart_chunk_size && body.respond_to?(:read)
|
220
275
|
data = multipart_save(options)
|
@@ -294,6 +349,30 @@ module Fog
|
|
294
349
|
service.complete_multipart_upload(directory.key, key, upload_id, part_tags)
|
295
350
|
end
|
296
351
|
|
352
|
+
def multipart_copy(options, upload_part_options, target_directory_key, target_file_key)
|
353
|
+
# Initiate the upload
|
354
|
+
res = service.initiate_multipart_upload(target_directory_key, target_file_key, options)
|
355
|
+
upload_id = res.body["UploadId"]
|
356
|
+
|
357
|
+
# Store ETags of upload parts
|
358
|
+
part_tags = []
|
359
|
+
pending = PartList.new(create_part_list(upload_part_options))
|
360
|
+
thread_count = self.concurrency
|
361
|
+
completed = PartList.new
|
362
|
+
errors = upload_in_threads(target_directory_key, target_file_key, upload_id, pending, completed, thread_count)
|
363
|
+
|
364
|
+
raise errors.first if errors.any?
|
365
|
+
|
366
|
+
part_tags = completed.to_a.sort_by { |part| part.part_number }.map(&:etag)
|
367
|
+
rescue => e
|
368
|
+
# Abort the upload & reraise
|
369
|
+
service.abort_multipart_upload(target_directory_key, target_file_key, upload_id) if upload_id
|
370
|
+
raise
|
371
|
+
else
|
372
|
+
# Complete the upload
|
373
|
+
service.complete_multipart_upload(target_directory_key, target_file_key, upload_id, part_tags)
|
374
|
+
end
|
375
|
+
|
297
376
|
def encryption_headers
|
298
377
|
if encryption && encryption_key
|
299
378
|
encryption_customer_key_headers
|
@@ -318,6 +397,49 @@ module Fog
|
|
318
397
|
'x-amz-server-side-encryption-customer-key-md5' => Base64.encode64(OpenSSL::Digest::MD5.digest(encryption_key.to_s)).chomp!
|
319
398
|
}
|
320
399
|
end
|
400
|
+
|
401
|
+
def create_part_list(upload_part_options)
|
402
|
+
current_pos = 0
|
403
|
+
count = 0
|
404
|
+
pending = []
|
405
|
+
|
406
|
+
while current_pos < self.content_length do
|
407
|
+
start_pos = current_pos
|
408
|
+
end_pos = [current_pos + self.multipart_chunk_size, self.content_length - 1].min
|
409
|
+
range = "bytes=#{start_pos}-#{end_pos}"
|
410
|
+
part_options = upload_part_options.dup
|
411
|
+
part_options['x-amz-copy-source-range'] = range
|
412
|
+
pending << UploadPartData.new(count + 1, part_options, nil)
|
413
|
+
count += 1
|
414
|
+
current_pos = end_pos + 1
|
415
|
+
end
|
416
|
+
|
417
|
+
pending
|
418
|
+
end
|
419
|
+
|
420
|
+
def upload_in_threads(target_directory_key, target_file_key, upload_id, pending, completed, thread_count)
|
421
|
+
threads = []
|
422
|
+
|
423
|
+
thread_count.times do
|
424
|
+
thread = Thread.new do
|
425
|
+
begin
|
426
|
+
while part = pending.shift
|
427
|
+
part_upload = service.upload_part_copy(target_directory_key, target_file_key, upload_id, part.part_number, part.upload_options)
|
428
|
+
part.etag = part_upload.body['ETag']
|
429
|
+
completed.push(part)
|
430
|
+
end
|
431
|
+
rescue => error
|
432
|
+
pending.clear!
|
433
|
+
error
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
thread.abort_on_exception = true
|
438
|
+
threads << thread
|
439
|
+
end
|
440
|
+
|
441
|
+
threads.map(&:value).compact
|
442
|
+
end
|
321
443
|
end
|
322
444
|
end
|
323
445
|
end
|
@@ -17,6 +17,15 @@ module Fog
|
|
17
17
|
|
18
18
|
model Fog::AWS::Storage::File
|
19
19
|
|
20
|
+
DASHED_HEADERS = %w(
|
21
|
+
Cache-Control
|
22
|
+
Content-Disposition
|
23
|
+
Content-Encoding
|
24
|
+
Content-Length
|
25
|
+
Content-MD5
|
26
|
+
Content-Type
|
27
|
+
).freeze
|
28
|
+
|
20
29
|
def all(options = {})
|
21
30
|
requires :directory
|
22
31
|
options = {
|
@@ -114,8 +123,29 @@ module Fog
|
|
114
123
|
end
|
115
124
|
|
116
125
|
def normalize_headers(data)
|
117
|
-
data.headers['Last-Modified'] = Time.parse(data
|
118
|
-
|
126
|
+
data.headers['Last-Modified'] = Time.parse(fetch_and_delete_header(data, 'Last-Modified'))
|
127
|
+
|
128
|
+
etag = fetch_and_delete_header(data, 'ETag').gsub('"','')
|
129
|
+
data.headers['ETag'] = etag
|
130
|
+
|
131
|
+
DASHED_HEADERS.each do |header|
|
132
|
+
value = fetch_and_delete_header(data, header)
|
133
|
+
data.headers[header] = value if value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def fetch_and_delete_header(response, header)
|
140
|
+
value = response.get_header(header)
|
141
|
+
|
142
|
+
return unless value
|
143
|
+
|
144
|
+
response.headers.keys.each do |key|
|
145
|
+
response.headers.delete(key) if key.downcase == header.downcase
|
146
|
+
end
|
147
|
+
|
148
|
+
value
|
119
149
|
end
|
120
150
|
end
|
121
151
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Fog
|
2
|
+
module Parsers
|
3
|
+
module AWS
|
4
|
+
module Storage
|
5
|
+
class UploadPartCopyObject < Fog::Parsers::Base
|
6
|
+
def end_element(name)
|
7
|
+
case name
|
8
|
+
when 'ETag'
|
9
|
+
@response[name] = value.gsub('"', '')
|
10
|
+
when 'LastModified'
|
11
|
+
@response[name] = Time.parse(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -113,7 +113,7 @@ module Fog
|
|
113
113
|
raise Fog::AWS::Compute::Error.new(message)
|
114
114
|
end
|
115
115
|
|
116
|
-
if !image_id.match(/^ami-[a-f0-9]{8}$/)
|
116
|
+
if !image_id.match(/^ami-[a-f0-9]{8,17}$/)
|
117
117
|
message = "The image id '[#{image_id}]' does not exist"
|
118
118
|
raise Fog::AWS::Compute::NotFound.new(message)
|
119
119
|
end
|
@@ -30,6 +30,8 @@ module Fog
|
|
30
30
|
# * 'Ebs.Encrypted'<~Boolean> - specifies whether or not the volume is to be encrypted unless snapshot is specified
|
31
31
|
# * 'Ebs.VolumeType'<~String> - Type of EBS volue. Valid options in ['standard', 'io1'] default is 'standard'.
|
32
32
|
# * 'Ebs.Iops'<~String> - The number of I/O operations per second (IOPS) that the volume supports. Required when VolumeType is 'io1'
|
33
|
+
# * 'HibernationOptions'<~Array>: array of hashes
|
34
|
+
# * 'Configured'<~Boolean> - specifies whether or not the instance is configued for hibernation. This parameter is valid only if the instance meets the hibernation prerequisites.
|
33
35
|
# * 'NetworkInterfaces'<~Array>: array of hashes
|
34
36
|
# * 'NetworkInterfaceId'<~String> - An existing interface to attach to a single instance
|
35
37
|
# * 'DeviceIndex'<~String> - The device index. Applies both to attaching an existing network interface and creating a network interface
|
@@ -75,6 +77,8 @@ module Fog
|
|
75
77
|
# * 'deviceName'<~String> - specifies how volume is exposed to instance
|
76
78
|
# * 'status'<~String> - status of attached volume
|
77
79
|
# * 'volumeId'<~String> - Id of attached volume
|
80
|
+
# * 'hibernationOptions'<~Array>
|
81
|
+
# * 'configured'<~Boolean> - whether or not the instance is enabled for hibernation
|
78
82
|
# * 'dnsName'<~String> - public dns name, blank until instance is running
|
79
83
|
# * 'imageId'<~String> - image id of ami used to launch instance
|
80
84
|
# * 'instanceId'<~String> - id of the instance
|
@@ -111,6 +115,13 @@ module Fog
|
|
111
115
|
end
|
112
116
|
end
|
113
117
|
end
|
118
|
+
if hibernation_options = options.delete('HibernationOptions')
|
119
|
+
hibernation_options.each_with_index do |mapping, index|
|
120
|
+
for key, value in mapping
|
121
|
+
options.merge!({ format("HibernationOptions.%d.#{key}", index) => value })
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
114
125
|
if security_groups = options.delete('SecurityGroup')
|
115
126
|
options.merge!(Fog::AWS.indexed_param('SecurityGroup', [*security_groups]))
|
116
127
|
end
|
@@ -182,6 +193,14 @@ module Fog
|
|
182
193
|
}
|
183
194
|
end
|
184
195
|
|
196
|
+
hibernation_options = (options['HibernationOptions'] || []).reduce([]) do |mapping, device|
|
197
|
+
configure = device.fetch("Configure", true)
|
198
|
+
|
199
|
+
mapping << {
|
200
|
+
"Configure" => configure,
|
201
|
+
}
|
202
|
+
end
|
203
|
+
|
185
204
|
if options['SubnetId']
|
186
205
|
if options['PrivateIpAddress']
|
187
206
|
ni_options = {'PrivateIpAddress' => options['PrivateIpAddress']}
|
@@ -221,6 +240,7 @@ module Fog
|
|
221
240
|
'associatePublicIP' => options['associatePublicIP'] || false,
|
222
241
|
'architecture' => 'i386',
|
223
242
|
'blockDeviceMapping' => block_device_mapping,
|
243
|
+
'hibernationOptions' => hibernation_options,
|
224
244
|
'networkInterfaces' => network_interfaces,
|
225
245
|
'clientToken' => options['clientToken'],
|
226
246
|
'dnsName' => nil,
|
@@ -16,9 +16,17 @@ module Fog
|
|
16
16
|
# * TODO: fill in the blanks
|
17
17
|
#
|
18
18
|
# {Amazon API Reference}[http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/ApiReference-query-StopInstances.html]
|
19
|
-
def stop_instances(instance_id,
|
19
|
+
def stop_instances(instance_id, options = {})
|
20
20
|
params = Fog::AWS.indexed_param('InstanceId', instance_id)
|
21
|
-
|
21
|
+
unless options.is_a?(Hash)
|
22
|
+
Fog::Logger.warning("stop_instances with #{options.class} param is deprecated, use stop_instances('force' => boolean) instead [light_black](#{caller.first})[/]")
|
23
|
+
options = {'force' => options}
|
24
|
+
end
|
25
|
+
params.merge!('Force' => 'true') if options['force']
|
26
|
+
if options['hibernate']
|
27
|
+
params.merge!('Hibernate' => 'true')
|
28
|
+
params.merge!('Force' => 'false')
|
29
|
+
end
|
22
30
|
request({
|
23
31
|
'Action' => 'StopInstances',
|
24
32
|
:idempotent => true,
|
@@ -28,7 +36,7 @@ module Fog
|
|
28
36
|
end
|
29
37
|
|
30
38
|
class Mock
|
31
|
-
def stop_instances(instance_id,
|
39
|
+
def stop_instances(instance_id, options = {})
|
32
40
|
instance_ids = Array(instance_id)
|
33
41
|
|
34
42
|
instance_set = self.data[:instances].values
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Fog
|
2
|
+
module AWS
|
3
|
+
class Storage
|
4
|
+
# From https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPartCopy.html
|
5
|
+
ALLOWED_UPLOAD_PART_OPTIONS = %i(
|
6
|
+
x-amz-copy-source
|
7
|
+
x-amz-copy-source-if-match
|
8
|
+
x-amz-copy-source-if-modified-since
|
9
|
+
x-amz-copy-source-if-none-match
|
10
|
+
x-amz-copy-source-if-unmodified-since
|
11
|
+
x-amz-copy-source-range
|
12
|
+
x-amz-copy-source-server-side-encryption-customer-algorithm
|
13
|
+
x-amz-copy-source-server-side-encryption-customer-key
|
14
|
+
x-amz-copy-source-server-side-encryption-customer-key-MD5
|
15
|
+
x-amz-expected-bucket-owner
|
16
|
+
x-amz-request-payer
|
17
|
+
x-amz-server-side-encryption-customer-algorithm
|
18
|
+
x-amz-server-side-encryption-customer-key
|
19
|
+
x-amz-server-side-encryption-customer-key-MD5
|
20
|
+
x-amz-source-expected-bucket-owner
|
21
|
+
).freeze
|
22
|
+
|
23
|
+
class Real
|
24
|
+
require 'fog/aws/parsers/storage/upload_part_copy_object'
|
25
|
+
|
26
|
+
# Upload a part for a multipart copy
|
27
|
+
#
|
28
|
+
# @param target_bucket_name [String] Name of bucket to create copy in
|
29
|
+
# @param target_object_name [String] Name for new copy of object
|
30
|
+
# @param upload_id [String] Id of upload to add part to
|
31
|
+
# @param part_number [String] Index of part in upload
|
32
|
+
# @param options [Hash]:
|
33
|
+
# @option options [String] x-amz-metadata-directive Specifies whether to copy metadata from source or replace with data in request. Must be in ['COPY', 'REPLACE']
|
34
|
+
# @option options [String] x-amz-copy_source-if-match Copies object if its etag matches this value
|
35
|
+
# @option options [Time] x-amz-copy_source-if-modified_since Copies object it it has been modified since this time
|
36
|
+
# @option options [String] x-amz-copy_source-if-none-match Copies object if its etag does not match this value
|
37
|
+
# @option options [Time] x-amz-copy_source-if-unmodified-since Copies object it it has not been modified since this time
|
38
|
+
# @option options [Time] x-amz-copy-source-range Specifes the range of bytes to copy from the source object
|
39
|
+
#
|
40
|
+
# @return [Excon::Response]
|
41
|
+
# * body [Hash]:
|
42
|
+
# * ETag [String] - etag of new object
|
43
|
+
# * LastModified [Time] - date object was last modified
|
44
|
+
#
|
45
|
+
# @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPartCopy.html
|
46
|
+
#
|
47
|
+
def upload_part_copy(target_bucket_name, target_object_name, upload_id, part_number, options = {})
|
48
|
+
headers = options
|
49
|
+
request({
|
50
|
+
:expects => 200,
|
51
|
+
:idempotent => true,
|
52
|
+
:headers => headers,
|
53
|
+
:bucket_name => target_bucket_name,
|
54
|
+
:object_name => target_object_name,
|
55
|
+
:method => 'PUT',
|
56
|
+
:query => {'uploadId' => upload_id, 'partNumber' => part_number},
|
57
|
+
:parser => Fog::Parsers::AWS::Storage::UploadPartCopyObject.new,
|
58
|
+
})
|
59
|
+
end
|
60
|
+
end # Real
|
61
|
+
|
62
|
+
class Mock # :nodoc:all
|
63
|
+
require 'fog/aws/requests/storage/shared_mock_methods'
|
64
|
+
include Fog::AWS::Storage::SharedMockMethods
|
65
|
+
|
66
|
+
def upload_part_copy(target_bucket_name, target_object_name, upload_id, part_number, options = {})
|
67
|
+
validate_options!(options)
|
68
|
+
|
69
|
+
copy_source = options['x-amz-copy-source']
|
70
|
+
copy_range = options['x-amz-copy-source-range']
|
71
|
+
|
72
|
+
raise 'No x-amz-copy-source header provided' unless copy_source
|
73
|
+
raise 'No x-amz-copy-source-range header provided' unless copy_range
|
74
|
+
|
75
|
+
source_bucket_name, source_object_name = copy_source.split('/', 2)
|
76
|
+
verify_mock_bucket_exists(source_bucket_name)
|
77
|
+
|
78
|
+
source_bucket = self.data[:buckets][source_bucket_name]
|
79
|
+
source_object = source_bucket && source_bucket[:objects][source_object_name] && source_bucket[:objects][source_object_name].first
|
80
|
+
upload_info = get_upload_info(target_bucket_name, upload_id)
|
81
|
+
|
82
|
+
response = Excon::Response.new
|
83
|
+
|
84
|
+
if source_object
|
85
|
+
start_pos, end_pos = byte_range(copy_range, source_object[:body].length)
|
86
|
+
upload_info[:parts][part_number] = source_object[:body][start_pos..end_pos]
|
87
|
+
|
88
|
+
response.status = 200
|
89
|
+
response.body = {
|
90
|
+
# just use the part number as the ETag, for simplicity
|
91
|
+
'ETag' => part_number.to_i,
|
92
|
+
'LastModified' => Time.parse(source_object['Last-Modified'])
|
93
|
+
}
|
94
|
+
response
|
95
|
+
else
|
96
|
+
response.status = 404
|
97
|
+
raise(Excon::Errors.status_error({:expects => 200}, response))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def byte_range(range, size)
|
102
|
+
matches = range.match(/bytes=(\d*)-(\d*)/)
|
103
|
+
|
104
|
+
return nil unless matches
|
105
|
+
|
106
|
+
end_pos = [matches[2].to_i, size].min
|
107
|
+
|
108
|
+
[matches[1].to_i, end_pos]
|
109
|
+
end
|
110
|
+
|
111
|
+
def validate_options!(options)
|
112
|
+
options.keys.each do |key|
|
113
|
+
raise "Invalid UploadPart option: #{key}" unless ::Fog::AWS::Storage::ALLOWED_UPLOAD_PART_OPTIONS.include?(key.to_sym)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end # Mock
|
117
|
+
end # Storage
|
118
|
+
end # AWS
|
119
|
+
end # Fog
|