Files
KantSites/src/Ops/Tasks/CleanupTask.php
T
kantarciandClaude Fable 5.2 7d0369bada Serve images without PHP, add the 720 and 1280 steps, trim two hot paths
- 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>
2026-11-10 12:24:30 +01:00

148 lines
4.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}