Merge pull request #941 from georgepsarakis/feature/s3_delete_markers

S3 DeleteMarker support
This commit is contained in:
Steve Pulec 2017-05-14 16:17:57 -04:00 committed by GitHub
commit d45727e028
3 changed files with 161 additions and 14 deletions

View File

@ -18,6 +18,17 @@ UPLOAD_ID_BYTES = 43
UPLOAD_PART_MIN_SIZE = 5242880 UPLOAD_PART_MIN_SIZE = 5242880
class FakeDeleteMarker(BaseModel):
def __init__(self, key):
self.key = key
self._version_id = key.version_id + 1
@property
def version_id(self):
return self._version_id
class FakeKey(BaseModel): class FakeKey(BaseModel):
def __init__(self, name, value, storage="STANDARD", etag=None, is_versioned=False, version_id=0): def __init__(self, name, value, storage="STANDARD", etag=None, is_versioned=False, version_id=0):
@ -33,6 +44,10 @@ class FakeKey(BaseModel):
self._version_id = version_id self._version_id = version_id
self._is_versioned = is_versioned self._is_versioned = is_versioned
@property
def version_id(self):
return self._version_id
def copy(self, new_name=None): def copy(self, new_name=None):
r = copy.deepcopy(self) r = copy.deepcopy(self)
if new_name is not None: if new_name is not None:
@ -102,7 +117,7 @@ class FakeKey(BaseModel):
res['x-amz-restore'] = rhdr.format(self.expiry_date) res['x-amz-restore'] = rhdr.format(self.expiry_date)
if self._is_versioned: if self._is_versioned:
res['x-amz-version-id'] = str(self._version_id) res['x-amz-version-id'] = str(self.version_id)
if self.website_redirect_location: if self.website_redirect_location:
res['x-amz-website-redirect-location'] = self.website_redirect_location res['x-amz-website-redirect-location'] = self.website_redirect_location
@ -356,6 +371,26 @@ class S3Backend(BaseBackend):
def get_bucket_versioning(self, bucket_name): def get_bucket_versioning(self, bucket_name):
return self.get_bucket(bucket_name).versioning_status return self.get_bucket(bucket_name).versioning_status
def get_bucket_latest_versions(self, bucket_name):
versions = self.get_bucket_versions(bucket_name)
maximum_version_per_key = {}
latest_versions = {}
for version in versions:
if isinstance(version, FakeDeleteMarker):
name = version.key.name
else:
name = version.name
version_id = version.version_id
maximum_version_per_key[name] = max(
version_id,
maximum_version_per_key.get(name, -1)
)
if version_id == maximum_version_per_key[name]:
latest_versions[name] = version_id
return latest_versions
def get_bucket_versions(self, bucket_name, delimiter=None, def get_bucket_versions(self, bucket_name, delimiter=None,
encoding_type=None, encoding_type=None,
key_marker=None, key_marker=None,
@ -423,15 +458,22 @@ class S3Backend(BaseBackend):
def get_key(self, bucket_name, key_name, version_id=None): def get_key(self, bucket_name, key_name, version_id=None):
key_name = clean_key_name(key_name) key_name = clean_key_name(key_name)
bucket = self.get_bucket(bucket_name) bucket = self.get_bucket(bucket_name)
key = None
if bucket: if bucket:
if version_id is None: if version_id is None:
if key_name in bucket.keys: if key_name in bucket.keys:
return bucket.keys[key_name] key = bucket.keys[key_name]
else: else:
for key in bucket.keys.getlist(key_name): for key_version in bucket.keys.getlist(key_name):
if str(key._version_id) == str(version_id): if str(key_version.version_id) == str(version_id):
return key key = key_version
raise MissingKey(key_name=key_name) break
if isinstance(key, FakeKey):
return key
else:
raise MissingKey(key_name=key_name)
def initiate_multipart(self, bucket_name, key_name, metadata): def initiate_multipart(self, bucket_name, key_name, metadata):
bucket = self.get_bucket(bucket_name) bucket = self.get_bucket(bucket_name)
@ -510,12 +552,33 @@ class S3Backend(BaseBackend):
return key_results, folder_results return key_results, folder_results
def delete_key(self, bucket_name, key_name): def _set_delete_marker(self, bucket_name, key_name):
bucket = self.get_bucket(bucket_name)
bucket.keys[key_name] = FakeDeleteMarker(
key=bucket.keys[key_name]
)
def delete_key(self, bucket_name, key_name, version_id=None):
key_name = clean_key_name(key_name) key_name = clean_key_name(key_name)
bucket = self.get_bucket(bucket_name) bucket = self.get_bucket(bucket_name)
try: try:
bucket.keys.pop(key_name) if not bucket.is_versioned:
bucket.keys.pop(key_name)
else:
if version_id is None:
self._set_delete_marker(bucket_name, key_name)
else:
if key_name not in bucket.keys:
raise KeyError
bucket.keys.setlist(
key_name,
[
key
for key in bucket.keys.getlist(key_name)
if str(key.version_id) != str(version_id)
]
)
return True return True
except KeyError: except KeyError:
return False return False

View File

@ -13,7 +13,7 @@ from moto.s3bucket_path.utils import bucket_name_from_url as bucketpath_bucket_n
from .exceptions import BucketAlreadyExists, S3ClientError, InvalidPartOrder from .exceptions import BucketAlreadyExists, S3ClientError, InvalidPartOrder
from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl from .models import s3_backend, get_canned_acl, FakeGrantee, FakeGrant, FakeAcl, FakeKey
from .utils import bucket_name_from_url, metadata_from_headers from .utils import bucket_name_from_url, metadata_from_headers
from xml.dom import minidom from xml.dom import minidom
@ -219,9 +219,21 @@ class ResponseObject(_TemplateEnvironmentMixin):
max_keys=max_keys, max_keys=max_keys,
version_id_marker=version_id_marker version_id_marker=version_id_marker
) )
latest_versions = self.backend.get_bucket_latest_versions(
bucket_name=bucket_name
)
key_list = []
delete_marker_list = []
for version in versions:
if isinstance(version, FakeKey):
key_list.append(version)
else:
delete_marker_list.append(version)
template = self.response_template(S3_BUCKET_GET_VERSIONS) template = self.response_template(S3_BUCKET_GET_VERSIONS)
return 200, {}, template.render( return 200, {}, template.render(
key_list=versions, key_list=key_list,
delete_marker_list=delete_marker_list,
latest_versions=latest_versions,
bucket=bucket, bucket=bucket,
prefix='', prefix='',
max_keys=1000, max_keys=1000,
@ -478,7 +490,7 @@ class ResponseObject(_TemplateEnvironmentMixin):
return self._key_response_post(request, body, bucket_name, query, key_name, headers) return self._key_response_post(request, body, bucket_name, query, key_name, headers)
else: else:
raise NotImplementedError( raise NotImplementedError(
"Method {0} has not been impelemented in the S3 backend yet".format(method)) "Method {0} has not been implemented in the S3 backend yet".format(method))
def _key_response_get(self, bucket_name, query, key_name, headers): def _key_response_get(self, bucket_name, query, key_name, headers):
response_headers = {} response_headers = {}
@ -630,7 +642,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
upload_id = query['uploadId'][0] upload_id = query['uploadId'][0]
self.backend.cancel_multipart(bucket_name, upload_id) self.backend.cancel_multipart(bucket_name, upload_id)
return 204, {}, "" return 204, {}, ""
self.backend.delete_key(bucket_name, key_name) version_id = query.get('versionId', [None])[0]
self.backend.delete_key(bucket_name, key_name, version_id=version_id)
template = self.response_template(S3_DELETE_OBJECT_SUCCESS) template = self.response_template(S3_DELETE_OBJECT_SUCCESS)
return 204, {}, template.render() return 204, {}, template.render()
@ -851,8 +864,8 @@ S3_BUCKET_GET_VERSIONS = """<?xml version="1.0" encoding="UTF-8"?>
{% for key in key_list %} {% for key in key_list %}
<Version> <Version>
<Key>{{ key.name }}</Key> <Key>{{ key.name }}</Key>
<VersionId>{{ key._version_id }}</VersionId> <VersionId>{{ key.version_id }}</VersionId>
<IsLatest>false</IsLatest> <IsLatest>{% if latest_versions[key.name] == key.version_id %}true{% else %}false{% endif %}</IsLatest>
<LastModified>{{ key.last_modified_ISO8601 }}</LastModified> <LastModified>{{ key.last_modified_ISO8601 }}</LastModified>
<ETag>{{ key.etag }}</ETag> <ETag>{{ key.etag }}</ETag>
<Size>{{ key.size }}</Size> <Size>{{ key.size }}</Size>
@ -863,6 +876,18 @@ S3_BUCKET_GET_VERSIONS = """<?xml version="1.0" encoding="UTF-8"?>
</Owner> </Owner>
</Version> </Version>
{% endfor %} {% endfor %}
{% for marker in delete_marker_list %}
<DeleteMarker>
<Key>{{ marker.key.name }}</Key>
<VersionId>{{ marker.version_id }}</VersionId>
<IsLatest>{% if latest_versions[marker.key.name] == marker.version_id %}true{% else %}false{% endif %}</IsLatest>
<LastModified>{{ marker.key.last_modified_ISO8601 }}</LastModified>
<Owner>
<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>
<DisplayName>webfile</DisplayName>
</Owner>
</DeleteMarker>
{% endfor %}
</ListVersionsResult> </ListVersionsResult>
""" """

View File

@ -1300,6 +1300,12 @@ def test_boto3_list_object_versions():
bucket_name = 'mybucket' bucket_name = 'mybucket'
key = 'key-with-versions' key = 'key-with-versions'
s3.create_bucket(Bucket=bucket_name) s3.create_bucket(Bucket=bucket_name)
s3.put_bucket_versioning(
Bucket=bucket_name,
VersioningConfiguration={
'Status': 'Enabled'
}
)
items = (six.b('v1'), six.b('v2')) items = (six.b('v1'), six.b('v2'))
for body in items: for body in items:
s3.put_object( s3.put_object(
@ -1319,6 +1325,58 @@ def test_boto3_list_object_versions():
response['Body'].read().should.equal(items[-1]) response['Body'].read().should.equal(items[-1])
@mock_s3
def test_boto3_delete_markers():
s3 = boto3.client('s3', region_name='us-east-1')
bucket_name = 'mybucket'
key = 'key-with-versions'
s3.create_bucket(Bucket=bucket_name)
s3.put_bucket_versioning(
Bucket=bucket_name,
VersioningConfiguration={
'Status': 'Enabled'
}
)
items = (six.b('v1'), six.b('v2'))
for body in items:
s3.put_object(
Bucket=bucket_name,
Key=key,
Body=body
)
s3.delete_object(
Bucket=bucket_name,
Key=key
)
with assert_raises(ClientError) as e:
s3.get_object(
Bucket=bucket_name,
Key=key
)
e.response['Error']['Code'].should.equal('NoSuchKey')
s3.delete_object(
Bucket=bucket_name,
Key=key,
VersionId='2'
)
response = s3.get_object(
Bucket=bucket_name,
Key=key
)
response['Body'].read().should.equal(items[-1])
response = s3.list_object_versions(
Bucket=bucket_name
)
response['Versions'].should.have.length_of(2)
response['Versions'][-1]['IsLatest'].should.be.true
response['Versions'][0]['IsLatest'].should.be.false
[(key_metadata['Key'], key_metadata['VersionId'])
for key_metadata in response['Versions']].should.equal(
[('key-with-versions', '0'), ('key-with-versions', '1')]
)
TEST_XML = """\ TEST_XML = """\
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ns0:WebsiteConfiguration xmlns:ns0="http://s3.amazonaws.com/doc/2006-03-01/"> <ns0:WebsiteConfiguration xmlns:ns0="http://s3.amazonaws.com/doc/2006-03-01/">
@ -1337,3 +1395,4 @@ TEST_XML = """\
</ns0:RoutingRules> </ns0:RoutingRules>
</ns0:WebsiteConfiguration> </ns0:WebsiteConfiguration>
""" """