Skip to content

Commit b7da334

Browse files
committed
Add static path precompressed option
1 parent 0fbc4b9 commit b7da334

File tree

19 files changed

+1256
-71
lines changed

19 files changed

+1256
-71
lines changed

granian/_granian.pyi

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import threading
33
from typing import Any
44

55
from ._types import WebsocketMessage
6+
from .files import StaticFilesSettings
67
from .http import HTTP1Settings, HTTP2Settings
78

89
__version__: str
@@ -71,7 +72,7 @@ class ASGIWorker:
7172
http1_opts: HTTP1Settings | None,
7273
http2_opts: HTTP2Settings | None,
7374
websockets_enabled: bool,
74-
static_files: tuple[str, str, str] | None,
75+
static_files: StaticFilesSettings | None,
7576
ssl_enabled: bool,
7677
ssl_cert: str | None,
7778
ssl_key: str | None,
@@ -95,7 +96,7 @@ class WSGIWorker:
9596
http_mode: str,
9697
http1_opts: HTTP1Settings | None,
9798
http2_opts: HTTP2Settings | None,
98-
static_files: tuple[str, str, str] | None,
99+
static_files: StaticFilesSettings | None,
99100
ssl_enabled: bool,
100101
ssl_cert: str | None,
101102
ssl_key: str | None,
@@ -120,7 +121,7 @@ class RSGIWorker:
120121
http1_opts: HTTP1Settings | None,
121122
http2_opts: HTTP2Settings | None,
122123
websockets_enabled: bool,
123-
static_files: tuple[str, str, str] | None,
124+
static_files: StaticFilesSettings | None,
124125
ssl_enabled: bool,
125126
ssl_cert: str | None,
126127
ssl_key: str | None,

granian/cli.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,11 @@ def option(*param_decls: str, cls: type[click.Option] | None = None, **attrs: An
360360
default=86400,
361361
help='Cache headers expiration (in seconds or a human-readable duration) for static file serving. 0 to disable.',
362362
)
363+
@option(
364+
'--static-path-precompressed/--no-static-path-precompressed',
365+
default=False,
366+
help='Enable serving precompressed static files with .br, .gz, or .zst extensions',
367+
)
363368
@option(
364369
'--reload/--no-reload',
365370
default=False,
@@ -473,6 +478,7 @@ def cli(
473478
static_path_route: str,
474479
static_path_mount: pathlib.Path | None,
475480
static_path_expires: int,
481+
static_path_precompressed: bool,
476482
reload: bool,
477483
reload_paths: list[pathlib.Path] | None,
478484
reload_ignore_dirs: list[str] | None,
@@ -558,6 +564,7 @@ def cli(
558564
static_path_route=static_path_route,
559565
static_path_mount=static_path_mount,
560566
static_path_expires=static_path_expires,
567+
static_path_precompressed=static_path_precompressed,
561568
reload=reload,
562569
reload_paths=reload_paths,
563570
reload_ignore_paths=reload_ignore_paths,

granian/files.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class StaticFilesSettings:
6+
"""Configuration for static file serving.
7+
8+
Attributes:
9+
mount: The filesystem path to serve static files from.
10+
prefix: The URL path prefix for static file routes.
11+
expires: Cache-Control max-age value in seconds (as string), or None to disable.
12+
precompressed: Whether to serve pre-compressed sidecar files (.br, .gz, .zst).
13+
"""
14+
15+
mount: str
16+
prefix: str = '/static'
17+
expires: str | None = None
18+
precompressed: bool = False

granian/server/common.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .._signals import set_main_signals
1919
from ..constants import HTTPModes, Interfaces, Loops, RuntimeModes, SSLProtocols, TaskImpl
2020
from ..errors import ConfigurationError, PidFileError
21+
from ..files import StaticFilesSettings
2122
from ..http import HTTP1Settings, HTTP2Settings
2223
from ..log import DEFAULT_ACCESSLOG_FMT, LogLevels, configure_logging, logger
2324
from ..net import SocketSpec, UnixSocketSpec
@@ -127,6 +128,7 @@ def __init__(
127128
static_path_route: str = '/static',
128129
static_path_mount: Path | None = None,
129130
static_path_expires: int = 86400,
131+
static_path_precompressed: bool = False,
130132
reload: bool = False,
131133
reload_paths: Sequence[Path] | None = None,
132134
reload_ignore_dirs: Sequence[str] | None = None,
@@ -180,11 +182,12 @@ def __init__(
180182
self.factory = factory
181183
self.working_dir = working_dir
182184
self.env_files = env_files or ()
183-
self.static_path = (
184-
(
185-
static_path_route,
186-
str(static_path_mount.resolve()),
187-
(str(static_path_expires) if static_path_expires else None),
185+
self.static_files = (
186+
StaticFilesSettings(
187+
prefix=static_path_route,
188+
mount=str(static_path_mount.resolve()),
189+
expires=str(static_path_expires) if static_path_expires else None,
190+
precompressed=static_path_precompressed,
188191
)
189192
if static_path_mount
190193
else None

granian/server/embed.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .._types import SSLCtx
1515
from ..asgi import LifespanProtocol, _callback_wrapper as _asgi_call_wrap
1616
from ..errors import ConfigurationError, FatalError
17+
from ..files import StaticFilesSettings
1718
from ..rsgi import _callback_wrapper as _rsgi_call_wrap, _callbacks_from_target as _rsgi_cbs_from_target
1819
from .common import (
1920
_PY_312,
@@ -127,6 +128,7 @@ def __init__(
127128
static_path_route: str = '/static',
128129
static_path_mount: Path | None = None,
129130
static_path_expires: int = 86400,
131+
static_path_precompressed: bool = False,
130132
):
131133
super().__init__(
132134
target=target,
@@ -162,6 +164,7 @@ def __init__(
162164
static_path_route=static_path_route,
163165
static_path_mount=static_path_mount,
164166
static_path_expires=static_path_expires,
167+
static_path_precompressed=static_path_precompressed,
165168
)
166169
self.main_loop_interrupt = asyncio.Event()
167170

@@ -187,7 +190,7 @@ def _spawn_worker(self, idx, target, callback_loader) -> AsyncWorker:
187190
self.http1_settings,
188191
self.http2_settings,
189192
self.websockets,
190-
self.static_path,
193+
self.static_files,
191194
self.log_access_format if self.log_access else None,
192195
self.ssl_ctx,
193196
{'url_path_prefix': self.url_path_prefix},
@@ -213,7 +216,7 @@ async def _spawn_asgi_worker(
213216
http1_settings: HTTP1Settings | None,
214217
http2_settings: HTTP2Settings | None,
215218
websockets: bool,
216-
static_path: tuple[str, str, str] | None,
219+
static_files: StaticFilesSettings | None,
217220
log_access_fmt: str | None,
218221
ssl_ctx: SSLCtx,
219222
scope_opts: dict[str, Any],
@@ -232,7 +235,7 @@ async def _spawn_asgi_worker(
232235
http1_settings,
233236
http2_settings,
234237
websockets,
235-
static_path,
238+
static_files,
236239
*ssl_ctx,
237240
)
238241
serve = worker.serve_async_uds if sock.is_uds() else worker.serve_async
@@ -257,7 +260,7 @@ async def _spawn_asgi_lifespan_worker(
257260
http1_settings: HTTP1Settings | None,
258261
http2_settings: HTTP2Settings | None,
259262
websockets: bool,
260-
static_path: tuple[str, str, str] | None,
263+
static_files: StaticFilesSettings | None,
261264
log_access_fmt: str | None,
262265
ssl_ctx: SSLCtx,
263266
scope_opts: dict[str, Any],
@@ -284,7 +287,7 @@ async def _spawn_asgi_lifespan_worker(
284287
http1_settings,
285288
http2_settings,
286289
websockets,
287-
static_path,
290+
static_files,
288291
*ssl_ctx,
289292
)
290293
serve = worker.serve_async_uds if sock.is_uds() else worker.serve_async
@@ -310,7 +313,7 @@ async def _spawn_rsgi_worker(
310313
http1_settings: HTTP1Settings | None,
311314
http2_settings: HTTP2Settings | None,
312315
websockets: bool,
313-
static_path: tuple[str, str, str] | None,
316+
static_files: StaticFilesSettings | None,
314317
log_access_fmt: str | None,
315318
ssl_ctx: SSLCtx,
316319
scope_opts: dict[str, Any],
@@ -331,7 +334,7 @@ async def _spawn_rsgi_worker(
331334
http1_settings,
332335
http2_settings,
333336
websockets,
334-
static_path,
337+
static_files,
335338
*ssl_ctx,
336339
)
337340
serve = worker.serve_async_uds if sock.is_uds() else worker.serve_async

granian/server/mp.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .._internal import load_env
1111
from .._types import SSLCtx
1212
from ..asgi import LifespanProtocol, _callback_wrapper as _asgi_call_wrap
13+
from ..files import StaticFilesSettings
1314
from ..rsgi import _callback_wrapper as _rsgi_call_wrap, _callbacks_from_target as _rsgi_cbs_from_target
1415
from ..wsgi import _callback_wrapper as _wsgi_call_wrap
1516
from .common import (
@@ -112,7 +113,7 @@ def _spawn_asgi_worker(
112113
http1_settings: HTTP1Settings | None,
113114
http2_settings: HTTP2Settings | None,
114115
websockets: bool,
115-
static_path: tuple[str, str, str | None] | None,
116+
static_files: StaticFilesSettings | None,
116117
log_access_fmt: str | None,
117118
ssl_ctx: SSLCtx,
118119
scope_opts: dict[str, Any],
@@ -134,7 +135,7 @@ def _spawn_asgi_worker(
134135
http1_settings,
135136
http2_settings,
136137
websockets,
137-
static_path,
138+
static_files,
138139
*ssl_ctx,
139140
)
140141
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
@@ -159,7 +160,7 @@ def _spawn_asgi_lifespan_worker(
159160
http1_settings: HTTP1Settings | None,
160161
http2_settings: HTTP2Settings | None,
161162
websockets: bool,
162-
static_path: tuple[str, str, str | None] | None,
163+
static_files: StaticFilesSettings | None,
163164
log_access_fmt: str | None,
164165
ssl_ctx: SSLCtx,
165166
scope_opts: dict[str, Any],
@@ -189,7 +190,7 @@ def _spawn_asgi_lifespan_worker(
189190
http1_settings,
190191
http2_settings,
191192
websockets,
192-
static_path,
193+
static_files,
193194
*ssl_ctx,
194195
)
195196
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
@@ -215,7 +216,7 @@ def _spawn_rsgi_worker(
215216
http1_settings: HTTP1Settings | None,
216217
http2_settings: HTTP2Settings | None,
217218
websockets: bool,
218-
static_path: tuple[str, str, str | None] | None,
219+
static_files: StaticFilesSettings | None,
219220
log_access_fmt: str | None,
220221
ssl_ctx: SSLCtx,
221222
scope_opts: dict[str, Any],
@@ -239,7 +240,7 @@ def _spawn_rsgi_worker(
239240
http1_settings,
240241
http2_settings,
241242
websockets,
242-
static_path,
243+
static_files,
243244
*ssl_ctx,
244245
)
245246
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
@@ -265,7 +266,7 @@ def _spawn_wsgi_worker(
265266
http1_settings: HTTP1Settings | None,
266267
http2_settings: HTTP2Settings | None,
267268
websockets: bool,
268-
static_path: tuple[str, str, str | None] | None,
269+
static_files: StaticFilesSettings | None,
269270
log_access_fmt: str | None,
270271
ssl_ctx: SSLCtx,
271272
scope_opts: dict[str, Any],
@@ -286,7 +287,7 @@ def _spawn_wsgi_worker(
286287
http_mode,
287288
http1_settings,
288289
http2_settings,
289-
static_path,
290+
static_files,
290291
*ssl_ctx,
291292
)
292293
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
@@ -359,7 +360,7 @@ def _spawn_worker(self, idx, target, callback_loader) -> WorkerProcess:
359360
self.http1_settings,
360361
self.http2_settings,
361362
self.websockets,
362-
self.static_path,
363+
self.static_files,
363364
self.log_access_format if self.log_access else None,
364365
self.ssl_ctx,
365366
{'url_path_prefix': self.url_path_prefix},

granian/server/mt.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .._types import SSLCtx
1111
from ..asgi import LifespanProtocol, _callback_wrapper as _asgi_call_wrap
1212
from ..errors import ConfigurationError, FatalError
13+
from ..files import StaticFilesSettings
1314
from ..rsgi import _callback_wrapper as _rsgi_call_wrap, _callbacks_from_target as _rsgi_cbs_from_target
1415
from ..wsgi import _callback_wrapper as _wsgi_call_wrap
1516
from .common import (
@@ -88,7 +89,7 @@ def _spawn_asgi_worker(
8889
http1_settings: HTTP1Settings | None,
8990
http2_settings: HTTP2Settings | None,
9091
websockets: bool,
91-
static_path: tuple[str, str, str | None] | None,
92+
static_files: StaticFilesSettings | None,
9293
log_access_fmt: str | None,
9394
ssl_ctx: SSLCtx,
9495
scope_opts: dict[str, Any],
@@ -107,7 +108,7 @@ def _spawn_asgi_worker(
107108
http1_settings,
108109
http2_settings,
109110
websockets,
110-
static_path,
111+
static_files,
111112
*ssl_ctx,
112113
)
113114
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
@@ -133,7 +134,7 @@ def _spawn_asgi_lifespan_worker(
133134
http1_settings: HTTP1Settings | None,
134135
http2_settings: HTTP2Settings | None,
135136
websockets: bool,
136-
static_path: tuple[str, str, str | None] | None,
137+
static_files: StaticFilesSettings | None,
137138
log_access_fmt: str | None,
138139
ssl_ctx: SSLCtx,
139140
scope_opts: dict[str, Any],
@@ -160,7 +161,7 @@ def _spawn_asgi_lifespan_worker(
160161
http1_settings,
161162
http2_settings,
162163
websockets,
163-
static_path,
164+
static_files,
164165
*ssl_ctx,
165166
)
166167
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
@@ -187,7 +188,7 @@ def _spawn_rsgi_worker(
187188
http1_settings: HTTP1Settings | None,
188189
http2_settings: HTTP2Settings | None,
189190
websockets: bool,
190-
static_path: tuple[str, str, str | None] | None,
191+
static_files: StaticFilesSettings | None,
191192
log_access_fmt: str | None,
192193
ssl_ctx: SSLCtx,
193194
scope_opts: dict[str, Any],
@@ -208,7 +209,7 @@ def _spawn_rsgi_worker(
208209
http1_settings,
209210
http2_settings,
210211
websockets,
211-
static_path,
212+
static_files,
212213
*ssl_ctx,
213214
)
214215
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
@@ -235,7 +236,7 @@ def _spawn_wsgi_worker(
235236
http1_settings: HTTP1Settings | None,
236237
http2_settings: HTTP2Settings | None,
237238
websockets: bool,
238-
static_path: tuple[str, str, str | None] | None,
239+
static_files: StaticFilesSettings | None,
239240
log_access_fmt: str | None,
240241
ssl_ctx: SSLCtx,
241242
scope_opts: dict[str, Any],
@@ -253,7 +254,7 @@ def _spawn_wsgi_worker(
253254
http_mode,
254255
http1_settings,
255256
http2_settings,
256-
static_path,
257+
static_files,
257258
*ssl_ctx,
258259
)
259260
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
@@ -284,7 +285,7 @@ def _spawn_worker(self, idx, target, callback_loader) -> WorkerThread:
284285
self.http1_settings,
285286
self.http2_settings,
286287
self.websockets,
287-
self.static_path,
288+
self.static_files,
288289
self.log_access_format if self.log_access else None,
289290
self.ssl_ctx,
290291
{'url_path_prefix': self.url_path_prefix},

0 commit comments

Comments
 (0)