Add If-Match, If-None-Match and If-Unmodified-Since to S3 GET/HEAD (#3021)
fixes #2705
This commit is contained in:
parent
7054143701
commit
c2d1ce2c14
@ -1,6 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
from moto.core.exceptions import RESTError
|
||||
|
||||
from moto.core.exceptions import RESTError
|
||||
|
||||
ERROR_WITH_BUCKET_NAME = """{% extends 'single_error' %}
|
||||
{% block extra %}<BucketName>{{ bucket }}</BucketName>{% endblock %}
|
||||
@ -10,6 +10,10 @@ ERROR_WITH_KEY_NAME = """{% extends 'single_error' %}
|
||||
{% block extra %}<KeyName>{{ key_name }}</KeyName>{% endblock %}
|
||||
"""
|
||||
|
||||
ERROR_WITH_CONDITION_NAME = """{% extends 'single_error' %}
|
||||
{% block extra %}<Condition>{{ condition }}</Condition>{% endblock %}
|
||||
"""
|
||||
|
||||
|
||||
class S3ClientError(RESTError):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -386,3 +390,17 @@ class NoSuchUpload(S3ClientError):
|
||||
super(NoSuchUpload, self).__init__(
|
||||
"NoSuchUpload", "The specified multipart upload does not exist."
|
||||
)
|
||||
|
||||
|
||||
class PreconditionFailed(S3ClientError):
|
||||
code = 412
|
||||
|
||||
def __init__(self, failed_condition, **kwargs):
|
||||
kwargs.setdefault("template", "condition_error")
|
||||
self.templates["condition_error"] = ERROR_WITH_CONDITION_NAME
|
||||
super(PreconditionFailed, self).__init__(
|
||||
"PreconditionFailed",
|
||||
"At least one of the pre-conditions you specified did not hold",
|
||||
condition=failed_condition,
|
||||
**kwargs
|
||||
)
|
||||
|
@ -36,6 +36,7 @@ from .exceptions import (
|
||||
InvalidNotificationEvent,
|
||||
ObjectNotInActiveTierError,
|
||||
NoSystemTags,
|
||||
PreconditionFailed,
|
||||
)
|
||||
from .models import (
|
||||
s3_backend,
|
||||
@ -1149,13 +1150,28 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
)
|
||||
version_id = query.get("versionId", [None])[0]
|
||||
if_modified_since = headers.get("If-Modified-Since", None)
|
||||
if_match = headers.get("If-Match", None)
|
||||
if_none_match = headers.get("If-None-Match", None)
|
||||
if_unmodified_since = headers.get("If-Unmodified-Since", None)
|
||||
|
||||
key = self.backend.get_object(bucket_name, key_name, version_id=version_id)
|
||||
if key is None:
|
||||
raise MissingKey(key_name)
|
||||
|
||||
if if_unmodified_since:
|
||||
if_unmodified_since = str_to_rfc_1123_datetime(if_unmodified_since)
|
||||
if key.last_modified > if_unmodified_since:
|
||||
raise PreconditionFailed("If-Unmodified-Since")
|
||||
if if_match and key.etag != if_match:
|
||||
raise PreconditionFailed("If-Match")
|
||||
|
||||
if if_modified_since:
|
||||
if_modified_since = str_to_rfc_1123_datetime(if_modified_since)
|
||||
if if_modified_since and key.last_modified < if_modified_since:
|
||||
if key.last_modified < if_modified_since:
|
||||
return 304, response_headers, "Not Modified"
|
||||
if if_none_match and key.etag == if_none_match:
|
||||
return 304, response_headers, "Not Modified"
|
||||
|
||||
if "acl" in query:
|
||||
template = self.response_template(S3_OBJECT_ACL_RESPONSE)
|
||||
return 200, response_headers, template.render(obj=key)
|
||||
@ -1319,8 +1335,9 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
part_number = int(part_number)
|
||||
|
||||
if_modified_since = headers.get("If-Modified-Since", None)
|
||||
if if_modified_since:
|
||||
if_modified_since = str_to_rfc_1123_datetime(if_modified_since)
|
||||
if_match = headers.get("If-Match", None)
|
||||
if_none_match = headers.get("If-None-Match", None)
|
||||
if_unmodified_since = headers.get("If-Unmodified-Since", None)
|
||||
|
||||
key = self.backend.get_object(
|
||||
bucket_name, key_name, version_id=version_id, part_number=part_number
|
||||
@ -1329,10 +1346,21 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
response_headers.update(key.metadata)
|
||||
response_headers.update(key.response_dict)
|
||||
|
||||
if if_modified_since and key.last_modified < if_modified_since:
|
||||
if if_unmodified_since:
|
||||
if_unmodified_since = str_to_rfc_1123_datetime(if_unmodified_since)
|
||||
if key.last_modified > if_unmodified_since:
|
||||
return 412, response_headers, ""
|
||||
if if_match and key.etag != if_match:
|
||||
return 412, response_headers, ""
|
||||
|
||||
if if_modified_since:
|
||||
if_modified_since = str_to_rfc_1123_datetime(if_modified_since)
|
||||
if key.last_modified < if_modified_since:
|
||||
return 304, response_headers, "Not Modified"
|
||||
if if_none_match and key.etag == if_none_match:
|
||||
return 304, response_headers, "Not Modified"
|
||||
else:
|
||||
return 200, response_headers, ""
|
||||
|
||||
return 200, response_headers, ""
|
||||
else:
|
||||
return 404, response_headers, ""
|
||||
|
||||
|
@ -2335,6 +2335,64 @@ def test_boto3_get_object_if_modified_since():
|
||||
e.response["Error"].should.equal({"Code": "304", "Message": "Not Modified"})
|
||||
|
||||
|
||||
@mock_s3
|
||||
def test_boto3_get_object_if_unmodified_since():
|
||||
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
|
||||
bucket_name = "blah"
|
||||
s3.create_bucket(Bucket=bucket_name)
|
||||
|
||||
key = "hello.txt"
|
||||
|
||||
s3.put_object(Bucket=bucket_name, Key=key, Body="test")
|
||||
|
||||
with assert_raises(botocore.exceptions.ClientError) as err:
|
||||
s3.get_object(
|
||||
Bucket=bucket_name,
|
||||
Key=key,
|
||||
IfUnmodifiedSince=datetime.datetime.utcnow() - datetime.timedelta(hours=1),
|
||||
)
|
||||
e = err.exception
|
||||
e.response["Error"]["Code"].should.equal("PreconditionFailed")
|
||||
e.response["Error"]["Condition"].should.equal("If-Unmodified-Since")
|
||||
|
||||
|
||||
@mock_s3
|
||||
def test_boto3_get_object_if_match():
|
||||
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
|
||||
bucket_name = "blah"
|
||||
s3.create_bucket(Bucket=bucket_name)
|
||||
|
||||
key = "hello.txt"
|
||||
|
||||
s3.put_object(Bucket=bucket_name, Key=key, Body="test")
|
||||
|
||||
with assert_raises(botocore.exceptions.ClientError) as err:
|
||||
s3.get_object(
|
||||
Bucket=bucket_name, Key=key, IfMatch='"hello"',
|
||||
)
|
||||
e = err.exception
|
||||
e.response["Error"]["Code"].should.equal("PreconditionFailed")
|
||||
e.response["Error"]["Condition"].should.equal("If-Match")
|
||||
|
||||
|
||||
@mock_s3
|
||||
def test_boto3_get_object_if_none_match():
|
||||
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
|
||||
bucket_name = "blah"
|
||||
s3.create_bucket(Bucket=bucket_name)
|
||||
|
||||
key = "hello.txt"
|
||||
|
||||
etag = s3.put_object(Bucket=bucket_name, Key=key, Body="test")["ETag"]
|
||||
|
||||
with assert_raises(botocore.exceptions.ClientError) as err:
|
||||
s3.get_object(
|
||||
Bucket=bucket_name, Key=key, IfNoneMatch=etag,
|
||||
)
|
||||
e = err.exception
|
||||
e.response["Error"].should.equal({"Code": "304", "Message": "Not Modified"})
|
||||
|
||||
|
||||
@mock_s3
|
||||
def test_boto3_head_object_if_modified_since():
|
||||
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
|
||||
@ -2355,6 +2413,62 @@ def test_boto3_head_object_if_modified_since():
|
||||
e.response["Error"].should.equal({"Code": "304", "Message": "Not Modified"})
|
||||
|
||||
|
||||
@mock_s3
|
||||
def test_boto3_head_object_if_unmodified_since():
|
||||
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
|
||||
bucket_name = "blah"
|
||||
s3.create_bucket(Bucket=bucket_name)
|
||||
|
||||
key = "hello.txt"
|
||||
|
||||
s3.put_object(Bucket=bucket_name, Key=key, Body="test")
|
||||
|
||||
with assert_raises(botocore.exceptions.ClientError) as err:
|
||||
s3.head_object(
|
||||
Bucket=bucket_name,
|
||||
Key=key,
|
||||
IfUnmodifiedSince=datetime.datetime.utcnow() - datetime.timedelta(hours=1),
|
||||
)
|
||||
e = err.exception
|
||||
e.response["Error"].should.equal({"Code": "412", "Message": "Precondition Failed"})
|
||||
|
||||
|
||||
@mock_s3
|
||||
def test_boto3_head_object_if_match():
|
||||
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
|
||||
bucket_name = "blah"
|
||||
s3.create_bucket(Bucket=bucket_name)
|
||||
|
||||
key = "hello.txt"
|
||||
|
||||
s3.put_object(Bucket=bucket_name, Key=key, Body="test")
|
||||
|
||||
with assert_raises(botocore.exceptions.ClientError) as err:
|
||||
s3.head_object(
|
||||
Bucket=bucket_name, Key=key, IfMatch='"hello"',
|
||||
)
|
||||
e = err.exception
|
||||
e.response["Error"].should.equal({"Code": "412", "Message": "Precondition Failed"})
|
||||
|
||||
|
||||
@mock_s3
|
||||
def test_boto3_head_object_if_none_match():
|
||||
s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
|
||||
bucket_name = "blah"
|
||||
s3.create_bucket(Bucket=bucket_name)
|
||||
|
||||
key = "hello.txt"
|
||||
|
||||
etag = s3.put_object(Bucket=bucket_name, Key=key, Body="test")["ETag"]
|
||||
|
||||
with assert_raises(botocore.exceptions.ClientError) as err:
|
||||
s3.head_object(
|
||||
Bucket=bucket_name, Key=key, IfNoneMatch=etag,
|
||||
)
|
||||
e = err.exception
|
||||
e.response["Error"].should.equal({"Code": "304", "Message": "Not Modified"})
|
||||
|
||||
|
||||
@mock_s3
|
||||
@reduced_min_part_size
|
||||
def test_boto3_multipart_etag():
|
||||
|
Loading…
Reference in New Issue
Block a user