diff options
Diffstat (limited to 'venv/lib/python3.11/site-packages/litestar/static_files/base.py')
-rw-r--r-- | venv/lib/python3.11/site-packages/litestar/static_files/base.py | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/litestar/static_files/base.py b/venv/lib/python3.11/site-packages/litestar/static_files/base.py new file mode 100644 index 0000000..9827697 --- /dev/null +++ b/venv/lib/python3.11/site-packages/litestar/static_files/base.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from os.path import commonpath +from pathlib import Path +from typing import TYPE_CHECKING, Literal, Sequence + +from litestar.enums import ScopeType +from litestar.exceptions import MethodNotAllowedException, NotFoundException +from litestar.file_system import FileSystemAdapter +from litestar.response.file import ASGIFileResponse +from litestar.status_codes import HTTP_404_NOT_FOUND + +__all__ = ("StaticFiles",) + + +if TYPE_CHECKING: + from litestar.types import Receive, Scope, Send + from litestar.types.composite_types import PathType + from litestar.types.file_types import FileInfo, FileSystemProtocol + + +class StaticFiles: + """ASGI App that handles file sending.""" + + __slots__ = ("is_html_mode", "directories", "adapter", "send_as_attachment", "headers") + + def __init__( + self, + is_html_mode: bool, + directories: Sequence[PathType], + file_system: FileSystemProtocol, + send_as_attachment: bool = False, + resolve_symlinks: bool = True, + headers: dict[str, str] | None = None, + ) -> None: + """Initialize the Application. + + Args: + is_html_mode: Flag dictating whether serving html. If true, the default file will be ``index.html``. + directories: A list of directories to serve files from. + file_system: The file_system spec to use for serving files. + send_as_attachment: Whether to send the file with a ``content-disposition`` header of + ``attachment`` or ``inline`` + resolve_symlinks: Resolve symlinks to the directories + headers: Headers that will be sent with every response. + """ + self.adapter = FileSystemAdapter(file_system) + self.directories = tuple(Path(p).resolve() if resolve_symlinks else Path(p) for p in directories) + self.is_html_mode = is_html_mode + self.send_as_attachment = send_as_attachment + self.headers = headers + + async def get_fs_info( + self, directories: Sequence[PathType], file_path: PathType + ) -> tuple[Path, FileInfo] | tuple[None, None]: + """Return the resolved path and a :class:`stat_result <os.stat_result>`. + + Args: + directories: A list of directory paths. + file_path: A file path to resolve + + Returns: + A tuple with an optional resolved :class:`Path <anyio.Path>` instance and an optional + :class:`stat_result <os.stat_result>`. + """ + for directory in directories: + try: + joined_path = Path(directory, file_path) + file_info = await self.adapter.info(joined_path) + if file_info and commonpath([str(directory), file_info["name"], joined_path]) == str(directory): + return joined_path, file_info + except FileNotFoundError: + continue + return None, None + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """ASGI callable. + + Args: + scope: ASGI scope + receive: ASGI ``receive`` callable + send: ASGI ``send`` callable + + Returns: + None + """ + if scope["type"] != ScopeType.HTTP or scope["method"] not in {"GET", "HEAD"}: + raise MethodNotAllowedException() + + res = await self.handle(path=scope["path"], is_head_response=scope["method"] == "HEAD") + await res(scope=scope, receive=receive, send=send) + + async def handle(self, path: str, is_head_response: bool) -> ASGIFileResponse: + split_path = path.split("/") + filename = split_path[-1] + joined_path = Path(*split_path) + resolved_path, fs_info = await self.get_fs_info(directories=self.directories, file_path=joined_path) + content_disposition_type: Literal["inline", "attachment"] = ( + "attachment" if self.send_as_attachment else "inline" + ) + + if self.is_html_mode and fs_info and fs_info["type"] == "directory": + filename = "index.html" + resolved_path, fs_info = await self.get_fs_info( + directories=self.directories, + file_path=Path(resolved_path or joined_path) / filename, + ) + + if fs_info and fs_info["type"] == "file": + return ASGIFileResponse( + file_path=resolved_path or joined_path, + file_info=fs_info, + file_system=self.adapter.file_system, + filename=filename, + content_disposition_type=content_disposition_type, + is_head_response=is_head_response, + headers=self.headers, + ) + + if self.is_html_mode: + # for some reason coverage doesn't catch these two lines + filename = "404.html" # pragma: no cover + resolved_path, fs_info = await self.get_fs_info( # pragma: no cover + directories=self.directories, file_path=filename + ) + + if fs_info and fs_info["type"] == "file": + return ASGIFileResponse( + file_path=resolved_path or joined_path, + file_info=fs_info, + file_system=self.adapter.file_system, + filename=filename, + status_code=HTTP_404_NOT_FOUND, + content_disposition_type=content_disposition_type, + is_head_response=is_head_response, + headers=self.headers, + ) + + raise NotFoundException( + f"no file or directory match the path {resolved_path or joined_path} was found" + ) # pragma: no cover |