We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
StaticFiles middleware doesn't follow symlinks
### Checklist

- [x] The bug is reproducible against the latest release and/or `master`.
- [x] There are no similar issues or pull requests to fix it yet.

### Describe the bug

The StaticFiles middleware is checking the `os.realpath` of a file and returning a 404 for symlinks that lead outside the static directory.

### To reproduce

1. create a minimal app with a staticfiles middleware
1. put a symlink in your static directory. the link's target must be above the static directory.
1. you'll get a 404

### Expected behavior

Support symlinks in static directory.

The use case for symlinks in static is to target frontend assets that are being generated in file-watch mode.

### Actual behavior

404.

### Debugging material

It's happening here:
https://github.com/encode/starlette/blob/b95acea973c20eea3e7cbbca42d09b1f5d4a3412/starlette/staticfiles.py#L147-L149

### Environment

- OS: linux
- Python version: 3.7.5
- Starlette version: 0.13.8

### Additional context

I'm happy to post a PR for this if useful, ideally adding a bool param to the StaticFiles middleware that allows symlinks.

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

{"golden_diff": "diff --git a/starlette/staticfiles.py b/starlette/staticfiles.py\n--- a/starlette/staticfiles.py\n+++ b/starlette/staticfiles.py\n@@ -3,6 +3,7 @@\n import stat\n import typing\n from email.utils import parsedate\n+from pathlib import Path\n \n import anyio\n \n@@ -51,7 +52,7 @@\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+        if check_dir and directory is not None and not Path(directory).is_dir():\n             raise RuntimeError(f\"Directory '{directory}' does not exist\")\n \n     def get_directories(\n@@ -77,11 +78,9 @@\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+            package_directory = Path(spec.origin).joinpath(\"..\", statics_dir).resolve()\n+            assert (\n+                package_directory.is_dir()\n             ), f\"Directory '{statics_dir!r}' in package {package!r} could not be found.\"\n             directories.append(package_directory)\n \n@@ -101,14 +100,14 @@\n         response = await self.get_response(path, scope)\n         await response(scope, receive, send)\n \n-    def get_path(self, scope: Scope) -> str:\n+    def get_path(self, scope: Scope) -> Path:\n         \"\"\"\n         Given the ASGI scope, return the `path` string to serve up,\n         with OS specific path separators, and any '..', '.' components removed.\n         \"\"\"\n-        return os.path.normpath(os.path.join(*scope[\"path\"].split(\"/\")))\n+        return Path(*scope[\"path\"].split(\"/\"))\n \n-    async def get_response(self, path: str, scope: Scope) -> Response:\n+    async def get_response(self, path: Path, scope: Scope) -> Response:\n         \"\"\"\n         Returns an HTTP response, given the incoming path, method and request headers.\n         \"\"\"\n@@ -131,7 +130,7 @@\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+            index_path = path.joinpath(\"index.html\")\n             full_path, stat_result = await anyio.to_thread.run_sync(\n                 self.lookup_path, index_path\n             )\n@@ -158,20 +157,25 @@\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+        self, path: Path\n+    ) -> typing.Tuple[Path, 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+            original_path = Path(directory).joinpath(path)\n+            full_path = original_path.resolve()\n+            directory = Path(directory).resolve()\n             try:\n-                return full_path, os.stat(full_path)\n+                stat_result = os.lstat(original_path)\n+                full_path.relative_to(directory)\n+                return full_path, stat_result\n+            except ValueError:\n+                # Allow clients to break out of the static files directory\n+                # if following symlinks.\n+                if stat.S_ISLNK(stat_result.st_mode):\n+                    stat_result = os.lstat(full_path)\n+                    return full_path, stat_result\n             except (FileNotFoundError, NotADirectoryError):\n                 continue\n-        return \"\", None\n+        return Path(), None\n \n     def file_response(\n         self,\n", "issue": "StaticFiles middleware doesn't follow symlinks\n### Checklist\r\n\r\n- [x] The bug is reproducible against the latest release and/or `master`.\r\n- [x] There are no similar issues or pull requests to fix it yet.\r\n\r\n### Describe the bug\r\n\r\nThe StaticFiles middleware is checking the `os.realpath` of a file and returning a 404 for symlinks that lead outside the static directory.\r\n\r\n### To reproduce\r\n\r\n1. create a minimal app with a staticfiles middleware\r\n1. put a symlink in your static directory. the link's target must be above the static directory.\r\n1. you'll get a 404\r\n\r\n### Expected behavior\r\n\r\nSupport symlinks in static directory.\r\n\r\nThe use case for symlinks in static is to target frontend assets that are being generated in file-watch mode.\r\n\r\n### Actual behavior\r\n\r\n404.\r\n\r\n### Debugging material\r\n\r\nIt's happening here:\r\nhttps://github.com/encode/starlette/blob/b95acea973c20eea3e7cbbca42d09b1f5d4a3412/starlette/staticfiles.py#L147-L149\r\n\r\n### Environment\r\n\r\n- OS: linux\r\n- Python version: 3.7.5\r\n- Starlette version: 0.13.8\r\n\r\n### Additional context\r\n\r\nI'm happy to post a PR for this if useful, ideally adding a bool param to the StaticFiles middleware that allows symlinks.\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: typing.Optional[PathLike] = None,\n        packages: typing.Optional[\n            typing.List[typing.Union[str, typing.Tuple[str, str]]]\n        ] = 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: typing.Optional[PathLike] = None,\n        packages: typing.Optional[\n            typing.List[typing.Union[str, typing.Tuple[str, str]]]\n        ] = 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 separators, 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\nfrom pathlib import Path\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: typing.Optional[PathLike] = None,\n        packages: typing.Optional[\n            typing.List[typing.Union[str, typing.Tuple[str, str]]]\n        ] = 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 Path(directory).is_dir():\n            raise RuntimeError(f\"Directory '{directory}' does not exist\")\n\n    def get_directories(\n        self,\n        directory: typing.Optional[PathLike] = None,\n        packages: typing.Optional[\n            typing.List[typing.Union[str, typing.Tuple[str, str]]]\n        ] = 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 = Path(spec.origin).joinpath(\"..\", statics_dir).resolve()\n            assert (\n                package_directory.is_dir()\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) -> Path:\n        \"\"\"\n        Given the ASGI scope, return the `path` string to serve up,\n        with OS specific path separators, and any '..', '.' components removed.\n        \"\"\"\n        return Path(*scope[\"path\"].split(\"/\"))\n\n    async def get_response(self, path: Path, 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 = path.joinpath(\"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: Path\n    ) -> typing.Tuple[Path, typing.Optional[os.stat_result]]:\n        for directory in self.all_directories:\n            original_path = Path(directory).joinpath(path)\n            full_path = original_path.resolve()\n            directory = Path(directory).resolve()\n            try:\n                stat_result = os.lstat(original_path)\n                full_path.relative_to(directory)\n                return full_path, stat_result\n            except ValueError:\n                # Allow clients to break out of the static files directory\n                # if following symlinks.\n                if stat.S_ISLNK(stat_result.st_mode):\n                    stat_result = os.lstat(full_path)\n                    return full_path, stat_result\n            except (FileNotFoundError, NotADirectoryError):\n                continue\n        return Path(), 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"}]}