From 14aac0d7a478581710ae4896cad99383f48ef109 Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Thu, 3 Sep 2026 14:45:24 +0200 Subject: [PATCH] Make cache clear use atomic directory cutover --- include/cache_clear_atomic.inc.php | 154 +++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 include/cache_clear_atomic.inc.php diff --git a/include/cache_clear_atomic.inc.php b/include/cache_clear_atomic.inc.php new file mode 100644 index 0000000..3a0fecf --- /dev/null +++ b/include/cache_clear_atomic.inc.php @@ -0,0 +1,154 @@ +getPathname(); + if ($item->isLink() || $item->isFile()) + { + if (!@unlink($path)) $failed[] = $path; + continue; + } + + if ($item->isDir() && !@rmdir($path)) + { + $failed[] = $path; + } + } + + if (!@rmdir($root) && is_dir($root)) $failed[] = $root; +} + +function bratonien_tools_clear_image_cache_atomic() +{ + global $conf; + + if (!defined('PWG_DERIVATIVE_DIR')) + { + throw new RuntimeException('PWG_DERIVATIVE_DIR ist nicht definiert.'); + } + + if (bratonien_tools_main_cache_process_active() || bratonien_tools_main_cache_is_running()) + { + bratonien_tools_request_main_cache_cancel(); + if (!bratonien_tools_wait_main_cache_stopped(10.0)) + { + throw new RuntimeException('Der laufende Cache-Aufbau konnte noch nicht beendet werden. Bitte den Abbruch kurz abschließen lassen und erneut leeren.'); + } + } + @unlink(bratonien_tools_main_cache_cancel_file()); + + $piwigo_root = realpath(PHPWG_ROOT_PATH); + if ($piwigo_root === false) + { + throw new RuntimeException('Piwigo-Root konnte für die Cache-Sicherheitsprüfung nicht aufgelöst werden.'); + } + + $cache_root = rtrim(PHPWG_ROOT_PATH, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.trim(PWG_DERIVATIVE_DIR, '/\\'); + $real_cache_root = realpath($cache_root); + if ($real_cache_root === false || !is_dir($real_cache_root)) + { + throw new RuntimeException('Bildcache-Verzeichnis wurde nicht gefunden: '.$cache_root); + } + + $root_prefix = rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + if (strpos(rtrim($real_cache_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR, $root_prefix) !== 0) + { + throw new RuntimeException('Bildcache liegt außerhalb der Piwigo-Installation. Abbruch.'); + } + if (rtrim($real_cache_root, DIRECTORY_SEPARATOR) === rtrim($piwigo_root, DIRECTORY_SEPARATOR)) + { + throw new RuntimeException('Bildcache-Pfad entspricht dem Piwigo-Root. Sicherheitsabbruch.'); + } + + $before = bratonien_tools_scan_image_cache($real_cache_root); + $parent = dirname($real_cache_root); + if (!is_dir($parent) || !is_writable($parent)) + { + throw new RuntimeException('Übergeordnetes Bildcache-Verzeichnis ist nicht beschreibbar: '.$parent); + } + + $detached = $parent.'/.'.basename($real_cache_root).'.bratonien-clear-'.date('YmdHis').'-'.bin2hex(random_bytes(4)); + if (file_exists($detached) || is_link($detached)) + { + throw new RuntimeException('Temporäres Cache-Auslagerungsverzeichnis existiert bereits. Abbruch.'); + } + + // Der gesamte bisherige Cache wird in einem einzigen Rename aus dem aktiven + // Piwigo-Pfad entfernt. Neue Requests schreiben danach nur noch in den neu + // angelegten Cachebaum und können nicht mit dem Löschen des Altbestands + // konkurrieren. + if (!@rename($real_cache_root, $detached)) + { + throw new RuntimeException('Bildcache konnte nicht atomar aus dem aktiven Pfad ausgelagert werden.'); + } + + $mode = isset($conf['chmod_value']) ? (int)$conf['chmod_value'] : 0755; + $umask = umask(0); + $created = @mkdir($real_cache_root, $mode, true); + umask($umask); + if (!$created && !is_dir($real_cache_root)) + { + // Der aktive Pfad muss in diesem Fehlerfall wiederhergestellt werden. + if (!file_exists($real_cache_root) && @rename($detached, $real_cache_root)) + { + throw new RuntimeException('Neuer Bildcache konnte nicht angelegt werden; der bisherige Cache wurde vollständig wiederhergestellt.'); + } + throw new RuntimeException('Neuer Bildcache konnte nicht angelegt und der bisherige Cache nicht automatisch wiederhergestellt werden. Manueller Eingriff erforderlich.'); + } + + @chmod($real_cache_root, $mode); + @file_put_contents($real_cache_root.'/index.htm', 'Not allowed!'); + + $failed = array(); + bratonien_tools_atomic_cache_remove_tree($detached, $failed); + + $active = bratonien_tools_scan_image_cache($real_cache_root); + + bratonien_tools_write_main_cache_status(array( + 'state'=>'idle', + 'message'=>'Bildcache wurde atomar geleert. Kein manueller Cache-Aufbau aktiv.', + )); + + if ($failed) + { + throw new RuntimeException(sprintf( + 'Der aktive Bildcache wurde erfolgreich geleert und neu angelegt, aber %d Datei(en)/Verzeichnis(se) des ausgelagerten Altbestands konnten nicht entfernt werden. Erste problematische Stelle: %s', + count($failed), + $failed[0] + )); + } + + $message = sprintf( + 'Bildcache atomar geleert: %d alte Datei(en) (%s) entfernt, davon %d Custom-Derivate.', + $before['files'], + bratonien_tools_format_bytes($before['bytes']), + $before['custom'] + ); + + if ($active['files'] > 0) + { + $message .= sprintf( + ' Während bzw. unmittelbar nach dem Umschalten wurden bereits %d neue Derivatdatei(en) durch laufende Anfragen erzeugt; diese gehören zum neuen Cache und sind kein Rest des gelöschten Bestands.', + $active['files'] + ); + } + else + { + $message .= ' Der neu aktive Cache ist zum Abschluss der Prüfung leer.'; + } + + return array('message'=>$message); +}