1987 support transfer acceleration (#2018)

* chore(): remove executable flag on moto/s3/response.py

* chore(): ignore .eggs temp file

* feat(#1987): get bucket acceleration support

* feat(#1987): put bucket acceleration support

* feat(#1987): suspend undefined bucket is a no-op

* feat(#1987): validate accelerate_configuration status

* feat(#1987): bucket containing dots do not support acceleration

* doc(#1987): update implementation coverage
This commit is contained in:
Gregory Bataille 2019-05-25 12:19:00 +02:00 committed by Terry Cain
parent 1b91534165
commit d952410965
5 changed files with 133 additions and 6 deletions

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ venv/
.python-version
.vscode/
tests/file.tmp
.eggs/

View File

@ -3540,7 +3540,7 @@
- [ ] delete_object
- [ ] delete_object_tagging
- [ ] delete_objects
- [ ] get_bucket_accelerate_configuration
- [X] get_bucket_accelerate_configuration
- [X] get_bucket_acl
- [ ] get_bucket_analytics_configuration
- [ ] get_bucket_cors
@ -3574,7 +3574,7 @@
- [ ] list_objects
- [ ] list_objects_v2
- [ ] list_parts
- [ ] put_bucket_accelerate_configuration
- [X] put_bucket_accelerate_configuration
- [ ] put_bucket_acl
- [ ] put_bucket_analytics_configuration
- [X] put_bucket_cors

View File

@ -17,8 +17,11 @@ import six
from bisect import insort
from moto.core import BaseBackend, BaseModel
from moto.core.utils import iso_8601_datetime_with_milliseconds, rfc_1123_datetime
from .exceptions import BucketAlreadyExists, MissingBucket, InvalidBucketName, InvalidPart, \
EntityTooSmall, MissingKey, InvalidNotificationDestination, MalformedXML, InvalidStorageClass, DuplicateTagKeys
from .exceptions import (
BucketAlreadyExists, MissingBucket, InvalidBucketName, InvalidPart, InvalidRequest,
EntityTooSmall, MissingKey, InvalidNotificationDestination, MalformedXML, InvalidStorageClass,
InvalidTargetBucketForLogging, DuplicateTagKeys, CrossLocationLoggingProhibitted
)
from .utils import clean_key_name, _VersionedKeyStore
MAX_BUCKET_NAME_LENGTH = 63
@ -463,6 +466,7 @@ class FakeBucket(BaseModel):
self.cors = []
self.logging = {}
self.notification_configuration = None
self.accelerate_configuration = None
@property
def location(self):
@ -557,7 +561,6 @@ class FakeBucket(BaseModel):
self.rules = []
def set_cors(self, rules):
from moto.s3.exceptions import InvalidRequest, MalformedXML
self.cors = []
if len(rules) > 100:
@ -607,7 +610,6 @@ class FakeBucket(BaseModel):
self.logging = {}
return
from moto.s3.exceptions import InvalidTargetBucketForLogging, CrossLocationLoggingProhibitted
# Target bucket must exist in the same account (assuming all moto buckets are in the same account):
if not bucket_backend.buckets.get(logging_config["TargetBucket"]):
raise InvalidTargetBucketForLogging("The target bucket for logging does not exist.")
@ -655,6 +657,13 @@ class FakeBucket(BaseModel):
if region != self.region_name:
raise InvalidNotificationDestination()
def set_accelerate_configuration(self, accelerate_config):
if self.accelerate_configuration is None and accelerate_config == 'Suspended':
# Cannot "suspend" a not active acceleration. Leaves it undefined
return
self.accelerate_configuration = accelerate_config
def set_website_configuration(self, website_configuration):
self.website_configuration = website_configuration
@ -857,6 +866,15 @@ class S3Backend(BaseBackend):
bucket = self.get_bucket(bucket_name)
bucket.set_notification_configuration(notification_config)
def put_bucket_accelerate_configuration(self, bucket_name, accelerate_configuration):
if accelerate_configuration not in ['Enabled', 'Suspended']:
raise MalformedXML()
bucket = self.get_bucket(bucket_name)
if bucket.name.find('.') != -1:
raise InvalidRequest('PutBucketAccelerateConfiguration')
bucket.set_accelerate_configuration(accelerate_configuration)
def initiate_multipart(self, bucket_name, key_name, metadata):
bucket = self.get_bucket(bucket_name)
new_multipart = FakeMultipart(key_name, metadata)

31
moto/s3/responses.py Executable file → Normal file
View File

@ -257,6 +257,13 @@ class ResponseObject(_TemplateEnvironmentMixin):
return 200, {}, ""
template = self.response_template(S3_GET_BUCKET_NOTIFICATION_CONFIG)
return template.render(bucket=bucket)
elif "accelerate" in querystring:
bucket = self.backend.get_bucket(bucket_name)
if bucket.accelerate_configuration is None:
template = self.response_template(S3_BUCKET_ACCELERATE_NOT_SET)
return 200, {}, template.render()
template = self.response_template(S3_BUCKET_ACCELERATE)
return template.render(bucket=bucket)
elif 'versions' in querystring:
delimiter = querystring.get('delimiter', [None])[0]
@ -442,6 +449,15 @@ class ResponseObject(_TemplateEnvironmentMixin):
raise MalformedXML()
except Exception as e:
raise e
elif "accelerate" in querystring:
try:
accelerate_status = self._accelerate_config_from_xml(body)
self.backend.put_bucket_accelerate_configuration(bucket_name, accelerate_status)
return ""
except KeyError:
raise MalformedXML()
except Exception as e:
raise e
else:
if body:
@ -1034,6 +1050,11 @@ class ResponseObject(_TemplateEnvironmentMixin):
return parsed_xml["NotificationConfiguration"]
def _accelerate_config_from_xml(self, xml):
parsed_xml = xmltodict.parse(xml)
config = parsed_xml['AccelerateConfiguration']
return config['Status']
def _key_response_delete(self, bucket_name, query, key_name, headers):
if query.get('uploadId'):
upload_id = query['uploadId'][0]
@ -1686,3 +1707,13 @@ S3_GET_BUCKET_NOTIFICATION_CONFIG = """<?xml version="1.0" encoding="UTF-8"?>
{% endfor %}
</NotificationConfiguration>
"""
S3_BUCKET_ACCELERATE = """
<AccelerateConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Status>{{ bucket.accelerate_configuration }}</Status>
</AccelerateConfiguration>
"""
S3_BUCKET_ACCELERATE_NOT_SET = """
<AccelerateConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>
"""

View File

@ -2820,3 +2820,80 @@ def test_boto3_bucket_name_too_short():
with assert_raises(ClientError) as exc:
s3.create_bucket(Bucket='x'*2)
exc.exception.response['Error']['Code'].should.equal('InvalidBucketName')
@mock_s3
def test_accelerated_none_when_unspecified():
bucket_name = 'some_bucket'
s3 = boto3.client('s3')
s3.create_bucket(Bucket=bucket_name)
resp = s3.get_bucket_accelerate_configuration(Bucket=bucket_name)
resp.shouldnt.have.key('Status')
@mock_s3
def test_can_enable_bucket_acceleration():
bucket_name = 'some_bucket'
s3 = boto3.client('s3')
s3.create_bucket(Bucket=bucket_name)
resp = s3.put_bucket_accelerate_configuration(
Bucket=bucket_name,
AccelerateConfiguration={'Status': 'Enabled'},
)
resp.keys().should.have.length_of(1) # Response contains nothing (only HTTP headers)
resp = s3.get_bucket_accelerate_configuration(Bucket=bucket_name)
resp.should.have.key('Status')
resp['Status'].should.equal('Enabled')
@mock_s3
def test_can_suspend_bucket_acceleration():
bucket_name = 'some_bucket'
s3 = boto3.client('s3')
s3.create_bucket(Bucket=bucket_name)
resp = s3.put_bucket_accelerate_configuration(
Bucket=bucket_name,
AccelerateConfiguration={'Status': 'Enabled'},
)
resp = s3.put_bucket_accelerate_configuration(
Bucket=bucket_name,
AccelerateConfiguration={'Status': 'Suspended'},
)
resp.keys().should.have.length_of(1) # Response contains nothing (only HTTP headers)
resp = s3.get_bucket_accelerate_configuration(Bucket=bucket_name)
resp.should.have.key('Status')
resp['Status'].should.equal('Suspended')
@mock_s3
def test_suspending_acceleration_on_not_configured_bucket_does_nothing():
bucket_name = 'some_bucket'
s3 = boto3.client('s3')
s3.create_bucket(Bucket=bucket_name)
resp = s3.put_bucket_accelerate_configuration(
Bucket=bucket_name,
AccelerateConfiguration={'Status': 'Suspended'},
)
resp.keys().should.have.length_of(1) # Response contains nothing (only HTTP headers)
resp = s3.get_bucket_accelerate_configuration(Bucket=bucket_name)
resp.shouldnt.have.key('Status')
@mock_s3
def test_accelerate_configuration_status_validation():
bucket_name = 'some_bucket'
s3 = boto3.client('s3')
s3.create_bucket(Bucket=bucket_name)
with assert_raises(ClientError) as exc:
s3.put_bucket_accelerate_configuration(
Bucket=bucket_name,
AccelerateConfiguration={'Status': 'bad_status'},
)
exc.exception.response['Error']['Code'].should.equal('MalformedXML')
@mock_s3
def test_accelerate_configuration_is_not_supported_when_bucket_name_has_dots():
bucket_name = 'some.bucket.with.dots'
s3 = boto3.client('s3')
s3.create_bucket(Bucket=bucket_name)
with assert_raises(ClientError) as exc:
s3.put_bucket_accelerate_configuration(
Bucket=bucket_name,
AccelerateConfiguration={'Status': 'Enabled'},
)
exc.exception.response['Error']['Code'].should.equal('InvalidRequest')