This commit is contained in:
soraefir
2026-06-09 00:18:17 +02:00
parent 1ce2a94786
commit 44b7c5858c

View File

@@ -5,6 +5,14 @@ let
palette = serverCfg.colorScheme.palette or { }; palette = serverCfg.colorScheme.palette or { };
port = 8080; port = 8080;
assetSize = 64; assetSize = 64;
cacheMode = containerCfg.extra.cacheMode or "off";
cacheControl =
if cacheMode == "disk" then
containerCfg.extra.cacheControl or "public, max-age=3600"
else if cacheMode == "off" then
"no-store"
else
throw "favicon cacheMode must be either `off` or `disk`";
priority = toString (containerCfg.extra.priority or 2147482647); priority = toString (containerCfg.extra.priority or 2147482647);
logoSvgFileName = builtins.baseNameOf (toString mediaCfg.logo.svg); logoSvgFileName = builtins.baseNameOf (toString mediaCfg.logo.svg);
logoSvgMount = "/assets/${logoSvgFileName}"; logoSvgMount = "/assets/${logoSvgFileName}";
@@ -68,7 +76,11 @@ let
]); ]);
serverScript = pkgs.writeText "favicon-server.py" '' serverScript = pkgs.writeText "favicon-server.py" ''
from io import BytesIO from io import BytesIO
import hashlib
import os
from pathlib import Path
import re import re
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import cairosvg import cairosvg
@@ -78,7 +90,9 @@ let
LISTEN_HOST = "0.0.0.0" LISTEN_HOST = "0.0.0.0"
LISTEN_PORT = ${toString port} LISTEN_PORT = ${toString port}
ASSET_SIZE = ${toString assetSize} ASSET_SIZE = ${toString assetSize}
CACHE_CONTROL = "no-store" CACHE_MODE = ${builtins.toJSON cacheMode}
CACHE_CONTROL = ${builtins.toJSON cacheControl}
CACHE_DIR = Path("/cache")
with open(LOGO_PATH, "rb") as fh: with open(LOGO_PATH, "rb") as fh:
LOGO_BYTES = fh.read() LOGO_BYTES = fh.read()
@@ -86,6 +100,8 @@ let
DEFAULT_PROFILE = ${if defaultProfile == null then "None" else builtins.toJSON defaultProfile} DEFAULT_PROFILE = ${if defaultProfile == null then "None" else builtins.toJSON defaultProfile}
APP_DOMAIN = (${builtins.toJSON serverCfg.domain} or "").strip().lower() APP_DOMAIN = (${builtins.toJSON serverCfg.domain} or "").strip().lower()
DEFAULT_COLORS = {"bg": "#111827", "fg": "#f8fafc"} DEFAULT_COLORS = {"bg": "#111827", "fg": "#f8fafc"}
LOGO_HASH = hashlib.sha256(LOGO_BYTES).hexdigest()
ICON_CACHE_LOCK = threading.Lock()
def _request_host(headers): def _request_host(headers):
host = ( host = (
@@ -113,25 +129,19 @@ let
return candidates return candidates
def _profile_for_host(host): def _profile_for_host(host):
candidates = _host_candidates(host) for candidate in _host_candidates(host):
print(f"favicon-profile host={host!r} candidates={candidates!r} mappings={list(MAPPINGS.keys())!r}")
for candidate in candidates:
profile = MAPPINGS.get(candidate) profile = MAPPINGS.get(candidate)
print(f"favicon-profile-check candidate={candidate!r} hit={profile is not None}")
if profile: if profile:
print(f"favicon-profile-match candidate={candidate!r} profile={profile!r}")
return candidate, profile return candidate, profile
print(f"favicon-profile-default host={host!r} default={DEFAULT_PROFILE!r}")
return None, DEFAULT_PROFILE return None, DEFAULT_PROFILE
def _replace_logo_fill(svg, color): def _replace_logo_fill(svg, color):
svg, count = re.subn( svg, _ = re.subn(
"fill:#3193f5", "fill:#3193f5",
f"fill:{color}", f"fill:{color}",
svg, svg,
flags=re.IGNORECASE, flags=re.IGNORECASE,
) )
print(f"favicon-fill replace_count={count} color={color}")
return svg return svg
def _colors(profile): def _colors(profile):
@@ -154,8 +164,6 @@ let
svg = LOGO_BYTES.decode("utf-8") svg = LOGO_BYTES.decode("utf-8")
svg = _replace_logo_fill(svg, colors["fg"]) svg = _replace_logo_fill(svg, colors["fg"])
svg = _add_background(svg, colors["bg"]) svg = _add_background(svg, colors["bg"])
print(f"favicon-render fg={colors['fg']} bg={colors['bg']} mode=svg2png")
print(f"favicon-svg {svg}")
png = cairosvg.svg2png( png = cairosvg.svg2png(
bytestring=svg.encode("utf-8"), bytestring=svg.encode("utf-8"),
@@ -168,21 +176,43 @@ let
rgba.save(output, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)]) rgba.save(output, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)])
return output.getvalue() return output.getvalue()
def _cache_path(colors):
digest = hashlib.sha256(
f"{ASSET_SIZE}:{LOGO_HASH}:{colors['bg']}:{colors['fg']}".encode("utf-8")
).hexdigest()
return CACHE_DIR / f"{digest}.ico"
def _payload_for(colors):
if CACHE_MODE != "disk":
return _render_icon(colors)
cache_path = _cache_path(colors)
with ICON_CACHE_LOCK:
if cache_path.exists():
return cache_path.read_bytes()
payload = _render_icon(colors)
with ICON_CACHE_LOCK:
if cache_path.exists():
return cache_path.read_bytes()
CACHE_DIR.mkdir(parents=True, exist_ok=True)
tmp_path = cache_path.with_suffix(".tmp")
tmp_path.write_bytes(payload)
os.replace(tmp_path, cache_path)
return payload
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
server_version = "favicon-router/1.0" server_version = "favicon-router/1.0"
def _serve(self, include_body): def _serve(self, include_body):
host = _request_host(self.headers) host = _request_host(self.headers)
matched_host, profile = _profile_for_host(host) matched_host, profile = _profile_for_host(host)
print(f"favicon-request host={host!r} matched={matched_host!r} include_body={include_body}")
if not profile: if not profile:
print("favicon-request no-profile")
self.send_error(404, "No favicon mapping for host") self.send_error(404, "No favicon mapping for host")
return return
colors = _colors(profile) colors = _colors(profile)
print(f"favicon-colors bg={colors['bg']} fg={colors['fg']}") payload = _payload_for(colors)
payload = _render_icon(colors)
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "image/x-icon") self.send_header("Content-Type", "image/x-icon")
self.send_header("Content-Length", str(len(payload))) self.send_header("Content-Length", str(len(payload)))
@@ -222,13 +252,13 @@ let
}; };
in { in {
runtime = { runtime = {
# paths = [ paths = [
# { {
# path = "${serverCfg.path.config.path}/favicon"; path = "${serverCfg.path.config.path}/favicon";
# mode = "0755"; mode = "0755";
# dirs = [ "cache" ]; dirs = [ "cache" ];
# } }
# ]; ];
containers = { containers = {
server = builder.mkContainer { server = builder.mkContainer {
@@ -244,7 +274,7 @@ in {
}; };
overrides = { overrides = {
volumes = [ volumes = [
# "${serverCfg.path.config.path}/favicon/cache:/cache" "${serverCfg.path.config.path}/favicon/cache:/cache"
"${mediaCfg.logo.svg}:${logoSvgMount}:ro" "${mediaCfg.logo.svg}:${logoSvgMount}:ro"
]; ];
}; };