From 44b7c5858ceed8af3f1854eed87b29a88f374e64 Mon Sep 17 00:00:00 2001 From: soraefir Date: Tue, 9 Jun 2026 00:18:17 +0200 Subject: [PATCH] fixed --- modules/server/containers/apps/favicon.nix | 76 +++++++++++++++------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/modules/server/containers/apps/favicon.nix b/modules/server/containers/apps/favicon.nix index 3f4822e..3d83bec 100644 --- a/modules/server/containers/apps/favicon.nix +++ b/modules/server/containers/apps/favicon.nix @@ -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" ]; };