Merge pull request #86 from markbeep/add-etag-caching

add etag & cache-control headers to static files
This commit is contained in:
Mark
2025-04-08 20:35:41 +02:00
committed by GitHub
10 changed files with 77 additions and 24 deletions

View File

@@ -1,18 +1,20 @@
import hashlib
from os import PathLike
from pathlib import Path
from typing import Annotated
from typing import Annotated, Callable
from urllib.parse import urlencode
from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response
from fastapi.responses import FileResponse, RedirectResponse
from sqlmodel import Session
from app.internal.auth.config import LoginTypeEnum, auth_config
from app.internal.auth.authentication import (
DetailedUser,
create_user,
get_authenticated_user,
raise_for_invalid_password,
)
from app.internal.auth.config import LoginTypeEnum, auth_config
from app.internal.models import GroupEnum
from app.util.db import get_session
from app.util.templates import templates
@@ -22,38 +24,63 @@ router = APIRouter()
root = Path("static")
etag_cache: dict[PathLike[str] | str, str] = {}
def add_cache_headers(func: Callable[..., FileResponse]):
def wrapper(v: str):
file = func()
if not (etag := etag_cache.get(file.path)):
with open(file.path, "rb") as f:
etag = hashlib.sha1(f.read(), usedforsecurity=False).hexdigest()
etag_cache[file.path] = etag
file.headers.append("Etag", etag)
# cache for a year. All static files should do cache busting with `?v=<version>`
file.headers.append("Cache-Control", f"public, max-age={60 * 60 * 24 * 365}")
return file
return wrapper
@router.get("/static/globals.css")
@add_cache_headers
def read_globals_css():
return FileResponse(root / "globals.css", media_type="text/css")
@router.get("/static/nouislider.css")
@add_cache_headers
def read_nouislider_css():
return FileResponse(root / "nouislider.min.css", media_type="text/css")
@router.get("/static/nouislider.js")
@add_cache_headers
def read_nouislider_js():
return FileResponse(root / "nouislider.min.js", media_type="text/javascript")
@router.get("/static/apple-touch-icon.png")
@add_cache_headers
def read_apple_touch_icon():
return FileResponse(root / "apple-touch-icon.png", media_type="image/png")
@router.get("/static/favicon-32x32.png")
@add_cache_headers
def read_favicon_32():
return FileResponse(root / "favicon-32x32.png", media_type="image/png")
@router.get("/static/favicon-16x16.png")
@add_cache_headers
def read_favicon_16():
return FileResponse(root / "favicon-16x16.png", media_type="image/png")
@router.get("/static/site.webmanifest")
@add_cache_headers
def read_site_webmanifest():
return FileResponse(
root / "site.webmanifest", media_type="application/manifest+json"
@@ -61,21 +88,37 @@ def read_site_webmanifest():
@router.get("/static/htmx.js")
@add_cache_headers
def read_htmx():
return FileResponse(root / "htmx.js", media_type="application/javascript")
return FileResponse(root / "htmx.js", media_type="text/javascript")
@router.get("/static/htmx-preload.js")
@add_cache_headers
def read_htmx_preload():
return FileResponse(root / "htmx-preload.js", media_type="application/javascript")
return FileResponse(root / "htmx-preload.js", media_type="text/javascript")
@router.get("/static/alpine.js")
@add_cache_headers
def read_alpinejs():
return FileResponse(root / "alpine.js", media_type="application/javascript")
return FileResponse(root / "alpine.js", media_type="text/javascript")
@router.get("/static/toastify.js")
@add_cache_headers
def read_toastifyjs():
return FileResponse(root / "toastify.js", media_type="text/javascript")
@router.get("/static/toastify.css")
@add_cache_headers
def read_toastifycss():
return FileResponse(root / "toastify.css", media_type="text/css")
@router.get("/static/favicon.svg")
@add_cache_headers
def read_favicon_svg():
return FileResponse(root / "favicon.svg", media_type="image/svg+xml")

View File

@@ -98,7 +98,6 @@ def read_users(
"page": "users",
"users": users,
"is_oidc": is_oidc,
"version": Settings().app.version,
},
)
@@ -230,7 +229,6 @@ def read_prowlarr(
"indexer_categories": indexer_categories,
"selected_categories": selected,
"prowlarr_misconfigured": True if prowlarr_misconfigured else False,
"version": Settings().app.version,
},
)
@@ -320,7 +318,6 @@ def read_download(
"name_ratio": name_ratio,
"title_ratio": title_ratio,
"indexer_flags": flags,
"version": Settings().app.version,
},
)
@@ -460,7 +457,6 @@ def read_notifications(
"page": "notifications",
"notifications": notifications,
"event_types": event_types,
"version": Settings().app.version,
},
)
@@ -605,7 +601,6 @@ def read_security(
"oidc_group_claim": oidc_config.get(session, "oidc_group_claim", ""),
"oidc_redirect_https": oidc_config.get_redirect_https(session),
"oidc_logout_url": oidc_config.get(session, "oidc_logout_url", ""),
"version": Settings().app.version,
},
)
@@ -733,7 +728,6 @@ async def read_indexers(
{
"page": "indexers",
"indexers": contexts,
"version": Settings().app.version,
},
)

View File

@@ -9,6 +9,8 @@ files = {
"htmx-preload.js": "https://unpkg.com/htmx-ext-preload@2.1.0/preload.js",
"htmx.js": "https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js",
"alpine.js": "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js",
"toastify.js": "https://cdn.jsdelivr.net/npm/toastify-js",
"toastify.css": "https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css",
}

View File

@@ -6,12 +6,14 @@ from jinja2_fragments.fastapi import Jinja2Blocks
from starlette.background import BackgroundTask
from app.internal.auth.authentication import DetailedUser
from app.internal.env_settings import Settings
templates = Jinja2Blocks(directory="templates")
templates.env.filters["quote_plus"] = lambda u: quote_plus(u) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType,reportUnknownArgumentType]
templates.env.filters["zfill"] = lambda val, num: str(val).zfill(num) # pyright: ignore[reportUnknownLambdaType,reportUnknownMemberType,reportUnknownArgumentType]
templates.env.globals["vars"] = vars # pyright: ignore[reportUnknownMemberType]
templates.env.globals["getattr"] = getattr # pyright: ignore[reportUnknownMemberType]
templates.env.globals["version"] = Settings().app.version # pyright: ignore[reportUnknownMemberType]
@overload

View File

@@ -93,6 +93,14 @@
url = "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js";
sha256 = "sha256:1lqa3v5p7pwz3599xnxf5bwxf17bbmqxcqz3cpgj32a8ab9fxl9y";
};
toastifyjs = builtins.fetchurl {
url = "https://cdn.jsdelivr.net/npm/toastify-js";
sha256 = "sha256:0v22qkipd2y4z08qkl8hd28d0bgjahn9q08nx05bxfg282zgxavg";
};
toastifycss = builtins.fetchurl {
url = "https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css";
sha256 = "sha256:13z5076jlvy1p4fqmmvic3ywbi153jrs0hy8mrl1z45s2js2qgpf";
};
in
pkgs.dockerTools.buildImage {
@@ -113,6 +121,8 @@
cp ${htmx-preload} $out/app/static/htmx-preload.js
cp ${htmx} $out/app/static/htmx.js
cp ${alpinejs} $out/app/static/alpine.js
cp ${toastifyjs} $out/app/static/toastify.js
cp ${toastifycss} $out/app/static/toastify.css
'';
};

2
static/.gitignore vendored
View File

@@ -1,3 +1,5 @@
alpine.js
htmx.js
htmx-preload.js
toastify.js
toastify.css

View File

@@ -5,9 +5,9 @@
{% block head %}
<title>AudioBookRequest</title>
{% endblock %}
<link rel="stylesheet" href="/static/globals.css" />
<script src="/static/htmx.js"></script>
<script defer src="/static/htmx-preload.js"></script>
<link rel="stylesheet" href="/static/globals.css?v={{ version }}" />
<script src="/static/htmx.js?v={{ version }}"></script>
<script defer src="/static/htmx-preload.js?v={{ version }}"></script>
<script>
const setTheme = theme => {
if (!theme) {
@@ -42,27 +42,27 @@
<link
rel="apple-touch-icon"
sizes="180x180"
href="/static/apple-touch-icon.png"
href="/static/apple-touch-icon.png?v={{ version }}"
/>
<link
rel="icon"
sizes="any"
type="image/svg+xml"
href="/static/favicon.svg"
href="/static/favicon.svg?v={{ version }}"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/static/favicon-32x32.png"
href="/static/favicon-32x32.png?v={{ version }}"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/static/favicon-16x16.png"
href="/static/favicon-16x16.png?v={{ version }}"
/>
<link rel="manifest" href="/static/site.webmanifest" />
<link rel="manifest" href="/static/site.webmanifest?v={{ version }}" />
{% include 'scripts/toast.html' %}
</head>

View File

@@ -1 +1 @@
<script defer src="/static/alpine.js"></script>
<script defer src="/static/alpine.js?v={{ version }}"></script>

View File

@@ -1,11 +1,11 @@
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css"
href="/static/toastify.css?v={{ version }}"
/>
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/toastify-js"
src="/static/toastify.js?v={{ version }}"
></script>
<script>
const toast = (message, type = "success") => {

View File

@@ -1,7 +1,7 @@
{% extends "settings_page/base.html" %} {% block head %}
<title>Settings - Download</title>
<link href="/static/nouislider.css" rel="stylesheet" />
<script src="/static/nouislider.js"></script>
<link href="/static/nouislider.css?v={{ version }}" rel="stylesheet" />
<script src="/static/nouislider.js?v={{ version }}"></script>
<script>
const createSlider = (sliderId, fromId, toId, start, stop) => {
const slider = document.getElementById(sliderId);