From 7ff06d58fd2af150d479cacf9e7fc3a546f5a02b Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Wed, 15 Jun 2016 14:04:12 -0700 Subject: [PATCH] Allow copying of null version Even though we don't support versioning yet, we can at least tolerate a client that explicitly requests a null versionId The syntax is described at http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectCOPY.html Change-Id: Iedd8cc1b0c6f3a770f28be74eafc58bbef0259ad --- swift3/request.py | 28 ++++++++-- swift3/test/functional/test_object.py | 35 ++++++++++++ swift3/test/unit/test_obj.py | 78 +++++++++++++++------------ 3 files changed, 101 insertions(+), 40 deletions(-) diff --git a/swift3/request.py b/swift3/request.py index 3abfd8a1..9248949f 100644 --- a/swift3/request.py +++ b/swift3/request.py @@ -19,8 +19,9 @@ from hashlib import sha1, sha256, md5 import hmac import re import six +# pylint: disable-msg=import-error +from six.moves.urllib.parse import quote, unquote, parse_qsl import string -from urllib import quote, unquote from swift.common.utils import split_path from swift.common import swob @@ -728,13 +729,30 @@ class Request(swob.Request): :returns: the source HEAD response """ - if 'X-Amz-Copy-Source' not in self.headers: + try: + src_path = self.headers['X-Amz-Copy-Source'] + except KeyError: return None - src_path = unquote(self.headers['X-Amz-Copy-Source']) - src_path = src_path if src_path.startswith('/') else \ - ('/' + src_path) + if '?' in src_path: + src_path, qs = src_path.split('?', 1) + query = parse_qsl(qs, True) + if not query: + pass # ignore it + elif len(query) > 1 or query[0][0] != 'versionId': + raise InvalidArgument('X-Amz-Copy-Source', + self.headers['X-Amz-Copy-Source'], + 'Unsupported copy source parameter.') + elif query[0][1] != 'null': + # TODO: once we support versioning, we'll need to translate + # src_path to the proper location in the versions container + raise S3NotImplemented('Versioning is not yet supported') + self.headers['X-Amz-Copy-Source'] = src_path + + src_path = unquote(src_path) + src_path = src_path if src_path.startswith('/') else ('/' + src_path) src_bucket, src_obj = split_path(src_path, 0, 2, True) + headers = swob.HeaderKeyDict() headers.update(self._copy_source_headers()) diff --git a/swift3/test/functional/test_object.py b/swift3/test/functional/test_object.py index 04c39c5b..bb0571f7 100644 --- a/swift3/test/functional/test_object.py +++ b/swift3/test/functional/test_object.py @@ -370,6 +370,41 @@ class TestSwift3Object(Swift3FunctionalTestCase): self.assertCommonResponseHeaders(headers) self._assertObjectEtag(self.bucket, obj, etag) + def test_put_object_copy_source_params(self): + obj = 'object' + src_headers = {'X-Amz-Meta-Test': 'src'} + src_body = 'some content' + dst_bucket = 'dst-bucket' + dst_obj = 'dst_object' + self.conn.make_request('PUT', self.bucket, obj, src_headers, src_body) + self.conn.make_request('PUT', dst_bucket) + + headers = {'X-Amz-Copy-Source': '/%s/%s?nonsense' % ( + self.bucket, obj)} + status, headers, body = \ + self.conn.make_request('PUT', dst_bucket, dst_obj, headers) + self.assertEqual(status, 400) + self.assertEqual(get_error_code(body), 'InvalidArgument') + + headers = {'X-Amz-Copy-Source': '/%s/%s?versionId=null&nonsense' % ( + self.bucket, obj)} + status, headers, body = \ + self.conn.make_request('PUT', dst_bucket, dst_obj, headers) + self.assertEqual(status, 400) + self.assertEqual(get_error_code(body), 'InvalidArgument') + + headers = {'X-Amz-Copy-Source': '/%s/%s?versionId=null' % ( + self.bucket, obj)} + status, headers, body = \ + self.conn.make_request('PUT', dst_bucket, dst_obj, headers) + self.assertEqual(status, 200) + self.assertCommonResponseHeaders(headers) + status, headers, body = \ + self.conn.make_request('GET', dst_bucket, dst_obj) + self.assertEqual(status, 200) + self.assertEqual(headers['x-amz-meta-test'], 'src') + self.assertEqual(body, src_body) + def test_put_object_copy_source(self): obj = 'object' content = 'abcdefghij' diff --git a/swift3/test/unit/test_obj.py b/swift3/test/unit/test_obj.py index 898bc0d1..cbe5a666 100644 --- a/swift3/test/unit/test_obj.py +++ b/swift3/test/unit/test_obj.py @@ -417,6 +417,28 @@ class TestSwift3Obj(Swift3TestCase): swob.HTTPCreated, {'X-Amz-Copy-Source': '/bucket/'}) self.assertEqual(code, 'InvalidArgument') + code = self._test_method_error( + 'PUT', '/bucket/object', + swob.HTTPCreated, + {'X-Amz-Copy-Source': '/bucket/src_obj?foo=bar'}) + self.assertEqual(code, 'InvalidArgument') + # adding other query paramerters will cause an error + code = self._test_method_error( + 'PUT', '/bucket/object', + swob.HTTPCreated, + {'X-Amz-Copy-Source': '/bucket/src_obj?versionId=foo&bar=baz'}) + self.assertEqual(code, 'InvalidArgument') + # ...even versionId appears in the last + code = self._test_method_error( + 'PUT', '/bucket/object', + swob.HTTPCreated, + {'X-Amz-Copy-Source': '/bucket/src_obj?bar=baz&versionId=foo'}) + self.assertEqual(code, 'InvalidArgument') + code = self._test_method_error( + 'PUT', '/bucket/object', + swob.HTTPCreated, + {'X-Amz-Copy-Source': '/bucket/src_obj?versionId=foo'}) + self.assertEqual(code, 'NotImplemented') code = self._test_method_error( 'PUT', '/bucket/object', swob.HTTPCreated, @@ -535,46 +557,32 @@ class TestSwift3Obj(Swift3TestCase): @s3acl def test_object_PUT_copy(self): - date_header = self.get_date_header() - timestamp = mktime(date_header) - last_modified = S3Timestamp(timestamp).s3xmlformat - status, headers, body = self._test_object_PUT_copy( - swob.HTTPOk, put_header={'Date': date_header}, - timestamp=timestamp) - self.assertEqual(status.split()[0], '200') - self.assertEqual(headers['Content-Type'], 'application/xml') + def do_test(src_path=None): + date_header = self.get_date_header() + timestamp = mktime(date_header) + last_modified = S3Timestamp(timestamp).s3xmlformat + status, headers, body = self._test_object_PUT_copy( + swob.HTTPOk, put_header={'Date': date_header}, + timestamp=timestamp, src_path=src_path) + self.assertEqual(status.split()[0], '200') + self.assertEqual(headers['Content-Type'], 'application/xml') - self.assertTrue(headers.get('etag') is None) - self.assertTrue(headers.get('x-amz-meta-something') is None) - elem = fromstring(body, 'CopyObjectResult') - self.assertEqual(elem.find('LastModified').text, last_modified) - self.assertEqual(elem.find('ETag').text, '"%s"' % self.etag) + self.assertTrue(headers.get('etag') is None) + self.assertTrue(headers.get('x-amz-meta-something') is None) + elem = fromstring(body, 'CopyObjectResult') + self.assertEqual(elem.find('LastModified').text, last_modified) + self.assertEqual(elem.find('ETag').text, '"%s"' % self.etag) - _, _, headers = self.swift.calls_with_headers[-1] - self.assertEqual(headers['X-Copy-From'], '/some/source') - self.assertEqual(headers['Content-Length'], '0') + _, _, headers = self.swift.calls_with_headers[-1] + self.assertEqual(headers['X-Copy-From'], '/some/source') + self.assertEqual(headers['Content-Length'], '0') - @s3acl - def test_object_PUT_copy_no_slash(self): - date_header = self.get_date_header() - timestamp = mktime(date_header) - last_modified = S3Timestamp(timestamp).s3xmlformat + do_test('/some/source') + do_test('/some/source?') + do_test('/some/source?versionId=null') # Some clients (like Boto) don't include the leading slash; # AWS seems to tolerate this so we should, too - status, headers, body = self._test_object_PUT_copy( - swob.HTTPOk, src_path='some/source', - put_header={'Date': date_header}, timestamp=timestamp) - self.assertEqual(status.split()[0], '200') - self.assertEqual(headers['Content-Type'], 'application/xml') - self.assertTrue(headers.get('etag') is None) - self.assertTrue(headers.get('x-amz-meta-something') is None) - elem = fromstring(body, 'CopyObjectResult') - self.assertEqual(elem.find('LastModified').text, last_modified) - self.assertEqual(elem.find('ETag').text, '"%s"' % self.etag) - - _, _, headers = self.swift.calls_with_headers[-1] - self.assertEqual(headers['X-Copy-From'], '/some/source') - self.assertEqual(headers['Content-Length'], '0') + do_test('some/source') @s3acl def test_object_PUT_copy_self(self):