From 91fffbb83bfb2315635633f956d3b02c183a3165 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Tue, 2 Jun 2015 23:11:23 -0400 Subject: [PATCH] Add basics for S3 bucket lifecycles. --- moto/s3/models.py | 38 ++++++++++++++++++++++++++++ moto/s3/responses.py | 60 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index 6c7788e7a..6796894f8 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -161,6 +161,20 @@ class FakeMultipart(object): 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): def __init__(self, name, region_name): @@ -169,6 +183,7 @@ class FakeBucket(object): self.keys = _VersionedKeyStore() self.multiparts = {} self.versioning_status = None + self.rules = [] @property def location(self): @@ -178,6 +193,25 @@ class FakeBucket(object): def is_versioned(self): 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): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == 'DomainName': @@ -235,6 +269,10 @@ class S3Backend(BaseBackend): 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): key_name = clean_key_name(key_name) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index a5f469812..7eb1af124 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -4,6 +4,7 @@ import re import six from six.moves.urllib.parse import parse_qs, urlparse +import xmltodict from moto.core.responses import _TemplateEnvironmentMixin @@ -65,7 +66,7 @@ class ResponseObject(_TemplateEnvironmentMixin): elif method == 'PUT': return self._bucket_response_put(request, region_name, bucket_name, querystring, headers) elif method == 'DELETE': - return self._bucket_response_delete(bucket_name, headers) + return self._bucket_response_delete(bucket_name, querystring, headers) elif method == 'POST': return self._bucket_response_post(request, bucket_name, headers) else: @@ -92,6 +93,12 @@ class ResponseObject(_TemplateEnvironmentMixin): bucket = self.backend.get_bucket(bucket_name) template = self.response_template(S3_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: versioning = self.backend.get_bucket_versioning(bucket_name) template = self.response_template(S3_BUCKET_GET_VERSIONING) @@ -143,14 +150,23 @@ class ResponseObject(_TemplateEnvironmentMixin): else: # Flask server body = request.data + body = body.decode('utf-8') + if 'versioning' in querystring: - ver = re.search('([A-Za-z]+)', body.decode('utf-8')) + ver = re.search('([A-Za-z]+)', body) if ver: self.backend.set_bucket_versioning(bucket_name, ver.group(1)) template = self.response_template(S3_BUCKET_VERSIONING) return template.render(bucket_versioning_status=ver.group(1)) else: 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: try: 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) 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) if removed_bucket: @@ -497,6 +518,39 @@ S3_DELETE_BUCKET_WITH_ITEMS_ERROR = """ S3_BUCKET_LOCATION = """ {{ location }}""" +S3_BUCKET_LIFECYCLE_CONFIGURATION = """ + + {% for rule in rules %} + + {{ rule.id }} + {{ rule.prefix if rule.prefix != None }} + {{ rule.status }} + {% if rule.storage_class %} + + {% if rule.transition_days %} + {{ rule.transition_days }} + {% endif %} + {% if rule.transition_date %} + {{ rule.transition_date }} + {% endif %} + {{ rule.storage_class }} + + {% endif %} + {% if rule.expiration_days or rule.expiration_date %} + + {% if rule.expiration_days %} + {{ rule.expiration_days }} + {% endif %} + {% if rule.expiration_date %} + {{ rule.expiration_date }} + {% endif %} + + {% endif %} + + {% endfor %} + +""" + S3_BUCKET_VERSIONING = """