Merge "s3api: Delete multipart uploads via multi-delete"
This commit is contained in:
commit
7641fd380d
@ -14,6 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
|
|
||||||
from swift.common.constraints import MAX_OBJECT_NAME_LENGTH
|
from swift.common.constraints import MAX_OBJECT_NAME_LENGTH
|
||||||
from swift.common.utils import public, StreamingPile
|
from swift.common.utils import public, StreamingPile
|
||||||
@ -115,7 +116,30 @@ class MultiObjectDeleteController(Controller):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
query = req.gen_multipart_manifest_delete_query(self.app)
|
query = req.gen_multipart_manifest_delete_query(self.app)
|
||||||
req.get_response(self.app, method='DELETE', query=query)
|
resp = req.get_response(self.app, method='DELETE', query=query,
|
||||||
|
headers={'Accept': 'application/json'})
|
||||||
|
# Have to read the response to actually do the SLO delete
|
||||||
|
if query:
|
||||||
|
try:
|
||||||
|
delete_result = json.loads(resp.body)
|
||||||
|
if delete_result['Errors']:
|
||||||
|
# NB: bulk includes 404s in "Number Not Found",
|
||||||
|
# not "Errors"
|
||||||
|
msg_parts = [delete_result['Response Status']]
|
||||||
|
msg_parts.extend(
|
||||||
|
'%s: %s' % (obj, status)
|
||||||
|
for obj, status in delete_result['Errors'])
|
||||||
|
return key, {'code': 'SLODeleteError',
|
||||||
|
'message': '\n'.join(msg_parts)}
|
||||||
|
# else, all good
|
||||||
|
except (ValueError, TypeError, KeyError):
|
||||||
|
# Logs get all the gory details
|
||||||
|
self.logger.exception(
|
||||||
|
'Could not parse SLO delete response: %r',
|
||||||
|
resp.body)
|
||||||
|
# Client gets something more generic
|
||||||
|
return key, {'code': 'SLODeleteError',
|
||||||
|
'message': 'Unexpected swift response'}
|
||||||
except NoSuchKey:
|
except NoSuchKey:
|
||||||
pass
|
pass
|
||||||
except ErrorResponse as e:
|
except ErrorResponse as e:
|
||||||
|
@ -32,7 +32,8 @@ from swift.common.middleware.s3api.utils import mktime
|
|||||||
|
|
||||||
from test.functional.s3api import S3ApiBase
|
from test.functional.s3api import S3ApiBase
|
||||||
from test.functional.s3api.s3_test_client import Connection
|
from test.functional.s3api.s3_test_client import Connection
|
||||||
from test.functional.s3api.utils import get_error_code, get_error_msg
|
from test.functional.s3api.utils import get_error_code, get_error_msg, \
|
||||||
|
calculate_md5
|
||||||
|
|
||||||
|
|
||||||
def setUpModule():
|
def setUpModule():
|
||||||
@ -907,6 +908,27 @@ class TestS3ApiMultiUploadSigV4(TestS3ApiMultiUpload):
|
|||||||
self.assertEqual(status, 200) # sanity
|
self.assertEqual(status, 200) # sanity
|
||||||
self.assertEqual(content, body) # sanity
|
self.assertEqual(content, body) # sanity
|
||||||
|
|
||||||
|
# Can delete it with DeleteMultipleObjects request
|
||||||
|
elem = Element('Delete')
|
||||||
|
SubElement(elem, 'Quiet').text = 'true'
|
||||||
|
obj_elem = SubElement(elem, 'Object')
|
||||||
|
SubElement(obj_elem, 'Key').text = key
|
||||||
|
body = tostring(elem, use_s3ns=False)
|
||||||
|
|
||||||
|
status, headers, body = self.conn.make_request(
|
||||||
|
'POST', bucket, body=body, query='delete',
|
||||||
|
headers={'Content-MD5': calculate_md5(body)})
|
||||||
|
self.assertEqual(status, 200)
|
||||||
|
self.assertCommonResponseHeaders(headers)
|
||||||
|
|
||||||
|
status, headers, body = \
|
||||||
|
self.conn.make_request('GET', bucket, key)
|
||||||
|
self.assertEqual(status, 404) # sanity
|
||||||
|
|
||||||
|
# Now we can delete
|
||||||
|
status, headers, body = \
|
||||||
|
self.conn.make_request('DELETE', bucket)
|
||||||
|
self.assertEqual(status, 204) # sanity
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest2.main()
|
unittest2.main()
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
@ -64,8 +65,15 @@ class TestS3ApiMultiDelete(S3ApiTestCase):
|
|||||||
swob.HTTPNoContent, {}, None)
|
swob.HTTPNoContent, {}, None)
|
||||||
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key2',
|
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key2',
|
||||||
swob.HTTPNotFound, {}, None)
|
swob.HTTPNotFound, {}, None)
|
||||||
|
slo_delete_resp = {
|
||||||
|
'Number Not Found': 0,
|
||||||
|
'Response Status': '200 OK',
|
||||||
|
'Errors': [],
|
||||||
|
'Response Body': '',
|
||||||
|
'Number Deleted': 8
|
||||||
|
}
|
||||||
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key3',
|
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key3',
|
||||||
swob.HTTPOk, {}, None)
|
swob.HTTPOk, {}, json.dumps(slo_delete_resp))
|
||||||
|
|
||||||
elem = Element('Delete')
|
elem = Element('Delete')
|
||||||
for key in ['Key1', 'Key2', 'Key3']:
|
for key in ['Key1', 'Key2', 'Key3']:
|
||||||
@ -97,15 +105,31 @@ class TestS3ApiMultiDelete(S3ApiTestCase):
|
|||||||
|
|
||||||
@s3acl
|
@s3acl
|
||||||
def test_object_multi_DELETE_with_error(self):
|
def test_object_multi_DELETE_with_error(self):
|
||||||
self.swift.register('HEAD', '/v1/AUTH_test/bucket/Key3',
|
|
||||||
swob.HTTPForbidden, {}, None)
|
|
||||||
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key1',
|
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key1',
|
||||||
swob.HTTPNoContent, {}, None)
|
swob.HTTPNoContent, {}, None)
|
||||||
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key2',
|
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key2',
|
||||||
swob.HTTPNotFound, {}, None)
|
swob.HTTPNotFound, {}, None)
|
||||||
|
self.swift.register('HEAD', '/v1/AUTH_test/bucket/Key3',
|
||||||
|
swob.HTTPForbidden, {}, None)
|
||||||
|
self.swift.register('HEAD', '/v1/AUTH_test/bucket/Key4',
|
||||||
|
swob.HTTPOk,
|
||||||
|
{'x-static-large-object': 'True'},
|
||||||
|
None)
|
||||||
|
slo_delete_resp = {
|
||||||
|
'Number Not Found': 0,
|
||||||
|
'Response Status': '400 Bad Request',
|
||||||
|
'Errors': [
|
||||||
|
["/bucket+segments/obj1", "403 Forbidden"],
|
||||||
|
["/bucket+segments/obj2", "403 Forbidden"]
|
||||||
|
],
|
||||||
|
'Response Body': '',
|
||||||
|
'Number Deleted': 8
|
||||||
|
}
|
||||||
|
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key4',
|
||||||
|
swob.HTTPOk, {}, json.dumps(slo_delete_resp))
|
||||||
|
|
||||||
elem = Element('Delete')
|
elem = Element('Delete')
|
||||||
for key in ['Key1', 'Key2', 'Key3']:
|
for key in ['Key1', 'Key2', 'Key3', 'Key4']:
|
||||||
obj = SubElement(elem, 'Object')
|
obj = SubElement(elem, 'Object')
|
||||||
SubElement(obj, 'Key').text = key
|
SubElement(obj, 'Key').text = key
|
||||||
body = tostring(elem, use_s3ns=False)
|
body = tostring(elem, use_s3ns=False)
|
||||||
@ -123,13 +147,24 @@ class TestS3ApiMultiDelete(S3ApiTestCase):
|
|||||||
|
|
||||||
elem = fromstring(body)
|
elem = fromstring(body)
|
||||||
self.assertEqual(len(elem.findall('Deleted')), 2)
|
self.assertEqual(len(elem.findall('Deleted')), 2)
|
||||||
self.assertEqual(len(elem.findall('Error')), 1)
|
self.assertEqual(len(elem.findall('Error')), 2)
|
||||||
|
self.assertEqual(
|
||||||
|
[(el.find('Code').text, el.find('Message').text)
|
||||||
|
for el in elem.findall('Error')],
|
||||||
|
[('AccessDenied', 'Access Denied.'),
|
||||||
|
('SLODeleteError', '\n'.join([
|
||||||
|
'400 Bad Request',
|
||||||
|
'/bucket+segments/obj1: 403 Forbidden',
|
||||||
|
'/bucket+segments/obj2: 403 Forbidden']))]
|
||||||
|
)
|
||||||
self.assertEqual(self.swift.calls, [
|
self.assertEqual(self.swift.calls, [
|
||||||
('HEAD', '/v1/AUTH_test/bucket'),
|
('HEAD', '/v1/AUTH_test/bucket'),
|
||||||
('HEAD', '/v1/AUTH_test/bucket/Key1'),
|
('HEAD', '/v1/AUTH_test/bucket/Key1'),
|
||||||
('DELETE', '/v1/AUTH_test/bucket/Key1'),
|
('DELETE', '/v1/AUTH_test/bucket/Key1'),
|
||||||
('HEAD', '/v1/AUTH_test/bucket/Key2'),
|
('HEAD', '/v1/AUTH_test/bucket/Key2'),
|
||||||
('HEAD', '/v1/AUTH_test/bucket/Key3'),
|
('HEAD', '/v1/AUTH_test/bucket/Key3'),
|
||||||
|
('HEAD', '/v1/AUTH_test/bucket/Key4'),
|
||||||
|
('DELETE', '/v1/AUTH_test/bucket/Key4?multipart-manifest=delete'),
|
||||||
])
|
])
|
||||||
|
|
||||||
@s3acl
|
@s3acl
|
||||||
|
Loading…
Reference in New Issue
Block a user