summaryrefslogtreecommitdiff
path: root/venv/lib/python3.11/site-packages/litestar/static_files/base.py
blob: 98276979332e41c38390ff5b83b52266c24558ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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