Thanks to visit codestin.com
Credit goes to github.com

Skip to content

S3 ASF fix VersionId and Expires, implement ResponseHeaders override #6906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions localstack/services/s3/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,7 @@ class S3Store(BaseStore):
default=dict
)

bucket_versioning_status: Dict[BucketName, bool] = LocalAttribute(default=dict)


s3_stores = AccountRegionBundle("s3", S3Store)
56 changes: 49 additions & 7 deletions localstack/services/s3/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
PutBucketLifecycleConfigurationRequest,
PutBucketLifecycleRequest,
PutBucketRequestPaymentRequest,
PutBucketVersioningRequest,
PutObjectOutput,
PutObjectRequest,
S3Api,
Expand All @@ -55,6 +56,7 @@
from localstack.services.plugins import ServiceLifecycleHook
from localstack.services.s3.models import S3Store, s3_stores
from localstack.services.s3.utils import (
ALLOWED_HEADER_OVERRIDES,
VALID_ACL_PREDEFINED_GROUPS,
VALID_GRANTEE_PERMISSIONS,
get_header_name,
Expand Down Expand Up @@ -101,6 +103,11 @@ class S3Provider(S3Api, ServiceLifecycleHook):
def get_store() -> S3Store:
return s3_stores[get_aws_account_id()][aws_stack.get_region()]

def _clear_bucket_from_store(self, bucket: BucketName):
store = self.get_store()
store.bucket_lifecycle_configuration.pop(bucket, None)
store.bucket_versioning_status.pop(bucket, None)

def on_after_init(self):
apply_moto_patches()
self.add_custom_routes()
Expand All @@ -127,9 +134,7 @@ def delete_bucket(
self, context: RequestContext, bucket: BucketName, expected_bucket_owner: AccountId = None
) -> None:
call_moto(context)
store = self.get_store()
# TODO: create a helper method for cleaning up the store, already done in next PR
store.bucket_lifecycle_configuration.pop(bucket, None)
self._clear_bucket_from_store(bucket)

def get_bucket_location(
self, context: RequestContext, bucket: BucketName, expected_bucket_owner: AccountId = None
Expand Down Expand Up @@ -203,14 +208,24 @@ def head_object(
@handler("GetObject", expand=False)
def get_object(self, context: RequestContext, request: GetObjectRequest) -> GetObjectOutput:
key = request["Key"]
if is_object_expired(context, bucket=request["Bucket"], key=key):
bucket = request["Bucket"]
if is_object_expired(context, bucket=bucket, key=key):
# TODO: old behaviour was deleting key instantly if expired. AWS cleans up only once a day generally
# see if we need to implement a feature flag
# but you can still HeadObject on it and you get the expiry time
ex = NoSuchKey("The specified key does not exist.")
ex.Key = key
raise ex

response: GetObjectOutput = call_moto(context)
# check for the presence in the response, might be fixed by moto one day
if "VersionId" in response and bucket not in self.get_store().bucket_versioning_status:
response.pop("VersionId")

for request_param, response_param in ALLOWED_HEADER_OVERRIDES.items():
if request_param_value := request.get(request_param): # noqa
response[response_param] = request_param_value # noqa

response["AcceptRanges"] = "bytes"
return response

Expand All @@ -225,11 +240,16 @@ def put_object(

response: PutObjectOutput = call_moto(context)

# moto interprets the Expires in query string for presigned URL as an Expires header and use it for the object
# we set it to the correctly parsed value in Request, else we remove it from moto metadata
moto_backend = get_moto_s3_backend(context)
bucket = get_bucket_from_moto(moto_backend, bucket=request["Bucket"])
key_object = get_key_from_moto_bucket(bucket, key=request["Key"])
if expires := request.get("Expires"):
moto_backend = get_moto_s3_backend(context)
bucket = get_bucket_from_moto(moto_backend, bucket=request["Bucket"])
key_object = get_key_from_moto_bucket(bucket, key=request["Key"])
key_object.set_expiry(expires)
elif "expires" in key_object.metadata: # if it got added from query string parameter
metadata = {k: v for k, v in key_object.metadata.items() if k != "expires"}
key_object.set_metadata(metadata, replace=True)

return response

Expand Down Expand Up @@ -362,6 +382,19 @@ def put_bucket_acl(

call_moto(context)

@handler("PutBucketVersioning", expand=False)
def put_bucket_versioning(
self,
context: RequestContext,
request: PutBucketVersioningRequest,
) -> None:
call_moto(context)
# set it in the store, so we can keep the state if it was ever enabled
if versioning_status := request.get("VersioningConfiguration", {}).get("Status"):
bucket_name = request.get("Bucket", "")
store = self.get_store()
store.bucket_versioning_status[bucket_name] = versioning_status == "Enabled"

def add_custom_routes(self):
# virtual-host style: https://bucket-name.s3.region-code.amazonaws.com/key-name
# host_pattern_vhost_style = f"{bucket}.s3.<regex('({AWS_REGION_REGEX}\.)?'):region>{LOCALHOST_HOSTNAME}:{get_edge_port_http()}"
Expand Down Expand Up @@ -603,6 +636,15 @@ def _fix_key_response_get(fn, *args, **kwargs):

return code, headers, body

@patch(moto_s3_responses.S3Response._key_response_post)
def _fix_key_response_post(fn, self, request, body, bucket_name, *args, **kwargs):
code, headers, body = fn(self, request, body, bucket_name, *args, **kwargs)
bucket = self.backend.get_bucket(bucket_name)
if not bucket.is_versioned:
headers.pop("x-amz-version-id", None)

return code, headers, body

@patch(moto_s3_responses.S3Response.all_buckets)
def _fix_owner_id_list_bucket(fn, *args, **kwargs) -> str:
"""
Expand Down
10 changes: 10 additions & 0 deletions localstack/services/s3/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@
Permission.WRITE_ACP,
}

# response header overrides the client may request
ALLOWED_HEADER_OVERRIDES = {
"ResponseContentType": "ContentType",
"ResponseContentLanguage": "ContentLanguage",
"ResponseExpires": "Expires",
"ResponseCacheControl": "CacheControl",
"ResponseContentDisposition": "ContentDisposition",
"ResponseContentEncoding": "ContentEncoding",
}


class InvalidRequest(ServiceException):
"""The lifecycle configuration does not exist."""
Expand Down
Loading