Skip to content

Media: adresy plików i cache miniatur ​

Pliki z dysku publicznego mają adresy, które serwer HTTP obsługuje statycznie, bez PHP.

CoAdresKto serwuje
Oryginał, dysk publiczny/storage/2024/foo.jpgserwer HTTP przez storage:link
Wariant, dysk publiczny/media/widen_640/2024/foo.jpgLaravel 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 statycznie

Dział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.jpg

Argumenty 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:

PoleZasóbWartość
urlobaadres do wyświetlenia: oryginał albo wariant z crop, gdy załącznik ma współrzędne kadrowania
conversion_urlobaszablon z {pipes}, np. https://host/media/{pipes}/2024/foo.jpg; null, gdy pliku nie da się przekształcić
original_urlFileResourcenietknię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 — trasa media.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_expires na 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 w public/. 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. PublicImageCache zapisuje wyłącznie żądania spod /{prefix}/, więc trasa API nigdy nie tworzy plików w public/.
  • 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.php w 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.tpl

Zmiana 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 regexpowe location ~ \.php$ i plik z tych katalogów mógłby zostać wykonany.
  • add_header nie dziedziczy się do bloku z własnym add_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/media musi 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_minute działa na adres IP. Za lokalnym proxy (np. Varnish w CloudPanel) Laravel bierze adres klienta z X-Forwarded-For, bo bootstrap/app.php ufa proxy na 127.0.0.1. Gdy proxy działa na innym hoście, dopisz jego adres w trustProxies(), 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.

Made by Moonbite