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
This commit is contained in:
Tim Burke 2016-06-15 14:04:12 -07:00
parent 8019c2234c
commit 7ff06d58fd
3 changed files with 101 additions and 40 deletions

View File

@ -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())

View File

@ -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'

View File

@ -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):