[aws|storage] first pass at multipart uploads

This commit is contained in:
geemus 2010-11-15 17:17:37 -08:00
parent cd7a6771e1
commit 6d7f859c34
15 changed files with 635 additions and 11 deletions

View File

@ -0,0 +1,24 @@
module Fog
module Parsers
module AWS
module Storage
class CompleteMultipartUpload < Fog::Parsers::Base
def reset
@response = {}
end
def end_element(name)
case name
when 'Bucket', 'ETag', 'Key', 'Location'
@response[name] = @value
end
end
end
end
end
end
end

View File

@ -0,0 +1,24 @@
module Fog
module Parsers
module AWS
module Storage
class InitiateMultipartUpload < Fog::Parsers::Base
def reset
@response = {}
end
def end_element(name)
case name
when 'Bucket', 'Key', 'UploadId'
@response[name] = @value
end
end
end
end
end
end
end

View File

@ -0,0 +1,56 @@
module Fog
module Parsers
module AWS
module Storage
class ListMultipartUploads < Fog::Parsers::Base
def reset
@upload = { 'Initiator' => {}, 'Owner' => {} }
@response = { 'Upload' => [] }
end
def start_element(name, attrs = [])
super
case name
when 'Initiator'
@in_initiator = true
when 'Owner'
@in_owner = true
end
end
def end_element(name)
case name
when 'Bucket', 'KeyMarker', 'NextKeyMarker', 'NextUploadIdMarker', 'UploadIdMarker'
@response[name] = @value
when 'DisplayName', 'ID'
if @in_initiator
@upload['Initiator'][name] = @value
elsif @in_owner
@upload['Owner'][name] = @value
end
when 'Initiated'
@upload[name] = Time.parse(@value)
when 'Initiator'
@in_initiator = false
when 'IsTruncated'
@response[name] = @value == 'true'
when 'Key', 'StorageClass', 'UploadId'
@upload[name] = @value
when 'MaxUploads'
@response[name] = @value.to_i
when 'Owner'
@in_owner = false
when 'Upload'
@response['Upload'] << @upload
@upload = { 'Initiator' => {}, 'Owner' => {} }
end
end
end
end
end
end
end

View File

@ -0,0 +1,40 @@
module Fog
module Parsers
module AWS
module Storage
class ListParts < Fog::Parsers::Base
def reset
@part = {}
@response = { 'Initiator' => {}, 'Part' => [] }
end
def end_element(name)
case name
when 'Bucket', 'Key', 'NextPartNumberMarker', 'PartNumberMarker', 'StorageClass', 'UploadId'
@response[name] = @value
when 'DisplayName', 'ID'
@response['Initiator'][name] = @value
when 'ETag'
@part[name] = @value
when 'IsTruncated'
@response[name] = @value == 'true'
when 'LastModified'
@part[name] = Time.parse(@value)
when 'MaxParts'
@response[name] = @value.to_i
when 'Part'
@response['Part'] << @part
@part = {}
when 'PartNumber', 'Size'
@part[name] = @value.to_i
end
end
end
end
end
end
end

View File

@ -0,0 +1,38 @@
module Fog
module AWS
class Storage
class Real
# Abort a multipart upload
#
# ==== Parameters
# * bucket_name<~String> - Name of bucket to abort multipart upload on
# * object_name<~String> - Name of object to abort multipart upload on
# * upload_id<~String> - Id of upload to add part to
#
# ==== See Also
# http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadAbort.html
#
def abort_multipart_upload(bucket_name, object_name, upload_id)
request({
:expects => 204,
:headers => {},
:host => "#{bucket_name}.#{@host}",
:method => 'DELETE',
:path => CGI.escape(object_name),
:query => {'uploadId' => upload_id}
})
end
end # Real
class Mock # :nodoc:all
def abort_multipart_upload(bucket_name, object_name, upload_id)
Fog::Mock.not_implemented
end
end # Mock
end # Storage
end # AWS
end # Fog

View File

@ -0,0 +1,60 @@
module Fog
module AWS
class Storage
class Real
require 'fog/aws/parsers/storage/complete_multipart_upload'
# Complete a multipart upload
#
# ==== Parameters
# * bucket_name<~String> - Name of bucket to complete multipart upload for
# * object_name<~String> - Name of object to complete multipart upload for
# * upload_id<~String> - Id of upload to add part to
# * parts<~Array>: Array of etags for parts
# * :etag<~String> - Etag for this part
#
# ==== Returns
# * response<~Excon::Response>:
# * headers<~Hash>:
# * 'Bucket'<~String> - bucket of new object
# * 'ETag'<~String> - etag of new object (will be needed to complete upload)
# * 'Key'<~String> - key of new object
# * 'Location'<~String> - location of new object
#
# ==== See Also
# http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadComplete.html
#
def complete_multipart_upload(bucket_name, object_name, upload_id, parts)
data = "<CompleteMultipartUpload>"
parts.each_with_index do |part, index|
data << "<Part>"
data << "<PartNumber>#{index + 1}</PartNumber>"
data << "<ETag>#{part}</ETag>"
data << "</Part>"
end
data << "</CompleteMultipartUpload>"
request({
:body => data,
:expects => 200,
:headers => { 'Content-Length' => data.length },
:host => "#{bucket_name}.#{@host}",
:method => 'POST',
:parser => Fog::Parsers::AWS::Storage::CompleteMultipartUpload.new,
:path => CGI.escape(object_name),
:query => {'uploadId' => upload_id}
})
end
end # Real
class Mock # :nodoc:all
def complete_multipart_upload(bucket_name, object_name, upload_id, parts)
Fog::Mock.not_implemented
end
end # Mock
end # Storage
end # AWS
end # Fog

View File

@ -19,7 +19,7 @@ module Fog
#
# ==== See Also
# http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTServiceGET.html
#
def get_service
request({
:expects => 200,

View File

@ -0,0 +1,55 @@
module Fog
module AWS
class Storage
class Real
require 'fog/aws/parsers/storage/initiate_multipart_upload'
# Initiate a multipart upload to an S3 bucket
#
# ==== Parameters
# * bucket_name<~String> - Name of bucket to create object in
# * object_name<~String> - Name of object to create
# * options<~Hash>:
# * 'Cache-Control'<~String> - Caching behaviour
# * 'Content-Disposition'<~String> - Presentational information for the object
# * 'Content-Encoding'<~String> - Encoding of object data
# * 'Content-MD5'<~String> - Base64 encoded 128-bit MD5 digest of message (defaults to Base64 encoded MD5 of object.read)
# * 'Content-Type'<~String> - Standard MIME type describing contents (defaults to MIME::Types.of.first)
# * 'x-amz-acl'<~String> - Permissions, must be in ['private', 'public-read', 'public-read-write', 'authenticated-read']
# * "x-amz-meta-#{name}" - Headers to be returned with object, note total size of request without body must be less than 8 KB.
#
# ==== Returns
# * response<~Excon::Response>:
# * body<~Hash>:
# * 'Bucket'<~String> - Bucket where upload was initiated
# * 'Key'<~String> - Object key where the upload was initiated
# * 'UploadId'<~String> - Id for initiated multipart upload
#
# ==== See Also
# http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html
#
def initiate_multipart_upload(bucket_name, object_name, options = {})
request({
:expects => 200,
:headers => options,
:host => "#{bucket_name}.#{@host}",
:method => 'POST',
:parser => Fog::Parsers::AWS::Storage::InitiateMultipartUpload.new,
:path => CGI.escape(object_name),
:query => {'uploads' => nil}
})
end
end # Real
class Mock # :nodoc:all
def initiate_multipart_upload(bucket_name, object_name, options = {})
Fog::Mock.not_implemented
end
end # Mock
end # Storage
end # AWS
end # Fog

View File

@ -0,0 +1,68 @@
module Fog
module AWS
class Storage
class Real
require 'fog/aws/parsers/storage/list_multipart_uploads'
# List multipart uploads for a bucket
#
# ==== Parameters
# * bucket_name<~String> - Name of bucket to list multipart uploads for
# * upload_id<~String> - upload id to list objects for
# * options<~Hash> - config arguments for list. Defaults to {}.
# * 'key-marker'<~String> - limits parts to only those that appear
# lexicographically after this key.
# * 'max-uploads'<~Integer> - limits number of uploads returned
# * 'upload-id-marker'<~String> - limits uploads to only those that appear
# lexicographically after this upload id.
#
# ==== Returns
# * response<~Excon::Response>:
# * body<~Hash>:
# * 'Bucket'<~string> - Bucket where the multipart upload was initiated
# * 'IsTruncated'<~Boolean> - Whether or not the listing is truncated
# * 'KeyMarker'<~String> - first key in list, only upload ids after this lexographically will appear
# * 'MaxUploads'<~Integer> - Maximum results to return
# * 'NextKeyMarker'<~String> - last key in list, for further pagination
# * 'NextUploadIdMarker'<~String> - last key in list, for further pagination
# * 'Upload'<~Hash>:
# * 'Initiated'<~Time> - Time when upload was initiated
# * 'Initiator'<~Hash>:
# * 'DisplayName'<~String> - Display name of upload initiator
# * 'ID'<~String> - Id of upload initiator
# * 'Key'<~String> - Key where multipart upload was initiated
# * 'Owner'<~Hash>:
# * 'DisplayName'<~String> - Display name of upload owner
# * 'ID'<~String> - Id of upload owner
# * 'StorageClass'<~String> - Storage class of object
# * 'UploadId'<~String> - upload id of upload containing part
# * 'UploadIdMarker'<String> - first key in list, only upload ids after this lexographically will appear
#
# ==== See Also
# http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadListMPUpload.html
#
def list_multipart_uploads(bucket_name, options = {})
request({
:expects => 200,
:headers => {},
:host => "#{bucket_name}.#{@host}",
:idempotent => true,
:method => 'GET',
:parser => Fog::Parsers::AWS::Storage::ListMultipartUploads.new,
:query => options.merge!({'uploads' => nil})
})
end
end
class Mock # :nodoc:all
def list_multipart_uploads(bucket_name, options = {})
Fog::Mock.not_implemented
end
end
end
end
end

View File

@ -0,0 +1,67 @@
module Fog
module AWS
class Storage
class Real
require 'fog/aws/parsers/storage/list_parts'
# List parts for a multipart upload
#
# ==== Parameters
# * bucket_name<~String> - Name of bucket to list parts for
# * object_name<~String> - Name of object to list parts for
# * upload_id<~String> - upload id to list objects for
# * options<~Hash> - config arguments for list. Defaults to {}.
# * 'max-parts'<~Integer> - limits number of parts returned
# * 'part-number-marker'<~String> - limits parts to only those that appear
# lexicographically after this part number.
#
# ==== Returns
# * response<~Excon::Response>:
# * body<~Hash>:
# * 'Bucket'<~string> - Bucket where the multipart upload was initiated
# * 'Initiator'<~Hash>:
# * 'DisplayName'<~String> - Display name of upload initiator
# * 'ID'<~String> - Id of upload initiator
# * 'IsTruncated'<~Boolean> - Whether or not the listing is truncated
# * 'Key'<~String> - Key where multipart upload was initiated
# * 'MaxParts'<~String> - maximum number of replies alllowed in response
# * 'NextPartNumberMarker'<~String> - last item in list, for further pagination
# * 'Part'<~Array>:
# * 'ETag'<~String> - ETag of part
# * 'LastModified'<~Timestamp> - Last modified for part
# * 'PartNumber'<~String> - Part number for part
# * 'Size'<~Integer> - Size of part
# * 'PartNumberMarker'<~String> - Part number after which listing begins
# * 'StorageClass'<~String> - Storage class of object
# * 'UploadId'<~String> - upload id of upload containing part
#
# ==== See Also
# http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadListParts.html
#
def list_parts(bucket_name, object_name, upload_id, options = {})
options['uploadId'] = upload_id
request({
:expects => 200,
:headers => {},
:host => "#{bucket_name}.#{@host}",
:idempotent => true,
:method => 'GET',
:parser => Fog::Parsers::AWS::Storage::ListParts.new,
:path => CGI.escape(object_name),
:query => options.merge!({'uploadId' => upload_id})
})
end
end
class Mock # :nodoc:all
def list_parts(bucket_name, object_name, upload_id, options = {})
Fog::Mock.not_implemented
end
end
end
end
end

View File

@ -8,13 +8,13 @@ module Fog
# ==== Parameters
# * bucket_name<~String> - Name of bucket to create object in
# * object_name<~String> - Name of object to create
# * data<~File> - File or String to create object from
# * data<~File||String> - File or String to create object from
# * options<~Hash>:
# * 'Cache-Control'<~String> - Caching behaviour
# * 'Content-Disposition'<~String> - Presentational information for the object
# * 'Content-Encoding'<~String> - Encoding of object data
# * 'Content-Length'<~String> - Size of object in bytes (defaults to object.read.length)
# * 'Content-MD5'<~String> - Base64 encoded 128-bit MD5 digest of message (defaults to Base64 encoded MD5 of object.read)
# * 'Content-MD5'<~String> - Base64 encoded 128-bit MD5 digest of message
# * 'Content-Type'<~String> - Standard MIME type describing contents (defaults to MIME::Types.of.first)
# * 'x-amz-acl'<~String> - Permissions, must be in ['private', 'public-read', 'public-read-write', 'authenticated-read']
# * "x-amz-meta-#{name}" - Headers to be returned with object, note total size of request without body must be less than 8 KB.
@ -26,7 +26,7 @@ module Fog
#
# ==== See Also
# http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectPUT.html
#
def put_object(bucket_name, object_name, data, options = {})
data = parse_data(data)
headers = data[:headers].merge!(options)

View File

@ -0,0 +1,51 @@
module Fog
module AWS
class Storage
class Real
# Upload a part for a multipart upload
#
# ==== Parameters
# * bucket_name<~String> - Name of bucket to add part to
# * object_name<~String> - Name of object to add part to
# * upload_id<~String> - Id of upload to add part to
# * part_number<~String> - Index of part in upload
# * data<~File||String> - Content for part
# * options<~Hash>:
# * 'Content-MD5'<~String> - Base64 encoded 128-bit MD5 digest of message
#
# ==== Returns
# * response<~Excon::Response>:
# * headers<~Hash>:
# * 'ETag'<~String> - etag of new object (will be needed to complete upload)
#
# ==== See Also
# http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadUploadPart.html
#
def upload_part(bucket_name, object_name, upload_id, part_number, data, options = {})
data = parse_data(data)
headers = options
headers['Content-Length'] = data[:headers]['Content-Length']
request({
:body => data[:body],
:expects => 200,
:headers => headers,
:host => "#{bucket_name}.#{@host}",
:method => 'PUT',
:path => CGI.escape(object_name),
:query => {'uploadId' => upload_id, 'partNumber' => part_number}
})
end
end # Real
class Mock # :nodoc:all
def upload_part(bucket_name, object_name, upload_id, part_number, data, options = {})
Fog::Mock.not_implemented
end
end # Mock
end # Storage
end # AWS
end # Fog

View File

@ -11,6 +11,8 @@ module Fog
model :file
request_path 'fog/aws/requests/storage'
request :abort_multipart_upload
request :complete_multipart_upload
request :copy_object
request :delete_bucket
request :delete_object
@ -27,6 +29,9 @@ module Fog
request :get_request_payment
request :get_service
request :head_object
request :initiate_multipart_upload
request :list_multipart_uploads
request :list_parts
request :post_object_hidden_fields
request :put_bucket
request :put_bucket_acl
@ -35,6 +40,7 @@ module Fog
request :put_object
request :put_object_url
request :put_request_payment
request :upload_part
module Utils
@ -219,7 +225,7 @@ DATA
for key, value in amz_headers
canonical_amz_headers << "#{key}:#{value}\n"
end
string_to_sign << "#{canonical_amz_headers}"
string_to_sign << canonical_amz_headers
subdomain = params[:host].split(".#{@host}").first
unless subdomain =~ /^(?:[a-z]|\d(?!\d{0,2}(?:\.\d{1,3}){3}$))(?:[a-z0-9]|\.(?![\.\-])|\-(?![\.])){1,61}[a-z0-9]$/
@ -228,7 +234,7 @@ DATA
if params[:path]
params[:path] = "#{subdomain}/#{params[:path]}"
else
params[:path] = "#{subdomain}"
params[:path] = subdomain
end
subdomain = nil
end
@ -237,15 +243,15 @@ DATA
unless subdomain.nil? || subdomain == @host
canonical_resource << "#{CGI.escape(subdomain).downcase}/"
end
canonical_resource << "#{params[:path]}"
canonical_resource << params[:path].to_s
canonical_resource << '?'
for key in (params[:query] || {}).keys
if ['acl', 'location', 'logging', 'requestPayment', 'torrent', 'versions', 'versioning'].include?(key)
canonical_resource << "#{key}&"
if %w{acl location logging notification partNumber policy requestPayment torrent uploadId uploads versionId versioning versions}.include?(key)
canonical_resource << "#{key}#{"=#{params[:query][key]}" unless params[:query][key].nil?}&"
end
end
canonical_resource.chop!
string_to_sign << "#{canonical_resource}"
string_to_sign << canonical_resource
signed_string = @hmac.sign(string_to_sign)
signature = Base64.encode64(signed_string).chomp!

View File

@ -0,0 +1,135 @@
Shindo.tests('AWS::Storage | object requests', ['aws']) do
@directory = AWS[:storage].directories.create(:key => 'fogmultipartuploadtests')
tests('success') do
@initiate_multipart_upload_format = {
'Bucket' => String,
'Key' => String,
'UploadId' => String
}
tests("#initiate_multipart_upload('#{@directory.identity}')", 'fog_multipart_upload').formats(@initiate_multipart_upload_format) do
pending if Fog.mocking?
data = AWS[:storage].initiate_multipart_upload(@directory.identity, 'fog_multipart_upload').body
@upload_id = data['UploadId']
data
end
@list_multipart_uploads_format = {
'Bucket' => String,
'IsTruncated' => Fog::Boolean,
'MaxUploads' => Integer,
'KeyMarker' => NilClass,
'NextKeyMarker' => String,
'NextUploadIdMarker' => String,
'Upload' => [{
'Initiated' => Time,
'Initiator' => {
'DisplayName' => String,
'ID' => String
},
'Key' => String,
'Owner' => {
'DisplayName' => String,
'ID' => String
},
'StorageClass' => String,
'UploadId' => String
}],
'UploadIdMarker' => NilClass,
}
tests("#list_multipart_uploads('#{@directory.identity})").formats(@list_multipart_uploads_format) do
pending if Fog.mocking?
AWS[:storage].list_multipart_uploads(@directory.identity).body
end
@parts = []
tests("#upload_part('#{@directory.identity}', 'fog_multipart_upload', '#{@upload_id}', 1, ('x' * 6 * 1024 * 1024))").succeeds do
pending if Fog.mocking?
data = AWS[:storage].upload_part(@directory.identity, 'fog_multipart_upload', @upload_id, 1, ('x' * 6 * 1024 * 1024))
@parts << data.headers['ETag']
end
@list_parts_format = {
'Bucket' => String,
'Initiator' => {
'DisplayName' => String,
'ID' => String
},
'IsTruncated' => Fog::Boolean,
'Key' => String,
'MaxParts' => Integer,
'NextPartNumberMarker' => String,
'Part' => [{
'ETag' => String,
'LastModified' => Time,
'PartNumber' => Integer,
'Size' => Integer
}],
'PartNumberMarker' => String,
'StorageClass' => String,
'UploadId' => String
}
tests("#list_parts('#{@directory.identity}', 'fog_multipart_upload', '#{@upload_id}')").formats(@list_parts_format) do
pending if Fog.mocking?
AWS[:storage].list_parts(@directory.identity, 'fog_multipart_upload', @upload_id).body
end
if !Fog.mocking?
@parts << AWS[:storage].upload_part(@directory.identity, 'fog_multipart_upload', @upload_id, 2, ('x' * 4 * 1024 * 1024)).headers['ETag']
end
@complete_multipart_upload_format = {
'Bucket' => String,
'ETag' => String,
'Key' => String,
'Location' => String
}
tests("#complete_multipart_upload('#{@directory.identity}', 'fog_multipart_upload', '#{@upload_id}', #{@parts.inspect})").formats(@complete_multipart_upload_format) do
pending if Fog.mocking?
AWS[:storage].complete_multipart_upload(@directory.identity, 'fog_multipart_upload', @upload_id, @parts).body
end
tests("#get_object('#{@directory.identity}', 'fog_multipart_upload').body").succeeds do
pending if Fog.mocking?
data = AWS[:storage].get_object(@directory.identity, 'fog_multipart_upload').body
unless data == ('x' * 10 * 1024 * 1024)
raise 'content mismatch'
end
end
if !Fog.mocking?
@directory.files.new(:key => 'fog_multipart_upload').destroy
end
if !Fog.mocking?
@upload_id = AWS[:storage].initiate_multipart_upload(@directory.identity, 'fog_multipart_abort').body['UploadId']
end
tests("#abort_multipart_upload('#{@directory.identity}', 'fog_multipart_abort', '#{@upload_id}')").succeeds do
pending if Fog.mocking?
AWS[:storage].abort_multipart_upload(@directory.identity, 'fog_multipart_abort', @upload_id)
end
end
tests('failure') do
tests("initiate_multipart_upload")
tests("list_multipart_uploads")
tests("upload_part")
tests("list_parts")
tests("complete_multipart_upload")
tests("abort_multipart_upload")
end
@directory.destroy
end

View File

@ -49,7 +49,7 @@ module Shindo
def formats(format)
test('has proper format') do
formats_kernel(instance_eval(&block), format)
formats_kernel(instance_eval(&Proc.new), format)
end
end