Add basics for S3 bucket lifecycles.
This commit is contained in:
parent
72dbd349ac
commit
91fffbb83b
@ -161,6 +161,20 @@ class FakeMultipart(object):
|
|||||||
yield self.parts[part_id]
|
yield self.parts[part_id]
|
||||||
|
|
||||||
|
|
||||||
|
class LifecycleRule(object):
|
||||||
|
def __init__(self, id=None, prefix=None, status=None, expiration_days=None,
|
||||||
|
expiration_date=None, transition_days=None,
|
||||||
|
transition_date=None, storage_class=None):
|
||||||
|
self.id = id
|
||||||
|
self.prefix = prefix
|
||||||
|
self.status = status
|
||||||
|
self.expiration_days = expiration_days
|
||||||
|
self.expiration_date = expiration_date
|
||||||
|
self.transition_days = transition_days
|
||||||
|
self.transition_date = transition_date
|
||||||
|
self.storage_class = storage_class
|
||||||
|
|
||||||
|
|
||||||
class FakeBucket(object):
|
class FakeBucket(object):
|
||||||
|
|
||||||
def __init__(self, name, region_name):
|
def __init__(self, name, region_name):
|
||||||
@ -169,6 +183,7 @@ class FakeBucket(object):
|
|||||||
self.keys = _VersionedKeyStore()
|
self.keys = _VersionedKeyStore()
|
||||||
self.multiparts = {}
|
self.multiparts = {}
|
||||||
self.versioning_status = None
|
self.versioning_status = None
|
||||||
|
self.rules = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def location(self):
|
def location(self):
|
||||||
@ -178,6 +193,25 @@ class FakeBucket(object):
|
|||||||
def is_versioned(self):
|
def is_versioned(self):
|
||||||
return self.versioning_status == 'Enabled'
|
return self.versioning_status == 'Enabled'
|
||||||
|
|
||||||
|
def set_lifecycle(self, rules):
|
||||||
|
self.rules = []
|
||||||
|
for rule in rules:
|
||||||
|
expiration = rule.get('Expiration')
|
||||||
|
transition = rule.get('Transition')
|
||||||
|
self.rules.append(LifecycleRule(
|
||||||
|
id=rule.get('ID'),
|
||||||
|
prefix=rule['Prefix'],
|
||||||
|
status=rule['Status'],
|
||||||
|
expiration_days=expiration.get('Days') if expiration else None,
|
||||||
|
expiration_date=expiration.get('Date') if expiration else None,
|
||||||
|
transition_days=transition.get('Days') if transition else None,
|
||||||
|
transition_date=transition.get('Date') if transition else None,
|
||||||
|
storage_class=transition['StorageClass'] if transition else None,
|
||||||
|
))
|
||||||
|
|
||||||
|
def delete_lifecycle(self):
|
||||||
|
self.rules = []
|
||||||
|
|
||||||
def get_cfn_attribute(self, attribute_name):
|
def get_cfn_attribute(self, attribute_name):
|
||||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||||
if attribute_name == 'DomainName':
|
if attribute_name == 'DomainName':
|
||||||
@ -235,6 +269,10 @@ class S3Backend(BaseBackend):
|
|||||||
|
|
||||||
return itertools.chain(*(l for _, l in bucket.keys.iterlists()))
|
return itertools.chain(*(l for _, l in bucket.keys.iterlists()))
|
||||||
|
|
||||||
|
def set_bucket_lifecycle(self, bucket_name, rules):
|
||||||
|
bucket = self.get_bucket(bucket_name)
|
||||||
|
bucket.set_lifecycle(rules)
|
||||||
|
|
||||||
def set_key(self, bucket_name, key_name, value, storage=None, etag=None):
|
def set_key(self, bucket_name, key_name, value, storage=None, etag=None):
|
||||||
key_name = clean_key_name(key_name)
|
key_name = clean_key_name(key_name)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import re
|
|||||||
|
|
||||||
import six
|
import six
|
||||||
from six.moves.urllib.parse import parse_qs, urlparse
|
from six.moves.urllib.parse import parse_qs, urlparse
|
||||||
|
import xmltodict
|
||||||
|
|
||||||
from moto.core.responses import _TemplateEnvironmentMixin
|
from moto.core.responses import _TemplateEnvironmentMixin
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||||||
elif method == 'PUT':
|
elif method == 'PUT':
|
||||||
return self._bucket_response_put(request, region_name, bucket_name, querystring, headers)
|
return self._bucket_response_put(request, region_name, bucket_name, querystring, headers)
|
||||||
elif method == 'DELETE':
|
elif method == 'DELETE':
|
||||||
return self._bucket_response_delete(bucket_name, headers)
|
return self._bucket_response_delete(bucket_name, querystring, headers)
|
||||||
elif method == 'POST':
|
elif method == 'POST':
|
||||||
return self._bucket_response_post(request, bucket_name, headers)
|
return self._bucket_response_post(request, bucket_name, headers)
|
||||||
else:
|
else:
|
||||||
@ -92,6 +93,12 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||||||
bucket = self.backend.get_bucket(bucket_name)
|
bucket = self.backend.get_bucket(bucket_name)
|
||||||
template = self.response_template(S3_BUCKET_LOCATION)
|
template = self.response_template(S3_BUCKET_LOCATION)
|
||||||
return 200, headers, template.render(location=bucket.location)
|
return 200, headers, template.render(location=bucket.location)
|
||||||
|
elif 'lifecycle' in querystring:
|
||||||
|
bucket = self.backend.get_bucket(bucket_name)
|
||||||
|
if not bucket.rules:
|
||||||
|
return 404, headers, "NoSuchLifecycleConfiguration"
|
||||||
|
template = self.response_template(S3_BUCKET_LIFECYCLE_CONFIGURATION)
|
||||||
|
return 200, headers, template.render(rules=bucket.rules)
|
||||||
elif 'versioning' in querystring:
|
elif 'versioning' in querystring:
|
||||||
versioning = self.backend.get_bucket_versioning(bucket_name)
|
versioning = self.backend.get_bucket_versioning(bucket_name)
|
||||||
template = self.response_template(S3_BUCKET_GET_VERSIONING)
|
template = self.response_template(S3_BUCKET_GET_VERSIONING)
|
||||||
@ -143,14 +150,23 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||||||
else:
|
else:
|
||||||
# Flask server
|
# Flask server
|
||||||
body = request.data
|
body = request.data
|
||||||
|
body = body.decode('utf-8')
|
||||||
|
|
||||||
if 'versioning' in querystring:
|
if 'versioning' in querystring:
|
||||||
ver = re.search('<Status>([A-Za-z]+)</Status>', body.decode('utf-8'))
|
ver = re.search('<Status>([A-Za-z]+)</Status>', body)
|
||||||
if ver:
|
if ver:
|
||||||
self.backend.set_bucket_versioning(bucket_name, ver.group(1))
|
self.backend.set_bucket_versioning(bucket_name, ver.group(1))
|
||||||
template = self.response_template(S3_BUCKET_VERSIONING)
|
template = self.response_template(S3_BUCKET_VERSIONING)
|
||||||
return template.render(bucket_versioning_status=ver.group(1))
|
return template.render(bucket_versioning_status=ver.group(1))
|
||||||
else:
|
else:
|
||||||
return 404, headers, ""
|
return 404, headers, ""
|
||||||
|
elif 'lifecycle' in querystring:
|
||||||
|
rules = xmltodict.parse(body)['LifecycleConfiguration']['Rule']
|
||||||
|
if not isinstance(rules, list):
|
||||||
|
# If there is only one rule, xmldict returns just the item
|
||||||
|
rules = [rules]
|
||||||
|
self.backend.set_bucket_lifecycle(bucket_name, rules)
|
||||||
|
return ""
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
new_bucket = self.backend.create_bucket(bucket_name, region_name)
|
new_bucket = self.backend.create_bucket(bucket_name, region_name)
|
||||||
@ -163,7 +179,12 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||||||
template = self.response_template(S3_BUCKET_CREATE_RESPONSE)
|
template = self.response_template(S3_BUCKET_CREATE_RESPONSE)
|
||||||
return 200, headers, template.render(bucket=new_bucket)
|
return 200, headers, template.render(bucket=new_bucket)
|
||||||
|
|
||||||
def _bucket_response_delete(self, bucket_name, headers):
|
def _bucket_response_delete(self, bucket_name, querystring, headers):
|
||||||
|
if 'lifecycle' in querystring:
|
||||||
|
bucket = self.backend.get_bucket(bucket_name)
|
||||||
|
bucket.delete_lifecycle()
|
||||||
|
return 204, headers, ""
|
||||||
|
|
||||||
removed_bucket = self.backend.delete_bucket(bucket_name)
|
removed_bucket = self.backend.delete_bucket(bucket_name)
|
||||||
|
|
||||||
if removed_bucket:
|
if removed_bucket:
|
||||||
@ -497,6 +518,39 @@ S3_DELETE_BUCKET_WITH_ITEMS_ERROR = """<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
S3_BUCKET_LOCATION = """<?xml version="1.0" encoding="UTF-8"?>
|
S3_BUCKET_LOCATION = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">{{ location }}</LocationConstraint>"""
|
<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">{{ location }}</LocationConstraint>"""
|
||||||
|
|
||||||
|
S3_BUCKET_LIFECYCLE_CONFIGURATION = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||||
|
{% for rule in rules %}
|
||||||
|
<Rule>
|
||||||
|
<ID>{{ rule.id }}</ID>
|
||||||
|
<Prefix>{{ rule.prefix if rule.prefix != None }}</Prefix>
|
||||||
|
<Status>{{ rule.status }}</Status>
|
||||||
|
{% if rule.storage_class %}
|
||||||
|
<Transition>
|
||||||
|
{% if rule.transition_days %}
|
||||||
|
<Days>{{ rule.transition_days }}</Days>
|
||||||
|
{% endif %}
|
||||||
|
{% if rule.transition_date %}
|
||||||
|
<Date>{{ rule.transition_date }}</Date>
|
||||||
|
{% endif %}
|
||||||
|
<StorageClass>{{ rule.storage_class }}</StorageClass>
|
||||||
|
</Transition>
|
||||||
|
{% endif %}
|
||||||
|
{% if rule.expiration_days or rule.expiration_date %}
|
||||||
|
<Expiration>
|
||||||
|
{% if rule.expiration_days %}
|
||||||
|
<Days>{{ rule.expiration_days }}</Days>
|
||||||
|
{% endif %}
|
||||||
|
{% if rule.expiration_date %}
|
||||||
|
<Date>{{ rule.expiration_date }}</Date>
|
||||||
|
{% endif %}
|
||||||
|
</Expiration>
|
||||||
|
{% endif %}
|
||||||
|
</Rule>
|
||||||
|
{% endfor %}
|
||||||
|
</LifecycleConfiguration>
|
||||||
|
"""
|
||||||
|
|
||||||
S3_BUCKET_VERSIONING = """
|
S3_BUCKET_VERSIONING = """
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||||
|
Loading…
Reference in New Issue
Block a user