diff --git a/swift/common/middleware/slo.py b/swift/common/middleware/slo.py index bbe2cdca0b..0ce8d6e351 100644 --- a/swift/common/middleware/slo.py +++ b/swift/common/middleware/slo.py @@ -901,7 +901,7 @@ class SloGetContext(WSGIContext): seg_dict['size_bytes'] = seg_dict.pop('bytes', None) seg_dict['etag'] = seg_dict.pop('hash', None) - json_data = json.dumps(segments) # convert to string + json_data = json.dumps(segments, sort_keys=True) # convert to string if six.PY3: json_data = json_data.encode('utf-8') @@ -909,6 +909,8 @@ class SloGetContext(WSGIContext): for header, value in resp_headers: if header.lower() == 'content-length': new_headers.append(('Content-Length', len(json_data))) + elif header.lower() == 'etag': + new_headers.append(('Etag', md5(json_data).hexdigest())) else: new_headers.append((header, value)) self._response_headers = new_headers diff --git a/test/functional/test_slo.py b/test/functional/test_slo.py index a07d91c496..c055f7bbd3 100644 --- a/test/functional/test_slo.py +++ b/test/functional/test_slo.py @@ -1098,6 +1098,12 @@ class TestSlo(Base): manifest = self.env.container.file("manifest-db") got_body = manifest.read(parms={'multipart-manifest': 'get', 'format': 'raw'}) + body_md5 = hashlib.md5(got_body).hexdigest() + headers = dict( + (h.lower(), v) + for h, v in manifest.conn.response.getheaders()) + self.assertIn('etag', headers) + self.assertEqual(headers['etag'], body_md5) # raw format should have the actual manifest object content-type self.assertEqual('application/octet-stream', manifest.content_type) diff --git a/test/unit/common/middleware/test_slo.py b/test/unit/common/middleware/test_slo.py index 980352c8dc..ebdd65b036 100644 --- a/test/unit/common/middleware/test_slo.py +++ b/test/unit/common/middleware/test_slo.py @@ -1724,27 +1724,30 @@ class TestSloGetRawManifest(SloTestCase): 'HTTP_ACCEPT': 'application/json'}) status, headers, body = self.call_slo(req) + expected_body = json.dumps([ + {'etag': md5hex('b' * 10), 'size_bytes': '10', + 'path': '/gettest/b_10'}, + {'etag': md5hex('c' * 15), 'size_bytes': '15', + 'path': '/gettest/c_15'}, + {'etag': md5hex(md5hex("e" * 5) + md5hex("f" * 5)), + 'size_bytes': '10', + 'path': '/gettest/d_10'}], sort_keys=True) + expected_etag = md5hex(expected_body) + if six.PY3: + expected_body = expected_body.encode('utf-8') + + self.assertEqual(body, expected_body) self.assertEqual(status, '200 OK') - self.assertTrue(('Etag', self.bc_etag) in headers, headers) + self.assertTrue(('Etag', expected_etag) in headers, headers) self.assertTrue(('X-Static-Large-Object', 'true') in headers, headers) # raw format should return the actual manifest object content-type self.assertIn(('Content-Type', 'text/plain'), headers) try: - resp_data = json.loads(body) + json.loads(body) except ValueError: self.fail("Invalid JSON in manifest GET: %r" % body) - self.assertEqual( - resp_data, - [{'etag': md5hex('b' * 10), 'size_bytes': '10', - 'path': '/gettest/b_10'}, - {'etag': md5hex('c' * 15), 'size_bytes': '15', - 'path': '/gettest/c_15'}, - {'etag': md5hex(md5hex("e" * 5) + md5hex("f" * 5)), - 'size_bytes': '10', - 'path': '/gettest/d_10'}]) - def test_get_raw_manifest_passthrough_with_ranges(self): req = Request.blank( '/v1/AUTH_test/gettest/manifest-bc-r'