diff --git a/starlette/responses.py b/starlette/responses.py index a31758d65..f2294437c 100644 --- a/starlette/responses.py +++ b/starlette/responses.py @@ -319,6 +319,7 @@ def set_stat_headers(self, stat_result: os.stat_result) -> None: self.headers.setdefault("etag", etag) async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + send_header_only: bool = scope["method"].upper() == "HEAD" if self.stat_result is None: try: stat_result = await anyio.to_thread.run_sync(os.stat, self.path) @@ -336,9 +337,25 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: http_range = headers.get("range") # http_if_range = headers.get("if-range") - ranges = self._parse_range_header(http_range, stat_result.st_size) - print(ranges) + if http_range is None: + await self._handle_simple(send, send_header_only) + else: + ranges = self._parse_range_header(http_range, stat_result.st_size) + + if len(ranges) == 1: + start, end = ranges[0] + await self._handle_single_range( + send, start, end, stat_result.st_size, send_header_only + ) + else: + await self._handle_multiple_ranges( + send, ranges, stat_result.st_size, send_header_only + ) + if self.background is not None: + await self.background() + + async def _handle_simple(self, send: Send, send_header_only: bool) -> None: await send( { "type": "http.response.start", @@ -346,7 +363,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: "headers": self.raw_headers, } ) - if scope["method"].upper() == "HEAD": + if send_header_only: await send({"type": "http.response.body", "body": b"", "more_body": False}) else: async with await anyio.open_file(self.path, mode="rb") as file: @@ -361,8 +378,103 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: "more_body": more_body, } ) - if self.background is not None: - await self.background() + + async def _handle_single_range( + self, send: Send, start: int, end: int, file_size: int, send_header_only: bool + ) -> None: + self.headers["content-range"] = f"bytes {start}-{end - 1}/{file_size}" + await send( + { + "type": "http.response.start", + "status": 206, + "headers": self.raw_headers, + } + ) + if send_header_only: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + else: + async with await anyio.open_file(self.path, mode="rb") as file: + await file.seek(start) + more_body = True + while more_body: + chunk = await file.read(min(self.chunk_size, end - start)) + start += len(chunk) + more_body = len(chunk) == self.chunk_size and start < end + await send( + { + "type": "http.response.body", + "body": chunk, + "more_body": more_body, + } + ) + + async def _handle_multiple_ranges( + self, + send: Send, + ranges: typing.List[typing.Tuple[int, int]], + file_size: int, + send_header_only: bool, + ) -> None: + boundary = md5_hexdigest(os.urandom(16), usedforsecurity=False) + content_type = f"multipart/byteranges; boundary={boundary}" + self.headers["content-type"] = content_type + await send( + { + "type": "http.response.start", + "status": 206, + "headers": self.raw_headers, + } + ) + if send_header_only: + await send({"type": "http.response.body", "body": b"", "more_body": False}) + else: + async with await anyio.open_file(self.path, mode="rb") as file: + for start, end in ranges: + await send( + { + "type": "http.response.body", + "body": b"\r\n--" + boundary.encode("ascii") + b"\r\n", + "more_body": True, + } + ) + await send( + { + "type": "http.response.body", + "body": f"Content-Type: {self.media_type}\r\n".encode( + "utf-8" + ), + "more_body": True, + } + ) + await send( + { + "type": "http.response.body", + "body": f"Content-Range: bytes {start}-{end - 1}/{file_size}\r\n\r\n".encode( + "utf-8" + ), + "more_body": True, + } + ) + await file.seek(start) + more_body = True + while more_body: + chunk = await file.read(min(self.chunk_size, end - start)) + start += len(chunk) + more_body = len(chunk) == self.chunk_size and start < end + await send( + { + "type": "http.response.body", + "body": chunk, + "more_body": more_body, + } + ) + await send( + { + "type": "http.response.body", + "body": b"\r\n--" + boundary.encode("ascii") + b"--\r\n", + "more_body": False, + } + ) def _parse_range_header( self, http_range: typing.Optional[str], file_size: int