Compare commits

...

8 Commits

Author SHA1 Message Date
Terranom674
43178483e5 Exclude WebDAV images from legacy cache worker totals 2026-09-03 15:57:22 +02:00
Terranom674
5b0f7f93af Bump patch version to 0.9.7.1.12 2026-09-03 15:51:28 +02:00
Terranom674
5076a76d29 Route manual cache build through combined WebDAV rebuild 2026-09-03 15:51:08 +02:00
Terranom674
7094288534 Coordinate manual cache build with WebDAV rebuild 2026-09-03 15:48:53 +02:00
Terranom674
f7d0959eba Support full WebDAV cache rebuilds through Piwigo 2026-09-03 15:48:28 +02:00
Terranom674
2a96d0165c Add explicit WebDAV cache rebuild launcher 2026-09-03 15:47:17 +02:00
Terranom674
b6621d36db Add explicit WebDAV rebuild dispatch mode 2026-09-03 15:46:53 +02:00
Terranom674
41ffa29af7 Handle symlinks safely while removing detached cache trees 2026-09-03 15:45:54 +02:00
7 changed files with 140 additions and 77 deletions

View File

@@ -8,24 +8,53 @@ function bratonien_tools_atomic_cache_remove_tree($root, array &$failed)
{
if (!file_exists($root) && !is_link($root)) return;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $item)
// Niemals einem Symlink folgen. Auch ein Link auf ein Verzeichnis ist nur
// ein einzelner Eintrag des ausgelagerten Cachebaums und wird per unlink()
// entfernt. So kann das Cleanup kein Ziel ausserhalb des Cachebaums beruehren.
if (is_link($root))
{
$path = $item->getPathname();
if ($item->isLink() || $item->isFile())
if (!@unlink($root)) $failed[] = $root;
return;
}
if (is_file($root))
{
if (!@unlink($root)) $failed[] = $root;
return;
}
if (!is_dir($root))
{
$failed[] = $root;
return;
}
$entries = @scandir($root);
if ($entries === false)
{
$failed[] = $root;
return;
}
foreach ($entries as $entry)
{
if ($entry === '.' || $entry === '..') continue;
$path = $root.DIRECTORY_SEPARATOR.$entry;
if (is_link($path) || is_file($path))
{
if (!@unlink($path)) $failed[] = $path;
continue;
}
if ($item->isDir() && !@rmdir($path))
if (is_dir($path))
{
$failed[] = $path;
bratonien_tools_atomic_cache_remove_tree($path, $failed);
continue;
}
// Sonderdateien werden wie einzelne Cacheeintraege behandelt. unlink()
// ist hier sicherer als ein rekursiver Iterator, der Verweise interpretieren
// koennte.
if (!@unlink($path)) $failed[] = $path;
}
if (!@rmdir($root) && is_dir($root)) $failed[] = $root;
@@ -86,10 +115,6 @@ function bratonien_tools_clear_image_cache_atomic()
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.');
@@ -101,7 +126,6 @@ function bratonien_tools_clear_image_cache_atomic()
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.');

View File

@@ -30,7 +30,7 @@ function bratonien_tools_get_tools()
{
return array(
'image_cache_clear' => array('handler' => 'bratonien_tools_clear_image_cache_atomic'),
'image_cache_build' => array('handler' => 'bratonien_tools_start_main_cache_build'),
'image_cache_build' => array('handler' => 'bratonien_tools_start_combined_image_cache_build'),
'image_cache_cancel' => array('handler' => 'bratonien_tools_cancel_main_cache_build'),
'image_cache_worker_settings' => array('handler' => 'bratonien_tools_save_cache_worker_settings'),
'image_cache_webdav_warmup_settings' => array('handler' => 'bratonien_tools_save_webdav_warmup_settings'),

View File

@@ -119,7 +119,7 @@ function bratonien_tools_webdav_warmup_php_cli()
return null;
}
function bratonien_tools_start_webdav_warmup_manual()
function bratonien_tools_start_webdav_warmup_mode($mode, $message)
{
if (!function_exists('exec')) throw new RuntimeException('PHP exec() ist deaktiviert; Warmup kann nicht gestartet werden.');
$php = bratonien_tools_webdav_warmup_php_cli();
@@ -127,15 +127,47 @@ function bratonien_tools_start_webdav_warmup_manual()
$dispatcher = realpath(BRATONIEN_TOOLS_PATH.'runtime/webdav-warmup-dispatch.php');
if (!$dispatcher || !is_file($dispatcher)) throw new RuntimeException('WebDAV-Warmup-Dispatcher wurde nicht gefunden.');
if (!in_array($mode, array('manual','rebuild'), true)) throw new RuntimeException('Ungültiger manueller Warmup-Modus.');
$log = PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup-dispatch.log';
$command = 'nohup '.escapeshellarg($php).' '.escapeshellarg($dispatcher).' --mode=manual >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!';
$command = 'nohup '.escapeshellarg($php).' '.escapeshellarg($dispatcher).' --mode='.escapeshellarg($mode).' >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!';
$output = array();
$exit = 1;
@exec($command, $output, $exit);
$pid = isset($output[0]) ? (int)$output[0] : 0;
if ($exit !== 0 || $pid <= 0) throw new RuntimeException('Manuelle Warmup-Prüfung konnte nicht gestartet werden.');
if ($exit !== 0 || $pid <= 0) throw new RuntimeException('WebDAV-Warmup konnte nicht gestartet werden.');
return array('message'=>'WebDAV-Warmup: Prüfung auf neue oder geänderte Bilder wurde gestartet.');
return array('message'=>$message, 'pid'=>$pid);
}
function bratonien_tools_start_webdav_warmup_manual()
{
return bratonien_tools_start_webdav_warmup_mode(
'manual',
'WebDAV-Warmup: Prüfung auf neue oder geänderte Bilder wurde gestartet.'
);
}
function bratonien_tools_start_webdav_cache_rebuild()
{
return bratonien_tools_start_webdav_warmup_mode(
'rebuild',
'WebDAV-Bildcache: vollständiger Wiederaufbau wurde gestartet.'
);
}
function bratonien_tools_start_combined_image_cache_build()
{
$main = bratonien_tools_start_main_cache_build();
$webdav = bratonien_tools_start_webdav_cache_rebuild();
$parts = array();
if (!empty($main['message'])) $parts[] = $main['message'];
if (!empty($webdav['message'])) $parts[] = $webdav['message'];
return array(
'started'=>!empty($main['started']) || !empty($webdav['pid']),
'message'=>implode(' ', $parts),
);
}
function bratonien_tools_run_webdav_warmup_audit()

View File

@@ -23,7 +23,7 @@ $_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/main-cache-build.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Piwigo-Cache-Builder/1.4';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Piwigo-Cache-Builder/1.5';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
@@ -208,7 +208,18 @@ function bratonien_tools_cache_builder_worker($worker_index, $worker_count, $run
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result))
{
if (((int)$row['id'] % $worker_count) === $worker_index)
$image_id = (int)$row['id'];
// WebDAV-Bilder gehören vollständig in den materialisierenden Rebuild-/Warmup-Pfad.
// Sie dürfen im Legacy-Worker weder erzeugt noch als "übersprungen" gezählt werden,
// weil das die Fortschrittsanzeige nach einem Cache-Leeren fälschlich wie einen
// abgeschlossenen Lauf aussehen lässt.
if (bratonien_tools_webdav_materialize_source_info($image_id))
{
continue;
}
if (($image_id % $worker_count) === $worker_index)
{
$rows[] = $row;
}
@@ -230,7 +241,6 @@ function bratonien_tools_cache_builder_worker($worker_index, $worker_count, $run
$source_path = $src->get_path();
$image_id = (int)$image_row['id'];
$metadata = null;
$webdav_source = bratonien_tools_webdav_materialize_source_info($image_id);
foreach ($variants as $variant_name => $requested_params)
{
@@ -251,27 +261,6 @@ function bratonien_tools_cache_builder_worker($worker_index, $worker_count, $run
$derivative = new DerivativeImage($requested_params, $src);
$target_path = $derivative->get_path();
// WebDAV-Connector-Bilder besitzen absichtlich nur einen Placeholder als
// lokale Quelle. Der alte Full-Cache-Builder darf diesen niemals direkt
// rendern. Fehlende Connector-Derivate gehören ausschließlich in den
// materialisierenden WebDAV-Warmup-/On-Demand-Pfad.
if ($webdav_source)
{
if (
strpos($target_path, PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR) === 0
&& is_file($target_path)
&& is_readable($target_path)
)
{
$cached++;
}
else
{
$skipped++;
}
continue;
}
if (strpos($target_path, PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR) !== 0)
{
$skipped++;
@@ -497,7 +486,7 @@ for ($i=0; $i<$worker_count; $i++)
}
}
$running_message = sprintf('Piwigo-Bildcache wird mit %d parallelen Worker(n) aufgebaut.', $worker_count);
$running_message = sprintf('Piwigo-Bildcache wird mit %d parallelen Worker(n) aufgebaut. WebDAV-Bilder laufen getrennt über den Piwigo-Rebuild.', $worker_count);
bratonien_tools_write_main_cache_status(array(
'state'=>'running',
'message'=>$running_message,
@@ -546,7 +535,7 @@ $state = bratonien_tools_cache_builder_aggregate(
$worker_count,
$was_cancelled
? 'Piwigo-Bildcache-Aufbau wurde abgebrochen.'
: ($exit_error ? 'Piwigo-Bildcache beendet; mindestens ein Worker meldete einen Prozessfehler.' : 'Piwigo-Bildcache wurde aufgebaut.')
: ($exit_error ? 'Piwigo-Bildcache beendet; mindestens ein Worker meldete einen Prozessfehler.' : 'Lokaler Piwigo-Bildcache wurde aufgebaut; WebDAV-Bilder laufen getrennt über den Piwigo-Rebuild.')
);
for ($i=0; $i<$worker_count; $i++)
@@ -560,3 +549,4 @@ fclose($lock);
@unlink($lock_path);
exit(($state === 'error' || $exit_error) ? 1 : 0);
}

View File

@@ -1,7 +1,7 @@
<?php
/*
Plugin Name: Bratonien Tools
Version: 0.9.7.1.11
Version: 0.9.7.1.12
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien

View File

@@ -5,7 +5,7 @@ if (PHP_SAPI !== 'cli')
exit;
}
$piwigo_root = realpath(dirname(__DIR__, 3));
$piwigo_root = realpath(dirname(__DIR__, 4));
if ($piwigo_root === false)
{
fwrite(STDERR, "Piwigo-Root wurde nicht gefunden.\n");
@@ -23,7 +23,7 @@ $_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/lib/webdav-cache-warmup.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Cache-Warmup/0.9.7.1.8';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Cache-Warmup/0.9.7.1.12';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
@@ -35,6 +35,7 @@ if (!defined('BRATONIEN_TOOLS_PATH'))
}
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_cache_validation.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_warmup_settings.inc.php');
@@ -258,6 +259,11 @@ function bratonien_tools_cache_warmup_select(array $scan, array $state, $mode)
if (!isset($known_albums[(int)$album_id])) $new_albums[(int)$album_id] = true;
}
if ($mode === 'rebuild')
{
return array('images'=>$scan['images'], 'new_albums'=>array_map('intval', array_keys($new_albums)));
}
$selected = array();
foreach ($scan['images'] as $image_id=>$image)
{
@@ -299,7 +305,7 @@ function bratonien_tools_cache_warmup_variant_rows(array $image, $stage)
$priority = $max_edge > 0 && $max_edge <= 1920;
if (($stage === 1 && $priority) || ($stage === 2 && !$priority))
{
$variants[] = array('name'=>'standard:'.$type, 'target'=>$derivative->get_path());
$variants[] = array('name'=>'standard:'.$type, 'target'=>$derivative->get_path(), 'derivative'=>$derivative, 'params'=>$params);
}
}
foreach (ImageStdParams::$custom as $key=>$last_used)
@@ -313,7 +319,7 @@ function bratonien_tools_cache_warmup_variant_rows(array $image, $stage)
$priority = $max_edge > 0 && $max_edge <= 1920;
if (($stage === 1 && $priority) || ($stage === 2 && !$priority))
{
$variants[] = array('name'=>'custom:'.$key, 'target'=>$derivative->get_path());
$variants[] = array('name'=>'custom:'.$key, 'target'=>$derivative->get_path(), 'derivative'=>$derivative, 'params'=>$params);
}
}
return $variants;
@@ -371,7 +377,7 @@ function bratonien_tools_cache_warmup_download(array $source, array $credentials
CURLOPT_RETURNTRANSFER=>false,
CURLOPT_FAILONERROR=>false,
CURLOPT_FILE=>$fp,
CURLOPT_USERAGENT=>'Bratonien-WebDAV-Cache-Warmup/0.9.7.1.8',
CURLOPT_USERAGENT=>'Bratonien-WebDAV-Cache-Warmup/0.9.7.1.12',
));
$ok = curl_exec($ch);
$errno = curl_errno($ch);
@@ -456,11 +462,12 @@ function bratonien_tools_cache_warmup_process(array $image, $temp_file, array $c
$pending = array();
foreach ($variants as $variant)
{
if (!$force && is_file($variant['target']) && is_readable($variant['target'])) continue;
$cache_reason = '';
if (!$force && bratonien_tools_webdav_derivative_cache_valid($variant['target'], $variant['derivative'], $variant['params'], $cache_reason)) continue;
$request = bratonien_tools_cache_warmup_i_request($variant['target']);
if ($request !== null) $pending[] = array('name'=>$variant['name'], 'target'=>$variant['target'], 'request'=>$request);
if ($request !== null) $pending[] = $variant + array('request'=>$request);
}
if (!$pending) return array('ok'=>true, 'generated'=>0, 'message'=>'Bereits im Piwigo-Cache vorhanden.');
if (!$pending) return array('ok'=>true, 'generated'=>0, 'message'=>'Bereits gültig im Piwigo-Cache vorhanden.');
$image_lock = null;
if (!bratonien_tools_cache_warmup_image_lock($image_id, $image_lock))
@@ -545,9 +552,10 @@ function bratonien_tools_cache_warmup_process(array $image, $temp_file, array $c
throw new RuntimeException($variant['name'].': '.$call_detail);
}
clearstatcache(true, $variant['target']);
if (!is_file($variant['target']) || !is_readable($variant['target']))
$verify_reason = '';
if (!bratonien_tools_webdav_derivative_cache_valid($variant['target'], $variant['derivative'], $variant['params'], $verify_reason))
{
throw new RuntimeException($variant['name'].': Piwigo hat kein Derivat im eigenen Cache erzeugt.');
throw new RuntimeException($variant['name'].': Piwigo hat kein gültiges Derivat erzeugt ('.$verify_reason.').');
}
$generated++;
}
@@ -681,7 +689,7 @@ function bratonien_tools_cache_warmup_run_stage($connection_id, $stage, array $s
function bratonien_tools_cache_warmup_run($connection_id, $mode)
{
$settings = bratonien_tools_get_webdav_warmup_settings();
if ($mode !== 'manual' && empty($settings['enabled']))
if (!in_array($mode, array('manual','rebuild'), true) && empty($settings['enabled']))
{
bratonien_tools_cache_warmup_log('disabled', array('connection_id'=>$connection_id, 'mode'=>$mode));
return 0;
@@ -704,23 +712,26 @@ function bratonien_tools_cache_warmup_run($connection_id, $mode)
try
{
// Nur der Sync-Worker, der den Prozess-Lock tatsächlich erhalten hat,
// konsumiert seine eigene Prioritätsmarke. Ein zweiter Sync-Aufruf während
// eines laufenden Workers lässt die Marke stehen, damit der aktive Lauf
// Stufe 2 sicher zwischen zwei vollständigen Batches unterbrechen kann.
if ($mode === 'sync' && is_file($priority_file)) @unlink($priority_file);
$scan = bratonien_tools_cache_warmup_scan($connection_id);
$state = bratonien_tools_cache_warmup_load_state($connection_id);
if ($state === null)
{
bratonien_tools_cache_warmup_baseline($connection_id, $scan);
bratonien_tools_cache_warmup_status($connection_id, 'baseline', 'Bestehender produktiver Bestand wurde nur als Ausgangszustand erfasst.', array(
'images'=>count($scan['images']),
'albums'=>count($scan['albums']),
));
bratonien_tools_cache_warmup_log('baseline', array('connection_id'=>$connection_id, 'images'=>count($scan['images']), 'albums'=>count($scan['albums'])));
return 0;
if ($mode === 'rebuild')
{
$state = array('albums'=>array(), 'images'=>array(), 'last_periodic_at'=>0);
}
else
{
bratonien_tools_cache_warmup_baseline($connection_id, $scan);
bratonien_tools_cache_warmup_status($connection_id, 'baseline', 'Bestehender produktiver Bestand wurde nur als Ausgangszustand erfasst.', array(
'images'=>count($scan['images']),
'albums'=>count($scan['albums']),
));
bratonien_tools_cache_warmup_log('baseline', array('connection_id'=>$connection_id, 'images'=>count($scan['images']), 'albums'=>count($scan['albums'])));
return 0;
}
}
if ($mode === 'periodic')
@@ -752,6 +763,15 @@ function bratonien_tools_cache_warmup_run($connection_id, $mode)
return 0;
}
if ($mode === 'rebuild')
{
foreach ($selected as $image_id=>$image)
{
if (!isset($state['images'][(string)$image_id]) || !is_array($state['images'][(string)$image_id])) $state['images'][(string)$image_id] = array();
unset($state['images'][(string)$image_id]['stage1_signature'], $state['images'][(string)$image_id]['stage2_signature']);
}
}
$stage1 = bratonien_tools_cache_warmup_run_stage($connection_id, 1, $selected, $state, $credentials, $settings['batch_size'], $priority_file);
$stage2_candidates = array();
foreach ($selected as $image_id=>$image)
@@ -776,7 +796,7 @@ function bratonien_tools_cache_warmup_run($connection_id, $mode)
bratonien_tools_cache_warmup_save_state($connection_id, $state);
$failed = (int)$stage1['failed'] + (int)$stage2['failed'];
bratonien_tools_cache_warmup_status($connection_id, $failed ? 'error' : 'complete', $failed ? 'Warmup mit einzelnen Fehlern beendet.' : 'Warmup vollständig beendet.', array(
bratonien_tools_cache_warmup_status($connection_id, $failed ? 'error' : 'complete', $failed ? 'Warmup mit einzelnen Fehlern beendet.' : ($mode === 'rebuild' ? 'WebDAV-Bildcache vollständig wiederaufgebaut.' : 'Warmup vollständig beendet.'), array(
'mode'=>$mode,
'stage1_failed'=>(int)$stage1['failed'],
'stage2_failed'=>(int)$stage2['failed'],
@@ -795,7 +815,7 @@ $mode = 'periodic';
foreach ($argv as $arg)
{
if (preg_match('/^--connection-id=(\d+)$/', $arg, $m)) $connection_id = (int)$m[1];
elseif (preg_match('/^--mode=(sync|periodic|manual)$/', $arg, $m)) $mode = $m[1];
elseif (preg_match('/^--mode=(sync|periodic|manual|rebuild)$/', $arg, $m)) $mode = $m[1];
}
if ($connection_id < 1)
{

View File

@@ -23,7 +23,7 @@ $_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/webdav-warmup-dispatch.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Warmup-Dispatcher/0.9.7.1.8';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Warmup-Dispatcher/0.9.7.1.12';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
@@ -40,13 +40,13 @@ $wait = false;
$connection_filter = 0;
foreach ($argv as $arg)
{
if (preg_match('/^--mode=(sync|periodic|manual)$/', $arg, $m)) $mode = $m[1];
if (preg_match('/^--mode=(sync|periodic|manual|rebuild)$/', $arg, $m)) $mode = $m[1];
elseif (preg_match('/^--connection-id=(\d+)$/', $arg, $m)) $connection_filter = (int)$m[1];
elseif ($arg === '--wait') $wait = true;
}
$settings = bratonien_tools_get_webdav_warmup_settings();
if ($mode !== 'manual' && empty($settings['enabled']))
if (!in_array($mode, array('manual','rebuild'), true) && empty($settings['enabled']))
{
fwrite(STDOUT, "WebDAV-Cache-Warmup ist deaktiviert.\n");
exit(0);
@@ -141,9 +141,6 @@ foreach (bratonien_tools_nc_connector_connections() as $connection)
if ($mode === 'sync')
{
// Ein bereits laufender Worker darf seinen aktuellen Bild-/Batchvorgang
// vollständig restaurieren. Der Waiter startet den priorisierten Sync-Lauf
// danach automatisch, statt die Prioritätsmarke verloren gehen zu lassen.
$base = escapeshellarg('/bin/bash').' '.escapeshellarg($priority_waiter).' '.escapeshellarg($state_dir).' '.$worker_command;
}
else