Skip to content

Commit

Permalink
Make last_modified property round in one direction (aio-libs#5303)
Browse files Browse the repository at this point in the history
  • Loading branch information
greshilov committed Dec 6, 2020
1 parent 4121c94 commit 84bd28b
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGES/5303.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make last_modified property round in one direction.
11 changes: 7 additions & 4 deletions aiohttp/web_fileresponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,20 @@ async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter
loop = asyncio.get_event_loop()
st = await loop.run_in_executor(None, filepath.stat)

self.last_modified = st.st_mtime # type: ignore
assert self.last_modified is not None

modsince = request.if_modified_since
if modsince is not None and st.st_mtime <= modsince.timestamp():

if modsince is not None and self.last_modified <= modsince:
self.set_status(HTTPNotModified.status_code)
self._length_check = False
# Delete any Content-Length headers provided by user. HTTP 304
# should always have empty response body
return await super().prepare(request)

unmodsince = request.if_unmodified_since
if unmodsince is not None and st.st_mtime > unmodsince.timestamp():
if unmodsince is not None and self.last_modified > unmodsince:
self.set_status(HTTPPreconditionFailed.status_code)
return await super().prepare(request)

Expand All @@ -143,7 +147,7 @@ async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter
start = None

ifrange = request.if_range
if ifrange is None or st.st_mtime <= ifrange.timestamp():
if ifrange is None or self.last_modified <= ifrange:
# If-Range header check:
# condition = cached date >= last modification date
# return 206 if True else 200.
Expand Down Expand Up @@ -216,7 +220,6 @@ async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter
self.headers[hdrs.CONTENT_ENCODING] = encoding
if gzip:
self.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
self.last_modified = st.st_mtime # type: ignore
self.content_length = count

self.headers[hdrs.ACCEPT_RANGES] = "bytes"
Expand Down
2 changes: 1 addition & 1 deletion aiohttp/web_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def last_modified(
self._headers.pop(hdrs.LAST_MODIFIED, None)
elif isinstance(value, (int, float)):
self._headers[hdrs.LAST_MODIFIED] = time.strftime(
"%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value))
"%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.floor(value))
)
elif isinstance(value, datetime.datetime):
self._headers[hdrs.LAST_MODIFIED] = time.strftime(
Expand Down
12 changes: 12 additions & 0 deletions tests/test_web_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import aiosignal
import pytest
from freezegun import freeze_time
from multidict import CIMultiDict, CIMultiDictProxy
from re_assert import Matches

Expand Down Expand Up @@ -257,6 +258,17 @@ def test_last_modified_reset() -> None:
assert resp.last_modified is None


@freeze_time("2020-12-06 15:38:05.5555")
def test_last_modified_round_consistency() -> None:
resp = StreamResponse()

resp.last_modified = datetime.datetime.now(tz=datetime.timezone.utc)
datetime_last_modified = resp.last_modified
resp.last_modified = datetime.datetime.now().timestamp()
float_last_modified = resp.last_modified
assert datetime_last_modified == float_last_modified


async def test_start() -> None:
req = make_request("GET", "/")
resp = StreamResponse()
Expand Down
15 changes: 10 additions & 5 deletions tests/test_web_sendfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ def test_using_gzip_if_header_present_and_file_available(loop: Any) -> None:
gz_filepath.open = mock.mock_open()
gz_filepath.is_file.return_value = True
gz_filepath.stat.return_value = mock.MagicMock()
gz_filepath.stat.st_size = 1024
gz_filepath.stat.return_value.st_size = 1024
gz_filepath.stat.return_value.st_mtime = 0

filepath = mock.Mock()
filepath.name = "logo.png"
Expand Down Expand Up @@ -43,7 +44,8 @@ def test_gzip_if_header_not_present_and_file_available(loop: Any) -> None:
filepath.open = mock.mock_open()
filepath.with_name.return_value = gz_filepath
filepath.stat.return_value = mock.MagicMock()
filepath.stat.st_size = 1024
filepath.stat.return_value.st_size = 1024
filepath.stat.return_value.st_mtime = 0

file_sender = FileResponse(filepath)
file_sender._sendfile = make_mocked_coro(None) # type: ignore
Expand All @@ -66,7 +68,8 @@ def test_gzip_if_header_not_present_and_file_not_available(loop: Any) -> None:
filepath.open = mock.mock_open()
filepath.with_name.return_value = gz_filepath
filepath.stat.return_value = mock.MagicMock()
filepath.stat.st_size = 1024
filepath.stat.return_value.st_size = 1024
filepath.stat.return_value.st_mtime = 0

file_sender = FileResponse(filepath)
file_sender._sendfile = make_mocked_coro(None) # type: ignore
Expand All @@ -91,7 +94,8 @@ def test_gzip_if_header_present_and_file_not_available(loop: Any) -> None:
filepath.open = mock.mock_open()
filepath.with_name.return_value = gz_filepath
filepath.stat.return_value = mock.MagicMock()
filepath.stat.st_size = 1024
filepath.stat.return_value.st_size = 1024
filepath.stat.return_value.st_mtime = 0

file_sender = FileResponse(filepath)
file_sender._sendfile = make_mocked_coro(None) # type: ignore
Expand All @@ -109,7 +113,8 @@ def test_status_controlled_by_user(loop: Any) -> None:
filepath.name = "logo.png"
filepath.open = mock.mock_open()
filepath.stat.return_value = mock.MagicMock()
filepath.stat.st_size = 1024
filepath.stat.return_value.st_size = 1024
filepath.stat.return_value.st_mtime = 0

file_sender = FileResponse(filepath, status=203)
file_sender._sendfile = make_mocked_coro(None) # type: ignore
Expand Down

0 comments on commit 84bd28b

Please sign in to comment.