We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
StaticFiles support for directories other than "statics"
### Checklist

<!-- Please make sure you check all these items before submitting your feature request. -->

- [/] There are no similar issues or pull requests for this yet.
- [X - tried to get feedback but no replies] I discussed this idea on the [community chat](https://gitter.im/encode/community) and feedback is positive.

### Is your feature related to a problem? Please describe.

I want to be able to serve static files from arbitrary locations within a python package but I can't because currently it looks in a `statics` folder which is hardcoded.

## Describe the solution you would like.

I'd like to be able to pass as a parameter the path within the python package to look in for the static files.

## Describe alternatives you considered

<!-- Please describe any alternative solutions or features you've considered to solve
your problem and why they wouldn't solve it. -->

I've considered changing the location of my packaged files to `statics` but this will have knock on effects across other systems, and `statics` itself is non-standard as far as I can tell.

## Additional context

<!-- Provide any additional context, screenshots, tracebacks, etc. about the feature here. -->


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `starlette/staticfiles.py`
Content:
```
1 import importlib.util
2 import os
3 import stat
4 import typing
5 from email.utils import parsedate
6 
7 import anyio
8 
9 from starlette.datastructures import URL, Headers
10 from starlette.exceptions import HTTPException
11 from starlette.responses import FileResponse, RedirectResponse, Response
12 from starlette.types import Receive, Scope, Send
13 
14 PathLike = typing.Union[str, "os.PathLike[str]"]
15 
16 
17 class NotModifiedResponse(Response):
18     NOT_MODIFIED_HEADERS = (
19         "cache-control",
20         "content-location",
21         "date",
22         "etag",
23         "expires",
24         "vary",
25     )
26 
27     def __init__(self, headers: Headers):
28         super().__init__(
29             status_code=304,
30             headers={
31                 name: value
32                 for name, value in headers.items()
33                 if name in self.NOT_MODIFIED_HEADERS
34             },
35         )
36 
37 
38 class StaticFiles:
39     def __init__(
40         self,
41         *,
42         directory: PathLike = None,
43         packages: typing.List[str] = None,
44         html: bool = False,
45         check_dir: bool = True,
46     ) -> None:
47         self.directory = directory
48         self.packages = packages
49         self.all_directories = self.get_directories(directory, packages)
50         self.html = html
51         self.config_checked = False
52         if check_dir and directory is not None and not os.path.isdir(directory):
53             raise RuntimeError(f"Directory '{directory}' does not exist")
54 
55     def get_directories(
56         self, directory: PathLike = None, packages: typing.List[str] = None
57     ) -> typing.List[PathLike]:
58         """
59         Given `directory` and `packages` arguments, return a list of all the
60         directories that should be used for serving static files from.
61         """
62         directories = []
63         if directory is not None:
64             directories.append(directory)
65 
66         for package in packages or []:
67             spec = importlib.util.find_spec(package)
68             assert spec is not None, f"Package {package!r} could not be found."
69             assert (
70                 spec.origin is not None
71             ), f"Directory 'statics' in package {package!r} could not be found."
72             package_directory = os.path.normpath(
73                 os.path.join(spec.origin, "..", "statics")
74             )
75             assert os.path.isdir(
76                 package_directory
77             ), f"Directory 'statics' in package {package!r} could not be found."
78             directories.append(package_directory)
79 
80         return directories
81 
82     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
83         """
84         The ASGI entry point.
85         """
86         assert scope["type"] == "http"
87 
88         if not self.config_checked:
89             await self.check_config()
90             self.config_checked = True
91 
92         path = self.get_path(scope)
93         response = await self.get_response(path, scope)
94         await response(scope, receive, send)
95 
96     def get_path(self, scope: Scope) -> str:
97         """
98         Given the ASGI scope, return the `path` string to serve up,
99         with OS specific path seperators, and any '..', '.' components removed.
100         """
101         return os.path.normpath(os.path.join(*scope["path"].split("/")))
102 
103     async def get_response(self, path: str, scope: Scope) -> Response:
104         """
105         Returns an HTTP response, given the incoming path, method and request headers.
106         """
107         if scope["method"] not in ("GET", "HEAD"):
108             raise HTTPException(status_code=405)
109 
110         try:
111             full_path, stat_result = await anyio.to_thread.run_sync(
112                 self.lookup_path, path
113             )
114         except PermissionError:
115             raise HTTPException(status_code=401)
116         except OSError:
117             raise
118 
119         if stat_result and stat.S_ISREG(stat_result.st_mode):
120             # We have a static file to serve.
121             return self.file_response(full_path, stat_result, scope)
122 
123         elif stat_result and stat.S_ISDIR(stat_result.st_mode) and self.html:
124             # We're in HTML mode, and have got a directory URL.
125             # Check if we have 'index.html' file to serve.
126             index_path = os.path.join(path, "index.html")
127             full_path, stat_result = await anyio.to_thread.run_sync(
128                 self.lookup_path, index_path
129             )
130             if stat_result is not None and stat.S_ISREG(stat_result.st_mode):
131                 if not scope["path"].endswith("/"):
132                     # Directory URLs should redirect to always end in "/".
133                     url = URL(scope=scope)
134                     url = url.replace(path=url.path + "/")
135                     return RedirectResponse(url=url)
136                 return self.file_response(full_path, stat_result, scope)
137 
138         if self.html:
139             # Check for '404.html' if we're in HTML mode.
140             full_path, stat_result = await anyio.to_thread.run_sync(
141                 self.lookup_path, "404.html"
142             )
143             if stat_result and stat.S_ISREG(stat_result.st_mode):
144                 return FileResponse(
145                     full_path,
146                     stat_result=stat_result,
147                     method=scope["method"],
148                     status_code=404,
149                 )
150         raise HTTPException(status_code=404)
151 
152     def lookup_path(
153         self, path: str
154     ) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
155         for directory in self.all_directories:
156             full_path = os.path.realpath(os.path.join(directory, path))
157             directory = os.path.realpath(directory)
158             if os.path.commonprefix([full_path, directory]) != directory:
159                 # Don't allow misbehaving clients to break out of the static files
160                 # directory.
161                 continue
162             try:
163                 return full_path, os.stat(full_path)
164             except (FileNotFoundError, NotADirectoryError):
165                 continue
166         return "", None
167 
168     def file_response(
169         self,
170         full_path: PathLike,
171         stat_result: os.stat_result,
172         scope: Scope,
173         status_code: int = 200,
174     ) -> Response:
175         method = scope["method"]
176         request_headers = Headers(scope=scope)
177 
178         response = FileResponse(
179             full_path, status_code=status_code, stat_result=stat_result, method=method
180         )
181         if self.is_not_modified(response.headers, request_headers):
182             return NotModifiedResponse(response.headers)
183         return response
184 
185     async def check_config(self) -> None:
186         """
187         Perform a one-off configuration check that StaticFiles is actually
188         pointed at a directory, so that we can raise loud errors rather than
189         just returning 404 responses.
190         """
191         if self.directory is None:
192             return
193 
194         try:
195             stat_result = await anyio.to_thread.run_sync(os.stat, self.directory)
196         except FileNotFoundError:
197             raise RuntimeError(
198                 f"StaticFiles directory '{self.directory}' does not exist."
199             )
200         if not (stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)):
201             raise RuntimeError(
202                 f"StaticFiles path '{self.directory}' is not a directory."
203             )
204 
205     def is_not_modified(
206         self, response_headers: Headers, request_headers: Headers
207     ) -> bool:
208         """
209         Given the request and response headers, return `True` if an HTTP
210         "Not Modified" response could be returned instead.
211         """
212         try:
213             if_none_match = request_headers["if-none-match"]
214             etag = response_headers["etag"]
215             if if_none_match == etag:
216                 return True
217         except KeyError:
218             pass
219 
220         try:
221             if_modified_since = parsedate(request_headers["if-modified-since"])
222             last_modified = parsedate(response_headers["last-modified"])
223             if (
224                 if_modified_since is not None
225                 and last_modified is not None
226                 and if_modified_since >= last_modified
227             ):
228                 return True
229         except KeyError:
230             pass
231 
232         return False
233
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/starlette/staticfiles.py b/starlette/staticfiles.py
--- a/starlette/staticfiles.py
+++ b/starlette/staticfiles.py
@@ -40,7 +40,7 @@
         self,
         *,
         directory: PathLike = None,
-        packages: typing.List[str] = None,
+        packages: typing.List[typing.Union[str, typing.Tuple[str, str]]] = None,
         html: bool = False,
         check_dir: bool = True,
     ) -> None:
@@ -53,7 +53,9 @@
             raise RuntimeError(f"Directory '{directory}' does not exist")
 
     def get_directories(
-        self, directory: PathLike = None, packages: typing.List[str] = None
+        self,
+        directory: PathLike = None,
+        packages: typing.List[typing.Union[str, typing.Tuple[str, str]]] = None,
     ) -> typing.List[PathLike]:
         """
         Given `directory` and `packages` arguments, return a list of all the
@@ -64,17 +66,19 @@
             directories.append(directory)
 
         for package in packages or []:
+            if isinstance(package, tuple):
+                package, statics_dir = package
+            else:
+                statics_dir = "statics"
             spec = importlib.util.find_spec(package)
             assert spec is not None, f"Package {package!r} could not be found."
-            assert (
-                spec.origin is not None
-            ), f"Directory 'statics' in package {package!r} could not be found."
+            assert spec.origin is not None, f"Package {package!r} could not be found."
             package_directory = os.path.normpath(
-                os.path.join(spec.origin, "..", "statics")
+                os.path.join(spec.origin, "..", statics_dir)
             )
             assert os.path.isdir(
                 package_directory
-            ), f"Directory 'statics' in package {package!r} could not be found."
+            ), f"Directory '{statics_dir!r}' in package {package!r} could not be found."
             directories.append(package_directory)
 
         return directories

{"golden_diff": "diff --git a/starlette/staticfiles.py b/starlette/staticfiles.py\n--- a/starlette/staticfiles.py\n+++ b/starlette/staticfiles.py\n@@ -40,7 +40,7 @@\n         self,\n         *,\n         directory: PathLike = None,\n-        packages: typing.List[str] = None,\n+        packages: typing.List[typing.Union[str, typing.Tuple[str, str]]] = None,\n         html: bool = False,\n         check_dir: bool = True,\n     ) -> None:\n@@ -53,7 +53,9 @@\n             raise RuntimeError(f\"Directory '{directory}' does not exist\")\n \n     def get_directories(\n-        self, directory: PathLike = None, packages: typing.List[str] = None\n+        self,\n+        directory: PathLike = None,\n+        packages: typing.List[typing.Union[str, typing.Tuple[str, str]]] = None,\n     ) -> typing.List[PathLike]:\n         \"\"\"\n         Given `directory` and `packages` arguments, return a list of all the\n@@ -64,17 +66,19 @@\n             directories.append(directory)\n \n         for package in packages or []:\n+            if isinstance(package, tuple):\n+                package, statics_dir = package\n+            else:\n+                statics_dir = \"statics\"\n             spec = importlib.util.find_spec(package)\n             assert spec is not None, f\"Package {package!r} could not be found.\"\n-            assert (\n-                spec.origin is not None\n-            ), f\"Directory 'statics' in package {package!r} could not be found.\"\n+            assert spec.origin is not None, f\"Package {package!r} could not be found.\"\n             package_directory = os.path.normpath(\n-                os.path.join(spec.origin, \"..\", \"statics\")\n+                os.path.join(spec.origin, \"..\", statics_dir)\n             )\n             assert os.path.isdir(\n                 package_directory\n-            ), f\"Directory 'statics' in package {package!r} could not be found.\"\n+            ), f\"Directory '{statics_dir!r}' in package {package!r} could not be found.\"\n             directories.append(package_directory)\n \n         return directories\n", "issue": "StaticFiles support for directories other than \"statics\"\n### Checklist\r\n\r\n<!-- Please make sure you check all these items before submitting your feature request. -->\r\n\r\n- [/] There are no similar issues or pull requests for this yet.\r\n- [X - tried to get feedback but no replies] I discussed this idea on the [community chat](https://gitter.im/encode/community) and feedback is positive.\r\n\r\n### Is your feature related to a problem? Please describe.\r\n\r\nI want to be able to serve static files from arbitrary locations within a python package but I can't because currently it looks in a `statics` folder which is hardcoded.\r\n\r\n## Describe the solution you would like.\r\n\r\nI'd like to be able to pass as a parameter the path within the python package to look in for the static files.\r\n\r\n## Describe alternatives you considered\r\n\r\n<!-- Please describe any alternative solutions or features you've considered to solve\r\nyour problem and why they wouldn't solve it. -->\r\n\r\nI've considered changing the location of my packaged files to `statics` but this will have knock on effects across other systems, and `statics` itself is non-standard as far as I can tell.\r\n\r\n## Additional context\r\n\r\n<!-- Provide any additional context, screenshots, tracebacks, etc. about the feature here. -->\r\n\n", "before_files": [{"content": "import importlib.util\nimport os\nimport stat\nimport typing\nfrom email.utils import parsedate\n\nimport anyio\n\nfrom starlette.datastructures import URL, Headers\nfrom starlette.exceptions import HTTPException\nfrom starlette.responses import FileResponse, RedirectResponse, Response\nfrom starlette.types import Receive, Scope, Send\n\nPathLike = typing.Union[str, \"os.PathLike[str]\"]\n\n\nclass NotModifiedResponse(Response):\n    NOT_MODIFIED_HEADERS = (\n        \"cache-control\",\n        \"content-location\",\n        \"date\",\n        \"etag\",\n        \"expires\",\n        \"vary\",\n    )\n\n    def __init__(self, headers: Headers):\n        super().__init__(\n            status_code=304,\n            headers={\n                name: value\n                for name, value in headers.items()\n                if name in self.NOT_MODIFIED_HEADERS\n            },\n        )\n\n\nclass StaticFiles:\n    def __init__(\n        self,\n        *,\n        directory: PathLike = None,\n        packages: typing.List[str] = None,\n        html: bool = False,\n        check_dir: bool = True,\n    ) -> None:\n        self.directory = directory\n        self.packages = packages\n        self.all_directories = self.get_directories(directory, packages)\n        self.html = html\n        self.config_checked = False\n        if check_dir and directory is not None and not os.path.isdir(directory):\n            raise RuntimeError(f\"Directory '{directory}' does not exist\")\n\n    def get_directories(\n        self, directory: PathLike = None, packages: typing.List[str] = None\n    ) -> typing.List[PathLike]:\n        \"\"\"\n        Given `directory` and `packages` arguments, return a list of all the\n        directories that should be used for serving static files from.\n        \"\"\"\n        directories = []\n        if directory is not None:\n            directories.append(directory)\n\n        for package in packages or []:\n            spec = importlib.util.find_spec(package)\n            assert spec is not None, f\"Package {package!r} could not be found.\"\n            assert (\n                spec.origin is not None\n            ), f\"Directory 'statics' in package {package!r} could not be found.\"\n            package_directory = os.path.normpath(\n                os.path.join(spec.origin, \"..\", \"statics\")\n            )\n            assert os.path.isdir(\n                package_directory\n            ), f\"Directory 'statics' in package {package!r} could not be found.\"\n            directories.append(package_directory)\n\n        return directories\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        \"\"\"\n        The ASGI entry point.\n        \"\"\"\n        assert scope[\"type\"] == \"http\"\n\n        if not self.config_checked:\n            await self.check_config()\n            self.config_checked = True\n\n        path = self.get_path(scope)\n        response = await self.get_response(path, scope)\n        await response(scope, receive, send)\n\n    def get_path(self, scope: Scope) -> str:\n        \"\"\"\n        Given the ASGI scope, return the `path` string to serve up,\n        with OS specific path seperators, and any '..', '.' components removed.\n        \"\"\"\n        return os.path.normpath(os.path.join(*scope[\"path\"].split(\"/\")))\n\n    async def get_response(self, path: str, scope: Scope) -> Response:\n        \"\"\"\n        Returns an HTTP response, given the incoming path, method and request headers.\n        \"\"\"\n        if scope[\"method\"] not in (\"GET\", \"HEAD\"):\n            raise HTTPException(status_code=405)\n\n        try:\n            full_path, stat_result = await anyio.to_thread.run_sync(\n                self.lookup_path, path\n            )\n        except PermissionError:\n            raise HTTPException(status_code=401)\n        except OSError:\n            raise\n\n        if stat_result and stat.S_ISREG(stat_result.st_mode):\n            # We have a static file to serve.\n            return self.file_response(full_path, stat_result, scope)\n\n        elif stat_result and stat.S_ISDIR(stat_result.st_mode) and self.html:\n            # We're in HTML mode, and have got a directory URL.\n            # Check if we have 'index.html' file to serve.\n            index_path = os.path.join(path, \"index.html\")\n            full_path, stat_result = await anyio.to_thread.run_sync(\n                self.lookup_path, index_path\n            )\n            if stat_result is not None and stat.S_ISREG(stat_result.st_mode):\n                if not scope[\"path\"].endswith(\"/\"):\n                    # Directory URLs should redirect to always end in \"/\".\n                    url = URL(scope=scope)\n                    url = url.replace(path=url.path + \"/\")\n                    return RedirectResponse(url=url)\n                return self.file_response(full_path, stat_result, scope)\n\n        if self.html:\n            # Check for '404.html' if we're in HTML mode.\n            full_path, stat_result = await anyio.to_thread.run_sync(\n                self.lookup_path, \"404.html\"\n            )\n            if stat_result and stat.S_ISREG(stat_result.st_mode):\n                return FileResponse(\n                    full_path,\n                    stat_result=stat_result,\n                    method=scope[\"method\"],\n                    status_code=404,\n                )\n        raise HTTPException(status_code=404)\n\n    def lookup_path(\n        self, path: str\n    ) -> typing.Tuple[str, typing.Optional[os.stat_result]]:\n        for directory in self.all_directories:\n            full_path = os.path.realpath(os.path.join(directory, path))\n            directory = os.path.realpath(directory)\n            if os.path.commonprefix([full_path, directory]) != directory:\n                # Don't allow misbehaving clients to break out of the static files\n                # directory.\n                continue\n            try:\n                return full_path, os.stat(full_path)\n            except (FileNotFoundError, NotADirectoryError):\n                continue\n        return \"\", None\n\n    def file_response(\n        self,\n        full_path: PathLike,\n        stat_result: os.stat_result,\n        scope: Scope,\n        status_code: int = 200,\n    ) -> Response:\n        method = scope[\"method\"]\n        request_headers = Headers(scope=scope)\n\n        response = FileResponse(\n            full_path, status_code=status_code, stat_result=stat_result, method=method\n        )\n        if self.is_not_modified(response.headers, request_headers):\n            return NotModifiedResponse(response.headers)\n        return response\n\n    async def check_config(self) -> None:\n        \"\"\"\n        Perform a one-off configuration check that StaticFiles is actually\n        pointed at a directory, so that we can raise loud errors rather than\n        just returning 404 responses.\n        \"\"\"\n        if self.directory is None:\n            return\n\n        try:\n            stat_result = await anyio.to_thread.run_sync(os.stat, self.directory)\n        except FileNotFoundError:\n            raise RuntimeError(\n                f\"StaticFiles directory '{self.directory}' does not exist.\"\n            )\n        if not (stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)):\n            raise RuntimeError(\n                f\"StaticFiles path '{self.directory}' is not a directory.\"\n            )\n\n    def is_not_modified(\n        self, response_headers: Headers, request_headers: Headers\n    ) -> bool:\n        \"\"\"\n        Given the request and response headers, return `True` if an HTTP\n        \"Not Modified\" response could be returned instead.\n        \"\"\"\n        try:\n            if_none_match = request_headers[\"if-none-match\"]\n            etag = response_headers[\"etag\"]\n            if if_none_match == etag:\n                return True\n        except KeyError:\n            pass\n\n        try:\n            if_modified_since = parsedate(request_headers[\"if-modified-since\"])\n            last_modified = parsedate(response_headers[\"last-modified\"])\n            if (\n                if_modified_since is not None\n                and last_modified is not None\n                and if_modified_since >= last_modified\n            ):\n                return True\n        except KeyError:\n            pass\n\n        return False\n", "path": "starlette/staticfiles.py"}], "after_files": [{"content": "import importlib.util\nimport os\nimport stat\nimport typing\nfrom email.utils import parsedate\n\nimport anyio\n\nfrom starlette.datastructures import URL, Headers\nfrom starlette.exceptions import HTTPException\nfrom starlette.responses import FileResponse, RedirectResponse, Response\nfrom starlette.types import Receive, Scope, Send\n\nPathLike = typing.Union[str, \"os.PathLike[str]\"]\n\n\nclass NotModifiedResponse(Response):\n    NOT_MODIFIED_HEADERS = (\n        \"cache-control\",\n        \"content-location\",\n        \"date\",\n        \"etag\",\n        \"expires\",\n        \"vary\",\n    )\n\n    def __init__(self, headers: Headers):\n        super().__init__(\n            status_code=304,\n            headers={\n                name: value\n                for name, value in headers.items()\n                if name in self.NOT_MODIFIED_HEADERS\n            },\n        )\n\n\nclass StaticFiles:\n    def __init__(\n        self,\n        *,\n        directory: PathLike = None,\n        packages: typing.List[typing.Union[str, typing.Tuple[str, str]]] = None,\n        html: bool = False,\n        check_dir: bool = True,\n    ) -> None:\n        self.directory = directory\n        self.packages = packages\n        self.all_directories = self.get_directories(directory, packages)\n        self.html = html\n        self.config_checked = False\n        if check_dir and directory is not None and not os.path.isdir(directory):\n            raise RuntimeError(f\"Directory '{directory}' does not exist\")\n\n    def get_directories(\n        self,\n        directory: PathLike = None,\n        packages: typing.List[typing.Union[str, typing.Tuple[str, str]]] = None,\n    ) -> typing.List[PathLike]:\n        \"\"\"\n        Given `directory` and `packages` arguments, return a list of all the\n        directories that should be used for serving static files from.\n        \"\"\"\n        directories = []\n        if directory is not None:\n            directories.append(directory)\n\n        for package in packages or []:\n            if isinstance(package, tuple):\n                package, statics_dir = package\n            else:\n                statics_dir = \"statics\"\n            spec = importlib.util.find_spec(package)\n            assert spec is not None, f\"Package {package!r} could not be found.\"\n            assert spec.origin is not None, f\"Package {package!r} could not be found.\"\n            package_directory = os.path.normpath(\n                os.path.join(spec.origin, \"..\", statics_dir)\n            )\n            assert os.path.isdir(\n                package_directory\n            ), f\"Directory '{statics_dir!r}' in package {package!r} could not be found.\"\n            directories.append(package_directory)\n\n        return directories\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        \"\"\"\n        The ASGI entry point.\n        \"\"\"\n        assert scope[\"type\"] == \"http\"\n\n        if not self.config_checked:\n            await self.check_config()\n            self.config_checked = True\n\n        path = self.get_path(scope)\n        response = await self.get_response(path, scope)\n        await response(scope, receive, send)\n\n    def get_path(self, scope: Scope) -> str:\n        \"\"\"\n        Given the ASGI scope, return the `path` string to serve up,\n        with OS specific path seperators, and any '..', '.' components removed.\n        \"\"\"\n        return os.path.normpath(os.path.join(*scope[\"path\"].split(\"/\")))\n\n    async def get_response(self, path: str, scope: Scope) -> Response:\n        \"\"\"\n        Returns an HTTP response, given the incoming path, method and request headers.\n        \"\"\"\n        if scope[\"method\"] not in (\"GET\", \"HEAD\"):\n            raise HTTPException(status_code=405)\n\n        try:\n            full_path, stat_result = await anyio.to_thread.run_sync(\n                self.lookup_path, path\n            )\n        except PermissionError:\n            raise HTTPException(status_code=401)\n        except OSError:\n            raise\n\n        if stat_result and stat.S_ISREG(stat_result.st_mode):\n            # We have a static file to serve.\n            return self.file_response(full_path, stat_result, scope)\n\n        elif stat_result and stat.S_ISDIR(stat_result.st_mode) and self.html:\n            # We're in HTML mode, and have got a directory URL.\n            # Check if we have 'index.html' file to serve.\n            index_path = os.path.join(path, \"index.html\")\n            full_path, stat_result = await anyio.to_thread.run_sync(\n                self.lookup_path, index_path\n            )\n            if stat_result is not None and stat.S_ISREG(stat_result.st_mode):\n                if not scope[\"path\"].endswith(\"/\"):\n                    # Directory URLs should redirect to always end in \"/\".\n                    url = URL(scope=scope)\n                    url = url.replace(path=url.path + \"/\")\n                    return RedirectResponse(url=url)\n                return self.file_response(full_path, stat_result, scope)\n\n        if self.html:\n            # Check for '404.html' if we're in HTML mode.\n            full_path, stat_result = await anyio.to_thread.run_sync(\n                self.lookup_path, \"404.html\"\n            )\n            if stat_result and stat.S_ISREG(stat_result.st_mode):\n                return FileResponse(\n                    full_path,\n                    stat_result=stat_result,\n                    method=scope[\"method\"],\n                    status_code=404,\n                )\n        raise HTTPException(status_code=404)\n\n    def lookup_path(\n        self, path: str\n    ) -> typing.Tuple[str, typing.Optional[os.stat_result]]:\n        for directory in self.all_directories:\n            full_path = os.path.realpath(os.path.join(directory, path))\n            directory = os.path.realpath(directory)\n            if os.path.commonprefix([full_path, directory]) != directory:\n                # Don't allow misbehaving clients to break out of the static files\n                # directory.\n                continue\n            try:\n                return full_path, os.stat(full_path)\n            except (FileNotFoundError, NotADirectoryError):\n                continue\n        return \"\", None\n\n    def file_response(\n        self,\n        full_path: PathLike,\n        stat_result: os.stat_result,\n        scope: Scope,\n        status_code: int = 200,\n    ) -> Response:\n        method = scope[\"method\"]\n        request_headers = Headers(scope=scope)\n\n        response = FileResponse(\n            full_path, status_code=status_code, stat_result=stat_result, method=method\n        )\n        if self.is_not_modified(response.headers, request_headers):\n            return NotModifiedResponse(response.headers)\n        return response\n\n    async def check_config(self) -> None:\n        \"\"\"\n        Perform a one-off configuration check that StaticFiles is actually\n        pointed at a directory, so that we can raise loud errors rather than\n        just returning 404 responses.\n        \"\"\"\n        if self.directory is None:\n            return\n\n        try:\n            stat_result = await anyio.to_thread.run_sync(os.stat, self.directory)\n        except FileNotFoundError:\n            raise RuntimeError(\n                f\"StaticFiles directory '{self.directory}' does not exist.\"\n            )\n        if not (stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)):\n            raise RuntimeError(\n                f\"StaticFiles path '{self.directory}' is not a directory.\"\n            )\n\n    def is_not_modified(\n        self, response_headers: Headers, request_headers: Headers\n    ) -> bool:\n        \"\"\"\n        Given the request and response headers, return `True` if an HTTP\n        \"Not Modified\" response could be returned instead.\n        \"\"\"\n        try:\n            if_none_match = request_headers[\"if-none-match\"]\n            etag = response_headers[\"etag\"]\n            if if_none_match == etag:\n                return True\n        except KeyError:\n            pass\n\n        try:\n            if_modified_since = parsedate(request_headers[\"if-modified-since\"])\n            last_modified = parsedate(response_headers[\"last-modified\"])\n            if (\n                if_modified_since is not None\n                and last_modified is not None\n                and if_modified_since >= last_modified\n            ):\n                return True\n        except KeyError:\n            pass\n\n        return False\n", "path": "starlette/staticfiles.py"}]}