From d724fabc826862fe39a1224fc4c6e0460c5990ae Mon Sep 17 00:00:00 2001 From: Kojiro20 <74428472+Kojiro20@users.noreply.github.com> Date: Fri, 13 Nov 2020 11:32:35 -0800 Subject: [PATCH 1/6] Enable offset in FileResponse Without this FileResponse cannot be used to satisfy range headers. --- starlette/responses.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/starlette/responses.py b/starlette/responses.py index ff122fba1..0c933112f 100644 --- a/starlette/responses.py +++ b/starlette/responses.py @@ -235,7 +235,7 @@ class FileResponse(Response): def __init__( self, - path: typing.Union[str, "os.PathLike[str]"], + path: str, status_code: int = 200, headers: dict = None, media_type: str = None, @@ -243,6 +243,7 @@ def __init__( filename: str = None, stat_result: os.stat_result = None, method: str = None, + offset: int = 0 ) -> None: assert aiofiles is not None, "'aiofiles' must be installed to use FileResponse" self.path = path @@ -266,6 +267,7 @@ def __init__( self.stat_result = stat_result if stat_result is not None: self.set_stat_headers(stat_result) + self.offset = offset def set_stat_headers(self, stat_result: os.stat_result) -> None: content_length = str(stat_result.st_size) @@ -298,10 +300,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if self.send_header_only: await send({"type": "http.response.body", "body": b"", "more_body": False}) else: - # Tentatively ignoring type checking failure to work around the wrong type - # definitions for aiofile that come with typeshed. See - # https://github.com/python/typeshed/pull/4650 - async with aiofiles.open(self.path, mode="rb") as file: # type: ignore + async with aiofiles.threadpool.open(self.path, mode='rb') as file: + await file.seek(self.offset) more_body = True while more_body: chunk = await file.read(self.chunk_size) From e1bd79863dbbf3a9dd1cb8268b03a3f7b85b88ec Mon Sep 17 00:00:00 2001 From: Kojiro20 <74428472+Kojiro20@users.noreply.github.com> Date: Fri, 13 Nov 2020 11:36:30 -0800 Subject: [PATCH 2/6] Add unit test for FileResponse offset --- tests/test_responses.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_responses.py b/tests/test_responses.py index 10fbe673c..262dc7a05 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -244,6 +244,19 @@ def test_file_response_with_chinese_filename(tmpdir): assert response.headers["content-disposition"] == expected_disposition +def test_file_response_with_offset(tmpdir): + content = b"000 111 222 333 444 555" + filename = "offset" + path = os.path.join(tmpdir, filename) + with open(path, "wb") as f: + f.write(content) + for offset in range(0,24,4): # skip blocks of 4 + app = FileResponse(path=path, offset=offset) + client = TestClient(app) + response = client.get("/") + assert response.content == content[offset:] + + def test_set_cookie(): async def app(scope, receive, send): response = Response("Hello, world!", media_type="text/plain") From 8d40b80ead0a0d88515964f7b2561a5e1513b868 Mon Sep 17 00:00:00 2001 From: Kojiro20 <74428472+Kojiro20@users.noreply.github.com> Date: Fri, 13 Nov 2020 12:34:14 -0800 Subject: [PATCH 3/6] Fix quotes to align with linter --- starlette/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starlette/responses.py b/starlette/responses.py index 0c933112f..54ceff05c 100644 --- a/starlette/responses.py +++ b/starlette/responses.py @@ -300,7 +300,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if self.send_header_only: await send({"type": "http.response.body", "body": b"", "more_body": False}) else: - async with aiofiles.threadpool.open(self.path, mode='rb') as file: + async with aiofiles.threadpool.open(self.path, mode="rb") as file: await file.seek(self.offset) more_body = True while more_body: From dc0223db1bc01c79b6738929f7df17d929baf640 Mon Sep 17 00:00:00 2001 From: Kojiro20 <74428472+Kojiro20@users.noreply.github.com> Date: Fri, 13 Nov 2020 12:37:29 -0800 Subject: [PATCH 4/6] Fix spacing to align with linter --- tests/test_responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_responses.py b/tests/test_responses.py index 262dc7a05..8487f8cb7 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -250,7 +250,7 @@ def test_file_response_with_offset(tmpdir): path = os.path.join(tmpdir, filename) with open(path, "wb") as f: f.write(content) - for offset in range(0,24,4): # skip blocks of 4 + for offset in range(0, 24, 4): # skip blocks of 4 app = FileResponse(path=path, offset=offset) client = TestClient(app) response = client.get("/") From 07b13bc94686c2d57093bcb9ea4b801f717b4dc9 Mon Sep 17 00:00:00 2001 From: Kojiro20 <74428472+Kojiro20@users.noreply.github.com> Date: Fri, 13 Nov 2020 12:39:46 -0800 Subject: [PATCH 5/6] Fix linter issues --- starlette/responses.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/starlette/responses.py b/starlette/responses.py index 54ceff05c..bf2958983 100644 --- a/starlette/responses.py +++ b/starlette/responses.py @@ -235,7 +235,7 @@ class FileResponse(Response): def __init__( self, - path: str, + path: typing.Union[str, "os.PathLike[str]"], status_code: int = 200, headers: dict = None, media_type: str = None, @@ -243,7 +243,7 @@ def __init__( filename: str = None, stat_result: os.stat_result = None, method: str = None, - offset: int = 0 + offset: int = 0, ) -> None: assert aiofiles is not None, "'aiofiles' must be installed to use FileResponse" self.path = path @@ -300,7 +300,10 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if self.send_header_only: await send({"type": "http.response.body", "body": b"", "more_body": False}) else: - async with aiofiles.threadpool.open(self.path, mode="rb") as file: + # Tentatively ignoring type checking failure to work around the wrong type + # definitions for aiofile that come with typeshed. See + # https://github.com/python/typeshed/pull/4650 + async with aiofiles.threadpool.open(self.path, mode="rb") as file: # type: ignore await file.seek(self.offset) more_body = True while more_body: From e1de51db8d4b0875849940c77136946c60a1b752 Mon Sep 17 00:00:00 2001 From: Kojiro20 <74428472+Kojiro20@users.noreply.github.com> Date: Fri, 13 Nov 2020 12:43:26 -0800 Subject: [PATCH 6/6] Align new aiofiles import to convention --- starlette/responses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/starlette/responses.py b/starlette/responses.py index bf2958983..e3696ca3a 100644 --- a/starlette/responses.py +++ b/starlette/responses.py @@ -20,6 +20,7 @@ try: import aiofiles from aiofiles.os import stat as aio_stat + from aiofiles.threadpool import open as aio_open except ImportError: # pragma: nocover aiofiles = None # type: ignore aio_stat = None # type: ignore @@ -303,7 +304,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # Tentatively ignoring type checking failure to work around the wrong type # definitions for aiofile that come with typeshed. See # https://github.com/python/typeshed/pull/4650 - async with aiofiles.threadpool.open(self.path, mode="rb") as file: # type: ignore + async with aio_open(self.path, mode="rb") as file: # type: ignore await file.seek(self.offset) more_body = True while more_body: