Merge pull request #960 from spg/feat-s3-if-modified-since
feat(s3) HeadObject: honor If-Modified-Since header
This commit is contained in:
commit
6163363c15
@ -174,11 +174,17 @@ def iso_8601_datetime_without_milliseconds(datetime):
|
|||||||
return datetime.strftime("%Y-%m-%dT%H:%M:%S") + 'Z'
|
return datetime.strftime("%Y-%m-%dT%H:%M:%S") + 'Z'
|
||||||
|
|
||||||
|
|
||||||
def rfc_1123_datetime(datetime):
|
|
||||||
RFC1123 = '%a, %d %b %Y %H:%M:%S GMT'
|
RFC1123 = '%a, %d %b %Y %H:%M:%S GMT'
|
||||||
|
|
||||||
|
|
||||||
|
def rfc_1123_datetime(datetime):
|
||||||
return datetime.strftime(RFC1123)
|
return datetime.strftime(RFC1123)
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_rfc_1123_datetime(str):
|
||||||
|
return datetime.datetime.strptime(str, RFC1123)
|
||||||
|
|
||||||
|
|
||||||
def unix_time(dt=None):
|
def unix_time(dt=None):
|
||||||
dt = dt or datetime.datetime.utcnow()
|
dt = dt or datetime.datetime.utcnow()
|
||||||
epoch = datetime.datetime.utcfromtimestamp(0)
|
epoch = datetime.datetime.utcfromtimestamp(0)
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
from moto.core.utils import str_to_rfc_1123_datetime
|
||||||
from six.moves.urllib.parse import parse_qs, urlparse
|
from six.moves.urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
import xmltodict
|
import xmltodict
|
||||||
@ -483,7 +484,7 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||||||
elif method == 'PUT':
|
elif method == 'PUT':
|
||||||
return self._key_response_put(request, body, bucket_name, query, key_name, headers)
|
return self._key_response_put(request, body, bucket_name, query, key_name, headers)
|
||||||
elif method == 'HEAD':
|
elif method == 'HEAD':
|
||||||
return self._key_response_head(bucket_name, query, key_name, headers)
|
return self._key_response_head(bucket_name, query, key_name, headers=request.headers)
|
||||||
elif method == 'DELETE':
|
elif method == 'DELETE':
|
||||||
return self._key_response_delete(bucket_name, query, key_name, headers)
|
return self._key_response_delete(bucket_name, query, key_name, headers)
|
||||||
elif method == 'POST':
|
elif method == 'POST':
|
||||||
@ -597,11 +598,20 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
|||||||
def _key_response_head(self, bucket_name, query, key_name, headers):
|
def _key_response_head(self, bucket_name, query, key_name, headers):
|
||||||
response_headers = {}
|
response_headers = {}
|
||||||
version_id = query.get('versionId', [None])[0]
|
version_id = query.get('versionId', [None])[0]
|
||||||
|
|
||||||
|
if_modified_since = headers.get('If-Modified-Since', None)
|
||||||
|
if if_modified_since:
|
||||||
|
if_modified_since = str_to_rfc_1123_datetime(if_modified_since)
|
||||||
|
|
||||||
key = self.backend.get_key(
|
key = self.backend.get_key(
|
||||||
bucket_name, key_name, version_id=version_id)
|
bucket_name, key_name, version_id=version_id)
|
||||||
if key:
|
if key:
|
||||||
response_headers.update(key.metadata)
|
response_headers.update(key.metadata)
|
||||||
response_headers.update(key.response_dict)
|
response_headers.update(key.response_dict)
|
||||||
|
|
||||||
|
if if_modified_since and key.last_modified < if_modified_since:
|
||||||
|
return 304, response_headers, 'Not Modified'
|
||||||
|
else:
|
||||||
return 200, response_headers, ""
|
return 200, response_headers, ""
|
||||||
else:
|
else:
|
||||||
return 404, response_headers, ""
|
return 404, response_headers, ""
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
from six.moves.urllib.request import urlopen
|
from six.moves.urllib.request import urlopen
|
||||||
from six.moves.urllib.error import HTTPError
|
from six.moves.urllib.error import HTTPError
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@ -10,6 +12,7 @@ import json
|
|||||||
import boto
|
import boto
|
||||||
import boto3
|
import boto3
|
||||||
from botocore.client import ClientError
|
from botocore.client import ClientError
|
||||||
|
import botocore.exceptions
|
||||||
from boto.exception import S3CreateError, S3ResponseError
|
from boto.exception import S3CreateError, S3ResponseError
|
||||||
from boto.s3.connection import S3Connection
|
from boto.s3.connection import S3Connection
|
||||||
from boto.s3.key import Key
|
from boto.s3.key import Key
|
||||||
@ -1267,6 +1270,30 @@ def test_boto3_head_object_with_versioning():
|
|||||||
old_head_object['ContentLength'].should.equal(len(old_content))
|
old_head_object['ContentLength'].should.equal(len(old_content))
|
||||||
|
|
||||||
|
|
||||||
|
@mock_s3
|
||||||
|
def test_boto3_head_object_if_modified_since():
|
||||||
|
s3 = boto3.client('s3', region_name='us-east-1')
|
||||||
|
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,
|
||||||
|
IfModifiedSince=datetime.datetime.utcnow() + datetime.timedelta(hours=1)
|
||||||
|
)
|
||||||
|
e = err.exception
|
||||||
|
e.response['Error'].should.equal({'Code': '304', 'Message': 'Not Modified'})
|
||||||
|
|
||||||
|
|
||||||
@mock_s3
|
@mock_s3
|
||||||
@reduced_min_part_size
|
@reduced_min_part_size
|
||||||
def test_boto3_multipart_etag():
|
def test_boto3_multipart_etag():
|
||||||
|
Loading…
Reference in New Issue
Block a user