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 { };
port = 8080;
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);
logoSvgFileName = builtins.baseNameOf (toString mediaCfg.logo.svg);
logoSvgMount = "/assets/${logoSvgFileName}";
@@ -68,7 +76,11 @@ let
]);
serverScript = pkgs.writeText "favicon-server.py" ''
from io import BytesIO
import hashlib
import os
from pathlib import Path
import re
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import cairosvg
@@ -78,7 +90,9 @@ let
LISTEN_HOST = "0.0.0.0"
LISTEN_PORT = ${toString port}
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:
LOGO_BYTES = fh.read()
@@ -86,6 +100,8 @@ let
DEFAULT_PROFILE = ${if defaultProfile == null then "None" else builtins.toJSON defaultProfile}
APP_DOMAIN = (${builtins.toJSON serverCfg.domain} or "").strip().lower()
DEFAULT_COLORS = {"bg": "#111827", "fg": "#f8fafc"}
LOGO_HASH = hashlib.sha256(LOGO_BYTES).hexdigest()
ICON_CACHE_LOCK = threading.Lock()
def _request_host(headers):
host = (
@@ -113,25 +129,19 @@ let
return candidates
def _profile_for_host(host):
candidates = _host_candidates(host)
print(f"favicon-profile host={host!r} candidates={candidates!r} mappings={list(MAPPINGS.keys())!r}")
for candidate in candidates:
for candidate in _host_candidates(host):
profile = MAPPINGS.get(candidate)
print(f"favicon-profile-check candidate={candidate!r} hit={profile is not None}")
if profile:
print(f"favicon-profile-match candidate={candidate!r} profile={profile!r}")
return candidate, profile
print(f"favicon-profile-default host={host!r} default={DEFAULT_PROFILE!r}")
return None, DEFAULT_PROFILE
def _replace_logo_fill(svg, color):
svg, count = re.subn(
svg, _ = re.subn(
"fill:#3193f5",
f"fill:{color}",
svg,
flags=re.IGNORECASE,
)
print(f"favicon-fill replace_count={count} color={color}")
return svg
def _colors(profile):
@@ -154,8 +164,6 @@ let
svg = LOGO_BYTES.decode("utf-8")
svg = _replace_logo_fill(svg, colors["fg"])
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(
bytestring=svg.encode("utf-8"),
@@ -168,21 +176,43 @@ let
rgba.save(output, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)])
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):
server_version = "favicon-router/1.0"
def _serve(self, include_body):
host = _request_host(self.headers)
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:
print("favicon-request no-profile")
self.send_error(404, "No favicon mapping for host")
return
colors = _colors(profile)
print(f"favicon-colors bg={colors['bg']} fg={colors['fg']}")
payload = _render_icon(colors)
payload = _payload_for(colors)
self.send_response(200)
self.send_header("Content-Type", "image/x-icon")
self.send_header("Content-Length", str(len(payload)))
@@ -222,13 +252,13 @@ let
};
in {
runtime = {
# paths = [
# {
# path = "${serverCfg.path.config.path}/favicon";
# mode = "0755";
# dirs = [ "cache" ];
# }
# ];
paths = [
{
path = "${serverCfg.path.config.path}/favicon";
mode = "0755";
dirs = [ "cache" ];
}
];
containers = {
server = builder.mkContainer {
@@ -244,7 +274,7 @@ in {
};
overrides = {
volumes = [
# "${serverCfg.path.config.path}/favicon/cache:/cache"
"${serverCfg.path.config.path}/favicon/cache:/cache"
"${mediaCfg.logo.svg}:${logoSvgMount}:ro"
];
};