Compare commits

..

2 Commits

Author SHA1 Message Date
Terranom674
370ead2b90 ignore stale branch marker 2026-08-19 14:59:22 +02:00
Terranom674
5d9e017131 noop 2026-08-19 14:59:03 +02:00
39 changed files with 1951 additions and 2121 deletions

View File

@@ -1 +0,0 @@
0.9.7.7

View File

@@ -5,12 +5,8 @@ on:
pull_request:
jobs:
php-syntax:
syntax:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version: ['8.2', '8.3', '8.4', '8.5']
steps:
- name: Repository auschecken
uses: actions/checkout@v4
@@ -18,7 +14,7 @@ jobs:
- name: PHP installieren
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
php-version: '8.2'
coverage: none
- name: PHP Syntax pruefen
@@ -40,12 +36,6 @@ jobs:
exit 1
fi
script-syntax:
runs-on: ubuntu-latest
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: Admin JavaScript Syntax pruefen
shell: bash
run: |

View File

@@ -9,74 +9,6 @@ check_status(ACCESS_ADMINISTRATOR);
require_once(BRATONIEN_TOOLS_PATH . 'include/tool_registry.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_system.inc.php');
function bratonien_tools_nc_connector_admin_connections(array $connections)
{
$by_id = array();
foreach ($connections as $connection)
{
$by_id[(int)$connection['id']] = $connection;
}
$hidden_legacy_ids = array();
$logical_remote = array();
foreach ($connections as $connection)
{
if ((string)($connection['adapter'] ?? '') !== 'remote') continue;
$migration = isset($connection['config']['migration']) && is_array($connection['config']['migration'])
? $connection['config']['migration']
: array();
if ((string)($migration['role'] ?? '') !== 'webdav-primary-candidate') continue;
$legacy_id = (int)($migration['legacy_fallback_connection_id'] ?? 0);
if ($legacy_id < 1 || empty($by_id[$legacy_id]) || (string)$by_id[$legacy_id]['adapter'] !== 'local') continue;
$legacy = $by_id[$legacy_id];
$hidden_legacy_ids[$legacy_id] = true;
$connection['logical_connection'] = true;
$connection['legacy_fallback_connection_id'] = $legacy_id;
$connection['enabled'] = !empty($connection['enabled']) || !empty($legacy['enabled']);
if ($connection['enabled']) $connection['takeover_state'] = 'active';
$connection['fallback_stored'] = !empty($connection['fallback_stored']) || !empty($legacy['fallback_stored']);
if (trim((string)$connection['name']) === '') $connection['name'] = (string)$legacy['name'];
$logical_remote[(int)$connection['id']] = $connection;
}
$visible = array();
foreach ($connections as $connection)
{
$id = (int)$connection['id'];
if (isset($hidden_legacy_ids[$id])) continue;
if (isset($logical_remote[$id])) $connection = $logical_remote[$id];
$visible[] = $connection;
}
return $visible;
}
function bratonien_tools_nc_connector_admin_last_status(array $connection)
{
$latest = bratonien_tools_nc_connector_connection_last_status($connection);
$legacy_id = (int)($connection['legacy_fallback_connection_id'] ?? 0);
if ($legacy_id < 1)
{
return $latest;
}
$legacy = bratonien_tools_nc_connector_connection($legacy_id, false);
if (!$legacy)
{
return $latest;
}
$legacy_status = bratonien_tools_nc_connector_connection_last_status($legacy);
if ((int)($legacy_status['timestamp'] ?? 0) > (int)($latest['timestamp'] ?? 0))
{
return $legacy_status;
}
return $latest;
}
$tools = bratonien_tools_get_tools();
$messages = array();
$errors = array();
@@ -162,19 +94,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bratonien_tool']))
);
$redirect_url = get_root_url().'admin.php?page=plugin-'.BRATONIEN_TOOLS_ID;
$wizard_action = strpos($tool_id, 'nc_connector_wizard_') === 0
|| $tool_id === 'nc_connector_migrate_start'
|| $tool_id === 'nc_connector_edit_start';
if ($wizard_action)
{
$wizard_closed = $tool_id === 'nc_connector_wizard_reset'
|| ($tool_id === 'nc_connector_wizard_finish' && empty($errors));
if (($tool_id === 'nc_connector_migrate_start' || $tool_id === 'nc_connector_edit_start') && !empty($errors))
{
$wizard_closed = true;
}
$redirect_url .= '&nc_wizard='.($wizard_closed ? 'closed' : 'open');
}
if (!headers_sent())
{
header('Location: '.$redirect_url, true, 303);
@@ -196,17 +115,9 @@ $asset_environment = bratonien_tools_get_asset_environment();
$album_shares = bratonien_tools_get_album_shares();
$private_albums = bratonien_tools_get_private_albums();
$nc_connector = bratonien_tools_nc_connector_status();
$nc_connector_runtime_connections = $nc_connector['connections'];
$nc_connector['connections'] = bratonien_tools_nc_connector_admin_connections($nc_connector_runtime_connections);
$nc_connector['connection_count'] = count($nc_connector['connections']);
$nc_connector['active_count'] = 0;
foreach ($nc_connector['connections'] as $visible_connection)
{
if (!empty($visible_connection['enabled'])) $nc_connector['active_count']++;
}
foreach ($nc_connector['connections'] as &$nc_connection)
{
$nc_connection['last_sync'] = bratonien_tools_nc_connector_admin_last_status($nc_connection);
$nc_connection['last_sync'] = bratonien_tools_nc_connector_connection_last_status($nc_connection);
$nc_connection['display_name'] = $nc_connection['name'];
$storage_lines = array();
@@ -255,7 +166,7 @@ $nc_system_defaults = array(
);
$nc_connector['system'] = array_merge(
$nc_system_defaults,
bratonien_tools_nc_connector_system_status($nc_connector_runtime_connections)
bratonien_tools_nc_connector_system_status($nc_connector['connections'])
);
$album_lock_page_number = isset($_GET['br_album_page']) ? max(1, (int)$_GET['br_album_page']) : 1;
$album_lock_search = isset($_GET['br_album_search']) ? trim((string)$_GET['br_album_search']) : '';
@@ -381,4 +292,4 @@ $admin_content .= $template->parse('asset_manager_admin_content', true);
$admin_content .= $template->parse('album_shares_admin_content', true);
$admin_content .= $template->parse('nc_connector_admin_content', true);
$admin_content .= $template->parse('system_admin_content', true);
$template->assign('ADMIN_CONTENT', $admin_content);
$template->assign('ADMIN_CONTENT', $admin_content);

View File

@@ -4,84 +4,49 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_db_prefix_from_absolute($path)
function bratonien_tools_nc_connector_webdav_site_id(array $connection)
{
$path = rtrim(str_replace('\\', '/', (string)$path), '/');
$piwigo_root = rtrim(str_replace('\\', '/', PHPWG_ROOT_PATH), '/');
if ($path === '' || strpos($path, $piwigo_root.'/') !== 0) return '';
$relative = ltrim(substr($path, strlen($piwigo_root)), '/');
return $relative === '' ? '' : './'.rtrim($relative, '/').'/';
}
function bratonien_tools_nc_connector_owned_db_prefixes(array $connection)
{
$id = (int)($connection['id'] ?? 0);
if ($id < 1) return array();
$prefixes = array(
'./_data/bratonien-tools/nc-webdav-gallery/connection-'.$id.'/',
'./_data/bratonien-tools/nc-webdav-source/connection-'.$id.'/',
'./galleries/bratonien-webdav-'.$id.'/',
);
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$configured = bratonien_tools_nc_connector_db_prefix_from_absolute($config['parallel_gallery_root'] ?? '');
if ($configured !== '') $prefixes[] = $configured;
if ((string)($connection['adapter'] ?? '') !== 'remote') return 0;
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') return 0;
return array_values(array_unique($prefixes));
}
$gallery_root = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
$piwigo_root = rtrim(PHPWG_ROOT_PATH, '/');
if ($gallery_root === '' || strpos($gallery_root, $piwigo_root.'/') !== 0) return 0;
function bratonien_tools_nc_connector_owned_site_urls(array $connection)
{
$id = (int)($connection['id'] ?? 0);
if ($id < 1) return array();
$relative = ltrim(substr($gallery_root, strlen($piwigo_root)), '/');
if ($relative === '') return 0;
$site_url = './'.rtrim($relative, '/').'/';
$urls = array(
'./_data/bratonien-tools/nc-webdav-gallery/connection-'.$id.'/',
'./galleries/bratonien-webdav-'.$id.'/',
$result = pwg_query(
"SELECT id FROM ".SITES_TABLE.
" WHERE galleries_url='".pwg_db_real_escape_string($site_url)."' LIMIT 1"
);
if (!pwg_db_num_rows($result)) return 0;
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$configured = bratonien_tools_nc_connector_db_prefix_from_absolute($config['parallel_gallery_root'] ?? '');
if ($configured !== '') $urls[] = $configured;
return array_values(array_unique($urls));
$row = pwg_db_fetch_assoc($result);
return (int)$row['id'];
}
function bratonien_tools_nc_connector_remove_webdav_piwigo_content(array $connection)
{
$site_id = bratonien_tools_nc_connector_webdav_site_id($connection);
if ($site_id < 1) return array('site_id'=>0, 'images'=>0);
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$prefixes = bratonien_tools_nc_connector_owned_db_prefixes($connection);
if (!$prefixes) return array('sites'=>0, 'images'=>0);
$where = array();
foreach ($prefixes as $prefix)
{
$escaped = pwg_db_real_escape_string(addcslashes($prefix, '_%\\'));
$where[] = "path LIKE '".$escaped."%' ESCAPE '\\\\'";
}
$image_rows = array();
$result = pwg_query(
'SELECT id, path, representative_ext FROM '.IMAGES_TABLE.
' WHERE '.implode(' OR ', $where)
);
$query = '
SELECT DISTINCT i.id, i.path, i.representative_ext
FROM '.IMAGES_TABLE.' AS i
LEFT JOIN '.CATEGORIES_TABLE.' AS sc ON sc.id = i.storage_category_id
LEFT JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON ic.image_id = i.id
LEFT JOIN '.CATEGORIES_TABLE.' AS vc ON vc.id = ic.category_id
WHERE sc.site_id = '.$site_id.' OR vc.site_id = '.$site_id.'
;';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
$path = (string)$row['path'];
$owned = false;
foreach ($prefixes as $prefix)
{
if (strpos($path, $prefix) === 0)
{
$owned = true;
break;
}
}
if (!$owned) continue;
$row['id'] = (int)$row['id'];
$image_rows[$row['id']] = $row;
}
@@ -98,33 +63,31 @@ function bratonien_tools_nc_connector_remove_webdav_piwigo_content(array $connec
}
}
delete_site($site_id);
if ($image_rows)
{
delete_elements(array_map('intval', array_keys($image_rows)), false);
}
$site_count = 0;
foreach (bratonien_tools_nc_connector_owned_site_urls($connection) as $site_url)
{
$result = pwg_query(
"SELECT id FROM ".SITES_TABLE.
" WHERE galleries_url='".pwg_db_real_escape_string($site_url)."'"
$ids = array_keys($image_rows);
$remaining = query2array(
'SELECT id FROM '.IMAGES_TABLE.' WHERE id IN ('.implode(',', array_map('intval', $ids)).')',
null,
'id'
);
while ($row = pwg_db_fetch_assoc($result))
if ($remaining)
{
delete_site((int)$row['id']);
$site_count++;
delete_elements(array_map('intval', $remaining), false);
}
}
update_category('all');
invalidate_user_cache(true);
return array('sites'=>$site_count, 'images'=>count($image_rows));
return array('site_id'=>$site_id, 'images'=>count($image_rows));
}
/**
* Delete exactly the connection selected by the user.
* No other connector record is implicitly removed.
* Delete a connector connection from Piwigo without depending on the web
* server being allowed to write into root-owned runtime directories.
* WebDAV-managed Piwigo records are removed before the connection itself so
* deleted remote content cannot remain visible or addressable in Piwigo.
*/
function bratonien_tools_nc_connector_delete_safe()
{
@@ -135,7 +98,7 @@ function bratonien_tools_nc_connector_delete_safe()
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
$cleanup = array('sites'=>0, 'images'=>0);
$cleanup = array('site_id'=>0, 'images'=>0);
if (
(string)($connection['adapter'] ?? '') === 'remote'
&& (string)($connection['config']['source_mode'] ?? '') === 'webdav-placeholder'
@@ -149,14 +112,24 @@ function bratonien_tools_nc_connector_delete_safe()
$status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status';
$public_status = $status_dir.'/connection-'.$id.'.json';
if (is_file($public_status)) @unlink($public_status);
if (is_file($public_status))
{
@unlink($public_status);
}
if (is_dir($status_dir) || @mkdir($status_dir, 0755, true))
{
@file_put_contents($status_dir.'/deleted-'.$id, date('c')."\n", LOCK_EX);
}
if ((int)$cleanup['site_id'] > 0)
{
return array(
'message'=>'Connector-Verbindung wurde gelöscht. Die zugehörigen Piwigo-Alben und '.(int)$cleanup['images'].' Bilder wurden aus Piwigo entfernt. Nextcloud-Dateien blieben unverändert. Laufzeit- und Vorschaudaten werden automatisch bereinigt.',
);
}
return array(
'message'=>'Verbindung wurde gelöscht. '.(int)$cleanup['images'].' eindeutig zu dieser Verbindung gehörende Bilder wurden aus Piwigo entfernt. Nextcloud-Dateien blieben unverändert.',
'message'=>'Connector-Verbindung wurde gelöscht. Verbliebene Laufzeitdateien werden vor dem nächsten Connector-Lauf automatisch entfernt. Quelldateien blieben unverändert.',
);
}

View File

@@ -13,22 +13,12 @@ function bratonien_tools_nc_connector_migration_state(array $connection)
if (trim((string)($config['nextcloud_url'] ?? '')) === '') $missing[] = 'Nextcloud-Adresse';
if (trim((string)($credentials['nextcloud_user'] ?? '')) === '') $missing[] = 'Nextcloud-Benutzer';
if ((string)($credentials['nextcloud_password'] ?? '') === '') $missing[] = 'Nextcloud-Passwort';
$api_id = trim((string)($credentials['api_key_id'] ?? ''));
$api_secret = trim((string)($credentials['api_key_secret'] ?? ''));
$fallback_user = trim((string)($credentials['piwigo_user'] ?? ''));
$fallback_password = (string)($credentials['piwigo_password'] ?? '');
$api_complete = $api_id !== '' && $api_secret !== '';
$fallback_complete = $fallback_user !== '' && $fallback_password !== '';
if (($api_id === '') !== ($api_secret === '')) $missing[] = 'vollstaendiger Piwigo-API-Zugang';
if (!$api_complete && !$fallback_complete) $missing[] = 'Piwigo-API oder Benutzer/Passwort-Fallback';
if (trim((string)($credentials['api_key_id'] ?? '')) === '') $missing[] = 'Piwigo-API-Schluessel-ID';
if (trim((string)($credentials['api_key_secret'] ?? '')) === '') $missing[] = 'Piwigo-API-Geheimnis';
return array(
'ready'=>!$missing,
'missing'=>$missing,
'api_available'=>$api_complete,
'fallback_available'=>$fallback_complete,
);
}
@@ -122,7 +112,7 @@ function bratonien_tools_nc_connector_prepare_webdav_wizard_from_connection(arra
{
$path = trim((string)($root['webdav_path'] ?? ''), '/');
$fileid = (int)($root['fileid'] ?? 0);
if ($fileid < 1) continue;
if ($path === '' || $fileid < 1) continue;
$selected[] = $path;
$selected_ids[$path] = $fileid;
}

View File

@@ -1,210 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_scheduler_paths()
{
$base = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools';
return array(
'base'=>$base,
'runtime'=>$base.'/nc-connector-runtime',
'state_root'=>$base.'/nc-connector-state',
'scheduler'=>$base.'/nc-connector-scheduler',
'state'=>$base.'/nc-connector-scheduler/state.json',
'trigger_lock'=>$base.'/nc-connector-scheduler/trigger.lock',
'worker_lock'=>$base.'/nc-connector-scheduler/worker.lock',
'log'=>$base.'/nc-connector-scheduler/last-worker.log',
'status_root'=>$base.'/nc-connector-status',
);
}
function bratonien_tools_nc_scheduler_interval()
{
global $conf;
$interval = isset($conf['bratonien_nc_scheduler_interval']) ? (int)$conf['bratonien_nc_scheduler_interval'] : 60;
return max(60, $interval);
}
function bratonien_tools_nc_scheduler_ensure_dirs()
{
foreach (bratonien_tools_nc_scheduler_paths() as $key=>$path)
{
if (in_array($key, array('state','trigger_lock','worker_lock','log'), true)) continue;
if (!is_dir($path) && !@mkdir($path, 0750, true) && !is_dir($path))
{
return false;
}
}
return true;
}
function bratonien_tools_nc_scheduler_read_state()
{
$paths = bratonien_tools_nc_scheduler_paths();
if (!is_readable($paths['state'])) return array();
$decoded = json_decode((string)@file_get_contents($paths['state']), true);
return is_array($decoded) ? $decoded : array();
}
function bratonien_tools_nc_scheduler_write_state(array $state)
{
$paths = bratonien_tools_nc_scheduler_paths();
if (!bratonien_tools_nc_scheduler_ensure_dirs()) return false;
$json = json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if (!is_string($json)) return false;
$tmp = $paths['state'].'.tmp';
if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false) return false;
@chmod($tmp, 0640);
return @rename($tmp, $paths['state']);
}
function bratonien_tools_nc_scheduler_write_connection_status($connection_id, $state, $message)
{
$connection_id = (int)$connection_id;
if ($connection_id < 1) return false;
$paths = bratonien_tools_nc_scheduler_paths();
if (!bratonien_tools_nc_scheduler_ensure_dirs()) return false;
$target = $paths['status_root'].'/connection-'.$connection_id.'.json';
$existing = array();
if (is_readable($target))
{
$decoded = json_decode((string)@file_get_contents($target), true);
if (is_array($decoded)) $existing = $decoded;
}
$existing['state'] = (string)$state;
$existing['message'] = (string)$message;
$existing['timestamp'] = time();
$existing['connection_id'] = $connection_id;
$json = json_encode($existing, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if (!is_string($json)) return false;
$tmp = $target.'.tmp';
if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false) return false;
@chmod($tmp, 0644);
return @rename($tmp, $target);
}
function bratonien_tools_nc_scheduler_install()
{
if (!bratonien_tools_nc_scheduler_ensure_dirs()) return false;
$state = bratonien_tools_nc_scheduler_read_state();
if (empty($state['next_due']))
{
$state['next_due'] = time();
$state['enabled'] = true;
$state['mode'] = 'piwigo-native';
bratonien_tools_nc_scheduler_write_state($state);
}
return true;
}
function bratonien_tools_nc_scheduler_php_binary()
{
foreach (array('/usr/bin/php', '/usr/local/bin/php', PHP_BINARY) as $candidate)
{
if (is_string($candidate) && $candidate !== '' && is_executable($candidate)) return $candidate;
}
return '';
}
function bratonien_tools_nc_scheduler_spawn($force = false, $connection_id = 0)
{
$connection_id = max(0, (int)$connection_id);
$paths = bratonien_tools_nc_scheduler_paths();
if (!bratonien_tools_nc_scheduler_ensure_dirs())
{
throw new RuntimeException('Der native NC-Scheduler kann sein Laufzeitverzeichnis nicht anlegen.');
}
$lock = @fopen($paths['trigger_lock'], 'c+');
if (!is_resource($lock) || !@flock($lock, LOCK_EX | LOCK_NB))
{
if (is_resource($lock)) fclose($lock);
return array('started'=>false, 'message'=>'Ein NC-Abgleich wird bereits vorbereitet.');
}
$state = bratonien_tools_nc_scheduler_read_state();
$now = time();
$next_due = (int)($state['next_due'] ?? 0);
if (!$force && $next_due > $now)
{
@flock($lock, LOCK_UN);
fclose($lock);
return array('started'=>false, 'message'=>'Der nächste NC-Abgleich ist noch nicht fällig.');
}
$php = bratonien_tools_nc_scheduler_php_binary();
if ($php === '')
{
@flock($lock, LOCK_UN);
fclose($lock);
throw new RuntimeException('Kein ausführbares PHP-CLI für den nativen NC-Scheduler gefunden.');
}
$state['enabled'] = true;
$state['mode'] = 'piwigo-native';
$state['state'] = 'queued';
$state['message'] = $connection_id > 0 ? 'NC-Abgleich für Verbindung #'.$connection_id.' wurde angefordert.' : 'NC-Abgleich für alle Verbindungen wurde angefordert.';
$state['queued_at'] = $now;
$state['timestamp'] = $now;
$state['connection_id'] = $connection_id;
$state['next_due'] = $now + bratonien_tools_nc_scheduler_interval();
bratonien_tools_nc_scheduler_write_state($state);
if ($connection_id > 0)
{
bratonien_tools_nc_scheduler_write_connection_status($connection_id, 'queued', 'Abgleich wurde angefordert.');
}
$runner = BRATONIEN_TOOLS_PATH.'runtime/native-runner.php';
$runner_args = $connection_id > 0 ? ' --connection-id='.escapeshellarg((string)$connection_id) : '';
$command = escapeshellarg($php).' '.escapeshellarg($runner).$runner_args.' >> '.escapeshellarg($paths['log']).' 2>&1 &';
$spec = array(
0=>array('file','/dev/null','r'),
1=>array('file','/dev/null','a'),
2=>array('file','/dev/null','a'),
);
$process = function_exists('proc_open') ? @proc_open(array('/bin/sh','-c',$command), $spec, $pipes) : false;
$exit = is_resource($process) ? proc_close($process) : 1;
@flock($lock, LOCK_UN);
fclose($lock);
if ($exit !== 0)
{
$state = bratonien_tools_nc_scheduler_read_state();
$state['state'] = 'error';
$state['message'] = 'Der native NC-Abgleich konnte nicht gestartet werden.';
$state['timestamp'] = time();
bratonien_tools_nc_scheduler_write_state($state);
if ($connection_id > 0)
{
bratonien_tools_nc_scheduler_write_connection_status($connection_id, 'error', 'Abgleich konnte nicht gestartet werden.');
}
throw new RuntimeException('Der native NC-Abgleich konnte nicht gestartet werden.');
}
return array('started'=>true, 'message'=>$connection_id > 0 ? 'Abgleich für Verbindung #'.$connection_id.' wurde angefordert.' : 'Abgleich für alle Verbindungen wurde angefordert.');
}
function bratonien_tools_nc_scheduler_tick()
{
$state = bratonien_tools_nc_scheduler_read_state();
if (isset($state['enabled']) && !$state['enabled']) return;
$next_due = (int)($state['next_due'] ?? 0);
if ($next_due > time()) return;
try
{
bratonien_tools_nc_scheduler_spawn(false, 0);
}
catch (Throwable $e)
{
$state = bratonien_tools_nc_scheduler_read_state();
$state['state'] = 'error';
$state['message'] = $e->getMessage();
$state['timestamp'] = time();
$state['next_due'] = time() + bratonien_tools_nc_scheduler_interval();
bratonien_tools_nc_scheduler_write_state($state);
}
}

View File

@@ -4,13 +4,109 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!');
}
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_scheduler.inc.php');
function bratonien_tools_nc_connector_systemctl_value(array $args)
{
if (!function_exists('proc_open'))
{
return '';
}
$command = array_merge(array('/usr/bin/systemctl'), $args);
$spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$environment = array_merge($_ENV, array('LC_ALL'=>'C', 'LANG'=>'C'));
$process = @proc_open($command, $spec, $pipes, null, $environment);
if (!is_resource($process))
{
return '';
}
$stdout = stream_get_contents($pipes[1]);
stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
return $exit === 0 ? trim((string)$stdout) : '';
}
function bratonien_tools_nc_connector_parse_systemd_time($value)
{
$value = trim((string)$value);
if ($value === '' || strtolower($value) === 'n/a')
{
return 0;
}
$parsed = strtotime($value);
return $parsed === false ? 0 : (int)$parsed;
}
function bratonien_tools_nc_connector_monotonic_to_timestamp($value)
{
$value = trim((string)$value);
if ($value === '' || $value === '0' || strtolower($value) === 'n/a')
{
return 0;
}
if (preg_match('/^([0-9]+)(?:us)?$/', $value, $matches))
{
$next_boot_seconds = ((float)$matches[1]) / 1000000;
}
else
{
return 0;
}
$uptime_raw = @file_get_contents('/proc/uptime');
if (!is_string($uptime_raw) || !preg_match('/^([0-9]+(?:\.[0-9]+)?)/', trim($uptime_raw), $uptime_match))
{
return 0;
}
$remaining = $next_boot_seconds - (float)$uptime_match[1];
if ($remaining < -1)
{
return 0;
}
return (int)round(time() + max(0, $remaining));
}
function bratonien_tools_nc_connector_next_from_timer_list($timer)
{
$line = bratonien_tools_nc_connector_systemctl_value(array(
'list-timers', '--all', '--no-pager', '--no-legend', $timer,
));
if ($line === '')
{
return 0;
}
$first_line = trim((string)strtok($line, "\n"));
if (preg_match('/^(\S+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+\S+)/', $first_line, $matches))
{
$parsed = strtotime($matches[1]);
return $parsed === false ? 0 : (int)$parsed;
}
return 0;
}
function bratonien_tools_nc_connector_connection_last_status(array $connection)
{
$empty = array(
'timestamp'=>0,'label'=>'Nicht verfügbar','state'=>'','message'=>'','auth_mode'=>'',
'api_state'=>'','api_message'=>'','fallback_state'=>'','fallback_message'=>'','error_detail'=>'',
'timestamp'=>0,
'label'=>'Nicht verfügbar',
'state'=>'',
'message'=>'',
'auth_mode'=>'',
'api_state'=>'',
'api_message'=>'',
'fallback_state'=>'',
'fallback_message'=>'',
'error_detail'=>'',
);
$connection_id = (int)($connection['id'] ?? 0);
@@ -18,25 +114,42 @@ function bratonien_tools_nc_connector_connection_last_status(array $connection)
if ($connection_id > 0)
{
$candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$connection_id.'.json';
$candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-state/connection-'.$connection_id.'/connector-status.json';
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '') $candidates[] = $state_dir.'/connector-status.json';
if ($state_dir === '' && $connection_id > 0)
{
$state_dir = '/var/lib/bratonien-tools/nc-connector/connection-'.$connection_id;
}
if ($state_dir !== '')
{
$candidates[] = $state_dir.'/connector-status.json';
}
$decoded = null;
foreach (array_unique($candidates) as $candidate)
foreach ($candidates as $candidate)
{
if (!is_readable($candidate)) continue;
if (!is_readable($candidate))
{
continue;
}
$value = json_decode((string)@file_get_contents($candidate), true);
if (is_array($value)) { $decoded = $value; break; }
if (is_array($value))
{
$decoded = $value;
break;
}
}
if (!is_array($decoded))
{
return $empty;
}
if (!is_array($decoded)) return $empty;
$timestamp = (int)($decoded['timestamp'] ?? 0);
$api = isset($decoded['api']) && is_array($decoded['api']) ? $decoded['api'] : array();
$fallback = isset($decoded['fallback']) && is_array($decoded['fallback']) ? $decoded['fallback'] : array();
return array(
'timestamp'=>$timestamp,
'label'=>$timestamp > 0 ? date('d.m.Y H:i:s', $timestamp) : 'Nicht verfügbar',
@@ -53,88 +166,73 @@ function bratonien_tools_nc_connector_connection_last_status(array $connection)
function bratonien_tools_nc_connector_last_status(array $connections)
{
$latest = array('timestamp'=>0,'state'=>'','message'=>'','auth_mode'=>'','api_state'=>'','api_message'=>'','fallback_state'=>'','fallback_message'=>'','error_detail'=>'');
$latest = array('timestamp'=>0, 'state'=>'', 'message'=>'', 'auth_mode'=>'', 'api_state'=>'', 'api_message'=>'', 'fallback_state'=>'', 'fallback_message'=>'', 'error_detail'=>'');
foreach ($connections as $connection)
{
if (empty($connection['enabled'])) continue;
if (empty($connection['enabled']) || (string)($connection['takeover_state'] ?? '') !== 'active')
{
continue;
}
$status = bratonien_tools_nc_connector_connection_last_status($connection);
if ((int)$status['timestamp'] >= (int)$latest['timestamp']) $latest = $status;
if ((int)$status['timestamp'] >= (int)$latest['timestamp'])
{
$latest = $status;
}
}
return $latest;
}
function bratonien_tools_nc_connector_system_status(array $connections = array())
{
$scheduler = bratonien_tools_nc_scheduler_read_state();
$enabled = !isset($scheduler['enabled']) || !empty($scheduler['enabled']);
$scheduler_state = (string)($scheduler['state'] ?? '');
$running = $scheduler_state === 'running';
$queued = $scheduler_state === 'queued';
$started = (int)($scheduler['started_at'] ?? 0);
$next = (int)($scheduler['next_due'] ?? 0);
$timer = 'bratonien-nc-connector.timer';
$service = 'bratonien-nc-connector.service';
$next_realtime_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $timer, '--property=NextElapseUSecRealtime', '--value'));
$next_timestamp = bratonien_tools_nc_connector_parse_systemd_time($next_realtime_raw);
if ($next_timestamp <= 0)
{
$next_monotonic_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $timer, '--property=NextElapseUSecMonotonic', '--value'));
$next_timestamp = bratonien_tools_nc_connector_monotonic_to_timestamp($next_monotonic_raw);
}
if ($next_timestamp <= 0)
{
$next_timestamp = bratonien_tools_nc_connector_next_from_timer_list($timer);
}
$active = bratonien_tools_nc_connector_systemctl_value(array('is-active', $timer));
$enabled = bratonien_tools_nc_connector_systemctl_value(array('is-enabled', $timer));
$last = bratonien_tools_nc_connector_last_status($connections);
if ($next > 0)
if ($last['timestamp'] <= 0)
{
$next_label = date('d.m.Y H:i:s', $next).' (beim nächsten Piwigo-Aufruf)';
$last_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $service, '--property=ExecMainExitTimestamp', '--value'));
$last['timestamp'] = bratonien_tools_nc_connector_parse_systemd_time($last_raw);
}
else
{
$next_label = 'Beim nächsten Piwigo-Aufruf';
}
$scheduler_timestamp = (int)($scheduler['timestamp'] ?? 0);
if ($scheduler_timestamp > (int)$last['timestamp'])
{
$last['timestamp'] = $scheduler_timestamp;
$last['state'] = $scheduler_state;
$last['message'] = (string)($scheduler['message'] ?? '');
if ($scheduler_state === 'error')
{
$detail = trim((string)($scheduler['stderr'] ?? ''));
if ($detail === '') $detail = trim((string)($scheduler['stdout'] ?? ''));
$last['error_detail'] = $detail;
}
}
elseif ((int)$last['timestamp'] <= 0 && !empty($scheduler['finished_at']))
{
$last['timestamp'] = (int)$scheduler['finished_at'];
$last['state'] = $scheduler_state;
$last['message'] = (string)($scheduler['message'] ?? '');
if ((string)$last['state'] === 'error')
{
$detail = trim((string)($scheduler['stderr'] ?? ''));
if ($detail === '') $detail = trim((string)($scheduler['stdout'] ?? ''));
$last['error_detail'] = $detail;
}
}
$current_label = 'Kein Lauf aktiv';
if ($queued) $current_label = 'Abgleich angefordert';
if ($running) $current_label = $started > 0 ? 'Läuft seit '.date('d.m.Y H:i:s', $started) : 'Läuft gerade';
return array(
'timer_name'=>'Piwigo nativer NC-Scheduler',
'timer_active'=>$enabled,
'timer_enabled'=>$enabled,
'service_active'=>$running || $queued,
'current_run_timestamp'=>$queued ? (int)($scheduler['queued_at'] ?? 0) : $started,
'current_run_label'=>$current_label,
'last_run_timestamp'=>(int)$last['timestamp'],
'last_run_label'=>(int)$last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar',
'last_run_state'=>(string)$last['state'],
'last_run_message'=>(string)$last['message'],
'last_run_auth_mode'=>(string)$last['auth_mode'],
'last_run_api_state'=>(string)$last['api_state'],
'last_run_api_message'=>(string)$last['api_message'],
'last_run_fallback_state'=>(string)$last['fallback_state'],
'last_run_fallback_message'=>(string)$last['fallback_message'],
'last_run_error_detail'=>(string)$last['error_detail'],
'next_run_timestamp'=>$next,
'next_run_label'=>$next_label,
'legacy_runtime_exists'=>is_dir('/opt/piwigo-sync'),
'legacy_config_exists'=>is_dir('/etc/piwigo-sync'),
'legacy_service_exists'=>is_file('/etc/systemd/system/piwigo-sync.service'),
'legacy_timer_exists'=>is_file('/etc/systemd/system/piwigo-sync.timer'),
'timer_name' => $timer,
'timer_active' => $active === 'active',
'timer_enabled' => $enabled === 'enabled',
'last_run_timestamp' => (int)$last['timestamp'],
'last_run_label' => $last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar',
'last_run_state' => (string)$last['state'],
'last_run_message' => (string)$last['message'],
'last_run_auth_mode' => (string)$last['auth_mode'],
'last_run_api_state' => (string)$last['api_state'],
'last_run_api_message' => (string)$last['api_message'],
'last_run_fallback_state' => (string)$last['fallback_state'],
'last_run_fallback_message' => (string)$last['fallback_message'],
'last_run_error_detail' => (string)$last['error_detail'],
'next_run_timestamp' => $next_timestamp,
'next_run_label' => $next_timestamp > 0 ? date('d.m.Y H:i:s', $next_timestamp) : 'Nicht verfügbar',
'legacy_runtime_exists' => is_dir('/opt/piwigo-sync'),
'legacy_config_exists' => is_dir('/etc/piwigo-sync'),
'legacy_service_exists' => is_file('/etc/systemd/system/piwigo-sync.service'),
'legacy_timer_exists' => is_file('/etc/systemd/system/piwigo-sync.timer'),
);
}

View File

@@ -0,0 +1,135 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
/**
* Controlled handover preparation for verified NC Connector connections.
*
* This phase intentionally does not stop, disable or modify the legacy
* piwigo-sync service. It only marks a verified Connector connection as ready
* for a later controlled takeover. The marker can be rolled back at any time.
*/
function bratonien_tools_nc_connector_ensure_takeover_state()
{
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
pwg_query("ALTER TABLE `$table` MODIFY takeover_state enum('imported','verified','ready','active','disabled') NOT NULL DEFAULT 'imported'");
}
function bratonien_tools_nc_connector_prepare_takeover()
{
$id = isset($_POST['connection_id']) ? (int)$_POST['connection_id'] : 0;
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
if ($connection['takeover_state'] !== 'verified')
{
throw new RuntimeException('Nur eine erfolgreich verifizierte Verbindung kann fuer die Uebergabe vorbereitet werden.');
}
$config = $connection['config'];
$verification = isset($config['verification']) && is_array($config['verification'])
? $config['verification']
: array();
if (empty($verification['ok']))
{
throw new RuntimeException('Die gespeicherte Verifikation ist nicht erfolgreich. Bitte die Verbindung erneut pruefen.');
}
$config['takeover'] = array(
'prepared_at' => date('Y-m-d H:i:s'),
'legacy_sync_untouched' => true,
'connector_enabled' => false,
'first_run' => array(
'status' => 'pending',
'finished_at' => null,
'detail' => '',
),
);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Uebergabestatus konnte nicht gespeichert werden.');
}
bratonien_tools_nc_connector_ensure_takeover_state();
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET
takeover_state = 'ready',
enabled = 0,
config_json = '".pwg_db_real_escape_string($config_json)."',
updated = '".pwg_db_real_escape_string($now)."'
WHERE id = ".(int)$connection['id']);
return array(
'message' => 'Die verifizierte Nextcloud-Verbindung ist jetzt fuer die kontrollierte Uebergabe vorbereitet. Der Connector wurde noch nicht aktiviert und der Legacy-Sync bleibt unveraendert Produktionsverbindung.',
);
}
function bratonien_tools_nc_connector_cancel_takeover()
{
$id = isset($_POST['connection_id']) ? (int)$_POST['connection_id'] : 0;
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
if ($connection['takeover_state'] !== 'ready')
{
throw new RuntimeException('Diese Verbindung befindet sich nicht in der Uebergabevorbereitung.');
}
$config = $connection['config'];
if (isset($config['takeover']))
{
unset($config['takeover']);
}
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Uebergabestatus konnte nicht zurueckgesetzt werden.');
}
bratonien_tools_nc_connector_ensure_takeover_state();
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET
takeover_state = 'verified',
enabled = 0,
config_json = '".pwg_db_real_escape_string($config_json)."',
updated = '".pwg_db_real_escape_string($now)."'
WHERE id = ".(int)$connection['id']);
return array(
'message' => 'Die Uebergabevorbereitung wurde zurueckgenommen. Die Verbindung bleibt verifiziert und deaktiviert; der Legacy-Sync bleibt unveraendert aktiv.',
);
}
/**
* Normalize the outcome of a Connector sync run during takeover.
*
* Both "changed" and "no_changes" are successful results. Only "error"
* represents a failed run and may trigger rollback of a controlled handover.
*/
function bratonien_tools_nc_connector_takeover_result($status, $detail = '')
{
$status = trim((string)$status);
if (!in_array($status, array('changed', 'no_changes', 'error'), true))
{
throw new RuntimeException('Unbekannter Connector-Laufstatus: '.$status);
}
return array(
'status' => $status,
'success' => $status !== 'error',
'changed' => $status === 'changed',
'finished_at' => date('Y-m-d H:i:s'),
'detail' => (string)$detail,
);
}

View File

@@ -5,8 +5,81 @@ if (!defined('PHPWG_ROOT_PATH'))
}
/**
* Create or update one independent WebDAV connector connection.
* No migration pair, successor or implicit legacy fallback is created.
* Return the only existing local connector as migration fallback.
*/
function bratonien_tools_nc_connector_single_local_fallback()
{
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
$result = pwg_query("SELECT id, enabled, takeover_state, config_json FROM `$table` WHERE adapter='local' ORDER BY id");
$rows = array();
while ($row = pwg_db_fetch_assoc($result))
{
$rows[] = $row;
if (count($rows) > 1) return null;
}
if (count($rows) !== 1) return null;
$row = $rows[0];
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) $config = array();
return array(
'id'=>(int)$row['id'],
'enabled'=>(bool)$row['enabled'],
'takeover_state'=>(string)$row['takeover_state'],
'config'=>$config,
);
}
/**
* Link a WebDAV connection with the single existing local fallback.
*/
function bratonien_tools_nc_connector_pair_migration_fallback($webdav_id, array &$webdav_config, $now)
{
$legacy = bratonien_tools_nc_connector_single_local_fallback();
if (!$legacy) return null;
$legacy_id = (int)$legacy['id'];
if ($legacy_id < 1 || $legacy_id === (int)$webdav_id) return null;
$webdav_config['migration'] = array(
'role'=>'webdav-primary-candidate',
'legacy_fallback_connection_id'=>$legacy_id,
'fallback_policy'=>'keep-running',
'paired_at'=>(string)$now,
'cutover_state'=>'parallel',
);
$legacy_config = $legacy['config'];
$legacy_config['migration'] = array(
'role'=>'legacy-fallback',
'webdav_successor_connection_id'=>(int)$webdav_id,
'fallback_policy'=>'keep-running',
'paired_at'=>(string)$now,
'cutover_state'=>'parallel',
);
$legacy_json = json_encode($legacy_config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($legacy_json))
{
throw new RuntimeException('Die bestehende Verbindung konnte nicht als Migrations-Fallback markiert werden.');
}
$table = bratonien_tools_nc_connector_table();
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($legacy_json)."', updated='".pwg_db_real_escape_string((string)$now)."' WHERE id=".$legacy_id." LIMIT 1");
return $legacy_id;
}
/**
* Create or update a WebDAV-backed connector from the user-facing wizard.
*
* Remote connections are updated in place. When the editor was opened for a
* legacy connection, a WebDAV successor is created and the legacy connection
* stays available as fallback.
*/
function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
{
@@ -14,7 +87,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
if (empty($state['scan_ok']) || empty($state['technical_complete']))
{
throw new RuntimeException('Die WebDAV-Verbindung wurde im Assistenten noch nicht vollstaendig vorbereitet.');
throw new RuntimeException('Die WebDAV-Verbindung wurde im Assistenten noch nicht vollständig vorbereitet.');
}
$base_url = rtrim(trim((string)($state['base_url'] ?? '')), '/');
@@ -22,7 +95,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$password = (string)($state['_password'] ?? '');
if ($base_url === '' || $username === '' || $password === '')
{
throw new RuntimeException('Fuer den WebDAV-Zugang fehlen Nextcloud-Adresse oder Zugangsdaten.');
throw new RuntimeException('Für den WebDAV-Zugang fehlen Nextcloud-Adresse oder Zugangsdaten.');
}
$selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
@@ -31,26 +104,17 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$selected_ids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids'])
? $state['directory_selected_fileids']
: array();
if (!$selected)
{
$selected = array('');
}
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
$roots = array();
foreach ($selected as $path)
{
$fileid = isset($selected_ids[$path]) ? (int)$selected_ids[$path] : 0;
if ($fileid < 1)
{
throw new RuntimeException('Fuer ein ausgewaehltes Nextcloud-Verzeichnis fehlt die eindeutige Datei-ID.');
}
$display_name = $path === ''
? (trim((string)($state['display_name'] ?? '')) !== '' ? trim((string)$state['display_name']) : $username)
: basename($path);
if ($fileid < 1) throw new RuntimeException('Für ein ausgewähltes Nextcloud-Verzeichnis fehlt die eindeutige Datei-ID.');
$roots[] = array(
'fileid'=>$fileid,
'display_name'=>$display_name,
'display_name'=>basename($path),
'webdav_path'=>$path,
);
}
@@ -74,11 +138,6 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
&& (string)($editing_connection['config']['source_mode'] ?? '') === 'webdav-placeholder'
&& $editing_mode === 'update';
if ($editing_mode === 'migrate')
{
throw new RuntimeException('Die Migrationsfunktion wurde entfernt. Bitte eine normale WebDAV-Verbindung anlegen.');
}
$config = array(
'origin'=>'native',
'source_mode'=>'webdav-placeholder',
@@ -93,6 +152,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
'quiet_seconds'=>120,
'max_wait_seconds'=>900,
'full_sync_seconds'=>86400,
'parallel_test'=>true,
'piwigo_auth'=>'connection-scoped',
'api_enabled'=>$api_enabled,
);
@@ -100,7 +160,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
if ($editing_remote)
{
$old_config = is_array($editing_connection['config'] ?? null) ? $editing_connection['config'] : array();
foreach (array('state_dir','status_file','parallel_gallery_root','source_fingerprint','runtime') as $preserve)
foreach (array('state_dir','status_file','parallel_gallery_root','source_fingerprint','runtime','migration') as $preserve)
{
if (array_key_exists($preserve, $old_config)) $config[$preserve] = $old_config[$preserve];
}
@@ -140,12 +200,12 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nicht serialisiert werden.');
pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$editing_id." AND adapter='remote' LIMIT 1");
pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$editing_id." LIMIT 1");
unset($_SESSION['bratonien_nc_wizard']);
return array(
'connection_id'=>$editing_id,
'message'=>'WebDAV-Verbindung wurde gespeichert.',
'message'=>'WebDAV-Verbindung #'.$editing_id.' wurde gespeichert. Der nächste Connector-Lauf verwendet die neuen Einstellungen.',
);
}
@@ -164,14 +224,36 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$id = (int)pwg_db_insert_id();
if ($id < 1) throw new RuntimeException('Die WebDAV-Verbindung konnte nicht eindeutig angelegt werden.');
$config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file'] = $config['state_dir'].'/connector-status.json';
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nach dem Anlegen nicht serialisiert werden.');
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id." AND adapter='remote' LIMIT 1");
try
{
$config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file'] = $config['state_dir'].'/connector-status.json';
$legacy_fallback_id = bratonien_tools_nc_connector_pair_migration_fallback($id, $config, $now);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Die WebDAV-Verbindung konnte nach dem Anlegen nicht serialisiert werden.');
}
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id." LIMIT 1");
}
catch (Throwable $e)
{
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
throw $e;
}
unset($_SESSION['bratonien_nc_wizard']);
if ($legacy_fallback_id !== null)
{
return array(
'connection_id'=>$id,
'legacy_fallback_connection_id'=>$legacy_fallback_id,
'message'=>'WebDAV-Nachfolger wurde angelegt. Die bestehende lokale Verbindung #'.$legacy_fallback_id.' bleibt als Fallback erhalten.',
);
}
return array(
'connection_id'=>$id,
'message'=>'WebDAV-Verbindung wurde angelegt.',

View File

@@ -28,7 +28,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
{
try
{
$response = bratonien_tools_nc_transport_http($candidate_url.'/status.php');
$response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if ($response['status'] < 200 || $response['status'] >= 300) continue;
$candidate_status = json_decode($response['body'], true);
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue;
@@ -44,7 +44,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
}
$user_response = bratonien_tools_nc_transport_http(
$user_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/user?format=json',
$username,
$password,
@@ -104,7 +104,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
'api_error'=>'',
));
bratonien_tools_nc_transport_refresh_directory_state($state, '');
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Nextcloud und WebDAV wurden bestätigt. Jetzt können die Verzeichnisse des angemeldeten Benutzers ausgewählt werden.');
@@ -126,6 +126,7 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
? array_values(array_unique(array_map(function($path) { return trim((string)$path, '/'); }, $state['directory_selected'])))
: array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected)
{
throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
@@ -142,12 +143,9 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
{
throw new RuntimeException('Für eine Auswahl fehlt die eindeutige Nextcloud-Datei-ID. Bitte das Verzeichnis erneut auswählen.');
}
$display_name = $path === ''
? (trim((string)($state['display_name'] ?? '')) !== '' ? trim((string)$state['display_name']) : (string)$state['username'])
: basename($path);
$roots[] = array(
'fileid'=>$fileid,
'display_name'=>$display_name,
'display_name'=>basename($path),
'webdav_path'=>$path,
);
}
@@ -158,35 +156,9 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$state['technical_source'] = 'WebDAV-Verzeichnisse ausgewählt';
$state['technical_error'] = '';
$state['directory_selection_ready'] = false;
if ((string)($state['editing_mode'] ?? '') === 'migrate')
{
$editing_id = (int)($state['editing_connection_id'] ?? 0);
$connection = $editing_id > 0 ? bratonien_tools_nc_connector_connection($editing_id, true) : null;
if (!$connection || (string)$connection['adapter'] !== 'local')
{
throw new RuntimeException('Die zu migrierende Legacy-Verbindung ist nicht mehr verfügbar.');
}
$migration = bratonien_tools_nc_connector_migration_state($connection);
if (empty($migration['ready']))
{
throw new RuntimeException('Die WebDAV-Migration kann noch nicht abgeschlossen werden. Es fehlen: '.implode(', ', $migration['missing']).'.');
}
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$state['_api_key_id'] = (string)($credentials['api_key_id'] ?? '');
$state['_api_key_secret'] = (string)($credentials['api_key_secret'] ?? '');
$state['api_status'] = !empty($migration['api_available']) ? 'ok' : 'skipped';
$state['api_error'] = '';
$state['step'] = 4;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'WebDAV-Quelle wurde übernommen. Mit „Migration starten“ wird jetzt der WebDAV-Nachfolger angelegt; die Legacy-Verbindung bleibt als Fallback erhalten.');
}
bratonien_tools_nc_wizard_store($state);
return array('message'=>'WebDAV-Verzeichnisse wurden übernommen. Die Verbindung ist für den nächsten Einrichtungsschritt vorbereitet.');
return array('message'=>'WebDAV-Verzeichnisse wurden übernommen. Die Verbindung ist für den parallelen Testweg vorbereitet.');
}
function bratonien_tools_nc_wizard_finish_dispatch()

View File

@@ -1,340 +0,0 @@
<?php
function bratonien_tools_nc_transport_host($url)
{
$host = trim((string)parse_url((string)$url, PHP_URL_HOST));
if ($host === '') throw new RuntimeException('Die Nextcloud-Adresse enthält keinen gültigen Hostnamen oder keine IP-Adresse.');
return trim($host, '[]');
}
function bratonien_tools_nc_transport_scheme($url)
{
$scheme = strtolower(trim((string)parse_url((string)$url, PHP_URL_SCHEME)));
if (!in_array($scheme, array('http','https'), true)) throw new RuntimeException('Nextcloud muss per HTTP oder HTTPS angesprochen werden.');
return $scheme;
}
function bratonien_tools_nc_transport_is_ip($host)
{
return filter_var(trim((string)$host, '[]'), FILTER_VALIDATE_IP) !== false;
}
function bratonien_tools_nc_transport_public_ip($host)
{
static $cache = array();
$host = strtolower(trim((string)$host, '[]'));
if ($host === '') throw new RuntimeException('Für die Nextcloud-Verbindung fehlt der Hostname.');
if (bratonien_tools_nc_transport_is_ip($host)) return $host;
if (isset($cache[$host])) return $cache[$host];
if (!function_exists('curl_init')) throw new RuntimeException('Der öffentliche DNS-Abgleich benötigt PHP-cURL.');
$providers = array(
array('host'=>'dns.google', 'ips'=>array('8.8.8.8','8.8.4.4'), 'url'=>'https://dns.google/resolve?name='.rawurlencode($host).'&type=A'),
array('host'=>'cloudflare-dns.com', 'ips'=>array('1.1.1.1','1.0.0.1'), 'url'=>'https://cloudflare-dns.com/dns-query?name='.rawurlencode($host).'&type=A'),
);
foreach ($providers as $provider)
{
foreach ($provider['ips'] as $resolver_ip)
{
$ch = curl_init($provider['url']);
$options = array(
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>false,
CURLOPT_CONNECTTIMEOUT=>5,
CURLOPT_TIMEOUT=>10,
CURLOPT_HTTPHEADER=>array('Accept: application/dns-json'),
CURLOPT_USERAGENT=>'Bratonien-Tools-DNS/0.9.7.1',
);
if (defined('CURLOPT_RESOLVE'))
{
$options[CURLOPT_RESOLVE] = array($provider['host'].':443:'.$resolver_ip);
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $errno !== 0 || $status < 200 || $status >= 300) continue;
$decoded = json_decode((string)$body, true);
if (!is_array($decoded) || !isset($decoded['Answer']) || !is_array($decoded['Answer'])) continue;
foreach ($decoded['Answer'] as $answer)
{
if ((int)($answer['type'] ?? 0) !== 1) continue;
$ip = trim((string)($answer['data'] ?? ''));
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) continue;
return $cache[$host] = $ip;
}
}
}
throw new RuntimeException('Für '.$host.' konnte keine öffentliche IPv4-Adresse ermittelt werden.');
}
function bratonien_tools_nc_transport_resolve_entry($url)
{
$scheme = bratonien_tools_nc_transport_scheme($url);
$host = bratonien_tools_nc_transport_host($url);
if (bratonien_tools_nc_transport_is_ip($host)) return null;
$port = (int)parse_url((string)$url, PHP_URL_PORT);
if ($port < 1) $port = $scheme === 'https' ? 443 : 80;
$ip = bratonien_tools_nc_transport_public_ip($host);
return $host.':'.$port.':'.$ip;
}
function bratonien_tools_nc_transport_apply_curl(array &$options, $url)
{
$entry = bratonien_tools_nc_transport_resolve_entry($url);
if ($entry !== null)
{
if (!defined('CURLOPT_RESOLVE')) throw new RuntimeException('Diese cURL-Version unterstützt keine direkte Host-zu-IP-Zuordnung.');
$options[CURLOPT_RESOLVE] = array($entry);
}
}
function bratonien_tools_nc_transport_http($url, $username = '', $password = '', array $headers = array())
{
if (!function_exists('curl_init')) throw new RuntimeException('Der Server kann Nextcloud derzeit nicht per HTTP prüfen.');
$ch = curl_init($url);
$options = array(
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>true,
CURLOPT_MAXREDIRS=>3,
CURLOPT_CONNECTTIMEOUT=>8,
CURLOPT_TIMEOUT=>15,
CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'), $headers),
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Wizard/0.9.7.1',
);
bratonien_tools_nc_transport_apply_curl($options, $url);
if ($username !== '')
{
$options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
$options[CURLOPT_USERPWD] = $username.':'.$password;
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $errno !== 0)
{
throw new RuntimeException('Verbindung fehlgeschlagen'.($error !== '' ? ': '.$error : '.'));
}
return array('status'=>$status, 'body'=>(string)$body);
}
function bratonien_tools_nc_transport_webdav_list(array $state, $path = '')
{
if (empty($state['scan_ok']) || trim((string)$state['base_url']) === '' || trim((string)$state['username']) === '' || (string)$state['_password'] === '')
{
throw new RuntimeException('Die Nextcloud-Sitzung des Assistenten ist nicht vollständig.');
}
if (!function_exists('curl_init')) throw new RuntimeException('cURL ist für die Verzeichnisauswahl nicht verfügbar.');
$path = trim((string)$path, '/');
if ($path !== '' && preg_match('#(^|/)\.\.(/|$)#', $path)) throw new RuntimeException('Ungültiger Verzeichnispfad.');
$segments = $path === '' ? array() : array_map('rawurlencode', explode('/', $path));
$user = rawurlencode((string)$state['username']);
$url = rtrim((string)$state['base_url'], '/').'/remote.php/dav/files/'.$user.'/'.implode('/', $segments);
if (substr($url, -1) !== '/') $url .= '/';
$body = '<?xml version="1.0"?><d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"><d:prop><d:resourcetype/><d:displayname/><oc:fileid/></d:prop></d:propfind>';
$ch = curl_init($url);
$options = array(
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_CUSTOMREQUEST=>'PROPFIND',
CURLOPT_POSTFIELDS=>$body,
CURLOPT_HTTPHEADER=>array('Depth: 1','Content-Type: application/xml; charset=utf-8'),
CURLOPT_HTTPAUTH=>CURLAUTH_BASIC,
CURLOPT_USERPWD=>(string)$state['username'].':'.(string)$state['_password'],
CURLOPT_CONNECTTIMEOUT=>8,
CURLOPT_TIMEOUT=>20,
);
bratonien_tools_nc_transport_apply_curl($options, $url);
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $errno !== 0) throw new RuntimeException('Nextcloud-Verzeichnisse konnten nicht geladen werden'.($error !== '' ? ': '.$error : '.'));
if ($status === 401 || $status === 403) throw new RuntimeException('Nextcloud hat den Zugriff auf dieses Verzeichnis abgelehnt.');
if ($status !== 207) throw new RuntimeException('Nextcloud-Verzeichnisabfrage antwortete mit HTTP '.$status.'.');
libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$response);
if ($xml === false) throw new RuntimeException('Nextcloud hat eine ungültige WebDAV-Antwort geliefert.');
$xml->registerXPathNamespace('d', 'DAV:');
$xml->registerXPathNamespace('oc', 'http://owncloud.org/ns');
$children = array();
$fileids = array();
$current_fileid = 0;
$base_path = (string)parse_url($url, PHP_URL_PATH);
foreach ($xml->xpath('//d:response') as $item)
{
$item->registerXPathNamespace('d', 'DAV:');
$item->registerXPathNamespace('oc', 'http://owncloud.org/ns');
$hrefs = $item->xpath('d:href');
$collections = $item->xpath('d:propstat/d:prop/d:resourcetype/d:collection');
$ids = $item->xpath('d:propstat/d:prop/oc:fileid');
if (!$hrefs || !$collections || !$ids) continue;
$fileid = (int)trim((string)$ids[0]);
if ($fileid < 1) continue;
$href = rawurldecode((string)$hrefs[0]);
$href_path = (string)parse_url($href, PHP_URL_PATH);
if (rtrim($href_path, '/') === rtrim($base_path, '/'))
{
$current_fileid = $fileid;
continue;
}
$name = basename(rtrim($href_path, '/'));
if ($name === '') continue;
$child_path = $path === '' ? $name : $path.'/'.$name;
$children[$child_path] = $name;
$fileids[$child_path] = $fileid;
}
natcasesort($children);
if ($current_fileid < 1) throw new RuntimeException('Nextcloud hat für das aktuelle Verzeichnis keine eindeutige Datei-ID geliefert.');
$parent = '';
if ($path !== '')
{
$parts = explode('/', $path);
array_pop($parts);
$parent = implode('/', $parts);
}
return array(
'current'=>$path,
'parent'=>$parent,
'children'=>$children,
'current_fileid'=>$current_fileid,
'fileids'=>$fileids,
);
}
function bratonien_tools_nc_transport_refresh_directory_state(array &$state, $path = null)
{
if ($path === null) $path = (string)($state['directory_path'] ?? '');
$listing = bratonien_tools_nc_transport_webdav_list($state, $path);
$state['directory_path'] = (string)$listing['current'];
$state['directory_parent'] = (string)$listing['parent'];
$state['directory_children'] = (array)$listing['children'];
$state['directory_current_fileid'] = (int)$listing['current_fileid'];
$state['directory_fileids'] = (array)$listing['fileids'];
if (!isset($state['directory_selected']) || !is_array($state['directory_selected'])) $state['directory_selected'] = array();
if (!isset($state['directory_selected_fileids']) || !is_array($state['directory_selected_fileids'])) $state['directory_selected_fileids'] = array();
}
function bratonien_tools_nc_transport_wizard_directory_browse()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts' || empty($state['directory_selection_ready'])) throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
$path = trim((string)($_POST['nc_wizard_directory_path'] ?? ''), '/');
bratonien_tools_nc_transport_refresh_directory_state($state, $path);
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Verzeichnis geöffnet.');
}
function bratonien_tools_nc_transport_edit_start()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$is_webdav = (string)$connection['adapter'] === 'remote'
&& (string)($config['source_mode'] ?? '') === 'webdav-placeholder';
if (!$is_webdav) return bratonien_tools_nc_connector_edit_start();
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$base_url = trim((string)($config['nextcloud_url'] ?? ''));
$username = trim((string)($credentials['nextcloud_user'] ?? ''));
if ($username === '') $username = trim((string)($config['nextcloud_access_user'] ?? $config['access_user'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
$roots = isset($config['roots']) && is_array($config['roots']) ? array_values($config['roots']) : array();
$selected = array();
$selected_ids = array();
foreach ($roots as $root)
{
$path = trim((string)($root['webdav_path'] ?? ''), '/');
$fileid = (int)($root['fileid'] ?? 0);
if ($fileid < 1) continue;
$selected[] = $path;
$selected_ids[$path] = $fileid;
}
$state = bratonien_tools_nc_wizard_state();
$state = array_merge($state, array(
'editing_connection_id'=>$id,
'editing_adapter'=>(string)$connection['adapter'],
'editing_mode'=>'update',
'connection_name'=>(string)$connection['name'],
'host_input'=>$base_url,
'base_url'=>$base_url,
'username'=>$username,
'_password'=>$password,
'_fallback_user'=>(string)($credentials['piwigo_user'] ?? ''),
'_fallback_password'=>(string)($credentials['piwigo_password'] ?? ''),
'_api_key_id'=>(string)($credentials['api_key_id'] ?? ''),
'_api_key_secret'=>(string)($credentials['api_key_secret'] ?? ''),
'api_status'=>trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '' ? 'ok' : 'pending',
'roots'=>$roots,
'directory_selected'=>$selected,
'directory_selected_fileids'=>$selected_ids,
'source_mode'=>'webdav-placeholder',
'transport'=>'webdav',
));
if ($base_url !== '' && $username !== '' && $password !== '')
{
$state['step'] = 2;
$state['scan_ok'] = true;
$state['technical_stage'] = 'mounts';
$state['technical_source'] = 'WebDAV';
$state['technical_error'] = '';
$state['technical_complete'] = false;
$state['directory_selection_ready'] = true;
$state['directory_path'] = '';
$state['directory_parent'] = '';
$state['directory_children'] = array();
$state['directory_current_fileid'] = 0;
try
{
bratonien_tools_nc_transport_refresh_directory_state($state, '');
}
catch (Throwable $e)
{
$state['step'] = 1;
$state['scan_ok'] = false;
$state['technical_error'] = $e->getMessage();
}
}
else
{
$state['step'] = 1;
$state['scan_ok'] = false;
}
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Verbindung #'.$id.' wurde zum Bearbeiten geöffnet.');
}

View File

@@ -6,13 +6,19 @@ if (!defined('PHPWG_ROOT_PATH'))
if (isset($GLOBALS['template']) && is_object($GLOBALS['template']) && method_exists($GLOBALS['template'], 'func_combine_script'))
{
$script_version = function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : '0.9.7.1';
$script_version = function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : '0.9.6.15';
$GLOBALS['template']->func_combine_script(array(
'id'=>'bratonien_nc_connector_edit_v2',
'path'=>BRATONIEN_TOOLS_PATH.'js/nc_connector_edit_v2.js',
'load'=>'footer',
'version'=>$script_version,
));
$GLOBALS['template']->func_combine_script(array(
'id'=>'bratonien_nc_connector_source_ui',
'path'=>BRATONIEN_TOOLS_PATH.'js/nc_connector_source_ui.js',
'load'=>'footer',
'version'=>$script_version,
));
}
require_once(BRATONIEN_TOOLS_PATH . 'tools/image_cache.inc.php');
@@ -28,6 +34,7 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_manage.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_takeover.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_auth.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php');
@@ -41,24 +48,6 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_edit.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_transport.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_scheduler.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_system.inc.php');
function bratonien_tools_nc_connector_run_now()
{
$connection_id = isset($_POST['connection_id']) ? max(0, (int)$_POST['connection_id']) : 0;
if ($connection_id > 0)
{
$connection = bratonien_tools_nc_connector_connection($connection_id, false);
if (!$connection)
{
throw new RuntimeException('Die ausgewählte Verbindung existiert nicht.');
}
}
$result = bratonien_tools_nc_scheduler_spawn(true, $connection_id);
return array('message'=>(string)($result['message'] ?? ($connection_id > 0 ? 'Abgleich wurde gestartet.' : 'Abgleich für alle Verbindungen wurde gestartet.')));
}
function bratonien_tools_get_tools()
{
@@ -83,16 +72,17 @@ function bratonien_tools_get_tools()
'album_share_create' => array('handler' => 'bratonien_tools_create_album_share'),
'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'),
'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'),
'nc_connector_create_local' => array('handler' => 'bratonien_tools_nc_connector_create_local_api_first'),
'nc_connector_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'),
'nc_connector_edit_start' => array('handler' => 'bratonien_tools_nc_transport_edit_start'),
'nc_connector_edit_start' => array('handler' => 'bratonien_tools_nc_connector_edit_start'),
'nc_connector_migrate_start' => array('handler' => 'bratonien_tools_nc_connector_migrate_start'),
'nc_connector_update_local' => array('handler' => 'bratonien_tools_nc_connector_update_local_friendly'),
'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'),
'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'),
'nc_connector_run_now' => array('handler' => 'bratonien_tools_nc_connector_run_now'),
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'),
'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'),
'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_transport_wizard_directory_browse'),
'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'),
'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),
'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_sources_dispatch'),
@@ -102,7 +92,10 @@ function bratonien_tools_get_tools()
'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_dispatch'),
'nc_connector_wizard_back' => array('handler' => 'bratonien_tools_nc_wizard_back'),
'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'),
'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'),
'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_connection_scoped'),
'nc_connector_prepare_takeover' => array('handler' => 'bratonien_tools_nc_connector_prepare_takeover'),
'nc_connector_cancel_takeover' => array('handler' => 'bratonien_tools_nc_connector_cancel_takeover'),
'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'),
'nc_connector_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'),
'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'),

View File

@@ -8,17 +8,8 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_base.inc.php');
function bratonien_tools_watermark_engine_enabled()
{
static $initialized = false;
static $enabled = false;
if ($initialized)
{
return $enabled;
}
$config = bratonien_tools_get_watermark_engine_config();
$enabled = !empty($config['enabled']);
$initialized = true;
// Piwigo recalculates use_watermark for custom derivatives from the native
// watermark file itself. Therefore an active Bratonien engine must keep the

View File

@@ -1,6 +1,94 @@
(function () {
'use strict';
function initNcRouteStatus() {
var section = document.getElementById('nc-connector');
if (!section) return;
var statusCard = section.querySelector('.bratonien-grid .bratonien-card');
if (!statusCard) return;
var grid = statusCard.querySelector('.bratonien-form-grid');
if (!grid) return;
var routeLabel = document.createElement('span');
routeLabel.className = 'bratonien-label';
routeLabel.textContent = 'Aktiver Datenweg';
var routeValue = document.createElement('strong');
routeValue.setAttribute('data-nc-route-status', '');
routeValue.textContent = 'Noch kein Lauf erfasst';
var routeTimeLabel = document.createElement('span');
routeTimeLabel.className = 'bratonien-label';
routeTimeLabel.textContent = 'Datenweg zuletzt';
var routeTimeValue = document.createElement('strong');
routeTimeValue.setAttribute('data-nc-route-time', '');
routeTimeValue.textContent = 'Nicht verfügbar';
grid.appendChild(routeLabel);
grid.appendChild(routeValue);
grid.appendChild(routeTimeLabel);
grid.appendChild(routeTimeValue);
var detail = document.createElement('p');
detail.className = 'bratonien-base-note';
detail.setAttribute('data-nc-route-detail', '');
detail.textContent = 'Der Datenweg wird nach dem nächsten Connector-Lauf angezeigt.';
statusCard.appendChild(detail);
var basePath = window.location.pathname.replace(/admin\.php.*$/, '');
var statusUrl = basePath + '_data/bratonien-tools/nc-connector-status/route-status.json';
function formatTime(timestamp) {
if (!timestamp) return 'Nicht verfügbar';
var date = new Date(Number(timestamp) * 1000);
if (Number.isNaN(date.getTime())) return 'Nicht verfügbar';
return date.toLocaleString('de-DE');
}
function render(data) {
var label = data && data.label ? String(data.label) : 'Unbekannt';
var route = data && data.route ? String(data.route) : '';
routeValue.textContent = label;
routeTimeValue.textContent = formatTime(data && data.timestamp);
if (route === 'webdav') {
routeValue.textContent = 'WEBDAV PRIMÄR';
detail.textContent = 'Portierung läuft über WebDAV. Legacy wurde in diesem Lauf nicht benutzt.';
} else if (route === 'legacy_fallback') {
routeValue.textContent = 'LEGACY-FALLBACK AKTIV';
detail.textContent = 'WebDAV war nicht erfolgreich. Dieser Lauf wurde über die alte Struktur abgefangen.';
} else if (route === 'failed') {
routeValue.textContent = 'FEHLER - KEIN ERFOLGREICHER DATENWEG';
detail.textContent = data && data.detail ? String(data.detail) : 'WebDAV und Fallback sind fehlgeschlagen.';
} else {
detail.textContent = data && data.detail ? String(data.detail) : 'Unbekannter Datenweg.';
}
}
function refresh() {
fetch(statusUrl + '?t=' + Date.now(), {
cache: 'no-store',
credentials: 'same-origin'
})
.then(function (response) {
if (!response.ok) throw new Error('status unavailable');
return response.json();
})
.then(render)
.catch(function () {
routeValue.textContent = 'Noch kein Lauf erfasst';
routeTimeValue.textContent = 'Nicht verfügbar';
detail.textContent = 'Nach dem nächsten Connector-Lauf wird hier WebDAV oder Legacy-Fallback angezeigt.';
});
}
refresh();
window.setInterval(refresh, 10000);
}
function initBratonienTabs() {
var admin = document.querySelector('.bratonien-admin');
if (!admin) return;
@@ -113,9 +201,14 @@
activate(initial, false);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBratonienTabs);
} else {
function init() {
initBratonienTabs();
initNcRouteStatus();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -1,193 +1,324 @@
(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 ready(callback) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { window.setTimeout(callback, 0); });
} else {
window.setTimeout(callback, 0);
}
}
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() {
ready(function () {
var section = document.getElementById('nc-connector');
if (!section) return;
var connectionCard = null;
var headings = section.querySelectorAll('h4');
for (var i = 0; i < headings.length; i++) {
if ((headings[i].textContent || '').trim() === 'Bestehende Verbindungen') {
connectionCard = headings[i].closest('.bratonien-card');
break;
var pwgTokenInput = section.querySelector('input[name="pwg_token"]');
var pwgToken = pwgTokenInput ? pwgTokenInput.value : '';
var modeKey = 'bratonienNcWizardMode';
function escapeHtml(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot',"'":'&#039;'}[c];
});
}
function setMode(value) {
try { sessionStorage.setItem(modeKey, value); } catch (e) {}
}
function postForm(label, tool, id, mode) {
var form = document.createElement('form');
form.method = 'post';
form.style.display = 'inline';
form.dataset.ncV2Action = tool;
form.innerHTML = '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<input type="hidden" name="connection_id" value="'+escapeHtml(id)+'">'
+ '<button class="buttonLike" type="submit" name="bratonien_tool" value="'+escapeHtml(tool)+'">'+escapeHtml(label)+'</button>';
if (mode) form.addEventListener('submit', function () { setMode(mode); }, true);
return form;
}
function loadConnection(id) {
return fetch('plugins/bratonien_tools/nc-connector-edit-data.php?connection_id='+encodeURIComponent(id)+'&_='+Date.now(), {
credentials:'same-origin',
cache:'no-store',
headers:{'Accept':'application/json'}
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok) throw new Error(data.error || ('HTTP '+response.status));
return data;
});
});
}
function dialog() {
var node = document.getElementById('bratonien-nc-connection-edit-v2');
if (node) return node;
node = document.createElement('dialog');
node.id = 'bratonien-nc-connection-edit-v2';
node.className = 'bratonien-edit-dialog';
node.innerHTML = '<div class="bratonien-edit-dialog__body">'
+ '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem">'
+ '<div><h4 style="margin:0">Verbindung bearbeiten</h4><p class="bratonien-base-note" style="margin:.35rem 0 0">Alle Daten dieser Verbindung werden hier gepflegt. Eine Migration ist ein eigener Vorgang.</p></div>'
+ '<button type="button" class="buttonLike" data-edit-v2-close>Schließen</button></div>'
+ '<div data-edit-v2-content style="margin-top:1rem"></div></div>';
document.body.appendChild(node);
node.querySelector('[data-edit-v2-close]').addEventListener('click', function () { node.close(); });
node.addEventListener('click', function (event) { if (event.target === node) node.close(); });
return node;
}
function storageRow(storage) {
storage = storage || {};
return '<div class="bratonien-storage-row" data-storage-row>'
+ '<label>Storage-ID<input name="nc_storage_id[]" value="'+escapeHtml(storage.storage_id || '')+'" required></label>'
+ '<label>Quellordner<input name="nc_source_prefix[]" value="'+escapeHtml(storage.source_prefix || '')+'" placeholder="optional"></label>'
+ '<label>Lokaler Speicherpfad<input name="nc_local_mount[]" value="'+escapeHtml(storage.local_mount || '')+'" required></label>'
+ '<button type="button" class="buttonLike" data-remove-storage>Entfernen</button></div>';
}
function clearValidation(form) {
[].slice.call(form.querySelectorAll('[aria-invalid="true"]')).forEach(function (field) {
field.removeAttribute('aria-invalid');
field.style.borderColor = '';
field.style.boxShadow = '';
});
[].slice.call(form.querySelectorAll('[data-edit-v2-field-error]')).forEach(function (node) { node.remove(); });
var box = form.querySelector('[data-edit-v2-error-summary]');
if (box) box.remove();
}
function fieldNodes(form, fieldName) {
return [].slice.call(form.querySelectorAll('[name="'+fieldName.replace(/"/g, '\\"')+'"]'));
}
function showValidation(form, data) {
clearValidation(form);
var message = (data && data.message) ? data.message : 'Die Verbindung konnte nicht gespeichert werden.';
var summary = document.createElement('div');
summary.dataset.editV2ErrorSummary = '1';
summary.className = 'bratonien-main-cache__warning';
summary.style.margin = '0 0 1rem';
summary.style.padding = '.75rem';
summary.style.border = '1px solid currentColor';
summary.innerHTML = '<strong>Speichern fehlgeschlagen:</strong> '+escapeHtml(message);
form.insertBefore(summary, form.firstChild);
var first = null;
var fields = data && Array.isArray(data.fields) ? data.fields : [];
fields.forEach(function (fieldName) {
fieldNodes(form, fieldName).forEach(function (field) {
field.setAttribute('aria-invalid', 'true');
field.style.borderColor = '#d65a5a';
field.style.boxShadow = '0 0 0 1px #d65a5a';
if (!first) first = field;
var note = document.createElement('span');
note.dataset.editV2FieldError = '1';
note.className = 'bratonien-main-cache__warning';
note.style.display = 'block';
note.style.marginTop = '.25rem';
note.textContent = message;
field.insertAdjacentElement('afterend', note);
});
});
if (first) {
first.scrollIntoView({block:'center', behavior:'smooth'});
window.setTimeout(function () { first.focus(); }, 150);
} else {
summary.scrollIntoView({block:'center', behavior:'smooth'});
}
}
if (!connectionCard) return;
var details = connectionCard.querySelectorAll(':scope > details');
details.forEach(function (detail) {
var actions = detail.querySelector('.bratonien-actions');
function submitEditor(form, editDialog) {
clearValidation(form);
if (!form.reportValidity()) return;
var submit = form.querySelector('button[type="submit"]');
if (submit) submit.disabled = true;
var body = new FormData(form);
fetch('plugins/bratonien_tools/nc-connector-edit-save.php', {
method:'POST',
credentials:'same-origin',
cache:'no-store',
headers:{'Accept':'application/json'},
body:body
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok || !data.ok) {
showValidation(form, data);
return;
}
editDialog.close();
window.location.reload();
});
}).catch(function (error) {
showValidation(form, {message:error.message || String(error), fields:[]});
}).finally(function () {
if (submit) submit.disabled = false;
});
}
function openEditor(id) {
var editDialog = dialog();
var content = editDialog.querySelector('[data-edit-v2-content]');
content.innerHTML = '<p class="bratonien-base-note">Verbindung wird geladen …</p>';
if (typeof editDialog.showModal === 'function') editDialog.showModal();
else editDialog.setAttribute('open', 'open');
loadConnection(id).then(function (data) {
if (data.adapter !== 'local') throw new Error('Diese Ansicht ist nur für die bestehende Legacy-Verbindung vorgesehen.');
var legacy = data.legacy || {};
var webdav = data.webdav || {};
var storages = Array.isArray(legacy.storages) ? legacy.storages : [];
var rows = storages.map(storageRow).join('') || storageRow({});
var migrationText = webdav.migration_ready
? '<strong>Bereit.</strong> Nextcloud-Zugang und verbindungseigener Piwigo-API-Key sind vollständig gespeichert.'
: '<strong>Noch nicht bereit.</strong> Es fehlen: '+escapeHtml((webdav.migration_missing || []).join(', ') || 'WebDAV-Zugangsdaten')+'.';
content.innerHTML = '<form method="post" data-edit-v2-form>'
+ '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<input type="hidden" name="connection_id" value="'+escapeHtml(data.id)+'">'
+ '<h5>Verbindung</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Name</label><input name="connection_name" value="'+escapeHtml(data.name)+'" required>'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Nextcloud / WebDAV</h5>'
+ '<p class="bratonien-base-note">Diese Angaben werden für den neuen WebDAV-Weg benötigt und gehören zu genau dieser Verbindung.</p>'
+ '<div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Nextcloud-Adresse</label><input name="nc_nextcloud_url" value="'+escapeHtml(webdav.nextcloud_url || '')+'" placeholder="https://cloud.example.de">'
+ '<label class="bratonien-label">Nextcloud-Benutzer</label><input name="nc_nextcloud_user" value="'+escapeHtml(webdav.nextcloud_user || '')+'" autocomplete="username">'
+ '<label class="bratonien-label">Nextcloud-Passwort</label><input name="nc_nextcloud_password" type="password" autocomplete="current-password" placeholder="'+(webdav.has_nextcloud_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Piwigo API dieser Verbindung</h5>'
+ '<p class="bratonien-base-note">Der API-Key wird verbindungseigen gespeichert und beim Speichern geprüft.</p>'
+ '<div class="bratonien-form-grid">'
+ '<label class="bratonien-label">API-Schlüssel-ID</label><input name="nc_connection_api_key_id" value="'+escapeHtml(webdav.api_key_id || '')+'" autocomplete="off">'
+ '<label class="bratonien-label">API-Geheimnis</label><input name="nc_connection_api_key_secret" type="password" autocomplete="new-password" placeholder="'+(webdav.has_api_key_secret ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<p class="bratonien-base-note"><strong>WebDAV-Migration:</strong> '+migrationText+'</p>'
+ '<h5 style="margin-top:1.2rem">Bestehender Legacy-Weg</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Datenbank-Server</label><input name="nc_host" value="'+escapeHtml(legacy.host)+'" required>'
+ '<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="'+escapeHtml(legacy.port || 5432)+'" required>'
+ '<label class="bratonien-label">Datenbank</label><input name="nc_database" value="'+escapeHtml(legacy.database)+'" required>'
+ '<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" value="'+escapeHtml(legacy.user)+'" required>'
+ '<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" placeholder="'+(legacy.has_db_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Speicherorte</h5><div data-storage-list>'+rows+'</div>'
+ '<button type="button" class="buttonLike" data-add-storage>Speicherort hinzufügen</button>'
+ '<details style="margin-top:1rem"><summary>Erweiterte Legacy-Einstellungen</summary><div class="bratonien-form-grid" style="margin-top:.75rem">'
+ '<label class="bratonien-label">Source-View</label><input name="nc_source_view" value="'+escapeHtml(legacy.source_view)+'" required>'
+ '<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" value="'+escapeHtml(legacy.activity_view)+'" required>'
+ '<label class="bratonien-label">Piwigo-Galerieordner</label><input name="nc_gallery_root" value="'+escapeHtml(legacy.gallery_root)+'" required>'
+ '<label class="bratonien-label">Ruhezeit (Sek.)</label><input name="nc_quiet_seconds" type="number" min="0" value="'+escapeHtml(legacy.quiet_seconds)+'">'
+ '<label class="bratonien-label">Maximale Wartezeit (Sek.)</label><input name="nc_max_wait_seconds" type="number" min="60" value="'+escapeHtml(legacy.max_wait_seconds)+'">'
+ '<label class="bratonien-label">Vollprüfung nach (Sek.)</label><input name="nc_full_sync_seconds" type="number" min="300" value="'+escapeHtml(legacy.full_sync_seconds)+'">'
+ '</div></details>'
+ '<div class="bratonien-actions" style="margin-top:1rem"><button class="buttonLike" type="submit">Änderungen prüfen und speichern</button><button class="buttonLike" type="button" data-edit-v2-cancel>Abbrechen</button></div>'
+ '</form>';
var form = content.querySelector('[data-edit-v2-form]');
form.querySelector('[data-edit-v2-cancel]').addEventListener('click', function () { editDialog.close(); });
form.querySelector('[data-add-storage]').addEventListener('click', function () {
form.querySelector('[data-storage-list]').insertAdjacentHTML('beforeend', storageRow({}));
});
form.addEventListener('click', function (event) {
var remove = event.target.closest('[data-remove-storage]');
if (!remove) return;
var row = remove.closest('[data-storage-row]');
if (row) row.remove();
});
form.addEventListener('submit', function (event) {
event.preventDefault();
event.stopPropagation();
submitEditor(form, editDialog);
});
}).catch(function (error) {
content.innerHTML = '<p class="bratonien-main-cache__warning"><strong>Bearbeiten nicht möglich:</strong> '+escapeHtml(error.message || String(error))+'</p>';
});
}
function rebuildLocalActions(deleteForm, id, connectionData) {
var actions = deleteForm.parentElement;
if (!actions) return;
var connectionInput = detail.querySelector('input[name="connection_id"]');
var tokenInput = detail.querySelector('input[name="pwg_token"]');
if (!connectionInput || !tokenInput) return;
[].slice.call(actions.querySelectorAll('form')).forEach(function (form) {
var migrate = form.querySelector('button[value="nc_connector_migrate_start"]');
var editStart = form.querySelector('button[value="nc_connector_edit_start"]');
if (migrate || editStart) form.remove();
});
[].slice.call(actions.children).forEach(function (child) {
if (child.tagName === 'BUTTON' && (child.textContent || '').trim() === 'Bearbeiten') child.remove();
if (child.dataset && child.dataset.ncMigrationInfo) child.remove();
});
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 edit = document.createElement('button');
edit.type = 'button';
edit.className = 'buttonLike';
edit.textContent = 'Bearbeiten';
edit.addEventListener('click', function () { openEditor(id); });
actions.insertBefore(edit, deleteForm);
var token = document.createElement('input');
token.type = 'hidden';
token.name = 'pwg_token';
token.value = tokenInput.value;
form.appendChild(token);
var info = document.createElement('span');
info.dataset.ncMigrationInfo = '1';
info.className = 'bratonien-base-note';
info.textContent = 'WebDAV-Migration wird geprüft …';
actions.insertBefore(info, deleteForm);
var connection = document.createElement('input');
connection.type = 'hidden';
connection.name = 'connection_id';
connection.value = connectionInput.value;
form.appendChild(connection);
var dataPromise = connectionData ? Promise.resolve(connectionData) : loadConnection(id);
dataPromise.then(function (data) {
var webdav = data.webdav || {};
if (webdav.migration_ready) {
info.remove();
actions.insertBefore(postForm('Auf WebDAV migrieren', 'nc_connector_migrate_start', id, 'migrate'), deleteForm);
} else {
var missing = (webdav.migration_missing || []).join(', ');
info.textContent = 'WebDAV-Migration noch nicht bereit: '+(missing || 'Zugangsdaten fehlen')+'. Zuerst „Bearbeiten“.';
}
}).catch(function (error) {
info.textContent = 'WebDAV-Migration kann nicht geprüft werden: '+(error.message || String(error));
});
}
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);
function rebuildRemoteActions(deleteForm, id) {
var actions = deleteForm.parentElement;
if (!actions) return;
[].slice.call(actions.querySelectorAll('form')).forEach(function (form) {
var oldEdit = form.querySelector('button[value="nc_connector_edit_start"]');
var oldMigrate = form.querySelector('button[value="nc_connector_migrate_start"]');
if (oldEdit || oldMigrate) form.remove();
});
[].slice.call(actions.children).forEach(function (child) {
if (child.tagName === 'BUTTON' && (child.textContent || '').trim() === 'Bearbeiten') child.remove();
if (child.dataset && child.dataset.ncMigrationInfo) child.remove();
});
actions.insertBefore(postForm('Bearbeiten', 'nc_connector_edit_start', id, 'edit'), deleteForm);
}
actions.insertBefore(form, actions.firstChild);
}
bindRunNow(form, detail, ensureLiveStatus(detail, actions));
[].slice.call(section.querySelectorAll('button[value="nc_connector_edit_start"]')).forEach(function (button) {
var form = button.closest('form');
if (form) form.remove();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', restoreRunNowButtons);
} else {
restoreRunNowButtons();
}
[].slice.call(section.querySelectorAll('button[value="nc_connector_delete"]')).forEach(function (deleteButton) {
var deleteForm = deleteButton.closest('form');
if (!deleteForm) return;
var idInput = deleteForm.querySelector('input[name="connection_id"]');
if (!idInput) return;
var id = idInput.value;
loadConnection(id).then(function (data) {
if (data.adapter === 'local') rebuildLocalActions(deleteForm, id, data);
else rebuildRemoteActions(deleteForm, id);
}).catch(function (error) {
var actions = deleteForm.parentElement;
if (!actions) return;
var info = document.createElement('span');
info.className = 'bratonien-main-cache__warning';
info.textContent = 'Verbindungstyp konnte nicht geladen werden: '+(error.message || String(error));
actions.insertBefore(info, deleteForm);
});
});
});
})();

View File

@@ -0,0 +1,90 @@
(function () {
'use strict';
function enhanceMigrationButtons(root) {
[].slice.call(root.querySelectorAll('button[value="nc_connector_migrate_start"]')).forEach(function (button) {
button.textContent = 'Nextcloud-Ordner auswählen & migrieren';
button.title = 'Die WebDAV-Quelle wird im Nextcloud-Ordnerbrowser ausgewählt. Der bestehende SMB-/Legacy-Speicher wird nicht als WebDAV-Quelle übernommen.';
});
}
function enhanceEditor(root) {
var form = root.querySelector('[data-edit-v2-form]');
if (!form || form.dataset.sourceUiEnhanced === '1') return;
form.dataset.sourceUiEnhanced = '1';
var headings = [].slice.call(form.querySelectorAll('h5'));
var legacyHeading = headings.find(function (node) {
return (node.textContent || '').trim() === 'Bestehender Legacy-Weg';
});
var storageHeading = headings.find(function (node) {
return (node.textContent || '').trim() === 'Speicherorte';
});
var storageList = form.querySelector('[data-storage-list]');
var addStorage = form.querySelector('[data-add-storage]');
if (legacyHeading && !form.querySelector('[data-webdav-source-note]')) {
var note = document.createElement('p');
note.dataset.webdavSourceNote = '1';
note.className = 'bratonien-main-cache__warning';
note.innerHTML = '<strong>Wichtig:</strong> Der folgende SMB-/lokale Speicher gehört ausschließlich zum bisherigen Legacy-Fallback. Er wird <strong>nicht</strong> als WebDAV-Quelle übernommen. Die WebDAV-Quellordner werden beim Start der Migration direkt aus Nextcloud angezeigt und ausgewählt.';
legacyHeading.insertAdjacentElement('beforebegin', note);
}
if (storageHeading && storageList && addStorage) {
storageHeading.textContent = 'Legacy-Speicher (nur Fallback)';
var details = document.createElement('details');
details.dataset.legacyStorageTechnical = '1';
details.style.marginTop = '.75rem';
var summary = document.createElement('summary');
summary.textContent = 'Technische Legacy-Speicherzuordnung bearbeiten';
details.appendChild(summary);
var info = document.createElement('p');
info.className = 'bratonien-base-note';
info.textContent = 'Diese Felder betreffen nur den alten Fallback-Weg. Für WebDAV werden keine SMB-Adressen oder lokalen Mount-Pfade eingegeben.';
details.appendChild(info);
storageHeading.insertAdjacentElement('beforebegin', details);
details.appendChild(storageHeading);
details.appendChild(storageList);
details.appendChild(addStorage);
addStorage.textContent = 'Legacy-Speicher manuell hinzufügen';
addStorage.title = 'Nur für den alten Legacy-Fallback. WebDAV-Ordner werden im Nextcloud-Ordnerbrowser ausgewählt.';
}
}
function enhance(root) {
if (!root || root.nodeType !== 1) return;
enhanceMigrationButtons(root);
if (root.matches && root.matches('#bratonien-nc-connection-edit-v2')) enhanceEditor(root);
var dialog = root.querySelector ? root.querySelector('#bratonien-nc-connection-edit-v2') : null;
if (dialog) enhanceEditor(dialog);
}
function start() {
var section = document.getElementById('nc-connector');
if (!section) return;
enhance(section);
enhanceMigrationButtons(document);
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
[].slice.call(mutation.addedNodes || []).forEach(function (node) {
if (node && node.nodeType === 1) enhance(node);
});
});
var dialog = document.getElementById('bratonien-nc-connection-edit-v2');
if (dialog) enhanceEditor(dialog);
enhanceMigrationButtons(document);
});
observer.observe(document.body, {childList:true, subtree:true});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
else start();
})();

View File

@@ -1,7 +1,7 @@
<?php
/*
Plugin Name: Bratonien Tools
Version: 0.9.7.24
Version: 0.9.6.15
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien
@@ -24,7 +24,6 @@ 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);

View File

@@ -8,7 +8,6 @@ require_once(dirname(__FILE__) . '/include/album_shares.inc.php');
require_once(dirname(__FILE__) . '/include/database.class.php');
require_once(dirname(__FILE__) . '/tools/watermark_profiles.inc.php');
require_once(dirname(__FILE__) . '/include/dependencies.inc.php');
require_once(dirname(__FILE__) . '/include/nc_connector_scheduler.inc.php');
class bratonien_tools_maintain extends PluginMaintain
{
@@ -19,14 +18,9 @@ class bratonien_tools_maintain extends PluginMaintain
$dependency_messages = array();
bratonien_tools_ensure_dependencies($dependency_messages);
if (!bratonien_tools_nc_scheduler_install())
{
$dependency_messages[] = 'Der native NC-Scheduler konnte sein Piwigo-Laufzeitverzeichnis nicht anlegen.';
}
if (function_exists('conf_update_param'))
{
conf_update_param('bratonien_nc_scheduler_interval', 60);
conf_update_param('bratonien_dependency_status', json_encode(array(
'checked_at' => time(),
'messages' => $dependency_messages,

100
nc-connector-edit-data.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
http_response_code(404);
echo json_encode(array('error'=>'Bratonien Tools ist nicht aktiv.'));
exit;
}
if (!function_exists('is_admin') || !is_admin())
{
http_response_code(403);
echo json_encode(array('error'=>'Administratorrechte erforderlich.'));
exit;
}
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector_manage.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector_connection_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector_edit.inc.php');
$id = (int)($_GET['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
http_response_code(404);
echo json_encode(array('error'=>'Connector-Verbindung wurde nicht gefunden.'));
exit;
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = array();
$migration = array('ready'=>false, 'missing'=>array());
try
{
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
if ((string)$connection['adapter'] === 'local')
{
$migration = bratonien_tools_nc_connector_migration_state($connection);
}
}
catch (Throwable $e)
{
$credentials = array();
$migration = array('ready'=>false, 'missing'=>array('gespeicherte Zugangsdaten konnten nicht gelesen werden'));
}
$storages = array();
foreach ((array)($config['storages'] ?? array()) as $storage)
{
$storages[] = array(
'storage_id'=>(string)($storage['storage_id'] ?? ''),
'source_prefix'=>(string)($storage['source_prefix'] ?? ''),
'local_mount'=>(string)($storage['local_mount'] ?? ''),
);
}
$payload = array(
'id'=>(int)$connection['id'],
'name'=>(string)$connection['name'],
'adapter'=>(string)$connection['adapter'],
'enabled'=>(bool)$connection['enabled'],
'takeover_state'=>(string)$connection['takeover_state'],
'source_mode'=>(string)($config['source_mode'] ?? ''),
'legacy'=>array(
'host'=>(string)($config['host'] ?? ''),
'port'=>(string)($config['port'] ?? '5432'),
'database'=>(string)($config['database'] ?? ''),
'user'=>(string)($config['user'] ?? ''),
'has_db_password'=>trim((string)($credentials['db_password'] ?? '')) !== '',
'source_view'=>(string)($config['source_view'] ?? ''),
'activity_view'=>(string)($config['activity_view'] ?? ''),
'gallery_root'=>(string)($config['gallery_root'] ?? ''),
'quiet_seconds'=>(int)($config['quiet_seconds'] ?? 120),
'max_wait_seconds'=>(int)($config['max_wait_seconds'] ?? 900),
'full_sync_seconds'=>(int)($config['full_sync_seconds'] ?? 86400),
'storages'=>$storages,
),
'webdav'=>array(
'nextcloud_url'=>(string)($config['nextcloud_url'] ?? ''),
'nextcloud_user'=>(string)($credentials['nextcloud_user'] ?? $config['nextcloud_access_user'] ?? $config['access_user'] ?? ''),
'has_nextcloud_password'=>(string)($credentials['nextcloud_password'] ?? '') !== '',
'api_key_id'=>(string)($credentials['api_key_id'] ?? ''),
'has_api_key_secret'=>trim((string)($credentials['api_key_secret'] ?? '')) !== '',
'migration_ready'=>!empty($migration['ready']),
'migration_missing'=>array_values((array)($migration['missing'] ?? array())),
),
);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

312
nc-connector-edit-save.php Normal file
View File

@@ -0,0 +1,312 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
function bratonien_tools_nc_edit_json($status, array $payload)
{
http_response_code((int)$status);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function bratonien_tools_nc_edit_fail($message, array $fields = array(), $stage = '', $detail = '')
{
$visible = trim((string)$message);
if ($stage !== '') $visible = 'Bereich: '.$stage.' — '.$visible;
if ($detail !== '') $visible .= ' Technische Ursache: '.$detail;
bratonien_tools_nc_edit_json(422, array(
'ok'=>false,
'message'=>$visible,
'stage'=>(string)$stage,
'detail'=>(string)$detail,
'fields'=>array_values(array_unique($fields)),
));
}
function bratonien_tools_nc_edit_probe($url, $username = '', $password = '', array $headers = array())
{
if (!function_exists('curl_init'))
{
return array('ok'=>false, 'http'=>0, 'errno'=>-1, 'error'=>'cURL ist in PHP nicht verfügbar.', 'body'=>'');
}
$ch = curl_init($url);
$options = array(
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_FOLLOWLOCATION=>true,
CURLOPT_MAXREDIRS=>3,
CURLOPT_CONNECTTIMEOUT=>8,
CURLOPT_TIMEOUT=>15,
CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'), $headers),
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Editor/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
);
if ($username !== '')
{
$options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
$options[CURLOPT_USERPWD] = $username.':'.$password;
}
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return array(
'ok'=>$body !== false && $errno === 0,
'http'=>$http,
'errno'=>$errno,
'error'=>$error,
'body'=>$body === false ? '' : (string)$body,
);
}
if (!defined('BRATONIEN_TOOLS_PATH'))
{
bratonien_tools_nc_edit_json(404, array('ok'=>false, 'message'=>'Bratonien Tools ist nicht aktiv.', 'fields'=>array()));
}
if (!function_exists('is_admin') || !is_admin())
{
bratonien_tools_nc_edit_json(403, array('ok'=>false, 'message'=>'Administratorrechte erforderlich.', 'fields'=>array()));
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST')
{
bratonien_tools_nc_edit_json(405, array('ok'=>false, 'message'=>'Nur POST ist erlaubt.', 'fields'=>array()));
}
try
{
check_pwg_token();
require_once(BRATONIEN_TOOLS_PATH.'include/tool_registry.inc.php');
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
bratonien_tools_nc_edit_fail('Die zu bearbeitende Verbindung wurde nicht gefunden.', array(), 'Verbindung');
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$nextcloud_input = trim((string)($_POST['nc_nextcloud_url'] ?? ($config['nextcloud_url'] ?? '')));
$nextcloud_user = trim((string)($_POST['nc_nextcloud_user'] ?? ($credentials['nextcloud_user'] ?? '')));
$submitted_nc_password = (string)($_POST['nc_nextcloud_password'] ?? '');
$nextcloud_password = $submitted_nc_password !== '' ? $submitted_nc_password : (string)($credentials['nextcloud_password'] ?? '');
$has_any_nextcloud = $nextcloud_input !== '' || $nextcloud_user !== '' || $nextcloud_password !== '';
if ($has_any_nextcloud)
{
$missing = array();
if ($nextcloud_input === '') $missing[] = 'nc_nextcloud_url';
if ($nextcloud_user === '') $missing[] = 'nc_nextcloud_user';
if ($nextcloud_password === '') $missing[] = 'nc_nextcloud_password';
if ($missing)
{
bratonien_tools_nc_edit_fail(
'Die WebDAV-Daten sind unvollständig. Adresse, Benutzer und Passwort werden gemeinsam benötigt.',
$missing,
'Nextcloud / WebDAV'
);
}
try
{
$candidate_urls = bratonien_tools_nc_wizard_candidate_urls($nextcloud_input);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail($e->getMessage(), array('nc_nextcloud_url'), 'Nextcloud / WebDAV');
}
$nextcloud_url = '';
$last_probe = null;
$last_status_url = '';
foreach ($candidate_urls as $candidate_url)
{
$status_url = $candidate_url.'/status.php';
$probe = bratonien_tools_nc_edit_probe($status_url);
$last_probe = $probe;
$last_status_url = $status_url;
if (!$probe['ok'] || $probe['http'] < 200 || $probe['http'] >= 300)
{
continue;
}
$status = json_decode($probe['body'], true);
if (!is_array($status) || empty($status['installed']))
{
continue;
}
$nextcloud_url = $candidate_url;
break;
}
if ($nextcloud_url === '')
{
$attempts = array();
foreach ($candidate_urls as $candidate_url)
{
$attempts[] = $candidate_url.'/status.php';
}
if (is_array($last_probe) && !$last_probe['ok'])
{
$technical = $last_probe['errno'] >= 0
? 'cURL '.$last_probe['errno'].($last_probe['error'] !== '' ? ': '.$last_probe['error'] : '')
: $last_probe['error'];
bratonien_tools_nc_edit_fail(
'Die Nextcloud-Adresse ist vom Piwigo-Server aus nicht erreichbar. Ohne angegebenes Protokoll wurden HTTPS und HTTP geprüft.',
array('nc_nextcloud_url'),
'Nextcloud / WebDAV Erreichbarkeit',
$technical.' · Geprüft: '.implode(' ; ', $attempts)
);
}
bratonien_tools_nc_edit_fail(
'Unter der angegebenen Adresse wurde keine erreichbare installierte Nextcloud gefunden. Ohne angegebenes Protokoll wurden HTTPS und HTTP geprüft.',
array('nc_nextcloud_url'),
'Nextcloud / WebDAV Erkennung',
'Geprüft: '.implode(' ; ', $attempts).($last_probe ? ' · Letzter HTTP-Status: '.$last_probe['http'] : '')
);
}
$_POST['nc_nextcloud_url'] = $nextcloud_url;
$user_url = $nextcloud_url.'/ocs/v2.php/cloud/user?format=json';
$user_probe = bratonien_tools_nc_edit_probe($user_url, $nextcloud_user, $nextcloud_password, array('OCS-APIRequest: true'));
if (!$user_probe['ok'])
{
$technical = $user_probe['errno'] >= 0
? 'cURL '.$user_probe['errno'].($user_probe['error'] !== '' ? ': '.$user_probe['error'] : '')
: $user_probe['error'];
bratonien_tools_nc_edit_fail(
'Die Nextcloud-Anmeldung konnte technisch nicht geprüft werden.',
array('nc_nextcloud_url','nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung',
$technical.' · Ziel: '.$user_url
);
}
if ($user_probe['http'] === 401 || $user_probe['http'] === 403)
{
bratonien_tools_nc_edit_fail(
'Nextcloud hat Benutzername oder Passwort abgelehnt.',
array('nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung',
'HTTP '.$user_probe['http']
);
}
if ($user_probe['http'] < 200 || $user_probe['http'] >= 300)
{
bratonien_tools_nc_edit_fail(
'Nextcloud ist erreichbar, aber die Benutzerprüfung wurde vom Server abgelehnt.',
array('nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung',
'HTTP '.$user_probe['http'].' · Ziel: '.$user_url
);
}
try
{
bratonien_tools_nc_wizard_ocs_data($user_probe['body']);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail(
'Nextcloud antwortet auf die Benutzerprüfung, die Antwort ist aber nicht gültig: '.$e->getMessage(),
array('nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung'
);
}
}
$api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? ($credentials['api_key_id'] ?? '')));
$submitted_api_secret = trim((string)($_POST['nc_connection_api_key_secret'] ?? ''));
$api_key_secret = $submitted_api_secret !== '' ? $submitted_api_secret : trim((string)($credentials['api_key_secret'] ?? ''));
if (($api_key_id === '') !== ($api_key_secret === ''))
{
$missing = $api_key_id === '' ? array('nc_connection_api_key_id') : array('nc_connection_api_key_secret');
bratonien_tools_nc_edit_fail(
'API-Schlüssel-ID und API-Geheimnis müssen gemeinsam vollständig sein.',
$missing,
'Piwigo API'
);
}
if ($api_key_id !== '')
{
try
{
bratonien_tools_nc_connector_validate_scoped_api($api_key_id, $api_key_secret);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail(
'Der Piwigo-API-Zugang konnte nicht bestätigt werden: '.$e->getMessage(),
array('nc_connection_api_key_id','nc_connection_api_key_secret'),
'Piwigo API'
);
}
}
$result = bratonien_tools_nc_connector_update_local_friendly();
bratonien_tools_nc_edit_json(200, array(
'ok'=>true,
'message'=>(string)($result['message'] ?? 'Verbindung wurde gespeichert.'),
'fields'=>array(),
));
}
catch (Throwable $e)
{
$message = trim($e->getMessage());
if ($message === '') $message = 'Die Verbindung konnte nicht gespeichert werden.';
$fields = array();
$stage = 'Verbindung';
$add = function($name) use (&$fields)
{
if (!in_array($name, $fields, true)) $fields[] = $name;
};
if (stripos($message, 'Name') !== false && stripos($message, 'Datenbankname') === false) $add('connection_name');
if (stripos($message, 'Datenbank-Server') !== false) { $add('nc_host'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Datenbank-Port') !== false) { $add('nc_port'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Datenbankname') !== false) { $add('nc_database'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Reader-Benutzer') !== false) { $add('nc_user'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Datenbankpasswort') !== false) { $add('nc_db_password'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Storage-ID') !== false) { $add('nc_storage_id[]'); $stage = 'Speicherorte'; }
if (stripos($message, 'lokaler Pfad') !== false || stripos($message, 'Speicherort') !== false) { $add('nc_local_mount[]'); $stage = 'Speicherorte'; }
if (stripos($message, 'Galerieordner') !== false) { $add('nc_gallery_root'); $stage = 'Erweiterte Legacy-Einstellungen'; }
if (stripos($message, 'Datenbankansichten') !== false || stripos($message, 'Source-View') !== false) { $add('nc_source_view'); $stage = 'Erweiterte Legacy-Einstellungen'; }
if (stripos($message, 'Datenbankansichten') !== false || stripos($message, 'Activity-View') !== false) { $add('nc_activity_view'); $stage = 'Erweiterte Legacy-Einstellungen'; }
if (stripos($message, 'Nextcloud') !== false)
{
$stage = 'Nextcloud / WebDAV';
if (stripos($message, 'Adresse') !== false) $add('nc_nextcloud_url');
if (stripos($message, 'Benutzer') !== false) $add('nc_nextcloud_user');
if (stripos($message, 'Passwort') !== false) $add('nc_nextcloud_password');
}
if (stripos($message, 'API') !== false)
{
$stage = 'Piwigo API';
$add('nc_connection_api_key_id');
$add('nc_connection_api_key_secret');
}
bratonien_tools_nc_edit_fail(
$message,
$fields,
$stage,
'Interne Prüfung: '.get_class($e)
);
}

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);

1
noop2 Normal file
View File

@@ -0,0 +1 @@
x

1
noop2-remove-marker.txt Normal file
View File

@@ -0,0 +1 @@
stale branch only

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# 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

View File

@@ -1,142 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$options = getopt('', array('piwigo-root:', 'connection-id:'));
$piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/');
$connection_id = (int)($options['connection-id'] ?? 0);
if ($piwigo_root === '' || $connection_id < 1)
{
fwrite(STDERR, "Parameter --piwigo-root und --connection-id werden benoetigt.\n");
exit(1);
}
$db_config = $piwigo_root.'/local/config/database.inc.php';
if (!is_readable($db_config))
{
fwrite(STDERR, "Piwigo-Datenbankkonfiguration ist nicht lesbar.\n");
exit(1);
}
$conf = array();
$prefixeTable = 'piwigo_';
require $db_config;
foreach (array('db_host','db_user','db_password','db_base') as $key)
{
if (!isset($conf[$key]))
{
fwrite(STDERR, "Piwigo-Datenbankkonfiguration ist unvollstaendig: $key\n");
exit(1);
}
}
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno)
{
fwrite(STDERR, "Piwigo-Datenbank ist nicht erreichbar: ".$db->connect_error."\n");
exit(1);
}
$db->set_charset('utf8mb4');
$table = $prefixeTable.'bratonien_tools_nc_connections';
$result = $db->query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!$result || !$result->num_rows)
{
fwrite(STDERR, "Connector-Verbindung #$connection_id wurde nicht gefunden.\n");
exit(1);
}
$config = json_decode((string)$result->fetch_assoc()['config_json'], true);
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder')
{
echo "Keine WebDAV-Bereinigung erforderlich.\n";
exit(0);
}
$prefixes = array(
'./_data/bratonien-tools/nc-webdav-gallery/connection-'.$connection_id.'/',
'./_data/bratonien-tools/nc-webdav-source/connection-'.$connection_id.'/',
'./galleries/bratonien-webdav-'.$connection_id.'/',
);
$gallery_root = rtrim(str_replace('\\', '/', (string)($config['parallel_gallery_root'] ?? '')), '/');
$normalized_piwigo_root = rtrim(str_replace('\\', '/', $piwigo_root), '/');
if ($gallery_root !== '' && strpos($gallery_root, $normalized_piwigo_root.'/') === 0)
{
$relative = ltrim(substr($gallery_root, strlen($normalized_piwigo_root)), '/');
if ($relative !== '') $prefixes[] = './'.rtrim($relative, '/').'/';
}
$prefixes = array_values(array_unique($prefixes));
$where = array();
foreach ($prefixes as $prefix)
{
$escaped = $db->real_escape_string(addcslashes($prefix, "_%\\"));
$where[] = "path LIKE '{$escaped}%' ESCAPE '\\\\'";
}
$images_table = $prefixeTable.'images';
$rows = $db->query("SELECT id, path FROM `{$images_table}` WHERE ".implode(' OR ', $where));
if (!$rows)
{
fwrite(STDERR, "Connector-Bilder konnten nicht gelesen werden: ".$db->error."\n");
exit(1);
}
$missing_ids = array();
while ($row = $rows->fetch_assoc())
{
$path = (string)$row['path'];
$owned = false;
foreach ($prefixes as $prefix)
{
if (strpos($path, $prefix) === 0)
{
$owned = true;
break;
}
}
if (!$owned) continue;
$absolute = $piwigo_root.'/'.ltrim(preg_replace('#^\./#', '', $path), '/');
if (!is_file($absolute))
{
$missing_ids[] = (int)$row['id'];
}
}
$db->close();
if (!$missing_ids)
{
echo "WebDAV-Bereinigung: keine fehlenden Piwigo-Bilder.\n";
exit(0);
}
// Dieser Prozess wird ausschliesslich nach einem erfolgreich abgeschlossenen
// WebDAV-Aufbau und erfolgreicher Piwigo-Synchronisierung gestartet. Eine
// nicht erreichbare Verbindung kann deshalb niemals ueber diesen Pfad Bilder
// entfernen.
define('PHPWG_ROOT_PATH', $piwigo_root.'/');
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['SERVER_PORT'] = '80';
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/cleanup-missing-webdav-images.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$deleted = delete_elements(array_values(array_unique(array_map('intval', $missing_ids))), false);
update_category('all');
invalidate_user_cache(true);
echo 'WebDAV-Bereinigung: '.(int)$deleted." nicht mehr freigegebene/vorhandene Bilder aus Piwigo entfernt.\n";

View File

@@ -47,31 +47,24 @@ function bratonien_cleanup_tree($path, $allowed_root)
@rmdir($path);
}
function bratonien_connection_id_from_owned_path($path)
function bratonien_webdav_site_images($site_id)
{
$path = (string)$path;
foreach (array(
'#^\./_data/bratonien-tools/nc-webdav-gallery/connection-([0-9]+)/#',
'#^\./_data/bratonien-tools/nc-webdav-source/connection-([0-9]+)/#',
'#^\./galleries/bratonien-webdav-([0-9]+)/#',
) as $pattern)
$rows = array();
$query = '
SELECT DISTINCT i.id, i.path, i.representative_ext
FROM '.IMAGES_TABLE.' AS i
LEFT JOIN '.CATEGORIES_TABLE.' AS sc ON sc.id = i.storage_category_id
LEFT JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON ic.image_id = i.id
LEFT JOIN '.CATEGORIES_TABLE.' AS vc ON vc.id = ic.category_id
WHERE sc.site_id = '.(int)$site_id.' OR vc.site_id = '.(int)$site_id.'
;';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
if (preg_match($pattern, $path, $match)) return (int)$match[1];
$row['id'] = (int)$row['id'];
$rows[$row['id']] = $row;
}
return 0;
}
function bratonien_connection_id_from_site_url($url)
{
$url = (string)$url;
foreach (array(
'#^\./_data/bratonien-tools/nc-webdav-gallery/connection-([0-9]+)/$#',
'#^\./galleries/bratonien-webdav-([0-9]+)/$#',
) as $pattern)
{
if (preg_match($pattern, $url, $match)) return (int)$match[1];
}
return 0;
return $rows;
}
try
@@ -91,24 +84,24 @@ try
}
}
$removed_images = 0;
$stale_by_connection = array();
$site_prefix = './_data/bratonien-tools/nc-webdav-gallery/connection-';
$result = pwg_query(
"SELECT id, path, representative_ext FROM ".IMAGES_TABLE.
" WHERE path LIKE './_data/bratonien-tools/nc-webdav-gallery/connection-%'".
" OR path LIKE './_data/bratonien-tools/nc-webdav-source/connection-%'".
" OR path LIKE './galleries/bratonien-webdav-%'"
"SELECT id, galleries_url FROM ".SITES_TABLE.
" WHERE galleries_url LIKE '".pwg_db_real_escape_string($site_prefix)."%'"
);
while ($row = pwg_db_fetch_assoc($result))
{
$connection_id = bratonien_connection_id_from_owned_path($row['path'] ?? '');
if ($connection_id < 1 || isset($active[$connection_id])) continue;
$row['id'] = (int)$row['id'];
$stale_by_connection[$connection_id][$row['id']] = $row;
}
foreach ($stale_by_connection as $connection_id=>$images)
$removed_sites = 0;
$removed_images = 0;
while ($site = pwg_db_fetch_assoc($result))
{
$site_id = (int)$site['id'];
$site_url = (string)$site['galleries_url'];
if (!preg_match('#^\./_data/bratonien-tools/nc-webdav-gallery/connection-([0-9]+)/$#', $site_url, $match)) continue;
$connection_id = (int)$match[1];
if ($connection_id < 1 || isset($active[$connection_id])) continue;
$images = bratonien_webdav_site_images($site_id);
foreach ($images as $row)
{
try
@@ -121,60 +114,48 @@ try
}
}
delete_elements(array_map('intval', array_keys($images)), false);
$removed_images += count($images);
echo 'NC WebDAV: entferne '.count($images).' verwaiste Bilder der gelöschten Verbindung '.$connection_id.".\n";
}
delete_site($site_id);
$removed_sites = 0;
$result = pwg_query('SELECT id, galleries_url FROM '.SITES_TABLE);
$stale_sites = array();
while ($site = pwg_db_fetch_assoc($result))
{
$connection_id = bratonien_connection_id_from_site_url($site['galleries_url'] ?? '');
if ($connection_id < 1 || isset($active[$connection_id])) continue;
$stale_sites[] = array('id'=>(int)$site['id'], 'connection_id'=>$connection_id);
}
if ($images)
{
$ids = array_keys($images);
$remaining = query2array(
'SELECT id FROM '.IMAGES_TABLE.' WHERE id IN ('.implode(',', array_map('intval', $ids)).')',
null,
'id'
);
if ($remaining)
{
delete_elements(array_map('intval', $remaining), false);
}
}
foreach ($stale_sites as $site)
{
delete_site($site['id']);
$removed_sites++;
echo 'NC WebDAV: entferne verwaiste Piwigo-Site '.$site['id'].' der gelöschten Verbindung '.$site['connection_id'].".\n";
}
$removed_images += count($images);
echo 'NC WebDAV: entferne verwaiste Piwigo-Site '.$site_id.' für gelöschte Verbindung '.$connection_id.".\n";
$stale_connection_ids = array_unique(array_merge(
array_map('intval', array_keys($stale_by_connection)),
array_map(function($site) { return (int)$site['connection_id']; }, $stale_sites)
));
$gallery_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-gallery';
$source_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-source';
$preview_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-preview';
$legacy_root = PHPWG_ROOT_PATH.'galleries';
$state_root = '/var/lib/bratonien-tools/nc-connector';
$status_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-connector-status';
foreach ($stale_connection_ids as $connection_id)
{
if ($connection_id < 1) continue;
$gallery_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-gallery';
$source_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-source';
$preview_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-preview';
bratonien_cleanup_tree($gallery_root.'/connection-'.$connection_id, $gallery_root);
bratonien_cleanup_tree($source_root.'/connection-'.$connection_id, $source_root);
bratonien_cleanup_tree($preview_root.'/connection-'.$connection_id, $preview_root);
bratonien_cleanup_tree($legacy_root.'/bratonien-webdav-'.$connection_id, $legacy_root);
$state_root = '/var/lib/bratonien-tools/nc-connector';
bratonien_cleanup_tree($state_root.'/connection-'.$connection_id, $state_root);
foreach (glob('/etc/bratonien-tools/nc-connector/webdav-connection-'.$connection_id.'.*') ?: array() as $file)
{
@unlink($file);
}
$status_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-connector-status';
@unlink($status_root.'/connection-'.$connection_id.'.json');
@unlink($status_root.'/deleted-'.$connection_id);
}
if ($removed_sites > 0 || $removed_images > 0)
if ($removed_sites > 0)
{
update_category('all');
invalidate_user_cache(true);
}

0
runtime/lib/build-webdav-derivatives.php Executable file → Normal file
View File

272
runtime/lib/build_webdav_placeholder_source.py Executable file → Normal file
View File

@@ -1,13 +1,9 @@
#!/usr/bin/env python3
"""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.
This is intentionally additive: it does not replace the existing local-storage
connector path. It creates only tiny placeholder files plus a metadata mapping;
no Nextcloud original media is downloaded.
"""
from __future__ import annotations
@@ -18,7 +14,6 @@ import getpass
import json
import os
import shutil
import socket
import ssl
import sys
import tempfile
@@ -26,15 +21,14 @@ import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from contextlib import contextmanager
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"}
# 1x1 transparent GIF, 34 bytes. The filename keeps the remote extension;
# the placeholder exists only so Piwigo can discover the logical image entry.
PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
DIMENSION_PROBE_BYTES = 4 * 1024 * 1024
def fail(message: str) -> None:
@@ -60,132 +54,9 @@ 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()
ip = ip.strip("[]")
if not host or not ip or host == ip:
yield
return
original = socket.getaddrinfo
def resolve(name, port, family=0, type=0, proto=0, flags=0):
normalized = str(name).strip("[]").lower()
if normalized == host:
return original(ip, port, family, type, proto, flags)
return original(name, port, family, type, proto, flags)
socket.getaddrinfo = resolve
try:
yield
finally:
socket.getaddrinfo = original
class WebDavClient:
def __init__(self, base_url: str, user: str, password: str, timeout: int = 30, connect_ip: str = "") -> None:
def __init__(self, base_url: str, user: str, password: str, timeout: int = 30) -> None:
self.base_url = base_url.rstrip("/")
parsed = urllib.parse.urlparse(self.base_url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
fail("Nextcloud base URL must use HTTP or HTTPS and contain a host")
self.host = parsed.hostname.strip("[]")
self.connect_ip = connect_ip.strip("[]") or self.host
self.user = user
self.password = password
self.timeout = timeout
@@ -193,60 +64,31 @@ 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)
request.add_header("Depth", "1")
request.add_header("Content-Type", "application/xml; charset=utf-8")
try:
with pinned_resolution(self.host, self.connect_ip):
with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response:
status = response.status
payload = response.read()
with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response:
status = response.status
payload = response.read()
except urllib.error.HTTPError as error:
if error.code in {401, 403}:
fail("Nextcloud rejected the WebDAV credentials or directory access")
@@ -256,14 +98,14 @@ class WebDavClient:
if status != 207:
fail(f"Nextcloud PROPFIND returned HTTP {status}")
base_path = urllib.parse.unquote(urllib.parse.urlparse(url).path).rstrip("/")
current: dict[str, object] | None = None
children: list[dict[str, object]] = []
try:
root = ET.fromstring(payload)
except ET.ParseError as error:
raise RuntimeError("Nextcloud returned invalid WebDAV XML") from error
base_path = urllib.parse.unquote(urllib.parse.urlparse(url).path).rstrip("/")
current: dict[str, object] | None = None
children: list[dict[str, object]] = []
for response in root.findall(f"{{{DAV}}}response"):
href = response.findtext(f"{{{DAV}}}href", default="")
href_path = urllib.parse.unquote(urllib.parse.urlparse(href).path).rstrip("/")
@@ -280,7 +122,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 +129,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 +140,24 @@ 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 +187,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 +197,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:
@@ -387,7 +221,6 @@ def atomic_text(path: Path, text: str) -> None:
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base-url", required=True)
parser.add_argument("--connect-ip", default="", help="IP address used for the TCP connection while preserving the URL host for HTTP Host and TLS SNI")
parser.add_argument("--user", required=True)
parser.add_argument("--password-file", type=Path)
parser.add_argument("--root", action="append", required=True, help="WebDAV path relative to the authenticated user's files root")
@@ -408,46 +241,34 @@ 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)
client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout), args.connect_ip)
client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout))
mapping: dict[str, dict[str, object]] = {}
manifest: list[str] = []
total_files = total_folders = total_skipped = total_probed = 0
used_fileids: set[int] = set()
total_files = total_folders = total_skipped = 0
used_names: set[str] = set()
for remote_root_raw in args.root:
remote_root = validate_relative(remote_root_raw)
current, root_children = client.list_collection(remote_root)
current, _ = client.list_collection(remote_root)
fileid = int(current["fileid"])
if fileid in used_fileids:
fail(f"duplicate selected Nextcloud root fileid: {fileid}")
used_fileids.add(fileid)
display = str(current.get("display_name", "")).strip() or (PurePosixPath(remote_root).name if remote_root else args.user)
local_name = f"root-{fileid}"
if local_name in used_names:
fail(f"duplicate selected Nextcloud root fileid: {fileid}")
used_names.add(local_name)
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()):
name = safe_local_name(str(child.get("display_name", "")))
child_fileid = int(child.get("fileid", 0))
if child_fileid < 1:
fail(f"Nextcloud returned no stable fileid for root child {name!r}")
child_path = source_dir / local_name / name
if bool(child.get("is_dir")):
manifest.append(f"webdav:{child_fileid}\tfolder\t{name}\t{child_path}")
elif Path(name).suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS:
manifest.append(f"webdav:{child_fileid}\tfile\t{name}\t{child_path}")
continue
display = str(current.get("display_name", "")).strip() or PurePosixPath(remote_root).name
manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}")
if previous.exists():
@@ -474,9 +295,8 @@ 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,
"files": final_mapping,
})
@@ -485,8 +305,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),
"mapping": str(args.mapping),

0
runtime/lib/piwigo-sync.php Executable file → Normal file
View File

View File

@@ -6,8 +6,6 @@ if (PHP_SAPI !== 'cli')
exit(1);
}
require_once(dirname(__DIR__, 2).'/include/nc_transport.inc.php');
const BRATONIEN_WEBDAV_PREVIEW_VERSION = 2;
const BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE = 4096;
const BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY = 88;
@@ -26,7 +24,7 @@ function quote_webdav_path($path)
function fetch_remote_blob($url, $user, $password)
{
$ch = curl_init($url);
$options = array(
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
@@ -34,10 +32,8 @@ function fetch_remote_blob($url, $user, $password)
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.7.16',
);
bratonien_tools_nc_transport_apply_curl($options, $url);
curl_setopt_array($ch, $options);
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.6.1',
));
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
@@ -52,11 +48,12 @@ function fetch_remote_blob($url, $user, $password)
function preview_extension_for_entry(array $entry)
{
$content_type = strtolower(trim((string)($entry['content_type'] ?? '')));
$path = strtolower((string)($entry['webdav_path'] ?? ''));
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if ($extension === 'png') return 'png';
if ($extension === 'gif') return 'gif';
if ($extension === 'webp') return 'webp';
if ($content_type === 'image/png' || preg_match('/\.png$/', $path))
{
return 'png';
}
return 'jpg';
}
@@ -67,7 +64,7 @@ function preview_target_for_entry($cache_dir, $key, array $entry)
function remove_other_preview_format($cache_dir, $key, $keep_target)
{
foreach (array('jpg', 'png', 'gif', 'webp') as $ext)
foreach (array('jpg', 'png', 'webp') as $ext)
{
$candidate = $cache_dir.'/'.$key.'.'.$ext;
if ($candidate !== $keep_target && is_file($candidate)) @unlink($candidate);
@@ -89,19 +86,13 @@ function write_preview_imagick($blob, $target, $extension)
{
$image->setImageFormat('png');
}
elseif ($extension === 'gif')
{
$image->setImageFormat('gif');
}
elseif ($extension === 'webp')
{
$image->setImageFormat('webp');
$image->setImageCompressionQuality(BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY);
}
else
{
$image->setImageBackgroundColor('white');
if (method_exists($image, 'mergeImageLayers')) $image = $image->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
if (method_exists($image, 'mergeImageLayers'))
{
$image = $image->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
}
$image->setImageFormat('jpeg');
$image->setImageCompression(Imagick::COMPRESSION_JPEG);
$image->setImageCompressionQuality(BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY);
@@ -136,7 +127,7 @@ function write_preview_gd($blob, $target, $extension)
try
{
if (in_array($extension, array('png', 'gif', 'webp'), true))
if ($extension === 'png')
{
imagealphablending($preview, false);
imagesavealpha($preview, true);
@@ -154,12 +145,10 @@ function write_preview_gd($blob, $target, $extension)
fail_preview('GD konnte das Preview nicht skalieren.');
}
if ($extension === 'png') $written = imagepng($preview, $target, 6);
elseif ($extension === 'gif') $written = function_exists('imagegif') ? imagegif($preview, $target) : false;
elseif ($extension === 'webp') $written = function_exists('imagewebp') ? imagewebp($preview, $target, BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY) : false;
else $written = imagejpeg($preview, $target, BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY);
if (!$written) fail_preview('GD konnte das Preview im Format '.$extension.' nicht schreiben.');
$written = $extension === 'png'
? imagepng($preview, $target, 6)
: imagejpeg($preview, $target, BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY);
if (!$written) fail_preview('GD konnte das Preview nicht schreiben.');
}
finally
{
@@ -175,7 +164,7 @@ function write_preview_gd($blob, $target, $extension)
function write_preview($blob, $target, $extension)
{
$dir = dirname($target);
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) fail_preview('Preview-Verzeichnis konnte nicht angelegt werden.');
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) fail_preview('Preview-Verzeichnis konnte nicht angelegt werden.');
if (class_exists('Imagick'))
{
@@ -197,48 +186,7 @@ function write_preview($blob, $target, $extension)
@unlink($target);
fail_preview('Das erzeugte Preview ist ungültig.');
}
@chmod($target, 0664);
}
function local_source_is_hydrated($source_path, $preview)
{
if (!is_file($source_path) || !is_readable($source_path)) return false;
$source_size = @getimagesize($source_path);
$preview_size = @getimagesize($preview);
if (!is_array($source_size) || !is_array($preview_size)) return false;
if ((int)$source_size[0] !== (int)$preview_size[0] || (int)$source_size[1] !== (int)$preview_size[1]) return false;
$source_bytes = @filesize($source_path);
$preview_bytes = @filesize($preview);
return $source_bytes !== false && $preview_bytes !== false && (int)$source_bytes === (int)$preview_bytes;
}
function hydrate_local_source($source_path, $preview)
{
$normalized = str_replace('\\', '/', (string)$source_path);
if (!preg_match('#/nc-webdav-source/connection-[0-9]+/root-[0-9]+/#', $normalized))
{
fail_preview('Unsicherer lokaler Connector-Quellpfad: '.$source_path);
}
if (!is_file($preview) || !is_readable($preview)) fail_preview('Preview für lokale Quelle fehlt: '.$preview);
if (local_source_is_hydrated($source_path, $preview)) return false;
$dir = dirname($source_path);
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) fail_preview('Lokales Connector-Verzeichnis konnte nicht angelegt werden: '.$dir);
$tmp = $source_path.'.bratonien-next-'.getmypid();
if (!@copy($preview, $tmp)) fail_preview('Preview konnte nicht in lokale Piwigo-Quelle kopiert werden: '.$source_path);
@chmod($tmp, 0664);
$mtime = @filemtime($preview);
if ($mtime) @touch($tmp, $mtime);
if (!@rename($tmp, $source_path))
{
@unlink($tmp);
fail_preview('Lokale Piwigo-Quelle konnte nicht ersetzt werden: '.$source_path);
}
clearstatcache(true, $source_path);
$size = @getimagesize($source_path);
if (!is_array($size) || empty($size[0]) || empty($size[1])) fail_preview('Lokale Piwigo-Quelle ist nach dem Ersetzen ungültig: '.$source_path);
return true;
@chmod($target, 0644);
}
try
@@ -260,8 +208,8 @@ try
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (!is_array($mapping) || !isset($mapping['files']) || !is_array($mapping['files'])) fail_preview('WebDAV-Mapping ist ungültig.');
if (!is_dir($cache_dir) && !mkdir($cache_dir, 0775, true) && !is_dir($cache_dir)) fail_preview('Preview-Cache konnte nicht angelegt werden.');
@chmod($cache_dir, 2775);
if (!is_dir($cache_dir) && !mkdir($cache_dir, 0755, true) && !is_dir($cache_dir)) fail_preview('Preview-Cache konnte nicht angelegt werden.');
@chmod($cache_dir, 0755);
$state_file = $cache_dir.'/state.json';
$old_state = array();
@@ -274,10 +222,9 @@ try
$new_state = array();
$generated = 0;
$cached = 0;
$hydrated = 0;
$errors = 0;
foreach ($mapping['files'] as $local_path=>$entry)
foreach ($mapping['files'] as $entry)
{
if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue;
$webdav_path = trim((string)($entry['webdav_path'] ?? ''), '/');
@@ -294,29 +241,26 @@ try
'version' => BRATONIEN_WEBDAV_PREVIEW_VERSION,
);
$old = $old_state[$key] ?? null;
if (
is_file($target)
&& is_array($old)
&& (string)($old['etag'] ?? '') === $etag
&& (string)($old['format'] ?? '') === $extension
&& (int)($old['version'] ?? 0) === BRATONIEN_WEBDAV_PREVIEW_VERSION
)
{
$cached++;
continue;
}
try
{
$old = $old_state[$key] ?? null;
$valid_cached = is_file($target)
&& is_array($old)
&& (string)($old['etag'] ?? '') === $etag
&& (string)($old['format'] ?? '') === $extension
&& (int)($old['version'] ?? 0) === BRATONIEN_WEBDAV_PREVIEW_VERSION;
if (!$valid_cached)
{
$url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.quote_webdav_path($webdav_path);
$blob = fetch_remote_blob($url, $user, $password);
write_preview($blob, $target, $extension);
remove_other_preview_format($cache_dir, $key, $target);
$generated++;
}
else
{
$cached++;
}
if (hydrate_local_source((string)$local_path, $target)) $hydrated++;
$url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.quote_webdav_path($webdav_path);
$blob = fetch_remote_blob($url, $user, $password);
write_preview($blob, $target, $extension);
remove_other_preview_format($cache_dir, $key, $target);
$generated++;
}
catch (Throwable $e)
{
@@ -329,14 +273,14 @@ try
{
if (!is_file($file) || basename($file) === 'state.json') continue;
$name = basename($file);
if (!preg_match('/^([a-f0-9]{40})\.(jpg|png|gif|webp)$/', $name, $m)) continue;
if (!preg_match('/^([a-f0-9]{40})\.(jpg|png|webp)$/', $name, $m)) continue;
if (!isset($new_state[$m[1]])) @unlink($file);
}
file_put_contents($state_file, json_encode($new_state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)."\n", LOCK_EX);
@chmod($state_file, 0664);
@chmod($state_file, 0644);
echo 'WebDAV-Previews: erzeugt='.$generated.' vorhanden='.$cached.' lokale_quellen='.$hydrated.' fehler='.$errors."\n";
echo 'WebDAV-Previews: erzeugt='.$generated.' vorhanden='.$cached.' fehler='.$errors."\n";
exit($errors > 0 ? 1 : 0);
}
catch (Throwable $e)

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
require_once(dirname(__DIR__, 2).'/include/nc_transport.inc.php');
try
{
if ($argc !== 2) throw new RuntimeException('Aufruf: resolve-nextcloud-target.php <nextcloud-url>');
$url = rtrim(trim((string)$argv[1]), '/');
bratonien_tools_nc_transport_scheme($url);
$host = bratonien_tools_nc_transport_host($url);
echo bratonien_tools_nc_transport_public_ip($host)."\n";
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, $e->getMessage()."\n");
exit(1);
}

0
runtime/lib/shadow_tree.py Executable file → Normal file
View File

View File

@@ -1,108 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$options = getopt('', array('connection-id::'));
$connectionId = isset($options['connection-id']) ? (int)$options['connection-id'] : 0;
$pluginRoot = dirname(__DIR__);
$piwigoRoot = dirname($pluginRoot, 2);
$base = rtrim($piwigoRoot, '/').'/_data/bratonien-tools';
$schedulerDir = $base.'/nc-connector-scheduler';
$stateFile = $schedulerDir.'/state.json';
$runtimeDir = $base.'/nc-connector-runtime';
$stateRoot = $base.'/nc-connector-state';
function native_scheduler_state($path)
{
if (!is_readable($path)) return array();
$decoded = json_decode((string)@file_get_contents($path), true);
return is_array($decoded) ? $decoded : array();
}
function native_scheduler_write($path, array $state)
{
$json = json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if (!is_string($json)) return false;
$tmp = $path.'.tmp';
if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false) return false;
@chmod($tmp, 0640);
return @rename($tmp, $path);
}
foreach (array($schedulerDir, $runtimeDir, $stateRoot) as $dir)
{
if (!is_dir($dir) && !@mkdir($dir, 0750, true) && !is_dir($dir))
{
fwrite(STDERR, "Runtime-Verzeichnis konnte nicht angelegt werden: {$dir}\n");
exit(1);
}
}
$state = native_scheduler_state($stateFile);
$state['enabled'] = true;
$state['mode'] = 'piwigo-native';
$state['state'] = 'running';
$state['message'] = $connectionId > 0 ? 'NC-Abgleich für Verbindung #'.$connectionId.' läuft.' : 'NC-Abgleich läuft.';
$state['started_at'] = time();
$state['timestamp'] = time();
$state['connection_id'] = $connectionId;
native_scheduler_write($stateFile, $state);
$env = $_ENV;
$env['PATH'] = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
$env['BRATONIEN_NC_NATIVE'] = '1';
$env['BRATONIEN_NC_PIWIGO_ROOT'] = $piwigoRoot;
$env['BRATONIEN_NC_CONFIG_DIR'] = $runtimeDir;
$env['BRATONIEN_NC_STATE_ROOT'] = $stateRoot;
$env['BRATONIEN_NC_CONNECTION_ID'] = (string)$connectionId;
$env['LC_ALL'] = 'C';
$env['LANG'] = 'C';
$bash = is_executable('/usr/bin/bash') ? '/usr/bin/bash' : '/bin/bash';
$command = array($bash, $pluginRoot.'/runtime/run-all.sh');
$spec = array(
0=>array('file','/dev/null','r'),
1=>array('pipe','w'),
2=>array('pipe','w'),
);
$process = @proc_open($command, $spec, $pipes, null, $env);
$stdout = '';
$stderr = '';
$exit = 1;
if (is_resource($process))
{
$stdout = (string)stream_get_contents($pipes[1]);
$stderr = (string)stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
}
else
{
$stderr = 'run-all.sh konnte nicht gestartet werden.';
}
$state = native_scheduler_state($stateFile);
$state['enabled'] = true;
$state['mode'] = 'piwigo-native';
$state['state'] = $exit === 0 ? 'success' : 'error';
$state['message'] = $exit === 0
? ($connectionId > 0 ? 'NC-Abgleich für Verbindung #'.$connectionId.' erfolgreich abgeschlossen.' : 'NC-Abgleich erfolgreich abgeschlossen.')
: ($connectionId > 0 ? 'NC-Abgleich für Verbindung #'.$connectionId.' fehlgeschlagen.' : 'NC-Abgleich fehlgeschlagen.');
$state['timestamp'] = time();
$state['finished_at'] = time();
$state['exit_code'] = $exit;
$state['connection_id'] = $connectionId;
$state['stdout'] = trim($stdout);
$state['stderr'] = trim($stderr);
if (empty($state['next_due']) || (int)$state['next_due'] < time())
{
$state['next_due'] = time() + 60;
}
native_scheduler_write($stateFile, $state);
exit($exit === 0 ? 0 : 1);

View File

@@ -78,11 +78,8 @@ function webdav_source_fingerprint($baseUrl, $user, array $roots)
$pluginRoot = dirname(__DIR__);
$piwigoRoot = dirname($pluginRoot, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$nativeMode = getenv('BRATONIEN_NC_NATIVE') === '1';
$configDir = trim((string)getenv('BRATONIEN_NC_CONFIG_DIR'));
if ($configDir === '') $configDir = $nativeMode ? rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-runtime' : '/etc/bratonien-tools/nc-connector';
$stateRoot = trim((string)getenv('BRATONIEN_NC_STATE_ROOT'));
if ($stateRoot === '') $stateRoot = $nativeMode ? rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-state' : '/var/lib/bratonien-tools/nc-connector';
$configDir = '/etc/bratonien-tools/nc-connector';
$stateRoot = '/var/lib/bratonien-tools/nc-connector';
$publicSourceRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-source';
$publicGalleryRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-gallery';
$legacyGalleryRoot = rtrim($piwigoRoot, '/').'/galleries';
@@ -110,12 +107,11 @@ try
$rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id DESC");
if (!$rows) fail_webdav_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
foreach (array($configDir, $stateRoot, $publicSourceRoot, $publicGalleryRoot) as $dir)
foreach (array($configDir=>$configDir, $publicSourceRoot=>$publicSourceRoot, $publicGalleryRoot=>$publicGalleryRoot) as $dir=>$unused)
{
if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0750, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir);
if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0755, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir);
}
@chmod($configDir, 0700);
@chmod($stateRoot, 0750);
@chmod($publicSourceRoot, 0755);
@chmod($publicGalleryRoot, 0755);
@@ -129,6 +125,7 @@ try
if (!is_array($config)) $config = array();
if ((string)$row['adapter'] !== 'remote') continue;
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
if (empty($config['parallel_test'])) continue;
try
{
@@ -154,7 +151,7 @@ try
$seenFingerprints[$fingerprint] = $id;
$known[$id] = true;
$stateDir = $nativeMode ? $stateRoot.'/connection-'.$id : rtrim((string)($config['state_dir'] ?? ''), '/');
$stateDir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($stateDir === '') $stateDir = $stateRoot.'/connection-'.$id;
if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_webdav_reconcile('State-Verzeichnis konnte nicht angelegt werden.');
@chmod($stateDir, 0750);
@@ -167,6 +164,8 @@ try
}
if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.');
@chmod($galleryRoot, 0755);
// Alte technische Wrapper unter ./galleries duerfen nicht als Piwigo-Alben auftauchen.
webdav_remove_generated_tree($legacyDefault, $legacyGalleryRoot);
$sourceDir = $publicSourceRoot.'/connection-'.$id;
@@ -190,9 +189,8 @@ try
{
$path = trim((string)($root['webdav_path'] ?? ''), '/');
$display = trim((string)($root['display_name'] ?? ''));
$runtimePath = $path === '' ? '/' : $path;
if ($display === '' || preg_match('/[\t\r\n]/', $runtimePath.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.');
$rootLines[] = $runtimePath."\t".$display;
if ($path === '' || $display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.');
$rootLines[] = $path."\t".$display;
}
file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX);
@chmod($rootsPath, 0600);
@@ -217,23 +215,21 @@ try
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
@chmod($configPath, 0600);
unset($config['migration'], $config['parallel_test']);
$config['state_dir'] = $stateDir;
$config['status_file'] = $statusFile;
$config['parallel_gallery_root'] = $galleryRoot;
$config['source_fingerprint'] = $fingerprint;
$config['runtime'] = array(
'mode'=>$nativeMode ? 'piwigo-native-webdav' : 'webdav',
'mode'=>'parallel-webdav',
'config'=>$configPath,
'piwigo_sync_enabled'=>true,
'reconciled_at'=>date('Y-m-d H:i:s'),
);
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json)) fail_webdav_reconcile('WebDAV-Runtime-Konfiguration konnte nicht serialisiert werden.');
$escaped = $db->real_escape_string($json);
if (!$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} AND adapter='remote' LIMIT 1"))
if (is_string($json))
{
fail_webdav_reconcile('WebDAV-Runtime-Status konnte nicht gespeichert werden: '.$db->error);
$escaped = $db->real_escape_string($json);
$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} LIMIT 1");
}
}
catch (Throwable $e)

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
PIWIGO_ROOT="${1:-/var/www/piwigo}"
PIWIGO_ROOT="${PIWIGO_ROOT%/}"
DATA_DIR="$PIWIGO_ROOT/_data"
[[ -d "$DATA_DIR" ]] || { echo "Piwigo-_data wurde nicht gefunden: $DATA_DIR" >&2; exit 1; }
[[ "$(id -u)" -eq 0 ]] || { echo "Die Reparatur muss als root ausgeführt werden." >&2; exit 1; }
DATA_UID="$(stat -c '%u' "$DATA_DIR")"
DATA_GID="$(stat -c '%g' "$DATA_DIR")"
PATHS=(
"$DATA_DIR/bratonien-tools/nc-webdav-gallery"
"$DATA_DIR/bratonien-tools/nc-webdav-preview"
"$DATA_DIR/i/_data/bratonien-tools/nc-webdav-gallery"
"$DATA_DIR/i/bratonien-watermark"
)
found=0
for path in "${PATHS[@]}"; do
[[ -e "$path" ]] || continue
found=1
echo "Repariere: $path"
chown -R "$DATA_UID:$DATA_GID" -- "$path"
find "$path" -type d -exec chmod 2775 {} +
find "$path" -type f -exec chmod 0664 {} +
done
if [[ "$found" -eq 0 ]]; then
echo "Keine Bratonien-Cache-Verzeichnisse gefunden."
exit 0
fi
echo "Bratonien-Cache-Rechte wurden an $DATA_DIR angeglichen (UID $DATA_UID, GID $DATA_GID)."

View File

@@ -1,26 +1,10 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_DIR="/etc/bratonien-tools/nc-connector"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
PIWIGO_ROOT_DEFAULT="${BRATONIEN_NC_PIWIGO_ROOT:-$(cd -- "$SCRIPT_DIR/../../.." && pwd)}"
CONFIG_DIR="${BRATONIEN_NC_CONFIG_DIR:-/etc/bratonien-tools/nc-connector}"
NATIVE_MODE="${BRATONIEN_NC_NATIVE:-0}"
TARGET_CONNECTION_ID="${BRATONIEN_NC_CONNECTION_ID:-0}"
GLOBAL_LOCK_DIR="${PIWIGO_ROOT_DEFAULT%/}/_data/bratonien-tools/nc-connector-scheduler"
GLOBAL_LOCK_FILE="$GLOBAL_LOCK_DIR/worker.lock"
mkdir -p -- "$GLOBAL_LOCK_DIR"
exec 8>"$GLOBAL_LOCK_FILE"
if ! flock -n 8; then
echo "NC Connector: ein Lauf ist bereits aktiv."
exit 0
fi
shopt -s nullglob
if [[ ! "$TARGET_CONNECTION_ID" =~ ^[0-9]+$ ]]; then
echo "NC Connector: ungültige Ziel-Verbindungs-ID: $TARGET_CONNECTION_ID" >&2
exit 1
fi
read_config_value() {
local key="$1"
local file="$2"
@@ -33,35 +17,103 @@ read_config_value() {
printf '%s' "$value"
}
compact_text() {
printf '%s\n' "$1" | tail -n 20 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
}
read_webdav_failure_detail() {
local config="$1"
local captured_output="${2:-}"
local connection_id piwigo_root status_file status_detail
connection_id="$(read_config_value CONNECTION_ID "$config")"
piwigo_root="$(read_config_value PIWIGO_ROOT "$config")"
[[ -n "$piwigo_root" ]] || piwigo_root="/var/www/piwigo"
status_detail=""
if [[ -n "$connection_id" ]]; then
status_file="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status/connection-${connection_id}.json"
if [[ -r "$status_file" ]]; then
status_detail="$(php -r '
$file = $argv[1];
$data = json_decode((string)@file_get_contents($file), true);
if (!is_array($data)) exit(0);
$message = trim((string)($data["message"] ?? ""));
$detail = trim((string)($data["error_detail"] ?? ""));
$parts = array();
if ($message !== "") $parts[] = $message;
if ($detail !== "") $parts[] = $detail;
if ($parts) {
$text = preg_replace("/\\s+/u", " ", implode(" - ", $parts));
echo trim((string)$text);
}
' "$status_file")"
fi
fi
if [[ -n "$captured_output" ]]; then
captured_output="$(compact_text "$captured_output")"
fi
if [[ -n "$status_detail" && -n "$captured_output" ]]; then
printf '%s' "$status_detail | Prozessausgabe: $captured_output"
elif [[ -n "$status_detail" ]]; then
printf '%s' "$status_detail"
elif [[ -n "$captured_output" ]]; then
printf '%s' "$captured_output"
elif [[ -z "$connection_id" ]]; then
printf '%s' 'WebDAV-Prozess wurde gestartet, aber die Verbindungs-ID fehlt in der Runtime-Konfiguration.'
else
printf '%s' "WebDAV-Prozess fuer Verbindung ${connection_id} endete mit Fehler, lieferte aber weder Statusdatei noch Prozessausgabe."
fi
}
write_route_status() {
local route="$1"
local label="$2"
local detail="$3"
local success="$4"
local fallback_used="$4"
local success="$5"
[[ -n "${ROUTE_STATUS_FILE:-}" ]] || return 0
mkdir -p -- "$(dirname -- "$ROUTE_STATUS_FILE")"
php -r '
$payload = array(
"timestamp" => time(),
"route" => (string)$argv[2],
"label" => (string)$argv[3],
"detail" => (string)$argv[4],
"fallback_used" => false,
"success" => $argv[5] === "1"
"fallback_used" => $argv[5] === "1",
"success" => $argv[6] === "1"
);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if (!is_string($json) || file_put_contents($argv[1], $json.PHP_EOL, LOCK_EX) === false) exit(1);
if (!is_string($json) || file_put_contents($argv[1], $json.PHP_EOL, LOCK_EX) === false) {
fwrite(STDERR, "Route-Status konnte nicht geschrieben werden.\n");
exit(1);
}
@chmod($argv[1], 0644);
' "$ROUTE_STATUS_FILE" "$route" "$label" "$detail" "$success"
' "$ROUTE_STATUS_FILE" "$route" "$label" "$detail" "$fallback_used" "$success"
}
if [[ "$NATIVE_MODE" != "1" ]]; then
php "$SCRIPT_DIR/reconcile.php"
if ! php "$SCRIPT_DIR/reconcile.php"; then
echo "NC Connector: gespeicherte lokale Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2
exit 1
fi
php "$SCRIPT_DIR/reconcile-webdav.php"
php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"
if [[ "$NATIVE_MODE" != "1" ]]; then
php "$SCRIPT_DIR/cleanup-stale.php"
if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then
echo "NC Connector: WebDAV-Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2
exit 1
fi
if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then
echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2
exit 1
fi
if ! php "$SCRIPT_DIR/cleanup-stale.php"; then
echo "NC Connector: verwaiste Laufzeitdateien konnten nicht bereinigt werden." >&2
exit 1
fi
configs=("$CONFIG_DIR"/connection-*.conf)
@@ -75,120 +127,115 @@ fi
route_piwigo_root=""
for candidate in "${webdav_configs[@]}" "${configs[@]}"; do
[[ -f "$candidate" ]] || continue
candidate_id="$(read_config_value CONNECTION_ID "$candidate")"
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$candidate_id" != "$TARGET_CONNECTION_ID" ]]; then
continue
fi
route_piwigo_root="$(read_config_value PIWIGO_ROOT "$candidate")"
[[ -n "$route_piwigo_root" ]] && break
done
[[ -n "$route_piwigo_root" ]] || route_piwigo_root="$PIWIGO_ROOT_DEFAULT"
[[ -n "$route_piwigo_root" ]] || route_piwigo_root="/var/www/piwigo"
ROUTE_STATUS_FILE="${route_piwigo_root%/}/_data/bratonien-tools/nc-connector-status/route-status.json"
failure_count=0
webdav_count=0
local_count=0
summary_parts=()
matched_count=0
webdav_success=0
webdav_failed=0
webdav_failure_detail=""
for config in "${webdav_configs[@]}"; do
[[ -f "$config" ]] || continue
name="$(basename "$config")"
connection_id="$(read_config_value CONNECTION_ID "$config")"
if [[ ! "$connection_id" =~ ^[0-9]+$ ]] || [[ "$connection_id" -lt 1 ]]; then
echo "NC Connector: $name besitzt keine gueltige Verbindungs-ID." >&2
failure_count=$((failure_count + 1))
summary_parts+=("$name: ungueltige Verbindungs-ID")
continue
fi
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$connection_id" != "$TARGET_CONNECTION_ID" ]]; then
continue
fi
matched_count=$((matched_count + 1))
webdav_count=$((webdav_count + 1))
echo "NC Connector WebDAV #$connection_id: $name"
output=""
if output="$(env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh" 2>&1)"; then
[[ -z "$output" ]] || printf '%s\n' "$output"
summary_parts+=("WebDAV #$connection_id erfolgreich")
else
code=$?
[[ -z "$output" ]] || printf '%s\n' "$output" >&2
failure_count=$((failure_count + 1))
summary_parts+=("WebDAV #$connection_id fehlgeschlagen (Exit $code)")
fi
done
if [[ "$NATIVE_MODE" != "1" ]]; then
for config in "${configs[@]}"; do
[[ -f "$config" ]] || continue
if [[ ${#webdav_configs[@]} -eq 0 ]]; then
webdav_failed=1
webdav_failure_detail="Keine WebDAV-Runtime-Verbindung ist konfiguriert. Es existiert aktuell nur die Legacy-Verbindung; deshalb kann WebDAV nicht primaer laufen."
echo "NC Connector: $webdav_failure_detail" >&2
else
for config in "${webdav_configs[@]}"; do
name="$(basename "$config")"
connection_id="0"
if [[ "$name" =~ ^connection-([0-9]+)\.conf$ ]]; then
connection_id="${BASH_REMATCH[1]}"
fi
if [[ "$connection_id" -lt 1 ]]; then
echo "NC Connector: $name besitzt keine gueltige Verbindungs-ID." >&2
failure_count=$((failure_count + 1))
summary_parts+=("$name: ungueltige Verbindungs-ID")
continue
fi
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$connection_id" != "$TARGET_CONNECTION_ID" ]]; then
continue
fi
matched_count=$((matched_count + 1))
piwigo_root="$(read_config_value PIWIGO_ROOT "$config")"
[[ -n "$piwigo_root" ]] || piwigo_root="$PIWIGO_ROOT_DEFAULT"
tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status"
if [[ -f "$tombstone_dir/deleted-$connection_id" ]]; then
echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt."
rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \
"$CONFIG_DIR/connection-$connection_id.db-password" \
"$CONFIG_DIR/connection-$connection_id.piwigo-password" \
"$CONFIG_DIR/connection-$connection_id.storages.tsv" \
"$CONFIG_DIR/connection-$connection_id.roots.tsv"
rm -f -- "$tombstone_dir/deleted-$connection_id"
continue
fi
local_count=$((local_count + 1))
echo "NC Connector Local #$connection_id: $name"
if env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
summary_parts+=("Local #$connection_id erfolgreich")
echo "NC Connector WebDAV primaer: $name"
webdav_output=""
if webdav_output="$(env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh" 2>&1)"; then
webdav_success=1
[[ -z "$webdav_output" ]] || printf '%s\n' "$webdav_output"
else
failure_count=$((failure_count + 1))
summary_parts+=("Local #$connection_id fehlgeschlagen")
webdav_exit=$?
webdav_failed=1
[[ -z "$webdav_output" ]] || printf '%s\n' "$webdav_output" >&2
webdav_failure_detail="$(read_webdav_failure_detail "$config" "$webdav_output")"
webdav_failure_detail="Exit-Code ${webdav_exit}: ${webdav_failure_detail}"
echo "NC Connector: WebDAV-Lauf fehlgeschlagen: $webdav_failure_detail" >&2
echo "NC Connector: Legacy-Fallback bleibt verfuegbar." >&2
fi
done
fi
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$matched_count" -eq 0 ]]; then
write_route_status "failed" "FEHLER - Verbindung #$TARGET_CONNECTION_ID" "Keine Laufzeitkonfiguration für Verbindung #$TARGET_CONNECTION_ID gefunden." "0"
echo "NC Connector: keine Laufzeitkonfiguration für Verbindung #$TARGET_CONNECTION_ID gefunden." >&2
exit 1
fi
summary_detail="$(IFS='; '; printf '%s' "${summary_parts[*]}")"
[[ -n "$summary_detail" ]] || summary_detail="Keine Verbindung wurde ausgefuehrt."
if [[ "$failure_count" -eq 0 ]]; then
if [[ "$webdav_count" -gt 0 && "$local_count" -gt 0 ]]; then
route="mixed"
label="WebDAV + Local"
elif [[ "$webdav_count" -gt 0 ]]; then
route="webdav"
label="WebDAV"
else
route="local"
label="Local"
fi
write_route_status "$route" "$label" "$summary_detail" "1"
echo "NC Connector: angeforderte Verbindung wurde erfolgreich verarbeitet."
if [[ "$webdav_success" -eq 1 ]]; then
write_route_status \
"webdav" \
"WebDAV (primaer)" \
"WebDAV erfolgreich. Legacy-Fallback wurde in diesem Lauf nicht ausgefuehrt." \
"0" \
"1"
echo "NC Connector: WebDAV erfolgreich; Legacy-Verbindung wird in diesem Lauf nicht ausgefuehrt."
exit 0
fi
write_route_status "failed" "FEHLER - angeforderte Verbindung" "$summary_detail" "0"
echo "NC Connector: die angeforderte Verbindung ist fehlgeschlagen." >&2
if [[ ${#configs[@]} -eq 0 ]]; then
[[ -n "$webdav_failure_detail" ]] || webdav_failure_detail="WebDAV ist fehlgeschlagen; Ursache konnte nicht ermittelt werden."
write_route_status \
"failed" \
"FEHLER - kein Datenweg" \
"WebDAV-Fehler: $webdav_failure_detail Keine Legacy-Fallback-Verbindung vorhanden." \
"0" \
"0"
echo "NC Connector: WebDAV ist fehlgeschlagen und es ist keine Legacy-Fallback-Verbindung vorhanden." >&2
exit 1
fi
echo "NC Connector: kein erfolgreicher WebDAV-Lauf; Legacy-Fallback wird ausgefuehrt."
legacy_result=0
for config in "${configs[@]}"; do
name="$(basename "$config")"
connection_id=""
if [[ "$name" =~ ^connection-([0-9]+)\.conf$ ]]; then
connection_id="${BASH_REMATCH[1]}"
fi
piwigo_root="$(read_config_value PIWIGO_ROOT "$config")"
[[ -n "$piwigo_root" ]] || piwigo_root="/var/www/piwigo"
tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status"
if [[ -n "$connection_id" && -f "$tombstone_dir/deleted-$connection_id" ]]; then
echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt."
rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \
"$CONFIG_DIR/connection-$connection_id.db-password" \
"$CONFIG_DIR/connection-$connection_id.piwigo-password" \
"$CONFIG_DIR/connection-$connection_id.storages.tsv" \
"$CONFIG_DIR/connection-$connection_id.roots.tsv"
rm -f -- "$tombstone_dir/deleted-$connection_id"
continue
fi
echo "NC Connector Legacy-Fallback: $name"
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
legacy_result=1
fi
done
if [[ "$legacy_result" -eq 0 ]]; then
[[ -n "$webdav_failure_detail" ]] || webdav_failure_detail="WebDAV war nicht erfolgreich; Ursache konnte nicht ermittelt werden."
write_route_status \
"legacy_fallback" \
"LEGACY-FALLBACK AKTIV" \
"WebDAV-Fehler: $webdav_failure_detail Legacy-Fallback wurde erfolgreich ausgefuehrt." \
"1" \
"1"
echo "NC Connector: Legacy-Fallback erfolgreich."
exit 0
fi
[[ -n "$webdav_failure_detail" ]] || webdav_failure_detail="WebDAV war nicht erfolgreich; Ursache konnte nicht ermittelt werden."
write_route_status \
"failed" \
"FEHLER - WebDAV und Fallback" \
"WebDAV-Fehler: $webdav_failure_detail Legacy-Fallback ist ebenfalls fehlgeschlagen." \
"1" \
"0"
if [[ "$webdav_failed" -eq 1 ]]; then
echo "NC Connector: WebDAV und Legacy-Fallback sind fehlgeschlagen." >&2
fi
exit 1

View File

@@ -59,6 +59,10 @@ for target in (status_file, public_file):
PY
}
compact_output() {
tail -n 12 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
}
failure() {
local code="$1" command="$2" line="$3"
trap - ERR
@@ -75,57 +79,43 @@ done < "$WEBDAV_ROOTS_FILE"
[[ ${#ROOT_ARGS[@]} -gt 0 ]] || { write_status error "Keine WebDAV-Wurzeln konfiguriert"; exit 1; }
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" \
--password-file "$WEBDAV_PASSWORD_FILE" \
"${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
PREVIEW_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-preview/connection-$CONNECTION_ID"
PREVIEW_OUTPUT=""
PREVIEW_EXIT=0
if PREVIEW_OUTPUT="$(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" 2>&1)"; then
PREVIEW_EXIT=0
else
PREVIEW_EXIT=$?
fi
[[ -z "$PREVIEW_OUTPUT" ]] || printf '%s\n' "$PREVIEW_OUTPUT"
if [[ "$PREVIEW_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PREVIEW_EXIT"
if [[ -n "$PREVIEW_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PREVIEW_OUTPUT" | compact_output)"
fi
write_status error "WebDAV-Vorschaubilder konnten beim Einlesen nicht erzeugt werden" "$DETAIL"
exit "$PREVIEW_EXIT"
fi
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
PIWIGO_OUTPUT=""
@@ -143,7 +133,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PIWIGO_EXIT"
if [[ -n "$PIWIGO_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | tail -n 12 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//')"
DETAIL+="; Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | compact_output)"
fi
if grep -qi 'Invalid username/password' <<<"$PIWIGO_OUTPUT"; then
@@ -170,9 +160,28 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
exit "$PIWIGO_EXIT"
fi
DERIVATIVE_OUTPUT=""
DERIVATIVE_EXIT=0
if DERIVATIVE_OUTPUT="$(php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID" 2>&1)"; then
DERIVATIVE_EXIT=0
else
DERIVATIVE_EXIT=$?
fi
[[ -z "$DERIVATIVE_OUTPUT" ]] || printf '%s\n' "$DERIVATIVE_OUTPUT"
if [[ "$DERIVATIVE_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $DERIVATIVE_EXIT"
if [[ -n "$DERIVATIVE_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$DERIVATIVE_OUTPUT" | compact_output)"
fi
write_status error "Piwigo-Derivate für WebDAV-Bilder konnten nicht erzeugt werden" "$DETAIL"
exit "$DERIVATIVE_EXIT"
fi
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \
"WebDAV eingelesen und Piwigo synchronisiert" \
"WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" \
"" \
"api" \
"ok" \
@@ -181,7 +190,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, Piwigo über Fallback synchronisiert und Derivate erzeugt" \
"" \
"fallback" \
"not_used" \
@@ -189,8 +198,8 @@ 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, Piwigo synchronisiert und Derivate erzeugt"
fi
else
write_status ok "WebDAV eingelesen; Piwigo-Synchronisierung ist für diese Verbindung deaktiviert"
write_status ok "WebDAV eingelesen und Vorschaubilder erzeugt; Registrierung erfolgt über den bestehenden produktiven Piwigo-Sync"
fi

View File

@@ -9,7 +9,13 @@
.bratonien-tab-panel { width:100%; max-width:none; margin-left:0 !important; margin-right:0 !important; }
.bratonien-tab-panel[hidden] { display:none !important; }
.bratonien-card { margin-left:0; margin-right:0; }
@media (max-width:760px) { .bratonien-tabs{gap:5px}.bratonien-tab{border-bottom:1px solid rgba(255,255,255,.16);border-radius:4px;margin-bottom:0;padding:9px 11px} }
.bratonien-edit-dialog { width:min(920px,calc(100vw - 3rem)); max-height:88vh; overflow:auto; background:#444; color:inherit; border:1px solid #777; border-radius:4px; padding:0; box-shadow:0 18px 60px rgba(0,0,0,.55); }
.bratonien-edit-dialog::backdrop { background:rgba(0,0,0,.55); }
.bratonien-edit-dialog__body { padding:1.25rem 1.5rem; }
.bratonien-storage-row { display:grid; grid-template-columns:minmax(120px,.7fr) minmax(150px,1fr) minmax(240px,1.5fr) auto; gap:.5rem; align-items:end; margin:.5rem 0; }
.bratonien-storage-row label { display:flex; flex-direction:column; gap:.25rem; }
.bratonien-storage-row input { width:100%; box-sizing:border-box; }
@media (max-width:760px) { .bratonien-tabs{gap:5px}.bratonien-tab{border-bottom:1px solid rgba(255,255,255,.16);border-radius:4px;margin-bottom:0;padding:9px 11px}.bratonien-storage-row{grid-template-columns:1fr} }
</style>
<div id="bratonien-tabs-anchor"></div>
<script>
@@ -53,15 +59,19 @@
function token(){var input=section.querySelector('input[name="pwg_token"]');return input?input.value:'';}
function setOpen(value){try{if(value)sessionStorage.setItem(storageKey,'1');else sessionStorage.removeItem(storageKey);}catch(e){}}
function setMode(value){try{if(value)sessionStorage.setItem(modeKey,value);else sessionStorage.removeItem(modeKey);}catch(e){}}
function mode(){try{return sessionStorage.getItem(modeKey)||'';}catch(e){return '';}}
function clearMode(){try{sessionStorage.removeItem(modeKey);}catch(e){}}
function applyMode(){
if(!dialog)return;
var current=mode();
var heading=dialog.querySelector('h4');
var finish=dialog.querySelector('button[value="nc_connector_wizard_finish"]');
if(mode()==='edit'){
if(current==='edit'){
if(heading)heading.textContent='Verbindung bearbeiten';
if(finish)finish.textContent='Änderungen speichern';
}else if(current==='migrate'){
if(heading)heading.textContent='Auf WebDAV migrieren';
if(finish)finish.textContent='Migration starten';
}else{
if(heading)heading.textContent='Neue Verbindung';
if(finish)finish.textContent='Verbindung anlegen';
@@ -80,13 +90,13 @@
return fetch(window.location.href,{method:'POST',credentials:'same-origin',cache:'no-store',headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:body.toString()});
}
function closeAfterReset(){
if(resetBusy)return;resetBusy=true;setOpen(false);setMode('');
if(resetBusy)return;resetBusy=true;setOpen(false);clearMode();
resetServer().catch(function(){}).finally(function(){resetBusy=false;if(dialog){if(typeof dialog.close==='function'&&dialog.open)dialog.close();else dialog.removeAttribute('open');}});
}
if(openButton){
openButton.addEventListener('click',function(event){
event.preventDefault();event.stopImmediatePropagation();setMode('');showWizard();
event.preventDefault();event.stopImmediatePropagation();clearMode();showWizard();
},true);
}
if(closeButton){
@@ -95,33 +105,111 @@
if(dialog){
dialog.addEventListener('cancel',function(event){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();},true);
dialog.addEventListener('click',function(event){if(event.target===dialog){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();}},true);
[].slice.call(dialog.querySelectorAll('form[data-bratonien-wizard-form]')).forEach(function(form){
form.addEventListener('submit',function(event){
var submitter=event.submitter;
setOpen(true);
if(submitter&&submitter.hasAttribute('data-bratonien-wizard-end')){
setOpen(false);
if(submitter.value==='nc_connector_wizard_reset'||submitter.value==='nc_connector_wizard_finish')setMode('');
if(submitter.value==='nc_connector_wizard_reset')clearMode();
}
},true);
});
[].slice.call(section.querySelectorAll('form')).forEach(function(form){
var editButton=form.querySelector('button[value="nc_connector_edit_start"]');
if(!editButton)return;
form.addEventListener('submit',function(event){
if(event.submitter&&event.submitter.value==='nc_connector_edit_start'){
setMode('edit');setOpen(true);
}
},true);
});
try{if(sessionStorage.getItem(storageKey)==='1')showWizard();}catch(e){}
applyMode();
}
}
function initNCConnectionEditing(){
var section=document.getElementById('nc-connector');if(!section)return;
var modeKey='bratonienNcWizardMode';
var pwgTokenInput=section.querySelector('input[name="pwg_token"]');
var pwgToken=pwgTokenInput?pwgTokenInput.value:'';
function setMode(value){try{sessionStorage.setItem(modeKey,value);}catch(e){}}
function makePostButton(label,tool,id,modeValue){
var form=document.createElement('form');form.method='post';form.style.display='inline';
var token=document.createElement('input');token.type='hidden';token.name='pwg_token';token.value=pwgToken;form.appendChild(token);
var connection=document.createElement('input');connection.type='hidden';connection.name='connection_id';connection.value=String(id);form.appendChild(connection);
var button=document.createElement('button');button.type='submit';button.className='buttonLike';button.name='bratonien_tool';button.value=tool;button.textContent=label;
if(modeValue)form.addEventListener('submit',function(){setMode(modeValue);});
form.appendChild(button);return form;
}
function escapeHtml(value){return String(value==null?'':value).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c];});}
function localEditorDialog(){
var dialog=document.getElementById('bratonien-nc-local-edit-dialog');
if(dialog)return dialog;
dialog=document.createElement('dialog');dialog.id='bratonien-nc-local-edit-dialog';dialog.className='bratonien-edit-dialog';
dialog.innerHTML='<div class="bratonien-edit-dialog__body"><div style="display:flex;align-items:center;justify-content:space-between;gap:1rem"><div><h4 style="margin:0">Verbindung bearbeiten</h4><p class="bratonien-base-note" style="margin:.35rem 0 0">Bestehende Legacy-Verbindung. Änderungen werden in derselben Verbindung gespeichert.</p></div><button type="button" class="buttonLike" data-local-edit-close>Schließen</button></div><div data-local-edit-content style="margin-top:1rem"></div></div>';
document.body.appendChild(dialog);
dialog.querySelector('[data-local-edit-close]').addEventListener('click',function(){dialog.close();});
dialog.addEventListener('click',function(event){if(event.target===dialog)dialog.close();});
return dialog;
}
function storageRow(storage){
return '<div class="bratonien-storage-row" data-storage-row>'+
'<label>Storage-ID<input name="nc_storage_id[]" value="'+escapeHtml(storage.storage_id||'')+'" required></label>'+
'<label>Quellordner<input name="nc_source_prefix[]" value="'+escapeHtml(storage.source_prefix||'')+'" placeholder="optional"></label>'+
'<label>Lokaler Speicherpfad<input name="nc_local_mount[]" value="'+escapeHtml(storage.local_mount||'')+'" required></label>'+
'<button type="button" class="buttonLike" data-remove-storage>Entfernen</button></div>';
}
function openLocalEditor(id){
var dialog=localEditorDialog();
var content=dialog.querySelector('[data-local-edit-content]');
content.innerHTML='<p class="bratonien-base-note">Verbindung wird geladen …</p>';
if(typeof dialog.showModal==='function')dialog.showModal();else dialog.setAttribute('open','open');
fetch('plugins/bratonien_tools/nc-connector-edit-data.php?connection_id='+encodeURIComponent(id)+'&_='+Date.now(),{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
.then(function(response){return response.json().then(function(data){if(!response.ok)throw new Error(data.error||('HTTP '+response.status));return data;});})
.then(function(data){
if(data.adapter!=='local')throw new Error('Diese Verbindung ist keine Legacy-Verbindung.');
var legacy=data.legacy||{};var storages=Array.isArray(legacy.storages)?legacy.storages:[];
var rows=storages.map(storageRow).join('');
if(!rows)rows=storageRow({});
content.innerHTML='<form method="post" data-local-edit-form>'+
'<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'+
'<input type="hidden" name="connection_id" value="'+escapeHtml(data.id)+'">'+
'<div class="bratonien-form-grid">'+
'<label class="bratonien-label">Name</label><input name="connection_name" value="'+escapeHtml(data.name)+'" required>'+
'<label class="bratonien-label">Datenbank-Server</label><input name="nc_host" value="'+escapeHtml(legacy.host)+'" required>'+
'<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="'+escapeHtml(legacy.port||5432)+'" required>'+
'<label class="bratonien-label">Datenbank</label><input name="nc_database" value="'+escapeHtml(legacy.database)+'" required>'+
'<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" value="'+escapeHtml(legacy.user)+'" required>'+
'<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" placeholder="leer = unverändert">'+
'</div>'+
'<h5 style="margin-top:1rem">Speicherorte</h5><p class="bratonien-base-note">Jeder Speicherort wird einzeln bearbeitet. Es ist kein Pipe-/Textformat erforderlich.</p><div data-storage-list>'+rows+'</div><button type="button" class="buttonLike" data-add-storage>Speicherort hinzufügen</button>'+
'<details style="margin-top:1rem"><summary>Erweiterte Legacy-Einstellungen</summary><div class="bratonien-form-grid" style="margin-top:.75rem">'+
'<label class="bratonien-label">Source-View</label><input name="nc_source_view" value="'+escapeHtml(legacy.source_view)+'" required>'+
'<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" value="'+escapeHtml(legacy.activity_view)+'" required>'+
'<label class="bratonien-label">Piwigo-Galerieordner</label><input name="nc_gallery_root" value="'+escapeHtml(legacy.gallery_root)+'" required>'+
'<label class="bratonien-label">Ruhezeit (Sek.)</label><input name="nc_quiet_seconds" type="number" min="0" value="'+escapeHtml(legacy.quiet_seconds)+'">'+
'<label class="bratonien-label">Maximale Wartezeit (Sek.)</label><input name="nc_max_wait_seconds" type="number" min="60" value="'+escapeHtml(legacy.max_wait_seconds)+'">'+
'<label class="bratonien-label">Vollprüfung nach (Sek.)</label><input name="nc_full_sync_seconds" type="number" min="300" value="'+escapeHtml(legacy.full_sync_seconds)+'">'+
'</div></details>'+
'<div class="bratonien-actions" style="margin-top:1rem"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_local">Änderungen speichern</button><button class="buttonLike" type="button" data-local-edit-cancel>Abbrechen</button></div></form>';
var form=content.querySelector('[data-local-edit-form]');
form.querySelector('[data-local-edit-cancel]').addEventListener('click',function(){dialog.close();});
form.querySelector('[data-add-storage]').addEventListener('click',function(){form.querySelector('[data-storage-list]').insertAdjacentHTML('beforeend',storageRow({}));});
form.addEventListener('click',function(event){var remove=event.target.closest('[data-remove-storage]');if(remove){var row=remove.closest('[data-storage-row]');if(row)row.remove();}});
})
.catch(function(error){content.innerHTML='<p class="bratonien-main-cache__warning"><strong>Bearbeiten nicht möglich:</strong> '+escapeHtml(error.message||String(error))+'</p>';});
}
[].slice.call(section.querySelectorAll('button[value="nc_connector_edit_start"]')).forEach(function(button){var form=button.closest('form');if(form)form.remove();});
[].slice.call(section.querySelectorAll('button[value="nc_connector_delete"]')).forEach(function(deleteButton){
var deleteForm=deleteButton.closest('form');var card=deleteButton.closest('details');if(!deleteForm||!card)return;
var idInput=deleteForm.querySelector('input[name="connection_id"]');if(!idInput)return;var id=idInput.value;
var actions=deleteForm.parentElement;if(!actions)return;
var text=card.textContent||'';var isLocal=text.indexOf('bestehende Legacy-Konfiguration')!==-1;
if(isLocal){
var edit=document.createElement('button');edit.type='button';edit.className='buttonLike';edit.textContent='Bearbeiten';edit.addEventListener('click',function(){openLocalEditor(id);});actions.insertBefore(edit,deleteForm);
actions.insertBefore(makePostButton('Auf WebDAV migrieren','nc_connector_migrate_start',id,'migrate'),deleteForm);
}else{
actions.insertBefore(makePostButton('Bearbeiten','nc_connector_edit_start',id,'edit'),deleteForm);
}
});
}
function initNCConnectorPolling(){
var section=document.getElementById('nc-connector');if(!section)return;
var endpoint='plugins/bratonien_tools/nc-connector-status.php';
@@ -174,7 +262,7 @@
poll();schedule();
}
function initAll(){initBratonienTabs();initNCWizardLifecycle();initNCConnectorPolling();}
function initAll(){initBratonienTabs();initNCWizardLifecycle();initNCConnectionEditing();initNCConnectorPolling();}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initAll);else initAll();
})();
</script>

View File

@@ -8,7 +8,6 @@ if (!defined('BRATONIEN_TOOLS_PATH'))
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_transport.inc.php');
function bratonien_tools_webdav_image_abort($status, $message)
{
@@ -52,18 +51,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 +120,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.6.1',
CURLOPT_HEADERFUNCTION => function($ch, $line)
{
$length = strlen($line);
@@ -160,7 +147,6 @@ $options = array(
return strlen($data);
},
);
bratonien_tools_nc_transport_apply_curl($options, $url);
if (!empty($_SERVER['HTTP_RANGE']))
{
$range = trim((string)$_SERVER['HTTP_RANGE']);