diff --git a/include/cache_clear_atomic.inc.php b/include/cache_clear_atomic.inc.php index bfb7365..d6cb9fb 100644 --- a/include/cache_clear_atomic.inc.php +++ b/include/cache_clear_atomic.inc.php @@ -60,6 +60,114 @@ function bratonien_tools_atomic_cache_remove_tree($root, array &$failed) if (!@rmdir($root) && is_dir($root)) $failed[] = $root; } +function bratonien_tools_atomic_cache_acquire_lock($path, $label, $timeout_seconds=10.0) +{ + $directory = dirname($path); + if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory)) + { + throw new RuntimeException($label.'-Lockverzeichnis konnte nicht angelegt werden.'); + } + + $handle = @fopen($path, 'c'); + if (!$handle) + { + throw new RuntimeException($label.'-Lock konnte nicht geöffnet werden: '.$path); + } + + $deadline = microtime(true) + max(0.1, (float)$timeout_seconds); + do + { + if (@flock($handle, LOCK_EX | LOCK_NB)) + { + return $handle; + } + usleep(100000); + } + while (microtime(true) < $deadline); + + fclose($handle); + throw new RuntimeException($label.' ist noch aktiv. Der Cache wurde nicht angefasst; bitte den laufenden Vorgang kurz abschließen lassen und erneut leeren.'); +} + +function bratonien_tools_atomic_cache_release_locks(array &$locks) +{ + foreach (array_reverse($locks) as $handle) + { + if (!is_resource($handle)) continue; + @flock($handle, LOCK_UN); + fclose($handle); + } + $locks = array(); +} + +function bratonien_tools_atomic_cache_prepare_webdav_guards(array &$locks, array &$cancel_files) +{ + if (!function_exists('bratonien_tools_webdav_warmup_connections')) return 0; + + $connections = bratonien_tools_webdav_warmup_connections(); + if (!$connections) return 0; + ksort($connections, SORT_NUMERIC); + + // Jeder aktive WebDAV-Worker bekommt zuerst sein reguläres Abbruchsignal. + // Ein bereits gestarteter 10er-Batch darf sauber fertig werden; ein neuer + // Batch darf danach nicht mehr beginnen. + if (function_exists('bratonien_tools_request_webdav_cache_cancel')) + { + bratonien_tools_request_webdav_cache_cancel(); + } + + // Das Signal wird für alle Verbindungen gesetzt, auch wenn deren Statusdatei + // veraltet sein sollte. Sobald wir anschließend den Prozess-Lock besitzen, + // kann garantiert kein WebDAV-Worker während des Cache-Leerens schreiben. + foreach ($connections as $connection_id=>$runtime) + { + $cancel = function_exists('bratonien_tools_webdav_warmup_cancel_file_for_connection') + ? bratonien_tools_webdav_warmup_cancel_file_for_connection($connection_id) + : PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.cancel-'.(int)$connection_id; + + if (@file_put_contents($cancel, (string)time()."\n", LOCK_EX) === false) + { + throw new RuntimeException('WebDAV-Abbruchsignal für Verbindung #'.(int)$connection_id.' konnte nicht geschrieben werden.'); + } + @chmod($cancel, 0664); + $cancel_files[] = $cancel; + } + + foreach ($connections as $connection_id=>$runtime) + { + $state_dir = rtrim((string)($runtime['state_dir'] ?? ''), '/'); + if ($state_dir === '') + { + throw new RuntimeException('WebDAV-State-Verzeichnis für Verbindung #'.(int)$connection_id.' fehlt.'); + } + $locks[] = bratonien_tools_atomic_cache_acquire_lock( + $state_dir.'/webdav-cache-warmup.lock', + 'WebDAV-Cache-Worker #'.(int)$connection_id, + 10.0 + ); + } + + return count($connections); +} + +function bratonien_tools_atomic_cache_prepare_presentation_guard(array &$locks) +{ + if (!function_exists('bratonien_tools_presentation_refresh_queue_dir')) return false; + + $queue_dir = bratonien_tools_presentation_refresh_queue_dir(); + if (!is_dir($queue_dir)) return false; + + // Der Presentation-Worker erzeugt Bratonien-Wasserzeichen-Vorschauen im + // selben Derivatbaum. Deshalb muss auch er während des atomaren Leerens + // vollständig draußen bleiben. + $locks[] = bratonien_tools_atomic_cache_acquire_lock( + rtrim($queue_dir, '/').'/.worker.lock', + 'Vorschau-Aktualisierung', + 10.0 + ); + return true; +} + function bratonien_tools_clear_image_cache_atomic() { global $conf; @@ -69,125 +177,159 @@ function bratonien_tools_clear_image_cache_atomic() throw new RuntimeException('PWG_DERIVATIVE_DIR ist nicht definiert.'); } - if (bratonien_tools_main_cache_process_active() || bratonien_tools_main_cache_is_running()) + $held_locks = array(); + $webdav_cancel_files = array(); + + try { - bratonien_tools_request_main_cache_cancel(); - if (!bratonien_tools_wait_main_cache_stopped(10.0)) + // 1. Lokalen Cache-Builder sauber anhalten und seinen Prozess-Lock selbst + // halten. Damit kann zwischen Prüfung und atomarem Rename kein neuer + // lokaler Builder starten. + if (bratonien_tools_main_cache_process_active() || bratonien_tools_main_cache_is_running()) { - throw new RuntimeException('Der laufende Cache-Aufbau konnte noch nicht beendet werden. Bitte den Abbruch kurz abschließen lassen und erneut leeren.'); + 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.'); - } - - 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)) - { - 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!'); - - // Der Quellenindex selbst bleibt bestehen, weil sich die Quellen durch ein - // Cache-Leeren nicht ändern. Seine Fertigmarkierungen sind danach aber - // zwingend ungültig. Der Worker darf sonst einen leeren Piwigo-Cache als - // bereits verarbeitet ansehen, ohne den Cache selbst anzuschauen. - $webdav_invalidated = 0; - if (function_exists('bratonien_tools_invalidate_webdav_cache_completion')) - { - $webdav_invalidated = bratonien_tools_invalidate_webdav_cache_completion('Piwigo-Bildcache wurde atomar geleert.'); - } - - $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 ($webdav_invalidated > 0) - { - $message .= sprintf(' Die Cache-Fertigmarkierungen von %d WebDAV-Worker-Index(en) wurden verworfen; die Quellenindizes selbst bleiben erhalten.', $webdav_invalidated); - } - - 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'] + @unlink(bratonien_tools_main_cache_cancel_file()); + $held_locks[] = bratonien_tools_atomic_cache_acquire_lock( + bratonien_tools_main_cache_lock_file(), + 'Lokaler Piwigo-Cache-Worker', + 10.0 ); - } - else - { - $message .= ' Der neu aktive Cache ist zum Abschluss der Prüfung leer.'; - } - return array('message'=>$message); + // 2. WebDAV-Warmup regulär abbrechen und danach alle Verbindungs-Locks + // exklusiv halten. Der laufende Batch wird nicht hart beendet. + bratonien_tools_atomic_cache_prepare_webdav_guards($held_locks, $webdav_cancel_files); + + // 3. Seit der Presentation-Refresh-Einführung kann auch dieser Worker + // Derivate erzeugen. Ohne seinen Lock konnte ein gerade geleerter Cache + // sofort wieder gefüllt werden und wie ein fehlgeschlagenes Löschen wirken. + bratonien_tools_atomic_cache_prepare_presentation_guard($held_locks); + + $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.'); + } + + 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)) + { + 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!'); + + // Der Quellenindex selbst bleibt bestehen, weil sich die Quellen durch ein + // Cache-Leeren nicht ändern. Seine Fertigmarkierungen sind danach aber + // zwingend ungültig. Der Worker darf sonst einen leeren Piwigo-Cache als + // bereits verarbeitet ansehen, ohne den Cache selbst anzuschauen. + $webdav_invalidated = 0; + if (function_exists('bratonien_tools_invalidate_webdav_cache_completion')) + { + $webdav_invalidated = bratonien_tools_invalidate_webdav_cache_completion('Piwigo-Bildcache wurde atomar geleert.'); + } + + $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 Cache-Worker schreibt während des Löschvorgangs in den Derivatbaum.', + )); + + 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 ($webdav_invalidated > 0) + { + $message .= sprintf(' Die Cache-Fertigmarkierungen von %d WebDAV-Worker-Index(en) wurden verworfen; die Quellenindizes selbst bleiben erhalten.', $webdav_invalidated); + } + + if ($active['files'] > 0) + { + $message .= sprintf( + ' Unmittelbar nach dem atomaren Umschalten wurden bereits %d neue Derivatdatei(en) durch normale Piwigo-Anfragen erzeugt; die eigenen Cache-Worker waren während des Löschens gesperrt und der alte Cachebaum wurde vollständig entfernt.', + $active['files'] + ); + } + else + { + $message .= ' Der neu aktive Cache ist zum Abschluss der Prüfung leer.'; + } + + return array('message'=>$message); + } + finally + { + // Die Cancel-Dateien bleiben so lange bestehen, bis wir alle Worker-Locks + // wieder freigeben. Erst danach darf ein bewusst neu gestarteter Lauf den + // leeren Cache erneut aufbauen. + foreach ($webdav_cancel_files as $cancel) + { + @unlink($cancel); + } + bratonien_tools_atomic_cache_release_locks($held_locks); + } }