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 = """