4fc8327789
Orchestrator-side: - routers/img_proxy_router.py: 4xx/5xx → 1x1 transparent PNG (eliminates cascade <img onerror>) - static/sport2.html: removed standalone three.min.js (3d-force-graph bundles), bumped to 1.73.4 CC3 (before limit hit): - Logo home link applied to ALL HTML pages (admin.html, admin_users.html, audit.html, crm.html, erp.html, kpi.html, login.html) - Backups in _backups/*.cc3_pre_logo.$ts CC4 R3 (before plan mode): - _backups/r3_cc4/ocr.py.pre_S2.$ts Audit screenshots (80 pages) committed to _audit/audit_20260505_023639/shots/
73 lines
3.0 KiB
Python
73 lines
3.0 KiB
Python
"""Image proxy endpoint za HNS/HBS/external slike - rješava CORS + cache."""
|
|
import hashlib, os, time
|
|
from fastapi import APIRouter, HTTPException, Response
|
|
from fastapi.responses import StreamingResponse
|
|
import requests, redis
|
|
|
|
CACHE_DIR = "/var/cache/pgz-sport-img"
|
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
|
ALLOWED_DOMAINS = ('hns.family', 'hns.hr', 'hbs.hr', 'hrvatski-bocarski-savez.hr',
|
|
'rk-zamet.hr', 'hvs.hr', 'rezultati.hvs.hr', 'sport-pgz.hr')
|
|
MAX_AGE = 86400 * 7 # 7 dana
|
|
|
|
try:
|
|
rds = redis.Redis(host='localhost', port=6379, db=2)
|
|
rds.ping()
|
|
except: rds = None
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/img-proxy")
|
|
def proxy_image(u: str):
|
|
if not u.startswith(('http://', 'https://')):
|
|
raise HTTPException(400, "Invalid URL")
|
|
if not any(d in u for d in ALLOWED_DOMAINS):
|
|
raise HTTPException(403, "Domain not allowed")
|
|
|
|
# Cache key
|
|
h = hashlib.sha1(u.encode()).hexdigest()
|
|
cf = os.path.join(CACHE_DIR, h)
|
|
|
|
# Disk cache check
|
|
if os.path.exists(cf) and (time.time() - os.path.getmtime(cf)) < MAX_AGE:
|
|
with open(cf+'.ct') as f: ct = f.read().strip()
|
|
with open(cf, 'rb') as f: data = f.read()
|
|
return Response(content=data, media_type=ct, headers={
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Cache-Control": "public, max-age=604800",
|
|
"X-Proxy-Cache": "HIT"
|
|
})
|
|
|
|
# Fetch from origin
|
|
try:
|
|
r = requests.get(u, timeout=10, headers={"User-Agent": "RiNET-Civic/1.0"})
|
|
if r.status_code != 200:
|
|
# Graceful fallback: return 1x1 transparent PNG (avoids cascading <img onerror> noise)
|
|
import base64
|
|
TRANS_PNG = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=")
|
|
return Response(content=TRANS_PNG, media_type="image/png", headers={
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Cache-Control": "public, max-age=3600",
|
|
"X-Proxy-Cache": "ORIGIN_4XX",
|
|
"X-Origin-Status": str(r.status_code),
|
|
})
|
|
ct = r.headers.get('content-type', 'image/jpeg')
|
|
# Save to cache
|
|
with open(cf, 'wb') as f: f.write(r.content)
|
|
with open(cf+'.ct', 'w') as f: f.write(ct)
|
|
return Response(content=r.content, media_type=ct, headers={
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Cache-Control": "public, max-age=604800",
|
|
"X-Proxy-Cache": "MISS"
|
|
})
|
|
except requests.RequestException as e:
|
|
# Network error: return 1x1 transparent PNG instead of 502
|
|
import base64
|
|
TRANS_PNG = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=")
|
|
return Response(content=TRANS_PNG, media_type="image/png", headers={
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Cache-Control": "public, max-age=300",
|
|
"X-Proxy-Cache": "ORIGIN_NET_ERROR",
|
|
"X-Origin-Error": str(e)[:100],
|
|
})
|