Merge "WIP mpu: introduce s3-compat mode for object-versioning" into feature/mpu
This commit is contained in:
@@ -21,7 +21,8 @@ from urllib.parse import quote
|
||||
from swift.common import swob
|
||||
from swift.common.http import HTTP_OK
|
||||
from swift.common.middleware.versioned_writes.object_versioning import \
|
||||
DELETE_MARKER_CONTENT_TYPE, validate_version
|
||||
DELETE_MARKER_CONTENT_TYPE, validate_version, \
|
||||
SYSMETA_S3_COMPATIBLE_VERSIONS
|
||||
from swift.common.utils import json, public, config_true_value, cap_length
|
||||
from swift.common.registry import get_swift_info
|
||||
|
||||
@@ -388,6 +389,10 @@ class BucketController(Controller):
|
||||
# s3api cannot support multiple regions currently.
|
||||
raise InvalidLocationConstraint()
|
||||
|
||||
# default to s3 compat
|
||||
# TODO: need backwards compatibility with containers already using the
|
||||
# swift mode
|
||||
req.headers[SYSMETA_S3_COMPATIBLE_VERSIONS] = 'true'
|
||||
resp = req.get_response(self.app)
|
||||
|
||||
resp.status = HTTP_OK
|
||||
|
||||
@@ -334,7 +334,7 @@ class ObjectController(Controller):
|
||||
pass # drain the bulk-deleter response
|
||||
resp.status = HTTP_NO_CONTENT
|
||||
resp.body = b''
|
||||
if resp.sw_headers.get('X-Object-Current-Version-Id') == 'null':
|
||||
if resp.sw_headers.get('X-Object-Current-Version-Id') == 'none':
|
||||
new_resp = self._restore_on_delete(req)
|
||||
if new_resp:
|
||||
resp = new_resp
|
||||
|
||||
@@ -77,6 +77,7 @@ class VersioningController(Controller):
|
||||
# Set up versioning
|
||||
# NB: object_versioning responsible for ensuring its container exists
|
||||
req.headers['X-Versions-Enabled'] = str(status == 'Enabled').lower()
|
||||
req.headers['X-S3-Compatible-Versions'] = 'true'
|
||||
req.get_response(self.app, 'POST')
|
||||
|
||||
return HTTPOk()
|
||||
|
||||
@@ -62,7 +62,7 @@ from swift.common.middleware.s3api.s3response import AccessDenied, \
|
||||
AuthorizationQueryParametersError, ServiceUnavailable, BrokenMPU, \
|
||||
XAmzContentSHA256Mismatch, IncompleteBody, InvalidChunkSizeError, \
|
||||
InvalidPartNumber, InvalidPartArgument, MalformedTrailerError, \
|
||||
NoSuchUpload
|
||||
NoSuchUpload, NoSuchVersion
|
||||
from swift.common.middleware.s3api.exception import NotS3Request, \
|
||||
S3InputError, S3InputSizeError, S3InputIncomplete, \
|
||||
S3InputChunkSignatureMismatch, S3InputChunkTooSmall, \
|
||||
@@ -2073,6 +2073,9 @@ class S3Request(swob.Request):
|
||||
def not_found_handler():
|
||||
if is_success(get_container_info(
|
||||
env, app, swift_source='S3').get('status')):
|
||||
version_id = self.params.get('versionId')
|
||||
if version_id:
|
||||
return NoSuchVersion(obj, version_id)
|
||||
return NoSuchKey(obj)
|
||||
return NoSuchBucket(container)
|
||||
|
||||
|
||||
@@ -177,6 +177,10 @@ DELETE_MARKER_CONTENT_TYPE = 'application/x-deleted;swift_versions_deleted=1'
|
||||
CLIENT_VERSIONS_ENABLED = 'x-versions-enabled'
|
||||
SYSMETA_VERSIONS_ENABLED = \
|
||||
get_sys_meta_prefix('container') + 'versions-enabled'
|
||||
S3_COMPATIBLE_VERSIONS = 's3-compatible-versions'
|
||||
CLIENT_S3_COMPATIBLE_VERSIONS = 'x-' + S3_COMPATIBLE_VERSIONS
|
||||
SYSMETA_S3_COMPATIBLE_VERSIONS = \
|
||||
get_sys_meta_prefix('container') + S3_COMPATIBLE_VERSIONS
|
||||
SYSMETA_VERSIONS_CONT = get_sys_meta_prefix('container') + 'versions-container'
|
||||
SYSMETA_PARENT_CONT = get_sys_meta_prefix('container') + 'parent-container'
|
||||
SYSMETA_VERSIONS_SYMLINK = get_sys_meta_prefix('object') + 'versions-symlink'
|
||||
@@ -227,8 +231,9 @@ def build_versions_object_name(object_name, version):
|
||||
# Drop any offset from ts. Timestamp offsets are never exposed to
|
||||
# clients, so Timestamp.normal is sufficient to define a version as
|
||||
# perceived by clients.
|
||||
inv = ~Timestamp(Timestamp(version).normal)
|
||||
return get_reserved_name(object_name, inv.internal)
|
||||
if version != 'null':
|
||||
version = (~Timestamp(Timestamp(version).normal)).internal
|
||||
return get_reserved_name(object_name, version)
|
||||
|
||||
|
||||
def build_versions_object_marker(object_name):
|
||||
@@ -251,7 +256,10 @@ def parse_versions_object_name(versioned_name):
|
||||
"""
|
||||
try:
|
||||
name, suffix = split_reserved_name(versioned_name)
|
||||
version = (~Timestamp(suffix)).internal
|
||||
if suffix == 'null':
|
||||
version = suffix
|
||||
else:
|
||||
version = (~Timestamp(suffix)).internal
|
||||
except ValueError:
|
||||
return versioned_name, None
|
||||
return name, version
|
||||
@@ -318,7 +326,7 @@ class ObjectVersioningContext(WSGIContext):
|
||||
|
||||
class ObjectContext(ObjectVersioningContext):
|
||||
def __init__(self, wsgi_app, logger, api_version, account,
|
||||
container, obj, versions_cont, is_enabled):
|
||||
container, obj, versions_cont, is_enabled, s3_compat):
|
||||
"""
|
||||
Note that account, container, obj should be unquoted by caller
|
||||
if the url path is under url encoding (e.g. %FF)
|
||||
@@ -339,6 +347,7 @@ class ObjectContext(ObjectVersioningContext):
|
||||
self.obj = obj
|
||||
self.versions_cont = versions_cont
|
||||
self.is_enabled = is_enabled
|
||||
self.s3_compat = s3_compat
|
||||
|
||||
def get_version(self, req):
|
||||
"""
|
||||
@@ -404,6 +413,7 @@ class ObjectContext(ObjectVersioningContext):
|
||||
put_resp = put_req.get_response(self.app)
|
||||
drain_and_close(put_resp)
|
||||
# the PUT should have already drained source_resp
|
||||
# TODO: why are we trying to close an *iter*?
|
||||
close_if_possible(source_resp.app_iter)
|
||||
return put_resp
|
||||
|
||||
@@ -547,8 +557,12 @@ class ObjectContext(ObjectVersioningContext):
|
||||
|
||||
# if there's an existing object, then copy it to
|
||||
# X-Versions-Location
|
||||
version = self.get_null_version(get_resp)
|
||||
get_resp.headers.pop('x-timestamp', None)
|
||||
if self.s3_compat:
|
||||
version = 'null'
|
||||
else:
|
||||
version = self.get_null_version(get_resp)
|
||||
get_resp.headers.pop('x-timestamp', None)
|
||||
|
||||
vers_obj_name = build_versions_object_name(self.obj, version)
|
||||
put_path_info = "/%s/%s/%s/%s" % (
|
||||
self.api_version, self.account, self.versions_cont, vers_obj_name)
|
||||
@@ -574,16 +588,20 @@ class ObjectContext(ObjectVersioningContext):
|
||||
|
||||
:param req: original request.
|
||||
"""
|
||||
# handle object request for a disabled versioned container.
|
||||
if not self.is_enabled:
|
||||
if self.is_enabled:
|
||||
# attempt to copy current object to versions container
|
||||
self._copy_current(req)
|
||||
# then put to versions container
|
||||
req.ensure_x_timestamp()
|
||||
version = self.get_version(req)
|
||||
elif self.s3_compat:
|
||||
# put to versions container while versioning is suspended
|
||||
version = 'null'
|
||||
else:
|
||||
# put to user container while versioning is suspended
|
||||
return req.get_response(self.app)
|
||||
|
||||
# attempt to copy current object to versions container
|
||||
self._copy_current(req)
|
||||
|
||||
# write client's put directly to versioned container
|
||||
req.ensure_x_timestamp()
|
||||
version = self.get_version(req)
|
||||
put_resp, put_vers_obj_name, put_bytes, put_content_type = \
|
||||
self._put_versioned_obj_from_client(req, version)
|
||||
|
||||
@@ -602,13 +620,19 @@ class ObjectContext(ObjectVersioningContext):
|
||||
:param req: original request.
|
||||
"""
|
||||
# handle object request for a disabled versioned container.
|
||||
if not self.is_enabled:
|
||||
if self.is_enabled:
|
||||
# attempt to copy current object to versions container
|
||||
self._copy_current(req)
|
||||
# then put to versions container
|
||||
req.ensure_x_timestamp()
|
||||
version = self.get_version(req)
|
||||
elif self.s3_compat:
|
||||
# put to versions container while versioning is suspended
|
||||
version = 'null'
|
||||
else:
|
||||
# put to user container while versioning is suspended
|
||||
return req.get_response(self.app)
|
||||
|
||||
self._copy_current(req)
|
||||
|
||||
req.ensure_x_timestamp()
|
||||
version = self.get_version(req)
|
||||
marker_name = build_versions_object_name(self.obj, version)
|
||||
marker_path = "/%s/%s/%s/%s" % (
|
||||
self.api_version, self.account, self.versions_cont, marker_name)
|
||||
@@ -681,7 +705,6 @@ class ObjectContext(ObjectVersioningContext):
|
||||
req.environ, path=wsgi_quote(req.path_info) + '?symlink=get',
|
||||
method='HEAD', headers=obj_head_headers, swift_source='OV')
|
||||
hresp = head_req.get_response(self.app)
|
||||
head_is_tombstone = False
|
||||
symlink_target = None
|
||||
if hresp.status_int == HTTP_NOT_FOUND:
|
||||
head_is_tombstone = True
|
||||
@@ -702,8 +725,9 @@ class ObjectContext(ObjectVersioningContext):
|
||||
:param req: original request.
|
||||
:param version: version to delete.
|
||||
"""
|
||||
if version == 'null':
|
||||
# let the request go directly through to the is_latest link
|
||||
if version == 'null' and not (self.s3_compat and self.versions_cont):
|
||||
# let the request go directly through to the is_latest link unless
|
||||
# for an s3-compat versioned container
|
||||
return req.get_response(self.app)
|
||||
auth_token_header = {'X-Auth-Token': req.headers.get('X-Auth-Token')}
|
||||
head_is_tombstone, symlink_target = self._check_head(
|
||||
@@ -711,8 +735,23 @@ class ObjectContext(ObjectVersioningContext):
|
||||
|
||||
versions_obj = build_versions_object_name(self.obj, version)
|
||||
req_obj_path = '%s/%s' % (self.versions_cont, versions_obj)
|
||||
if head_is_tombstone or not symlink_target or (
|
||||
wsgi_unquote(symlink_target) != wsgi_unquote(req_obj_path)):
|
||||
if head_is_tombstone:
|
||||
version_is_latest = False
|
||||
resp_version_id = None
|
||||
elif symlink_target:
|
||||
symlink_target = wsgi_unquote(symlink_target)
|
||||
if symlink_target == wsgi_unquote(req_obj_path):
|
||||
version_is_latest = True
|
||||
resp_version_id = None
|
||||
else:
|
||||
version_is_latest = False
|
||||
_, vers_obj_name = symlink_target.split('/', 1)
|
||||
resp_version_id = parse_versions_object_name(vers_obj_name)[1]
|
||||
else:
|
||||
# de-facto null version (never been copied to versions container)
|
||||
version_is_latest = version == 'null'
|
||||
resp_version_id = None
|
||||
if not version_is_latest:
|
||||
# If there's no current version (i.e., tombstone or unversioned
|
||||
# object) or if current version links to another version, then
|
||||
# just delete the version requested to be deleted
|
||||
@@ -720,11 +759,6 @@ class ObjectContext(ObjectVersioningContext):
|
||||
self.api_version, self.account, self.versions_cont,
|
||||
versions_obj)
|
||||
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
||||
if head_is_tombstone or not symlink_target:
|
||||
resp_version_id = 'null'
|
||||
else:
|
||||
_, vers_obj_name = wsgi_unquote(symlink_target).split('/', 1)
|
||||
resp_version_id = parse_versions_object_name(vers_obj_name)[1]
|
||||
else:
|
||||
# if version-id is the latest version, delete the link too
|
||||
# First, kill the link...
|
||||
@@ -738,10 +772,19 @@ class ObjectContext(ObjectVersioningContext):
|
||||
self.api_version, self.account, self.versions_cont,
|
||||
versions_obj)
|
||||
req.headers['X-Backend-Allow-Reserved-Names'] = 'true'
|
||||
resp_version_id = 'null'
|
||||
resp = req.get_response(self.app)
|
||||
resp.headers['X-Object-Version-Id'] = version
|
||||
resp.headers['X-Object-Current-Version-Id'] = resp_version_id
|
||||
if self.s3_compat and (version_is_latest or not resp_version_id):
|
||||
# The only in-tree use for this header is in the s3api object
|
||||
# response handler, which performs a restore-on-delete if the
|
||||
# header is 'none'.
|
||||
resp.headers['X-Object-Current-Version-Id'] = 'none'
|
||||
elif not resp_version_id:
|
||||
# For backwards compatibility with any out-of-tree use case, 'null'
|
||||
# is returned in the non-s3-compat scenario.
|
||||
resp.headers['X-Object-Current-Version-Id'] = 'null'
|
||||
else:
|
||||
resp.headers['X-Object-Current-Version-Id'] = resp_version_id
|
||||
return resp
|
||||
|
||||
def handle_put_with_version_id(self, req, version):
|
||||
@@ -752,6 +795,7 @@ class ObjectContext(ObjectVersioningContext):
|
||||
:param req: original request.
|
||||
:param version: version to make the latest.
|
||||
"""
|
||||
# this is used by s3api restore-on-delete
|
||||
if req.is_chunked:
|
||||
has_body = (req.body_file.read(1) != b'')
|
||||
elif req.content_length is None:
|
||||
@@ -829,6 +873,10 @@ class ObjectContext(ObjectVersioningContext):
|
||||
if req.method == 'HEAD':
|
||||
drain_and_close(resp)
|
||||
return resp
|
||||
elif self.s3_compat and self.versions_cont:
|
||||
# allow the request to be redirected to the versions container
|
||||
close_if_possible(resp.app_iter)
|
||||
return None
|
||||
elif is_version_link:
|
||||
# Have a latest version, but it's got a real version-id.
|
||||
# Since the user specifically asked for null, return 404
|
||||
@@ -1123,11 +1171,14 @@ class ContainerContext(ObjectVersioningContext):
|
||||
|
||||
versions_cont = container_info.get(
|
||||
'sysmeta', {}).get('versions-container')
|
||||
is_enabled = config_true_value(
|
||||
req.headers[CLIENT_VERSIONS_ENABLED])
|
||||
|
||||
is_enabled = config_true_value(req.headers[CLIENT_VERSIONS_ENABLED])
|
||||
req.headers[SYSMETA_VERSIONS_ENABLED] = is_enabled
|
||||
|
||||
if CLIENT_S3_COMPATIBLE_VERSIONS in req.headers:
|
||||
s3_compat = config_true_value(
|
||||
req.headers[CLIENT_S3_COMPATIBLE_VERSIONS])
|
||||
if not s3_compat:
|
||||
raise HTTPBadRequest('Cannot disable s3 compatible versions')
|
||||
req.headers[SYSMETA_S3_COMPATIBLE_VERSIONS] = s3_compat
|
||||
# TODO: a POST request to a primary container that doesn't exist
|
||||
# will fail, so we will create and delete the versions container
|
||||
# for no reason
|
||||
@@ -1207,6 +1258,20 @@ class ContainerContext(ObjectVersioningContext):
|
||||
self._response_exc_info)
|
||||
return app_resp
|
||||
|
||||
def _insert_null_item(self, listing, null_item):
|
||||
# TODO: this is ok for tests but obvs won't work for paged or marker
|
||||
# listing. We'll need to do a prefix listing for the null and then
|
||||
# insert it in each page of a listing.
|
||||
i = len(listing)
|
||||
for item in reversed(listing):
|
||||
if item['name'] != null_item['name']:
|
||||
break
|
||||
if item['last_modified'] > null_item['last_modified']:
|
||||
break
|
||||
i -= 1
|
||||
listing.insert(i, null_item)
|
||||
return listing
|
||||
|
||||
def _list_versions(self, req, start_response, location, primary_listing):
|
||||
# Only supports JSON listings
|
||||
req.environ['swift.format_listing'] = False
|
||||
@@ -1314,9 +1379,11 @@ class ContainerContext(ObjectVersioningContext):
|
||||
try:
|
||||
listing = json.loads(versions_resp.body)
|
||||
except ValueError:
|
||||
# TODO: fix, body unresolved here
|
||||
app_resp = [body]
|
||||
else:
|
||||
versions_listing = []
|
||||
null_item = None
|
||||
for item in listing:
|
||||
if 'name' not in item:
|
||||
# remove reserved chars from subdir
|
||||
@@ -1346,7 +1413,16 @@ class ContainerContext(ObjectVersioningContext):
|
||||
|
||||
item['name'] = name
|
||||
item['version_id'] = version
|
||||
versions_listing.append(item)
|
||||
if null_item and null_item['name'] != name:
|
||||
self._insert_null_item(versions_listing, null_item)
|
||||
null_item = None
|
||||
if version == 'null':
|
||||
null_item = item
|
||||
else:
|
||||
versions_listing.append(item)
|
||||
|
||||
if null_item:
|
||||
self._insert_null_item(versions_listing, null_item)
|
||||
|
||||
subdir_listing = [{'subdir': s} for s in subdir_set]
|
||||
broken_listing = []
|
||||
@@ -1527,15 +1603,16 @@ class ObjectVersioningMiddleware(object):
|
||||
container_info = get_container_info(
|
||||
req.environ, self.app, swift_source='OV')
|
||||
|
||||
versions_cont = container_info.get(
|
||||
'sysmeta', {}).get('versions-container', '')
|
||||
sysmeta = container_info.get('sysmeta', {})
|
||||
versions_cont = sysmeta.get('versions-container', '')
|
||||
if versions_cont:
|
||||
versions_cont = wsgi_unquote(str_to_wsgi(
|
||||
versions_cont)).split('/')[0]
|
||||
is_enabled = is_versioning_enabled(container_info)
|
||||
s3_compat = config_true_value(sysmeta.get(S3_COMPATIBLE_VERSIONS))
|
||||
object_ctx = ObjectContext(
|
||||
self.app, self.logger, api_version, account, container, obj,
|
||||
versions_cont, is_enabled)
|
||||
versions_cont, is_enabled, s3_compat)
|
||||
return object_ctx.handle_request(req)
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
|
||||
@@ -138,6 +138,7 @@ class TestObjectVersioningBase(Base):
|
||||
# since it gets disabled in tearDown
|
||||
self.env.container.update_metadata(
|
||||
hdrs={self.env.versions_header_key: 'True'})
|
||||
self.maxDiff = None
|
||||
|
||||
def _tear_down_files(self, container):
|
||||
try:
|
||||
@@ -1121,6 +1122,8 @@ class TestObjectVersioning(TestObjectVersioningBase):
|
||||
obj.info(parms={'version-id': dm_version_id})
|
||||
resp_headers = {
|
||||
h.lower(): v for h, v in cm.exception.headers}
|
||||
self.assertIn('x-object-version-id', resp_headers,
|
||||
(cm.exception.status, cm.exception.details))
|
||||
self.assertEqual(dm_version_id,
|
||||
resp_headers['x-object-version-id'])
|
||||
self.assertEqual(DELETE_MARKER_CONTENT_TYPE,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from unittest import mock
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
import io
|
||||
@@ -93,6 +94,21 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
self.clear_bucket(self.client, self.bucket_name)
|
||||
super(TestObjectVersioning, self).tearDown()
|
||||
|
||||
def assert_no_such_key(self, bucket_name, obj_name):
|
||||
with self.assertRaises(ClientError) as caught:
|
||||
self.client.get_object(Bucket=bucket_name, Key=obj_name)
|
||||
expected_err = 'An error occurred (NoSuchKey) when calling the ' \
|
||||
'GetObject operation: The specified key does not exist.'
|
||||
self.assertEqual(expected_err, str(caught.exception))
|
||||
|
||||
def assert_no_such_version(self, bucket_name, obj_name, version_id):
|
||||
with self.assertRaises(ClientError) as caught:
|
||||
self.client.get_object(Bucket=bucket_name, Key=obj_name,
|
||||
VersionId=version_id)
|
||||
expected_err = 'An error occurred (NoSuchVersion) when calling the ' \
|
||||
'GetObject operation: The specified version does not exist.'
|
||||
self.assertEqual(expected_err, str(caught.exception))
|
||||
|
||||
def test_setup(self):
|
||||
bucket_name = self.create_name('new-bucket')
|
||||
resp = self.client.create_bucket(Bucket=bucket_name)
|
||||
@@ -152,6 +168,296 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
self.assertEqual('Suspended', resp['Status'])
|
||||
retry(check_status)
|
||||
|
||||
def test_no_overwrite_while_versioning_enabled(self):
|
||||
# verify that the null version prior to versioning being enabled will
|
||||
# *not* become a version if it is not overwritten while versioning is
|
||||
# enabled.
|
||||
obj_name = self.create_name('unversioned-obj')
|
||||
self.client.upload_fileobj(io.BytesIO(b'some-data'),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self.enable_versioning()
|
||||
resp = self.get_versioning_status()
|
||||
self.assertEqual('Enabled', resp.get('Status'), resp)
|
||||
|
||||
self.disable_versioning()
|
||||
resp = self.get_versioning_status()
|
||||
self.assertEqual('Suspended', resp.get('Status'), resp)
|
||||
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
objs = resp.get('Versions', [])
|
||||
self.assertEqual(1, len(objs))
|
||||
self.assertEqual(obj_name, objs[0]['Key'])
|
||||
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
self.assertNotIn('Versions', resp)
|
||||
markers = resp.get('DeleteMarkers', [])
|
||||
self.assertEqual(1, len(markers))
|
||||
self.assertEqual(obj_name, markers[0]['Key'])
|
||||
|
||||
def test_null_versions_replaced(self):
|
||||
# verify that there is only ever one null version retained
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'some-data'),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
# there's a 'null' version even before versioning has been enabled
|
||||
version_ids, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertEqual(['null'], version_ids)
|
||||
self.assertFalse(marker_ids)
|
||||
|
||||
self.enable_versioning()
|
||||
# put version
|
||||
self.client.upload_fileobj(io.BytesIO(b'some-data'),
|
||||
self.bucket_name, obj_name)
|
||||
self.disable_versioning()
|
||||
|
||||
version_ids, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertEqual(2, len(version_ids))
|
||||
vers0 = version_ids[0]
|
||||
self.assertEqual([vers0, 'null'], version_ids, version_ids)
|
||||
self.assertFalse(marker_ids)
|
||||
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'some-data'),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
version_ids, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertEqual(['null', vers0], version_ids)
|
||||
self.assertFalse(marker_ids)
|
||||
|
||||
# delete
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
# null version gone...
|
||||
version_ids, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertEqual([vers0], version_ids)
|
||||
self.assertEqual(['null'], marker_ids)
|
||||
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'some-data'),
|
||||
self.bucket_name, obj_name)
|
||||
version_ids, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertFalse(marker_ids)
|
||||
self.assertEqual(['null', vers0], version_ids)
|
||||
|
||||
self.enable_versioning()
|
||||
# put version
|
||||
self.client.upload_fileobj(io.BytesIO(b'some-data'),
|
||||
self.bucket_name, obj_name)
|
||||
self.disable_versioning()
|
||||
|
||||
version_ids, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertFalse(marker_ids)
|
||||
self.assertEqual(3, len(version_ids), version_ids)
|
||||
vers1 = version_ids[0]
|
||||
self.assertEqual([vers1, 'null', vers0], version_ids)
|
||||
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'some-data'),
|
||||
self.bucket_name, obj_name)
|
||||
version_ids, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertFalse(marker_ids)
|
||||
self.assertEqual(['null', vers1, vers0], version_ids)
|
||||
|
||||
def test_null_version_listing(self):
|
||||
# verify that null version is positioned in listing according to its
|
||||
# created time
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
self.enable_versioning()
|
||||
# put version
|
||||
self.client.upload_fileobj(io.BytesIO(b'retained-version'),
|
||||
self.bucket_name, obj_name)
|
||||
version_ids_0, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertEqual(1, len(version_ids_0), version_ids_0)
|
||||
self.assertFalse(marker_ids)
|
||||
|
||||
self.disable_versioning()
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'null-version'),
|
||||
self.bucket_name, obj_name)
|
||||
version_ids_1, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertEqual(2, len(version_ids_1), version_ids_1)
|
||||
self.assertFalse(marker_ids)
|
||||
# null version is first in listing
|
||||
self.assertEqual(version_ids_0, version_ids_1[1:])
|
||||
|
||||
self.enable_versioning()
|
||||
# put version
|
||||
self.client.upload_fileobj(io.BytesIO(b'retained-version'),
|
||||
self.bucket_name, obj_name)
|
||||
version_ids_2, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertEqual(3, len(version_ids_2), version_ids_2)
|
||||
self.assertFalse(marker_ids)
|
||||
# null version is middle of listing
|
||||
self.assertEqual(version_ids_1, version_ids_2[1:])
|
||||
|
||||
self.disable_versioning()
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'null-version'),
|
||||
self.bucket_name, obj_name)
|
||||
version_ids_3, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertEqual(3, len(version_ids_3), version_ids_3)
|
||||
self.assertFalse(marker_ids)
|
||||
# null version is first in listing
|
||||
self.assertEqual([version_ids_2[0], version_ids_2[2]],
|
||||
version_ids_3[1:])
|
||||
|
||||
def _do_test_null_version_is_latest_delete(self, obj_name):
|
||||
version_ids, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertEqual(['null'], version_ids)
|
||||
self.assertFalse(marker_ids)
|
||||
|
||||
resp = self.client.get_object(Bucket=self.bucket_name, Key=obj_name,
|
||||
VersionId='null')
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
# delete the null version
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name,
|
||||
VersionId='null')
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
version_ids, marker_ids = self.get_version_ids(obj_name)
|
||||
self.assertFalse(version_ids)
|
||||
self.assertFalse(marker_ids)
|
||||
self.assert_no_such_version(self.bucket_name, obj_name, 'null')
|
||||
|
||||
def test_null_version_is_latest_delete_before_versioning_enabled(self):
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'null-version'),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self._do_test_null_version_is_latest_delete(obj_name)
|
||||
|
||||
def test_null_version_is_latest_delete_while_versioning_enabled(self):
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'null-version'),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self.enable_versioning()
|
||||
|
||||
self._do_test_null_version_is_latest_delete(obj_name)
|
||||
|
||||
def test_null_version_is_latest_delete_while_versioning_suspended(self):
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'null-version'),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self.enable_versioning()
|
||||
self.disable_versioning()
|
||||
|
||||
self._do_test_null_version_is_latest_delete(obj_name)
|
||||
|
||||
def test_null_version_delete_while_versioning_enabled(self):
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'null-version'),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self.enable_versioning()
|
||||
# get the null version
|
||||
resp = self.client.get_object(Bucket=self.bucket_name, Key=obj_name,
|
||||
VersionId='null')
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
# put version
|
||||
self.client.upload_fileobj(io.BytesIO(b'retained-version'),
|
||||
self.bucket_name, obj_name)
|
||||
version_ids_0, marker_ids_0 = self.get_version_ids(obj_name)
|
||||
self.assertEqual(2, len(version_ids_0), version_ids_0)
|
||||
self.assertFalse(marker_ids_0)
|
||||
vers1, null_vers1 = version_ids_0
|
||||
|
||||
# get the null version
|
||||
resp = self.client.get_object(Bucket=self.bucket_name, Key=obj_name,
|
||||
VersionId='null')
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
# delete the null version
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name,
|
||||
VersionId=null_vers1)
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
version_ids_1, marker_ids_1 = self.get_version_ids(obj_name)
|
||||
self.assertEqual(version_ids_0[:1], version_ids_1)
|
||||
|
||||
# check the latest version is intact
|
||||
resp = self.client.get_object(Bucket=self.bucket_name, Key=obj_name)
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
exp_etag = md5(b'retained-version', usedforsecurity=False).hexdigest()
|
||||
self.assertEqual('"%s"' % exp_etag, resp['ETag'])
|
||||
|
||||
def test_null_version_delete_while_versioning_suspended(self):
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
# put null version
|
||||
self.client.upload_fileobj(io.BytesIO(b'null-version'),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self.enable_versioning()
|
||||
# put version
|
||||
self.client.upload_fileobj(io.BytesIO(b'retained-version'),
|
||||
self.bucket_name, obj_name)
|
||||
version_ids_0, marker_ids_0 = self.get_version_ids(obj_name)
|
||||
self.assertEqual(2, len(version_ids_0), version_ids_0)
|
||||
self.assertFalse(marker_ids_0)
|
||||
vers1, null_vers1 = version_ids_0
|
||||
|
||||
self.disable_versioning()
|
||||
# delete the null version
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name,
|
||||
VersionId=null_vers1)
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
version_ids_1, marker_ids_1 = self.get_version_ids(obj_name)
|
||||
self.assertEqual(version_ids_0[:1], version_ids_1)
|
||||
|
||||
# check the latest version is intact
|
||||
resp = self.client.get_object(Bucket=self.bucket_name, Key=obj_name)
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
exp_etag = md5(b'retained-version', usedforsecurity=False).hexdigest()
|
||||
self.assertEqual('"%s"' % exp_etag, resp['ETag'])
|
||||
|
||||
def test_null_version_get(self):
|
||||
# verify that null version is always valid
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
|
||||
# before object exists...
|
||||
self.assert_no_such_version(self.bucket_name, obj_name, 'null')
|
||||
|
||||
# before versioning is enabled...
|
||||
self.client.upload_fileobj(io.BytesIO(b'null-version'),
|
||||
self.bucket_name, obj_name)
|
||||
self.assertEqual((['null'], []), self.get_version_ids(obj_name))
|
||||
resp = self.client.get_object(Bucket=self.bucket_name, Key=obj_name,
|
||||
VersionId='null')
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
# while versioning is enabled...
|
||||
self.enable_versioning()
|
||||
self.assertEqual((['null'], []), self.get_version_ids(obj_name))
|
||||
resp = self.client.get_object(Bucket=self.bucket_name, Key=obj_name,
|
||||
VersionId='null')
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
# while versioning is suspended...
|
||||
self.disable_versioning()
|
||||
self.assertEqual((['null'], []), self.get_version_ids(obj_name))
|
||||
resp = self.client.get_object(Bucket=self.bucket_name, Key=obj_name,
|
||||
VersionId='null')
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
def test_upload_fileobj_versioned(self):
|
||||
retry(self.enable_versioning)
|
||||
obj_data = self.create_name('some-data').encode('ascii')
|
||||
@@ -224,16 +530,7 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
'StorageClass': 'STANDARD',
|
||||
}], objs)
|
||||
|
||||
def test_delete_versioned_objects(self):
|
||||
retry(self.enable_versioning)
|
||||
etags = []
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
for i in range(3):
|
||||
obj_data = self.create_name('some-data-%s' % i).encode('ascii')
|
||||
etags.insert(0, md5(obj_data, usedforsecurity=False).hexdigest())
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
def _do_test_delete_versioned_objects(self, obj_name, obj_data, etags):
|
||||
# only one object appears in the listing
|
||||
resp = self.client.list_objects_v2(Bucket=self.bucket_name)
|
||||
objs = resp.get('Contents', [])
|
||||
@@ -246,6 +543,12 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
'StorageClass': 'STANDARD',
|
||||
}], objs)
|
||||
|
||||
# ...and that's the object that a plain GET will return
|
||||
resp = self.client.get_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
self.assertEqual('"%s"' % etags[0], resp['ETag'])
|
||||
|
||||
# but everything is layed out in the object versions listing
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
objs = resp.get('Versions', [])
|
||||
@@ -272,6 +575,7 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
'Size': len(obj_data),
|
||||
'StorageClass': 'STANDARD',
|
||||
}], objs)
|
||||
self.assertFalse(resp.get('DeleteMarkers'))
|
||||
|
||||
# we can delete a specific version
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
@@ -283,20 +587,22 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
objs = resp.get('Versions', [])
|
||||
for obj in objs:
|
||||
self._sanitize_obj_listing(obj)
|
||||
obj.pop('VersionId')
|
||||
self.assertEqual([{
|
||||
'ETag': '"%s"' % etags[0],
|
||||
'IsLatest': True,
|
||||
'Key': obj_name,
|
||||
'Size': len(obj_data),
|
||||
'StorageClass': 'STANDARD',
|
||||
'VersionId': versions[0],
|
||||
}, {
|
||||
'ETag': '"%s"' % etags[2],
|
||||
'IsLatest': False,
|
||||
'Key': obj_name,
|
||||
'Size': len(obj_data),
|
||||
'StorageClass': 'STANDARD',
|
||||
'VersionId': versions[2],
|
||||
}], objs)
|
||||
self.assertFalse(resp.get('DeleteMarkers'))
|
||||
|
||||
# ... but the current listing is unaffected
|
||||
resp = self.client.list_objects_v2(Bucket=self.bucket_name)
|
||||
@@ -310,25 +616,33 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
'StorageClass': 'STANDARD',
|
||||
}], objs)
|
||||
|
||||
# ...and that's still the object that a plain GET will return
|
||||
resp = self.client.get_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
self.assertEqual('"%s"' % etags[0], resp['ETag'])
|
||||
|
||||
# OTOH, if you delete specifically the latest version
|
||||
# we can delete a specific version
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name,
|
||||
VersionId=versions[0])
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
# the versions listing has a new IsLatest
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
objs = resp.get('Versions', [])
|
||||
for obj in objs:
|
||||
self._sanitize_obj_listing(obj)
|
||||
obj.pop('VersionId')
|
||||
self.assertEqual([{
|
||||
'ETag': '"%s"' % etags[2],
|
||||
'IsLatest': True,
|
||||
'Key': obj_name,
|
||||
'Size': len(obj_data),
|
||||
'StorageClass': 'STANDARD',
|
||||
'VersionId': versions[2],
|
||||
}], objs)
|
||||
self.assertFalse(resp.get('DeleteMarkers'))
|
||||
|
||||
# and the stack pops
|
||||
resp = self.client.list_objects_v2(Bucket=self.bucket_name)
|
||||
@@ -342,6 +656,305 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
'StorageClass': 'STANDARD',
|
||||
}], objs)
|
||||
|
||||
# ...and the restored version is now what a plain GET will return
|
||||
resp = self.client.get_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
self.assertEqual('"%s"' % etags[2], resp['ETag'])
|
||||
|
||||
def test_delete_versioned_objects_while_versioning_enabled(self):
|
||||
# verify DELETE?versionId=xxx and restore-on-delete
|
||||
retry(self.enable_versioning)
|
||||
etags = []
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
for i in range(3):
|
||||
obj_data = self.create_name('some-data-%s' % i).encode('ascii')
|
||||
etags.insert(0, md5(obj_data, usedforsecurity=False).hexdigest())
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self._do_test_delete_versioned_objects(obj_name, obj_data, etags)
|
||||
|
||||
def test_delete_versioned_objects_while_versioning_suspended(self):
|
||||
# verify DELETE?versionId=xxx and restore-on-delete
|
||||
retry(self.enable_versioning)
|
||||
etags = []
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
for i in range(3):
|
||||
obj_data = self.create_name('some-data-%s' % i).encode('ascii')
|
||||
etags.insert(0, md5(obj_data, usedforsecurity=False).hexdigest())
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
retry(self.disable_versioning)
|
||||
self._do_test_delete_versioned_objects(obj_name, obj_data, etags)
|
||||
|
||||
def _test_delete_version_previous_restored(
|
||||
self, obj_name, obj_data, etags, expected_versions):
|
||||
# only latest version appears in the listing
|
||||
resp = self.client.list_objects_v2(Bucket=self.bucket_name)
|
||||
objs = resp.get('Contents', [])
|
||||
for obj in objs:
|
||||
self._sanitize_obj_listing(obj)
|
||||
self.assertEqual([{
|
||||
'ETag': '"%s"' % etags[0],
|
||||
'Key': obj_name,
|
||||
'Size': len(obj_data),
|
||||
'StorageClass': 'STANDARD',
|
||||
}], objs)
|
||||
|
||||
# ...and that's the object that a plain GET will return
|
||||
resp = self.client.get_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
self.assertEqual('"%s"' % etags[0], resp['ETag'])
|
||||
|
||||
# but everything is layed out in the object versions listing
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
objs = resp.get('Versions', [])
|
||||
versions = []
|
||||
for obj in objs:
|
||||
self._sanitize_obj_listing(obj)
|
||||
versions.append(obj.pop('VersionId'))
|
||||
self.assertEqual([{
|
||||
'ETag': '"%s"' % etags[0],
|
||||
'IsLatest': True,
|
||||
'Key': obj_name,
|
||||
'Size': len(obj_data),
|
||||
'StorageClass': 'STANDARD',
|
||||
}, {
|
||||
'ETag': '"%s"' % etags[1],
|
||||
'IsLatest': False,
|
||||
'Key': obj_name,
|
||||
'Size': len(obj_data),
|
||||
'StorageClass': 'STANDARD',
|
||||
}], objs)
|
||||
self.assertEqual(expected_versions, versions)
|
||||
self.assertFalse(resp.get('DeleteMarkers'))
|
||||
|
||||
# delete the latest version
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name,
|
||||
VersionId=versions[0])
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
# and that pulls it out of the versions listing
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
objs = resp.get('Versions', [])
|
||||
for obj in objs:
|
||||
self._sanitize_obj_listing(obj)
|
||||
self.assertEqual([{
|
||||
'ETag': '"%s"' % etags[1],
|
||||
'IsLatest': True,
|
||||
'Key': obj_name,
|
||||
'Size': len(obj_data),
|
||||
'StorageClass': 'STANDARD',
|
||||
'VersionId': versions[1],
|
||||
}], objs)
|
||||
self.assertFalse(resp.get('DeleteMarkers'))
|
||||
|
||||
# ...and the previous version is restored
|
||||
resp = self.client.list_objects_v2(Bucket=self.bucket_name)
|
||||
objs = resp.get('Contents', [])
|
||||
for obj in objs:
|
||||
self._sanitize_obj_listing(obj)
|
||||
self.assertEqual([{
|
||||
'ETag': '"%s"' % etags[1],
|
||||
'Key': obj_name,
|
||||
'Size': len(obj_data),
|
||||
'StorageClass': 'STANDARD',
|
||||
}], objs)
|
||||
|
||||
# ...and a plain GET will now return the previous version
|
||||
resp = self.client.get_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
self.assertEqual('"%s"' % etags[1], resp['ETag'])
|
||||
|
||||
# delete the current version and it's gone
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name,
|
||||
VersionId=versions[1])
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
self.assertFalse(resp.get('Versions'))
|
||||
self.assertFalse(resp.get('DeleteMarkers'))
|
||||
self.assert_no_such_version(self.bucket_name, obj_name, versions[1])
|
||||
self.assert_no_such_key(self.bucket_name, obj_name)
|
||||
|
||||
def test_delete_version_null_restored_while_versioning_enabled(self):
|
||||
# verify DELETE?versionId=xxx and null restore-on-delete
|
||||
# create a null version
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
obj_data = self.create_name('some-data-0').encode('ascii')
|
||||
etags = [md5(obj_data, usedforsecurity=False).hexdigest()]
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
retry(self.enable_versioning)
|
||||
|
||||
# create a non-null version
|
||||
obj_data = self.create_name('some-data-1').encode('ascii')
|
||||
etags.insert(0, md5(obj_data, usedforsecurity=False).hexdigest())
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self._test_delete_version_previous_restored(
|
||||
obj_name, obj_data, etags, [mock.ANY, 'null'])
|
||||
|
||||
def test_delete_version_null_restored_while_versioning_suspended(self):
|
||||
# verify DELETE?versionId=xxx and null restore-on-delete
|
||||
# create a null version
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
obj_data = self.create_name('some-data-0').encode('ascii')
|
||||
etags = [md5(obj_data, usedforsecurity=False).hexdigest()]
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
retry(self.enable_versioning)
|
||||
|
||||
# create a non-null version
|
||||
obj_data = self.create_name('some-data-1').encode('ascii')
|
||||
etags.insert(0, md5(obj_data, usedforsecurity=False).hexdigest())
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
retry(self.disable_versioning)
|
||||
|
||||
self._test_delete_version_previous_restored(
|
||||
obj_name, obj_data, etags, [mock.ANY, 'null'])
|
||||
|
||||
def test_delete_null_version_older_version_restored(self):
|
||||
# verify DELETE?versionId=null and restore-on-delete
|
||||
retry(self.enable_versioning)
|
||||
|
||||
# create a non-null version
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
obj_data = self.create_name('some-data-0').encode('ascii')
|
||||
etags = [md5(obj_data, usedforsecurity=False).hexdigest()]
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
retry(self.disable_versioning)
|
||||
|
||||
# create a null version
|
||||
obj_data = self.create_name('some-data-1').encode('ascii')
|
||||
etags.insert(0, md5(obj_data, usedforsecurity=False).hexdigest())
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self._test_delete_version_previous_restored(
|
||||
obj_name, obj_data, etags, ['null', mock.ANY])
|
||||
|
||||
def _test_delete_anon_does_not_restore(self, obj_name):
|
||||
orig_versions = []
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
objs = resp.get('Versions', [])
|
||||
for obj in objs:
|
||||
orig_versions.append(obj.pop('VersionId'))
|
||||
self.assertEqual(2, len(orig_versions))
|
||||
self.assertFalse(resp.get('DeleteMarkers'))
|
||||
|
||||
# delete without a version id does NOT restore the previous version
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
self.assert_no_such_key(self.bucket_name, obj_name)
|
||||
|
||||
# versions remain
|
||||
versions = []
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
objs = resp.get('Versions', [])
|
||||
for obj in objs:
|
||||
versions.append(obj.pop('VersionId'))
|
||||
self.assertEqual(orig_versions, versions)
|
||||
# ... but there's also a delete marker
|
||||
markers = resp.get('DeleteMarkers', [])
|
||||
for marker in markers:
|
||||
self._sanitize_obj_listing(marker)
|
||||
self.assertEqual([{
|
||||
'Key': obj_name,
|
||||
'VersionId': mock.ANY,
|
||||
'IsLatest': True,
|
||||
}], markers)
|
||||
self.assertNotIn(markers[0]['VersionId'], orig_versions)
|
||||
|
||||
def test_delete_anon_no_restore_while_versioning_enabled(self):
|
||||
# verify DELETE without versionId does not restore
|
||||
retry(self.enable_versioning)
|
||||
|
||||
# create two versions
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
for i in range(2):
|
||||
obj_data = self.create_name('some-data-%d' % i).encode('ascii')
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
self._test_delete_anon_does_not_restore(obj_name)
|
||||
|
||||
def test_delete_anon_no_restore_while_versioning_suspended(self):
|
||||
# verify DELETE without versionId does not restore
|
||||
retry(self.enable_versioning)
|
||||
|
||||
# create two versions
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
for i in range(2):
|
||||
obj_data = self.create_name('some-data-%d' % i).encode('ascii')
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
retry(self.disable_versioning)
|
||||
|
||||
self._test_delete_anon_does_not_restore(obj_name)
|
||||
|
||||
def test_delete_anon_current_is_null_no_restore(self):
|
||||
# verify DELETE of null version without versionId does not restore
|
||||
retry(self.enable_versioning)
|
||||
|
||||
# create version
|
||||
obj_name = self.create_name('versioned-obj')
|
||||
obj_data = self.create_name('some-data-0').encode('ascii')
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
retry(self.disable_versioning)
|
||||
|
||||
# create null version
|
||||
obj_data = self.create_name('some-data-1').encode('ascii')
|
||||
self.client.upload_fileobj(io.BytesIO(obj_data),
|
||||
self.bucket_name, obj_name)
|
||||
|
||||
orig_versions = []
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
objs = resp.get('Versions', [])
|
||||
for obj in objs:
|
||||
orig_versions.append(obj.pop('VersionId'))
|
||||
self.assertEqual(['null', mock.ANY], orig_versions)
|
||||
self.assertFalse(resp.get('DeleteMarkers'))
|
||||
|
||||
# delete without a version id does NOT restore the previous version
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
self.assert_no_such_key(self.bucket_name, obj_name)
|
||||
|
||||
# non-null version remains
|
||||
versions = []
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
objs = resp.get('Versions', [])
|
||||
for obj in objs:
|
||||
versions.append(obj.pop('VersionId'))
|
||||
self.assertEqual(orig_versions[1:], versions)
|
||||
# ... but there's also a delete marker
|
||||
markers = resp.get('DeleteMarkers', [])
|
||||
for marker in markers:
|
||||
self._sanitize_obj_listing(marker)
|
||||
self.assertEqual([{
|
||||
'Key': obj_name,
|
||||
'VersionId': 'null',
|
||||
'IsLatest': True,
|
||||
}], markers)
|
||||
|
||||
def test_delete_versioned_deletes(self):
|
||||
retry(self.enable_versioning)
|
||||
etags = []
|
||||
@@ -388,21 +1001,18 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name,
|
||||
VersionId=marker_versions[2])
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
# since IsLatest is still marker we'll raise NoSuchKey
|
||||
with self.assertRaises(ClientError) as caught:
|
||||
resp = self.client.get_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
expected_err = 'An error occurred (NoSuchKey) when calling the ' \
|
||||
'GetObject operation: The specified key does not exist.'
|
||||
self.assertEqual(expected_err, str(caught.exception))
|
||||
# since IsLatest is still a delete marker we'll raise NoSuchKey
|
||||
self.assert_no_such_key(self.bucket_name, obj_name)
|
||||
|
||||
# now delete the delete marker (IsLatest)
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name,
|
||||
VersionId=marker_versions[0])
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
# most recent version is now latest
|
||||
# most recent version is now restored to be the latest
|
||||
resp = self.client.get_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
self.assertEqual(200, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
@@ -412,14 +1022,10 @@ class TestObjectVersioning(BaseS3TestCase):
|
||||
resp = self.client.delete_object(Bucket=self.bucket_name,
|
||||
Key=obj_name,
|
||||
VersionId=versions[0])
|
||||
self.assertEqual(204, resp['ResponseMetadata']['HTTPStatusCode'])
|
||||
|
||||
# and object is deleted again
|
||||
with self.assertRaises(ClientError) as caught:
|
||||
resp = self.client.get_object(Bucket=self.bucket_name,
|
||||
Key=obj_name)
|
||||
expected_err = 'An error occurred (NoSuchKey) when calling the ' \
|
||||
'GetObject operation: The specified key does not exist.'
|
||||
self.assertEqual(expected_err, str(caught.exception))
|
||||
self.assert_no_such_key(self.bucket_name, obj_name)
|
||||
|
||||
# delete marker IsLatest
|
||||
resp = self.client.list_object_versions(Bucket=self.bucket_name)
|
||||
|
||||
@@ -306,6 +306,17 @@ class BaseS3ApiBucket(object):
|
||||
self.assertEqual(status.split()[0], '200')
|
||||
self.assertEqual(headers['Location'], '/bucket')
|
||||
|
||||
def test_bucket_PUT_s3_compat(self):
|
||||
req = Request.blank('/bucket',
|
||||
environ={'REQUEST_METHOD': 'PUT'},
|
||||
headers={'Authorization': 'AWS test:tester:hmac',
|
||||
'Date': self.get_date_header()})
|
||||
status, headers, body = self.call_s3api(req)
|
||||
self.assertEqual(status.split()[0], '200')
|
||||
self.assertTrue(self.swift.call_list)
|
||||
self.assertEqual('true', self.swift.call_list[0].headers.get(
|
||||
'X-Container-Sysmeta-S3-Compatible-Versions'))
|
||||
|
||||
def test_bucket_PUT_with_location(self):
|
||||
self._test_bucket_PUT_with_location('CreateBucketConfiguration')
|
||||
|
||||
|
||||
@@ -1508,7 +1508,7 @@ class TestS3ApiObj(BaseS3ApiObj, S3ApiTestCase):
|
||||
], self.swift.calls)
|
||||
|
||||
def test_object_DELETE_current_version_id(self):
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'null'}
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'none'}
|
||||
self.swift.register('DELETE', '/v1/AUTH_test/bucket/object'
|
||||
'?symlink=get&version-id=1574358170.12293',
|
||||
swob.HTTPNoContent, resp_headers, None)
|
||||
@@ -1560,7 +1560,7 @@ class TestS3ApiObj(BaseS3ApiObj, S3ApiTestCase):
|
||||
self.assertEqual(status.split()[0], '501', body)
|
||||
|
||||
def test_object_DELETE_current_version_id_is_delete_marker(self):
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'null'}
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'none'}
|
||||
self.swift.register('DELETE', '/v1/AUTH_test/bucket/object'
|
||||
'?symlink=get&version-id=1574358170.12293',
|
||||
swob.HTTPNoContent, resp_headers, None)
|
||||
@@ -1593,7 +1593,7 @@ class TestS3ApiObj(BaseS3ApiObj, S3ApiTestCase):
|
||||
], self.swift.calls)
|
||||
|
||||
def test_object_DELETE_current_version_id_is_missing(self):
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'null'}
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'none'}
|
||||
self.swift.register('DELETE', '/v1/AUTH_test/bucket/object'
|
||||
'?symlink=get&version-id=1574358170.12293',
|
||||
swob.HTTPNoContent, resp_headers, None)
|
||||
@@ -1640,7 +1640,7 @@ class TestS3ApiObj(BaseS3ApiObj, S3ApiTestCase):
|
||||
], self.swift.calls)
|
||||
|
||||
def test_object_DELETE_current_version_id_GET_error(self):
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'null'}
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'none'}
|
||||
self.swift.register('DELETE', '/v1/AUTH_test/bucket/object'
|
||||
'?symlink=get&version-id=1574358170.12293',
|
||||
swob.HTTPNoContent, resp_headers, None)
|
||||
@@ -1668,7 +1668,7 @@ class TestS3ApiObj(BaseS3ApiObj, S3ApiTestCase):
|
||||
], self.swift.calls)
|
||||
|
||||
def test_object_DELETE_current_version_id_PUT_error(self):
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'null'}
|
||||
resp_headers = {'X-Object-Current-Version-Id': 'none'}
|
||||
self.swift.register('DELETE', '/v1/AUTH_test/bucket/object'
|
||||
'?symlink=get&version-id=1574358170.12293',
|
||||
swob.HTTPNoContent, resp_headers, None)
|
||||
|
||||
@@ -3622,7 +3622,7 @@ class TestObjectContext(unittest.TestCase):
|
||||
def setUp(self):
|
||||
app = FakeSwift()
|
||||
self.obj_context = object_versioning.ObjectContext(
|
||||
app, app.logger, 'v1', 'c', 'a', 'o', None, False)
|
||||
app, app.logger, 'v1', 'c', 'a', 'o', None, False, False)
|
||||
self.ts_iter = make_timestamp_iter()
|
||||
|
||||
def test_get_version(self):
|
||||
|
||||
Reference in New Issue
Block a user