$value) { if (is_bool($value)) $value = $value ? '1' : '0'; elseif ($value === null) $value = 'NULL'; elseif (is_array($value) || is_object($value)) $value = json_encode($value, JSON_UNESCAPED_SLASHES); $parts[] = $key.'='.str_replace(array("\r", "\n"), array('\\r', '\\n'), (string)$value); } fwrite(STDOUT, implode(' ', $parts)."\n"); } function bratonien_tools_warmup_absolute_source_path(array $image_row) { $path = (string)($image_row['path'] ?? ''); if ($path === '') return null; if (strpos($path, '/') === 0) return $path; return PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $path), '/'); } function bratonien_tools_warmup_connection_credentials($connection_id) { $connection = bratonien_tools_nc_connector_connection((int)$connection_id, true); if (!$connection || empty($connection['enabled'])) return null; $plain = bratonien_tools_nc_connector_decrypt_secret((string)($connection['secret_blob'] ?? '')); $credentials = json_decode($plain, true); if (!is_array($credentials)) return null; $config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array(); $base_url = rtrim((string)($config['nextcloud_url'] ?? ''), '/'); $user = trim((string)($credentials['nextcloud_user'] ?? '')); $password = (string)($credentials['nextcloud_password'] ?? ''); if ($base_url === '' || $user === '' || $password === '') return null; return array( 'connection'=>$connection, 'config'=>$config, 'base_url'=>$base_url, 'user'=>$user, 'password'=>$password, ); } function bratonien_tools_warmup_quote_path($path) { $parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen')); return implode('/', array_map('rawurlencode', $parts)); } function bratonien_tools_warmup_download(array $source, array $credentials, $destination, &$detail=null) { $detail = ''; $webdav_path = trim((string)($source['webdav_path'] ?? ''), '/'); if ($webdav_path === '') { $detail = 'WebDAV-Pfad fehlt.'; return false; } $fp = @fopen($destination, 'xb'); if (!$fp) { $detail = 'Temporaere Datei konnte nicht angelegt werden.'; return false; } $url = $credentials['base_url'].'/remote.php/dav/files/'.rawurlencode($credentials['user']).'/'.bratonien_tools_warmup_quote_path($webdav_path); $ch = curl_init($url); curl_setopt_array($ch, array( CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 300, CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_USERPWD => $credentials['user'].':'.$credentials['password'], CURLOPT_RETURNTRANSFER => false, CURLOPT_FAILONERROR => false, CURLOPT_FILE => $fp, CURLOPT_USERAGENT => 'Bratonien-WebDAV-Warmup/0.9.7.1.8', )); $ok = curl_exec($ch); $errno = curl_errno($ch); $error = curl_error($ch); $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); fclose($fp); if ($ok === false || $errno !== 0 || $http < 200 || $http >= 300) { @unlink($destination); $detail = 'Nextcloud-Download fehlgeschlagen (HTTP '.$http.', cURL '.$errno.($error !== '' ? ': '.$error : '').').'; return false; } if (!is_file($destination) || filesize($destination) < 1 || @getimagesize($destination) === false) { @unlink($destination); $detail = 'Geladene Datei ist kein lesbares Bild.'; return false; } return true; } function bratonien_tools_warmup_state_file($connection_id) { return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.connection-'.(int)$connection_id.'.json'; } function bratonien_tools_warmup_load_state($connection_id) { $file = bratonien_tools_warmup_state_file($connection_id); if (!is_file($file) || !is_readable($file)) { return array('albums'=>array(), 'images'=>array(), 'updated_at'=>0); } $raw = @file_get_contents($file); $state = $raw !== false ? json_decode($raw, true) : null; if (!is_array($state)) return array('albums'=>array(), 'images'=>array(), 'updated_at'=>0); if (!isset($state['albums']) || !is_array($state['albums'])) $state['albums'] = array(); if (!isset($state['images']) || !is_array($state['images'])) $state['images'] = array(); return $state; } function bratonien_tools_warmup_save_state($connection_id, array $state) { $file = bratonien_tools_warmup_state_file($connection_id); $directory = dirname($file); if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory)) { throw new RuntimeException('Warmup-State-Verzeichnis konnte nicht angelegt werden.'); } $state['updated_at'] = time(); $json = json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); if ($json === false) throw new RuntimeException('Warmup-State konnte nicht serialisiert werden.'); $tmp = $file.'.tmp-'.bin2hex(random_bytes(4)); if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false) throw new RuntimeException('Warmup-State konnte nicht geschrieben werden.'); @chmod($tmp, 0664); if (!@rename($tmp, $file)) { @unlink($tmp); throw new RuntimeException('Warmup-State konnte nicht atomar gespeichert werden.'); } } function bratonien_tools_warmup_scan_connection($connection_id) { $images = array(); $albums = array(); $result = pwg_query('SELECT id, path, width, height, rotation, coi FROM '.IMAGES_TABLE.' ORDER BY id'); while ($row = pwg_db_fetch_assoc($result)) { $image_id = (int)$row['id']; $source = bratonien_tools_webdav_materialize_source_info($image_id); if (!$source || (int)$source['connection_id'] !== (int)$connection_id) continue; $row['id'] = $image_id; $images[$image_id] = array('row'=>$row, 'source'=>$source, 'albums'=>array()); } if (!$images) return array('images'=>array(), 'albums'=>array()); $ids = array_keys($images); foreach (array_chunk($ids, 500) as $chunk) { $sql = 'SELECT image_id, category_id FROM '.IMAGE_CATEGORY_TABLE.' WHERE image_id IN ('.implode(',', array_map('intval', $chunk)).')'; $rel = pwg_query($sql); while ($item = pwg_db_fetch_assoc($rel)) { $image_id = (int)$item['image_id']; $category_id = (int)$item['category_id']; if (!isset($images[$image_id]) || $category_id < 1) continue; $images[$image_id]['albums'][$category_id] = true; $albums[$category_id] = true; } } foreach ($images as &$image) { $image['albums'] = array_map('intval', array_keys($image['albums'])); } unset($image); return array('images'=>$images, 'albums'=>array_map('intval', array_keys($albums))); } function bratonien_tools_warmup_signature(array $source) { return sha1(implode('|', array( (int)($source['connection_id'] ?? 0), (int)($source['root_fileid'] ?? 0), (int)($source['fileid'] ?? 0), trim((string)($source['webdav_path'] ?? ''), '/'), (string)($source['etag'] ?? ''), (int)($source['size'] ?? 0), (int)($source['width'] ?? 0), (int)($source['height'] ?? 0), ))); } function bratonien_tools_warmup_select_ids(array $scan, array $state, $mode) { $new_albums = array(); $known_albums = array_fill_keys(array_map('intval', array_keys((array)$state['albums'])), true); foreach ($scan['albums'] as $category_id) { if (!isset($known_albums[(int)$category_id])) $new_albums[(int)$category_id] = true; } $selected = array(); foreach ($scan['images'] as $image_id => $image) { $signature = bratonien_tools_warmup_signature($image['source']); $known_signature = (string)($state['images'][(string)$image_id]['signature'] ?? ''); $in_new_album = false; foreach ($image['albums'] as $category_id) { if (isset($new_albums[(int)$category_id])) { $in_new_album = true; break; } } if ($mode === 'sync') { if ($in_new_album) $selected[$image_id] = $image; } else { if ($known_signature === '' || !hash_equals($known_signature, $signature)) $selected[$image_id] = $image; } } return array('images'=>$selected, 'new_albums'=>array_map('intval', array_keys($new_albums))); } function bratonien_tools_warmup_variants_for_stage(array $image_row, $stage) { $src = new SrcImage($image_row); $variants = array(); foreach (ImageStdParams::get_defined_type_map() as $type => $params) { $derivative = new DerivativeImage($params, $src); if ($derivative->same_as_source()) continue; $size = $derivative->get_size(); $max_edge = max((int)($size[0] ?? 0), (int)($size[1] ?? 0)); $is_priority = $max_edge > 0 && $max_edge <= 1920; if (($stage === 1 && $is_priority) || ($stage === 2 && !$is_priority)) { $variants[] = array('name'=>'standard:'.$type, 'derivative'=>$derivative); } } foreach (ImageStdParams::$custom as $custom_key => $last_used) { $params = bratonien_tools_webdav_materialize_custom_params($custom_key); if (!$params) continue; $derivative = new DerivativeImage($params, $src); if ($derivative->same_as_source()) continue; $size = $derivative->get_size(); $max_edge = max((int)($size[0] ?? 0), (int)($size[1] ?? 0)); $is_priority = $max_edge > 0 && $max_edge <= 1920; if (($stage === 1 && $is_priority) || ($stage === 2 && !$is_priority)) { $variants[] = array('name'=>'custom:'.$custom_key, 'derivative'=>$derivative); } } return $variants; } function bratonien_tools_warmup_i_url($target_path, $base_url) { $derivative_root = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR; if (strpos($target_path, $derivative_root) !== 0) return null; $location = ltrim(substr($target_path, strlen($derivative_root)), '/'); if ($location === '') return null; $segments = array_map('rawurlencode', explode('/', $location)); return rtrim((string)$base_url, '/').'/i.php?/'.implode('/', $segments); } function bratonien_tools_warmup_call_piwigo($url, &$detail=null) { $detail = ''; $ch = curl_init($url); curl_setopt_array($ch, array( CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 300, CURLOPT_RETURNTRANSFER => false, CURLOPT_FAILONERROR => false, CURLOPT_WRITEFUNCTION => function($ch, $data) { return strlen($data); }, CURLOPT_USERAGENT => 'Bratonien-WebDAV-Warmup/0.9.7.1.8', )); $ok = curl_exec($ch); $errno = curl_errno($ch); $error = curl_error($ch); $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($ok === false || $errno !== 0) { $detail = 'Piwigo-Aufruf fehlgeschlagen (cURL '.$errno.($error !== '' ? ': '.$error : '').').'; return false; } if ($http < 200 || $http >= 400) { $detail = 'Piwigo i.php antwortete mit HTTP '.$http.'.'; return false; } return true; } function bratonien_tools_warmup_acquire_sync_guard(array $credentials, &$handle=null, &$detail=null) { $handle = null; $detail = ''; $state_dir = rtrim((string)($credentials['config']['state_dir'] ?? ''), '/'); if ($state_dir === '') { $detail = 'State-Verzeichnis der Verbindung fehlt.'; return false; } $lock_path = $state_dir.'/webdav-sync.lock'; $handle = @fopen($lock_path, 'c'); if (!$handle) { $detail = 'WebDAV-Sync-Lock konnte nicht geöffnet werden.'; return false; } if (!@flock($handle, LOCK_SH | LOCK_NB)) { fclose($handle); $handle = null; $detail = 'WebDAV-Synchronisierung läuft; Warmup wird später fortgesetzt.'; return false; } return true; } function bratonien_tools_warmup_source_lock($image_id, &$handle=null) { $dir = PHPWG_ROOT_PATH.'upload/bratonien-webdav-materialize'; if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) return false; $path = $dir.'/image-'.(int)$image_id.'.lock'; $handle = @fopen($path, 'c'); if (!$handle) return false; return @flock($handle, LOCK_EX); } function bratonien_tools_warmup_release_lock(&$handle) { if (is_resource($handle)) { @flock($handle, LOCK_UN); fclose($handle); } $handle = null; } function bratonien_tools_warmup_validate_source(array $image_row, array $source, $temp_file, &$resolved=null, &$logical=null, &$detail=null) { $resolved = null; $logical = bratonien_tools_warmup_absolute_source_path($image_row); $detail = ''; if (!$logical) { $detail = 'Piwigo-Quellpfad fehlt.'; return false; } $resolved = realpath($logical); if ($resolved === false || !is_file($resolved)) { $detail = 'Piwigo-Placeholder konnte nicht aufgelöst werden.'; return false; } $normalized = str_replace('\\', '/', $resolved); $expected = '/nc-webdav-source/connection-'.(int)$source['connection_id'].'/root-'.(int)$source['root_fileid'].'/'; if (strpos($normalized, $expected) === false) { $detail = 'Aufgelöster Quellpfad gehört nicht zur erwarteten Verbindung/Wurzel.'; return false; } if (!is_file($temp_file) || !is_readable($temp_file) || @getimagesize($temp_file) === false) { $detail = 'Batch-Quelldatei ist nicht vollständig lesbar.'; return false; } return true; } function bratonien_tools_warmup_restore_placeholder($resolved, $backup, $staging, &$detail=null) { $detail = ''; @unlink($staging); if (is_file($resolved) || is_link($resolved)) { if (!@unlink($resolved)) { $detail = 'Temporäre Quelle konnte beim Restore nicht entfernt werden.'; return false; } } if (!is_file($backup)) { $detail = 'Placeholder-Backup fehlt beim Restore.'; return false; } if (!@rename($backup, $resolved)) { $detail = 'Placeholder konnte nicht restauriert werden.'; return false; } clearstatcache(true, $resolved); if (!is_file($resolved) || filesize($resolved) < 1) { $detail = 'Restaurierter Placeholder ist nicht lesbar.'; return false; } return true; } function bratonien_tools_warmup_process_image(array $image, $temp_file, array $credentials, $stage, array $settings) { $image_row = $image['row']; $image_id = (int)$image_row['id']; $source = bratonien_tools_webdav_materialize_source_info($image_id); if (!$source || (int)$source['connection_id'] !== (int)$credentials['connection']['id']) { return array('ok'=>false, 'fatal'=>false, 'message'=>'Quellzuordnung hat sich seit Batchbeginn geändert.'); } $image_lock = null; if (!bratonien_tools_warmup_source_lock($image_id, $image_lock)) { return array('ok'=>false, 'fatal'=>false, 'message'=>'Bild-Lock konnte nicht gesetzt werden.'); } $sync_lock = null; $detail = ''; if (!bratonien_tools_warmup_acquire_sync_guard($credentials, $sync_lock, $detail)) { bratonien_tools_warmup_release_lock($image_lock); return array('ok'=>false, 'fatal'=>false, 'defer'=>true, 'message'=>$detail); } $logical = null; $resolved = null; if (!bratonien_tools_warmup_validate_source($image_row, $source, $temp_file, $resolved, $logical, $detail)) { bratonien_tools_warmup_release_lock($sync_lock); bratonien_tools_warmup_release_lock($image_lock); return array('ok'=>false, 'fatal'=>false, 'message'=>$detail); } $variants = bratonien_tools_warmup_variants_for_stage($image_row, $stage); $pending = array(); foreach ($variants as $variant) { $target = $variant['derivative']->get_path(); if ($target !== '' && is_file($target) && is_readable($target)) continue; $url = bratonien_tools_warmup_i_url($target, $settings['piwigo_base_url']); if ($url !== null) $pending[] = array('name'=>$variant['name'], 'target'=>$target, 'url'=>$url); } if (!$pending) { bratonien_tools_warmup_release_lock($sync_lock); bratonien_tools_warmup_release_lock($image_lock); return array('ok'=>true, 'generated'=>0, 'message'=>'Alle Derivate der Stufe bereits vorhanden.'); } $request_id = substr(bin2hex(random_bytes(8)), 0, 8); $backup = $resolved.'.bratonien-placeholder-'.$request_id; $staging = $logical.'.bratonien-source-'.$request_id; @unlink($backup); @unlink($staging); if (!@copy($temp_file, $staging)) { bratonien_tools_warmup_release_lock($sync_lock); bratonien_tools_warmup_release_lock($image_lock); return array('ok'=>false, 'fatal'=>false, 'message'=>'Batch-Quelle konnte nicht am Piwigo-Pfad bereitgestellt werden.'); } $swapped = false; $generated = 0; try { if (!@rename($resolved, $backup)) throw new RuntimeException('Placeholder konnte nicht gesichert werden.'); if (!@rename($staging, $resolved)) { @rename($backup, $resolved); throw new RuntimeException('Temporäre Quelle konnte nicht eingesetzt werden.'); } $swapped = true; clearstatcache(true, $resolved); if (@getimagesize($resolved) === false) throw new RuntimeException('Temporäre Quelle ist am Piwigo-Pfad nicht als Bild lesbar.'); foreach ($pending as $variant) { $call_detail = ''; if (!bratonien_tools_warmup_call_piwigo($variant['url'], $call_detail)) { throw new RuntimeException($variant['name'].': '.$call_detail); } clearstatcache(true, $variant['target']); if (!is_file($variant['target']) || !is_readable($variant['target'])) { throw new RuntimeException($variant['name'].': Piwigo hat kein Cache-Derivat erzeugt.'); } $generated++; } } catch (Throwable $e) { $restore_detail = ''; $restored = $swapped ? bratonien_tools_warmup_restore_placeholder($resolved, $backup, $staging, $restore_detail) : true; if (!$swapped) @unlink($staging); bratonien_tools_warmup_release_lock($sync_lock); bratonien_tools_warmup_release_lock($image_lock); if (!$restored) { return array('ok'=>false, 'fatal'=>true, 'message'=>$e->getMessage().' Restore fehlgeschlagen: '.$restore_detail); } return array('ok'=>false, 'fatal'=>false, 'message'=>$e->getMessage()); } $restore_detail = ''; $restored = bratonien_tools_warmup_restore_placeholder($resolved, $backup, $staging, $restore_detail); bratonien_tools_warmup_release_lock($sync_lock); bratonien_tools_warmup_release_lock($image_lock); if (!$restored) { return array('ok'=>false, 'fatal'=>true, 'message'=>$restore_detail); } return array('ok'=>true, 'generated'=>$generated, 'message'=>'Piwigo-Derivate erzeugt.'); } function bratonien_tools_warmup_run($connection_id, $mode) { $settings = bratonien_tools_get_webdav_warmup_settings(); if (empty($settings['enabled'])) { bratonien_tools_warmup_log('disabled', array('connection_id'=>$connection_id)); return 0; } $credentials = bratonien_tools_warmup_connection_credentials($connection_id); if (!$credentials) throw new RuntimeException('WebDAV-Verbindung oder Zugangsdaten konnten nicht geladen werden.'); $state = bratonien_tools_warmup_load_state($connection_id); $scan = bratonien_tools_warmup_scan_connection($connection_id); $selection = bratonien_tools_warmup_select_ids($scan, $state, $mode); $selected = $selection['images']; bratonien_tools_warmup_log('scan', array( 'connection_id'=>$connection_id, 'mode'=>$mode, 'albums'=>count($scan['albums']), 'new_albums'=>count($selection['new_albums']), 'images'=>count($scan['images']), 'selected'=>count($selected), 'batch_size'=>$settings['batch_size'], )); if (!$selected) { foreach ($scan['albums'] as $category_id) $state['albums'][(string)$category_id] = array('seen_at'=>time()); bratonien_tools_warmup_save_state($connection_id, $state); return 0; } $temp_root = PHPWG_ROOT_PATH.'upload/bratonien-webdav-warmup'; if (!is_dir($temp_root) && !@mkdir($temp_root, 0775, true) && !is_dir($temp_root)) throw new RuntimeException('Warmup-Temp-Verzeichnis konnte nicht angelegt werden.'); $successful = array(); $failed = 0; $ids = array_keys($selected); $batch_number = 0; foreach (array_chunk($ids, max(1, (int)$settings['batch_size'])) as $batch_ids) { $batch_number++; $batch_dir = $temp_root.'/connection-'.(int)$connection_id.'-'.date('YmdHis').'-'.bin2hex(random_bytes(4)); if (!@mkdir($batch_dir, 0775, true) && !is_dir($batch_dir)) throw new RuntimeException('Batch-Verzeichnis konnte nicht angelegt werden.'); $downloaded = array(); foreach ($batch_ids as $image_id) { $image = $selected[$image_id]; $source = $image['source']; $temp_file = $batch_dir.'/image-'.(int)$image_id.'.img'; $detail = ''; if (bratonien_tools_warmup_download($source, $credentials, $temp_file, $detail)) { $downloaded[$image_id] = $temp_file; } else { $failed++; bratonien_tools_warmup_log('download_failed', array('image_id'=>$image_id, 'detail'=>$detail)); } } bratonien_tools_warmup_log('batch_downloaded', array('batch'=>$batch_number, 'requested'=>count($batch_ids), 'downloaded'=>count($downloaded))); foreach (array(1, 2) as $stage) { foreach ($downloaded as $image_id => $temp_file) { $result = bratonien_tools_warmup_process_image($selected[$image_id], $temp_file, $credentials, $stage, $settings); bratonien_tools_warmup_log('image_stage', array( 'batch'=>$batch_number, 'stage'=>$stage, 'image_id'=>$image_id, 'ok'=>!empty($result['ok']), 'generated'=>(int)($result['generated'] ?? 0), 'message'=>(string)($result['message'] ?? ''), )); if (!empty($result['fatal'])) { throw new RuntimeException('Sicherheitsabbruch bei Bild #'.$image_id.': '.(string)$result['message']); } if (empty($result['ok'])) { $failed++; continue; } if ($stage === 2) $successful[$image_id] = true; } } foreach ($downloaded as $temp_file) @unlink($temp_file); @rmdir($batch_dir); } foreach ($scan['albums'] as $category_id) $state['albums'][(string)$category_id] = array('seen_at'=>time()); foreach ($successful as $image_id => $true) { if (!isset($scan['images'][$image_id])) continue; $state['images'][(string)$image_id] = array( 'signature'=>bratonien_tools_warmup_signature($scan['images'][$image_id]['source']), 'completed_at'=>time(), ); } bratonien_tools_warmup_save_state($connection_id, $state); bratonien_tools_warmup_log('complete', array('connection_id'=>$connection_id, 'successful'=>count($successful), 'failed'=>$failed)); return $failed > 0 ? 2 : 0; } $connection_id = 0; $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]; } if ($connection_id < 1) { fwrite(STDERR, "--connection-id fehlt oder ist ungültig.\n"); exit(1); } try { exit(bratonien_tools_warmup_run($connection_id, $mode)); } catch (Throwable $e) { fwrite(STDERR, '[BRAT-WARMUP] fatal '.$e->getMessage()."\n"); exit(1); }