servestatic 4.3.2__tar.gz → 4.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {servestatic-4.3.2 → servestatic-4.3.3}/PKG-INFO +1 -1
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/__init__.py +1 -1
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/responders.py +26 -15
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/utils.py +40 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/.gitignore +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/LICENSE.md +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/README.md +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/pyproject.toml +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/apps.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/asgi.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/base.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/checks.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/cli.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/compress.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/management/__init__.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/management/commands/__init__.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/management/commands/runserver.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/manifest_hash.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/media_types.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/middleware.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/runserver_nostatic/__init__.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/runserver_nostatic/apps.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/runserver_nostatic/management/__init__.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/runserver_nostatic/management/commands/__init__.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/runserver_nostatic/management/commands/runserver.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/storage.py +0 -0
- {servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/wsgi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: servestatic
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.3
|
|
4
4
|
Summary: Production-grade static file server for Python WSGI & ASGI.
|
|
5
5
|
Project-URL: Changelog, https://archmonger.github.io/ServeStatic/latest/changelog/
|
|
6
6
|
Project-URL: Documentation, https://archmonger.github.io/ServeStatic/
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import errno
|
|
5
5
|
import os
|
|
6
|
-
import re
|
|
7
6
|
import stat
|
|
8
7
|
from email.utils import formatdate, parsedate
|
|
9
8
|
from http import HTTPStatus
|
|
@@ -13,7 +12,7 @@ from typing import TYPE_CHECKING
|
|
|
13
12
|
from urllib.parse import quote
|
|
14
13
|
from wsgiref.headers import Headers
|
|
15
14
|
|
|
16
|
-
from servestatic.utils import AsyncFile
|
|
15
|
+
from servestatic.utils import AsyncFile, parse_accept_encoding
|
|
17
16
|
|
|
18
17
|
if TYPE_CHECKING:
|
|
19
18
|
from collections.abc import Callable, Mapping
|
|
@@ -42,6 +41,7 @@ NOT_ALLOWED_RESPONSE = Response(
|
|
|
42
41
|
# Headers which should be returned with a 304 Not Modified response as
|
|
43
42
|
# specified here: https://tools.ietf.org/html/rfc7232#section-4.1
|
|
44
43
|
NOT_MODIFIED_HEADERS = (
|
|
44
|
+
"Access-Control-Allow-Origin",
|
|
45
45
|
"Cache-Control",
|
|
46
46
|
"Content-Location",
|
|
47
47
|
"Date",
|
|
@@ -184,7 +184,7 @@ class StaticFile:
|
|
|
184
184
|
raise ValueError(msg)
|
|
185
185
|
start, end = self.get_byte_range(range_header, size)
|
|
186
186
|
if start > end:
|
|
187
|
-
return self.get_range_not_satisfiable_response(file_handle, size)
|
|
187
|
+
return self.get_range_not_satisfiable_response(file_handle, size, headers)
|
|
188
188
|
if file_handle is not None:
|
|
189
189
|
file_handle = SlicedFile(file_handle, start, end)
|
|
190
190
|
headers.extend((
|
|
@@ -213,7 +213,7 @@ class StaticFile:
|
|
|
213
213
|
raise ValueError(msg)
|
|
214
214
|
start, end = self.get_byte_range(range_header, size)
|
|
215
215
|
if start > end:
|
|
216
|
-
return await self.aget_range_not_satisfiable_response(file_handle, size)
|
|
216
|
+
return await self.aget_range_not_satisfiable_response(file_handle, size, headers)
|
|
217
217
|
sliced_file: AsyncSlicedFile | None = None
|
|
218
218
|
if file_handle is not None:
|
|
219
219
|
sliced_file = AsyncSlicedFile(file_handle, start, end)
|
|
@@ -249,23 +249,35 @@ class StaticFile:
|
|
|
249
249
|
return start, end
|
|
250
250
|
|
|
251
251
|
@staticmethod
|
|
252
|
-
def get_range_not_satisfiable_response(
|
|
252
|
+
def get_range_not_satisfiable_response(
|
|
253
|
+
file_handle: BufferedIOBase | None,
|
|
254
|
+
size: int,
|
|
255
|
+
headers: list[tuple[str, str | None]] | None = None,
|
|
256
|
+
) -> Response:
|
|
253
257
|
if file_handle is not None:
|
|
254
258
|
file_handle.close()
|
|
259
|
+
response_headers = list(headers) if headers else []
|
|
260
|
+
response_headers.append(("Content-Range", f"bytes */{size}"))
|
|
255
261
|
return Response(
|
|
256
262
|
HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
|
|
257
|
-
|
|
263
|
+
response_headers,
|
|
258
264
|
None,
|
|
259
265
|
)
|
|
260
266
|
|
|
261
267
|
@staticmethod
|
|
262
|
-
async def aget_range_not_satisfiable_response(
|
|
268
|
+
async def aget_range_not_satisfiable_response(
|
|
269
|
+
file_handle: AsyncFile | None,
|
|
270
|
+
size: int,
|
|
271
|
+
headers: list[tuple[str, str | None]] | None = None,
|
|
272
|
+
) -> Response:
|
|
263
273
|
"""Variant of `get_range_not_satisfiable_response` that works with
|
|
264
274
|
async file objects. Async file handles do not need to be closed, since they
|
|
265
275
|
are only opened via context managers while being dispatched."""
|
|
276
|
+
response_headers = list(headers) if headers else []
|
|
277
|
+
response_headers.append(("Content-Range", f"bytes */{size}"))
|
|
266
278
|
return Response(
|
|
267
279
|
HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
|
|
268
|
-
|
|
280
|
+
response_headers,
|
|
269
281
|
None,
|
|
270
282
|
)
|
|
271
283
|
|
|
@@ -313,7 +325,7 @@ class StaticFile:
|
|
|
313
325
|
def get_alternatives(
|
|
314
326
|
base_headers: Headers,
|
|
315
327
|
files: Mapping[str | None, FileEntry],
|
|
316
|
-
) -> list[tuple[
|
|
328
|
+
) -> list[tuple[str | None, str, list[tuple[str, str | None]]]]:
|
|
317
329
|
# Sort by size so that the smallest compressed alternative matches first
|
|
318
330
|
alternatives = []
|
|
319
331
|
files_by_size = sorted(files.items(), key=lambda i: i[1].size)
|
|
@@ -322,10 +334,7 @@ class StaticFile:
|
|
|
322
334
|
headers["Content-Length"] = str(file_entry.size)
|
|
323
335
|
if encoding:
|
|
324
336
|
headers["Content-Encoding"] = encoding
|
|
325
|
-
|
|
326
|
-
else:
|
|
327
|
-
encoding_re = re.compile(r"")
|
|
328
|
-
alternatives.append((encoding_re, file_entry.path, headers.items()))
|
|
337
|
+
alternatives.append((encoding, file_entry.path, headers.items()))
|
|
329
338
|
return alternatives
|
|
330
339
|
|
|
331
340
|
def is_not_modified(self, request_headers: Mapping[str, str]) -> bool:
|
|
@@ -347,12 +356,14 @@ class StaticFile:
|
|
|
347
356
|
accept_encoding = request_headers.get("HTTP_ACCEPT_ENCODING", "")
|
|
348
357
|
if accept_encoding == "*":
|
|
349
358
|
accept_encoding = ""
|
|
359
|
+
accepted = parse_accept_encoding(accept_encoding)
|
|
350
360
|
# These are sorted by size so first match is the best
|
|
351
361
|
result = next(
|
|
352
362
|
(
|
|
353
363
|
(path, headers)
|
|
354
|
-
for
|
|
355
|
-
|
|
364
|
+
for encoding, path, headers in self.alternatives
|
|
365
|
+
# An uncompressed file is always an acceptable fallback.
|
|
366
|
+
if not encoding or encoding in accepted
|
|
356
367
|
),
|
|
357
368
|
None,
|
|
358
369
|
)
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import concurrent.futures
|
|
5
5
|
import contextlib
|
|
6
6
|
import functools
|
|
7
|
+
import math
|
|
7
8
|
import os
|
|
8
9
|
from concurrent.futures import ThreadPoolExecutor
|
|
9
10
|
from typing import TYPE_CHECKING, Concatenate, ParamSpec, TypeVar, cast
|
|
@@ -38,6 +39,45 @@ def ensure_leading_trailing_slash(path: str | None) -> str:
|
|
|
38
39
|
return f"/{path}/" if path else "/"
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
def parse_accept_encoding(header: str) -> set[str]:
|
|
43
|
+
"""
|
|
44
|
+
Return the set of content codings the client is willing to accept.
|
|
45
|
+
|
|
46
|
+
The header is parsed as described in RFC 9110 section 12.5.3 rather than
|
|
47
|
+
matched as a substring, so that a coding the client has refused with
|
|
48
|
+
``q=0`` is not offered to it, and a coding name is only recognised when it
|
|
49
|
+
appears as a whole token.
|
|
50
|
+
"""
|
|
51
|
+
accepted: set[str] = set()
|
|
52
|
+
for entry in header.split(","):
|
|
53
|
+
coding, _, params = entry.partition(";")
|
|
54
|
+
coding = coding.strip().lower()
|
|
55
|
+
if not coding:
|
|
56
|
+
continue
|
|
57
|
+
quality = 1.0
|
|
58
|
+
for param in params.split(";"):
|
|
59
|
+
name, sep, value = param.partition("=")
|
|
60
|
+
if name.strip().lower() != "q":
|
|
61
|
+
continue
|
|
62
|
+
if not sep:
|
|
63
|
+
break
|
|
64
|
+
try:
|
|
65
|
+
quality = float(value.strip())
|
|
66
|
+
except ValueError:
|
|
67
|
+
# An unparseable qvalue makes the whole entry invalid; treat
|
|
68
|
+
# the coding as unacceptable rather than guess.
|
|
69
|
+
quality = 0.0
|
|
70
|
+
else:
|
|
71
|
+
# Per RFC 9110 a qvalue must be a number in the range 0.000 to
|
|
72
|
+
# 1.000; any other finite value (e.g. ``inf``) is invalid.
|
|
73
|
+
if not math.isfinite(quality) or not 0.0 <= quality <= 1.0:
|
|
74
|
+
quality = 0.0
|
|
75
|
+
break
|
|
76
|
+
if quality > 0:
|
|
77
|
+
accepted.add(coding)
|
|
78
|
+
return accepted
|
|
79
|
+
|
|
80
|
+
|
|
41
81
|
def scantree(root: str | PathLike[str]) -> Iterator[tuple[str, os.stat_result]]:
|
|
42
82
|
"""
|
|
43
83
|
Recurse the given directory yielding (pathname, os.stat(pathname)) pairs
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{servestatic-4.3.2 → servestatic-4.3.3}/src/servestatic/runserver_nostatic/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|