diff --git a/lib/fog/aws/requests/storage/delete_multiple_objects.rb b/lib/fog/aws/requests/storage/delete_multiple_objects.rb index 1bf25ea76..363c03267 100644 --- a/lib/fog/aws/requests/storage/delete_multiple_objects.rb +++ b/lib/fog/aws/requests/storage/delete_multiple_objects.rb @@ -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 = "" data << "true" if options.delete(:quiet) + version_ids = options.delete('versionId') object_names.each do |object_name| data << "" - data << "#{object_name}" + data << "#{CGI.escape(object_name)}" + object_version = version_ids.nil? ? nil : version_ids[object_name] + if object_version + data << "#{CGI.escape(object_version)}" + end data << "" end data << "" 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 diff --git a/tests/aws/requests/storage/versioning_tests.rb b/tests/aws/requests/storage/versioning_tests.rb index 89689c854..f27c137c8 100644 --- a/tests/aws/requests/storage/versioning_tests.rb +++ b/tests/aws/requests/storage/versioning_tests.rb @@ -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