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:
parent
1b91534165
commit
d952410965
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,3 +18,4 @@ venv/
|
|||||||
.python-version
|
.python-version
|
||||||
.vscode/
|
.vscode/
|
||||||
tests/file.tmp
|
tests/file.tmp
|
||||||
|
.eggs/
|
||||||
|
@ -3540,7 +3540,7 @@
|
|||||||
- [ ] delete_object
|
- [ ] delete_object
|
||||||
- [ ] delete_object_tagging
|
- [ ] delete_object_tagging
|
||||||
- [ ] delete_objects
|
- [ ] delete_objects
|
||||||
- [ ] get_bucket_accelerate_configuration
|
- [X] get_bucket_accelerate_configuration
|
||||||
- [X] get_bucket_acl
|
- [X] get_bucket_acl
|
||||||
- [ ] get_bucket_analytics_configuration
|
- [ ] get_bucket_analytics_configuration
|
||||||
- [ ] get_bucket_cors
|
- [ ] get_bucket_cors
|
||||||
@ -3574,7 +3574,7 @@
|
|||||||
- [ ] list_objects
|
- [ ] list_objects
|
||||||
- [ ] list_objects_v2
|
- [ ] list_objects_v2
|
||||||
- [ ] list_parts
|
- [ ] list_parts
|
||||||
- [ ] put_bucket_accelerate_configuration
|
- [X] put_bucket_accelerate_configuration
|
||||||
- [ ] put_bucket_acl
|
- [ ] put_bucket_acl
|
||||||
- [ ] put_bucket_analytics_configuration
|
- [ ] put_bucket_analytics_configuration
|
||||||
- [X] put_bucket_cors
|
- [X] put_bucket_cors
|
||||||
|
@ -17,8 +17,11 @@ import six
|
|||||||
from bisect import insort
|
from bisect import insort
|
||||||
from moto.core import BaseBackend, BaseModel
|
from moto.core import BaseBackend, BaseModel
|
||||||
from moto.core.utils import iso_8601_datetime_with_milliseconds, rfc_1123_datetime
|
from moto.core.utils import iso_8601_datetime_with_milliseconds, rfc_1123_datetime
|
||||||
from .exceptions import BucketAlreadyExists, MissingBucket, InvalidBucketName, InvalidPart, \
|
from .exceptions import (
|
||||||
EntityTooSmall, MissingKey, InvalidNotificationDestination, MalformedXML, InvalidStorageClass, DuplicateTagKeys
|
BucketAlreadyExists, MissingBucket, InvalidBucketName, InvalidPart, InvalidRequest,
|
||||||
|
EntityTooSmall, MissingKey, InvalidNotificationDestination, MalformedXML, InvalidStorageClass,
|
||||||
|
InvalidTargetBucketForLogging, DuplicateTagKeys, CrossLocationLoggingProhibitted
|
||||||
|
)
|
||||||
from .utils import clean_key_name, _VersionedKeyStore
|
from .utils import clean_key_name, _VersionedKeyStore
|
||||||
|
|
||||||
MAX_BUCKET_NAME_LENGTH = 63
|
MAX_BUCKET_NAME_LENGTH = 63
|
||||||
@ -463,6 +466,7 @@ class FakeBucket(BaseModel):
|
|||||||
self.cors = []
|
self.cors = []
|
||||||
self.logging = {}
|
self.logging = {}
|
||||||
self.notification_configuration = None
|
self.notification_configuration = None
|
||||||
|
self.accelerate_configuration = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def location(self):
|
def location(self):
|
||||||
@ -557,7 +561,6 @@ class FakeBucket(BaseModel):
|
|||||||
self.rules = []
|
self.rules = []
|
||||||
|
|
||||||
def set_cors(self, rules):
|
def set_cors(self, rules):
|
||||||
from moto.s3.exceptions import InvalidRequest, MalformedXML
|
|
||||||
self.cors = []
|
self.cors = []
|
||||||
|
|
||||||
if len(rules) > 100:
|
if len(rules) > 100:
|
||||||
@ -607,7 +610,6 @@ class FakeBucket(BaseModel):
|
|||||||
self.logging = {}
|
self.logging = {}
|
||||||
return
|
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):
|
# 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"]):
|
if not bucket_backend.buckets.get(logging_config["TargetBucket"]):
|
||||||
raise InvalidTargetBucketForLogging("The target bucket for logging does not exist.")
|
raise InvalidTargetBucketForLogging("The target bucket for logging does not exist.")
|
||||||
@ -655,6 +657,13 @@ class FakeBucket(BaseModel):
|
|||||||
if region != self.region_name:
|
if region != self.region_name:
|
||||||
raise InvalidNotificationDestination()
|
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):
|
def set_website_configuration(self, website_configuration):
|
||||||
self.website_configuration = website_configuration
|
self.website_configuration = website_configuration
|
||||||
|
|
||||||
@ -857,6 +866,15 @@ class S3Backend(BaseBackend):
|
|||||||
bucket = self.get_bucket(bucket_name)
|
bucket = self.get_bucket(bucket_name)
|
||||||
bucket.set_notification_configuration(notification_config)
|
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):
|
def initiate_multipart(self, bucket_name, key_name, metadata):
|
||||||
bucket = self.get_bucket(bucket_name)
|
bucket = self.get_bucket(bucket_name)
|
||||||
new_multipart = FakeMultipart(key_name, metadata)
|
new_multipart = FakeMultipart(key_name, metadata)
|
||||||
|
31
moto/s3/responses.py
Executable file → Normal file
31
moto/s3/responses.py
Executable file → Normal file
@ -257,6 +257,13 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||||||
return 200, {}, ""
|
return 200, {}, ""
|
||||||
template = self.response_template(S3_GET_BUCKET_NOTIFICATION_CONFIG)
|
template = self.response_template(S3_GET_BUCKET_NOTIFICATION_CONFIG)
|
||||||
return template.render(bucket=bucket)
|
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:
|
elif 'versions' in querystring:
|
||||||
delimiter = querystring.get('delimiter', [None])[0]
|
delimiter = querystring.get('delimiter', [None])[0]
|
||||||
@ -442,6 +449,15 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||||||
raise MalformedXML()
|
raise MalformedXML()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise 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:
|
else:
|
||||||
if body:
|
if body:
|
||||||
@ -1034,6 +1050,11 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||||||
|
|
||||||
return parsed_xml["NotificationConfiguration"]
|
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):
|
def _key_response_delete(self, bucket_name, query, key_name, headers):
|
||||||
if query.get('uploadId'):
|
if query.get('uploadId'):
|
||||||
upload_id = query['uploadId'][0]
|
upload_id = query['uploadId'][0]
|
||||||
@ -1686,3 +1707,13 @@ S3_GET_BUCKET_NOTIFICATION_CONFIG = """<?xml version="1.0" encoding="UTF-8"?>
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</NotificationConfiguration>
|
</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/"/>
|
||||||
|
"""
|
||||||
|
@ -2820,3 +2820,80 @@ def test_boto3_bucket_name_too_short():
|
|||||||
with assert_raises(ClientError) as exc:
|
with assert_raises(ClientError) as exc:
|
||||||
s3.create_bucket(Bucket='x'*2)
|
s3.create_bucket(Bucket='x'*2)
|
||||||
exc.exception.response['Error']['Code'].should.equal('InvalidBucketName')
|
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')
|
||||||
|
Loading…
Reference in New Issue
Block a user