From 9e3ee75bd4d222d908178bf77b9ffba90da49775 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 7 Jul 2026 09:17:34 +0000 Subject: [PATCH] fixes for double nginx forward scenarios --- .../app/__pycache__/config.cpython-312.pyc | Bin 2243 -> 2367 bytes .../__pycache__/theme_export.cpython-312.pyc | Bin 6390 -> 13897 bytes backend/app/config.py | 5 + backend/app/theme_export.py | 151 +++++++++++++++++- docker-compose.yml | 1 + 5 files changed, 154 insertions(+), 3 deletions(-) diff --git a/backend/app/__pycache__/config.cpython-312.pyc b/backend/app/__pycache__/config.cpython-312.pyc index 290075e6c6266a4343ddd849ee69368dff9c7f9f..e68e6b6abbaaa34a84a79084b330c50e8b4cbfbb 100644 GIT binary patch delta 279 zcmX>sxL=6xG%qg~0}ych_s-&HpU79sIBR21C8JuDLMlrtYl^-NP%ecdN)gO60P_q} zj4~Oclq!uiO*Zp0Jz%!064fs)F48Z~NGwX#&r8qDtI*FV$<4VX5FFwd;^`dk=IP@a z9Pi>8bW0d25*iR366EUW8y_0vGxs$?hD+p86oI1|Y%^NZjJE z$<0qG%}KQ@G68ZKfw))>NPJ*sWMsU{p!J1KgHgMq<_iOm`WVK@z$4Oa)yM(X4**{a BOFaMp delta 156 zcmdllbXbt@G%qg~0}!zDduMU6P2{U(tlQXA$;g$WpUD`dP-&oPxLKR&0khaGuGGA; z_=2L;w9JZItlqvMe(`>qij(8n4o%i(4`S5VT*t1+$n!u%VS)MuVXa1=BE89nIgF)r zfkqeUfe3vdaf`zyH$SB`C)KXVXtDsOn4~hJc1O(@1|ap(hmnCtq}!^I1FReXWcey+ diff --git a/backend/app/__pycache__/theme_export.cpython-312.pyc b/backend/app/__pycache__/theme_export.cpython-312.pyc index 280ef73615531611e95f93f153c6f39b13752bb4..06d0e0888c7d43a65b8a9083bb15cf57fd17bf57 100644 GIT binary patch literal 13897 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 AO8@`> delta 1397 zcmZWpU2GIp6ux(Mes;Tm(?Yjf3)}4vt;T_uTo?`E@w;RWjLvVARvkOmqE0D%)5E_CB(afoyC`4mNN&ONIn<#EG~OBjU=2 zjD=rz6jwFW5G#)6MvbT&Gh!iEofbE4#6zq(2{&mZLmYKdZrVtPIOgbXtI>*)gorfV z52+jCH2)f8-KThs8td&A(r(8=5+Kr?hR~#HCr`^p#!itmSYxD}L?@+ASTHHuY4CLX z$12%6ShN4Lwt}_uxKwD9tU-1fUt_1F8#?C6@xTfyUUk%~PFBjz!;x8x3+y}jk$eJj z&?G1>AGLxhE>BoK@ntaNVf?<1J*NyR9Y8zSYsyP(peu(bnqys;aI}JK?F5?cWeXeI zl_;S85XAru2-F13@X@?bJwo*g)d#BiO;0DD)#l9R7;c`~GK^!?$ytIT#vknX7N=ha3L}G zJ^O3xDEngD*X>+8YEe=Rd>$qBiXWKXkzXYC$<*E4T>=a#iX z%+g9g{wc(??^Wpx1d3w#e}MWKt)4`8b=KVevZm+fGvDg>v(5+Jnu;L3d-Z(deBDiEC*Rcfwg6e&fWfht z!vg2k;)B}a04^31i-VfLLt1g9nH#>28|xtfeN`w^sE0v4d!QgwU$D?AOkDOWbEJ@< ziZF|Yp+qPV#Up+|AprWFP7AI$o@M(yMu`P&KK4)mmr(W=+VB(VUP1#)sGl7h8F_LWRxiG? zj9|JPER0@TxA4@JH&z5)K8(9?>%2Taez|Y{(52K00=m4r4dVMR`tzd~>mLl=ML@&T F{0){cZI}Q6 diff --git a/backend/app/config.py b/backend/app/config.py index 01d6180..2755cc8 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -35,6 +35,11 @@ class Settings(BaseSettings): # Theme export WEBSITE_URL: str = "https://kmountainflower.org" THEME_JSON_PATH: str = "/app/theme.json" + STATIC_FILES_DIR: str = "/usr/share/nginx/html" + # Internal URL for fetching static files when they're not on the local + # filesystem (e.g. separate Docker/K8s containers). Falls back to + # WEBSITE_URL if not set. + STATIC_UPSTREAM_URL: str = "" model_config = {"env_prefix": "KMTN_"} diff --git a/backend/app/theme_export.py b/backend/app/theme_export.py index 4c615fc..2bcace6 100644 --- a/backend/app/theme_export.py +++ b/backend/app/theme_export.py @@ -2,19 +2,22 @@ This module provides functions to: - Build a clean, consumer-friendly JSON structure from StationConfig. +- Embed actual image bytes as base64 data URIs for offline build use. - Write it atomically to disk (for scp by build machines). - Check whether the file exists and return its metadata. """ +import base64 import json import logging +import re from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict from app.config import settings from app.database import async_session -from app.models import StationConfig +from app.models import StationConfig, StorageBlob from sqlalchemy import select logger = logging.getLogger(__name__) @@ -35,8 +38,107 @@ def resolve_image_url(base_url: str, relative_url: str) -> str | None: return f"{base}/{path}" +def _detect_mime_from_extension(path: str) -> str: + """Guess MIME type from file extension.""" + ext = Path(path).suffix.lower() + return { + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".gif": "image/gif", + ".ico": "image/x-icon", + ".avif": "image/avif", + }.get(ext, "application/octet-stream") + + +async def fetch_image_bytes(url: str) -> tuple[bytes, str] | None: + """Fetch image bytes from any source the station config uses. + + Resolves three URL patterns: + - /api/storage/{blob_id} → read from StorageBlob in DB + - Relative paths (e.g. svg/logo.svg) → read from STATIC_FILES_DIR on disk + - Absolute HTTP(S) URLs → fetch via httpx + + Returns (raw_bytes, mime_type) or None on failure. + """ + if not url: + return None + + # Database blob + blob_match = re.match(r"^/api/storage/([a-f0-9]+)$", url) + if blob_match: + blob_id = blob_match.group(1) + async with async_session() as blob_session: + result = await blob_session.execute( + select(StorageBlob).where(StorageBlob.blob_id == blob_id) + ) + blob = result.scalar_one_or_none() + if blob: + return (blob.data, blob.content_type) + return None + + # Static file on disk or internal upstream HTTP + if not url.startswith(("http://", "https://")): + # Try local filesystem first + candidates: list[Path] = [Path(settings.STATIC_FILES_DIR)] + # Dev fallback: if the configured path (production nginx root) doesn't + # contain the file, try common Angular dist/public locations. + if not (Path(settings.STATIC_FILES_DIR) / url.lstrip("/")).is_file(): + candidates.extend([ + Path(__file__).resolve().parents[2] / "dist" / "radio-station" / "browser", + Path(__file__).resolve().parents[2] / "public", + ]) + + for static_dir in candidates: + if not static_dir.is_dir(): + continue + file_path = (static_dir / url.lstrip("/")).resolve() + # Safety: must be inside static_dir + try: + file_path.relative_to(static_dir.resolve()) + except ValueError: + continue + if file_path.is_file(): + data = file_path.read_bytes() + mime = _detect_mime_from_extension(url) + return (data, mime) + + # Not on local disk — try internal upstream (Docker/K8s) or public URL + upstream_base = settings.STATIC_UPSTREAM_URL or settings.WEBSITE_URL + fetch_url = f"{upstream_base.rstrip('/')}/{url.lstrip('/')}" + + try: + import httpx + async with httpx.AsyncClient() as client: + resp = await client.get(fetch_url, follow_redirects=True, timeout=10) + if resp.status_code == 200: + content_type = resp.headers.get("content-type", "application/octet-stream") + return (resp.content, content_type) + except Exception as e: + logger.warning("Failed to fetch image %s from %s: %s", url, fetch_url, e) + return None + + # Remote HTTP fetch + try: + import httpx + async with httpx.AsyncClient() as client: + resp = await client.get(url, follow_redirects=True, timeout=10) + if resp.status_code == 200: + content_type = resp.headers.get("content-type", "application/octet-stream") + return (resp.content, content_type) + except Exception as e: + logger.warning("Failed to fetch remote image %s: %s", url, e) + return None + + def build_theme_dict(config: StationConfig) -> Dict[str, Any]: - """Build the theme.json dict from a StationConfig ORM object.""" + """Build the theme.json dict from a StationConfig ORM object. + + Returns the theme structure with URL-based images. For embedded + (base64) images, use build_theme_with_embedded_images() instead. + """ base_url = settings.WEBSITE_URL images: Dict[str, str] = {} @@ -60,6 +162,10 @@ def build_theme_dict(config: StationConfig) -> Dict[str, Any]: "tagline": config.tagline, "frequency": config.frequency, }, + "streams": { + "url": config.stream_url or None, + "metadata_url": config.stream_metadata_url or None, + }, "colors": { "primary": config.color_primary, "primary_light": config.color_primary_light, @@ -83,9 +189,48 @@ def build_theme_dict(config: StationConfig) -> Dict[str, Any]: } +async def build_theme_with_embedded_images( + config: StationConfig, +) -> Dict[str, Any]: + """Build theme dict with images embedded as base64 data URIs. + + Fetches actual image bytes from static files, database blobs, or + remote URLs. Failed fetches are logged and omitted (graceful). + The original URL-based `images` section is kept for reference. + """ + theme = build_theme_dict(config) + + images_embedded: Dict[str, Dict[str, str]] = {} + image_fields = [ + ("logo", config.logo_url), + ("hero_background", config.hero_background_url), + ("hero_icon", config.hero_icon_url), + ("hero_divider", config.hero_divider_url), + ] + + for key, url in image_fields: + result = await fetch_image_bytes(url) + if result is None: + logger.warning("Could not fetch image for %s (%s) — skipping embed", key, url) + continue + data, mime_type = result + b64 = base64.b64encode(data).decode("ascii") + images_embedded[key] = { + "content_type": mime_type, + "size": len(data), + "data": b64, + } + + theme["images_embedded"] = images_embedded + return theme + + async def write_theme_json() -> Dict[str, Any]: """Read the current station config and write theme.json to disk. + Fetches actual image bytes and embeds them as base64 data URIs + in an `images_embedded` section for offline build use. + Uses atomic write (write to .tmp, then os.replace) to prevent reading a partially-written file. @@ -99,7 +244,7 @@ async def write_theme_json() -> Dict[str, Any]: if config is None: raise RuntimeError("Station config not found in database") - theme = build_theme_dict(config) + theme = await build_theme_with_embedded_images(config) # Atomic write path = Path(settings.THEME_JSON_PATH) diff --git a/docker-compose.yml b/docker-compose.yml index 378a548..354f7cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,7 @@ services: KMTN_CORS_ORIGINS: '["http://localhost:4200", "http://localhost"]' KMTN_ADMIN_USERNAME : "admin" KMTN_ADMIN_PASSWORD : "123" + KMTN_STATIC_UPSTREAM_URL: http://frontend:80 ports: - "8000:8000" volumes: