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:
JiriUhlir
2026-08-27 14:50:10 +02:00
co-authored by Claude Opus 5
parent e3cc8f418b
commit 156289fe2d
48 changed files with 4043 additions and 24 deletions
+55
View File
@@ -0,0 +1,55 @@
# Dokumentace sluzby html-to-pdf
Sluzba prevadi HTML dokument na PDF. Prijme adresu dokumentu nebo HTML primo
v tele requestu a vrati soubor PDF. Zvlada dokumenty o tisicich stranek.
## Obsah dokumentace
- [api.md](api.md) popis endpointu a tel requestu
- [architektura.md](architektura.md) jak sluzba funguje uvnitr
- [konfigurace.md](konfigurace.md) environment variables
- [provoz.md](provoz.md) nasazeni, Docker, znama omezeni
- [zmeny.md](zmeny.md) zaznam zmen
## Aktualni stav
Verze 1.0.0, stav development.
Hotovo:
- synchronni endpoint POST /convert
- asynchronni endpoint POST /jobs se sledovanim stavu a stahovanim vysledku
- dva render enginy, weasyprint a chromium, plus rezim auto
- chunkovani velkych dokumentu a slucovani vysledku
- dvoupruchodovy render obsahu se skutecnymi cisly stranek
- cislovani stranek pres CSS countery nebo pres cislovaci vrstvu
- SSRF ochrana s kontrolou po DNS resolvu a na kazdem presmerovani
- hlaseni nedostupnych assetu v odpovedi jobu
- fronta s omezenym poctem paralelnich workeru
- volitelny callback po dokonceni jobu
- strukturovane JSON logovani s job_id
Neni hotovo a neni ani v zadani:
- autentizace. Sluzba je bez overovani, pristup resi reverse proxy.
Pokud ma byt chranena, je potreba se domluvit na zpusobu, typicky hlavicka
X-Api-Key. Do te doby zadna neni.
- trvala fronta. Restart sluzby znamena ztratu rozpracovanych jobu, ty se
oznaci jako failed s duvodem service_restarted.
## Rychly priklad
```bash
curl -X POST https://services.csbot.cz/apps/html-to-pdf/convert \
-H "Content-Type: application/json" \
-d '{"source": {"url": "https://example.com/dokument.html"}}' \
--output dokument.pdf
```
Pro velky dokument se pouziva asynchronni cesta:
```bash
curl -X POST https://services.csbot.cz/apps/html-to-pdf/jobs \
-H "Content-Type: application/json" \
-d '{"source": {"url": "https://example.com/velky.html"}, "toc": {"enabled": true}}'
```
+245
View File
@@ -0,0 +1,245 @@
# API
Vsechny cesty jsou uvedene relativne. Verejne se volaji s prefixem
`/apps/html-to-pdf`, ktery Caddy pred predanim do containeru odstranuje.
Odpovedi jsou JSON, vyjimkou je stazeni hotoveho PDF.
## POST /convert
Synchronni prevod. Vraci primo `application/pdf`.
Urceno pro mensi dokumenty. Pokud konverze presahne `SYNC_TIMEOUT_SECONDS`
(vychozi 60 s), job se zrusi a sluzba vrati HTTP 413 s odkazem na asynchronni
endpoint. Zruseni se loguje, nikdy nezmizi potichu.
Hlavicky odpovedi:
| Hlavicka | Vyznam |
|---|---|
| `X-Page-Count` | pocet stranek vysledku |
| `X-Engine-Used` | engine, ktery dokument vyrenderoval |
| `X-Missing-Assets` | pocet assetu, ktere se nepodarilo nacist |
| `X-Warnings` | pocet varovani |
Pokud je `X-Missing-Assets` nenulovy, v PDF neco chybi. Detaily jsou dostupne
jen u asynchronni cesty, kde se vraci cely seznam.
## POST /jobs
Zaradi konverzi do fronty. Vraci HTTP 202.
```json
{
"job_id": "1f0e...",
"status": "queued",
"created_at": "2026-08-27T10:00:00Z",
"result_url": "/apps/html-to-pdf/jobs/1f0e.../result"
}
```
## GET /jobs/{job_id}
Stav jobu.
```json
{
"job_id": "1f0e...",
"status": "running",
"created_at": "2026-08-27T10:00:00Z",
"started_at": "2026-08-27T10:00:01Z",
"finished_at": null,
"expires_at": null,
"progress": {
"pages_rendered": 420,
"chunks_done": 9,
"chunks_total": 21,
"pass_number": 1
},
"engine_used": null,
"page_count": null,
"missing_assets": [],
"warnings": [],
"error": null,
"result_url": null
}
```
Stavy: `queued`, `running`, `done`, `failed`, `cancelled`, `expired`.
Pole `pass_number` rozlisuje prvni a druhy pruchod. Druhy pruchod nastava jen
tehdy, kdyz se generuje obsah se skutecnymi cisly stranek, takze u takoveho
dokumentu ukazatel postupu probehne dvakrat.
## GET /jobs/{job_id}/result
Stahne hotove PDF. Soubor se streamuje, nenacita se cely do pameti.
- 404 job neexistuje nebo uz expiroval
- 409 job jeste nedobehl nebo skoncil chybou
## DELETE /jobs/{job_id}
Zrusi bezici job nebo smaze hotovy vysledek.
## GET /health
Stav sluzby, verze, dostupnost enginu a stav fronty. U Chromia se dostupnost
overuje skutecnym nastartovanim prohlizece.
`status` je `ok`, pokud je k dispozici alespon jeden engine, jinak `degraded`.
## GET /version
Nazev aplikace, verze a aktualni `root_path`.
## GET /docs
Swagger UI. OpenAPI dokument obsahuje `servers` s prefixem `/apps/html-to-pdf`,
takze tlacitko Try it out vola spravnou verejnou cestu.
## Telo requestu
Stejne pro `/convert` i `/jobs`. Povinne je pouze `source`, vsechno ostatni ma
pouzitelnou vychozi hodnotu.
```json
{
"source": {
"url": "https://example.com/dokument.html",
"html": null,
"base_url": null
},
"engine": "auto",
"page": {
"format": "A4",
"orientation": "portrait",
"margin": { "top": "20mm", "right": "15mm", "bottom": "20mm", "left": "15mm" }
},
"page_numbers": {
"enabled": false,
"format": "{page} / {pages}",
"position": "bottom-center",
"mode": "auto",
"start_at": 1
},
"toc": { "enabled": false, "depth": 3, "title": "Obsah" },
"outline": true,
"pdf_profile": null,
"assets": { "allow_remote": true, "timeout_seconds": 10 },
"chunking": { "enabled": true, "pages_per_chunk": 50 },
"wait_for": { "state": "load", "selector": null, "timeout_seconds": 30 },
"filename": null,
"callback_url": null
}
```
### source
Vyplnene musi byt prave jedno z poli `url` a `html`, jinak sluzba vraci 422.
`base_url` slouzi k rozpadu relativnich cest a pouziva se hlavne spolu s `html`.
Pri pouziti `url` se `base_url` odvodi z finalni adresy po presmerovanich.
### engine
- `weasyprint` spravne strankovani, nizka pametova narocnost, nespousti JavaScript
- `chromium` zvladne i dokumenty dokreslovane JavaScriptem
- `auto` zvoli chromium, pokud dokument obsahuje aktivni skripty, jinak
weasyprint. Pokud render pres weasyprint selze, sluzba to zaloguje a zopakuje
ho pres chromium, coz se objevi ve `warnings`.
### page.format
Nazev formatu (`A4`, `A5`, `Letter`) nebo explicitni rozmer (`210mm 297mm`).
U nazvu se uplatni i `orientation`, u explicitniho rozmeru je orientace dana
poradim hodnot.
### page_numbers.mode
- `css` cislovani resi CSS countery pri renderu. Nejhezci vysledek, ale funguje
jen kdyz se cely dokument renderuje najednou, protoze v kazde casti se citac
stranek restartuje. Vyzaduje `engine: weasyprint` a `chunking.enabled: false`,
jinak sluzba vraci 400 s kodem `unsupported_combination`.
- `overlay` cisla se dopisi do hotoveho PDF jako pruhledna vrstva. Jedina
moznost u clenenych dokumentu a u Chromia.
- `auto` zvoli `css` u necleneneho dokumentu renderovaneho WeasyPrintem,
jinak `overlay`.
Cislovaci vrstvu kresli WeasyPrint i tehdy, kdyz dokument vyrenderovalo
Chromium. Bez nainstalovaneho WeasyPrintu proto cislovani stranek nefunguje.
### toc
Generuje obsah s odkazy a skutecnymi cisly stranek. Vyzaduje
`engine: weasyprint`, protoze Chromium neumi rict, na ktere strance nadpis
skoncil. Pri jine kombinaci sluzba vraci 400.
Nadpisy bez atributu `id` ho dostanou automaticky.
Pokud je dokument rozdelen na casti, odkazy v obsahu nejsou klikatelne, protoze
cil lezi v jine casti. Cisla stranek jsou spravna. Sluzba na to upozorni ve
`warnings`.
### outline
Zalozky PDF generovane z nadpisu h1 az h6. Vypnuti se resi CSS pravidlem
`bookmark-level: none`, takze funguje i uvnitr jednotlivych casti.
### pdf_profile
Predava se WeasyPrintu jako `pdf_variant`. Povolene hodnoty: `pdf/a-1b`,
`pdf/a-2b`, `pdf/a-3b`, `pdf/a-4b`, `pdf/ua-1`.
### assets
`allow_remote: false` zakaze stahovani externich assetu. Nedostupne assety
render nezastavi, ale objevi se v `missing_assets` a v logu.
### chunking
`pages_per_chunk` je cilova velikost casti. Skutecny pocet stranek je znamy az
po renderu, deleni proto vychazi z odhadu podle mnozstvi textu, obrazku a radku
tabulek. Rez vznika vzdy jen mezi primymi potomky kontejneru, takze tabulka ani
odstavec se nikdy nerozdeli.
Delici body jsou elementy `section`, `article`, `h1`, elementy s atributem
`data-chunk` a elementy se stylem obsahujicim `page-break-before` nebo
`break-before`. Pokud dokument zadny takovy nema, renderuje se vcelku a sluzba
to zaloguje.
### wait_for
Pouziva jen Chromium. `state` je `load`, `domcontentloaded` nebo `networkidle`,
`selector` navic ceka na konkretni element.
### callback_url
Po dokonceni nebo selhani jobu na nej sluzba posle POST se stejnym telem, jake
vraci `GET /jobs/{job_id}`. Neuspech se loguje a nekolikrat zopakuje, job se
kvuli nemu neoznaci jako failed.
## Chyby
```json
{
"error_code": "blocked_target",
"message": "Cilova adresa smeruje do privatniho nebo vyhrazeneho rozsahu a je zablokovana.",
"detail": { "url": "http://127.0.0.1/a.html", "host": "127.0.0.1" }
}
```
| error_code | HTTP | Kdy nastane |
|---|---|---|
| `invalid_request` | 400 | chybny vstup |
| `blocked_target` | 400 | adresa smeruje do zakazaneho rozsahu nebo ma nepovolene schema |
| `unsupported_combination` | 400 | nepodporovana kombinace parametru, napriklad obsah s Chromiem |
| `limit_exceeded` | 400 | prekrocen nakonfigurovany limit |
| `source_unavailable` | 502 | zdrojovy dokument se nepodarilo stahnout |
| `render_timeout` | 504 | render presahl casovy limit |
| `sync_too_long` | 413 | synchronni konverze presahla limit, pouzijte POST /jobs |
| `engine_unavailable` | 503 | pozadovany engine neni k dispozici |
| `queue_full` | 503 | fronta je plna |
| `job_not_found` | 404 | job neexistuje nebo expiroval |
| `result_not_ready` | 409 | job jeste nedobehl |
| `service_restarted` | v tele jobu | sluzba se restartovala drive, nez job dobehl |
| `internal_error` | 500 | neocekavana chyba |
+115
View File
@@ -0,0 +1,115 @@
# Architektura
## Struktura projektu
```text
app/
main.py vytvoreni aplikace, lifespan, obsluha chyb
config.py konfigurace z environment variables
models.py Pydantic modely requestu a odpovedi
errors.py typove chyby a mapovani na HTTP kody
logging_setup.py strukturovane JSON logovani
deps.py kontejner sluzeb sestaveny pri startu
routers/
health.py /health a /version
convert.py synchronni /convert
jobs.py asynchronni /jobs
services/
security.py SSRF kontroly, resolv adres
fetcher.py stahovani dokumentu a assetu, hlaseni vypadku
pipeline.py cely prubeh konverze
jobs.py fronta jobu a workeri
storage.py docasne ulozeni vysledku
engines/
base.py spolecne rozhrani enginu
weasy.py WeasyPrint
chromium.py Playwright a headless Chromium
pdf/
document.py parsovani, nadpisy, obsah
chunker.py deleni HTML na casti
merger.py slucovani PDF
paginator.py cislovaci vrstva
styles.py generovani stylu stranky
```
## Prubeh konverze
1. Nacteni zdroje. U `source.url` se dokument stahne, kazdy hop presmerovani
projde SSRF kontrolou. U `source.html` se pouzije telo requestu.
2. Volba enginu. Rezim `auto` hleda aktivni skripty, jinak bere WeasyPrint.
3. Volba rezimu cislovani stranek. Musi padnout pred injektazi stylu, protoze
styl se lisi podle toho, jestli cisla resi CSS countery nebo cislovaci vrstva.
4. Sestaveni dokumentu. Doplni se `base`, prida se styl stranky, nadpisy dostanou
`id` a pokud je zapnuty obsah, vlozi se jeho zastupna verze.
5. Deleni na casti.
6. Prvni pruchod renderu. Vysledkem jsou hotova PDF casti, jejich pocty stranek
a pozice kotev.
7. Druhy pruchod, pokud se generuje obsah. Zastupna cisla se nahradi skutecnymi.
8. Slouceni casti.
9. Nastampovani cislovaci vrstvy, pokud cisla neresi CSS countery.
## Proc dvoupruchodovy render
Cisla stranek v obsahu nejsou znama drive, nez se dokument vyrenderuje. Zaroven
plati, ze vlozeni obsahu posune cisla stranek, ktera obsah uvadi.
Reseni je vlozit obsah uz v prvnim pruchodu, jen s pevne sirokou vyplni misto
cisel. Rozvrzeni je proto v obou pruchodech stejne a cisla zjistena v prvnim
pruchodu plati i po druhem. Cislo stranky je v tabulce zarovnane doprava ve
sloupci pevne sirky, takze ani jiny pocet cislic rozvrzeni nezmeni.
Pozice nadpisu se ctou z kotev, ktere WeasyPrint hlasi u kazde stranky. To je
presnejsi nez odhad z poradi zalozek.
## Proc se cisla stranek u clenenych dokumentu dopisuji az nakonec
CSS counter `page` se v kazde renderovane casti restartuje od jednicky a
`counter(pages)` zna jen pocet stranek dane casti. Ve sloucenem dokumentu by
proto cislovani bylo nesmyslne.
Cislovaci vrstva je samostatny dokument o stejnem poctu stranek a stejnem
rozmeru, ktery obsahuje jen cisla v okraji. Ten se pres hotove PDF nastampuje
stranku po strance. Rozmer stranky se cte z prvni stranky vysledneho PDF, takze
vrstva sedi i kdyz si dokument nastavil vlastni `@page size`.
Fonty vrstvy kresli WeasyPrint a vklada je do souboru, vysledek proto nezavisi
na fontech v systemu, ktery PDF otevira.
## Pamet
Kriticky bod u tisicistrankovych dokumentu.
- Render probiha po castech, v pameti je vzdy jen jedna cast.
- Chromium se restartuje po `CHROMIUM_RESTART_AFTER_JOBS` jobech, protoze
postupne unika pamet.
- Slucovani pouziva pypdf, ktere drzi stranky vysledku v pameti. U velmi velkych
dokumentu je to nejnarocnejsi krok cele konverze. V image je nainstalovany
i `qpdf`, ktery by slo pouzit jako nahradu, pokud by pamet prestala stacit.
- Vysledek se ke klientovi streamuje, nenacita se cely do pameti.
## Fronta
Fronta je v pameti procesu. Zadna databaze, zadny broker.
- Soubezne bezi `WORKERS` jobu, ostatni cekaji ve fronte.
- Plna fronta vraci 503 s kodem `queue_full`.
- Pri ukonceni sluzby se rozpracovane joby oznaci jako failed s kodem
`service_restarted`. Nezmizi potichu.
- Vysledky se po `JOB_RESULT_TTL_SECONDS` smazou a job se odstrani.
- Pri startu se smazou adresare jobu, ktere po restartu zustaly bez zaznamu.
## Bezpecnost
Sluzba na pozadani stahuje libovolnou adresu, coz je presne tvar SSRF
zranitelnosti.
- Povolena jsou jen schemata `http` a `https`.
- Kontrola probiha az po DNS resolvu, takze verejna domena mirici na 127.0.0.1
neprojde.
- Kontrola se opakuje na kazdem presmerovani.
- Blokovane jsou loopback, privatni rozsahy, link local vcetne 169.254.169.254,
CGNAT, multicast a rezervovane rozsahy.
- U Chromia jde kazdy pozadavek prohlizece pres `context.route`, takze stejnou
kontrolou projdou i assety, ktere si stranka dotahne sama.
- Seznam blokovanych rozsahu jde rozsirit i zuzit konfiguraci. Vychozi stav je
blokovat.
+73
View File
@@ -0,0 +1,73 @@
# Konfigurace
Vsechno se cte z environment variables. AppFactory je predava pres vygenerovany
runtime `.env`. Zadna promenna neni povinna, sluzba nastartuje i bez nich.
## Aplikace
| Promenna | Vychozi | Vyznam |
|---|---|---|
| `APP_NAME` | `html-to-pdf` | nazev v dokumentaci a v odpovedi /version |
| `APP_VERSION` | `1.0.0` | verze |
| `ROOT_PATH` | prazdne | prefix reverse proxy, napriklad `/apps/html-to-pdf` |
| `BASE_PATH` | prazdne | pouzije se, kdyz `ROOT_PATH` neni nastavene |
| `LOG_LEVEL` | `INFO` | uroven logovani |
## Fronta a joby
| Promenna | Vychozi | Vyznam |
|---|---|---|
| `WORKERS` | `2` | pocet soubezne bezicich konverzi |
| `QUEUE_MAX_SIZE` | `100` | kapacita fronty, pri prekroceni se vraci 503 |
| `SYNC_TIMEOUT_SECONDS` | `60` | limit pro POST /convert |
| `JOB_RESULT_TTL_SECONDS` | `3600` | jak dlouho je vysledek k dispozici ke stazeni |
| `STORAGE_DIR` | `/tmp/html-to-pdf` | adresar pro docasne soubory |
## Enginy
| Promenna | Vychozi | Vyznam |
|---|---|---|
| `DEFAULT_ENGINE` | `auto` | engine pouzity, kdyz ho request neuvede |
| `CHROMIUM_ENABLED` | `true` | vypnuti Chromia usetri pamet, ale ztrati podporu JavaScriptu |
| `CHROMIUM_RESTART_AFTER_JOBS` | `50` | po kolika jobech se prohlizec restartuje |
## Sit
| Promenna | Vychozi | Vyznam |
|---|---|---|
| `FETCH_TIMEOUT_SECONDS` | `30` | timeout stazeni zdrojoveho dokumentu |
| `ASSET_TIMEOUT_SECONDS` | `10` | vychozi timeout stazeni jednoho assetu |
| `MAX_REDIRECTS` | `5` | maximalni pocet presmerovani |
## SSRF ochrana
| Promenna | Vychozi | Vyznam |
|---|---|---|
| `SSRF_BLOCK_PRIVATE` | `true` | blokovat loopback, privatni a rezervovane rozsahy |
| `SSRF_EXTRA_BLOCKED_CIDRS` | prazdne | dalsi blokovane rozsahy, oddelene carkou |
| `SSRF_ALLOWED_HOSTS` | prazdne | hostnames, ktere kontrolou neprochazi |
`SSRF_ALLOWED_HOSTS` je urcene pro vyjimky typu interniho generatoru HTML ve
stejne siti. Kazdy zaznam obchazi celou kontrolu, pouzivat opatrne.
Vypnuti `SSRF_BLOCK_PRIVATE` otevre sluzbe cestu do cele vnitrni site. Delat
jen tam, kde to ma duvod.
## Limity
Vsechny limity jsou ve vychozim stavu vypnute. Nula znamena bez limitu.
Pri prekroceni se vraci explicitni chyba `limit_exceeded`, dokument se nikdy
tise neorezava.
| Promenna | Vychozi | Vyznam |
|---|---|---|
| `MAX_PAGES` | `0` | maximalni pocet stranek vysledku |
| `MAX_HTML_BYTES` | `0` | maximalni velikost zdrojoveho HTML |
| `MAX_RENDER_SECONDS` | `0` | maximalni doba jednoho renderu |
## Callback
| Promenna | Vychozi | Vyznam |
|---|---|---|
| `CALLBACK_TIMEOUT_SECONDS` | `15` | timeout jednoho pokusu |
| `CALLBACK_RETRIES` | `3` | pocet pokusu o doruceni |
+100
View File
@@ -0,0 +1,100 @@
# Provoz
## Docker image
Image je dvoufazovy. Prvni faze stavi zavislosti, do vysledneho image se
prekladace nedostanou.
Runtime obsahuje:
- systemove knihovny WeasyPrintu: cairo, pango, gdk-pixbuf, harfbuzz
- Chromium nainstalovany pres `playwright install --with-deps chromium`
- fonty DejaVu, Liberation a Noto vcetne ceske diakritiky
- `qpdf` jako zaloha pro praci s PDF
- `curl` pro healthcheck
Image je velky, radove jednotky GB. To je u sluzby, ktera v sobe ma cely
prohlizec a dve renderovaci knihovny, ocekavane.
## Port
Sluzba posloucha na `0.0.0.0:8000`, coz odpovida `app.yml`. Port se nemeni bez
odpovidajici upravy metadat aplikace v AppFactory.
## Healthcheck
Dockerfile ma `HEALTHCHECK`, ktery vola `/health` na `127.0.0.1:8000`.
Endpoint pri kazdem volani overuje dostupnost enginu vcetne skutecneho
nastartovani Chromia.
## Sdilena pamet pro Chromium
Chromium se spousti s `--disable-dev-shm-usage`, takze si vystaci s malym
`/dev/shm`. Pokud by se v logu objevovaly pady rendereru, je potreba containeru
zvysit `--shm-size`.
## Overeni po nasazeni
```bash
curl -i https://services.csbot.cz/apps/html-to-pdf/health
curl -i https://services.csbot.cz/apps/html-to-pdf/docs
```
Ve Swagger UI overit, ze Try it out vola adresy s prefixem
`/apps/html-to-pdf`, ne bez nej.
Pozor na znamou vlastnost AppFactory: Caddy propousti GET pozadavky jen
z omezeneho seznamu IP adres. Try it out ve Swaggeru proto muze z bezneho
prohlizece vracet 403 jako `text/plain`, i kdyz je sluzba v poradku. Overovat
curlem primo ze serveru.
## Logy
Kazdy zaznam je jeden radek JSON. Vsechno, co patri k jednomu jobu, nese
`job_id`.
Co stoji za sledovani:
- `Asset could not be loaded` v PDF neco chybi
- `WeasyPrint render failed, falling back to Chromium` dokument neprosel
primarnim enginem
- `Restarting Chromium to release memory` bezna udrzba, ne chyba
- `Callback could not be delivered` job dobehl, ale klient se to nedozvedel
- `Job failed` konverze selhala, kod chyby je v poli `error_code`
Secrets se do logu nezapisuji.
## Znama omezeni
- Fronta je v pameti procesu. Restart znamena ztratu rozpracovanych jobu, ty se
oznaci jako failed s kodem `service_restarted`.
- Obsah s cisly stranek umi jen WeasyPrint. Chromium neumi rict, na ktere
strance nadpis skoncil.
- U cleneneho dokumentu nejsou odkazy v obsahu klikatelne, protoze cil lezi
v jine casti souboru. Cisla stranek jsou spravna a sluzba na to upozorni ve
`warnings`.
- Slucovani pres pypdf drzi stranky vysledku v pameti. Je to nejnarocnejsi krok
konverze.
- WeasyPrint nespousti JavaScript. Pro dokumenty dokreslovane skripty je nutne
Chromium.
## Testy
Testy se spousti jen na vyslovne pozadani, nikdy automaticky.
```bash
pip install -r requirements-dev.txt
pytest # bez pomalych testu spusti vse ostatni
pytest -m slow # jen mereni nad dokumentem o zhruba 1200 strankach
```
Testy, ktere potrebuji WeasyPrint, se same preskoci, pokud neni nainstalovany.
Co je pokryte:
- SSRF vcetne domeny mirici na 127.0.0.1
- deleni dokumentu, ktery se delit da, i toho, ktery se delit neda
- spravnost cisel stranek po slouceni
- obsah ukazuje na skutecne stranky
- nedostupny asset render nezastavi a objevi se v odpovedi
- zruseni beziciho jobu, plna fronta, chovani pri ukonceni sluzby
+34
View File
@@ -0,0 +1,34 @@
# Zaznam zmen
## 1.0.0
Prvni implementace sluzby.
Pridano:
- synchronni endpoint `POST /convert` s limitem a odkazem na asynchronni cestu
- asynchronni endpointy `POST /jobs`, `GET /jobs/{id}`,
`GET /jobs/{id}/result`, `DELETE /jobs/{id}`
- `GET /health` s overenim dostupnosti obou enginu a stavem fronty
- engine WeasyPrint jako vychozi, engine Chromium pres Playwright, rezim `auto`
s detekci skriptu a naslednym fallbackem
- deleni dokumentu na casti na strukturalnich hranicich a slucovani vysledku
- dvoupruchodovy render obsahu se skutecnymi cisly stranek
- cislovani stranek pres CSS countery nebo pres cislovaci vrstvu
- zalozky PDF z nadpisu, vypinatelne pres `outline`
- profily PDF/A a PDF/UA pres `pdf_profile`
- SSRF ochrana s kontrolou po DNS resolvu, na kazdem presmerovani a u vsech
pozadavku prohlizece
- hlaseni nedostupnych assetu v odpovedi jobu a v logu
- fronta s omezenym poctem workeru, zruseni jobu, expirace vysledku
- volitelny callback po dokonceni jobu
- strukturovane JSON logovani s `job_id`
- Dockerfile s WeasyPrintem, Chromiem, fonty s ceskou diakritikou a healthcheckem
- testy vcetne fixture o zhruba 1200 strankach
Zamerne neimplementovano:
- autentizace, zpusob neni domluveny
- databaze, Redis ani message broker
- webove UI
- jakekoliv limity zapnute ve vychozim stavu