Implementace prevodu HTML na PDF
Sluzba prijme adresu HTML dokumentu nebo HTML v tele requestu a vrati PDF. Navrzena pro dokumenty o stovkach az tisicich stranek. Rendering: - WeasyPrint jako vychozi engine, spravne CSS Paged Media, nizka pametova narocnost, bez JavaScriptu - Chromium pres Playwright pro dokumenty dokreslovane skripty - rezim auto s detekci skriptu a fallbackem pri selhani WeasyPrintu Velke dokumenty: - deleni na casti na strukturalnich hranicich, rez nikdy uvnitr tabulky nebo odstavce - dvoupruchodovy render obsahu se skutecnymi cisly stranek, pozice nadpisu se ctou z kotev hlasenych u kazde stranky - cislovani stranek bud pres CSS countery, nebo pres cislovaci vrstvu nastampovanou na hotove PDF, rozmer stranky se cte z vysledneho souboru - Chromium se restartuje po N jobech, nikdy vsak behem beziciho renderu API: - POST /convert synchronne, POST /jobs asynchronne se sledovanim stavu, stahovanim vysledku, rusenim a volitelnym callbackem - GET /health s overenim dostupnosti obou enginu a stavem fronty - OpenAPI respektuje prefix reverse proxy pres root_path Bezpecnost a provoz: - SSRF kontrola po DNS resolvu, na kazdem presmerovani a u vsech pozadavku prohlizece - nedostupne assety render nezastavi, ale hlasi se v odpovedi i v logu - fronta s omezenym poctem workeru, rozpracovane joby se pri ukonceni oznaci jako failed, nezmizi potichu - strukturovane JSON logovani s job_id - vsechny limity vypnute ve vychozim stavu Dockerfile je dvoufazovy, obsahuje zavislosti WeasyPrintu, Chromium a fonty s ceskou diakritikou. Autentizace zamerne neni implementovana, zpusob predavani neni domluveny. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3cc8f418b
commit
156289fe2d
@@ -0,0 +1,79 @@
|
||||
"""Runtime configuration read from environment variables.
|
||||
|
||||
AppFactory injects variables through the generated runtime .env file, so every
|
||||
option below has a safe default and the service starts without any variable set.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
def _bool(name: str, default: bool) -> bool:
|
||||
raw = os.getenv(name)
|
||||
if raw is None or raw.strip() == "":
|
||||
return default
|
||||
return raw.strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def _int(name: str, default: int) -> int:
|
||||
raw = os.getenv(name)
|
||||
if raw is None or raw.strip() == "":
|
||||
return default
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def _list(name: str) -> list[str]:
|
||||
raw = os.getenv(name, "")
|
||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Settings:
|
||||
app_name: str = field(default_factory=lambda: os.getenv("APP_NAME", "html-to-pdf"))
|
||||
app_version: str = field(default_factory=lambda: os.getenv("APP_VERSION", "1.0.0"))
|
||||
root_path: str = field(
|
||||
default_factory=lambda: os.getenv("ROOT_PATH") or os.getenv("BASE_PATH") or ""
|
||||
)
|
||||
log_level: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO").upper())
|
||||
|
||||
# Job queue
|
||||
workers: int = field(default_factory=lambda: _int("WORKERS", 2))
|
||||
queue_max_size: int = field(default_factory=lambda: _int("QUEUE_MAX_SIZE", 100))
|
||||
sync_timeout_seconds: int = field(default_factory=lambda: _int("SYNC_TIMEOUT_SECONDS", 60))
|
||||
job_result_ttl_seconds: int = field(default_factory=lambda: _int("JOB_RESULT_TTL_SECONDS", 3600))
|
||||
storage_dir: str = field(default_factory=lambda: os.getenv("STORAGE_DIR", "/tmp/html-to-pdf"))
|
||||
|
||||
# Engines
|
||||
default_engine: str = field(default_factory=lambda: os.getenv("DEFAULT_ENGINE", "auto"))
|
||||
chromium_enabled: bool = field(default_factory=lambda: _bool("CHROMIUM_ENABLED", True))
|
||||
chromium_restart_after_jobs: int = field(
|
||||
default_factory=lambda: _int("CHROMIUM_RESTART_AFTER_JOBS", 50)
|
||||
)
|
||||
|
||||
# Network
|
||||
fetch_timeout_seconds: int = field(default_factory=lambda: _int("FETCH_TIMEOUT_SECONDS", 30))
|
||||
asset_timeout_seconds: int = field(default_factory=lambda: _int("ASSET_TIMEOUT_SECONDS", 10))
|
||||
max_redirects: int = field(default_factory=lambda: _int("MAX_REDIRECTS", 5))
|
||||
|
||||
# SSRF protection. Blocking is on by default and can be widened or narrowed.
|
||||
ssrf_block_private: bool = field(default_factory=lambda: _bool("SSRF_BLOCK_PRIVATE", True))
|
||||
ssrf_extra_blocked_cidrs: list[str] = field(default_factory=lambda: _list("SSRF_EXTRA_BLOCKED_CIDRS"))
|
||||
ssrf_allowed_hosts: list[str] = field(default_factory=lambda: _list("SSRF_ALLOWED_HOSTS"))
|
||||
|
||||
# Optional limits. Zero means no limit, which is the default on purpose.
|
||||
max_pages: int = field(default_factory=lambda: _int("MAX_PAGES", 0))
|
||||
max_html_bytes: int = field(default_factory=lambda: _int("MAX_HTML_BYTES", 0))
|
||||
max_render_seconds: int = field(default_factory=lambda: _int("MAX_RENDER_SECONDS", 0))
|
||||
|
||||
# Callback delivery
|
||||
callback_timeout_seconds: int = field(default_factory=lambda: _int("CALLBACK_TIMEOUT_SECONDS", 15))
|
||||
callback_retries: int = field(default_factory=lambda: _int("CALLBACK_RETRIES", 3))
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
Reference in New Issue
Block a user