Varianty obrázků#
Z jednoho nahraného obrázku vzniká několik zmenšenin („variant“). Co a jak velké se vyrobí, se čte z databáze a edituje v administraci. Pro správce totéž v Varianty obrázků.
Tabulky a entity#
Migrace sql/2026-10-08_01_system_asset_variants_tables.sql (seed _02, Api RBAC _03,
stránky a Admin RBAC _04; rollbacky _51–_54).
Varianta jen se šířkou a sloupec usage_key přibyly migracemi sql/2026-10-09_01_system_asset_variants_height_zero.sql
(změna CHECKu, rollback _51) a sql/2026-10-09_02_system_images_usage_key.sql (rollback _52) —
viz Varianta jen se šířkou a Záznam obrázku.
| Tabulka | Co je | Důležité |
|---|---|---|
cms_system_asset_variants |
varianta = technické údaje: name, max_width, max_height (0 = bez omezení výšky), resize_mode (fit / cover), watermark, position, activated |
name je UNIQUE a po založení neměnný; UNIQUE také (max_width, max_height, resize_mode, watermark); CHECK chk_asset_variant_dims (max_width >= 1, max_height >= 0) |
cms_system_asset_usages |
použití = dřívější „asset type“ (usage_key, např. blog-articles), module_id, keep_original, position |
zakládá jen migrace; popisek se bere z překladu admin.system.assetUsages.<usage_key> |
cms_system_asset_usage_variants |
které varianty se pro použití vyrábějí: usage_id, variant_id, alias, position |
UNIQUE (usage_id, alias); FK na variantu je RESTRICT, na použití CASCADE |
- Entity:
ImageVariantaImageUsage(SharedMappedSuperclassvapp/Core/Base/Shared/System/Models/Entities/+ vrstvy Api a Admin), enumImageResizeMode(fit=NetteImage::ShrinkOnly,cover=NetteImage::Cover). - Přiřazení (
cms_system_asset_usage_variants) není ORM entita. Čtou se i zapisují raw DBAL vAssetUsageRepositorya ukládají se jednou Api operacísaveAssignments— jedna transakce,FOR UPDATEna řádek použití, DELETE + INSERT. Důvod: kolekce přes ApiBridge mají známé pasti (viz Hydrátory). - Originál není varianta. Je to příznak
keep_originalu použití; složka je vždyoriginala alias nemá. U videa se originál ukládá vždy. - Pravidla (alias
^[a-z0-9-]{1,32}$, jedinečnost aliasu a složky, povinné aliasy podle použití — viz Povinné aliasy podle použití, šířka 1–8000 px, výška 0–8000 px) drží čistá funkceApp\Core\Utils\Helpers\AssetVariantPlan. Admin ji použije k předběžné validaci, autoritou je Api, které validuje znovu.
Varianta jen se šířkou#
max_height = 0 znamená „bez omezení výšky“. Sloupec je smallint unsigned a UNIQUE nulu pouští,
měnil se jen CHECK chk_asset_variant_dims (migrace 2026-10-09_01, opakovatelná:
DROP CONSTRAINT IF EXISTS + ADD; rollback _51 smaže varianty s výškou 0 i jejich přiřazení).
AssetVariantPlan::validateVariant()povolí výšku 0..8000;covers výškou 0 odmítne kódemcoverNeedsHeight(formulář ho mapuje na polemaxHeight). Doporučený název:w1600.VariantRenderer:maxWidth > 0 && maxHeight === 0→resize($w, null, ShrinkOnly)(poměrově, bez zvětšení);0×0zůstává syrová kopie; vodoznak a kontrola obrázkové bomby beze změny.- ⚠️ Změna chování: obrázková knihovna (
system-images) a vlastní plány smaxWidth > 0amaxHeight = 0se nově zmenší podle šířky (dřív syrová kopie). VassetVariants.neontakový řádek není (všech 24 má0/0).
Vykreslení: srcset ze záznamu obrázku#
Responzivní <img srcset sizes> se skládá ze skutečných šířek v záznamu obrázku, ne z
maxWidth (ten je jen horní mez a w deskriptor s nepravdivou šířkou by prohlížeč svedl).
Entita Asset (app/Core/Base/Shared/System/Models/Entities/Asset.php):
| Metoda | Co vrací |
|---|---|
getVariantFiles() |
soubory variant bez disku: folder, variant, alias, width, height, maxWidth, original; width/height jen z meta.variants[folder], bez meta null |
getVariantsSortedByWidth() |
zmenšeniny vzestupně podle skutečné šířky (jinak maxWidth), originál vždy poslední |
getVariantFallbackOrder(?string $wanted) |
složky v pořadí náhrady chybějícího souboru (viz pravidla vykreslení níže); šířku neznámé složky odvodí z názvu WxH nebo wN |
getThumbnailPath() |
cesta náhledu: thumb → nejmenší zmenšenina → originál → plná cesta; video: poster thumb jen když existují postery, jinak soubor videa |
App\Core\Utils\Helpers\AssetSrcset (candidates(), build()) je čistá logika bez disku:
bez originálu, jen se skutečnou šířkou, shodná šířka jen jednou (vyhraje menší maxWidth),
méně než dva kandidáti = prázdný výsledek (vykreslí se prosté <img>); mezera a čárka v URL se kódují.
Front AssetRenderer ({control frontSystemAssetRenderer $asset, …}) má dva parametry:
responsive: true— zapnesrcset, výchozísizes="100vw";sizes: '(min-width: 992px) 33vw, 100vw'— vlastnísizes, zapínásrcseti bezresponsive.
Volání jen s variant: 'thumb' bez sizes (většina výpisů) se nemění: prosté <img>.
Šablona dává <picture><img src srcset sizes></picture> (obal zůstává kvůli stylům šablon,
<source media> zmizely); asset-fallback.js při chybě načtení odebere i srcset a sizes.
Chybějící soubor řeší AssetRenderer ve dvou krocích:
| Krok | Kdo | Co udělá |
|---|---|---|
| server | AssetPlaceholderHelper::exists() |
is_file() nad cestou odvozenou z veřejné URL; chybí-li soubor, vezme další složku podle pořadí náhrady, nakonec zástupný obrázek. Když cestu odvodit nejde, vykreslí normální <img> (existující obrázek se nikdy neschová) |
| klient | data-asset-fallback + asset-fallback.js |
při error (404, zastaralé meta, soubor mimo web server) dosadí do src zástupný obrázek |
Zástupný obrázek je obrázek, ne box s ikonou: prohodí se jen src, takže styly výpisu
(object-fit, hover zoom, výška boxu) platí dál. Video se nenahrazuje: bez souboru se místo
přehrávače vykreslí zástupný <img>. Volání {control frontSystemAssetRenderer null, alt: $name}
dá zástupný obrázek rovnou (položka bez obrázku); v {foreach} volej {multicontrol …}.
Volání bez variant (většina výpisů) míří nejdřív na soubor přímo ve složce assetu, který
zakládalo jen staré nahrávání; dnešní pipeline ukládá jen do složek variant. Když chybí,
zkusí se zmenšeniny vzestupně a originál až jako poslední, takže do malého okénka výpisu
nepoletí plná fotka.
Pravidla vykreslení:
| Téma | Pravidlo |
|---|---|
Originál v srcset |
Originál do srcset nejde (nemá strop velikosti); bez aspoň dvou zmenšenin se známou šířkou je výstup prosté <img>. |
Zapnutí srcset |
srcset jen na vyžádání: responsive: true nebo předané sizes. |
Výchozí sizes |
Výchozí sizes u responsive: true je 100vw. |
src |
src = požadovaná varianta, jinak big, jinak největší zmenšenina. |
width / height |
Na <img> se nedávají width/height (změnilo by rozvržení výpisů; samostatný krok). |
| Chybějící varianta | Náhrada chybějící varianty: požadovaná → nejbližší větší zmenšenina → menší sestupně → originál → zástupný obrázek. |
| Náhled v administraci | Náhled v administraci je složka thumb, jinak nejmenší zmenšenina, jinak originál. |
Varianta bez aliasu (u použití) se vyrobí do složky podle názvu a do srcset se dostane
jako další šířka; povinné aliasy použití (většinou big a thumb) zůstávají povinné.
Čtení: AssetVariantProvider#
App\UI\Api\System\Models\Helpers\AssetVariantProvider::getVariants(string $type) vrací
stejný tvar jako dřív, jen s několika klíči navíc:
array<string /* složka */, array{
maxWidth: int, maxHeight: int, resizeFlags: list<int>, useWatermark: bool,
variant?: string|null, alias?: string|null, original?: bool
}>
- Složka =
alias ?? název varianty; pořadí podleposition,originalvždy poslední (a jen přikeep_original = 1). Neaktivní varianty se vynechají. - Plán se načte jedním dotazem a drží v paměti requestu (žádný Redis, žádné zneplatňování).
assetVariants.neonje už jen výchozí sada a fallback pro 24 použití ostatních modulů. Použije se, když tabulky chybí (kód nasazený před migrací), dotaz selže, nebo použití nemá v DB aktivní přiřazení; provider pak zapíše logassets. Změna rozměrů v neonu se na webu s migrovanou DB neprojeví. Pět použití modulu Systém (Identita webu, Provozovatel, knihovna obrázků) zálohu v neonu nemá — jejich plán čte provider jen z DB.VariantRendereruž provider neinjektuje, dostává přímo%watermarkPath%(jinak by v DI vznikl cyklus).
Výjimky zanikly
Galerie a kategorie Blogu, kategorie Inzerce a Diskuze a produkt/sada v E-shopu dřív četli
rozměry z nastavení modulu (variantFromSettings(), EshopAssetTrait). Nyní všechny čtou
provider pod svým použitím. Do tabulek patří i Identita webu, Provozovatel a
knihovna obrázků, viz Obrázky modulu Systém.
Staré klíče nastavení se už nečtou
productImage*, productThumbnail*, productResizeType*, gallery*, article* a
category.image.* / category.thumb.* nečte žádný kód ani formulář. V tabulce
cms_system_settings zatím zůstávají (mazací migrace nevznikla). Nevracejte je do
formulářů — saveSettingForm() zapisuje všechny vyjmenované klíče.
Záznam obrázku#
Řádek cms_system_images nese ve variants u každé varianty kromě name (= složka),
rozměrů, vodoznaku a resizeFlags také:
{"name":"big","maxWidth":1200,"maxHeight":900,"resizeFlags":[],"useWatermark":false,
"variant":"1200x900","alias":"big"}
variant je název varianty, alias alias (nebo null); originál má "original": true.
Skutečné rozměry souborů zůstávají v meta.variants[<složka>]. Obrázek nahraný před
zavedením tabulek nemá klíč variant — čtenáři s name jako složkou fungují beze změny.
U fit je resizeFlags [] (dřív u některých typů [1]; efektivní chování je stejné).
Sloupec usage_key#
cms_system_images.usage_key (varchar(64) NULL, index idx_system_images_usage_key, bez FK
záměrně; migrace 2026-10-09_02, rollback _52) nese klíč použití, pod kterým byl obrázek
nahrán — příprava pro dogenerování variant. Zapisuje ho AssetProcessingTrait::saveAssetVariants()
z getAssetUsageKey() (výchozí hodnota = getAssetVariantType()); ImageManager ho přetěžuje
a vrací klíč právě zpracovaného použití (viz Obrázky modulu Systém).
V entitě Asset::$usageKey je #[ExternalReadonly] — klient ho nepřepíše. Staré
záznamy mají NULL; doplní je dogenerování, viz níž.
Povinné aliasy podle použití#
Šablony žádají některé složky jménem (<head> favicony, karta provozovatele), proto povinné
aliasy nejsou pro všechna použití stejné. Drží je konstanta
AssetVariantPlan::REQUIRED_ALIASES_BY_USAGE a čte je
AssetVariantPlan::requiredAliasesFor(string $usageKey): list<string>; použití mimo
konstantu dostane REQUIRED_ALIASES (big, thumb).
Použití (usage_key) |
Povinné aliasy |
|---|---|
system-identity-favicon |
icon32, icon180, icon512 |
system-identity-logo |
thumb |
system-operator-photo |
avatar, thumb |
system-images |
thumb |
system-identity-og-image a všechna ostatní |
big, thumb |
Funkci používá validateAssignments() (nepovinný 4. parametr $usageKey; prázdný = big +
thumb, takže staré volání se nemění), AssetUsageManager::saveAssignments(), komponenta
AssetUsageSettings (předběžná validace v adminu), AssetVariantManager::validate() a
AssetVariantFormTrait (variantu s povinným aliasem některého použití nejde vypnout —
chyba requiredInUse). Kód chyby missingAlias nese jméno chybějícího aliasu.
Nové použití s vlastními povinnými aliasy = přidejte řádek do konstanty, jinak se použije
dvojice big + thumb.
Obrázky modulu Systém#
Identita webu, Provozovatel a knihovna obrázků (dřív varianty natvrdo ve formulářových
traitech) berou plán z tabulek stejně jako ostatní použití. Migrace
sql/2026-10-17_01_system_asset_variants_system_usages.sql (rollback _51_…_rollback.sql;
opakovatelná, bez DELIMITER, deploy ji pustí sám) založí pět použití modulu System
(module_id 1), chybějící varianty (dedup podle rozměrů, režimu a vodoznaku), přiřazení
s NOT EXISTS a doplní usage_key existujícím obrázkům.
usage_key |
Popisek v administraci | Výchozí přiřazení (alias: rozměr) |
|---|---|---|
system-identity-favicon |
Identita webu — favicona | icon32: 32×32, icon180: 180×180, icon512: 512×512 (cover) + originál |
system-identity-og-image |
Identita webu — sdílecí obrázek | big: 1200×630 (cover), thumb: 400×210 (cover) + originál |
system-identity-logo |
Identita webu — logo | thumb: 400×200 (fit) + originál |
system-operator-photo |
Provozovatel — fotka | avatar: 240×240 (cover), thumb: 400×400 (fit) + originál |
system-images |
Knihovna obrázků | big: 1200×900, thumb: 400×300 (fit) + originál |
Logo = všechna obrazová pole Identity kromě favicony a sdílecího obrázku.
Plán určuje Api, ne formulář. Admin traity (SiteIdentityFormTrait, OperatorFormTrait,
ImageFormTrait) neposílají varianty; do $meta dají jen usageKey:
App\UI\Api\System\Models\Managers\ImageManager přijme jen klíče z whitelistu
USAGE_KEYS (těch pět výše); jiný, chybějící nebo nestringový klíč spadne na system-images,
takže si klient Api nemůže vymyslet cizí použití. Plán vezme z AssetVariantProvider a klíč
zapíše do cms_system_images.usage_key; při výměně souboru se usage_key kopíruje na
editovaný záznam spolu s variants. Chybí-li plán v DB (pro těchto pět použití není záloha
v neonu), vrátí Result::failure(ResultCode::NOT_AVAILABLE) místo obrázku bez složek. Složku original
už Api nevynucuje — řídí ji keep_original použití jako u ostatních. $meta teče Admin → Api
bez filtru (interní transport); na externí cestě ho filterExternalMeta() zahodí.
Bez zálohy v neonu. config/Shared/assetVariants.neon těchto pět typů neobsahuje —
jediným zdrojem pravdy jsou tabulky variant v DB. Když plán chybí (výpadek tabulek, použití
bez přiřazených variant), nahrání se odmítne: administrace (Identita webu, Provozovatel,
Obrázky) ukáže srozumitelnou hlášku, nic se neuloží a dosavadní obrázek zůstane.
Knihovna obrázků. Zadávání variant u jednotlivého obrázku (Multiplier) skončilo. Nové
nahrání bere plán použití system-images; editace (Image/edit.latte) ukazuje čtecí
přehled variant z Asset::getVariantFiles() a uložení bez nového souboru záznam variants
nemění.
Kontrakt. php tests/Manual/System/asset_variants_system_contract.php porovná efektivní
plán pěti použití s hodnotami natvrdo (tests/Fixtures/asset-variants-system-before.json),
ověří povinné aliasy a whitelist usageKey; výstup 5/5 OK, jinak diff a exit 1. Snímek
24 původních typů hlídá dál asset_variants_contract.php. Jednotkové testy povinných aliasů:
tests/Unit/System/AssetVariantPlanTest.php.
Administrace a RBAC#
- Obrazovka
Nastavení → Varianty obrázků:AssetVariantPresenter(výpis, přidání, úprava, mazání signálemdeleteVariant!(POST,#[Requires(methods: 'POST')]; řádková akce gridu má->setPost(), tlačítko se odešle prázdným formulářem mimogridForm), záložka Použití). - Přiřazení v nastavení modulů (Blog, E-shop, Inzerce, Katalog firem, Diskuze): komponenta
AssetUsageSettings(app/UI/Admin/System/Components/AssetUsageSettingsComponent/) přidaná traitemApp\Core\Traits\Admin\Presenters\AssetUsageSettingsTrait; má vlastní formulář a tlačítko, neukládá se s formulářem nastavení. - Záložka Použití staví jednu komponentu na modul:
AssetVariantPresenter::createComponent()vytváříassetUsageSettings_<moduleId>aAssetUsageSettings::setSection()řídí titulek nad prvním. Jeden společný formulář by měl kolem 1700 polí (29 použití × 19 variant × 3 pole) a překročil by PHPmax_input_vars(výchozí 1000) — odeslání by se tiše zahodilo. Největší formulář je teď kolem 420 polí; nové použití s mnoha variantami hlídejte. - Api presentery
api/system/asset-variant(CRUD) aapi/system/asset-usage(get-assignments,save-assignments,get-regeneration-stats,regenerate-variants,delete-originals,delete-outdated-variants).
| Vrstva | Klíče |
|---|---|
| Admin | :Admin:System:AssetVariant:show, :add, :edit, :del, :regenerate, :deleteOriginals, :deleteOutdated |
| Api — varianty | :Api:System:AssetVariant:get, getAll, getBy, save, delete, updateColumns, deleteAll |
| Api — přiřazení | :Api:System:AssetUsage:getAssignments, :saveAssignments, :getRegenerationStats, :regenerateVariants, :deleteOriginals, :deleteOutdatedVariants |
Přiřazení v nastavení modulu vyžaduje :Admin:<Modul>:Setting:show; záložka Použití
(vč. modulu Firma) :Admin:System:AssetVariant:edit. Api deleteAll varianty je odmítnuté
(obcházelo by kontrolu použití), updateColumns se validuje stejně jako uložení.
Dogenerování a mazání originálů#
Staré obrázky mají jen složky, které plán použití předepisoval při nahrání. Dogenerování je doplní
a umí smazat uložené originály. Migrace sql/2026-10-10_01 (sloupec variants_attempts),
_02 (Api RBAC) a _03 (Admin RBAC); mazání variant mimo nastavení: sql/2026-10-13_01 (Api RBAC deleteOutdatedVariants)
a _02 (Admin RBAC deleteOutdated); rollbacky 2026-10-10_51–_53, 2026-10-13_51–_52; všechny
opakovatelné, deploy je pustí sám. Pro správce:
Staré obrázky: dogenerování.
| Část | Co dělá |
|---|---|
App\Core\Utils\Helpers\AssetVariantBackfill |
čistá logika bez disku a DB: decide() (plán × záznam × soubory na disku → create / reconcile / skipped + důvod tooLarge, noSource, watermarkedSource; stav complete / pending / blocked / exhausted), classify() (tři počty u použití), expectedSize() / staleFolders() / withOverwrite() (přegenerování existujících), outdatedFolders() / withoutFolders() / canDeleteOutdated() (varianty mimo nastavení), applyToRecord(), canDeleteOriginal(), withoutOriginal(), usageKeyFromPath(), likePatterns(); MAX_ATTEMPTS = 3, PROTECTED_FOLDERS |
App\UI\Api\System\Models\Managers\AssetVariantRegenerationManager |
procesní orchestrátor mimo Services/: findStats(), regenerate(?usageId, batch, overwrite, retry), deleteOriginals(usageId), deleteOutdatedVariants(usageId); řádky obrázků jdou raw DBAL přes řetěz AssetManager → AssetService → AssetMapper → AssetRepository, nové složky vyrábí týž VariantRenderer jako nahrání |
AssetUsagePresenter (Api) |
get-regeneration-stats, regenerate-variants (overwrite, retry), delete-originals, delete-outdated-variants; zapisující jen POST (GET = 405), usageId a příznaky přes RequestParamShape (špatná hodnota = 400) |
AssetVariantRegeneratePresenter (Cron) |
/cron/system/asset-variant-regenerate?token=…[&limit=] |
AssetVariantPresenter (Admin) |
signály handleRegenerate(usageId) (pole POST overwrite, retry), handleDeleteOriginals(usageId) a handleDeleteOutdated(usageId) na záložce Použití, všechny #[Requires(methods: 'POST')] a vlastní brána |
Cron#
/cron/system/asset-variant-regenerate?token=…[&limit=25]: nejdřív doplní usage_key
starým obrázkům, pak zpracuje nejvýš limit obrázků na každé použití (výchozí
DEFAULT_BATCH = 25, strop MAX_BATCH = 500). Odpověď nese počty (processed, created,
overwritten, failed, skipped, remaining). Existující soubory cron nepřepisuje
(přepsání existujících je jen akce u jednoho použití v administraci, resp. Api regenerate-variants s overwrite). Po nasazení ho opakujte, dokud remaining není 0; pak
stačí denně v noci. Frekvence je v docblocku presenteru, takže se objeví v
přehledu naplánovaných úloh.
Zámek a dávky#
- Zámek
CronLockManagerse jménemasset.variants.regenerate(TTL 300 s, po každém obrázku se prodlužuje) sdílí cron, tlačítko i mazání originálů, takže dávky nejedou souběžně. Druhá strana dostaneDB_LOCKEDa administrace napíše „už běží". - Rozpočet času jedné dávky je 45 s; mazání originálů zpracuje nejvýš 500 souborů na kliknutí
(
DELETE_BATCH), zbytek vrátí jakoremaining.
Pokusy: variants_attempts#
Sloupec cms_system_images.variants_attempts (tinyint unsigned, #[ExternalReadonly]).
Neúspěch (nečitelný zdroj, výjimka při vykreslení) ho zvýší; po 3 pokusech je obrázek
exhausted a přeskakuje se. Úspěšné zpracování obrázku ho vynuluje (saveVariantRecord(…, resetAttempts: true)),
„nejde dogenerovat" ho nemění. Tlačítko Zkusit znovu (regenerate-variants s retry) pokusy
použití nejdřív vynuluje (resetVariantsAttempts()), pak spustí dávku. attempts > 0 je
sám o sobě stav „Nepodařilo se vygenerovat". Vlastní sloupec je
záměr: thumbnails_attempts patří catch-up cronu videí a u obrázků ho zvyšuje každé nahrání.
Doplnění usage_key z cesty#
Staré záznamy mají usage_key = NULL. Každý běh dávky je doplní jedním UPDATE … CASE WHEN
path LIKE … podle cesty (AssetVariantBackfill::PATH_USAGE, kořeny assetDir z
parameters.neon). Co se nedá určit jednoznačně, zůstane NULL a přeskočí se; dohad se
nezapisuje. ⚠️ Nové použití s vlastním kořenem patří i do PATH_USAGE, jinak jeho staré
obrázky zůstanou bez klíče.
Pravidla dogenerování a mazání#
| Téma | Pravidlo |
|---|---|
| Zdroj | Zdroj dogenerování je originál. Bez něj se z největší existující zmenšeniny bez vodoznaku vyrobí jen varianty s menší nebo stejnou šířkou (výšku neřeší); zdroj s vodoznakem = blocked (watermarkedSource). |
| Použití obrázku | Použití obrázku = usage_key; u NULL se doplní z cesty, nejednoznačné se přeskočí. |
| Doplňování | Jen doplňuje chybějící složky (chybí = soubor není na disku). Existující soubor se nepřepisuje. Složka, jejíž soubor na disku je a záznam ji nezná (přerušený běh), i neznámý original/ se jen zapíše do záznamu (reconcile) spolu s meta z getimagesize. Video a SVG se přeskakují. |
| Dávka | Dávka 25 obrázků na použití; GET_LOCK, 3 pokusy. |
| Smazat originály | „Smazat originály": jen rastr, jen když záznam i disk mají original a big. Nejdřív se zapíše záznam (bez original ve variants i meta), pak se smaže soubor. Použití s keep_original = 1 Api odmítne (INVALID_STATE), UI tlačítko neukáže. Vlastní klíč, jen POST. |
| Počty u použití | Tři čísla u použití — počty obrázků, obrázek může být ve více: nemá aktuální varianty (missing: plán má složku, kterou záznam nezná, soubor není na disku a jde vyrobit), nepodařilo se vygenerovat (failed: záznam složku uvádí a soubor chybí, nebo variants_attempts > 0), nelze dogenerovat (unfixable: soubor chybí a není z čeho — bez originálu je cíl větší než největší zmenšenina bez vodoznaku). Jen chybějící zápis o existujícím souboru se nepočítá nikam. Čistá funkce AssetVariantBackfill::classify(). |
| Přegenerovat i existující | „Přegenerovat i existující" (overwrite): přepíše soubory plánových složek, jejichž skutečné rozměry (z meta, jinak getimagesize) neodpovídají tomu, co by z originálu vzniklo dnes (expectedSize() počítá stejně jako VariantRenderer — Nette Image::calculateSize(), vč. Cover); jen s existujícím originálem, respektuje limit pokusů. Zápis atomicky: render do .regen-<náhodně>-<jméno>.<přípona> ve stejné složce, po vyrenderování všech rename(), finally uklidí dočasné soubory. Aktualizuje variants i meta. Vodoznak se nezkoumá. |
| Varianty mimo nastavení | „Smazat varianty mimo aktuální nastavení": složky, které záznam má a plán použití ne. Nikdy big / thumb / original — chrání se složka i alias (PROTECTED_FOLDERS). Jen rastr a jen s originálem na disku; plán bez zmenšeniny nic neuklízí. Nejdřív záznam (variants i meta přes withoutFolders()), pak soubor + rmdir prázdné složky (sdílí ji ostatní obrázky cesty). Nejvýš 500 obrázků na kliknutí. Vlastní Api i Admin klíč, jen POST. |
RBAC#
| Vrstva | Klíče |
|---|---|
| Api | :Api:System:AssetUsage:getRegenerationStats, :regenerateVariants, :deleteOriginals, :deleteOutdatedVariants (skupiny System:AssetUsage:show, regenerate, deleteOriginals, deleteOutdatedVariants; jen role admin) |
| Admin | :Admin:System:AssetVariant:regenerate, :deleteOriginals, :deleteOutdated (vlastní klíč pro nevratné mazání) |
Po nasazení smažte temp/cache (RBAC). Ověřte, že GET na signály dává 405 a že
NonstandardActionPrivilegeGateTest hlídá bránu i #[Requires(methods: 'POST')].
Formáty nahrávaných souborů#
Které formáty jde nahrát, ví jediné místo: AssetFormatRegistry. Sbírá přípony a MIME typy
z procesorů (AssetFormatProcessor), každý procesor obslouží jeden druh média
(AssetFormatKind). Volající nikdy neuvádí přípony, ale druhy, které v daném místě
připouští.
| Procesor | Druh | Přípony | Co udělá |
|---|---|---|---|
RasterImageProcessor |
Raster |
jpg, jpeg, png, gif, webp (jfif, jpe, jif se přejmenují na jpg) |
ověření ImageUploadPolicy, varianty přes VariantRenderer (resize, vodoznak) |
VectorImageProcessor |
Vector |
svg |
očištění SvgSanitizer, pak tytéž bajty do každé varianty |
VideoProcessor |
Video |
mp4, mov |
ověření obsahu přes ffprobe, originál do original/, varianty jako poster z prvního snímku |
Co kde platí, je v kódu managerů ($kinds před voláním saveAssetVariants()): obrázky Blogu,
Company a e-shopu berou jen Raster, ikony kategorií Inzerce a Katalogu firem Raster +
Vector, administrace „Obrázky“ (ImageManager) všechny tři. Bez zadání bere
AssetProcessingTrait jen Raster (saveAssetVariants(), resolveAssetExtension());
galerie z dropzone bere getAssetFormatKinds(), výchozí Raster + Video.
Varianta bez transformace (maxWidth i maxHeight 0, bez vodoznaku) se zapíše jako syrová
kopie bajtů, ne přes GD. Až když nějaká varianta potřebuje dekódování, ověří se rozměry
z hlavičky proti stropu ImageUploadPolicy. Malý soubor s obrovskými rozměry tak neprojde do GD
a nevyčerpá paměť.
Postup: přidávám nový formát
- Napiš třídu implementující
AssetFormatProcessor:getExtensions(),getMimeTypes(),getKind()aprocess(), který jen zapíše soubory a vrátíAssetProcessingResult. - Zaregistruj ji jako službu v
config/Shared/assetVariants.neona přidej ji doprocessorsslužbyassets.formatRegistry. Jinde se nic neupravuje. - U managerů, kde má formát projít, doplň jeho druh do
$kinds. U SVG navíc formulář administrace potřebujeaddImageUpload(…, allowVector: true), jinak soubor odmítne už validace pole. - Smaž
temp/cache(DI kontejner) a nahraj soubor na každém místě, kde má projít, i tam, kde projít nemá.
Procesor nezakládá Asset ani nesahá do databáze
Řádek obrázku zakládá a ukládá volající v save transakci. Procesor, který zapíše do DB sám, obejde rollback: neúspěšné uložení po sobě nechá řádek, který na nic nepatří.
Dva procesory se stejnou příponou shodí kontejner
Registr při sestavení vyhodí LogicException a padne každá stránka, která nahrávání používá.
Přípony musí být mezi procesory disjunktní.
Neznámá přípona se nepřejmenovává
resolveExtension() vrátí null a volající soubor zahodí. Nezkoušej neznámou příponu
„zachránit“ přejmenováním na .jpg: SVG by se tak uložilo jako .jpg se značkami uvnitř.
SVG#
SVG je XML, které umí spustit skript. Soubory se servírují přímo z www/, takže SVG otevřené
na vlastní adrese běží jako dokument na doméně, kde má administrátor session. Proto ho
SvgSanitizer čistí whitelistem:
| Pravidlo | Proč |
|---|---|
element a atribut mimo ALLOWED_ELEMENTS / ALLOWED_ATTRIBUTES zmizí, on* vždy |
blacklist vždy na něco zapomene |
<image> a <a> v seznamu nejsou |
ikona je nepotřebuje a oba umí odkázat ven |
href / xlink:href jen #kotva |
<use href="#icon"> funguje, odkaz ven ne |
maska DANGEROUS_CSS (url() mimo #, @import, expression(, javascript: …) běží na každý atribut i na <style> |
prezentační atributy (fill, filter) berou url() stejně jako CSS |
| před maskou se rozbalí CSS escapy a vypustí komentáře | @\69 mport čte prohlížeč jako @import |
neplatné XML, kořen jiný než <svg> nebo vnitřní DTD = odmítnuto |
entitní bomby a XXE |
Nebezpečný atribut se odstraní sám, ne celý element: ikona se pak vykreslí s výchozí výplní,
místo aby přišla o tvar. url(#grad) zůstává, exportované ikony ho běžně používají.
SVG se nezmenšuje, ale do každé složky varianty se zkopíruje (VariantRenderer::copyToVariants()):
plán variant se do Asset.variants ukládá vždy a šablony z něj skládají thumb/<jméno>.svg.
Vector jen tam, kde nahrává administrátor
Sanitizace je obrana, ne povolení. Kontext, do kterého nahrává uživatel z frontu (přílohy,
fotky inzerátů), Vector nedostává: obětí uloženého skriptu by byl administrátor, který
soubor otevře.
Video#
Ověření obsahu není best-effort: soubor bez hlavičky ISO BMFF, server bez ffprobe nebo obsah,
který ffprobe nepřijme jako MP4/MOV s video stopou, se odmítne a na disk se nic nezapíše.
Ověřuje se v dočasném souboru mimo webroot a teprve ověřené video se přesune na místo.
Poster je naopak best-effort. Bez ffmpeg nebo při chybě extrakce obrázek vznikne
s thumbnailsGenerated = false a varianty doplní cron
/cron/system/asset-thumbnail-catchup/generate (denně ve 4:00) přes
AssetManager::backfillThumbnails(). Volá tentýž VideoProcessor::renderPosterVariants() jako
nahrání. Bez ffmpeg cron skončí skipped.
Po dvou neúspěšných pokusech cron video vzdá
Každý neúspěch zvýší thumbnails_attempts (i chybějící soubor nebo chybějící plán variant),
a co dosáhlo AssetManager::MAX_ATTEMPTS (2), z výběru vypadne natrvalo. Po opravě serveru
(doinstalovaný ffmpeg) proto vynuluj thumbnails_attempts u videí s
thumbnails_generated = 0, jinak zůstanou bez posteru.
Uvolnění starého obrázku#
Řádek cms_system_images i soubory obrázku po výměně, odebrání nebo smazání majitele
nezmizí samy. Uvolňuje je AssetProcessingTrait a kde to dělá, záleží na situaci:
| Situace | Volání | Kdy běží |
|---|---|---|
| výměna nebo odebrání jednoho obrázku (logo, foto) | releaseAsset($stary, $novyId) |
v afterSave, uvnitř transakce, až po úspěšném updateColumn() cizího klíče |
| obrázky vypadlé z galerie nebo formuláře | releaseDroppedAssets($predtim, $ponechane) |
v afterSave, po zápisu relací a hlavního obrázku |
smazání entity přes delete() |
completedAssetCleanup() |
po commitu, sám z DeleteTrait |
smazání podstromu přes deleteNode() |
captureOwnedAssetIds() před mazáním, releaseOwnedAssets() po něm |
sám z TreeTrait |
měkké smazání (deleted = 1) |
nic | záznam jde vrátit, obrázek musí zůstat |
Výměna běží v transakci schválně: smazání řádku je atomické s přepnutím cizího klíče a kontrola vazeb běží na stejném spojení, takže vidí právě zapsaný klíč. Smazání entity naopak běží až po commitu, protože uvnitř transakce by cizí klíč na obrázek ještě žil.
O tom, jestli se obrázek smí smazat, nerozhoduje trait, ale
AssetManager::releaseAssetRow(). Vazby na obrázek hledá přes information_schema
(všechny sloupce s cizím klíčem na cms_system_images), a když na obrázek ještě něco ukazuje
nebo si není jistý, vrátí null a obrázek zůstane i se soubory.
Postup: manager s obrázky, které se mají uvolnit po smazání
- Manager používá
AssetProcessingTrait. - Definuj
ownedAssetIds(object $entity): array: vrať cizí klíče obrázků, které entita vlastní (id, entituAssetnebonull). VzorBlog\…\NewsManager::ownedAssetIds(). - Nic dalšího.
DeleteTraitiTreeTraitse podle téhle metody rozjedou samy. Výjimkou jsou obrázky ve vazební tabulce (galerie, úvodní panel), viz varování níž. - Ověř: smaž záznam a zkontroluj, že řádek v
cms_system_imagesi soubory zmizely, a že sdílený obrázek zůstal.
Obrázky z vazební tabulky zachyť v beforeDelete()
ownedAssetIds() běží až po commitu, kdy kaskáda (cms_mod_blog_gallery_image_relations,
cms_mod_blog_intro_image_relations) vazby už smazala. Přednačtená entita z DeleteTrait
kolekci vazeb nenese: je líná a UoW se po čtení čistí. beforeDelete() proto entitu načte
znovu s includem (find($id, ['assetRels'])), id obrázků uloží do vlastnosti manageru
a ownedAssetIds() je odtud jen vrátí (Blog\…\GalleryManager, IntroManager). Když
ownedAssetIds() čte kolekci z předané entity, vrátí prázdno a řádky i soubory všech
obrázků zůstanou osiřelé.
releaseAsset() až po úspěšném zápisu cizího klíče
Zavolaný dřív smaže obrázek, na který záznam pořád ukazuje: když potom cokoli selže,
databáze odkazuje na řádek, který už neexistuje. Ze stejného důvodu kanály s jedním
obrázkem staré soubory jen naskenují (listAssetFilesIn()) a smažou je až po úspěchu
(deleteAssetFiles()).
releaseAsset() vrátí false u sdíleného obrázku
Pak nemaž ani staré soubory ze složky ($stalePaths): patří obrázku, na který ukazuje
jiný záznam.
Rollback o souborech neví
Varianty nového obrázku se zapisují na disk uvnitř transakce. Když uložení skončí
neúspěchem, smaž na neúspěšné cestě, co jsi zapsal:
deleteAssetFiles($this->assetVariantFilePaths($absBase, $fileName, $variantConfigs)).
Hledá se podle náhodného fileName, cizí obrázek ve stejné složce se nezasáhne.
Úklid je fail-soft: chyba se jen zapíše do logu assets a uložení ani smazání nespadne.
Osiřelý řádek je menší vada než rozbitá editace.
Limit velikosti nahrávaného obrázku#
App\Core\Security\Upload\ImageUploadSizeLimit::bytes() určuje horní mez pro nahrávač
obrázků inzerce a katalogu firem (front i admin): maxSize parametru nahrávače (MB) >
nastavení modulu maxImageSize (MB) > 50 MB. Nahrávač souborů zůstává beze změny.