Skip to content

Commit b35fc41

Browse files
committed
s3api: Delete multipart uploads via multi-delete
We have code that's *supposed* to do it, but we weren't reading the result of the bulk-delete, so we never actually deleted anything! Change-Id: I5c972749cadf903161456f34371a6f83ebc05eb9 Closes-Bug: 1810567
1 parent 7b1679b commit b35fc41

File tree

3 files changed

+88
-7
lines changed

3 files changed

+88
-7
lines changed

swift/common/middleware/s3api/controllers/multi_delete.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# limitations under the License.
1515

1616
import copy
17+
import json
1718

1819
from swift.common.constraints import MAX_OBJECT_NAME_LENGTH
1920
from swift.common.utils import public, StreamingPile
@@ -115,7 +116,30 @@ def do_delete(base_req, key, version):
115116

116117
try:
117118
query = req.gen_multipart_manifest_delete_query(self.app)
118-
req.get_response(self.app, method='DELETE', query=query)
119+
resp = req.get_response(self.app, method='DELETE', query=query,
120+
headers={'Accept': 'application/json'})
121+
# Have to read the response to actually do the SLO delete
122+
if query:
123+
try:
124+
delete_result = json.loads(resp.body)
125+
if delete_result['Errors']:
126+
# NB: bulk includes 404s in "Number Not Found",
127+
# not "Errors"
128+
msg_parts = [delete_result['Response Status']]
129+
msg_parts.extend(
130+
'%s: %s' % (obj, status)
131+
for obj, status in delete_result['Errors'])
132+
return key, {'code': 'SLODeleteError',
133+
'message': '\n'.join(msg_parts)}
134+
# else, all good
135+
except (ValueError, TypeError, KeyError):
136+
# Logs get all the gory details
137+
self.logger.exception(
138+
'Could not parse SLO delete response: %r',
139+
resp.body)
140+
# Client gets something more generic
141+
return key, {'code': 'SLODeleteError',
142+
'message': 'Unexpected swift response'}
119143
except NoSuchKey:
120144
pass
121145
except ErrorResponse as e:

test/functional/s3api/test_multi_upload.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232

3333
from test.functional.s3api import S3ApiBase
3434
from test.functional.s3api.s3_test_client import Connection
35-
from test.functional.s3api.utils import get_error_code, get_error_msg
35+
from test.functional.s3api.utils import get_error_code, get_error_msg, \
36+
calculate_md5
3637

3738

3839
def setUpModule():
@@ -907,6 +908,27 @@ def test_delete_bucket_multi_upload_object_exisiting(self):
907908
self.assertEqual(status, 200) # sanity
908909
self.assertEqual(content, body) # sanity
909910

911+
# Can delete it with DeleteMultipleObjects request
912+
elem = Element('Delete')
913+
SubElement(elem, 'Quiet').text = 'true'
914+
obj_elem = SubElement(elem, 'Object')
915+
SubElement(obj_elem, 'Key').text = key
916+
body = tostring(elem, use_s3ns=False)
917+
918+
status, headers, body = self.conn.make_request(
919+
'POST', bucket, body=body, query='delete',
920+
headers={'Content-MD5': calculate_md5(body)})
921+
self.assertEqual(status, 200)
922+
self.assertCommonResponseHeaders(headers)
923+
924+
status, headers, body = \
925+
self.conn.make_request('GET', bucket, key)
926+
self.assertEqual(status, 404) # sanity
927+
928+
# Now we can delete
929+
status, headers, body = \
930+
self.conn.make_request('DELETE', bucket)
931+
self.assertEqual(status, 204) # sanity
910932

911933
if __name__ == '__main__':
912934
unittest2.main()

test/unit/common/middleware/s3api/test_multi_delete.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import json
1617
import unittest
1718
from datetime import datetime
1819
from hashlib import md5
@@ -64,8 +65,15 @@ def test_object_multi_DELETE(self):
6465
swob.HTTPNoContent, {}, None)
6566
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key2',
6667
swob.HTTPNotFound, {}, None)
68+
slo_delete_resp = {
69+
'Number Not Found': 0,
70+
'Response Status': '200 OK',
71+
'Errors': [],
72+
'Response Body': '',
73+
'Number Deleted': 8
74+
}
6775
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key3',
68-
swob.HTTPOk, {}, None)
76+
swob.HTTPOk, {}, json.dumps(slo_delete_resp))
6977

7078
elem = Element('Delete')
7179
for key in ['Key1', 'Key2', 'Key3']:
@@ -97,15 +105,31 @@ def test_object_multi_DELETE(self):
97105

98106
@s3acl
99107
def test_object_multi_DELETE_with_error(self):
100-
self.swift.register('HEAD', '/v1/AUTH_test/bucket/Key3',
101-
swob.HTTPForbidden, {}, None)
102108
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key1',
103109
swob.HTTPNoContent, {}, None)
104110
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key2',
105111
swob.HTTPNotFound, {}, None)
112+
self.swift.register('HEAD', '/v1/AUTH_test/bucket/Key3',
113+
swob.HTTPForbidden, {}, None)
114+
self.swift.register('HEAD', '/v1/AUTH_test/bucket/Key4',
115+
swob.HTTPOk,
116+
{'x-static-large-object': 'True'},
117+
None)
118+
slo_delete_resp = {
119+
'Number Not Found': 0,
120+
'Response Status': '400 Bad Request',
121+
'Errors': [
122+
["/bucket+segments/obj1", "403 Forbidden"],
123+
["/bucket+segments/obj2", "403 Forbidden"]
124+
],
125+
'Response Body': '',
126+
'Number Deleted': 8
127+
}
128+
self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key4',
129+
swob.HTTPOk, {}, json.dumps(slo_delete_resp))
106130

107131
elem = Element('Delete')
108-
for key in ['Key1', 'Key2', 'Key3']:
132+
for key in ['Key1', 'Key2', 'Key3', 'Key4']:
109133
obj = SubElement(elem, 'Object')
110134
SubElement(obj, 'Key').text = key
111135
body = tostring(elem, use_s3ns=False)
@@ -123,13 +147,24 @@ def test_object_multi_DELETE_with_error(self):
123147

124148
elem = fromstring(body)
125149
self.assertEqual(len(elem.findall('Deleted')), 2)
126-
self.assertEqual(len(elem.findall('Error')), 1)
150+
self.assertEqual(len(elem.findall('Error')), 2)
151+
self.assertEqual(
152+
[(el.find('Code').text, el.find('Message').text)
153+
for el in elem.findall('Error')],
154+
[('AccessDenied', 'Access Denied.'),
155+
('SLODeleteError', '\n'.join([
156+
'400 Bad Request',
157+
'/bucket+segments/obj1: 403 Forbidden',
158+
'/bucket+segments/obj2: 403 Forbidden']))]
159+
)
127160
self.assertEqual(self.swift.calls, [
128161
('HEAD', '/v1/AUTH_test/bucket'),
129162
('HEAD', '/v1/AUTH_test/bucket/Key1'),
130163
('DELETE', '/v1/AUTH_test/bucket/Key1'),
131164
('HEAD', '/v1/AUTH_test/bucket/Key2'),
132165
('HEAD', '/v1/AUTH_test/bucket/Key3'),
166+
('HEAD', '/v1/AUTH_test/bucket/Key4'),
167+
('DELETE', '/v1/AUTH_test/bucket/Key4?multipart-manifest=delete'),
133168
])
134169

135170
@s3acl

0 commit comments

Comments
 (0)