Motyw
Media: adresy plików i cache miniatur
Pliki z dysku publicznego mają adresy, które serwer HTTP obsługuje statycznie, bez PHP.
| Co | Adres | Kto serwuje |
|---|---|---|
| Oryginał, dysk publiczny | /storage/2024/foo.jpg | serwer HTTP przez storage:link |
| Wariant, dysk publiczny | /media/widen_640/2024/foo.jpg | Laravel raz, potem serwer HTTP |
| Dowolny plik, dysk prywatny | /api/media/c/private/fp/2024/foo.jpg?signature=… | zawsze Laravel |
Wygenerowany wariant jest zapisywany w public/ pod ścieżką identyczną z adresem żądania:
text
GET /media/widen_640/2024/foo.jpg
1. Laravel generuje wariant i zapisuje public/media/widen_640/2024/foo.jpg
2. Apache/nginx serwuje ten plik statycznieDziała to, bo public/.htaccess kieruje do index.php tylko nieistniejące pliki, a na nginksie robi to samo standardowe try_files $uri $uri/ /index.php?$query_string;.
Konwersje
Drugi segment adresu to lista konwersji: nazwa z media.available_image_conversions i jej argumenty rozdzielone _, kolejne konwersje rozdzielone ,.
text
/media/crop_800_450_10_20,fit_400_300_center/2024/foo.jpgArgumenty muszą pasować do media.available_image_conversion_values. Nieznana konwersja albo niedozwolony argument kończy się odpowiedzią 404 i niczego nie zapisuje. Podstawy konwersji opisuje Media.
Trasa API dla dysków prywatnych (/api/media/c/{disk}/{konwersje}/fp/{ścieżka}) przyjmuje tę samą składnię.
Format docelowy
Format docelowy jest sufiksem ścieżki, nie konwersją: /media/widen_640/2024/foo.jpg.webp. Middleware ResolveConversionFormat rozpoznaje sufiks z media.conversion_formats, gdy plik o pełnej nazwie nie istnieje, a istnieje źródło. Dzięki temu wariant w cache'u ma rozszerzenie zgodne z treścią i serwer statyczny wysyła poprawny Content-Type.
Adresy w zasobach API
Klient nie skleja adresów sam. Model File zwraca je gotowe:
| Pole | Zasób | Wartość |
|---|---|---|
url | oba | adres do wyświetlenia: oryginał albo wariant z crop, gdy załącznik ma współrzędne kadrowania |
conversion_url | oba | szablon z {pipes}, np. https://host/media/{pipes}/2024/foo.jpg; null, gdy pliku nie da się przekształcić |
original_url | FileResource | nietknięty plik przez trasę API |
Klient podmienia {pipes} na własne konwersje, np. widen_640. W PublicFileResource szablon zawiera już crop załącznika, w FileResource nie, bo panel nakłada współrzędne sam.
original_url idzie przez PHP celowo: pliki serwowane statycznie nie mają nagłówków CORS, a kadrowanie w panelu uruchomionym pod inną domeną czyta obraz przez canvas.
Elementy
Modules/Media/Models/File.php— budowanie adresów (getPublicUrl(),getConversionUrlTemplate(),getOriginalUrl()).Modules/Media/Providers/RouteServiceProvider.php— trasamedia.conversions.cached, poza grupąapi.Modules/Media/Services/ConversionService.php— generuje wariant i decyduje, czy trafia do cache'u (isCachedDisk()).Modules/Media/Services/PublicImageCache.php— zapis wariantu (store()) i czyszczenie (purge()).FileObserver,FileService— czyszczą warianty przy usunięciu, podmianie, przeniesieniu i zmianie nazwy pliku.public/.htaccess—mod_expiresna rok dla obrazków..gitignore—/public/media, katalog generowany w trakcie działania.
Konfiguracja
php
// Modules/Media/Config/config.php
'cached_conversions' => [
'prefix' => 'media',
'disk' => 'public',
'per_minute' => 300,
],prefix— pierwszy segment adresu i nazwa katalogu wpublic/. Zmień go, gdy/media/...koliduje z trasami aplikacji.disk— dysk, którego warianty są dostępne pod przyjaznym adresem. Pozostałe dyski korzystają z trasy API.per_minute— ile żądań do trasy wariantów jedno IP może wysłać w ciągu minuty; po przekroczeniu dostaje 429. PHP obsługuje tylko pierwsze żądanie każdego wariantu, więc limit ogranicza generowanie nowych wariantów, a nie oglądanie istniejących.
Niezmienniki
- Tylko dysk publiczny. Warianty z dysków prywatnych nie trafiają do katalogu publicznego, bo ominęłyby kontrolę sygnatury.
ConversionService::isCachedDisk()jest jedynym miejscem tej decyzji — nie usuwaj go ani nie obchodź. - Zapis tylko pod prefiksem.
PublicImageCachezapisuje wyłącznie żądania spod/{prefix}/, więc trasa API nigdy nie tworzy plików wpublic/. purge()opiera się na układzie adresu{prefix}/{konwersje}/{ścieżka}: kasuje plik o danej ścieżce z każdego katalogu wariantu.
Serwer HTTP
Cache wymaga tylko tego, żeby serwer oddawał istniejące pliki z public/ bez PHP, a brakujące przekazywał do index.php. Standardowa konfiguracja Laravela robi to na każdym z poniższych serwerów. Opisane zmiany dotyczą nagłówków i bezpieczeństwa: pliki z /storage/ i /media/ nigdy nie mogą zostać wykonane jako PHP.
CloudPanel (nginx + Varnish)
Vhost CloudPanel ma dwa bloki server: nginx na 443 oddaje istniejące pliki statyczne sam, a resztę przekazuje przez Varnish do nginx na 8080, który uruchamia PHP.
Szablon vhosta Mooncms Monolit na testmoonbite.pl zawiera poniższe bloki w pierwszym bloku server (443), nad blokiem obrazków location ~* ^.+\.(css|js|jpg|...)$:
nginx
location ^~ /media/ {
add_header Access-Control-Allow-Origin "*";
expires max;
access_log off;
try_files $uri @backend;
}
location ^~ /storage/ {
add_header Access-Control-Allow-Origin "*";
expires max;
access_log off;
try_files $uri =404;
}
location @backend {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
}- Istniejący wariant nginx oddaje z dysku. Brakujący trafia prosto do nginx na 8080 i do Laravela, z pominięciem Varnisha, więc po usunięciu lub podmianie obrazka Varnish nie odda starej wersji.
- Pliki z
/storage/są serwowane tylko z dysku; żądanie nigdy nie trafia do PHP. - Nie dodawaj
try_files ... /index.phpw bloku na 443 — tam nie ma PHP.
Szablony CloudPanel zmienia się tylko z konsoli, jako root. clpctl nie czyta plików z /root, więc plik szablonu trzymaj w /tmp z prawami 644:
bash
clpctl vhost-template:view --name='Mooncms Monolit' > /tmp/monolit-vhost.tpl
chmod 644 /tmp/monolit-vhost.tpl
# edycja pliku
clpctl vhost-template:delete --name='Mooncms Monolit'
clpctl vhost-template:add --name='Mooncms Monolit' --file=/tmp/monolit-vhost.tplZmiana szablonu nie dotyczy stron już założonych.
Samodzielny nginx
nginx
location ^~ /media/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri /index.php?$query_string;
}
location ^~ /storage/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
}- Modyfikator
^~jest konieczny: bez niego pierwszeństwo ma regexpowelocation ~ \.php$i plik z tych katalogów mógłby zostać wykonany. add_headernie dziedziczy się do bloku z własnymadd_header; globalne nagłówki bezpieczeństwa powtórz w tych blokach.- Gdy nginx i PHP-FPM działają jako różni użytkownicy przy zaostrzonym umasku, nginx zwróci 403.
Apache
public/.htaccess kieruje do index.php tylko nieistniejące pliki, więc cache działa bez zmian. Ten sam plik ustawia mod_expires na rok dla obrazków i zawiera regułę, która blokuje wykonanie PHP w /storage/ i /media/:
apache
RewriteRule ^(storage|media)/.*\.(php[0-9]?|phtml|phar)$ - [F,L,NC]Nie usuwaj jej przy dostosowywaniu .htaccess w projekcie.
Utrzymanie
- Wymagany jest
php artisan storage:link; bez niego adresy oryginałów zwracają 404. public/mediamusi być zapisywalny dla użytkownika PHP. Nieudany zapis trafia do logu i nie psuje odpowiedzi, tylko wyłącza zysk z cache'u.- Ręczne czyszczenie:
rm -rf public/media; warianty odtworzą się przy kolejnych żądaniach. - Każda instancja buduje własny cache; współdzielony storage nie jest potrzebny. Przy deployu z podmianą katalogu release'u cache zaczyna od zera.
- Unieważnianie jest zdarzeniowe (usunięcie, podmiana, przeniesienie, zmiana nazwy pliku). Przeniesienie całego katalogu i podmiana pliku poza aplikacją nie są wykrywane — wyczyść cache ręcznie.
- Limit
per_minutedziała na adres IP. Za lokalnym proxy (np. Varnish w CloudPanel) Laravel bierze adres klienta zX-Forwarded-For, bobootstrap/app.phpufa proxy na127.0.0.1. Gdy proxy działa na innym hoście, dopisz jego adres wtrustProxies(), inaczej wszyscy odwiedzający dzielą jeden limit. - Limit nie zatrzyma generowania z wielu adresów naraz. Pełną ochronę dają dopiero podpisane adresy albo lista dozwolonych rozmiarów.