Compare commits

..
Author SHA1 Message Date
Terranom674 ef69bee47e Disable automatic workflow triggers; manual runs only 2026-10-19 20:30:49 +02:00
11 changed files with 688 additions and 692 deletions

No files matched your search

+249 -87
View File
@@ -18,7 +18,7 @@ function bratonien_tools_register_nc_productive_ws_methods($arr)
'info' => 'Piwigo storage site to synchronize. Default: 1.',
),
),
'Runs the approved Piwigo filesystem synchronization for the Bratonien NC Connector.',
'Synchronizes the NC Connector into the existing Piwigo album hierarchy.',
null,
array(
'admin_only' => true,
@@ -27,117 +27,279 @@ function bratonien_tools_register_nc_productive_ws_methods($arr)
);
}
function bratonien_tools_nc_productive_error(&$errors, $path, $type)
{
$errors[] = array(
'path' => (string)$path,
'type' => (string)$type,
);
}
function bratonien_tools_nc_relative_path($basedir, $path)
{
$basedir = rtrim(str_replace('\\', '/', (string)$basedir), '/');
$path = str_replace('\\', '/', (string)$path);
if ($path === $basedir) return '';
if (strpos($path, $basedir.'/') !== 0)
{
throw new RuntimeException('WebDAV-Pfad liegt ausserhalb der Connector-Wurzel: '.$path);
}
return trim(substr($path, strlen($basedir)), '/');
}
function bratonien_tools_nc_find_album($parent_id, $dir, $name, $excluded_site_id)
{
$where_parent = $parent_id === null ? 'id_uppercat IS NULL' : 'id_uppercat='.(int)$parent_id;
$dir_sql = pwg_db_real_escape_string((string)$dir);
$name_sql = pwg_db_real_escape_string((string)$name);
$query = '
SELECT id, dir, name
FROM '.CATEGORIES_TABLE.'
WHERE '.$where_parent.'
AND (
dir = \''.$dir_sql.'\'
OR LOWER(name) = LOWER(\''.$name_sql.'\')
)
ORDER BY CASE WHEN dir = \''.$dir_sql.'\' THEN 0 ELSE 1 END, id
LIMIT 1
;';
$result = pwg_query($query);
if (!pwg_db_num_rows($result)) return null;
$row = pwg_db_fetch_assoc($result);
return (int)$row['id'];
}
function bratonien_tools_nc_ensure_album_path($relative_dir, $excluded_site_id, array &$cache, array &$created_ids)
{
$relative_dir = trim((string)$relative_dir, '/');
if ($relative_dir === '') return null;
if (isset($cache[$relative_dir])) return $cache[$relative_dir];
$parts = explode('/', $relative_dir);
$parent_id = null;
$path = '';
foreach ($parts as $part)
{
if ($part === '') continue;
$path = $path === '' ? $part : $path.'/'.$part;
if (isset($cache[$path]))
{
$parent_id = $cache[$path];
continue;
}
$display_name = str_replace('_', ' ', $part);
$album_id = bratonien_tools_nc_find_album($parent_id, $part, $display_name, $excluded_site_id);
if ($album_id === null)
{
$created = create_virtual_category($display_name, $parent_id);
if (!is_array($created) || empty($created['id']))
{
$detail = is_array($created) && !empty($created['error']) ? (string)$created['error'] : 'unbekannter Fehler';
throw new RuntimeException('Album "'.$display_name.'" konnte nicht angelegt werden: '.$detail);
}
$album_id = (int)$created['id'];
pwg_query('UPDATE '.CATEGORIES_TABLE." SET status='private' WHERE id=".$album_id.' LIMIT 1');
add_permission_on_category(array($album_id), get_admins());
$created_ids[] = $album_id;
}
$cache[$path] = $album_id;
$parent_id = $album_id;
}
return $parent_id;
}
function bratonien_tools_nc_managed_images($basedir)
{
$prefix = rtrim((string)$basedir, '/').'/';
$escaped = pwg_db_real_escape_string(addcslashes($prefix, '_%\\'));
$query = "SELECT id, path FROM ".IMAGES_TABLE." WHERE path LIKE '".$escaped."%' ESCAPE '\\\\'";
return simple_hash_from_query($query, 'id', 'path');
}
function bratonien_tools_nc_remove_storage_categories($site_id)
{
// Bestehende Piwigo-Alben gehoeren nicht automatisch dem Connector.
// Ohne eindeutige Connector-Eigentumsmarkierung darf hier nichts geloescht werden.
return 0;
}
function bratonien_tools_ws_nc_sync_productive($params, &$service)
{
global $conf, $user, $template, $page;
global $conf, $user;
$piwigo_version = defined('PHPWG_VERSION') ? (string)PHPWG_VERSION : '';
if ($piwigo_version !== '16.4.0')
{
return new PwgError(
409,
'Bratonien API synchronization is not approved for Piwigo '.$piwigo_version.'. Use the administrator fallback until this Piwigo version has been verified.'
);
return new PwgError(409, 'Bratonien API synchronization is not approved for Piwigo '.$piwigo_version.'.');
}
if (empty($conf['enable_synchronization']))
{
return new PwgError(403, 'Piwigo filesystem synchronization is disabled.');
}
$site_id = isset($params['site_id']) ? (int)$params['site_id'] : 1;
if ($site_id < 1)
{
return new PwgError(400, 'Invalid site_id.');
}
if ($site_id < 1) return new PwgError(400, 'Invalid site_id.');
$saved_get = $_GET;
$saved_post = $_POST;
$saved_request_method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
$saved_default_status = isset($conf['newcat_default_status']) ? $conf['newcat_default_status'] : null;
$saved_template_dirs = null;
$output = '';
$counts = array();
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
include_once(PHPWG_ROOT_PATH.'admin/site_reader_local.php');
$result = pwg_query('SELECT galleries_url FROM '.SITES_TABLE.' WHERE id='.$site_id.' LIMIT 1');
if (!pwg_db_num_rows($result)) return new PwgError(404, 'Piwigo site does not exist.');
list($site_url) = pwg_db_fetch_row($result);
if (url_is_remote($site_url)) return new PwgError(400, 'Remote Piwigo sites are not supported.');
$site_reader = new LocalSiteReader($site_url);
if (!$site_reader->open()) return new PwgError(500, 'Piwigo could not open the configured local site.');
$basedir = preg_replace('#/*$#', '', (string)$site_url);
$errors = array();
$counts = array(
'reused_categories'=>0,
'new_categories'=>0,
'removed_duplicate_categories'=>0,
'new_elements'=>0,
'del_elements'=>0,
'upd_elements'=>0,
);
try
{
if (!defined('IN_ADMIN'))
list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW()'));
$fs_dirs = $site_reader->get_full_directories($basedir);
usort($fs_dirs, function($a, $b)
{
define('IN_ADMIN', true);
return substr_count((string)$a, '/') <=> substr_count((string)$b, '/');
});
$album_cache = array();
$created_ids = array();
$dir_to_album = array();
foreach ($fs_dirs as $full_dir)
{
$relative = bratonien_tools_nc_relative_path($basedir, $full_dir);
if ($relative === '') continue;
$before = count($created_ids);
$album_id = bratonien_tools_nc_ensure_album_path($relative, $site_id, $album_cache, $created_ids);
if ($album_id !== null)
{
$dir_to_album[$full_dir] = $album_id;
if (count($created_ids) === $before) $counts['reused_categories']++;
}
}
$counts['new_categories'] = count($created_ids);
$fs = $site_reader->get_elements($basedir);
$db_elements = bratonien_tools_nc_managed_images($basedir);
$db_by_path = array_flip($db_elements);
// Nicht-destruktiver Schutz: Bestehende Piwigo-Bilder werden niemals allein
// deshalb geloescht, weil sie im aktuellen WebDAV-Scan nicht vorkommen.
// Das Entfernen ist erst wieder zulaessig, wenn Connector-Eigentum eindeutig
// und verbindungsbezogen gespeichert wird.
$next_element_id = pwg_db_nextval('id', IMAGES_TABLE);
$image_inserts = array();
$image_links = array();
$new_ids = array();
$all_ids = array();
foreach ($fs as $path=>$file_info)
{
$dirname = dirname($path);
$relative_dir = bratonien_tools_nc_relative_path($basedir, $dirname);
$category_id = null;
if ($relative_dir !== '')
{
$category_id = $dir_to_album[$dirname] ?? bratonien_tools_nc_ensure_album_path($relative_dir, $site_id, $album_cache, $created_ids);
}
if (isset($db_by_path[$path]))
{
// Altbestand niemals umhaengen. Vorhandene Bild-Album-Zuordnungen bleiben
// exakt bestehen; der Connector darf nur neue Datensaetze ergaenzen.
$all_ids[] = (int)$db_by_path[$path];
continue;
}
$filename = basename($path);
if (!preg_match($conf['sync_chars_regex'], $filename))
{
bratonien_tools_nc_productive_error($errors, $path, 'PWG-UPDATE-1');
continue;
}
$id = $next_element_id++;
$image_inserts[] = array(
'id'=>$id,
'file'=>$filename,
'name'=>get_name_from_file($filename),
'date_available'=>$dbnow,
'path'=>$path,
'representative_ext'=>$file_info['representative_ext'],
'storage_category_id'=>null,
'added_by'=>(int)$user['id'],
);
if ($category_id !== null)
{
$image_links[] = array('image_id'=>$id, 'category_id'=>$category_id);
}
$new_ids[] = $id;
$all_ids[] = $id;
}
if (is_object($template) && isset($template->smarty) && method_exists($template, 'set_template_dir'))
if ($image_inserts)
{
$saved_template_dirs = $template->smarty->getTemplateDir();
$template->set_template_dir(PHPWG_ROOT_PATH.'admin/themes/default/template');
mass_inserts(IMAGES_TABLE, array_keys($image_inserts[0]), $image_inserts);
if ($image_links) mass_inserts(IMAGE_CATEGORY_TABLE, array_keys($image_links[0]), $image_links);
pwg_activity('photo', $new_ids, 'add', array('sync'=>true));
$counts['new_elements'] = count($new_ids);
}
$conf['newcat_default_status'] = 'private';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_GET = array(
'page' => 'site_update',
'site' => $site_id,
// Bestehende Bilder werden nicht durch den Connector aktualisiert. Nur neu
// angelegte Connector-Bilder erhalten die aus der Quelle ermittelten Attribute.
$updates = array();
foreach ($new_ids as $id)
{
$path_result = pwg_query('SELECT path FROM '.IMAGES_TABLE.' WHERE id='.(int)$id.' LIMIT 1');
if (!pwg_db_num_rows($path_result)) continue;
list($path) = pwg_db_fetch_row($path_result);
$data = $site_reader->get_element_update_attributes($path);
if (!is_array($data)) continue;
$data['id'] = (int)$id;
$updates[] = $data;
}
if ($updates)
{
mass_updates(
IMAGES_TABLE,
array('primary'=>array('id'), 'update'=>$site_reader->get_update_attributes()),
$updates
);
}
$counts['upd_elements'] = count($updates);
images_integrity();
categories_integrity();
update_uppercats();
update_category('all');
update_global_rank();
invalidate_user_cache();
return array(
'mode'=>'productive',
'piwigo_version'=>$piwigo_version,
'site_id'=>$site_id,
'site_url'=>$site_url,
'counts'=>$counts,
'errors'=>$errors,
'database_writes'=>array_sum($counts) > 0,
);
$_POST = array(
'sync' => 'files',
'display_info' => 1,
'privacy_level' => 0,
'sync_meta' => 1,
'simulate' => 0,
'subcats-included' => 1,
'bratonien_connector' => 1,
'submit' => 1,
);
ob_start();
include(PHPWG_ROOT_PATH.'admin/site_update.php');
$output = (string)ob_get_clean();
}
catch (Throwable $e)
catch (Throwable $error)
{
if (ob_get_level() > 0)
{
ob_end_clean();
}
return new PwgError(500, 'Piwigo core synchronization failed: '.$e->getMessage());
return new PwgError(500, 'Bratonien NC synchronization failed: '.$error->getMessage());
}
finally
{
$_GET = $saved_get;
$_POST = $saved_post;
if ($saved_template_dirs !== null && is_object($template) && isset($template->smarty))
{
$template->smarty->setTemplateDir($saved_template_dirs);
}
if ($saved_default_status === null)
{
unset($conf['newcat_default_status']);
}
else
{
$conf['newcat_default_status'] = $saved_default_status;
}
if ($saved_request_method === null)
{
unset($_SERVER['REQUEST_METHOD']);
}
else
{
$_SERVER['REQUEST_METHOD'] = $saved_request_method;
}
}
return array(
'mode' => 'productive',
'approved_piwigo_version' => '16.4.0',
'piwigo_version' => $piwigo_version,
'site_id' => $site_id,
'counts' => is_array($counts) ? $counts : array(),
'database_writes' => true,
'core_output_bytes' => strlen($output),
'username' => isset($user['username']) ? (string)$user['username'] : '',
'status' => isset($user['status']) ? (string)$user['status'] : '',
);
}
+58 -25
View File
@@ -7,6 +7,9 @@ if (!defined('PHPWG_ROOT_PATH'))
function bratonien_tools_webdav_image_source_info($image_id)
{
static $cache = array();
static $connection_cache = array();
static $mapping_cache = array();
$image_id = (int)$image_id;
if ($image_id < 1) return null;
if (array_key_exists($image_id, $cache)) return $cache[$image_id];
@@ -20,12 +23,12 @@ function bratonien_tools_webdav_image_source_info($image_id)
$absolute = $path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $absolute), '/');
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\\./#', '', $absolute), '/');
}
$resolved = realpath($absolute);
if ($resolved === false) return $cache[$image_id] = null;
$normalized = str_replace('\\', '/', $resolved);
$normalized = str_replace('\\\\', '/', $resolved);
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{
return $cache[$image_id] = null;
@@ -36,60 +39,82 @@ function bratonien_tools_webdav_image_source_info($image_id)
$relative_path = trim((string)$match[3], '/');
if ($relative_path === '') return $cache[$image_id] = null;
$table = defined('BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE')
? BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$connection_result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($connection_result)) return $cache[$image_id] = null;
$connection_row = pwg_db_fetch_assoc($connection_result);
$config = json_decode((string)$connection_row['config_json'], true);
if (!array_key_exists($connection_id, $connection_cache))
{
$table = defined('BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE')
? BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$connection_result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($connection_result))
{
$connection_cache[$connection_id] = null;
}
else
{
$connection_row = pwg_db_fetch_assoc($connection_result);
$decoded = json_decode((string)$connection_row['config_json'], true);
$connection_cache[$connection_id] = is_array($decoded) ? $decoded : null;
}
}
$config = $connection_cache[$connection_id];
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder')
{
return $cache[$image_id] = null;
}
$root_path = '';
$root_found = false;
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
foreach ($roots as $root)
{
if ((int)($root['fileid'] ?? 0) === $root_fileid)
{
$root_path = trim((string)($root['webdav_path'] ?? ''), '/');
$root_found = true;
break;
}
}
if ($root_path === '') return $cache[$image_id] = null;
if (!$root_found) return $cache[$image_id] = null;
$webdav_path = $root_path.'/'.$relative_path;
$root_is_base = $root_path === '';
$webdav_path = $root_is_base ? $relative_path : $root_path.'/'.$relative_path;
$content_type = '';
$size = 0;
$etag = '';
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '')
if (!array_key_exists($connection_id, $mapping_cache))
{
$mapping_file = $state_dir.'/webdav-map.json';
if (is_readable($mapping_file))
$mapping_cache[$connection_id] = array();
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '')
{
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
$mapping_file = $state_dir.'/webdav-map.json';
if (is_readable($mapping_file))
{
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null;
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file')
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{
$webdav_path = trim((string)($entry['webdav_path'] ?? $webdav_path), '/');
$content_type = (string)($entry['content_type'] ?? '');
$size = (int)($entry['size'] ?? 0);
$etag = (string)($entry['etag'] ?? '');
$mapping_cache[$connection_id] = $mapping['files'];
}
}
}
}
$entry = $mapping_cache[$connection_id][$resolved] ?? $mapping_cache[$connection_id][$normalized] ?? null;
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file')
{
$webdav_path = trim((string)($entry['webdav_path'] ?? $webdav_path), '/');
$content_type = (string)($entry['content_type'] ?? '');
$size = (int)($entry['size'] ?? 0);
$etag = (string)($entry['etag'] ?? '');
}
return $cache[$image_id] = array(
'image_id'=>$image_id,
'connection_id'=>$connection_id,
'webdav_path'=>$webdav_path,
'root_is_base'=>$root_is_base,
'content_type'=>$content_type,
'size'=>$size,
'etag'=>$etag,
@@ -412,8 +437,16 @@ function bratonien_tools_filter_webdav_src_url($url, $src_image)
function bratonien_tools_filter_webdav_derivative_url($url, $params, $src_image, $rel_url)
{
if (!is_object($src_image) || empty($src_image->id)) return $url;
$info = bratonien_tools_webdav_image_source_info((int)$src_image->id);
if (!$info) return $url;
// Hotpath: Bei einem bereits vorbereiteten WebDAV-Derivat keinerlei
// Connection-DB oder Mapping-Datei mehr anfassen. Der reale Quellpfad
// reicht aus, um Connector-Bilder sicher zu erkennen.
$source_path = $src_image->get_path();
$resolved_source = $source_path !== '' ? realpath($source_path) : false;
if ($resolved_source === false || !preg_match('#/nc-webdav-source/connection-[0-9]+/root-[0-9]+/#', str_replace('\\\\', '/', $resolved_source)))
{
return $url;
}
try
{
+21 -155
View File
@@ -1,134 +1,6 @@
(function () {
'use strict';
var statusEndpoint = 'plugins/bratonien_tools/nc-connector-status.php';
function ensureLiveStatus(detail, actions) {
var node = detail.querySelector('[data-nc-run-live]');
if (node) return node;
node = document.createElement('div');
node.setAttribute('data-nc-run-live', '1');
node.className = 'bratonien-base-note';
node.style.marginTop = '.6rem';
node.hidden = true;
actions.parentNode.insertBefore(node, actions.nextSibling);
return node;
}
function renderLiveStatus(node, data) {
var state = String(data && data.state || '');
var message = String(data && data.message || '');
var detail = String(data && data.error_detail || '');
node.hidden = false;
node.innerHTML = '';
var strong = document.createElement('strong');
if (state === 'queued') strong.textContent = 'Angefordert: ';
else if (state === 'running') strong.textContent = 'Läuft: ';
else if (state === 'ok' || state === 'success') strong.textContent = 'Erfolgreich: ';
else if (state === 'error') strong.textContent = 'Fehler: ';
else strong.textContent = 'Status: ';
node.appendChild(strong);
node.appendChild(document.createTextNode(message || state || 'Status wird ermittelt …'));
if (detail) {
var details = document.createElement('details');
details.style.marginTop = '.35rem';
var summary = document.createElement('summary');
summary.textContent = 'Technische Laufzeitdetails';
var pre = document.createElement('pre');
pre.style.whiteSpace = 'pre-wrap';
pre.style.wordBreak = 'break-word';
pre.textContent = detail;
details.appendChild(summary);
details.appendChild(pre);
node.appendChild(details);
}
}
function pollConnection(connectionId, node, button) {
var attempts = 0;
var maxAttempts = 180;
var timer = null;
function stop() {
if (timer) window.clearInterval(timer);
timer = null;
if (button) {
button.disabled = false;
button.textContent = 'Jetzt abgleichen';
}
}
function poll() {
attempts += 1;
fetch(statusEndpoint + '?connection_id=' + encodeURIComponent(connectionId) + '&_=' + Date.now(), {
credentials: 'same-origin',
cache: 'no-store',
headers: {'Accept': 'application/json'}
})
.then(function (response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
})
.then(function (data) {
renderLiveStatus(node, data);
var state = String(data && data.state || '');
if (state === 'ok' || state === 'success' || state === 'error' || attempts >= maxAttempts) stop();
})
.catch(function (error) {
if (attempts >= maxAttempts) {
renderLiveStatus(node, {state: 'error', message: 'Status konnte nicht gelesen werden.', error_detail: error.message});
stop();
}
});
}
poll();
timer = window.setInterval(poll, 1000);
}
function bindRunNow(form, detail, liveNode) {
if (form.dataset.ncRunBound === '1') return;
form.dataset.ncRunBound = '1';
form.addEventListener('submit', function (event) {
event.preventDefault();
detail.open = true;
var button = form.querySelector('button[value="nc_connector_run_now"]');
var connectionInput = form.querySelector('input[name="connection_id"]');
if (!connectionInput) return;
if (button) {
button.disabled = true;
button.textContent = 'Abgleich wird gestartet …';
}
renderLiveStatus(liveNode, {state: 'queued', message: 'Abgleich wird angefordert …'});
fetch(form.action || window.location.href, {
method: 'POST',
credentials: 'same-origin',
cache: 'no-store',
body: new FormData(form)
})
.then(function (response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
renderLiveStatus(liveNode, {state: 'queued', message: 'Abgleich wurde angefordert.'});
pollConnection(connectionInput.value, liveNode, button);
})
.catch(function (error) {
renderLiveStatus(liveNode, {state: 'error', message: 'Abgleich konnte nicht angefordert werden.', error_detail: error.message});
if (button) {
button.disabled = false;
button.textContent = 'Jetzt abgleichen';
}
});
});
}
function restoreRunNowButtons() {
var section = document.getElementById('nc-connector');
if (!section) return;
@@ -146,42 +18,36 @@
var details = connectionCard.querySelectorAll(':scope > details');
details.forEach(function (detail) {
var actions = detail.querySelector('.bratonien-actions');
if (!actions) return;
if (!actions || actions.querySelector('[value="nc_connector_run_now"]')) return;
var connectionInput = detail.querySelector('input[name="connection_id"]');
var tokenInput = detail.querySelector('input[name="pwg_token"]');
if (!connectionInput || !tokenInput) return;
var form = actions.querySelector('form[data-nc-run-now]');
if (!form) {
form = document.createElement('form');
form.method = 'post';
form.setAttribute('data-nc-run-now', '1');
var form = document.createElement('form');
form.method = 'post';
var token = document.createElement('input');
token.type = 'hidden';
token.name = 'pwg_token';
token.value = tokenInput.value;
form.appendChild(token);
var token = document.createElement('input');
token.type = 'hidden';
token.name = 'pwg_token';
token.value = tokenInput.value;
form.appendChild(token);
var connection = document.createElement('input');
connection.type = 'hidden';
connection.name = 'connection_id';
connection.value = connectionInput.value;
form.appendChild(connection);
var connection = document.createElement('input');
connection.type = 'hidden';
connection.name = 'connection_id';
connection.value = connectionInput.value;
form.appendChild(connection);
var button = document.createElement('button');
button.className = 'buttonLike';
button.type = 'submit';
button.name = 'bratonien_tool';
button.value = 'nc_connector_run_now';
button.textContent = 'Jetzt abgleichen';
form.appendChild(button);
var button = document.createElement('button');
button.className = 'buttonLike';
button.type = 'submit';
button.name = 'bratonien_tool';
button.value = 'nc_connector_run_now';
button.textContent = 'Jetzt abgleichen';
form.appendChild(button);
actions.insertBefore(form, actions.firstChild);
}
bindRunNow(form, detail, ensureLiveStatus(detail, actions));
actions.insertBefore(form, actions.firstChild);
});
}
+3 -2
View File
@@ -1,7 +1,7 @@
<?php
/*
Plugin Name: Bratonien Tools
Version: 0.9.7.26
Version: 0.9.7.19
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien
@@ -22,13 +22,13 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/picture_navigation.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/batch_titles.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_scheduler.inc.php');
add_event_handler('get_admin_plugin_menu_links', 'bratonien_tools_admin_menu');
add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4);
add_event_handler('get_src_image_url', 'bratonien_tools_filter_webdav_src_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 2);
add_event_handler('get_derivative_url', 'bratonien_tools_filter_webdav_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 4);
add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action');
add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EVENT_HANDLER_PRIORITY_NEUTRAL - 30);
@@ -37,6 +37,7 @@ add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT
add_event_handler('init', 'bratonien_tools_album_shares_init');
add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories');
add_event_handler('ws_add_methods', 'bratonien_tools_register_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_productive_ws_methods');
function bratonien_tools_prepare_connector_private_import()
+47 -74
View File
@@ -24,7 +24,6 @@ if (!function_exists('is_admin') || !is_admin())
exit;
}
$requested_connection_id = isset($_GET['connection_id']) ? max(0, (int)$_GET['connection_id']) : 0;
$dir = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-connector-status';
$latest = array(
'state'=>'idle',
@@ -42,16 +41,11 @@ $latest = array(
'route_timestamp'=>0,
'route_time_label'=>'Nicht verfügbar',
'route_detail'=>'',
'connection_id'=>$requested_connection_id,
);
if (is_dir($dir))
{
$files = $requested_connection_id > 0
? array($dir.'/connection-'.$requested_connection_id.'.json')
: (glob($dir.'/connection-*.json') ?: array());
foreach ($files as $file)
foreach (glob($dir.'/connection-*.json') ?: array() as $file)
{
if (!is_readable($file))
{
@@ -69,58 +63,60 @@ if (is_dir($dir))
}
}
if ($requested_connection_id === 0)
$route_file = $dir.'/route-status.json';
if (is_readable($route_file))
{
$route_file = $dir.'/route-status.json';
if (is_readable($route_file))
$route = json_decode((string)@file_get_contents($route_file), true);
if (is_array($route))
{
$route = json_decode((string)@file_get_contents($route_file), true);
if (is_array($route))
$route_name = (string)($route['route'] ?? '');
$route_timestamp = (int)($route['timestamp'] ?? 0);
$route_label = (string)($route['label'] ?? '');
if ($route_name === 'webdav')
{
$route_name = (string)($route['route'] ?? '');
$route_timestamp = (int)($route['timestamp'] ?? 0);
$route_label = (string)($route['label'] ?? '');
if ($route_name === 'webdav')
{
$route_label = 'WEBDAV PRIMÄR';
}
elseif ($route_name === 'legacy_fallback')
{
$route_label = 'LEGACY-FALLBACK AKTIV';
}
elseif ($route_name === 'failed')
{
$route_label = 'FEHLER - KEIN ERFOLGREICHER DATENWEG';
}
elseif ($route_label === '')
{
$route_label = 'UNBEKANNTER DATENWEG';
}
$route_detail = trim((string)($route['detail'] ?? ''));
$latest['route'] = $route_name;
$latest['route_label'] = $route_label;
$latest['route_timestamp'] = $route_timestamp;
$latest['route_time_label'] = $route_timestamp > 0 ? date('d.m.Y H:i:s', $route_timestamp) : 'Nicht verfügbar';
$latest['route_detail'] = $route_detail;
$base_message = trim((string)($latest['message'] ?? ''));
$message_parts = array($route_label);
if ($route_name !== 'webdav' && $route_detail !== '')
{
$message_parts[] = $route_detail;
}
if ($base_message !== '')
{
$message_parts[] = $base_message;
}
$latest['message'] = implode(' · ', $message_parts);
$route_label = 'WEBDAV PRIMÄR';
}
elseif ($route_name === 'legacy_fallback')
{
$route_label = 'LEGACY-FALLBACK AKTIV';
}
elseif ($route_name === 'failed')
{
$route_label = 'FEHLER - KEIN ERFOLGREICHER DATENWEG';
}
elseif ($route_label === '')
{
$route_label = 'UNBEKANNTER DATENWEG';
}
$route_detail = trim((string)($route['detail'] ?? ''));
$latest['route'] = $route_name;
$latest['route_label'] = $route_label;
$latest['route_timestamp'] = $route_timestamp;
$latest['route_time_label'] = $route_timestamp > 0 ? date('d.m.Y H:i:s', $route_timestamp) : 'Nicht verfügbar';
$latest['route_detail'] = $route_detail;
$base_message = trim((string)($latest['message'] ?? ''));
$message_parts = array($route_label);
if ($route_name !== 'webdav' && $route_detail !== '')
{
$message_parts[] = $route_detail;
}
if ($base_message !== '')
{
$message_parts[] = $base_message;
}
$latest['message'] = implode(' · ', $message_parts);
}
}
}
if ((int)$latest['timestamp'] > 0)
{
$latest['last_run_label'] = date('d.m.Y H:i:s', (int)$latest['timestamp']);
}
$system_file = BRATONIEN_TOOLS_PATH.'include/nc_connector_system.inc.php';
if (is_readable($system_file))
{
@@ -133,27 +129,4 @@ if (is_readable($system_file))
}
}
$scheduler_file = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-connector-scheduler/state.json';
if ($requested_connection_id > 0 && is_readable($scheduler_file))
{
$scheduler = json_decode((string)@file_get_contents($scheduler_file), true);
if (
is_array($scheduler)
&& (int)($scheduler['connection_id'] ?? 0) === $requested_connection_id
&& in_array((string)($scheduler['state'] ?? ''), array('queued','running'), true)
&& (int)($scheduler['timestamp'] ?? 0) >= (int)($latest['timestamp'] ?? 0)
)
{
$latest['state'] = (string)$scheduler['state'];
$latest['message'] = (string)($scheduler['message'] ?? 'NC-Abgleich läuft.');
$latest['timestamp'] = (int)($scheduler['timestamp'] ?? time());
$latest['error_detail'] = '';
}
}
if ((int)$latest['timestamp'] > 0)
{
$latest['last_run_label'] = date('d.m.Y H:i:s', (int)$latest['timestamp']);
}
echo json_encode($latest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+76 -4
View File
@@ -1,7 +1,79 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 0002
# Seit 0.9.7.20 greift der NC Connector nicht mehr in Piwigos Bild-/Derivatlogik ein.
# Der Shadowtree dient nur der Piwigo-Synchronisation; die Bild-URL wird zur Laufzeit
# ueber den bestehenden WebDAV-URL-Hook auf Nextcloud aufgeloest.
exit 0
CONFIG_FILE="${PIWIGO_CONFIG:-}"
[[ -n "$CONFIG_FILE" && -r "$CONFIG_FILE" ]] || { echo "WebDAV-Konfiguration fehlt: ${CONFIG_FILE:-<leer>}" >&2; exit 1; }
# shellcheck source=/dev/null
source "$CONFIG_FILE"
: "${PIWIGO_ROOT:?PIWIGO_ROOT fehlt}"
: "${CONNECTION_ID:?CONNECTION_ID fehlt}"
: "${WEBDAV_BASE_URL:?WEBDAV_BASE_URL fehlt}"
: "${WEBDAV_USER:?WEBDAV_USER fehlt}"
: "${WEBDAV_PASSWORD_FILE:?WEBDAV_PASSWORD_FILE fehlt}"
: "${WEBDAV_MAPPING_FILE:?WEBDAV_MAPPING_FILE fehlt}"
: "${STATE_DIR:?STATE_DIR fehlt}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
LOCK_FILE="$STATE_DIR/webdav-media.lock"
mkdir -p -- "$STATE_DIR"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
PREVIEW_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-preview/connection-$CONNECTION_ID"
SOURCE_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-gallery/connection-$CONNECTION_ID"
DERIVATIVE_CACHE="$PIWIGO_ROOT/_data/i/_data/bratonien-tools/nc-webdav-gallery/connection-$CONNECTION_ID"
PIWIGO_DATA="$PIWIGO_ROOT/_data"
normalize_connector_cache_permissions() {
[[ -d "$PIWIGO_DATA" ]] || return 0
local data_uid data_gid current_uid path
data_uid="$(stat -c '%u' "$PIWIGO_DATA")"
data_gid="$(stat -c '%g' "$PIWIGO_DATA")"
current_uid="$(id -u)"
for path in "$SOURCE_CACHE" "$PREVIEW_CACHE" "$DERIVATIVE_CACHE"; do
[[ -e "$path" ]] || continue
if [[ "$current_uid" -eq 0 ]]; then
chown -R "$data_uid:$data_gid" -- "$path"
fi
if ! find "$path" -type d -exec chmod 2775 {} + 2>/dev/null; then
echo "Hinweis: Verzeichnisrechte konnten ohne Root-Rechte nicht vollständig repariert werden: $path" >&2
fi
if ! find "$path" -type f -exec chmod 0664 {} + 2>/dev/null; then
echo "Hinweis: Dateirechte konnten ohne Root-Rechte nicht vollständig repariert werden: $path" >&2
fi
done
}
# Altbestände aus früheren root/systemd-Läufen werden repariert, sobald dieser
# Lauf mit ausreichenden Rechten ausgeführt wird. Bei normalen Webserver-Läufen
# werden zumindest alle eigenen Dateien gruppenschreibbar gehalten.
normalize_connector_cache_permissions
# Dieser Medienlauf wird erst nach einem erfolgreichen WebDAV-Aufbau und einer
# erfolgreichen Piwigo-Synchronisierung gestartet. Deshalb darf erst hier aus
# "lokale Connector-Datei fehlt" auf eine in Nextcloud entfernte Freigabe/Datei
# geschlossen werden. Bei einer nicht erreichbaren Verbindung wird dieser Block
# niemals ausgeführt und der vorhandene Piwigo-Bestand bleibt unverändert.
php "$SCRIPT_DIR/cleanup-missing-webdav-images.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID"
php "$SCRIPT_DIR/lib/precache-webdav-previews.php" \
--mapping="$WEBDAV_MAPPING_FILE" \
--base-url="$WEBDAV_BASE_URL" \
--user="$WEBDAV_USER" \
--password-file="$WEBDAV_PASSWORD_FILE" \
--cache-dir="$PREVIEW_CACHE"
php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID"
normalize_connector_cache_permissions
+22 -84
View File
@@ -6,8 +6,6 @@ if (PHP_SAPI !== 'cli')
exit(1);
}
const BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION = '0.9.6.1';
$options = getopt('', array('piwigo-root:', 'connection-id:'));
$piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/');
$connection_id = (int)($options['connection-id'] ?? 0);
@@ -33,14 +31,11 @@ $_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/lib/build-webdav-derivatives.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Derivative-Builder/'.BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION;
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
require_once(PHPWG_ROOT_PATH.'admin/include/image.class.php');
if (!function_exists('bratonien_tools_webdav_image_source_info') || !function_exists('bratonien_tools_webdav_generate_derivative'))
if (!function_exists('bratonien_tools_webdav_image_source_info') || !function_exists('bratonien_tools_webdav_preview_path'))
{
fwrite(STDERR, "Bratonien WebDAV-Bildruntime ist nicht aktiv.\n");
exit(1);
@@ -48,20 +43,12 @@ if (!function_exists('bratonien_tools_webdav_image_source_info') || !function_ex
try
{
$variants = bratonien_tools_webdav_derivative_variants();
if (!$variants)
{
throw new RuntimeException('Keine Piwigo-Derivate konfiguriert.');
}
$images = 0;
$generated_or_ready = 0;
$identity = 0;
$metadata_repaired = 0;
$updated = 0;
$errors = 0;
$error_lines = array();
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' ORDER BY id');
$result = pwg_query('SELECT id, width, height, rotation FROM '.IMAGES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result))
{
$image_id = (int)$row['id'];
@@ -77,14 +64,6 @@ try
continue;
}
$preview_ext = strtolower(pathinfo($preview, PATHINFO_EXTENSION));
if (!in_array($preview_ext, array('jpg', 'jpeg', 'png', 'gif'), true))
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.': Preview-Format '.$preview_ext.' ist nicht Piwigo-kompatibel; Precache muss neu erzeugt werden.';
continue;
}
$size = @getimagesize($preview);
if (!is_array($size) || empty($size[0]) || empty($size[1]))
{
@@ -93,82 +72,41 @@ try
continue;
}
$preview_width = (int)$size[0];
$preview_height = (int)$size[1];
if ((int)($row['width'] ?? 0) !== $preview_width || (int)($row['height'] ?? 0) !== $preview_height || (int)($row['rotation'] ?? 0) !== 0)
$width = (int)$size[0];
$height = (int)$size[1];
if ((int)($row['width'] ?? 0) === $width && (int)($row['height'] ?? 0) === $height && (int)($row['rotation'] ?? 0) === 0)
{
pwg_query(
'UPDATE '.IMAGES_TABLE.
' SET width='.$preview_width.', height='.$preview_height.', rotation=0'.
' WHERE id='.$image_id
);
$row['width'] = $preview_width;
$row['height'] = $preview_height;
$row['rotation'] = 0;
$metadata_repaired++;
continue;
}
$src = new SrcImage($row);
foreach ($variants as $variant_name => $params)
{
try
{
$probe = new DerivativeImage($params, $src);
if ($probe->same_as_source())
{
$identity++;
continue;
}
$detail = '';
if (bratonien_tools_webdav_generate_derivative($params, $src, $detail))
{
$generated_or_ready++;
}
else
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.' '.$variant_name.': '.($detail !== '' ? $detail : 'Derivat konnte nicht erzeugt werden.');
}
}
catch (Throwable $e)
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.' '.$variant_name.': '.get_class($e).': '.$e->getMessage();
}
}
pwg_query(
'UPDATE '.IMAGES_TABLE.
' SET width='.$width.', height='.$height.', rotation=0'.
' WHERE id='.$image_id
);
$updated++;
}
if ($metadata_repaired > 0)
if ($updated > 0)
{
update_category('all');
invalidate_user_cache(true);
}
if ($errors > 0)
foreach (array_slice($error_lines, 0, 30) as $line)
{
foreach (array_slice($error_lines, 0, 30) as $line)
{
fwrite(STDERR, $line."\n");
}
if (count($error_lines) > 30)
{
fwrite(STDERR, 'Weitere Fehler: '.(count($error_lines) - 30)."\n");
}
fwrite(STDERR, $line."\n");
}
if (count($error_lines) > 30)
{
fwrite(STDERR, 'Weitere Fehler: '.(count($error_lines) - 30)."\n");
}
echo 'WebDAV-Derivative-Builder: version='.BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION.
' bilder='.$images.
' varianten='.count($variants).
' bereit='.$generated_or_ready.
' identisch='.$identity.
' metadaten_repariert='.$metadata_repaired.
' fehler='.$errors."\n";
echo 'WebDAV-Metadaten: bilder='.$images.' aktualisiert='.$updated.' fehler='.$errors."\n";
exit($errors > 0 ? 1 : 0);
}
catch (Throwable $e)
{
fwrite(STDERR, 'WebDAV-Derivate: '.get_class($e).': '.$e->getMessage()."\n");
fwrite(STDERR, 'WebDAV-Metadaten: '.get_class($e).': '.$e->getMessage()."\n");
exit(1);
}
+22 -164
View File
@@ -2,12 +2,8 @@
"""Build a placeholder-backed local source tree from Nextcloud WebDAV.
This creates only tiny placeholder files plus a metadata mapping; no Nextcloud
original media is stored locally. The authenticated Nextcloud user is never used
as an album name.
Each placeholder carries the original image dimensions. Dimensions are read
from Nextcloud metadata first. If those metadata are unavailable, only the
beginning of the original file is read through WebDAV and parsed locally.
original media is downloaded. The authenticated Nextcloud user is never used as
an album name.
"""
from __future__ import annotations
@@ -31,10 +27,8 @@ from pathlib import Path, PurePosixPath
DAV = "DAV:"
OC = "http://owncloud.org/ns"
NC = "http://nextcloud.org/ns"
SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
DIMENSION_PROBE_BYTES = 4 * 1024 * 1024
def fail(message: str) -> None:
@@ -60,101 +54,6 @@ def safe_local_name(name: str) -> str:
return name
def parse_dimensions(prop: ET.Element) -> tuple[int, int]:
for property_name in ("file-metadata-size", "metadata-photos-size"):
raw = prop.findtext(f"{{{NC}}}{property_name}", default="").strip()
if not raw:
continue
try:
decoded = json.loads(raw)
except (TypeError, ValueError, json.JSONDecodeError):
continue
if isinstance(decoded, dict) and isinstance(decoded.get("value"), dict):
decoded = decoded["value"]
if not isinstance(decoded, dict):
continue
try:
width = int(decoded.get("width", 0) or 0)
height = int(decoded.get("height", 0) or 0)
except (TypeError, ValueError):
continue
if width > 0 and height > 0:
return width, height
return 0, 0
def parse_image_header_dimensions(data: bytes) -> tuple[int, int]:
if len(data) >= 24 and data.startswith(b"\x89PNG\r\n\x1a\n") and data[12:16] == b"IHDR":
return int.from_bytes(data[16:20], "big"), int.from_bytes(data[20:24], "big")
if len(data) >= 10 and data[:6] in (b"GIF87a", b"GIF89a"):
return int.from_bytes(data[6:8], "little"), int.from_bytes(data[8:10], "little")
if len(data) >= 12 and data[:4] == b"RIFF" and data[8:12] == b"WEBP":
chunk = data[12:16]
if chunk == b"VP8X" and len(data) >= 30:
width = 1 + int.from_bytes(data[24:27], "little")
height = 1 + int.from_bytes(data[27:30], "little")
return width, height
if chunk == b"VP8 " and len(data) >= 30:
payload = 20
if data[payload + 3:payload + 6] == b"\x9d\x01\x2a":
width = int.from_bytes(data[payload + 6:payload + 8], "little") & 0x3FFF
height = int.from_bytes(data[payload + 8:payload + 10], "little") & 0x3FFF
return width, height
if chunk == b"VP8L" and len(data) >= 25 and data[20] == 0x2F:
b1, b2, b3, b4 = data[21:25]
width = 1 + b1 + ((b2 & 0x3F) << 8)
height = 1 + ((b2 & 0xC0) >> 6) + (b3 << 2) + ((b4 & 0x0F) << 10)
return width, height
if len(data) >= 4 and data[:2] == b"\xff\xd8":
sof_markers = {
0xC0, 0xC1, 0xC2, 0xC3,
0xC5, 0xC6, 0xC7,
0xC9, 0xCA, 0xCB,
0xCD, 0xCE, 0xCF,
}
pos = 2
while pos + 4 <= len(data):
if data[pos] != 0xFF:
pos += 1
continue
while pos < len(data) and data[pos] == 0xFF:
pos += 1
if pos >= len(data):
break
marker = data[pos]
pos += 1
if marker in (0xD8, 0xD9) or 0xD0 <= marker <= 0xD7:
continue
if marker == 0xDA:
break
if pos + 2 > len(data):
break
segment_length = int.from_bytes(data[pos:pos + 2], "big")
if segment_length < 2:
break
if marker in sof_markers:
if pos + 7 > len(data):
break
height = int.from_bytes(data[pos + 3:pos + 5], "big")
width = int.from_bytes(data[pos + 5:pos + 7], "big")
return width, height
pos += segment_length
return 0, 0
def placeholder_bytes(width: int, height: int) -> bytes:
if not (1 <= width <= 65535 and 1 <= height <= 65535):
fail(f"unsupported image dimensions for placeholder: {width}x{height}")
data = bytearray(PLACEHOLDER)
data[6:8] = width.to_bytes(2, "little")
data[8:10] = height.to_bytes(2, "little")
return bytes(data)
@contextmanager
def pinned_resolution(host: str, ip: str):
host = host.strip("[]").lower()
@@ -193,50 +92,22 @@ class WebDavClient:
self.auth_header = f"Basic {token}"
self.context = ssl.create_default_context()
def file_url(self, relative: str) -> str:
def collection_url(self, relative: str) -> str:
user = urllib.parse.quote(self.user, safe="")
suffix = quote_path(relative)
return f"{self.base_url}/remote.php/dav/files/{user}/{suffix}"
def collection_url(self, relative: str) -> str:
url = self.file_url(relative)
if not url.endswith("/"):
url += "/"
url = f"{self.base_url}/remote.php/dav/files/{user}/"
if suffix:
url += suffix + "/"
return url
def probe_dimensions(self, relative: str) -> tuple[int, int]:
request = urllib.request.Request(self.file_url(relative), method="GET")
request.add_header("Authorization", self.auth_header)
request.add_header("Range", f"bytes=0-{DIMENSION_PROBE_BYTES - 1}")
try:
with pinned_resolution(self.host, self.connect_ip):
with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response:
if response.status not in (200, 206):
fail(f"Nextcloud image header returned HTTP {response.status}")
data = response.read(DIMENSION_PROBE_BYTES)
except urllib.error.HTTPError as error:
if error.code in {401, 403}:
fail("Nextcloud rejected the WebDAV credentials or file access")
fail(f"Nextcloud image header request failed with HTTP {error.code}")
except urllib.error.URLError as error:
fail(f"Nextcloud WebDAV is unreachable while reading image dimensions: {error.reason}")
width, height = parse_image_header_dimensions(data)
if width < 1 or height < 1:
fail(f"original image dimensions could not be read from Nextcloud file header: {relative}")
return width, height
def list_collection(self, relative: str) -> tuple[dict[str, object], list[dict[str, object]]]:
relative = validate_relative(relative)
url = self.collection_url(relative)
body = (
'<?xml version="1.0"?>'
'<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" '
'xmlns:nc="http://nextcloud.org/ns">'
'<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">'
'<d:prop><d:displayname/><d:resourcetype/><d:getcontenttype/>'
'<d:getcontentlength/><d:getetag/><oc:fileid/>'
'<nc:file-metadata-size/><nc:metadata-photos-size/>'
'</d:prop></d:propfind>'
'<d:getcontentlength/><d:getetag/><oc:fileid/></d:prop></d:propfind>'
).encode("utf-8")
request = urllib.request.Request(url, data=body, method="PROPFIND")
request.add_header("Authorization", self.auth_header)
@@ -280,7 +151,6 @@ class WebDavClient:
fileid_text = prop.findtext(f"{{{OC}}}fileid", default="").strip()
resource_type = prop.find(f"{{{DAV}}}resourcetype")
is_dir = resource_type is not None and resource_type.find(f"{{{DAV}}}collection") is not None
width, height = parse_dimensions(prop)
item = {
"display_name": display,
"fileid": int(fileid_text) if fileid_text.isdigit() else 0,
@@ -288,8 +158,6 @@ class WebDavClient:
"content_type": prop.findtext(f"{{{DAV}}}getcontenttype", default=""),
"size": int(prop.findtext(f"{{{DAV}}}getcontentlength", default="0") or 0),
"etag": prop.findtext(f"{{{DAV}}}getetag", default="").strip('"'),
"width": width,
"height": height,
}
if href_path == base_path:
current = item
@@ -301,17 +169,18 @@ class WebDavClient:
return current, children
def write_placeholder(target: Path, width: int, height: int) -> None:
def link_placeholder(seed: Path, target: Path) -> None:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(placeholder_bytes(width, height))
os.chmod(target, 0o644)
try:
os.link(seed, target)
except OSError:
target.write_bytes(PLACEHOLDER)
def build_root(client: WebDavClient, remote_root: str, local_root: Path, mapping: dict[str, dict[str, object]]) -> tuple[int, int, int, int]:
def build_root(client: WebDavClient, remote_root: str, local_root: Path, seed: Path, mapping: dict[str, dict[str, object]]) -> tuple[int, int, int]:
files = 0
folders = 0
skipped = 0
probed = 0
stack: list[tuple[str, Path]] = [(validate_relative(remote_root), local_root)]
visited: set[str] = set()
@@ -341,16 +210,7 @@ def build_root(client: WebDavClient, remote_root: str, local_root: Path, mapping
if extension not in SUPPORTED_IMAGE_EXTENSIONS:
skipped += 1
continue
width = int(child.get("width", 0) or 0)
height = int(child.get("height", 0) or 0)
dimension_source = "metadata"
if width < 1 or height < 1:
width, height = client.probe_dimensions(child_remote)
dimension_source = "header"
probed += 1
write_placeholder(child_local, width, height)
link_placeholder(seed, child_local)
files += 1
mapping[str(child_local)] = {
"kind": "file",
@@ -360,11 +220,8 @@ def build_root(client: WebDavClient, remote_root: str, local_root: Path, mapping
"content_type": str(child.get("content_type", "")),
"size": int(child.get("size", 0)),
"etag": str(child.get("etag", "")),
"width": width,
"height": height,
"dimension_source": dimension_source,
}
return files, folders, skipped, probed
return files, folders, skipped
def atomic_json(path: Path, payload: object) -> None:
@@ -408,7 +265,10 @@ def main() -> int:
source_dir = args.source_dir.resolve()
staging = source_dir.with_name(f".{source_dir.name}.next")
previous = source_dir.with_name(f".{source_dir.name}.previous")
seed = source_dir.parent / ".bratonien-webdav-placeholder.gif"
source_dir.parent.mkdir(parents=True, exist_ok=True)
seed.write_bytes(PLACEHOLDER)
os.chmod(seed, 0o644)
if staging.exists():
shutil.rmtree(staging)
staging.mkdir(parents=True)
@@ -416,7 +276,7 @@ def main() -> int:
client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout), args.connect_ip)
mapping: dict[str, dict[str, object]] = {}
manifest: list[str] = []
total_files = total_folders = total_skipped = total_probed = 0
total_files = total_folders = total_skipped = 0
used_fileids: set[int] = set()
for remote_root_raw in args.root:
@@ -428,11 +288,10 @@ def main() -> int:
used_fileids.add(fileid)
local_name = f"root-{fileid}"
local_root = staging / local_name
files, folders, skipped, probed = build_root(client, remote_root, local_root, mapping)
files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping)
total_files += files
total_folders += folders
total_skipped += skipped
total_probed += probed
if remote_root == "":
for child in sorted(root_children, key=lambda item: str(item.get("display_name", "")).casefold()):
@@ -474,7 +333,7 @@ def main() -> int:
atomic_text(args.manifest, "\n".join(manifest) + "\n")
atomic_json(args.mapping, {
"version": 3,
"version": 1,
"base_url": args.base_url.rstrip("/"),
"connect_ip": client.connect_ip,
"user": args.user,
@@ -485,7 +344,6 @@ def main() -> int:
"files": total_files,
"folders": total_folders,
"skipped": total_skipped,
"dimension_header_probes": total_probed,
"connect_ip": client.connect_ip,
"source_dir": str(source_dir),
"manifest": str(args.manifest),
+158 -45
View File
@@ -15,12 +15,27 @@ function http_error_detail($body)
{
$body = trim((string)$body);
if ($body === '') return '';
$decoded = json_decode($body, true);
if (is_array($decoded))
{
$detail = (string)($decoded['message'] ?? $decoded['err'] ?? '');
if ($detail !== '') return $detail;
}
if (function_exists('simplexml_load_string'))
{
$previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string($body);
libxml_clear_errors();
libxml_use_internal_errors($previous);
if ($xml !== false)
{
$detail = trim((string)($xml->message ?? $xml->err ?? ''));
if ($detail !== '') return $detail;
}
}
$plain = trim(preg_replace('/\s+/', ' ', strip_tags($body)));
return $plain === '' ? '' : mb_substr($plain, 0, 500);
}
@@ -59,15 +74,55 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
return (string)$body;
}
function xml_value_sync(SimpleXMLElement $node)
{
$children = $node->children();
if (count($children) === 0) return (string)$node;
$result = array();
foreach ($children as $name => $child)
{
$value = xml_value_sync($child);
if (array_key_exists($name, $result))
{
if (!is_array($result[$name]) || !array_is_list($result[$name])) $result[$name] = array($result[$name]);
$result[$name][] = $value;
}
else $result[$name] = $value;
}
return $result;
}
function decode_ws($body)
{
$decoded = json_decode((string)$body, true);
if (!is_array($decoded)) fail_sync('Piwigo lieferte keine gueltige JSON-Webservice-Antwort.');
if (!is_array($decoded))
{
if (!function_exists('simplexml_load_string')) fail_sync('Piwigo lieferte XML, aber SimpleXML ist nicht verfuegbar.');
$previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$body);
libxml_clear_errors();
libxml_use_internal_errors($previous);
if ($xml === false || $xml->getName() !== 'rsp') fail_sync('Piwigo lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.');
$decoded = array('stat'=>(string)$xml['stat']);
foreach ($xml->children() as $name => $child)
{
$value = xml_value_sync($child);
if (array_key_exists($name, $decoded))
{
if (!is_array($decoded[$name]) || !array_is_list($decoded[$name])) $decoded[$name] = array($decoded[$name]);
$decoded[$name][] = $value;
}
else $decoded[$name] = $value;
}
}
if (($decoded['stat'] ?? '') !== 'ok')
{
fail_sync((string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo-Aufruf wurde abgelehnt.'));
$message = (string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo-Aufruf wurde abgelehnt.');
fail_sync($message);
}
return $decoded['result'] ?? array();
if (array_key_exists('result', $decoded)) return $decoded['result'];
unset($decoded['stat']);
return $decoded;
}
function decrypt_blob($blob, $hex_key)
@@ -79,28 +134,34 @@ function decrypt_blob($blob, $hex_key)
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) fail_sync('Gespeicherte Zugangsdaten sind unvollstaendig.');
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) fail_sync('Gespeicherte Zugangsdaten konnten nicht entschluesselt werden.');
$decoded = json_decode((string)$plain, true);
return is_array($decoded) ? $decoded : array();
return (string)$plain;
}
function ensure_webdav_site(mysqli $db, $prefixeTable, $piwigo_root, array $connection_config, $connection_id)
{
if ((string)($connection_config['source_mode'] ?? '') !== 'webdav-placeholder') return 1;
$gallery_root = rtrim((string)($connection_config['parallel_gallery_root'] ?? ''), '/');
if ($gallery_root === '') fail_sync('WebDAV-Galeriewurzel fehlt in der Verbindungskonfiguration.');
$piwigo_root = rtrim((string)$piwigo_root, '/');
if (strpos($gallery_root, $piwigo_root.'/') !== 0) fail_sync('WebDAV-Galeriewurzel liegt ausserhalb der Piwigo-Installation.');
if (strpos($gallery_root, $piwigo_root.'/') !== 0)
{
fail_sync('WebDAV-Galeriewurzel liegt ausserhalb der Piwigo-Installation.');
}
if (!is_dir($gallery_root)) fail_sync('WebDAV-Galeriewurzel existiert nicht: '.$gallery_root);
$relative = ltrim(substr($gallery_root, strlen($piwigo_root)), '/');
if ($relative === '') fail_sync('WebDAV-Galeriewurzel darf nicht dem Piwigo-Hauptverzeichnis entsprechen.');
$site_url = './'.rtrim($relative, '/').'/';
$escaped = $db->real_escape_string($site_url);
$result = $db->query("SELECT id FROM `{$prefixeTable}sites` WHERE galleries_url='{$escaped}' LIMIT 1");
if (!$result) fail_sync('Piwigo-Site konnte nicht gelesen werden: '.$db->error);
if ($result->num_rows) return (int)$result->fetch_assoc()['id'];
if ($result->num_rows)
{
return (int)$result->fetch_assoc()['id'];
}
if (!$db->query("INSERT INTO `{$prefixeTable}sites` (galleries_url) VALUES ('{$escaped}')"))
{
@@ -125,6 +186,7 @@ try
$conf = array();
$prefixeTable = 'piwigo_';
require $db_config;
foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail_sync('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) fail_sync('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
@@ -140,43 +202,78 @@ try
$connection_row = $connection_result->fetch_assoc();
$connection_config = json_decode((string)$connection_row['config_json'], true);
if (!is_array($connection_config)) $connection_config = array();
$credentials = decrypt_blob((string)$connection_row['secret_blob'], $hex_key);
$connection_plain = decrypt_blob((string)$connection_row['secret_blob'], $hex_key);
$connection_credentials = json_decode($connection_plain, true);
if (!is_array($connection_credentials)) $connection_credentials = array();
$site_id = ensure_webdav_site($db, $prefixeTable, $piwigo_root, $connection_config, $connection_id);
$api_enabled = !empty($connection_config['api_enabled']);
$api_key_id = $api_enabled ? trim((string)($credentials['api_key_id'] ?? '')) : '';
$api_key_secret = $api_enabled ? trim((string)($credentials['api_key_secret'] ?? '')) : '';
if ($api_key_id !== '' && $api_key_secret !== '')
$api = array('key_id'=>'', 'key_secret'=>'');
$connection_scoped = (string)($connection_config['piwigo_auth'] ?? '') === 'connection-scoped' || array_key_exists('api_enabled', $connection_config);
if ($connection_scoped)
{
try
if (!empty($connection_config['api_enabled']))
{
$headers = array(
'X-PIWIGO-API: '.$api_key_id.':'.$api_key_secret,
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
);
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id),
$headers
));
echo "Piwigo-Synchronisierung per API erfolgreich (Piwigo-Core, Site $site_id)\n";
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, 'Piwigo-API nicht nutzbar: '.$e->getMessage()."\n");
$api['key_id'] = trim((string)($connection_credentials['api_key_id'] ?? ''));
$api['key_secret'] = trim((string)($connection_credentials['api_key_secret'] ?? ''));
}
}
else
{
fwrite(STDERR, "Piwigo-API nicht nutzbar: Fuer diese Verbindung ist keine API konfiguriert.\n");
$api_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_piwigo_api' LIMIT 1");
if ($api_result && $api_result->num_rows)
{
$api_blob = (string)$api_result->fetch_assoc()['value'];
if ($api_blob !== '')
{
$api_plain = decrypt_blob($api_blob, $hex_key);
$api_decoded = json_decode($api_plain, true);
if (is_array($api_decoded))
{
$api['key_id'] = (string)($api_decoded['key_id'] ?? '');
$api['key_secret'] = (string)($api_decoded['key_secret'] ?? '');
}
}
}
}
$fallback_user = (string)($connection_credentials['piwigo_user'] ?? '');
$fallback_password = (string)($connection_credentials['piwigo_password'] ?? '');
$api_error = null;
if ($api['key_id'] !== '' && $api['key_secret'] !== '')
{
try
{
$headers = array(
'X-PIWIGO-API: '.$api['key_id'].':'.$api['key_secret'],
'Accept: application/json, text/xml;q=0.9',
'Content-Type: application/x-www-form-urlencoded',
);
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id), $headers));
$orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), $headers));
if ($site_id !== 1)
{
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), $headers));
}
$added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Synchronisierung per API erfolgreich (Site $site_id)\n";
echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";
exit(0);
}
catch (Throwable $e)
{
$api_error = $e->getMessage();
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n");
}
}
else
{
$api_error = $connection_scoped ? 'Fuer diese Verbindung ist keine API konfiguriert.' : 'Keine gespeicherten API-Zugangsdaten.';
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n");
}
$fallback_user = trim((string)($credentials['piwigo_user'] ?? ''));
$fallback_password = (string)($credentials['piwigo_password'] ?? '');
if ($fallback_user === '' || $fallback_password === '') fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback fuer diese Verbindung vorhanden.');
$cookie_file = tempnam(sys_get_temp_dir(), 'br-nc-sync-');
@@ -184,21 +281,37 @@ try
try
{
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password),
array(),
$cookie_file
));
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password), array(), $cookie_file));
// Der Fallback darf keinen zweiten Strukturpfad benutzen. Auch mit
// Benutzer/Passwort wird exakt derselbe Bratonien-Sync wie mit API-Key
// ausgefuehrt. Dadurch werden alte technische WebDAV-Kategorien entfernt
// und vorhandene Piwigo-Alben wiederverwendet.
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id),
array('Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'),
array(),
$cookie_file
));
echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich (Piwigo-Core, Site $site_id)\n";
$orphan = decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0),
array(),
$cookie_file
));
if ($site_id !== 1)
{
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0),
array(),
$cookie_file
));
}
$added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich (Site $site_id)\n";
echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";
}
finally
{
@@ -207,6 +320,6 @@ try
}
catch (Throwable $e)
{
fwrite(STDERR, 'Piwigo-Synchronisierung fehlgeschlagen: '.$e->getMessage()."\n");
fwrite(STDERR, $e->getMessage()."\n");
exit(1);
}
+31 -39
View File
@@ -78,9 +78,7 @@ done < "$WEBDAV_ROOTS_FILE"
WEBDAV_CONNECT_IP="$(php "$SCRIPT_DIR/lib/resolve-nextcloud-target.php" "$WEBDAV_BASE_URL")"
[[ -n "$WEBDAV_CONNECT_IP" ]] || { write_status error "Nextcloud-Zieladresse konnte nicht ermittelt werden"; exit 1; }
PLACEHOLDER_OUTPUT=""
PLACEHOLDER_EXIT=0
if PLACEHOLDER_OUTPUT="$(python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
--base-url "$WEBDAV_BASE_URL" \
--connect-ip "$WEBDAV_CONNECT_IP" \
--user "$WEBDAV_USER" \
@@ -88,42 +86,12 @@ if PLACEHOLDER_OUTPUT="$(python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_sourc
"${ROOT_ARGS[@]}" \
--source-dir "$WEBDAV_SOURCE_DIR" \
--manifest "$MANIFEST" \
--mapping "$WEBDAV_MAPPING_FILE" 2>&1)"; then
PLACEHOLDER_EXIT=0
else
PLACEHOLDER_EXIT=$?
fi
[[ -z "$PLACEHOLDER_OUTPUT" ]] || printf '%s\n' "$PLACEHOLDER_OUTPUT"
if [[ "$PLACEHOLDER_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PLACEHOLDER_EXIT"
if [[ -n "$PLACEHOLDER_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PLACEHOLDER_OUTPUT" | tail -n 20 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//')"
fi
trap - ERR
write_status error "WebDAV-Shadow-Tree fehlgeschlagen" "$DETAIL"
exit "$PLACEHOLDER_EXIT"
fi
--mapping "$WEBDAV_MAPPING_FILE"
SHADOW_OUTPUT=""
SHADOW_EXIT=0
if SHADOW_OUTPUT="$(python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--manifest "$MANIFEST" \
--destination "$GALLERY_ROOT" \
--state "$SHADOW_MAP_FILE" 2>&1)"; then
SHADOW_EXIT=0
else
SHADOW_EXIT=$?
fi
[[ -z "$SHADOW_OUTPUT" ]] || printf '%s\n' "$SHADOW_OUTPUT"
if [[ "$SHADOW_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $SHADOW_EXIT"
if [[ -n "$SHADOW_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$SHADOW_OUTPUT" | tail -n 20 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//')"
fi
trap - ERR
write_status error "WebDAV-Shadow-Tree fehlgeschlagen" "$DETAIL"
exit "$SHADOW_EXIT"
fi
--state "$SHADOW_MAP_FILE"
trap - ERR
@@ -170,9 +138,33 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
exit "$PIWIGO_EXIT"
fi
if [[ "${BRATONIEN_NC_NATIVE:-0}" == "1" ]]; then
if command -v timeout >/dev/null 2>&1; then
if ! timeout 30m env PIWIGO_CONFIG="$CONFIG_FILE" bash "$SCRIPT_DIR/build-webdav-media.sh"; then
write_status error "Bildaufbereitung ist fehlgeschlagen oder hat das 30-Minuten-Limit erreicht"
exit 1
fi
elif ! env PIWIGO_CONFIG="$CONFIG_FILE" bash "$SCRIPT_DIR/build-webdav-media.sh"; then
write_status error "Bildaufbereitung ist fehlgeschlagen"
exit 1
fi
else
MEDIA_UNIT="bratonien-nc-media-${CONNECTION_ID}-$(date +%s)"
if ! systemd-run \
--quiet \
--collect \
--unit="$MEDIA_UNIT" \
--property=RuntimeMaxSec=30min \
--setenv="PIWIGO_CONFIG=$CONFIG_FILE" \
/usr/bin/env bash "$SCRIPT_DIR/build-webdav-media.sh"; then
write_status error "Bildaufbereitung konnte nicht im Hintergrund gestartet werden"
exit 1
fi
fi
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \
"WebDAV eingelesen und Piwigo synchronisiert" \
"WebDAV eingelesen und Piwigo synchronisiert; Bildaufbereitung abgeschlossen" \
"" \
"api" \
"ok" \
@@ -181,7 +173,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"Fallback wurde nicht benötigt"
elif grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \
"WebDAV eingelesen und Piwigo über Fallback synchronisiert" \
"WebDAV eingelesen und Piwigo über Fallback synchronisiert; Bildaufbereitung abgeschlossen" \
"" \
"fallback" \
"not_used" \
@@ -189,7 +181,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"ok" \
"Benutzername/Passwort-Fallback erfolgreich"
else
write_status ok "WebDAV eingelesen und Piwigo synchronisiert"
write_status ok "WebDAV eingelesen und Piwigo synchronisiert; Bildaufbereitung abgeschlossen"
fi
else
write_status ok "WebDAV eingelesen; Piwigo-Synchronisierung ist für diese Verbindung deaktiviert"
+1 -13
View File
@@ -52,18 +52,6 @@ if (!pwg_db_num_rows($access_result)) bratonien_tools_webdav_image_abort(403, 'K
$source = bratonien_tools_webdav_image_source_info($image_id);
if (!$source) bratonien_tools_webdav_image_abort(404, 'Keine WebDAV-Quelle für dieses Bild gefunden.');
if (!empty($_GET['ajaxload']))
{
$preview = !empty($_GET['preview']);
$final_url = bratonien_tools_webdav_image_url($image_id, $preview);
if (!$final_url) bratonien_tools_webdav_image_abort(404, 'Keine WebDAV-Bild-URL verfügbar.');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
echo json_encode(array('url'=>$final_url), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
if (!empty($_GET['preview']))
{
$preview = bratonien_tools_webdav_preview_path($source);
@@ -133,7 +121,7 @@ $options = array(
CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.7.24',
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.7.16',
CURLOPT_HEADERFUNCTION => function($ch, $line)
{
$length = strlen($line);