mirror of
https://github.com/fog/fog.git
synced 2022-11-09 13:51:43 -05:00
Added versioned delete_multiple_objects support
Added the VersionId support in the delete_multiple_objects method. Assume that all object versions are specified in the versionId hash. The hash maps an object name to an array of versions. If a multiple versions are to be deleted, the caller must insert the object multiple times into the object_names array and insert all the versions into the array associated with that object_name.
This commit is contained in:
parent
89a7a9fc39
commit
fd1acc69bf
2 changed files with 165 additions and 5 deletions
|
@ -29,19 +29,32 @@ module Fog
|
|||
# ==== See Also
|
||||
# http://docs.amazonwebservices.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
|
||||
# bucket_name -- name of the bucket to use
|
||||
# object_names -- filename
|
||||
# For versioned deletes, options should include a version_ids hash, which
|
||||
# maps from filename to an array of versions.
|
||||
# The semantics are that for each (object_name, version) tuple, the
|
||||
# caller must insert the object_name and an associated version (if
|
||||
# desired), so for n versions, the object must be inserted n times.
|
||||
def delete_multiple_objects(bucket_name, object_names, options = {})
|
||||
data = "<Delete>"
|
||||
data << "<Quiet>true</Quiet>" if options.delete(:quiet)
|
||||
version_ids = options.delete('versionId')
|
||||
object_names.each do |object_name|
|
||||
data << "<Object>"
|
||||
data << "<Key>#{object_name}</Key>"
|
||||
data << "<Key>#{CGI.escape(object_name)}</Key>"
|
||||
object_version = version_ids.nil? ? nil : version_ids[object_name]
|
||||
if object_version
|
||||
data << "<VersionId>#{CGI.escape(object_version)}</VersionId>"
|
||||
end
|
||||
data << "</Object>"
|
||||
end
|
||||
data << "</Delete>"
|
||||
|
||||
headers = options
|
||||
headers['Content-Length'] = data.length
|
||||
headers['Content-MD5'] = Base64.encode64(Digest::MD5.digest(data)).strip
|
||||
headers['Content-MD5'] = Base64.encode64(Digest::MD5.digest(data)).
|
||||
gsub("\n", '')
|
||||
|
||||
request({
|
||||
:body => data,
|
||||
|
@ -63,10 +76,12 @@ module Fog
|
|||
if bucket = self.data[:buckets][bucket_name]
|
||||
response.status = 200
|
||||
response.body = { 'DeleteResult' => [] }
|
||||
version_ids = options.delete('versionId')
|
||||
object_names.each do |object_name|
|
||||
bucket[:objects].delete(object_name)
|
||||
deleted_entry = { 'Deleted' => { 'Key' => object_name } }
|
||||
response.body['DeleteResult'] << deleted_entry
|
||||
object_version = version_ids.nil? ? nil : version_ids[object_name]
|
||||
response.body['DeleteResult'] << delete_object_helper(bucket,
|
||||
object_name,
|
||||
object_version)
|
||||
end
|
||||
else
|
||||
response.status = 404
|
||||
|
@ -75,6 +90,81 @@ module Fog
|
|||
response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_object_helper(bucket, object_name, version_id)
|
||||
response = { 'Deleted' => {} }
|
||||
if bucket[:versioning]
|
||||
bucket[:objects][object_name] ||= []
|
||||
|
||||
if version_id
|
||||
version = bucket[:objects][object_name].find { |object| object['VersionId'] == version_id}
|
||||
|
||||
# S3 special cases the 'null' value to not error out if no such version exists.
|
||||
if version || (version_id == 'null')
|
||||
bucket[:objects][object_name].delete(version)
|
||||
bucket[:objects].delete(object_name) if bucket[:objects][object_name].empty?
|
||||
|
||||
response['Deleted'] = { 'Key' => object_name,
|
||||
'VersionId' => version_id,
|
||||
'DeleteMarker' => 'true',
|
||||
'DeleteMarkerVersionId' => version_id
|
||||
}
|
||||
else
|
||||
response = delete_error_body(object_name,
|
||||
version_id,
|
||||
'InvalidVersion',
|
||||
'Invalid version ID specified')
|
||||
end
|
||||
else
|
||||
delete_marker = {
|
||||
:delete_marker => true,
|
||||
'Key' => object_name,
|
||||
'VersionId' => bucket[:versioning] == 'Enabled' ? Fog::Mock.random_base64(32) : 'null',
|
||||
'Last-Modified' => Fog::Time.now.to_date_header
|
||||
}
|
||||
|
||||
# When versioning is suspended, a delete marker is placed if the last object ID is not the value 'null',
|
||||
# otherwise the last object is replaced.
|
||||
if bucket[:versioning] == 'Suspended' && bucket[:objects][object_name].first['VersionId'] == 'null'
|
||||
bucket[:objects][object_name].shift
|
||||
end
|
||||
|
||||
bucket[:objects][object_name].unshift(delete_marker)
|
||||
|
||||
response['Deleted'] = { 'Key' => object_name,
|
||||
'VersionId' => delete_marker['VersionId'],
|
||||
'DeleteMarkerVersionId' =>
|
||||
delete_marker['VersionId'],
|
||||
'DeleteMarker' => 'true',
|
||||
}
|
||||
end
|
||||
else
|
||||
if version_id && version_id != 'null'
|
||||
response = delete_error_body(object_name,
|
||||
version_id,
|
||||
'InvalidVersion',
|
||||
'Invalid version ID specified')
|
||||
response = invalid_version_id_payload(version_id)
|
||||
else
|
||||
bucket[:objects].delete(object_name)
|
||||
response['Deleted'] = { 'Key' => object_name }
|
||||
end
|
||||
end
|
||||
response
|
||||
end
|
||||
|
||||
def delete_error_body(key, version_id, message, code)
|
||||
{
|
||||
'Error' => {
|
||||
'Code' => code,
|
||||
'Message' => message,
|
||||
'VersionId' => version_id,
|
||||
'Key' => key,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -134,6 +134,75 @@ Shindo.tests('Fog::Storage[:aws] | versioning', [:aws]) do
|
|||
end
|
||||
end
|
||||
|
||||
tests("deleting_multiple_objects('#{@aws_bucket_name}", 'file') do
|
||||
clear_bucket
|
||||
|
||||
bucket = Fog::Storage[:aws].directories.get(@aws_bucket_name)
|
||||
|
||||
file_count = 5
|
||||
file_names = []
|
||||
files = {}
|
||||
file_count.times do |id|
|
||||
file_names << "file_#{id}"
|
||||
files[file_names.last] = bucket.files.create(:body => 'a',
|
||||
:key => file_names.last)
|
||||
end
|
||||
|
||||
tests("deleting an object just stores a delete marker").returns(true) do
|
||||
Fog::Storage[:aws].delete_multiple_objects(@aws_bucket_name,
|
||||
file_names)
|
||||
versions = Fog::Storage[:aws].get_bucket_object_versions(
|
||||
@aws_bucket_name)
|
||||
all_versions = {}
|
||||
versions.body['Versions'].each do |version|
|
||||
object = version[version.keys.first]
|
||||
next if file_names.index(object['Key']).nil?
|
||||
if !all_versions.has_key?(object['Key'])
|
||||
all_versions[object['Key']] = version.has_key?('DeleteMarker')
|
||||
else
|
||||
all_versions[object['Key']] |= version.has_key?('DeleteMarker')
|
||||
end
|
||||
end
|
||||
all_true = true
|
||||
all_versions.values.each do |marker|
|
||||
all_true = false if !marker
|
||||
end
|
||||
all_true
|
||||
end
|
||||
|
||||
tests("there are two versions: the original and the delete marker").
|
||||
returns(file_count*2) do
|
||||
versions = Fog::Storage[:aws].get_bucket_object_versions(
|
||||
@aws_bucket_name)
|
||||
versions.body['Versions'].size
|
||||
end
|
||||
|
||||
tests("deleting the delete marker makes the object available again").
|
||||
returns(true) do
|
||||
versions = Fog::Storage[:aws].get_bucket_object_versions(
|
||||
@aws_bucket_name)
|
||||
delete_markers = []
|
||||
file_versions = {}
|
||||
versions.body['Versions'].each do |version|
|
||||
object = version[version.keys.first]
|
||||
next if object['VersionId'] == files[object['Key']].version
|
||||
file_versions[object['Key']] = object['VersionId']
|
||||
end
|
||||
|
||||
Fog::Storage[:aws].delete_multiple_objects(@aws_bucket_name,
|
||||
file_names,
|
||||
'versionId' => file_versions)
|
||||
all_true = true
|
||||
file_names.each do |file|
|
||||
res = Fog::Storage[:aws].get_object(@aws_bucket_name, file)
|
||||
all_true = false if res.headers['x-amz-version-id'] !=
|
||||
files[file].version
|
||||
end
|
||||
all_true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
tests("get_bucket('#{@aws_bucket_name}'") do
|
||||
clear_bucket
|
||||
|
||||
|
@ -181,6 +250,7 @@ Shindo.tests('Fog::Storage[:aws] | versioning', [:aws]) do
|
|||
tests("#delete_object('#{@aws_bucket_name}', '#{file.key}', 'versionId' => 'bad_version'").raises(Excon::Errors::BadRequest) do
|
||||
Fog::Storage[:aws].delete_object(@aws_bucket_name, file.key, 'versionId' => '-1')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# don't keep the bucket around
|
||||
|
|
Loading…
Reference in a new issue