This commit is contained in:
Steve Pulec 2017-02-23 21:37:43 -05:00
commit f37bad0e00
260 changed files with 6363 additions and 3766 deletions

View file

@ -3,4 +3,4 @@ from .models import s3_backend
s3_backends = {"global": s3_backend}
mock_s3 = s3_backend.decorator
mock_s3_deprecated = s3_backend.deprecated_decorator
mock_s3_deprecated = s3_backend.deprecated_decorator

View file

@ -12,6 +12,7 @@ ERROR_WITH_KEY_NAME = """{% extends 'single_error' %}
class S3ClientError(RESTError):
def __init__(self, *args, **kwargs):
kwargs.setdefault('template', 'single_error')
self.templates['bucket_error'] = ERROR_WITH_BUCKET_NAME
@ -19,6 +20,7 @@ class S3ClientError(RESTError):
class BucketError(S3ClientError):
def __init__(self, *args, **kwargs):
kwargs.setdefault('template', 'bucket_error')
self.templates['bucket_error'] = ERROR_WITH_BUCKET_NAME

View file

@ -120,6 +120,7 @@ class FakeKey(object):
class FakeMultipart(object):
def __init__(self, key_name, metadata):
self.key_name = key_name
self.metadata = metadata
@ -167,6 +168,7 @@ class FakeMultipart(object):
class FakeGrantee(object):
def __init__(self, id='', uri='', display_name=''):
self.id = id
self.uri = uri
@ -177,9 +179,12 @@ class FakeGrantee(object):
return 'Group' if self.uri else 'CanonicalUser'
ALL_USERS_GRANTEE = FakeGrantee(uri='http://acs.amazonaws.com/groups/global/AllUsers')
AUTHENTICATED_USERS_GRANTEE = FakeGrantee(uri='http://acs.amazonaws.com/groups/global/AuthenticatedUsers')
LOG_DELIVERY_GRANTEE = FakeGrantee(uri='http://acs.amazonaws.com/groups/s3/LogDelivery')
ALL_USERS_GRANTEE = FakeGrantee(
uri='http://acs.amazonaws.com/groups/global/AllUsers')
AUTHENTICATED_USERS_GRANTEE = FakeGrantee(
uri='http://acs.amazonaws.com/groups/global/AuthenticatedUsers')
LOG_DELIVERY_GRANTEE = FakeGrantee(
uri='http://acs.amazonaws.com/groups/s3/LogDelivery')
PERMISSION_FULL_CONTROL = 'FULL_CONTROL'
PERMISSION_WRITE = 'WRITE'
@ -189,27 +194,32 @@ PERMISSION_READ_ACP = 'READ_ACP'
class FakeGrant(object):
def __init__(self, grantees, permissions):
self.grantees = grantees
self.permissions = permissions
class FakeAcl(object):
def __init__(self, grants=[]):
self.grants = grants
def get_canned_acl(acl):
owner_grantee = FakeGrantee(id='75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a')
owner_grantee = FakeGrantee(
id='75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a')
grants = [FakeGrant([owner_grantee], [PERMISSION_FULL_CONTROL])]
if acl == 'private':
pass # no other permissions
elif acl == 'public-read':
grants.append(FakeGrant([ALL_USERS_GRANTEE], [PERMISSION_READ]))
elif acl == 'public-read-write':
grants.append(FakeGrant([ALL_USERS_GRANTEE], [PERMISSION_READ, PERMISSION_WRITE]))
grants.append(FakeGrant([ALL_USERS_GRANTEE], [
PERMISSION_READ, PERMISSION_WRITE]))
elif acl == 'authenticated-read':
grants.append(FakeGrant([AUTHENTICATED_USERS_GRANTEE], [PERMISSION_READ]))
grants.append(
FakeGrant([AUTHENTICATED_USERS_GRANTEE], [PERMISSION_READ]))
elif acl == 'bucket-owner-read':
pass # TODO: bucket owner ACL
elif acl == 'bucket-owner-full-control':
@ -217,13 +227,15 @@ def get_canned_acl(acl):
elif acl == 'aws-exec-read':
pass # TODO: bucket owner, EC2 Read
elif acl == 'log-delivery-write':
grants.append(FakeGrant([LOG_DELIVERY_GRANTEE], [PERMISSION_READ_ACP, PERMISSION_WRITE]))
grants.append(FakeGrant([LOG_DELIVERY_GRANTEE], [
PERMISSION_READ_ACP, PERMISSION_WRITE]))
else:
assert False, 'Unknown canned acl: %s' % (acl,)
return FakeAcl(grants=grants)
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):
@ -271,7 +283,8 @@ class FakeBucket(object):
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,
storage_class=transition[
'StorageClass'] if transition else None,
))
def delete_lifecycle(self):
@ -283,9 +296,11 @@ class FakeBucket(object):
def get_cfn_attribute(self, attribute_name):
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
if attribute_name == 'DomainName':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DomainName" ]"')
raise NotImplementedError(
'"Fn::GetAtt" : [ "{0}" , "DomainName" ]"')
elif attribute_name == 'WebsiteURL':
raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "WebsiteURL" ]"')
raise NotImplementedError(
'"Fn::GetAtt" : [ "{0}" , "WebsiteURL" ]"')
raise UnformattedGetAttTemplateException()
def set_acl(self, acl):
@ -470,20 +485,24 @@ class S3Backend(BaseBackend):
key_without_prefix = key_name.replace(prefix, "", 1)
if delimiter and delimiter in key_without_prefix:
# If delimiter, we need to split out folder_results
key_without_delimiter = key_without_prefix.split(delimiter)[0]
folder_results.add("{0}{1}{2}".format(prefix, key_without_delimiter, delimiter))
key_without_delimiter = key_without_prefix.split(delimiter)[
0]
folder_results.add("{0}{1}{2}".format(
prefix, key_without_delimiter, delimiter))
else:
key_results.add(key)
else:
for key_name, key in bucket.keys.items():
if delimiter and delimiter in key_name:
# If delimiter, we need to split out folder_results
folder_results.add(key_name.split(delimiter)[0] + delimiter)
folder_results.add(key_name.split(
delimiter)[0] + delimiter)
else:
key_results.add(key)
key_results = sorted(key_results, key=lambda key: key.name)
folder_results = [folder_name for folder_name in sorted(folder_results, key=lambda key: key)]
folder_results = [folder_name for folder_name in sorted(
folder_results, key=lambda key: key)]
return key_results, folder_results
@ -502,7 +521,8 @@ class S3Backend(BaseBackend):
src_key_name = clean_key_name(src_key_name)
dest_key_name = clean_key_name(dest_key_name)
dest_bucket = self.get_bucket(dest_bucket_name)
key = self.get_key(src_bucket_name, src_key_name, version_id=src_version_id)
key = self.get_key(src_bucket_name, src_key_name,
version_id=src_version_id)
if dest_key_name != src_key_name:
key = key.copy(dest_key_name)
dest_bucket.keys[dest_key_name] = key

View file

@ -33,6 +33,7 @@ def is_delete_keys(request, path, bucket_name):
class ResponseObject(_TemplateEnvironmentMixin):
def __init__(self, backend):
super(ResponseObject, self).__init__()
self.backend = backend
@ -70,7 +71,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
if match:
return False
path_based = (host == 's3.amazonaws.com' or re.match(r"s3[\.\-]([^.]*)\.amazonaws\.com", host))
path_based = (host == 's3.amazonaws.com' or re.match(
r"s3[\.\-]([^.]*)\.amazonaws\.com", host))
return not path_based
def is_delete_keys(self, request, path, bucket_name):
@ -148,7 +150,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
elif method == 'POST':
return self._bucket_response_post(request, body, bucket_name, headers)
else:
raise NotImplementedError("Method {0} has not been impelemented in the S3 backend yet".format(method))
raise NotImplementedError(
"Method {0} has not been impelemented in the S3 backend yet".format(method))
def _bucket_response_head(self, bucket_name, headers):
self.backend.get_bucket(bucket_name)
@ -158,11 +161,14 @@ class ResponseObject(_TemplateEnvironmentMixin):
if 'uploads' in querystring:
for unsup in ('delimiter', 'max-uploads'):
if unsup in querystring:
raise NotImplementedError("Listing multipart uploads with {} has not been implemented yet.".format(unsup))
multiparts = list(self.backend.get_all_multiparts(bucket_name).values())
raise NotImplementedError(
"Listing multipart uploads with {} has not been implemented yet.".format(unsup))
multiparts = list(
self.backend.get_all_multiparts(bucket_name).values())
if 'prefix' in querystring:
prefix = querystring.get('prefix', [None])[0]
multiparts = [upload for upload in multiparts if upload.key_name.startswith(prefix)]
multiparts = [
upload for upload in multiparts if upload.key_name.startswith(prefix)]
template = self.response_template(S3_ALL_MULTIPARTS)
return template.render(
bucket_name=bucket_name,
@ -175,7 +181,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
bucket = self.backend.get_bucket(bucket_name)
if not bucket.rules:
return 404, {}, "NoSuchLifecycleConfiguration"
template = self.response_template(S3_BUCKET_LIFECYCLE_CONFIGURATION)
template = self.response_template(
S3_BUCKET_LIFECYCLE_CONFIGURATION)
return template.render(rules=bucket.rules)
elif 'versioning' in querystring:
versioning = self.backend.get_bucket_versioning(bucket_name)
@ -188,7 +195,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
return 404, {}, template.render(bucket_name=bucket_name)
return 200, {}, policy
elif 'website' in querystring:
website_configuration = self.backend.get_bucket_website_configuration(bucket_name)
website_configuration = self.backend.get_bucket_website_configuration(
bucket_name)
return website_configuration
elif 'acl' in querystring:
bucket = self.backend.get_bucket(bucket_name)
@ -226,7 +234,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
bucket = self.backend.get_bucket(bucket_name)
prefix = querystring.get('prefix', [None])[0]
delimiter = querystring.get('delimiter', [None])[0]
result_keys, result_folders = self.backend.prefix_query(bucket, prefix, delimiter)
result_keys, result_folders = self.backend.prefix_query(
bucket, prefix, delimiter)
template = self.response_template(S3_BUCKET_GET_RESPONSE)
return 200, {}, template.render(
bucket=bucket,
@ -242,7 +251,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
prefix = querystring.get('prefix', [None])[0]
delimiter = querystring.get('delimiter', [None])[0]
result_keys, result_folders = self.backend.prefix_query(bucket, prefix, delimiter)
result_keys, result_folders = self.backend.prefix_query(
bucket, prefix, delimiter)
fetch_owner = querystring.get('fetch-owner', [False])[0]
max_keys = int(querystring.get('max-keys', [1000])[0])
@ -308,7 +318,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
return ""
else:
try:
new_bucket = self.backend.create_bucket(bucket_name, region_name)
new_bucket = self.backend.create_bucket(
bucket_name, region_name)
except BucketAlreadyExists:
if region_name == DEFAULT_REGION_NAME:
# us-east-1 has different behavior
@ -335,7 +346,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
return 204, {}, template.render(bucket=removed_bucket)
else:
# Tried to delete a bucket that still has keys
template = self.response_template(S3_DELETE_BUCKET_WITH_ITEMS_ERROR)
template = self.response_template(
S3_DELETE_BUCKET_WITH_ITEMS_ERROR)
return 409, {}, template.render(bucket=removed_bucket)
def _bucket_response_post(self, request, body, bucket_name, headers):
@ -393,7 +405,9 @@ class ResponseObject(_TemplateEnvironmentMixin):
if ',' in rspec:
raise NotImplementedError(
"Multiple range specifiers not supported")
toint = lambda i: int(i) if i else None
def toint(i):
return int(i) if i else None
begin, end = map(toint, rspec.split('-'))
if begin is not None: # byte range
end = last if end is None else min(end, last)
@ -455,7 +469,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
elif method == 'POST':
return self._key_response_post(request, body, bucket_name, query, key_name, headers)
else:
raise NotImplementedError("Method {0} has not been impelemented in the S3 backend yet".format(method))
raise NotImplementedError(
"Method {0} has not been impelemented in the S3 backend yet".format(method))
def _key_response_get(self, bucket_name, query, key_name, headers):
response_headers = {}
@ -489,7 +504,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
if 'x-amz-copy-source' in request.headers:
src = request.headers.get("x-amz-copy-source")
src_bucket, src_key = src.split("/", 1)
src_range = request.headers.get('x-amz-copy-source-range', '').split("bytes=")[-1]
src_range = request.headers.get(
'x-amz-copy-source-range', '').split("bytes=")[-1]
try:
start_byte, end_byte = src_range.split("-")
@ -522,7 +538,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
# Copy key
src_key_parsed = urlparse(request.headers.get("x-amz-copy-source"))
src_bucket, src_key = src_key_parsed.path.split("/", 1)
src_version_id = parse_qs(src_key_parsed.query).get('versionId', [None])[0]
src_version_id = parse_qs(src_key_parsed.query).get(
'versionId', [None])[0]
self.backend.copy_key(src_bucket, src_key, bucket_name, key_name,
storage=storage_class, acl=acl, src_version_id=src_version_id)
new_key = self.backend.get_key(bucket_name, key_name)
@ -557,7 +574,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
def _key_response_head(self, bucket_name, query, key_name, headers):
response_headers = {}
version_id = query.get('versionId', [None])[0]
key = self.backend.get_key(bucket_name, key_name, version_id=version_id)
key = self.backend.get_key(
bucket_name, key_name, version_id=version_id)
if key:
response_headers.update(key.metadata)
response_headers.update(key.response_dict)
@ -585,7 +603,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
grantees = []
for key_and_value in value.split(","):
key, value = re.match('([^=]+)="([^"]+)"', key_and_value.strip()).groups()
key, value = re.match(
'([^=]+)="([^"]+)"', key_and_value.strip()).groups()
if key.lower() == 'id':
grantees.append(FakeGrantee(id=value))
else:
@ -610,7 +629,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
ps = minidom.parseString(body).getElementsByTagName('Part')
prev = 0
for p in ps:
pn = int(p.getElementsByTagName('PartNumber')[0].firstChild.wholeText)
pn = int(p.getElementsByTagName(
'PartNumber')[0].firstChild.wholeText)
if pn <= prev:
raise InvalidPartOrder()
yield (pn, p.getElementsByTagName('ETag')[0].firstChild.wholeText)
@ -618,7 +638,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
def _key_response_post(self, request, body, bucket_name, query, key_name, headers):
if body == b'' and 'uploads' in query:
metadata = metadata_from_headers(request.headers)
multipart = self.backend.initiate_multipart(bucket_name, key_name, metadata)
multipart = self.backend.initiate_multipart(
bucket_name, key_name, metadata)
template = self.response_template(S3_MULTIPART_INITIATE_RESPONSE)
response = template.render(
@ -648,7 +669,9 @@ class ResponseObject(_TemplateEnvironmentMixin):
key.restore(int(days))
return r, {}, ""
else:
raise NotImplementedError("Method POST had only been implemented for multipart uploads and restore operations, so far")
raise NotImplementedError(
"Method POST had only been implemented for multipart uploads and restore operations, so far")
S3ResponseInstance = ResponseObject(s3_backend)

View file

@ -29,7 +29,8 @@ def bucket_name_from_url(url):
def metadata_from_headers(headers):
metadata = {}
meta_regex = re.compile('^x-amz-meta-([a-zA-Z0-9\-_]+)$', flags=re.IGNORECASE)
meta_regex = re.compile(
'^x-amz-meta-([a-zA-Z0-9\-_]+)$', flags=re.IGNORECASE)
for header, value in headers.items():
if isinstance(header, six.string_types):
result = meta_regex.match(header)