From 13c7b70d481cbc60d6a24921882dd97bea7717b7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Nov 2016 15:33:34 -0800 Subject: [PATCH] Add gzip support to FileSender --- CHANGES.rst | 2 +- CONTRIBUTORS.txt | 1 + aiohttp/file_sender.py | 10 ++++ docs/web_reference.rst | 6 ++ tests/test_web_sendfile.py | 111 ++++++++++++++++++++++++++++++++++++- 5 files changed, 128 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 408825c8140..8f2d5415c95 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -34,7 +34,7 @@ CHANGES - Fix bugs related to the use of unicode hostnames #1444 -- +- FileSender will send gzipped response if gzip version available #1426 - diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 6062077bdb0..5410e461dab 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -108,6 +108,7 @@ Olaf Conradi Pankaj Pandey Pau Freixes Paul Colomiets +Paulus Schoutsen Philipp A. Rafael Viotti Raúl Cumplido diff --git a/aiohttp/file_sender.py b/aiohttp/file_sender.py index 73eb67aba44..b2005a668a2 100644 --- a/aiohttp/file_sender.py +++ b/aiohttp/file_sender.py @@ -144,6 +144,14 @@ def _sendfile_fallback(self, request, resp, fobj, count): @asyncio.coroutine def send(self, request, filepath): """Send filepath to client using request.""" + gzip = False + if 'gzip' in request.headers.get(hdrs.ACCEPT_ENCODING, ''): + gzip_path = filepath.with_name(filepath.name + '.gz') + + if gzip_path.is_file(): + filepath = gzip_path + gzip = True + st = filepath.stat() modsince = request.if_modified_since @@ -182,6 +190,8 @@ def send(self, request, filepath): resp.content_type = ct if encoding: resp.headers[hdrs.CONTENT_ENCODING] = encoding + if gzip: + resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING resp.last_modified = st.st_mtime resp.content_length = count diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 5295595b6cf..02720890194 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -1547,6 +1547,9 @@ Router is any object that implements :class:`AbstractRouter` interface. system call even if the platform supports it. This can be accomplished by by setting environment variable ``AIOHTTP_NOSENDFILE=1``. + If a gzip version of the static content exists at file path + ``.gz``, it + will be used for the response. + .. warning:: Use :meth:`add_static` for development only. In production, @@ -1561,6 +1564,9 @@ Router is any object that implements :class:`AbstractRouter` interface. Disable ``sendfile`` by setting environment variable ``AIOHTTP_NOSENDFILE=1`` + .. versionchanged:: 1.2.0 + Send gzip version if file path + ``.gz`` exists. + :param str prefix: URL path prefix for handled static files :param path: path to the folder in file system that contains diff --git a/tests/test_web_sendfile.py b/tests/test_web_sendfile.py index ccc24d8099f..420c8e9e9eb 100644 --- a/tests/test_web_sendfile.py +++ b/tests/test_web_sendfile.py @@ -1,8 +1,11 @@ import os from unittest import mock -from aiohttp import helpers +from yarl import URL + +from aiohttp import hdrs, helpers from aiohttp.file_sender import FileSender +from aiohttp.test_utils import make_mocked_coro, make_mocked_request def test_env_nosendfile(): @@ -75,3 +78,109 @@ def test__sendfile_cb_return_on_cancelling(loop): assert not fake_loop.add_writer.called assert not fake_loop.remove_writer.called assert not m_os.sendfile.called + + +def test_using_gzip_if_header_present_and_file_available(loop): + request = make_mocked_request( + 'GET', URL('http://python.org/logo.png'), headers={ + hdrs.ACCEPT_ENCODING: 'gzip' + } + ) + + gz_filepath = mock.Mock() + 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 + + filepath = mock.Mock() + filepath.name = 'logo.png' + filepath.open = mock.mock_open() + filepath.with_name.return_value = gz_filepath + + file_sender = FileSender() + file_sender._sendfile = make_mocked_coro(None) + + loop.run_until_complete(file_sender.send(request, filepath)) + + assert not filepath.open.called + assert gz_filepath.open.called + + +def test_gzip_if_header_not_present_and_file_available(loop): + request = make_mocked_request( + 'GET', URL('http://python.org/logo.png'), headers={ + } + ) + + gz_filepath = mock.Mock() + gz_filepath.open = mock.mock_open() + gz_filepath.is_file.return_value = True + + filepath = mock.Mock() + filepath.name = 'logo.png' + filepath.open = mock.mock_open() + filepath.with_name.return_value = gz_filepath + filepath.stat.return_value = mock.MagicMock() + filepath.stat.st_size = 1024 + + file_sender = FileSender() + file_sender._sendfile = make_mocked_coro(None) + + loop.run_until_complete(file_sender.send(request, filepath)) + + assert filepath.open.called + assert not gz_filepath.open.called + + +def test_gzip_if_header_not_present_and_file_not_available(loop): + request = make_mocked_request( + 'GET', URL('http://python.org/logo.png'), headers={ + } + ) + + gz_filepath = mock.Mock() + gz_filepath.open = mock.mock_open() + gz_filepath.is_file.return_value = False + + filepath = mock.Mock() + filepath.name = 'logo.png' + filepath.open = mock.mock_open() + filepath.with_name.return_value = gz_filepath + filepath.stat.return_value = mock.MagicMock() + filepath.stat.st_size = 1024 + + file_sender = FileSender() + file_sender._sendfile = make_mocked_coro(None) + + loop.run_until_complete(file_sender.send(request, filepath)) + + assert filepath.open.called + assert not gz_filepath.open.called + + +def test_gzip_if_header_present_and_file_not_available(loop): + request = make_mocked_request( + 'GET', URL('http://python.org/logo.png'), headers={ + hdrs.ACCEPT_ENCODING: 'gzip' + } + ) + + gz_filepath = mock.Mock() + gz_filepath.open = mock.mock_open() + gz_filepath.is_file.return_value = False + + filepath = mock.Mock() + filepath.name = 'logo.png' + filepath.open = mock.mock_open() + filepath.with_name.return_value = gz_filepath + filepath.stat.return_value = mock.MagicMock() + filepath.stat.st_size = 1024 + + file_sender = FileSender() + file_sender._sendfile = make_mocked_coro(None) + + loop.run_until_complete(file_sender.send(request, filepath)) + + assert filepath.open.called + assert not gz_filepath.open.called