- 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>
148 lines
4.8 KiB
PHP
148 lines
4.8 KiB
PHP
<?php
|
||
|
||
namespace App\Ops\Tasks;
|
||
|
||
use App\Content\MediaMirror;
|
||
use App\Ops\ArchiveUpdate;
|
||
use App\Ops\Scheduler;
|
||
use App\Ops\Updater;
|
||
|
||
/**
|
||
* Räumt in `var/` auf, was mit der Zeit liegen bleibt.
|
||
*
|
||
* Drei Dinge, und ausdrücklich nur diese drei: abgelaufene Sitzungsdateien, alte Arbeitsordner
|
||
* eines Updates und zu lang gewordene Protokolle. Nichts davon ist Inhalt, nichts davon lässt
|
||
* sich vermissen.
|
||
*
|
||
* Was diese Aufgabe **nicht** anfasst: Sicherungen (die rotiert `cms:backup` selbst und nach
|
||
* einer anderen Regel), den Cache (den baut Symfony auf, wenn er fehlt, aber ihn mitten im
|
||
* Betrieb zu löschen macht die nächste Anfrage langsam ohne Grund) und den Ordner eines Updates,
|
||
* das noch auf seinen zweiten Schritt wartet.
|
||
*/
|
||
final class CleanupTask implements Task
|
||
{
|
||
/** Sitzungen und Update-Arbeitsordner, die älter sind, werden entfernt. */
|
||
private const KEEP_DAYS = 30;
|
||
|
||
/** So viele Zeilen bleiben in den Protokollen stehen. */
|
||
private const KEEP_LINES = 200;
|
||
|
||
public function __construct(private string $projectDir, private ?MediaMirror $mirror = null)
|
||
{
|
||
}
|
||
|
||
public function name(): string
|
||
{
|
||
return 'cleanup';
|
||
}
|
||
|
||
public function label(): string
|
||
{
|
||
return 'Aufräumen';
|
||
}
|
||
|
||
public function explanation(): string
|
||
{
|
||
return sprintf(
|
||
'Entfernt Sitzungsdateien und Update-Arbeitsordner, die älter als %d Tage sind, und kürzt die Protokolle. Sicherungen und Inhalte bleiben unberührt.',
|
||
self::KEEP_DAYS,
|
||
);
|
||
}
|
||
|
||
public function interval(): int
|
||
{
|
||
return 86400;
|
||
}
|
||
|
||
public function run(): string
|
||
{
|
||
$limit = time() - self::KEEP_DAYS * 86400;
|
||
|
||
$bericht = sprintf(
|
||
'%d Sitzungsdatei(en), %d Update-Ordner entfernt, %d Protokoll(e) gekürzt.',
|
||
$this->sessions($limit),
|
||
$this->updateFolders($limit),
|
||
$this->logs(),
|
||
);
|
||
// Bildvarianten im öffentlichen Abbild, deren Bild es nicht mehr gibt.
|
||
$verwaist = $this->mirror?->prune() ?? 0;
|
||
|
||
return 0 === $verwaist ? $bericht : $bericht.sprintf(' %d verwaiste Bilddatei(en) aus public/media entfernt.', $verwaist);
|
||
}
|
||
|
||
/** Sitzungsdateien. Der Garbage Collector von PHP ist auf vielen Hostings abgeschaltet. */
|
||
private function sessions(int $limit): int
|
||
{
|
||
$removed = 0;
|
||
foreach (glob($this->projectDir.'/var/sessions/*/sess_*') ?: [] as $file) {
|
||
if (is_file($file) && (int) @filemtime($file) < $limit && @unlink($file)) {
|
||
++$removed;
|
||
}
|
||
}
|
||
|
||
return $removed;
|
||
}
|
||
|
||
/**
|
||
* Die Arbeitsordner des Archiv-Updates.
|
||
*
|
||
* Der jüngste ist der Rückweg aus dem letzten Update, und den lässt diese Aufgabe einen
|
||
* Monat lang stehen. Wartet ein Update noch auf seinen zweiten Schritt, wird gar nichts
|
||
* angefasst: dann ist einer dieser Ordner der Stand, auf den zurückgegangen wird.
|
||
*/
|
||
private function updateFolders(int $limit): int
|
||
{
|
||
if (is_file($this->projectDir.'/'.ArchiveUpdate::MARKE)) {
|
||
return 0;
|
||
}
|
||
$removed = 0;
|
||
foreach (glob($this->projectDir.'/var/update/*', GLOB_ONLYDIR) ?: [] as $folder) {
|
||
if ((int) @filemtime($folder) < $limit && $this->removeDirectory($folder)) {
|
||
++$removed;
|
||
}
|
||
}
|
||
|
||
return $removed;
|
||
}
|
||
|
||
private function logs(): int
|
||
{
|
||
$shortened = 0;
|
||
foreach ([Updater::LOG_FILE, Scheduler::LOG_FILE] as $relative) {
|
||
$file = $this->projectDir.'/'.$relative;
|
||
$raw = @file_get_contents($file);
|
||
if (!is_string($raw)) {
|
||
continue;
|
||
}
|
||
$lines = array_values(array_filter(explode("\n", $raw)));
|
||
if (count($lines) <= self::KEEP_LINES) {
|
||
continue;
|
||
}
|
||
if (false !== @file_put_contents($file, implode("\n", array_slice($lines, -self::KEEP_LINES))."\n", LOCK_EX)) {
|
||
++$shortened;
|
||
}
|
||
}
|
||
|
||
return $shortened;
|
||
}
|
||
|
||
/** Rekursiv löschen, aber nur unterhalb von `var/` – geprüft, nicht angenommen. */
|
||
private function removeDirectory(string $path): bool
|
||
{
|
||
$real = realpath($path);
|
||
$var = realpath($this->projectDir.'/var');
|
||
if (false === $real || false === $var || !str_starts_with($real, $var.DIRECTORY_SEPARATOR)) {
|
||
return false;
|
||
}
|
||
$items = new \RecursiveIteratorIterator(
|
||
new \RecursiveDirectoryIterator($real, \FilesystemIterator::SKIP_DOTS),
|
||
\RecursiveIteratorIterator::CHILD_FIRST,
|
||
);
|
||
foreach ($items as $item) {
|
||
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
|
||
}
|
||
|
||
return @rmdir($real);
|
||
}
|
||
}
|