- Image widths are 480, 720, 960, 1280, 1600 and 2400. A phone needed 721 pixels and got 960. Existing images receive the new steps from a task; stored pages pick them up at render time, so no document is rewritten. Mobile backgrounds use 720. - A variant served once is mirrored to public/media, and a rewrite rule lets the web server deliver it from there. var/media stays the source. - The hero preload follows the image each device really shows. - The post navigation reads the order and then two posts instead of up to 500 documents. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
93 lines
3.7 KiB
PHP
93 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Content;
|
|
|
|
/**
|
|
* Legt Bildvarianten dorthin, wo der Webserver sie ohne PHP ausliefern kann.
|
|
*
|
|
* Jedes Bild einer Seite war eine Anfrage an die Anwendung: Kernel starten, Datenbank fragen,
|
|
* Datei durchreichen. Gemessen auf kant-it.de (10.11.2026) rund 250 ms bis zum ersten Byte, eine
|
|
* statische Datei daneben rund 100 ms - und jede dieser Anfragen belegt einen der wenigen
|
|
* PHP-Prozesse, die ein geteiltes Hosting gleichzeitig hergibt.
|
|
*
|
|
* Der Medienordner unter var/ bleibt die Quelle; Sicherung, Website-Paket und Wiederherstellung
|
|
* kennen nur ihn. Dieser Ordner ist ein Abbild, das sich von selbst füllt: Beim ersten Abruf über
|
|
* PHP landet die Variante hier, alle weiteren beantwortet der Webserver (Regel in
|
|
* public/.htaccess). Fehlt das Abbild oder lässt es sich nicht schreiben, bleibt alles beim Alten.
|
|
*
|
|
* Die Adressen der Bilder ändern sich nicht. Was hier liegt, war auch vorher für jeden abrufbar,
|
|
* der die Adresse kennt - die Auslieferung über PHP prüft keine Anmeldung.
|
|
*/
|
|
final class MediaMirror
|
|
{
|
|
/** @param string $directory das Abbild, üblich public/media; leer schaltet es ab */
|
|
public function __construct(private string $directory, private string $mediaDirectory)
|
|
{
|
|
}
|
|
|
|
public function enabled(): bool
|
|
{
|
|
return '' !== $this->directory;
|
|
}
|
|
|
|
/** Legt eine Variante ab. Scheitert still: das Abbild ist eine Abkürzung, keine Voraussetzung. */
|
|
public function publish(string $id, int $width, string $source): void
|
|
{
|
|
if (!$this->enabled() || 1 !== preg_match('/^[a-f0-9]{32}$/D', $id) || $width < 1 || $width > 9999) {
|
|
return;
|
|
}
|
|
$folder = $this->directory.'/'.$id;
|
|
$target = $folder.'/'.$width.'.webp';
|
|
if (is_file($target)) {
|
|
return;
|
|
}
|
|
if (!is_dir($folder) && !@mkdir($folder, 0o775, true) && !is_dir($folder)) {
|
|
return;
|
|
}
|
|
// Ein harter Verweis kostet keinen zweiten Platz auf der Platte. Wo das Dateisystem ihn
|
|
// nicht kennt oder der Medienordner woanders liegt, wird kopiert - über eine Zwischendatei,
|
|
// damit der Webserver nie ein halbes Bild ausliefert.
|
|
if (@link($source, $target)) {
|
|
return;
|
|
}
|
|
$teil = $target.'.'.bin2hex(random_bytes(4)).'.tmp';
|
|
if (@copy($source, $teil) && filesize($source) === filesize($teil) && @rename($teil, $target)) {
|
|
return;
|
|
}
|
|
@unlink($teil);
|
|
}
|
|
|
|
/**
|
|
* Entfernt aus dem Abbild, was es im Medienordner nicht mehr gibt - nach dem Import eines
|
|
* Website-Pakets etwa. Ohne das lieferte der Webserver Bilder weiter aus, die die Anwendung
|
|
* selbst nicht mehr kennt.
|
|
*
|
|
* @return int entfernte Dateien
|
|
*/
|
|
public function prune(): int
|
|
{
|
|
if (!$this->enabled() || !is_dir($this->directory)) {
|
|
return 0;
|
|
}
|
|
$removed = 0;
|
|
foreach (scandir($this->directory) ?: [] as $id) {
|
|
if (1 !== preg_match('/^[a-f0-9]{32}$/D', $id) || !is_dir($this->directory.'/'.$id)) {
|
|
continue;
|
|
}
|
|
foreach (scandir($this->directory.'/'.$id) ?: [] as $name) {
|
|
if ('.' === $name || '..' === $name) {
|
|
continue;
|
|
}
|
|
$width = 1 === preg_match('/^(\d{1,4})\.webp$/D', $name, $treffer) ? $treffer[1] : null;
|
|
if (null === $width || !is_file($this->mediaDirectory.'/'.$id.'-'.$width.'.webp')) {
|
|
$removed += (int) @unlink($this->directory.'/'.$id.'/'.$name);
|
|
}
|
|
}
|
|
// Gelingt nur, wenn der Ordner leer ist - genau dann soll er weg.
|
|
@rmdir($this->directory.'/'.$id);
|
|
}
|
|
|
|
return $removed;
|
|
}
|
|
}
|