fix favicon
This commit is contained in:
@@ -5,7 +5,6 @@ let
|
||||
palette = serverCfg.colorScheme.palette or { };
|
||||
port = 8080;
|
||||
assetSize = 64;
|
||||
cacheControl = containerCfg.extra.cacheControl or "public, max-age=86400";
|
||||
priority = toString (containerCfg.extra.priority or 2147482647);
|
||||
logoSvgFileName = builtins.baseNameOf (toString mediaCfg.logo.svg);
|
||||
logoSvgMount = "/assets/${logoSvgFileName}";
|
||||
@@ -64,46 +63,29 @@ let
|
||||
+ "|apple-touch-icon(-precomposed)?\\.png"
|
||||
+ "|android-chrome-[0-9]+x[0-9]+\\.png"
|
||||
+ "|mstile-[0-9]+x[0-9]+\\.png)$";
|
||||
configFile = pkgs.writeText "favicon-config.json" (builtins.toJSON {
|
||||
inherit cacheControl;
|
||||
inherit borderRadius;
|
||||
domain = serverCfg.domain;
|
||||
mappings = hostMappings;
|
||||
default = defaultProfile;
|
||||
});
|
||||
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
||||
cairosvg
|
||||
pillow
|
||||
]);
|
||||
serverScript = pkgs.writeText "favicon-server.py" ''
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
import cairosvg
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
CONFIG_PATH = os.environ.get("FAVICON_CONFIG", "/config/config.json")
|
||||
LOGO_PATH = os.environ.get("FAVICON_LOGO", "/assets/logo.svg")
|
||||
CACHE_DIR = Path(os.environ.get("FAVICON_CACHE_DIR", "/cache"))
|
||||
LISTEN_HOST = os.environ.get("FAVICON_LISTEN_HOST", "0.0.0.0")
|
||||
LISTEN_PORT = int(os.environ.get("FAVICON_PORT", "8080"))
|
||||
ASSET_SIZE = int(os.environ.get("FAVICON_ASSET_SIZE", "${toString assetSize}"))
|
||||
DEFAULT_CACHE_CONTROL = "public, max-age=86400"
|
||||
LOGO_PATH = ${builtins.toJSON logoSvgMount}
|
||||
LISTEN_HOST = "0.0.0.0"
|
||||
LISTEN_PORT = ${toString port}
|
||||
ASSET_SIZE = ${toString assetSize}
|
||||
CACHE_CONTROL = "no-store"
|
||||
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as fh:
|
||||
APP_CONFIG = json.load(fh)
|
||||
with open(LOGO_PATH, "rb") as fh:
|
||||
LOGO_BYTES = fh.read()
|
||||
MAPPINGS = APP_CONFIG.get("mappings", {})
|
||||
DEFAULT_PROFILE = APP_CONFIG.get("default")
|
||||
APP_DOMAIN = (APP_CONFIG.get("domain", "") or "").strip().lower()
|
||||
CACHE_CONTROL = APP_CONFIG.get("cacheControl", DEFAULT_CACHE_CONTROL)
|
||||
LOGO_HASH = hashlib.sha256(LOGO_BYTES).hexdigest()
|
||||
MAPPINGS = ${builtins.toJSON hostMappings}
|
||||
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"}
|
||||
|
||||
def _request_host(headers):
|
||||
@@ -138,30 +120,15 @@ let
|
||||
return candidate, profile
|
||||
return None, DEFAULT_PROFILE
|
||||
|
||||
def _replace_svg_color(svg, attribute, color):
|
||||
if attribute not in {"fill", "stroke"}:
|
||||
return svg
|
||||
|
||||
svg = re.sub(
|
||||
rf'({attribute}\\s*=\\s*")(?!none\\b)[^"]*(")',
|
||||
rf"\\1{color}\\2",
|
||||
svg,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
svg = re.sub(
|
||||
rf"({attribute}\\s*=\\s*')(?!none\\b)[^']*(')",
|
||||
rf"\\1{color}\\2",
|
||||
svg,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
def _replace_logo_fill(svg, color):
|
||||
return re.sub(
|
||||
rf"({attribute}\\s*:\\s*)(?!none\\b)[^;\"']+",
|
||||
rf"\\1{color}",
|
||||
r"fill:\\s*#3193f5\\b",
|
||||
f"fill:{color}",
|
||||
svg,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
border_radius = str(APP_CONFIG.get("borderRadius", "8")).strip()
|
||||
border_radius = str(${builtins.toJSON borderRadius}).strip()
|
||||
if not border_radius.endswith("px"):
|
||||
border_radius = f"{border_radius}px"
|
||||
border_radius_px = max(0, int(float(border_radius[:-2])))
|
||||
@@ -173,34 +140,17 @@ let
|
||||
"fg": profile.get("fg") or profile.get("foreground") or DEFAULT_COLORS["fg"],
|
||||
}
|
||||
|
||||
def _asset_path(host, colors):
|
||||
payload = json.dumps(
|
||||
{
|
||||
"asset_size": ASSET_SIZE,
|
||||
"bg": colors["bg"],
|
||||
"border_radius": border_radius,
|
||||
"fg": colors["fg"],
|
||||
"host": host,
|
||||
"logo_hash": LOGO_HASH,
|
||||
},
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
safe_host = re.sub(r"[^a-z0-9.-]+", "_", host or "default")
|
||||
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
|
||||
return CACHE_DIR / f"{safe_host}-{digest}.ico"
|
||||
|
||||
def _render_icon(colors, target):
|
||||
def _render_icon(colors):
|
||||
svg = LOGO_BYTES.decode("utf-8")
|
||||
for attribute in ("fill", "stroke"):
|
||||
svg = _replace_svg_color(svg, attribute, colors["fg"])
|
||||
svg = _replace_logo_fill(svg, colors["fg"])
|
||||
print(f"favicon-render fg={colors['fg']} bg={colors['bg']}")
|
||||
|
||||
logo_png = cairosvg.svg2png(
|
||||
bytestring=svg.encode("utf-8"),
|
||||
output_width=ASSET_SIZE,
|
||||
output_height=ASSET_SIZE,
|
||||
)
|
||||
tmp_target = target.with_suffix(".tmp")
|
||||
output = BytesIO()
|
||||
with Image.new("RGBA", (ASSET_SIZE, ASSET_SIZE), (0, 0, 0, 0)) as canvas:
|
||||
ImageDraw.Draw(canvas).rounded_rectangle(
|
||||
(0, 0, ASSET_SIZE, ASSET_SIZE),
|
||||
@@ -209,8 +159,8 @@ let
|
||||
)
|
||||
with Image.open(BytesIO(logo_png)) as logo:
|
||||
canvas.alpha_composite(logo.convert("RGBA"))
|
||||
canvas.save(tmp_target, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)])
|
||||
os.replace(tmp_target, target)
|
||||
canvas.save(output, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)])
|
||||
return output.getvalue()
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "favicon-router/1.0"
|
||||
@@ -218,31 +168,19 @@ let
|
||||
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)
|
||||
asset_path = _asset_path(host, colors)
|
||||
if not asset_path.exists():
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
_render_icon(colors, asset_path)
|
||||
etag = f'"{asset_path.stem.rsplit("-", 1)[-1]}"'
|
||||
if self.headers.get("If-None-Match") == etag:
|
||||
self.send_response(304)
|
||||
self.send_header("ETag", etag)
|
||||
self.send_header("Cache-Control", CACHE_CONTROL)
|
||||
self.send_header("X-Favicon-Host", host or "default")
|
||||
self.send_header("X-Favicon-Mapping", matched_host or "default")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
payload = asset_path.read_bytes()
|
||||
print(f"favicon-colors bg={colors['bg']} fg={colors['fg']}")
|
||||
payload = _render_icon(colors)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "image/x-icon")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.send_header("Cache-Control", CACHE_CONTROL)
|
||||
self.send_header("ETag", etag)
|
||||
self.send_header("X-Favicon-Host", host or "default")
|
||||
self.send_header("X-Favicon-Mapping", matched_host or "default")
|
||||
self.end_headers()
|
||||
@@ -278,25 +216,18 @@ 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 {
|
||||
imageStream = image;
|
||||
port = port;
|
||||
extraEnv = {
|
||||
FAVICON_CONFIG = "/config/config.json";
|
||||
FAVICON_LOGO = logoSvgMount;
|
||||
FAVICON_CACHE_DIR = "/cache";
|
||||
FAVICON_PORT = toString port;
|
||||
FAVICON_ASSET_SIZE = toString assetSize;
|
||||
};
|
||||
extraLabels = {
|
||||
"traefik.enable" = "true";
|
||||
"traefik.http.routers.${name}.entrypoints" = "web-secure";
|
||||
@@ -307,8 +238,7 @@ in {
|
||||
};
|
||||
overrides = {
|
||||
volumes = [
|
||||
"${configFile}:/config/config.json:ro"
|
||||
"${serverCfg.path.config.path}/favicon/cache:/cache"
|
||||
# "${serverCfg.path.config.path}/favicon/cache:/cache"
|
||||
"${mediaCfg.logo.svg}:${logoSvgMount}:ro"
|
||||
];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user