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,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}}'
|
||||
```
|
||||
@@ -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 |
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user