mirror of
https://github.com/fog/fog.git
synced 2022-11-09 13:51:43 -05:00
Merge pull request #2191 from burns/openstack_bulk_delete_slo
[openstack|storage] add bulk delete and SLO support
This commit is contained in:
commit
dc545e42f2
8 changed files with 613 additions and 99 deletions
|
@ -0,0 +1,67 @@
|
||||||
|
module Fog
|
||||||
|
module Storage
|
||||||
|
class OpenStack
|
||||||
|
class Real
|
||||||
|
|
||||||
|
# Deletes multiple objects or containers with a single request.
|
||||||
|
#
|
||||||
|
# To delete objects from a single container, +container+ may be provided
|
||||||
|
# and +object_names+ should be an Array of object names within the container.
|
||||||
|
#
|
||||||
|
# To delete objects from multiple containers or delete containers,
|
||||||
|
# +container+ should be +nil+ and all +object_names+ should be prefixed with a container name.
|
||||||
|
#
|
||||||
|
# Containers must be empty when deleted. +object_names+ are processed in the order given,
|
||||||
|
# so objects within a container should be listed first to empty the container.
|
||||||
|
#
|
||||||
|
# Up to 10,000 objects may be deleted in a single request.
|
||||||
|
# The server will respond with +200 OK+ for all requests.
|
||||||
|
# +response.body+ must be inspected for actual results.
|
||||||
|
#
|
||||||
|
# @example Delete objects from a container
|
||||||
|
# object_names = ['object', 'another/object']
|
||||||
|
# conn.delete_multiple_objects('my_container', object_names)
|
||||||
|
#
|
||||||
|
# @example Delete objects from multiple containers
|
||||||
|
# object_names = ['container_a/object', 'container_b/object']
|
||||||
|
# conn.delete_multiple_objects(nil, object_names)
|
||||||
|
#
|
||||||
|
# @example Delete a container and all it's objects
|
||||||
|
# object_names = ['my_container/object_a', 'my_container/object_b', 'my_container']
|
||||||
|
# conn.delete_multiple_objects(nil, object_names)
|
||||||
|
#
|
||||||
|
# @param container [String,nil] Name of container.
|
||||||
|
# @param object_names [Array<String>] Object names to be deleted.
|
||||||
|
# @param options [Hash] Additional request headers.
|
||||||
|
#
|
||||||
|
# @return [Excon::Response]
|
||||||
|
# * body [Hash] - Results of the operation.
|
||||||
|
# * "Number Not Found" [Integer] - Number of missing objects or containers.
|
||||||
|
# * "Response Status" [String] - Response code for the subrequest of the last failed operation.
|
||||||
|
# * "Errors" [Array<object_name, response_status>]
|
||||||
|
# * object_name [String] - Object that generated an error when the delete was attempted.
|
||||||
|
# * response_status [String] - Response status from the subrequest for object_name.
|
||||||
|
# * "Number Deleted" [Integer] - Number of objects or containers deleted.
|
||||||
|
# * "Response Body" [String] - Response body for "Response Status".
|
||||||
|
def delete_multiple_objects(container, object_names, options = {})
|
||||||
|
body = object_names.map do |name|
|
||||||
|
object_name = container ? "#{ container }/#{ name }" : name
|
||||||
|
URI.encode(object_name)
|
||||||
|
end.join("\n")
|
||||||
|
|
||||||
|
response = request({
|
||||||
|
:expects => 200,
|
||||||
|
:method => 'DELETE',
|
||||||
|
:headers => options.merge('Content-Type' => 'text/plain',
|
||||||
|
'Accept' => 'application/json'),
|
||||||
|
:body => body,
|
||||||
|
:query => { 'bulk-delete' => true }
|
||||||
|
}, false)
|
||||||
|
response.body = Fog::JSON.decode(response.body)
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,43 @@
|
||||||
|
module Fog
|
||||||
|
module Storage
|
||||||
|
class OpenStack
|
||||||
|
class Real
|
||||||
|
|
||||||
|
# Delete a static large object.
|
||||||
|
#
|
||||||
|
# Deletes the SLO manifest +object+ and all segments that it references.
|
||||||
|
# The server will respond with +200 OK+ for all requests.
|
||||||
|
# +response.body+ must be inspected for actual results.
|
||||||
|
#
|
||||||
|
# @param container [String] Name of container.
|
||||||
|
# @param object [String] Name of the SLO manifest object.
|
||||||
|
# @param options [Hash] Additional request headers.
|
||||||
|
#
|
||||||
|
# @return [Excon::Response]
|
||||||
|
# * body [Hash] - Results of the operation.
|
||||||
|
# * "Number Not Found" [Integer] - Number of missing segments.
|
||||||
|
# * "Response Status" [String] - Response code for the subrequest of the last failed operation.
|
||||||
|
# * "Errors" [Array<object_name, response_status>]
|
||||||
|
# * object_name [String] - Object that generated an error when the delete was attempted.
|
||||||
|
# * response_status [String] - Response status from the subrequest for object_name.
|
||||||
|
# * "Number Deleted" [Integer] - Number of segments deleted.
|
||||||
|
# * "Response Body" [String] - Response body for Response Status.
|
||||||
|
#
|
||||||
|
# @see http://docs.openstack.org/api/openstack-object-storage/1.0/content/static-large-objects.html
|
||||||
|
def delete_static_large_object(container, object, options = {})
|
||||||
|
response = request({
|
||||||
|
:expects => 200,
|
||||||
|
:method => 'DELETE',
|
||||||
|
:headers => options.merge('Content-Type' => 'text/plain',
|
||||||
|
'Accept' => 'application/json'),
|
||||||
|
:path => "#{Fog::OpenStack.escape(container)}/#{Fog::OpenStack.escape(object)}",
|
||||||
|
:query => { 'multipart-manifest' => 'delete' }
|
||||||
|
}, false)
|
||||||
|
response.body = Fog::JSON.decode(response.body)
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,43 @@
|
||||||
|
module Fog
|
||||||
|
module Storage
|
||||||
|
class OpenStack
|
||||||
|
class Real
|
||||||
|
|
||||||
|
# Create a new dynamic large object manifest
|
||||||
|
#
|
||||||
|
# Creates an object with a +X-Object-Manifest+ header that specifies the common prefix ("<container>/<prefix>")
|
||||||
|
# for all uploaded segments. Retrieving the manifest object streams all segments matching this prefix.
|
||||||
|
# Segments must sort in the order they should be concatenated. Note that any future objects stored in the container
|
||||||
|
# along with the segments that match the prefix will be included when retrieving the manifest object.
|
||||||
|
#
|
||||||
|
# All segments must be stored in the same container, but may be in a different container than the manifest object.
|
||||||
|
# The default +X-Object-Manifest+ header is set to "+container+/+object+", but may be overridden in +options+
|
||||||
|
# to specify the prefix and/or the container where segments were stored.
|
||||||
|
# If overridden, names should be CGI escaped (excluding spaces) if needed (see {Fog::OpenStack.escape}).
|
||||||
|
#
|
||||||
|
# @param container [String] Name for container where +object+ will be stored. Should be < 256 bytes and must not contain '/'
|
||||||
|
# @param object [String] Name for manifest object.
|
||||||
|
# @param options [Hash] Config headers for +object+.
|
||||||
|
# @option options [String] 'X-Object-Manifest' ("container/object") "<container>/<prefix>" for segment objects.
|
||||||
|
#
|
||||||
|
# @raise [Fog::Storage::OpenStack::NotFound] HTTP 404
|
||||||
|
# @raise [Excon::Errors::BadRequest] HTTP 400
|
||||||
|
# @raise [Excon::Errors::Unauthorized] HTTP 401
|
||||||
|
# @raise [Excon::Errors::HTTPStatusError]
|
||||||
|
#
|
||||||
|
# @see http://docs.openstack.org/api/openstack-object-storage/1.0/content/dynamic-large-object-creation.html
|
||||||
|
def put_dynamic_obj_manifest(container, object, options = {})
|
||||||
|
path = "#{Fog::OpenStack.escape(container)}/#{Fog::OpenStack.escape(object)}"
|
||||||
|
headers = {'X-Object-Manifest' => path}.merge(options)
|
||||||
|
request(
|
||||||
|
:expects => 201,
|
||||||
|
:headers => headers,
|
||||||
|
:method => 'PUT',
|
||||||
|
:path => path
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,33 +3,11 @@ module Fog
|
||||||
class OpenStack
|
class OpenStack
|
||||||
class Real
|
class Real
|
||||||
|
|
||||||
# Create a new manifest object
|
# Create a new dynamic large object manifest
|
||||||
#
|
#
|
||||||
# Creates an object with a +X-Object-Manifest+ header that specifies the common prefix ("<container>/<prefix>")
|
# This is an alias for {#put_dynamic_obj_manifest} for backward compatibility.
|
||||||
# for all uploaded segments. Retrieving the manifest object streams all segments matching this prefix.
|
|
||||||
# Segments must sort in the order they should be concatenated. Note that any future objects stored in the container
|
|
||||||
# along with the segments that match the prefix will be included when retrieving the manifest object.
|
|
||||||
#
|
|
||||||
# All segments must be stored in the same container, but may be in a different container than the manifest object.
|
|
||||||
# The default +X-Object-Manifest+ header is set to "+container+/+object+", but may be overridden in +options+
|
|
||||||
# to specify the prefix and/or the container where segments were stored.
|
|
||||||
# If overridden, names should be CGI escaped (excluding spaces) if needed (see {Fog::Rackspace.escape}).
|
|
||||||
#
|
|
||||||
# @param container [String] Name for container where +object+ will be stored. Should be < 256 bytes and must not contain '/'
|
|
||||||
# @param object [String] Name for manifest object.
|
|
||||||
# @param options [Hash] Config headers for +object+.
|
|
||||||
# @option options [String] 'X-Object-Manifest' ("container/object") "<container>/<prefix>" for segment objects.
|
|
||||||
#
|
|
||||||
# @see http://docs.openstack.org/api/openstack-object-storage/1.0/content/large-object-creation.html
|
|
||||||
def put_object_manifest(container, object, options = {})
|
def put_object_manifest(container, object, options = {})
|
||||||
path = "#{Fog::OpenStack.escape(container)}/#{Fog::OpenStack.escape(object)}"
|
put_dynamic_obj_manifest(container, object, options)
|
||||||
headers = {'X-Object-Manifest' => path}.merge(options)
|
|
||||||
request(
|
|
||||||
:expects => 201,
|
|
||||||
:headers => headers,
|
|
||||||
:method => 'PUT',
|
|
||||||
:path => path
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
module Fog
|
||||||
|
module Storage
|
||||||
|
class OpenStack
|
||||||
|
class Real
|
||||||
|
|
||||||
|
# Create a new static large object manifest.
|
||||||
|
#
|
||||||
|
# A static large object is similar to a dynamic large object. Whereas a GET for a dynamic large object manifest
|
||||||
|
# will stream segments based on the manifest's +X-Object-Manifest+ object name prefix, a static large object
|
||||||
|
# manifest streams segments which are defined by the user within the manifest. Information about each segment is
|
||||||
|
# provided in +segments+ as an Array of Hash objects, ordered in the sequence which the segments should be streamed.
|
||||||
|
#
|
||||||
|
# When the SLO manifest is received, each segment's +etag+ and +size_bytes+ will be verified.
|
||||||
|
# The +etag+ for each segment is returned in the response to {#put_object}, but may also be calculated.
|
||||||
|
# e.g. +Digest::MD5.hexdigest(segment_data)+
|
||||||
|
#
|
||||||
|
# The maximum number of segments for a static large object is 1000, and all segments (except the last) must be
|
||||||
|
# at least 1 MiB in size. Unlike a dynamic large object, segments are not required to be in the same container.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# segments = [
|
||||||
|
# { :path => 'segments_container/first_segment',
|
||||||
|
# :etag => 'md5 for first_segment',
|
||||||
|
# :size_bytes => 'byte size of first_segment' },
|
||||||
|
# { :path => 'segments_container/second_segment',
|
||||||
|
# :etag => 'md5 for second_segment',
|
||||||
|
# :size_bytes => 'byte size of second_segment' }
|
||||||
|
# ]
|
||||||
|
# put_static_obj_manifest('my_container', 'my_large_object', segments)
|
||||||
|
#
|
||||||
|
# @param container [String] Name for container where +object+ will be stored.
|
||||||
|
# Should be < 256 bytes and must not contain '/'
|
||||||
|
# @param object [String] Name for manifest object.
|
||||||
|
# @param segments [Array<Hash>] Segment data for the object.
|
||||||
|
# @param options [Hash] Config headers for +object+.
|
||||||
|
#
|
||||||
|
# @raise [Fog::Storage::OpenStack::NotFound] HTTP 404
|
||||||
|
# @raise [Excon::Errors::BadRequest] HTTP 400
|
||||||
|
# @raise [Excon::Errors::Unauthorized] HTTP 401
|
||||||
|
# @raise [Excon::Errors::HTTPStatusError]
|
||||||
|
#
|
||||||
|
# @see http://docs.openstack.org/api/openstack-object-storage/1.0/content/static-large-objects.html
|
||||||
|
def put_static_obj_manifest(container, object, segments, options = {})
|
||||||
|
request(
|
||||||
|
:expects => 201,
|
||||||
|
:method => 'PUT',
|
||||||
|
:headers => options,
|
||||||
|
:body => Fog::JSON.encode(segments),
|
||||||
|
:path => "#{Fog::OpenStack.escape(container)}/#{Fog::OpenStack.escape(object)}",
|
||||||
|
:query => { 'multipart-manifest' => 'put' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,6 +21,8 @@ module Fog
|
||||||
request :copy_object
|
request :copy_object
|
||||||
request :delete_container
|
request :delete_container
|
||||||
request :delete_object
|
request :delete_object
|
||||||
|
request :delete_multiple_objects
|
||||||
|
request :delete_static_large_object
|
||||||
request :get_container
|
request :get_container
|
||||||
request :get_containers
|
request :get_containers
|
||||||
request :get_object
|
request :get_object
|
||||||
|
@ -31,6 +33,8 @@ module Fog
|
||||||
request :put_container
|
request :put_container
|
||||||
request :put_object
|
request :put_object
|
||||||
request :put_object_manifest
|
request :put_object_manifest
|
||||||
|
request :put_dynamic_obj_manifest
|
||||||
|
request :put_static_obj_manifest
|
||||||
|
|
||||||
class Mock
|
class Mock
|
||||||
|
|
||||||
|
@ -145,6 +149,7 @@ module Fog
|
||||||
response = @connection.request(params.merge({
|
response = @connection.request(params.merge({
|
||||||
:headers => {
|
:headers => {
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
|
'Accept' => 'application/json',
|
||||||
'X-Auth-Token' => @auth_token
|
'X-Auth-Token' => @auth_token
|
||||||
}.merge!(params[:headers] || {}),
|
}.merge!(params[:headers] || {}),
|
||||||
:host => @host,
|
:host => @host,
|
||||||
|
|
|
@ -1,106 +1,367 @@
|
||||||
Shindo.tests('Fog::Storage[:openstack] | large object requests', ["openstack"]) do
|
Shindo.tests('Fog::Storage[:openstack] | large object requests', ['openstack']) do
|
||||||
|
|
||||||
unless Fog.mocking?
|
unless Fog.mocking?
|
||||||
@directory = Fog::Storage[:openstack].directories.create(:key => 'foglargeobjecttests')
|
@directory = Fog::Storage[:openstack].directories.create(:key => 'foglargeobjecttests')
|
||||||
@directory2 = Fog::Storage[:openstack].directories.create(:key => 'foglargeobjecttests2')
|
@directory2 = Fog::Storage[:openstack].directories.create(:key => 'foglargeobjecttests2')
|
||||||
|
@segments = {
|
||||||
|
:a => {
|
||||||
|
:container => @directory.identity,
|
||||||
|
:name => 'fog_large_object/a',
|
||||||
|
:data => 'a' * (1024**2 + 10),
|
||||||
|
:size => 1024**2 + 10,
|
||||||
|
:etag => 'c2e97007d59f0c19b850debdcb80cca5'
|
||||||
|
},
|
||||||
|
:b => {
|
||||||
|
:container => @directory.identity,
|
||||||
|
:name => 'fog_large_object/b',
|
||||||
|
:data => 'b' * (1024**2 + 20),
|
||||||
|
:size => 1024**2 + 20,
|
||||||
|
:etag => 'd35f50622a1259daad75ff7d5512c7ef'
|
||||||
|
},
|
||||||
|
:c => {
|
||||||
|
:container => @directory.identity,
|
||||||
|
:name => 'fog_large_object2/a',
|
||||||
|
:data => 'c' * (1024**2 + 30),
|
||||||
|
:size => 1024**2 + 30,
|
||||||
|
:etag => '901d3531a87d188041d4d5b43cb464c1'
|
||||||
|
},
|
||||||
|
:d => {
|
||||||
|
:container => @directory2.identity,
|
||||||
|
:name => 'fog_large_object2/b',
|
||||||
|
:data => 'd' * (1024**2 + 40),
|
||||||
|
:size => 1024**2 + 40,
|
||||||
|
:etag => '350c0e00525198813920a157df185c8d'
|
||||||
|
}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
tests('success') do
|
tests('success') do
|
||||||
|
|
||||||
tests("#put_object('foglargeobjecttests', 'fog_large_object/1', ('x' * 4 * 1024 * 1024))").succeeds do
|
tests('upload test segments').succeeds do
|
||||||
pending if Fog.mocking?
|
pending if Fog.mocking?
|
||||||
Fog::Storage[:openstack].put_object(@directory.identity, 'fog_large_object/1', ('x' * 4 * 1024 * 1024))
|
|
||||||
|
@segments.each_value do |segment|
|
||||||
|
Fog::Storage[:openstack].put_object(segment[:container], segment[:name], segment[:data])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
tests("#put_object('foglargeobjecttests', 'fog_large_object/2', ('x' * 2 * 1024 * 1024))").succeeds do
|
tests('dynamic large object requests') do
|
||||||
pending if Fog.mocking?
|
pending if Fog.mocking?
|
||||||
Fog::Storage[:openstack].put_object(@directory.identity, 'fog_large_object/2', ('x' * 2 * 1024 * 1024))
|
|
||||||
end
|
|
||||||
|
|
||||||
tests("#put_object('foglargeobjecttests', 'fog_large_object2/1', ('x' * 1 * 1024 * 1024))").succeeds do
|
tests('#put_object_manifest alias').succeeds do
|
||||||
pending if Fog.mocking?
|
|
||||||
Fog::Storage[:openstack].put_object(@directory.identity, 'fog_large_object2/1', ('x' * 1 * 1024 * 1024))
|
|
||||||
end
|
|
||||||
|
|
||||||
tests("using default X-Object-Manifest header") do
|
|
||||||
|
|
||||||
tests("#put_object_manifest('foglargeobjecttests', 'fog_large_object')").succeeds do
|
|
||||||
pending if Fog.mocking?
|
|
||||||
Fog::Storage[:openstack].put_object_manifest(@directory.identity, 'fog_large_object')
|
Fog::Storage[:openstack].put_object_manifest(@directory.identity, 'fog_large_object')
|
||||||
end
|
end
|
||||||
|
|
||||||
tests("#get_object streams all segments matching the default prefix").succeeds do
|
tests('using default X-Object-Manifest header') do
|
||||||
pending if Fog.mocking?
|
|
||||||
Fog::Storage[:openstack].get_object(@directory.identity, 'fog_large_object').body == ('x' * 7 * 1024 * 1024)
|
tests('#put_dynamic_obj_manifest').succeeds do
|
||||||
|
Fog::Storage[:openstack].put_dynamic_obj_manifest(@directory.identity, 'fog_large_object')
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#get_object streams all segments matching the default prefix').succeeds do
|
||||||
|
expected = @segments[:a][:data] + @segments[:b][:data] + @segments[:c][:data]
|
||||||
|
Fog::Storage[:openstack].get_object(@directory.identity, 'fog_large_object').body == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
# When the manifest object name is equal to the segment prefix, OpenStack treats it as if it's the first segment.
|
||||||
|
# So you must prepend the manifest object's Etag - Digest::MD5.hexdigest('')
|
||||||
|
tests('#head_object returns Etag that includes manifest object in calculation').succeeds do
|
||||||
|
etags = ['d41d8cd98f00b204e9800998ecf8427e', @segments[:a][:etag], @segments[:b][:etag], @segments[:c][:etag]]
|
||||||
|
expected = "\"#{ Digest::MD5.hexdigest(etags.join) }\"" # returned in quotes "\"2577f38428e895c50de6ea78ccc7da2a"\"
|
||||||
|
Fog::Storage[:openstack].head_object(@directory.identity, 'fog_large_object').headers['Etag'] == expected
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
tests("#head_object returns Etag that includes manifest object in calculation").succeeds do
|
tests('specifying X-Object-Manifest segment prefix') do
|
||||||
pending if Fog.mocking?
|
|
||||||
|
tests('#put_dynamic_obj_manifest').succeeds do
|
||||||
|
options = { 'X-Object-Manifest' => "#{ @directory.identity }/fog_large_object/" }
|
||||||
|
Fog::Storage[:openstack].put_dynamic_obj_manifest(@directory.identity, 'fog_large_object', options)
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#get_object streams segments only matching the specified prefix').succeeds do
|
||||||
|
expected = @segments[:a][:data] + @segments[:b][:data]
|
||||||
|
Fog::Storage[:openstack].get_object(@directory.identity, 'fog_large_object').body == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#head_object returns Etag that does not include manifest object in calculation').succeeds do
|
||||||
|
etags = [@segments[:a][:etag], @segments[:b][:etag]]
|
||||||
|
expected = "\"#{ Digest::MD5.hexdigest(etags.join) }\"" # returned in quotes "\"0f035ed3cc38aa0ef46dda3478fad44d"\"
|
||||||
|
Fog::Storage[:openstack].head_object(@directory.identity, 'fog_large_object').headers['Etag'] == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('storing manifest in a different container than the segments') do
|
||||||
|
|
||||||
|
tests('#put_dynamic_obj_manifest').succeeds do
|
||||||
|
options = { 'X-Object-Manifest' => "#{ @directory.identity }/fog_large_object/" }
|
||||||
|
Fog::Storage[:openstack].put_dynamic_obj_manifest(@directory2.identity, 'fog_large_object', options)
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#get_object').succeeds do
|
||||||
|
expected = @segments[:a][:data] + @segments[:b][:data]
|
||||||
|
Fog::Storage[:openstack].get_object(@directory2.identity, 'fog_large_object').body == expected
|
||||||
|
end
|
||||||
|
|
||||||
etags = []
|
|
||||||
# When the manifest object name is equal to the prefix, OpenStack treats it as if it's the first segment.
|
|
||||||
etags << Digest::MD5.hexdigest('') # Etag for manifest object => "d41d8cd98f00b204e9800998ecf8427e"
|
|
||||||
etags << Digest::MD5.hexdigest('x' * 4 * 1024 * 1024) # => "44981362d3ba9b5bacaf017c2f29d355"
|
|
||||||
etags << Digest::MD5.hexdigest('x' * 2 * 1024 * 1024) # => "67b2f816a30e8956149b2d7beb479e51"
|
|
||||||
etags << Digest::MD5.hexdigest('x' * 1 * 1024 * 1024) # => "b561f87202d04959e37588ee05cf5b10"
|
|
||||||
expected = Digest::MD5.hexdigest(etags.join) # => "42e92048bd2c8085e7072b0b55fd76ab"
|
|
||||||
actual = Fog::Storage[:openstack].head_object(@directory.identity, 'fog_large_object').headers['Etag']
|
|
||||||
actual.gsub('"', '') == expected # actual is returned in quotes "\"42e92048bd2c8085e7072b0b55fd76abu"\"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
tests("specifying X-Object-Manifest segment prefix") do
|
tests('static large object requests') do
|
||||||
|
pending if Fog.mocking?
|
||||||
|
|
||||||
|
tests('single container') do
|
||||||
|
|
||||||
|
tests('#put_static_obj_manifest').succeeds do
|
||||||
|
segments = [
|
||||||
|
{ :path => "#{ @segments[:a][:container] }/#{ @segments[:a][:name] }",
|
||||||
|
:etag => @segments[:a][:etag],
|
||||||
|
:size_bytes => @segments[:a][:size] },
|
||||||
|
{ :path => "#{ @segments[:c][:container] }/#{ @segments[:c][:name] }",
|
||||||
|
:etag => @segments[:c][:etag],
|
||||||
|
:size_bytes => @segments[:c][:size] }
|
||||||
|
]
|
||||||
|
Fog::Storage[:openstack].put_static_obj_manifest(@directory.identity, 'fog_large_object', segments)
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#head_object') do
|
||||||
|
etags = [@segments[:a][:etag], @segments[:c][:etag]]
|
||||||
|
etag = "\"#{ Digest::MD5.hexdigest(etags.join) }\"" # "\"ad7e633a12e8a4915b45e6dd1d4b0b4b\""
|
||||||
|
content_length = (@segments[:a][:size] + @segments[:c][:size]).to_s
|
||||||
|
response = Fog::Storage[:openstack].head_object(@directory.identity, 'fog_large_object')
|
||||||
|
|
||||||
|
returns(etag, 'returns ETag computed from segments') { response.headers['Etag'] }
|
||||||
|
returns(content_length , 'returns Content-Length for all segments') { response.headers['Content-Length'] }
|
||||||
|
returns('True', 'returns X-Static-Large-Object header') { response.headers['X-Static-Large-Object'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#get_object').succeeds do
|
||||||
|
expected = @segments[:a][:data] + @segments[:c][:data]
|
||||||
|
Fog::Storage[:openstack].get_object(@directory.identity, 'fog_large_object').body == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#delete_static_large_object') do
|
||||||
|
expected = {
|
||||||
|
'Number Not Found' => 0,
|
||||||
|
'Response Status' => '200 OK',
|
||||||
|
'Errors' => [],
|
||||||
|
'Number Deleted' => 3,
|
||||||
|
'Response Body' => ''
|
||||||
|
}
|
||||||
|
returns(expected, 'deletes manifest and segments') do
|
||||||
|
Fog::Storage[:openstack].delete_static_large_object(@directory.identity, 'fog_large_object').body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
tests("#put_object_manifest('foglargeobjecttests', 'fog_large_object', {'X-Object-Manifest' => 'foglargeobjecttests/fog_large_object/')").succeeds do
|
|
||||||
pending if Fog.mocking?
|
|
||||||
Fog::Storage[:openstack].put_object_manifest(@directory.identity, 'fog_large_object', {'X-Object-Manifest' => "#{@directory.identity}/fog_large_object/"})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
tests("#get_object streams segments only matching the specified prefix").succeeds do
|
tests('multiple containers') do
|
||||||
pending if Fog.mocking?
|
|
||||||
Fog::Storage[:openstack].get_object(@directory.identity, 'fog_large_object').body == ('x' * 6 * 1024 * 1024)
|
tests('#put_static_obj_manifest').succeeds do
|
||||||
|
segments = [
|
||||||
|
{ :path => "#{ @segments[:b][:container] }/#{ @segments[:b][:name] }",
|
||||||
|
:etag => @segments[:b][:etag],
|
||||||
|
:size_bytes => @segments[:b][:size] },
|
||||||
|
{ :path => "#{ @segments[:d][:container] }/#{ @segments[:d][:name] }",
|
||||||
|
:etag => @segments[:d][:etag],
|
||||||
|
:size_bytes => @segments[:d][:size] }
|
||||||
|
]
|
||||||
|
Fog::Storage[:openstack].put_static_obj_manifest(@directory2.identity, 'fog_large_object', segments)
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#head_object') do
|
||||||
|
etags = [@segments[:b][:etag], @segments[:d][:etag]]
|
||||||
|
etag = "\"#{ Digest::MD5.hexdigest(etags.join) }\"" # "\"9801a4cc4472896a1e975d03f0d2c3f8\""
|
||||||
|
content_length = (@segments[:b][:size] + @segments[:d][:size]).to_s
|
||||||
|
response = Fog::Storage[:openstack].head_object(@directory2.identity, 'fog_large_object')
|
||||||
|
|
||||||
|
returns(etag, 'returns ETag computed from segments') { response.headers['Etag'] }
|
||||||
|
returns(content_length , 'returns Content-Length for all segments') { response.headers['Content-Length'] }
|
||||||
|
returns('True', 'returns X-Static-Large-Object header') { response.headers['X-Static-Large-Object'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#get_object').succeeds do
|
||||||
|
expected = @segments[:b][:data] + @segments[:d][:data]
|
||||||
|
Fog::Storage[:openstack].get_object(@directory2.identity, 'fog_large_object').body == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#delete_static_large_object') do
|
||||||
|
expected = {
|
||||||
|
'Number Not Found' => 0,
|
||||||
|
'Response Status' => '200 OK',
|
||||||
|
'Errors' => [],
|
||||||
|
'Number Deleted' => 3,
|
||||||
|
'Response Body' => ''
|
||||||
|
}
|
||||||
|
returns(expected, 'deletes manifest and segments') do
|
||||||
|
Fog::Storage[:openstack].delete_static_large_object(@directory2.identity, 'fog_large_object').body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
tests("#head_object returns Etag that does not include manifest object in calculation").succeeds do
|
|
||||||
pending if Fog.mocking?
|
|
||||||
|
|
||||||
etags = []
|
|
||||||
etags << Digest::MD5.hexdigest('x' * 4 * 1024 * 1024) # => "44981362d3ba9b5bacaf017c2f29d355"
|
|
||||||
etags << Digest::MD5.hexdigest('x' * 2 * 1024 * 1024) # => "67b2f816a30e8956149b2d7beb479e51"
|
|
||||||
expected = Digest::MD5.hexdigest(etags.join) # => "0b348495a774eaa4d4c4bbf770820f84"
|
|
||||||
actual = Fog::Storage[:openstack].head_object(@directory.identity, 'fog_large_object').headers['Etag']
|
|
||||||
actual.gsub('"', '') == expected # actual is returned in quotes "\"0b348495a774eaa4d4c4bbf770820f84"\"
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
tests("storing manifest object in a different container than the segments") do
|
|
||||||
|
|
||||||
tests("#put_object_manifest('foglargeobjecttests2', 'fog_large_object', {'X-Object-Manifest' => 'foglargeobjecttests/fog_large_object/'})").succeeds do
|
|
||||||
pending if Fog.mocking?
|
|
||||||
Fog::Storage[:openstack].put_object_manifest(@directory2.identity, 'fog_large_object', {'X-Object-Manifest' => "#{@directory.identity}/fog_large_object/"})
|
|
||||||
end
|
|
||||||
|
|
||||||
tests("#get_object('foglargeobjecttests2', 'fog_large_object').body").succeeds do
|
|
||||||
pending if Fog.mocking?
|
|
||||||
Fog::Storage[:openstack].get_object(@directory2.identity, 'fog_large_object').body == ('x' * 6 * 1024 * 1024)
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
unless Fog.mocking?
|
|
||||||
['fog_large_object', 'fog_large_object/1', 'fog_large_object/2', 'fog_large_object2/1'].each do |key|
|
|
||||||
@directory.files.new(:key => key).destroy
|
|
||||||
end
|
|
||||||
@directory2.files.new(:key => 'fog_large_object').destroy
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
tests('failure') do
|
tests('failure') do
|
||||||
|
|
||||||
tests("put_object_manifest")
|
tests('dynamic large object requests') do
|
||||||
|
pending if Fog.mocking?
|
||||||
|
|
||||||
|
tests('#put_dynamic_obj_manifest with missing container').raises(Fog::Storage::OpenStack::NotFound) do
|
||||||
|
Fog::Storage[:openstack].put_dynamic_obj_manifest('fognoncontainer', 'fog_large_object')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('static large object requests') do
|
||||||
|
pending if Fog.mocking?
|
||||||
|
|
||||||
|
tests('upload test segments').succeeds do
|
||||||
|
Fog::Storage[:openstack].put_object(@segments[:a][:container], @segments[:a][:name], @segments[:a][:data])
|
||||||
|
Fog::Storage[:openstack].put_object(@segments[:b][:container], @segments[:b][:name], @segments[:b][:data])
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#put_static_obj_manifest with missing container').raises(Fog::Storage::OpenStack::NotFound) do
|
||||||
|
Fog::Storage[:openstack].put_static_obj_manifest('fognoncontainer', 'fog_large_object', [])
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#put_static_obj_manifest with missing object') do
|
||||||
|
segments = [
|
||||||
|
{ :path => "#{ @segments[:c][:container] }/#{ @segments[:c][:name] }",
|
||||||
|
:etag => @segments[:c][:etag],
|
||||||
|
:size_bytes => @segments[:c][:size] }
|
||||||
|
]
|
||||||
|
expected = { 'Errors' => [[segments[0][:path], '404 Not Found']] }
|
||||||
|
|
||||||
|
error = nil
|
||||||
|
begin
|
||||||
|
Fog::Storage[:openstack].put_static_obj_manifest(@directory.identity, 'fog_large_object', segments)
|
||||||
|
rescue => err
|
||||||
|
error = err
|
||||||
|
end
|
||||||
|
|
||||||
|
raises(Excon::Errors::BadRequest) do
|
||||||
|
raise error if error
|
||||||
|
end
|
||||||
|
|
||||||
|
returns(expected, 'returns error information') do
|
||||||
|
Fog::JSON.decode(error.response.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#put_static_obj_manifest with invalid etag') do
|
||||||
|
segments = [
|
||||||
|
{ :path => "#{ @segments[:a][:container] }/#{ @segments[:a][:name] }",
|
||||||
|
:etag => @segments[:b][:etag],
|
||||||
|
:size_bytes => @segments[:a][:size] }
|
||||||
|
]
|
||||||
|
expected = { 'Errors' => [[segments[0][:path], 'Etag Mismatch']] }
|
||||||
|
|
||||||
|
error = nil
|
||||||
|
begin
|
||||||
|
Fog::Storage[:openstack].put_static_obj_manifest(@directory.identity, 'fog_large_object', segments)
|
||||||
|
rescue => err
|
||||||
|
error = err
|
||||||
|
end
|
||||||
|
|
||||||
|
raises(Excon::Errors::BadRequest) do
|
||||||
|
raise error if error
|
||||||
|
end
|
||||||
|
|
||||||
|
returns(expected, 'returns error information') do
|
||||||
|
Fog::JSON.decode(error.response.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#put_static_obj_manifest with invalid byte_size') do
|
||||||
|
segments = [
|
||||||
|
{ :path => "#{ @segments[:a][:container] }/#{ @segments[:a][:name] }",
|
||||||
|
:etag => @segments[:a][:etag],
|
||||||
|
:size_bytes => @segments[:b][:size] }
|
||||||
|
]
|
||||||
|
expected = { 'Errors' => [[segments[0][:path], 'Size Mismatch']] }
|
||||||
|
|
||||||
|
error = nil
|
||||||
|
begin
|
||||||
|
Fog::Storage[:openstack].put_static_obj_manifest(@directory.identity, 'fog_large_object', segments)
|
||||||
|
rescue => err
|
||||||
|
error = err
|
||||||
|
end
|
||||||
|
|
||||||
|
raises(Excon::Errors::BadRequest) do
|
||||||
|
raise error if error
|
||||||
|
end
|
||||||
|
|
||||||
|
returns(expected, 'returns error information') do
|
||||||
|
Fog::JSON.decode(error.response.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#delete_static_large_object with missing container') do
|
||||||
|
expected = {
|
||||||
|
'Number Not Found' => 1,
|
||||||
|
'Response Status' => '200 OK',
|
||||||
|
'Errors' => [],
|
||||||
|
'Number Deleted' => 0,
|
||||||
|
'Response Body' => ''
|
||||||
|
}
|
||||||
|
|
||||||
|
returns(expected, 'reports missing object') do
|
||||||
|
Fog::Storage[:openstack].delete_static_large_object('fognoncontainer', 'fog_large_object').body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#delete_static_large_object with missing manifest') do
|
||||||
|
expected = {
|
||||||
|
'Number Not Found' => 1,
|
||||||
|
'Response Status' => '200 OK',
|
||||||
|
'Errors' => [],
|
||||||
|
'Number Deleted' => 0,
|
||||||
|
'Response Body' => ''
|
||||||
|
}
|
||||||
|
|
||||||
|
returns(expected, 'reports missing manifest') do
|
||||||
|
Fog::Storage[:openstack].delete_static_large_object(@directory.identity, 'fog_non_object').body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#delete_static_large_object with missing segment') do
|
||||||
|
|
||||||
|
tests('#put_static_obj_manifest for segments :a and :b').succeeds do
|
||||||
|
segments = [
|
||||||
|
{ :path => "#{ @segments[:a][:container] }/#{ @segments[:a][:name] }",
|
||||||
|
:etag => @segments[:a][:etag],
|
||||||
|
:size_bytes => @segments[:a][:size] },
|
||||||
|
{ :path => "#{ @segments[:b][:container] }/#{ @segments[:b][:name] }",
|
||||||
|
:etag => @segments[:b][:etag],
|
||||||
|
:size_bytes => @segments[:b][:size] }
|
||||||
|
]
|
||||||
|
Fog::Storage[:openstack].put_static_obj_manifest(@directory.identity, 'fog_large_object', segments)
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#delete_object segment :b').succeeds do
|
||||||
|
Fog::Storage[:openstack].delete_object(@segments[:b][:container], @segments[:b][:name])
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('#delete_static_large_object') do
|
||||||
|
expected = {
|
||||||
|
'Number Not Found' => 1,
|
||||||
|
'Response Status' => '200 OK',
|
||||||
|
'Errors' => [],
|
||||||
|
'Number Deleted' => 2,
|
||||||
|
'Response Body' => ''
|
||||||
|
}
|
||||||
|
returns(expected, 'deletes manifest and segment :a, and reports missing segment :b') do
|
||||||
|
Fog::Storage[:openstack].delete_static_large_object(@directory.identity, 'fog_large_object').body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -17,18 +17,18 @@ Shindo.tests('Fog::Storage[:openstack] | object requests', ["openstack"]) do
|
||||||
Fog::Storage[:openstack].put_object('fogobjecttests', 'fog_object', lorem_file)
|
Fog::Storage[:openstack].put_object('fogobjecttests', 'fog_object', lorem_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
tests("#get_object('fogobjectests', 'fog_object')").returns(lorem_file.read) do
|
tests("#get_object('fogobjectests', 'fog_object')").succeeds do
|
||||||
pending if Fog.mocking?
|
pending if Fog.mocking?
|
||||||
Fog::Storage[:openstack].get_object('fogobjecttests', 'fog_object').body
|
Fog::Storage[:openstack].get_object('fogobjecttests', 'fog_object').body == lorem_file.read
|
||||||
end
|
end
|
||||||
|
|
||||||
tests("#get_object('fogobjecttests', 'fog_object', &block)").returns(lorem_file.read) do
|
tests("#get_object('fogobjecttests', 'fog_object', &block)").succeeds do
|
||||||
pending if Fog.mocking?
|
pending if Fog.mocking?
|
||||||
data = ''
|
data = ''
|
||||||
Fog::Storage[:openstack].get_object('fogobjecttests', 'fog_object') do |chunk, remaining_bytes, total_bytes|
|
Fog::Storage[:openstack].get_object('fogobjecttests', 'fog_object') do |chunk, remaining_bytes, total_bytes|
|
||||||
data << chunk
|
data << chunk
|
||||||
end
|
end
|
||||||
data
|
data == lorem_file.read
|
||||||
end
|
end
|
||||||
|
|
||||||
tests("#head_object('fogobjectests', 'fog_object')").succeeds do
|
tests("#head_object('fogobjectests', 'fog_object')").succeeds do
|
||||||
|
@ -65,6 +65,30 @@ Shindo.tests('Fog::Storage[:openstack] | object requests', ["openstack"]) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
tests('#delete_multiple_objects') do
|
||||||
|
pending if Fog.mocking?
|
||||||
|
|
||||||
|
Fog::Storage[:openstack].put_object('fogobjecttests', 'fog_object', lorem_file)
|
||||||
|
Fog::Storage[:openstack].put_object('fogobjecttests', 'fog_object2', lorem_file)
|
||||||
|
Fog::Storage[:openstack].directories.create(:key => 'fogobjecttests2')
|
||||||
|
Fog::Storage[:openstack].put_object('fogobjecttests2', 'fog_object', lorem_file)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"Number Not Found" => 0,
|
||||||
|
"Response Status" => "200 OK",
|
||||||
|
"Errors" => [],
|
||||||
|
"Number Deleted" => 2,
|
||||||
|
"Response Body" => ""
|
||||||
|
}
|
||||||
|
|
||||||
|
returns(expected, 'deletes multiple objects') do
|
||||||
|
Fog::Storage[:openstack].delete_multiple_objects('fogobjecttests', ['fog_object', 'fog_object2']).body
|
||||||
|
end
|
||||||
|
|
||||||
|
returns(expected, 'deletes object and container') do
|
||||||
|
Fog::Storage[:openstack].delete_multiple_objects(nil, ['fogobjecttests2/fog_object', 'fogobjecttests2']).body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -100,6 +124,42 @@ Shindo.tests('Fog::Storage[:openstack] | object requests', ["openstack"]) do
|
||||||
Fog::Storage[:openstack].delete_object('fognoncontainer', 'fog_non_object')
|
Fog::Storage[:openstack].delete_object('fognoncontainer', 'fog_non_object')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
tests('#delete_multiple_objects') do
|
||||||
|
pending if Fog.mocking?
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"Number Not Found" => 2,
|
||||||
|
"Response Status" => "200 OK",
|
||||||
|
"Errors" => [],
|
||||||
|
"Number Deleted" => 0,
|
||||||
|
"Response Body" => ""
|
||||||
|
}
|
||||||
|
|
||||||
|
returns(expected, 'reports missing objects') do
|
||||||
|
Fog::Storage[:openstack].delete_multiple_objects('fogobjecttests', ['fog_non_object', 'fog_non_object2']).body
|
||||||
|
end
|
||||||
|
|
||||||
|
returns(expected, 'reports missing container') do
|
||||||
|
Fog::Storage[:openstack].delete_multiple_objects('fognoncontainer', ['fog_non_object', 'fog_non_object2']).body
|
||||||
|
end
|
||||||
|
|
||||||
|
tests('deleting non-empty container') do
|
||||||
|
Fog::Storage[:openstack].put_object('fogobjecttests', 'fog_object', lorem_file)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"Number Not Found" => 0,
|
||||||
|
"Response Status" => "400 Bad Request",
|
||||||
|
"Errors" => [['fogobjecttests', '409 Conflict']],
|
||||||
|
"Number Deleted" => 1,
|
||||||
|
"Response Body" => ""
|
||||||
|
}
|
||||||
|
|
||||||
|
returns(expected, 'deletes object but not container') do
|
||||||
|
Fog::Storage[:openstack].delete_multiple_objects(nil, ['fogobjecttests', 'fogobjecttests/fog_object']).body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
unless Fog.mocking?
|
unless Fog.mocking?
|
||||||
|
|
Loading…
Reference in a new issue