Compare commits

...

45 Commits

Author SHA1 Message Date
Terranom674
0a300b6cbf Show stored Piwigo fallback in connector UI 2026-08-17 21:46:32 +02:00
Terranom674
4be078fe07 Bump version to 0.9.3.20 2026-08-17 21:45:59 +02:00
Terranom674
83e71316f0 Expose stored fallback status in connector overview 2026-08-17 21:45:24 +02:00
Terranom674
44f6487f94 Bump Bratonien Tools to 0.9.3.19 2026-08-17 21:43:40 +02:00
Terranom674
3f5606035e Handle direct XML payloads in connector runtime 2026-08-17 21:43:23 +02:00
Terranom674
f16b01ce18 Fix XML API payload handling for Piwigo responses 2026-08-17 21:42:54 +02:00
Terranom674
25650ef9e8 Bump Bratonien Tools to 0.9.3.18 2026-08-17 21:38:30 +02:00
Terranom674
8216a8c508 Accept XML responses in connector runtime 2026-08-17 21:38:15 +02:00
Terranom674
9ae3c79a0b Accept Piwigo XML API responses 2026-08-17 21:37:46 +02:00
Terranom674
a7a7214fda Document API-first NC connector auth in 0.9.3.17 2026-08-17 21:34:27 +02:00
Terranom674
a4083ee15d Use API-first connection creation 2026-08-17 21:33:50 +02:00
Terranom674
e17c45760a Make new NC connections API-first with optional login fallback 2026-08-17 21:33:32 +02:00
Terranom674
983af29aa5 Force private album default during productive API sync 2026-08-17 21:33:06 +02:00
Terranom674
04f7ef4758 Expose API-first auth and fallback controls in NC Connector 2026-08-17 21:32:48 +02:00
Terranom674
e25e519e7a Run productive API sync in Piwigo admin context 2026-08-17 21:32:09 +02:00
Terranom674
16e4c334bb Use API-first Piwigo sync runtime with fallback 2026-08-17 21:31:40 +02:00
Terranom674
0a341385fc Add API-first Piwigo sync runtime with login fallback 2026-08-17 21:31:25 +02:00
Terranom674
ad09e42c40 Store verified Piwigo API credentials for connector runtime 2026-08-17 21:31:00 +02:00
Terranom674
a57be97f48 Register NC API and fallback credential actions 2026-08-17 21:30:44 +02:00
Terranom674
61a3d8daeb Add NC API and fallback credential management 2026-08-17 21:30:29 +02:00
Terranom674
8e403ccd46 Register productive NC API sync and bump to 0.9.3.17 2026-08-17 21:30:04 +02:00
Terranom674
d6fbd0ff6d Add version-gated productive NC sync API endpoint 2026-08-17 21:29:48 +02:00
Terranom674
e2c46f21df Update current NC Connector development status 2026-08-17 21:22:04 +02:00
Terranom674
b91276f752 Update README for current NC Connector state 2026-08-17 21:21:41 +02:00
Terranom674
e28daf5e24 Import connector albums as private by default 2026-08-17 21:08:07 +02:00
Terranom674
76a48b89bd Mark connector admin sync requests for private album imports 2026-08-17 21:07:53 +02:00
Terranom674
b3fc198aea Bump version to 0.9.3.15 2026-08-17 21:00:50 +02:00
Terranom674
b834ffbdbc Fix SQL query strings in orphan sync 2026-08-17 21:00:38 +02:00
Terranom674
ebd3cc16f9 Document orphan synchronization in 0.9.3.14 2026-08-17 20:52:20 +02:00
Terranom674
0d02141c16 Sync NC root files as Piwigo orphans 2026-08-17 20:51:59 +02:00
Terranom674
fd8fa89ee0 Register NC orphan sync and bump version 2026-08-17 20:51:45 +02:00
Terranom674
c1ef3c5a2a Add NC root orphan synchronization endpoint 2026-08-17 20:51:32 +02:00
Terranom674
5d67b7e3a6 Document 0.9.3.13 legacy config compatibility 2026-08-17 20:20:07 +02:00
Terranom674
4d7a85436f Bump Bratonien Tools to 0.9.3.13 2026-08-17 20:19:49 +02:00
Terranom674
025bf3c4f8 Keep legacy connector configs compatible 2026-08-17 20:19:36 +02:00
Terranom674
be47674d20 Document 0.9.3.12 connector reconciliation fix 2026-08-17 20:18:08 +02:00
Terranom674
b56990c827 Bump Bratonien Tools to 0.9.3.12 2026-08-17 20:17:53 +02:00
Terranom674
b67300b8e8 Pass source view to activity gate 2026-08-17 20:17:00 +02:00
Terranom674
a980236cef Detect source share changes in activity gate 2026-08-17 20:16:46 +02:00
Terranom674
da846fd737 Document 0.9.3.11 legacy view compatibility 2026-08-17 20:08:09 +02:00
Terranom674
941e6a375b Bump version to 0.9.3.11 2026-08-17 20:07:52 +02:00
Terranom674
e76b054993 Handle legacy source views while building manifests 2026-08-17 20:07:35 +02:00
Terranom674
7450988ff0 Document version 0.9.3.10 2026-08-17 20:02:16 +02:00
Terranom674
5523c107e9 Bump Bratonien Tools to 0.9.3.10 2026-08-17 20:01:55 +02:00
Terranom674
69a939637c Document current NC Connector development status 2026-08-17 19:46:07 +02:00
17 changed files with 1586 additions and 333 deletions

122
CURRENT_STATUS.md Normal file
View File

@@ -0,0 +1,122 @@
# Aktueller Entwicklungsstand
Stand: 17.08.2026
## Plugin
- Aktuelle Plugin-Version: **0.9.3.17**
- Aktueller Entwicklungsblock: **NC Connector Verbindungsverwaltung / laufende Optimierung**
- NC Connector ist Feature 10 und noch nicht vollständig abgeschlossen.
- Solange dieser Optimierungsblock läuft, bleibt die Version im Bereich `0.9.3.x`.
## Aktueller GitHub-Stand
Der lokale NC Connector ist für den aktuellen Bratonien-Einsatz im bisherigen Login-Weg End-to-End funktionsfähig. Mit `0.9.3.17` beginnt der Umbau auf API-first für die Piwigo-Synchronisierung.
Bereits erfolgreich umgesetzt und getestet:
- Nextcloud-Freigaben vom Typ `folder` und `file` werden aus der Source-View gelesen.
- Ordnerfreigaben werden als Verzeichnisbaum im Shadow Tree gespiegelt.
- Einzeldateifreigaben werden als Symlink direkt im Galerie-Root angelegt und als echte Piwigo-Orphans ohne Albumzuordnung registriert.
- Entfernte Einzeldateifreigaben werden beim normalen Connector-Lauf aus Shadow Tree und Piwigo entfernt.
- Das Nextcloud-Original bleibt bei Löschvorgängen unangetastet.
- Connector-getriggerte neue physische Alben werden standardmäßig privat angelegt.
- Der gemeinsame systemd-Timer `bratonien-nc-connector.timer` läuft produktiv.
Neu in `0.9.3.17`:
- die Piwigo-API ist als bevorzugter Synchronisierungsweg vorgesehen;
- ein erfolgreich geprüfter API-Key wird verschlüsselt in der Piwigo-Konfiguration gespeichert;
- produktive API-Synchronisierung ist ausdrücklich an die freigegebene Piwigo-Version **16.4.0** gebunden;
- neuer Webservice `bratonien.nc.syncProductive` delegiert den freigegebenen produktiven Dateisync an Piwigos Core-Synchronisierung;
- `runtime/lib/piwigo-sync.php` versucht zuerst die API und fällt nur bei nicht nutzbarer API auf Benutzername/Passwort zurück;
- der bisherige Benutzername/Passwort-Zugang kann als verschlüsselter Fallback gespeichert oder gelöscht werden;
- ein Benutzername/Passwort-Fallback kann außerdem einmalig manuell ausgeführt werden, ohne die Zugangsdaten zu speichern;
- neue lokale Verbindungen werden intern als `api-first` angelegt; ein dauerhaft gespeicherter Login-Fallback ist technisch nicht mehr zwingend, sofern ein API-Zugang vorhanden ist.
## Architektur NC Connector
- Nextcloud bleibt die einzige dauerhafte Quelle der Originalbilder.
- Piwigo erhält nur die benötigte Verzeichnis-/Symlink-Struktur und erzeugt daraus seine Derivate und Cache-Dateien.
- Der Connector arbeitet mit einem PostgreSQL-Leser und den Views `piwigo_showcase_sources` und `piwigo_showcase_activity`.
- Laufzeitdaten liegen unter `/var/lib/bratonien-tools/nc-connector/...`.
- Verbindungs-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/connection-*.conf`.
- Der Shadow Tree enthält Verzeichnisse und Symlinks, keine Kopien der Originaldateien.
- Alle aktivierten Verbindungen werden über `runtime/run-all.sh` gemeinsam verarbeitet.
## Nextcloud-Freigabemodell
Der Connector unterstützt komplette Ordner und einzelne Bilder.
- `folder` -> Verzeichnisstruktur spiegeln und über Piwigos Dateisynchronisierung als physische Albumstruktur einlesen;
- `file` -> Symlink direkt im Galerie-Root und anschließende Registrierung als Piwigo-Orphan.
Für einzelne Dateien wird kein künstlicher Unterordner erzeugt.
## Activity-Gate
Das Activity-Gate berücksichtigt neben dem Nextcloud-Aktivitätsstand auch die Signatur der aktuell sichtbaren Freigaben. Weiterhin vorhanden sind Quiet-Time, maximale Wartezeit, periodischer Full-Sync und verbindungsspezifischer State.
## Piwigo-Synchronisation
### API-first
Der normale Runtime-Lauf verwendet ab `0.9.3.17` folgende Priorität:
1. gespeicherter Piwigo-API-Key;
2. bei API-Fehler, fehlender Freigabe für die installierte Piwigo-Version oder nicht vorhandener API: gespeicherter Benutzername/Passwort-Fallback;
3. ohne nutzbaren API- und Fallback-Zugang wird der Piwigo-Sync mit einer klaren Fehlermeldung abgebrochen.
Nach einem Piwigo-Update wird die produktive API nicht automatisch für die neue Version freigeschaltet. Erst nach einem Kompatibilitätstest wird die Versionsfreigabe im Plugin angehoben. Bis dahin bleibt nur der Login-Fallback zulässig.
### Physische Alben
`bratonien.nc.syncProductive` ist aktuell nur für Piwigo **16.4.0** freigegeben. Der Endpoint läuft als Administrator-Webservice und verwendet für den eigentlichen Dateisync Piwigos Core-Synchronisierung. Neue Connector-Alben werden dabei privat angelegt.
### Root-Dateien / Orphans
`bratonien.nc.syncOrphans` verarbeitet direkte Dateien im Galerie-Root separat. Neue Root-Dateien werden als Piwigo-Orphans ohne Albumzuordnung registriert. Entfernte Root-Dateien werden nur aus Piwigo entfernt; das Nextcloud-Original bleibt unberührt.
## Erfolgreiche Live-Tests bis 0.9.3.16
Bestätigt wurden:
- Ordnerfreigaben werden korrekt eingelesen;
- Einzeldateifreigaben werden als `file` erkannt und im Galerie-Root verlinkt;
- Orphan-Anlage und Orphan-Löschung funktionieren;
- physische neue Connector-Alben werden privat angelegt;
- der automatische Timer kann regulär laufen.
## Offener Test für 0.9.3.17
Vor Abschluss des API-first-Blocks muss noch live geprüft werden:
1. Plugin-Update auf `0.9.3.17`;
2. API-Key im NC Connector prüfen und speichern;
3. produktiver Lauf verwendet `bratonien.nc.syncProductive` erfolgreich;
4. Ordner- und Orphan-Sync bleiben unverändert korrekt;
5. gespeicherter Login-Fallback übernimmt bei absichtlich nicht nutzbarer API;
6. einmaliger Fallback funktioniert ohne Speicherung;
7. gelöschter Fallback wird vom Runtime-Lauf nicht mehr verwendet.
## Sicherheit / Betriebsmodell
- Nextcloud bleibt Eigentümer der Originaldateien.
- Der Connector löscht keine Originaldateien aus den Storage-Mounts.
- API- und Login-Zugangsdaten werden verschlüsselt gespeichert und nicht in das Repository geschrieben.
- Die API hat Vorrang, ist aber streng versionsgebunden.
- Benutzername/Passwort ist nur Fallback und kann dauerhaft, einmalig oder gar nicht verwendet werden.
- Neue Connector-Alben werden privat angelegt.
## Bekannte offene Punkte
Der NC Connector bleibt im Entwicklungsblock `0.9.3.x`. Nach erfolgreichem API-first-Livetest folgen insbesondere:
- Bereinigung und Vereinfachung der Verbindungsverwaltung im Admin-UI;
- Remote-Nextcloud-Adapter;
- Entscheidung über verbleibende Legacy-/Migrationshilfen;
- weitere Komfortfunktionen für Status, Diagnose und Administration.
## Repository-Fallback
Während der Entwicklung bleibt GitHub das führende Repository. Zusätzlich existiert auf dem privaten Gitea-System ein Pull-Mirror als Ausfall-/Fallback-Ebene.

View File

@@ -4,7 +4,7 @@ Modular aufgebautes Piwigo-Plugin mit erweiterten Werkzeugen fuer Administration
Das Projekt ist aus der Bratonien-Piwigo-Installation entstanden, wird aber bewusst so entwickelt, dass die einzelnen Funktionen moeglichst neutral und auch ausserhalb dieser Installation nutzbar bleiben.
Aktuelle Plugin-Version: **0.14.3**
Aktuelle Plugin-Version: **0.9.3.16**
## Funktionsumfang
@@ -27,13 +27,27 @@ Aktuell vorhanden:
- Verbindungen koennen kontrolliert deaktiviert und danach geloescht werden
- Anzeige von Timer-Status, letztem Lauf, letztem Ergebnis und naechstem geplanten Lauf
- Plugin-eigene Sync-Runtime unter `runtime/`
- Activity-Gate mit Quiet-Time, Max-Wartezeit und periodischem Full-Sync
- Activity-Gate mit Quiet-Time, Max-Wartezeit, Share-Fingerprint und periodischem Full-Sync
- Shadow Tree fuer Piwigo ohne Kopie der Originaldateien
- Unterstuetzung von Nextcloud-Freigaben fuer komplette Ordner und einzelne Dateien
- Ordnerfreigaben werden als physische Piwigo-Alben synchronisiert
- neu importierte Connector-Alben werden direkt als privat angelegt
- einzelne Dateifreigaben werden direkt im Galerie-Root verlinkt und als echte Piwigo-Orphans registriert
- entfernte Einzeldateifreigaben werden beim naechsten Lauf wieder aus Piwigo entfernt, ohne das Original zu loeschen
- Originalbilder bleiben an ihrer Nextcloud-/Storage-Quelle
- lokale Piwigo-Derivate und Caches bleiben davon getrennt und koennen jederzeit neu erzeugt werden
Die Runtime baut aus den freigegebenen Nextcloud-Quellen einen Piwigo-kompatiblen Shadow Tree. Dateien werden dabei nicht in eine zweite permanente Originalbibliothek kopiert, sondern ueber Symlinks auf die vorhandenen Storage-Mounts referenziert.
#### Freigabemodell
Der Connector unterscheidet zwei Quelltypen:
- `folder` - komplette Ordnerfreigabe; der Verzeichnisbaum wird gespiegelt und ueber Piwigos normale Dateisynchronisierung als Albumstruktur eingelesen
- `file` - einzelne Dateifreigabe; die Datei wird direkt als Symlink im Galerie-Root angelegt und separat als Piwigo-Orphan ohne Albumzuordnung registriert
Fuer Einzeldateien wird kein kuenstlicher Unterordner erzeugt. Entfernt Nextcloud eine solche Freigabe, entfernt der Connector den Root-Symlink und anschliessend nur den zugehoerigen Piwigo-Datenbankeintrag. Das Original bleibt unangetastet.
#### Neuinstallation
Eine frische Piwigo-Installation benoetigt keinen vorher vorhandenen `piwigo-sync` mehr.
@@ -203,13 +217,15 @@ Wichtige Bestandteile:
- `include/nc_connector_manage.inc.php` - native Connection-Verwaltung und Verifikation
- `include/nc_connector_takeover.inc.php` - kontrollierte Legacy-Uebergabe
- `include/nc_connector_system.inc.php` - Timer- und Laufzeitstatus ueber alle aktiven Verbindungen
- `include/nc_connector_ws.inc.php` - read-only Paritaets-/Synchronisationspruefung ueber den Piwigo-Webservice
- `include/nc_orphan_ws.inc.php` - produktive Synchronisation einzelner Root-Dateien als Piwigo-Orphans
- `runtime/sync.sh` - Plugin-eigene Sync-Runtime einer Verbindung
- `runtime/run-all.sh` - gemeinsamer Runner fuer alle installierten Verbindungen
- `runtime/lib/activity_gate.py` - Activity-Gate und zeitgesteuerte Reconciliation
- `runtime/lib/build_manifest.py` - Aufloesung der Nextcloud-Quellen auf konfigurierte Storage-Mounts
- `runtime/lib/activity_gate.py` - Activity-Gate, Share-Fingerprint und zeitgesteuerte Reconciliation
- `runtime/lib/build_manifest.py` - Aufloesung von Ordner- und Dateifreigaben auf konfigurierte Storage-Mounts
- `runtime/lib/shadow_tree.py` - Aufbau des Piwigo-kompatiblen Shadow Trees ohne Kopieren der Originale
- `runtime/lib/piwigo-db-check.php` - Konsistenzpruefung vorhandener Piwigo-Alben
- `runtime/lib/piwigo-db-sync.pl` - Ausloesen der Piwigo-Dateisynchronisierung
- `runtime/lib/piwigo-db-sync.pl` - Ausloesen der Piwigo-Dateisynchronisierung und anschliessender Orphan-Abgleich
- `nc-connector-install.php` - native Aktivierung einer verifizierten Verbindung
- `nc-connector-disable.php` - kontrollierte Deaktivierung einer aktiven Verbindung
- `nc-connector-normalize.php` - Ueberfuehrung einer migrierten aktiven Verbindung in den nativen State- und Multi-Connection-Aufbau
@@ -268,6 +284,7 @@ Weitere Schutzmechanismen sind funktionsabhaengig, unter anderem:
- eigene technische Benutzer mit minimalem Albumzugriff fuer Freigaben
- automatische Bereinigung widerrufener und geloeschter Albumfreigaben
- automatischer Erhalt des eigenen Zugriffs beim Umschalten eines Albums auf privat
- Connector-importierte physische Alben werden standardmaessig privat angelegt
- Beibehaltung bestehender Piwigo- und Plugin-Berechtigungen
- kontrollierte Uploadziele und Uploadgrenzen
- kein Zugriff auf Originalbilder beim Leeren des Bildcaches
@@ -281,4 +298,6 @@ Farben, Schatten, Hover-Effekte und individuelles Branding sollten ueber das akt
## Entwicklungsstand
Das Plugin befindet sich weiterhin in aktiver Entwicklung. Der NC Connector ist in Version 0.14.3 von der migrationsorientierten Uebergangsstruktur auf einen nativen Mehrverbindungs-Aufbau erweitert worden. Der Remote-Adapter ist der naechste noch offene Connector-Ausbauschritt.
Das Plugin befindet sich weiterhin in aktiver Entwicklung. Mit Version **0.9.3.16** ist der lokale NC-Connector fuer den aktuellen Bratonien-Einsatz End-to-End funktionsfaehig: Ordner- und Einzeldateifreigaben werden erkannt, der Shadow Tree wird automatisch gepflegt, Piwigo wird synchronisiert, Einzeldateien werden als Orphans verwaltet und der regelmaessige Lauf erfolgt ueber den gemeinsamen systemd-Timer.
Als naechster groesserer Connector-Ausbauschritt bleibt der Remote-Nextcloud-Adapter offen.

View File

@@ -75,6 +75,19 @@ $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();
foreach ($nc_connector['connections'] as &$nc_connection)
{
if (!empty($nc_connection['fallback_stored']))
{
$nc_connection['name'] .= ' · Fallback gespeichert';
$nc_connection['verification_checks'][] = array(
'name' => 'Piwigo-Fallback',
'ok' => true,
'detail' => 'Benutzername/Passwort verschluesselt gespeichert',
);
}
}
unset($nc_connection);
$nc_connector['piwigo_api_test'] = $nc_piwigo_api_test;
$nc_system_defaults = array(
'timer_name' => 'bratonien-nc-connector.timer',

View File

@@ -186,7 +186,7 @@ function bratonien_tools_nc_connector_connections()
{
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
$result = pwg_query("SELECT id, connection_key, name, adapter, enabled, takeover_state, config_json, created, updated FROM `$table` ORDER BY id");
$result = pwg_query("SELECT id, connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated FROM `$table` ORDER BY id");
$connections = array();
while ($row = pwg_db_fetch_assoc($result))
@@ -197,6 +197,24 @@ function bratonien_tools_nc_connector_connections()
$config = array();
}
$verification = isset($config['verification']) && is_array($config['verification']) ? $config['verification'] : array();
$has_fallback = false;
try
{
$plain = bratonien_tools_nc_connector_decrypt_secret($row['secret_blob'] ?? '');
$credentials = json_decode($plain, true);
if (is_array($credentials))
{
$has_fallback = trim((string)($credentials['piwigo_user'] ?? '')) !== ''
&& (string)($credentials['piwigo_password'] ?? '') !== '';
}
}
catch (Throwable $e)
{
$has_fallback = false;
}
unset($row['secret_blob']);
$row['id'] = (int)$row['id'];
$row['enabled'] = (bool)$row['enabled'];
$row['config'] = $config;
@@ -205,6 +223,7 @@ function bratonien_tools_nc_connector_connections()
$row['user'] = isset($config['user']) ? (string)$config['user'] : '';
$row['source_view'] = isset($config['source_view']) ? (string)$config['source_view'] : '';
$row['storage_count'] = isset($config['storages']) && is_array($config['storages']) ? count($config['storages']) : 0;
$row['fallback_stored'] = $has_fallback;
$row['verification'] = $verification;
$row['verified_ok'] = !empty($verification['ok']);
$row['verified_at'] = isset($verification['checked_at']) ? (string)$verification['checked_at'] : '';
@@ -418,7 +437,7 @@ function bratonien_tools_nc_connector_psql($config, $password, $query)
if ($exit !== 0)
{
$detail = $stderr !== '' ? $stderr : 'psql Exit-Code '.$exit;
throw new RuntimeException('PostgreSQL-Pruefung fehlgeschlagen: '.substr($detail, 0, 500));
throw new RuntimeException('PostgreSQL-Abfrage fehlgeschlagen: '.$detail);
}
return $stdout;
@@ -427,272 +446,39 @@ function bratonien_tools_nc_connector_psql($config, $password, $query)
function bratonien_tools_nc_connector_mount_points()
{
$mounts = array();
$lines = @file('/proc/self/mountinfo', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!is_array($lines))
$content = @file_get_contents('/proc/self/mountinfo');
if (!is_string($content))
{
return $mounts;
}
foreach ($lines as $line)
foreach (preg_split('/\r\n|\r|\n/', $content) as $line)
{
if ($line === '') continue;
$parts = explode(' ', $line);
if (count($parts) < 5)
{
continue;
}
$path = str_replace(array('\\040','\\011','\\012','\\134'), array(' ',"\t","\n",'\\'), $parts[4]);
$mounts[$path] = true;
if (count($parts) < 5) continue;
$mount = str_replace(array('\\040','\\011','\\012','\\134'), array(' ',"\t","\n",'\\'), $parts[4]);
$mounts[rtrim($mount, '/')] = true;
}
return $mounts;
}
function bratonien_tools_nc_connector_verify()
{
$id = isset($_POST['connection_id']) ? (int)$_POST['connection_id'] : 0;
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
if ($connection['adapter'] !== 'local')
{
throw new RuntimeException('Diese Verifikation ist derzeit nur fuer lokale Connector-Verbindungen verfuegbar.');
}
$config = $connection['config'];
$required = array('host','port','database','user','source_view');
foreach ($required as $key)
{
if (!isset($config[$key]) || trim((string)$config[$key]) === '')
{
throw new RuntimeException('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.');
}
}
$checks = array();
$ok = true;
$source_count = null;
$password = bratonien_tools_nc_connector_decrypt_secret($connection['secret_blob'] ?? '');
if ($password === '')
{
throw new RuntimeException('Fuer diese Verbindung sind keine Datenbank-Zugangsdaten gespeichert.');
}
try
{
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1');
$checks[] = array('name'=>'PostgreSQL-Verbindung', 'ok'=>true, 'detail'=>'Reader-Anmeldung erfolgreich');
}
catch (Throwable $e)
{
$checks[] = array('name'=>'PostgreSQL-Verbindung', 'ok'=>false, 'detail'=>$e->getMessage());
$ok = false;
}
if ($ok)
{
try
{
$view = bratonien_tools_nc_connector_view_name($config['source_view']);
$value = bratonien_tools_nc_connector_psql($config, $password, 'SELECT COUNT(*) FROM '.$view);
if (!preg_match('/^\d+$/', $value))
{
throw new RuntimeException('Source-View lieferte keinen gueltigen Zaehler.');
}
$source_count = (int)$value;
$checks[] = array('name'=>'Source-View', 'ok'=>true, 'detail'=>$source_count.' Quelle(n) lesbar');
}
catch (Throwable $e)
{
$checks[] = array('name'=>'Source-View', 'ok'=>false, 'detail'=>$e->getMessage());
$ok = false;
}
try
{
$activity = isset($config['activity_view']) ? trim((string)$config['activity_view']) : '';
if ($activity === '')
{
throw new RuntimeException('Keine Activity-View konfiguriert.');
}
$view = bratonien_tools_nc_connector_view_name($activity);
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1 FROM '.$view.' LIMIT 1');
$checks[] = array('name'=>'Activity-View', 'ok'=>true, 'detail'=>'View lesbar');
}
catch (Throwable $e)
{
$checks[] = array('name'=>'Activity-View', 'ok'=>false, 'detail'=>$e->getMessage());
$ok = false;
}
}
$storages = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array();
$mount_points = bratonien_tools_nc_connector_mount_points();
if (count($storages) === 0)
{
$checks[] = array('name'=>'Storage-Mounts', 'ok'=>false, 'detail'=>'Keine Storage-Zuordnung gespeichert');
$ok = false;
}
else
{
foreach ($storages as $index => $storage)
{
$path = isset($storage['local_mount']) ? rtrim((string)$storage['local_mount'], '/') : '';
if ($path === '')
{
$checks[] = array('name'=>'Storage '.($index + 1), 'ok'=>false, 'detail'=>'Kein lokaler Mountpfad gespeichert');
$ok = false;
continue;
}
$exists = is_dir($path);
$readable = $exists && is_readable($path);
$mounted = isset($mount_points[$path]);
$storage_ok = $exists && $readable && $mounted;
$storage_id = isset($storage['storage_id']) ? (string)$storage['storage_id'] : ('#'.($index + 1));
$detail = $storage_id.' -> '.$path;
if (!$exists)
{
$detail .= ' (Pfad fehlt)';
}
elseif (!$readable)
{
$detail .= ' (nicht lesbar)';
}
elseif (!$mounted)
{
$detail .= ' (kein aktiver Mount)';
}
else
{
$detail .= ' (bereit)';
}
$checks[] = array('name'=>'Storage '.($index + 1), 'ok'=>$storage_ok, 'detail'=>$detail);
if (!$storage_ok)
{
$ok = false;
}
}
}
$config['verification'] = array(
'checked_at' => date('Y-m-d H:i:s'),
'ok' => $ok,
'source_count' => $source_count,
'checks' => $checks,
);
$table = bratonien_tools_nc_connector_table();
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Verifikationsergebnis konnte nicht gespeichert werden.');
}
$state = $ok ? 'verified' : 'imported';
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET
takeover_state = '".pwg_db_real_escape_string($state)."',
enabled = 0,
config_json = '".pwg_db_real_escape_string($config_json)."',
updated = '".pwg_db_real_escape_string($now)."'
WHERE id = ".(int)$connection['id']);
if (!$ok)
{
throw new RuntimeException('Connector-Verifikation ist fehlgeschlagen. Die Verbindung bleibt importiert und deaktiviert; Details stehen im NC Connector.');
}
return array(
'message' => 'Connector-Verbindung wurde erfolgreich verifiziert. PostgreSQL, Views und Storage-Mounts sind erreichbar. Der Legacy-Sync bleibt weiterhin aktiv; die Connector-Verbindung bleibt deaktiviert.',
);
}
function bratonien_tools_nc_connector_status()
{
$config_path = '/etc/piwigo-sync/piwigo.conf';
$config_exists = is_file($config_path);
$config_readable = is_readable($config_path);
$config = bratonien_tools_nc_connector_read_config($config_path);
$required = array('NC_DB_HOST', 'NC_DB_PORT', 'NC_DB_NAME', 'NC_DB_USER', 'NC_DB_VIEW');
$missing = array();
foreach ($required as $key)
{
if (!isset($config[$key]) || trim((string)$config[$key]) === '')
{
$missing[] = $key;
}
}
$password_file = isset($config['NC_DB_PASSWORD_FILE']) ? (string)$config['NC_DB_PASSWORD_FILE'] : '';
$status_file = isset($config['STATUS_FILE']) && $config['STATUS_FILE'] !== ''
? (string)$config['STATUS_FILE']
: '/var/lib/piwigo-sync/status.json';
$sync_status = array('available'=>false, 'state'=>'', 'message'=>'', 'timestamp'=>0, 'time_label'=>'');
if (is_readable($status_file))
{
$raw_status = @file_get_contents($status_file);
$decoded = is_string($raw_status) ? json_decode($raw_status, true) : null;
if (is_array($decoded))
{
$timestamp = isset($decoded['timestamp']) ? (int)$decoded['timestamp'] : 0;
$sync_status = array(
'available' => true,
'state' => isset($decoded['state']) ? (string)$decoded['state'] : '',
'message' => isset($decoded['message']) ? (string)$decoded['message'] : '',
'timestamp' => $timestamp,
'time_label' => $timestamp > 0 ? date('d.m.Y H:i:s', $timestamp) : '',
);
}
}
$connections = bratonien_tools_nc_connector_connections();
$bundle_path = bratonien_tools_nc_connector_import_bundle_path();
$helper_path = realpath(BRATONIEN_TOOLS_PATH.'nc-connector-migrate.php');
if ($helper_path === false)
{
$helper_path = BRATONIEN_TOOLS_PATH.'nc-connector-migrate.php';
}
$verified_count = 0;
$legacy_config = '/etc/piwigo-sync/piwigo.conf';
$legacy_present = is_readable($legacy_config);
$active_count = 0;
foreach ($connections as $connection)
{
if ($connection['takeover_state'] === 'verified' || $connection['takeover_state'] === 'active')
{
$verified_count++;
}
if (!empty($connection['enabled'])) $active_count++;
}
return array(
'phase' => $verified_count > 0 ? 'Verifikation' : 'Migration',
'readonly' => true,
'legacy_present' => $config_exists,
'detected' => $config_exists,
'config_path' => $config_path,
'config_exists' => $config_exists,
'config_readable' => $config_readable,
'missing' => $missing,
'host' => isset($config['NC_DB_HOST']) ? (string)$config['NC_DB_HOST'] : '',
'port' => isset($config['NC_DB_PORT']) ? (string)$config['NC_DB_PORT'] : '',
'database' => isset($config['NC_DB_NAME']) ? (string)$config['NC_DB_NAME'] : '',
'user' => isset($config['NC_DB_USER']) ? (string)$config['NC_DB_USER'] : '',
'view' => isset($config['NC_DB_VIEW']) ? (string)$config['NC_DB_VIEW'] : '',
'password_file' => $password_file,
'password_file_exists' => $password_file !== '' && is_file($password_file),
'password_file_readable' => $password_file !== '' && is_readable($password_file),
'gallery_root' => isset($config['GALLERY_ROOT']) ? (string)$config['GALLERY_ROOT'] : '',
'state_dir' => isset($config['STATE_DIR']) ? (string)$config['STATE_DIR'] : '',
'status_file' => $status_file,
'sync_enabled' => isset($config['PIWIGO_SYNC_ENABLED']) && (string)$config['PIWIGO_SYNC_ENABLED'] === '1',
'quiet_seconds' => isset($config['QUIET_SECONDS']) ? (string)$config['QUIET_SECONDS'] : '',
'max_wait_seconds' => isset($config['MAX_WAIT_SECONDS']) ? (string)$config['MAX_WAIT_SECONDS'] : '',
'full_sync_seconds' => isset($config['FULL_SYNC_SECONDS']) ? (string)$config['FULL_SYNC_SECONDS'] : '',
'sync_status' => $sync_status,
'phase' => 'native-runtime',
'legacy_config_path' => $legacy_config,
'legacy_present' => $legacy_present,
'connections' => $connections,
'connection_count' => count($connections),
'verified_count' => $verified_count,
'migration_bundle_available' => is_readable($bundle_path),
'migration_bundle_path' => $bundle_path,
'migration_command' => 'sudo php '.$helper_path,
'active_count' => $active_count,
);
}

View File

@@ -0,0 +1,246 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_api_credentials()
{
global $conf;
$blob = isset($conf['bratonien_nc_piwigo_api']) ? trim((string)$conf['bratonien_nc_piwigo_api']) : '';
if ($blob === '')
{
return array('key_id'=>'', 'key_secret'=>'');
}
try
{
$plain = bratonien_tools_nc_connector_decrypt_secret($blob);
$decoded = json_decode($plain, true);
if (!is_array($decoded))
{
return array('key_id'=>'', 'key_secret'=>'');
}
return array(
'key_id' => (string)($decoded['key_id'] ?? ''),
'key_secret' => (string)($decoded['key_secret'] ?? ''),
);
}
catch (Throwable $e)
{
return array('key_id'=>'', 'key_secret'=>'');
}
}
function bratonien_tools_nc_api_credentials_store($key_id, $key_secret)
{
global $conf;
$key_id = trim((string)$key_id);
$key_secret = trim((string)$key_secret);
if ($key_id === '' || $key_secret === '')
{
throw new RuntimeException('API-Schluessel-ID und Geheimnis muessen angegeben werden.');
}
$payload = json_encode(array(
'v' => 1,
'key_id' => $key_id,
'key_secret' => $key_secret,
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($payload))
{
throw new RuntimeException('API-Zugangsdaten konnten nicht serialisiert werden.');
}
$blob = bratonien_tools_nc_connector_encrypt_secret($payload);
conf_update_param('bratonien_nc_piwigo_api', $blob);
$conf['bratonien_nc_piwigo_api'] = $blob;
}
function bratonien_tools_nc_connector_api_delete()
{
global $conf;
conf_update_param('bratonien_nc_piwigo_api', '');
$conf['bratonien_nc_piwigo_api'] = '';
return array('message'=>'Gespeicherte Piwigo-API-Zugangsdaten wurden geloescht.');
}
function bratonien_tools_nc_connector_auth_credentials($connection)
{
$credentials = bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob'] ?? '');
return array(
'db_password' => (string)($credentials['db_password'] ?? ''),
'piwigo_user' => (string)($credentials['piwigo_user'] ?? ''),
'piwigo_password' => (string)($credentials['piwigo_password'] ?? ''),
);
}
function bratonien_tools_nc_connector_fallback_save()
{
$id = (int)($_POST['connection_id'] ?? 0);
$username = trim((string)($_POST['nc_fallback_user'] ?? ''));
$password = (string)($_POST['nc_fallback_password'] ?? '');
if ($username === '' || $password === '')
{
throw new RuntimeException('Benutzername und Passwort fuer den Fallback muessen angegeben werden.');
}
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
$credentials = bratonien_tools_nc_connector_auth_credentials($connection);
if ($credentials['db_password'] === '')
{
throw new RuntimeException('Das Datenbankpasswort der Verbindung fehlt.');
}
$blob = bratonien_tools_nc_connector_encrypt_credentials(
$credentials['db_password'],
$username,
$password
);
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET secret_blob='".pwg_db_real_escape_string($blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
return array('message'=>'Piwigo-Benutzername und Passwort wurden verschluesselt als API-Fallback gespeichert.');
}
function bratonien_tools_nc_connector_fallback_delete()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
$credentials = bratonien_tools_nc_connector_auth_credentials($connection);
if ($credentials['db_password'] === '')
{
throw new RuntimeException('Das Datenbankpasswort der Verbindung fehlt.');
}
$blob = bratonien_tools_nc_connector_encrypt_credentials($credentials['db_password'], '', '');
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET secret_blob='".pwg_db_real_escape_string($blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
return array('message'=>'Gespeicherter Benutzername/Passwort-Fallback wurde geloescht.');
}
function bratonien_tools_nc_connector_fallback_http_request($url, array $fields, $cookie_file)
{
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($fields),
CURLOPT_COOKIEJAR => $cookie_file,
CURLOPT_COOKIEFILE => $cookie_file,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 900,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-NC-Fallback/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
));
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $errno !== 0)
{
throw new RuntimeException('Piwigo-Fallback konnte nicht ausgefuehrt werden: '.$error);
}
if ($http < 200 || $http >= 300)
{
throw new RuntimeException('Piwigo-Fallback antwortete mit HTTP '.$http.'.');
}
return (string)$body;
}
function bratonien_tools_nc_connector_fallback_once()
{
if (!function_exists('curl_init'))
{
throw new RuntimeException('cURL ist in PHP nicht verfuegbar.');
}
$username = trim((string)($_POST['nc_fallback_user'] ?? ''));
$password = (string)($_POST['nc_fallback_password'] ?? '');
if ($username === '' || $password === '')
{
throw new RuntimeException('Benutzername und Passwort fuer den einmaligen Fallback muessen angegeben werden.');
}
$base = rtrim(get_absolute_root_url(true), '/');
$cookie_file = tempnam(sys_get_temp_dir(), 'br-nc-fallback-');
if ($cookie_file === false)
{
throw new RuntimeException('Temporare Fallback-Sitzung konnte nicht angelegt werden.');
}
try
{
$login_body = bratonien_tools_nc_connector_fallback_http_request(
$base.'/ws.php?format=json',
array(
'method'=>'pwg.session.login',
'username'=>$username,
'password'=>$password,
),
$cookie_file
);
$login = json_decode($login_body, true);
if (!is_array($login) || ($login['stat'] ?? '') !== 'ok')
{
throw new RuntimeException('Piwigo-Anmeldung fuer den einmaligen Fallback wurde abgelehnt.');
}
bratonien_tools_nc_connector_fallback_http_request(
$base.'/admin.php?page=site_update&site=1',
array(
'sync'=>'files',
'display_info'=>1,
'privacy_level'=>0,
'sync_meta'=>1,
'simulate'=>0,
'subcats-included'=>1,
'bratonien_connector'=>1,
'submit'=>1,
),
$cookie_file
);
$orphan_body = bratonien_tools_nc_connector_fallback_http_request(
$base.'/ws.php?format=json',
array(
'method'=>'bratonien.nc.syncOrphans',
'site_id'=>1,
'simulate'=>0,
),
$cookie_file
);
$orphan = json_decode($orphan_body, true);
if (!is_array($orphan) || ($orphan['stat'] ?? '') !== 'ok')
{
throw new RuntimeException('Orphan-Synchronisierung im einmaligen Fallback wurde abgelehnt.');
}
}
finally
{
@unlink($cookie_file);
}
return array('message'=>'Einmaliger Benutzername/Passwort-Fallback wurde ausgefuehrt. Die Zugangsdaten wurden nicht gespeichert.');
}

View File

@@ -0,0 +1,110 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_create_local_api_first()
{
$required = array(
'nc_name' => 'Name',
'nc_host' => 'PostgreSQL-Host',
'nc_database' => 'Datenbank',
'nc_user' => 'Reader-Benutzer',
'nc_db_password' => 'Reader-Passwort',
'nc_source_view' => 'Source-View',
'nc_activity_view' => 'Activity-View',
'nc_gallery_root' => 'Galerie-Pfad',
);
foreach ($required as $field => $label)
{
if (trim((string)($_POST[$field] ?? '')) === '')
{
throw new RuntimeException($label.' fehlt.');
}
}
$fallback_user = trim((string)($_POST['nc_piwigo_user'] ?? ''));
$fallback_password = (string)($_POST['nc_piwigo_password'] ?? '');
if (($fallback_user === '') !== ($fallback_password === ''))
{
throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort muessen entweder beide angegeben oder beide leer gelassen werden.');
}
$api = bratonien_tools_nc_api_credentials();
if (($api['key_id'] ?? '') === '' && $fallback_user === '')
{
throw new RuntimeException('Es ist weder ein gespeicherter Piwigo-API-Zugang noch ein Benutzername/Passwort-Fallback vorhanden.');
}
$port = (int)($_POST['nc_port'] ?? 5432);
if ($port < 1 || $port > 65535)
{
throw new RuntimeException('Ungueltiger PostgreSQL-Port.');
}
$gallery_root = rtrim(trim((string)$_POST['nc_gallery_root']), '/');
if ($gallery_root === '' || $gallery_root[0] !== '/')
{
throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
}
$config = array(
'host' => trim((string)$_POST['nc_host']),
'port' => (string)$port,
'database' => trim((string)$_POST['nc_database']),
'user' => trim((string)$_POST['nc_user']),
'source_view' => trim((string)$_POST['nc_source_view']),
'activity_view' => trim((string)$_POST['nc_activity_view']),
'gallery_root' => $gallery_root,
'state_dir' => '',
'status_file' => '',
'quiet_seconds' => max(0, (int)($_POST['nc_quiet_seconds'] ?? 120)),
'max_wait_seconds' => max(60, (int)($_POST['nc_max_wait_seconds'] ?? 900)),
'full_sync_seconds' => max(300, (int)($_POST['nc_full_sync_seconds'] ?? 86400)),
'storages' => bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? ''),
'origin' => 'native',
'piwigo_auth' => 'api-first',
);
bratonien_tools_nc_connector_view_name($config['source_view']);
bratonien_tools_nc_connector_view_name($config['activity_view']);
$table = bratonien_tools_nc_connector_table();
bratonien_tools_nc_connector_ensure_table();
$now = date('Y-m-d H:i:s');
$connection_key = 'local-'.bin2hex(random_bytes(12));
$secret_blob = bratonien_tools_nc_connector_encrypt_credentials(
(string)$_POST['nc_db_password'],
$fallback_user,
$fallback_password
);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json))
{
throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
}
pwg_query("INSERT INTO `$table`
(connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated)
VALUES ('".pwg_db_real_escape_string($connection_key)."',
'".pwg_db_real_escape_string(trim((string)$_POST['nc_name']))."',
'local', 0, 'disabled',
'".pwg_db_real_escape_string($config_json)."',
'".pwg_db_real_escape_string($secret_blob)."',
'".pwg_db_real_escape_string($now)."',
'".pwg_db_real_escape_string($now)."')");
$id = (int)pwg_db_insert_id();
$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);
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id);
return array(
'message' => $fallback_user === ''
? 'Nextcloud-Verbindung wurde API-first ohne dauerhaft gespeicherten Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.'
: 'Nextcloud-Verbindung wurde API-first mit verschluesseltem Benutzername/Passwort-Fallback angelegt. Bitte technisch pruefen und danach im LXC aktivieren.'
);
}

View File

@@ -4,6 +4,88 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node)
{
$children = $node->children();
if (count($children) === 0)
{
return (string)$node;
}
$result = array();
foreach ($children as $name => $child)
{
$value = bratonien_tools_nc_connector_xml_value($child);
if (array_key_exists($name, $result))
{
if (!is_array($result[$name]) || !array_is_list($result[$name]))
{
$result[$name] = array($result[$name]);
}
$result[$name][] = $value;
}
else
{
$result[$name] = $value;
}
}
return $result;
}
function bratonien_tools_nc_connector_decode_api_response($body, $content_type = '')
{
$decoded = json_decode((string)$body, true);
if (is_array($decoded))
{
return $decoded;
}
if (!function_exists('simplexml_load_string'))
{
throw new RuntimeException('Piwigo-API lieferte XML, aber SimpleXML ist in PHP nicht verfuegbar.');
}
$previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$body);
libxml_clear_errors();
libxml_use_internal_errors($previous);
if ($xml === false || $xml->getName() !== 'rsp')
{
$detail = $content_type !== '' ? ' Antworttyp: '.$content_type.'.' : '';
throw new RuntimeException('Piwigo-API lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.'.$detail);
}
$response = array('stat'=>(string)$xml['stat']);
foreach ($xml->children() as $name => $child)
{
$value = bratonien_tools_nc_connector_xml_value($child);
if (array_key_exists($name, $response))
{
if (!is_array($response[$name]) || !array_is_list($response[$name]))
{
$response[$name] = array($response[$name]);
}
$response[$name][] = $value;
}
else
{
$response[$name] = $value;
}
}
return $response;
}
function bratonien_tools_nc_connector_api_payload(array $decoded)
{
if (array_key_exists('result', $decoded))
{
return $decoded['result'];
}
unset($decoded['stat']);
return $decoded;
}
function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, $method)
{
if (!function_exists('curl_init'))
@@ -18,8 +100,6 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s
throw new RuntimeException('API-Schluessel-ID und Geheimnis muessen angegeben werden.');
}
// Piwigo expects the exact header value "public-id:secret". The separator is
// added here by the connector and is not submitted through a form field.
$api_key = $api_key_id.':'.$api_key_secret;
$url = rtrim(get_absolute_root_url(true), '/').'/ws.php';
$ch = curl_init($url);
@@ -32,7 +112,7 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s
)),
CURLOPT_HTTPHEADER => array(
'X-PIWIGO-API: '.$api_key,
'Accept: application/json',
'Accept: application/json, text/xml;q=0.9',
'Content-Type: application/x-www-form-urlencoded',
),
CURLOPT_CONNECTTIMEOUT => 10,
@@ -57,19 +137,14 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s
throw new RuntimeException('Piwigo-API antwortete mit HTTP '.$http_code.'.');
}
$decoded = json_decode((string)$body, true);
if (!is_array($decoded))
{
$detail = $content_type !== '' ? ' Antworttyp: '.$content_type.'.' : '';
throw new RuntimeException('Piwigo-API lieferte keine gueltige JSON-Antwort.'.$detail);
}
$decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $content_type);
if (($decoded['stat'] ?? '') !== 'ok')
{
$message = (string)($decoded['message'] ?? $decoded['err'] ?? 'API-Aufruf wurde abgelehnt.');
throw new RuntimeException($message);
}
return $decoded['result'] ?? null;
return bratonien_tools_nc_connector_api_payload($decoded);
}
function bratonien_tools_nc_connector_piwigo_api_test()
@@ -94,12 +169,25 @@ function bratonien_tools_nc_connector_piwigo_api_test()
$username = (string)($status['username'] ?? $status['user'] ?? '');
$user_status = strtolower((string)($status['status'] ?? ''));
$is_admin = in_array($user_status, array('admin', 'webmaster'), true);
if (!$is_admin)
{
throw new RuntimeException('Der API-Key funktioniert, gehoert aber keinem Piwigo-Administrator/Webmaster. Er wurde nicht gespeichert.');
}
$method_result = bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, 'reflection.getMethodList');
$methods = array();
if (is_array($method_result))
{
foreach ($method_result as $entry)
if (isset($method_result['method']))
{
$entries = is_array($method_result['method']) ? $method_result['method'] : array($method_result['method']);
}
else
{
$entries = $method_result;
}
foreach ($entries as $entry)
{
if (is_string($entry))
{
@@ -112,6 +200,15 @@ function bratonien_tools_nc_connector_piwigo_api_test()
}
}
$required_methods = array('bratonien.nc.syncProductive', 'bratonien.nc.syncOrphans');
$missing = array_values(array_diff($required_methods, $methods));
if ($missing)
{
throw new RuntimeException('Der API-Key ist gueltig, aber die benoetigten Bratonien-Sync-Methoden fehlen: '.implode(', ', $missing).'.');
}
bratonien_tools_nc_api_credentials_store($api_key_id, $api_key_secret);
$sync_candidates = array_values(array_filter($methods, function($method)
{
return preg_match('/sync|synchron|site/i', (string)$method) === 1;
@@ -122,27 +219,16 @@ function bratonien_tools_nc_connector_piwigo_api_test()
'ok' => true,
'username' => $username !== '' ? $username : 'nicht gemeldet',
'status' => $user_status !== '' ? $user_status : 'nicht gemeldet',
'admin' => $is_admin,
'admin' => true,
'method_count' => count($methods),
'sync_candidates' => $sync_candidates,
'sync_api_detected' => count($sync_candidates) > 0,
'sync_api_detected' => true,
'stored' => true,
'conclusion' => 'Der API-Key ist als bevorzugter Connector-Zugang gespeichert. Produktive API-Synchronisierung bleibt auf die im Plugin freigegebene Piwigo-Version begrenzt; bei einer nicht freigegebenen Version greift nur der konfigurierte Benutzername/Passwort-Fallback.',
);
if (!$is_admin)
{
$result['conclusion'] = 'Der API-Key funktioniert, gehoert aber keinem Piwigo-Administrator/Webmaster. Fuer den heutigen NC-Connector-Sync reicht dieser Zugang nicht aus.';
}
elseif ($result['sync_api_detected'])
{
$result['conclusion'] = 'Der API-Key funktioniert mit Administratorrechten. Es wurden API-Methoden mit moeglichem Sync-/Site-Bezug gefunden. Diese muessen vor einer Runtime-Umstellung einzeln auf Eignung und Nebenwirkungen geprueft werden.';
}
else
{
$result['conclusion'] = 'Der API-Key funktioniert mit Administratorrechten. In der sichtbaren Web-API wurde jedoch keine offensichtliche Sync-/Site-Methode gefunden. Der bestehende Admin-Login wird deshalb noch nicht ersetzt.';
}
return array(
'message' => 'Piwigo-API-Key wurde geprueft. Es wurden keine Zugangsdaten gespeichert und keine Synchronisation ausgeloest.',
'message' => 'Piwigo-API-Key wurde geprueft und verschluesselt als bevorzugter Connector-Zugang gespeichert.',
'nc_piwigo_api_test' => $result,
);
}

View File

@@ -0,0 +1,251 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_register_nc_orphan_ws_methods($arr)
{
$service = &$arr[0];
$service->addMethod(
'bratonien.nc.syncOrphans',
'bratonien_tools_ws_nc_sync_orphans',
array(
'site_id' => array(
'default' => 1,
'type' => WS_TYPE_ID,
),
'simulate' => array(
'default' => true,
'type' => WS_TYPE_BOOL,
),
),
'Synchronizes connector-managed files directly in a local Piwigo gallery root as orphan photos.',
null,
array(
'admin_only' => true,
'post_only' => true,
)
);
}
function bratonien_tools_nc_root_files($basedir)
{
global $conf;
$files = array();
if (!is_dir($basedir) || !($handle = opendir($basedir)))
{
return $files;
}
$allowed = array_flip($conf['file_ext']);
while (($node = readdir($handle)) !== false)
{
if ($node === '.' || $node === '..')
{
continue;
}
$path = $basedir.'/'.$node;
if (!is_file($path))
{
continue;
}
$extension = strtolower(get_extension($node));
if (!isset($allowed[$extension]))
{
continue;
}
$files[$path] = $node;
}
closedir($handle);
ksort($files);
return $files;
}
function bratonien_tools_nc_existing_root_orphans($basedir)
{
$rows = array();
$query = '
SELECT id, path
FROM '.IMAGES_TABLE.'
WHERE storage_category_id IS NULL';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
$path = (string)$row['path'];
if (dirname($path) === $basedir)
{
$rows[$path] = (int)$row['id'];
}
}
ksort($rows);
return $rows;
}
function bratonien_tools_nc_register_root_orphan($path)
{
global $user;
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
include_once(PHPWG_ROOT_PATH.'admin/include/functions_upload.inc.php');
include_once(PHPWG_ROOT_PATH.'admin/include/image.class.php');
if (!is_file($path))
{
throw new Exception('Root file is not readable: '.$path);
}
$md5sum = md5_file($path);
if ($md5sum === false)
{
throw new Exception('Checksum could not be calculated: '.$path);
}
list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW();'));
$rotation_angle = pwg_image::get_rotation_angle($path);
$rotation = pwg_image::get_rotation_code_from_angle($rotation_angle);
$file_infos = pwg_image_infos($path);
$file = pwg_db_real_escape_string(basename($path));
$insert = array(
'file' => $file,
'name' => get_name_from_file($file),
'date_available' => $dbnow,
'path' => pwg_db_real_escape_string($path),
'filesize' => $file_infos['filesize'],
'width' => $file_infos['width'],
'height' => $file_infos['height'],
'md5sum' => $md5sum,
'added_by' => (int)$user['id'],
'rotation' => $rotation,
);
single_insert(IMAGES_TABLE, $insert);
$image_id = (int)pwg_db_insert_id(IMAGES_TABLE);
pwg_activity('photo', $image_id, 'add', array('sync'=>true, 'source'=>'bratonien_nc_root'));
sync_metadata(array($image_id));
$image_infos = array(
'id' => $image_id,
'path' => $path,
'representative_ext' => null,
);
trigger_notify('loc_end_add_uploaded_file', $image_infos);
return $image_id;
}
function bratonien_tools_ws_nc_sync_orphans($params, &$service)
{
global $conf;
if (empty($conf['enable_synchronization']))
{
return new PwgError(403, 'Piwigo filesystem synchronization is disabled.');
}
$piwigo_version = defined('PHPWG_VERSION') ? (string)PHPWG_VERSION : '';
if ($piwigo_version !== '16.4.0')
{
return new PwgError(409, 'Bratonien orphan synchronization is approved only for Piwigo 16.4.0.');
}
$site_id = isset($params['site_id']) ? (int)$params['site_id'] : 1;
if ($site_id < 1)
{
return new PwgError(400, 'Invalid site_id.');
}
$query = '
SELECT galleries_url
FROM '.SITES_TABLE.'
WHERE id = '.$site_id.'
LIMIT 1';
$result = pwg_query($query);
if (!pwg_db_num_rows($result))
{
return new PwgError(404, 'Piwigo site does not exist.');
}
list($site_url) = pwg_db_fetch_row($result);
if (url_is_remote($site_url))
{
return new PwgError(400, 'Remote Piwigo sites are not supported.');
}
$basedir = preg_replace('#/*$#', '', (string)$site_url);
if (!is_dir($basedir))
{
return new PwgError(500, 'Piwigo gallery root is not accessible.');
}
$filesystem = bratonien_tools_nc_root_files($basedir);
$database = bratonien_tools_nc_existing_root_orphans($basedir);
$to_add = array_values(array_diff(array_keys($filesystem), array_keys($database)));
$to_remove_paths = array_values(array_diff(array_keys($database), array_keys($filesystem)));
$to_remove_ids = array();
foreach ($to_remove_paths as $path)
{
$to_remove_ids[] = $database[$path];
}
$simulate = !isset($params['simulate']) || (bool)$params['simulate'];
if ($simulate)
{
return array(
'mode' => 'simulation',
'piwigo_version' => $piwigo_version,
'site_id' => $site_id,
'site_url' => $site_url,
'root_files' => count($filesystem),
'registered_orphans' => count($database),
'new_orphans' => count($to_add),
'deleted_orphans' => count($to_remove_ids),
'database_writes' => false,
);
}
$added_ids = array();
try
{
foreach ($to_add as $path)
{
$added_ids[] = bratonien_tools_nc_register_root_orphan($path);
}
if (count($to_remove_ids) > 0)
{
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
delete_elements($to_remove_ids, false);
}
if (count($added_ids) > 0 || count($to_remove_ids) > 0)
{
invalidate_user_cache();
}
}
catch (Throwable $error)
{
return new PwgError(500, 'Bratonien orphan synchronization failed: '.$error->getMessage());
}
return array(
'mode' => 'productive',
'piwigo_version' => $piwigo_version,
'site_id' => $site_id,
'site_url' => $site_url,
'root_files' => count($filesystem),
'added_orphans' => count($added_ids),
'deleted_orphans' => count($to_remove_ids),
'added_ids' => $added_ids,
'database_writes' => (count($added_ids) > 0 || count($to_remove_ids) > 0),
);
}

View File

@@ -0,0 +1,130 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_register_nc_productive_ws_methods($arr)
{
$service = &$arr[0];
$service->addMethod(
'bratonien.nc.syncProductive',
'bratonien_tools_ws_nc_sync_productive',
array(
'site_id' => array(
'default' => 1,
'type' => WS_TYPE_ID,
'info' => 'Piwigo storage site to synchronize. Default: 1.',
),
),
'Runs the approved Piwigo filesystem synchronization for the Bratonien NC Connector.',
null,
array(
'admin_only' => true,
'post_only' => true,
)
);
}
function bratonien_tools_ws_nc_sync_productive($params, &$service)
{
global $conf, $user, $template, $page;
$piwigo_version = defined('PHPWG_VERSION') ? (string)PHPWG_VERSION : '';
if ($piwigo_version !== '16.4.0')
{
return new PwgError(
409,
'Bratonien API synchronization is not approved for Piwigo '.$piwigo_version.'. Use the administrator fallback until this Piwigo version has been verified.'
);
}
if (empty($conf['enable_synchronization']))
{
return new PwgError(403, 'Piwigo filesystem synchronization is disabled.');
}
$site_id = isset($params['site_id']) ? (int)$params['site_id'] : 1;
if ($site_id < 1)
{
return new PwgError(400, 'Invalid site_id.');
}
$saved_get = $_GET;
$saved_post = $_POST;
$saved_request_method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
$saved_default_status = isset($conf['newcat_default_status']) ? $conf['newcat_default_status'] : null;
$output = '';
$counts = array();
try
{
if (!defined('IN_ADMIN'))
{
define('IN_ADMIN', true);
}
$conf['newcat_default_status'] = 'private';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_GET = array(
'page' => 'site_update',
'site' => $site_id,
);
$_POST = array(
'sync' => 'files',
'display_info' => 1,
'privacy_level' => 0,
'sync_meta' => 1,
'simulate' => 0,
'subcats-included' => 1,
'bratonien_connector' => 1,
'submit' => 1,
);
ob_start();
include(PHPWG_ROOT_PATH.'admin/site_update.php');
$output = (string)ob_get_clean();
}
catch (Throwable $e)
{
if (ob_get_level() > 0)
{
ob_end_clean();
}
return new PwgError(500, 'Piwigo core synchronization failed: '.$e->getMessage());
}
finally
{
$_GET = $saved_get;
$_POST = $saved_post;
if ($saved_default_status === null)
{
unset($conf['newcat_default_status']);
}
else
{
$conf['newcat_default_status'] = $saved_default_status;
}
if ($saved_request_method === null)
{
unset($_SERVER['REQUEST_METHOD']);
}
else
{
$_SERVER['REQUEST_METHOD'] = $saved_request_method;
}
}
return array(
'mode' => 'productive',
'approved_piwigo_version' => '16.4.0',
'piwigo_version' => $piwigo_version,
'site_id' => $site_id,
'counts' => is_array($counts) ? $counts : array(),
'database_writes' => true,
'core_output_bytes' => strlen($output),
'username' => isset($user['username']) ? (string)$user['username'] : '',
'status' => isset($user['status']) ? (string)$user['status'] : '',
);
}

View File

@@ -18,6 +18,8 @@ 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');
function bratonien_tools_get_tools()
@@ -84,7 +86,7 @@ function bratonien_tools_get_tools()
'handler' => 'bratonien_tools_revoke_album_share',
),
'nc_connector_create_local' => array(
'handler' => 'bratonien_tools_nc_connector_create_local',
'handler' => 'bratonien_tools_nc_connector_create_local_api_first',
),
'nc_connector_delete' => array(
'handler' => 'bratonien_tools_nc_connector_delete',
@@ -104,6 +106,18 @@ function bratonien_tools_get_tools()
'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',
),
'nc_connector_fallback_delete' => array(
'handler' => 'bratonien_tools_nc_connector_fallback_delete',
),
'nc_connector_fallback_once' => array(
'handler' => 'bratonien_tools_nc_connector_fallback_once',
),
'self_update_check' => array(
'handler' => 'bratonien_tools_self_update_check',
),

View File

@@ -1,7 +1,7 @@
<?php
/*
Plugin Name: Bratonien Tools
Version: 0.9.3.9
Version: 0.9.3.20
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien
@@ -21,16 +21,44 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/picture_navigation.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/batch_titles.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php');
add_event_handler('get_admin_plugin_menu_links', 'bratonien_tools_admin_menu');
add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4);
add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action');
add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EVENT_HANDLER_PRIORITY_NEUTRAL - 30);
add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', EVENT_HANDLER_PRIORITY_NEUTRAL - 20);
add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10);
add_event_handler('init', 'bratonien_tools_album_shares_init');
add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories');
add_event_handler('ws_add_methods', 'bratonien_tools_register_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_productive_ws_methods');
/**
* The NC Connector uses Piwigo's native filesystem synchronization for folder
* shares. Mark only connector-triggered synchronization requests so every new
* physical album created during that run starts private. The global Piwigo
* default remains untouched for manual album creation and unrelated imports.
*/
function bratonien_tools_prepare_connector_private_import()
{
global $conf;
if (
!defined('IN_ADMIN')
|| ($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST'
|| (string)($_GET['page'] ?? '') !== 'site_update'
|| (string)($_POST['bratonien_connector'] ?? '') !== '1'
)
{
return;
}
$conf['newcat_default_status'] = 'private';
}
/**
* Piwigo's per-album permissions form rewrites the complete direct-user

View File

@@ -4,6 +4,7 @@
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
@@ -13,14 +14,29 @@ import time
from pathlib import Path
def load(path: Path) -> dict[str, int]:
def load(path: Path) -> dict[str, object]:
defaults: dict[str, object] = {
"processed": 0,
"observed": 0,
"pending_since": 0,
"last_change": 0,
"last_full": 0,
"source_signature": "",
}
if not path.exists():
return {"processed": 0, "observed": 0, "pending_since": 0, "last_change": 0, "last_full": 0}
return defaults
data = json.loads(path.read_text(encoding="utf-8"))
return {key: int(data.get(key, 0)) for key in ("processed", "observed", "pending_since", "last_change", "last_full")}
return {
"processed": int(data.get("processed", 0)),
"observed": int(data.get("observed", 0)),
"pending_since": int(data.get("pending_since", 0)),
"last_change": int(data.get("last_change", 0)),
"last_full": int(data.get("last_full", 0)),
"source_signature": str(data.get("source_signature", "")),
}
def save(path: Path, state: dict[str, int]) -> None:
def save(path: Path, state: dict[str, object]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(dir=path.parent)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
@@ -29,12 +45,22 @@ def save(path: Path, state: dict[str, int]) -> None:
Path(name).replace(path)
def latest(args: argparse.Namespace) -> int:
def query(args: argparse.Namespace, sql: str) -> str:
env = os.environ.copy()
env["PGPASSWORD"] = args.password_file.read_text(encoding="utf-8").strip()
sql = f"SELECT COALESCE(MAX(activity_id), 0) FROM {args.view}"
command = ["psql", "-XAt", "-h", args.host, "-p", str(args.port), "-U", args.user, "-d", args.database, "-c", sql]
return int(subprocess.run(command, env=env, check=True, text=True, capture_output=True).stdout.strip())
return subprocess.run(command, env=env, check=True, text=True, capture_output=True).stdout
def latest(args: argparse.Namespace) -> int:
return int(query(args, f"SELECT COALESCE(MAX(activity_id), 0) FROM {args.view}").strip())
def source_signature(args: argparse.Namespace) -> str:
if not args.source_view:
return ""
payload = query(args, f"SELECT share_id, display_name, storage_id, source_path FROM {args.source_view} ORDER BY share_id")
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def main() -> int:
@@ -47,6 +73,7 @@ def main() -> int:
parser.add_argument("--user", required=True)
parser.add_argument("--password-file", required=True, type=Path)
parser.add_argument("--view", default="piwigo_showcase_activity")
parser.add_argument("--source-view", default="")
parser.add_argument("--quiet", type=int, default=120)
parser.add_argument("--max-wait", type=int, default=900)
parser.add_argument("--full-after", type=int, default=86400)
@@ -56,23 +83,27 @@ def main() -> int:
state = load(args.state)
now = int(time.time())
current = latest(args)
current_source = source_signature(args)
if args.action == "commit":
state.update(processed=current, observed=current, pending_since=0, last_change=0, last_full=now)
state.update(processed=current, observed=current, pending_since=0, last_change=0, last_full=now, source_signature=current_source)
save(args.state, state)
return 0
if not state["last_full"]:
if not int(state["last_full"]):
return 0
if current > state["observed"]:
if current_source and current_source != str(state["source_signature"]):
return 0
if current > int(state["observed"]):
state["observed"] = current
state["last_change"] = now
state["pending_since"] = state["pending_since"] or now
state["pending_since"] = int(state["pending_since"]) or now
save(args.state, state)
if now - state["last_full"] >= args.full_after:
if now - int(state["last_full"]) >= args.full_after:
return 0
if state["observed"] <= state["processed"]:
if int(state["observed"]) <= int(state["processed"]):
return 3
if now - state["last_change"] >= args.quiet or now - state["pending_since"] >= args.max_wait:
if now - int(state["last_change"]) >= args.quiet or now - int(state["pending_since"]) >= args.max_wait:
return 0
return 3
except Exception as error:

View File

@@ -28,18 +28,43 @@ def read_config(path: Path) -> dict[str, tuple[str, Path]]:
return result
def query_rows(args: argparse.Namespace) -> list[list[str]]:
password = args.password_file.read_text(encoding="utf-8").strip()
env = os.environ.copy()
env["PGPASSWORD"] = password
sql = f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {args.view} ORDER BY share_id"
def run_query(args: argparse.Namespace, env: dict[str, str], sql: str) -> subprocess.CompletedProcess[str]:
command = [
"psql", "-X", "-A", "-F", "\t", "-t",
"-h", args.host, "-p", str(args.port), "-U", args.user, "-d", args.database,
"-c", sql,
]
completed = subprocess.run(command, env=env, check=True, text=True, capture_output=True)
return list(csv.reader(completed.stdout.splitlines(), delimiter="\t"))
return subprocess.run(command, env=env, check=False, text=True, capture_output=True)
def query_rows(args: argparse.Namespace) -> list[list[str]]:
password = args.password_file.read_text(encoding="utf-8").strip()
env = os.environ.copy()
env["PGPASSWORD"] = password
modern_sql = f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {args.view} ORDER BY share_id"
completed = run_query(args, env, modern_sql)
if completed.returncode == 0:
return list(csv.reader(completed.stdout.splitlines(), delimiter="\t"))
# Existing installations may still expose the original four-column view.
# Keep them usable and derive folder/file from the resolved source path.
if "item_type" not in completed.stderr or "does not exist" not in completed.stderr:
raise RuntimeError(completed.stderr.strip() or "Nextcloud source view query failed")
legacy_sql = f"SELECT share_id, display_name, storage_id, source_path FROM {args.view} ORDER BY share_id"
completed = run_query(args, env, legacy_sql)
if completed.returncode != 0:
raise RuntimeError(completed.stderr.strip() or "legacy Nextcloud source view query failed")
rows: list[list[str]] = []
for row in csv.reader(completed.stdout.splitlines(), delimiter="\t"):
if len(row) == 4:
share_id, display_name, storage_id, source_path = row
rows.append([share_id, "", display_name, storage_id, source_path])
else:
rows.append(row)
return rows
def contained_join(root: Path, relative: str) -> Path:
@@ -67,7 +92,7 @@ def build(args: argparse.Namespace) -> dict[str, object]:
share_id, item_type, display_name, storage_id, source_path = row
item_type = item_type.strip().lower()
if item_type not in {"folder", "file"}:
if item_type and item_type not in {"folder", "file"}:
errors.append(f"share {share_id}: unsupported item_type {item_type!r}")
continue
@@ -90,6 +115,15 @@ def build(args: argparse.Namespace) -> dict[str, object]:
errors.append(f"share {share_id}: storage mount unavailable: {mount}")
continue
if not item_type:
if source.is_dir():
item_type = "folder"
elif source.is_file():
item_type = "file"
else:
errors.append(f"share {share_id}: source unavailable: {source}")
continue
if item_type == "folder":
if not source.is_dir():
errors.append(f"share {share_id}: source directory unavailable: {source}")

View File

@@ -53,10 +53,31 @@ my $sync = $ua->post(
sync_meta => 1,
simulate => 0,
'subcats-included' => 1,
bratonien_connector => 1,
submit => 1,
},
);
die "Piwigo-Datenbanksynchronisierung fehlgeschlagen: ".$sync->status_line."\n"
if !$sync->is_success;
my $orphans = $ua->post(
$opt{'base-url'}.'/ws.php?format=json',
{
method => 'bratonien.nc.syncOrphans',
site_id => 1,
simulate => 0,
},
);
die "Piwigo-Orphan-Synchronisierung fehlgeschlagen: ".$orphans->status_line."\n"
if !$orphans->is_success;
my $orphan_result = eval { decode_json($orphans->decoded_content) };
die "Piwigo-Orphan-Synchronisierung wurde abgelehnt\n"
if !$orphan_result || ($orphan_result->{stat} // '') ne 'ok';
my $result = $orphan_result->{result} || {};
my $added = $result->{added_orphans} // 0;
my $deleted = $result->{deleted_orphans} // 0;
print "Piwigo-Datenbanksynchronisierung erfolgreich\n";
print "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";

329
runtime/lib/piwigo-sync.php Normal file
View File

@@ -0,0 +1,329 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
function fail_sync($message)
{
throw new RuntimeException($message);
}
function http_request($url, array $fields, array $headers = array(), $cookie_file = null)
{
$ch = curl_init($url);
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($fields),
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 900,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-NC-Connector/0.9.3.19',
);
if ($headers)
{
$options[CURLOPT_HTTPHEADER] = $headers;
}
if ($cookie_file !== null)
{
$options[CURLOPT_COOKIEJAR] = $cookie_file;
$options[CURLOPT_COOKIEFILE] = $cookie_file;
}
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);
if ($body === false || $errno !== 0)
{
fail_sync('HTTP-Aufruf fehlgeschlagen: '.$error);
}
if ($http < 200 || $http >= 300)
{
fail_sync('HTTP-Aufruf antwortete mit Status '.$http.'.');
}
return (string)$body;
}
function xml_value_sync(SimpleXMLElement $node)
{
$children = $node->children();
if (count($children) === 0)
{
return (string)$node;
}
$result = array();
foreach ($children as $name => $child)
{
$value = xml_value_sync($child);
if (array_key_exists($name, $result))
{
if (!is_array($result[$name]) || !array_is_list($result[$name]))
{
$result[$name] = array($result[$name]);
}
$result[$name][] = $value;
}
else
{
$result[$name] = $value;
}
}
return $result;
}
function decode_ws($body)
{
$decoded = json_decode((string)$body, true);
if (!is_array($decoded))
{
if (!function_exists('simplexml_load_string'))
{
fail_sync('Piwigo lieferte XML, aber SimpleXML ist nicht verfuegbar.');
}
$previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$body);
libxml_clear_errors();
libxml_use_internal_errors($previous);
if ($xml === false || $xml->getName() !== 'rsp')
{
fail_sync('Piwigo lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.');
}
$decoded = array('stat'=>(string)$xml['stat']);
foreach ($xml->children() as $name => $child)
{
$value = xml_value_sync($child);
if (array_key_exists($name, $decoded))
{
if (!is_array($decoded[$name]) || !array_is_list($decoded[$name]))
{
$decoded[$name] = array($decoded[$name]);
}
$decoded[$name][] = $value;
}
else
{
$decoded[$name] = $value;
}
}
}
if (($decoded['stat'] ?? '') !== 'ok')
{
$message = (string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo-Aufruf wurde abgelehnt.');
fail_sync($message);
}
if (array_key_exists('result', $decoded))
{
return $decoded['result'];
}
unset($decoded['stat']);
return $decoded;
}
function decrypt_blob($blob, $hex_key)
{
if (!preg_match('/^[a-f0-9]{64}$/', (string)$hex_key))
{
fail_sync('Connector-Schluessel ist ungueltig.');
}
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1)
{
fail_sync('Gespeicherte Zugangsdaten haben ein unbekanntes Format.');
}
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false)
{
fail_sync('Gespeicherte Zugangsdaten konnten nicht entschluesselt werden.');
}
return (string)$plain;
}
try
{
$options = getopt('', array('piwigo-root:', 'connection-id:', 'base-url:'));
$piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/');
$connection_id = (int)($options['connection-id'] ?? 0);
$base_url = rtrim((string)($options['base-url'] ?? 'http://127.0.0.1'), '/');
if ($piwigo_root === '' || $connection_id < 1)
{
fail_sync('Parameter --piwigo-root und --connection-id werden benoetigt.');
}
if (!function_exists('curl_init'))
{
fail_sync('PHP-cURL ist nicht verfuegbar.');
}
$db_config = $piwigo_root.'/local/config/database.inc.php';
if (!is_readable($db_config))
{
fail_sync('Piwigo-Datenbankkonfiguration ist nicht lesbar.');
}
$conf = array();
$prefixeTable = 'piwigo_';
require $db_config;
foreach (array('db_host','db_user','db_password','db_base') as $key)
{
if (!isset($conf[$key]))
{
fail_sync('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
}
}
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno)
{
fail_sync('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
}
$db->set_charset('utf8mb4');
$key_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!$key_result || !$key_result->num_rows)
{
fail_sync('Connector-Schluessel wurde nicht gefunden.');
}
$hex_key = (string)$key_result->fetch_assoc()['value'];
$api = array('key_id'=>'', 'key_secret'=>'');
$api_result = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_piwigo_api' LIMIT 1");
if ($api_result && $api_result->num_rows)
{
$api_blob = (string)$api_result->fetch_assoc()['value'];
if ($api_blob !== '')
{
$api_plain = decrypt_blob($api_blob, $hex_key);
$api_decoded = json_decode($api_plain, true);
if (is_array($api_decoded))
{
$api['key_id'] = (string)($api_decoded['key_id'] ?? '');
$api['key_secret'] = (string)($api_decoded['key_secret'] ?? '');
}
}
}
$connection_table = $prefixeTable.'bratonien_tools_nc_connections';
$connection_result = $db->query('SELECT secret_blob FROM `'.$connection_table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!$connection_result || !$connection_result->num_rows)
{
fail_sync('Connector-Verbindung #'.$connection_id.' wurde nicht gefunden.');
}
$connection_blob = (string)$connection_result->fetch_assoc()['secret_blob'];
$connection_plain = decrypt_blob($connection_blob, $hex_key);
$connection_credentials = json_decode($connection_plain, true);
if (!is_array($connection_credentials))
{
$connection_credentials = array();
}
$fallback_user = (string)($connection_credentials['piwigo_user'] ?? '');
$fallback_password = (string)($connection_credentials['piwigo_password'] ?? '');
$api_error = null;
if ($api['key_id'] !== '' && $api['key_secret'] !== '')
{
try
{
$headers = array(
'X-PIWIGO-API: '.$api['key_id'].':'.$api['key_secret'],
'Accept: application/json, text/xml;q=0.9',
'Content-Type: application/x-www-form-urlencoded',
);
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncProductive', 'site_id'=>1),
$headers
));
$orphan = decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0),
$headers
));
$added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Synchronisierung per API erfolgreich\n";
echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";
exit(0);
}
catch (Throwable $e)
{
$api_error = $e->getMessage();
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n");
}
}
else
{
$api_error = 'Keine gespeicherten API-Zugangsdaten.';
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n");
}
if ($fallback_user === '' || $fallback_password === '')
{
fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback vorhanden.');
}
$cookie_file = tempnam(sys_get_temp_dir(), 'br-nc-sync-');
if ($cookie_file === false)
{
fail_sync('Temporare Piwigo-Sitzung konnte nicht angelegt werden.');
}
try
{
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password),
array(),
$cookie_file
));
http_request(
$base_url.'/admin.php?page=site_update&site=1',
array(
'sync'=>'files',
'display_info'=>1,
'privacy_level'=>0,
'sync_meta'=>1,
'simulate'=>0,
'subcats-included'=>1,
'bratonien_connector'=>1,
'submit'=>1,
),
array(),
$cookie_file
);
$orphan = decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0),
array(),
$cookie_file
));
$added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich\n";
echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";
}
finally
{
@unlink($cookie_file);
}
}
catch (Throwable $e)
{
fwrite(STDERR, $e->getMessage()."\n");
exit(1);
}

View File

@@ -4,14 +4,14 @@ set -Eeuo pipefail
CONFIG_FILE="${PIWIGO_CONFIG:-/etc/bratonien-tools/nc-connector/connection-1.conf}"
[[ -r "$CONFIG_FILE" ]] || { echo "Konfiguration fehlt: $CONFIG_FILE" >&2; exit 1; }
# Preserve an explicit per-run override before the connection config is sourced.
# This allows controlled shadow-tree updates without running the productive
# Piwigo database synchronization, while leaving the stored config untouched.
PIWIGO_SYNC_OVERRIDE_VALUE="${PIWIGO_SYNC_OVERRIDE-}"
# shellcheck source=/dev/null
source "$CONFIG_FILE"
NC_ACTIVITY_VIEW="${NC_ACTIVITY_VIEW:-piwigo_showcase_activity}"
NC_DB_VIEW="${NC_DB_VIEW:-piwigo_showcase_sources}"
if [[ -n "$PIWIGO_SYNC_OVERRIDE_VALUE" ]]; then
case "$PIWIGO_SYNC_OVERRIDE_VALUE" in
0|1) PIWIGO_SYNC_ENABLED="$PIWIGO_SYNC_OVERRIDE_VALUE" ;;
@@ -26,6 +26,17 @@ LOCK_FILE="$STATE_DIR/sync.lock"
ACTIVITY_STATE="$STATE_DIR/activity.json"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
CONNECTION_ID="${CONNECTION_ID:-}"
if [[ -z "$CONNECTION_ID" ]]; then
CONFIG_BASENAME="$(basename -- "$CONFIG_FILE")"
if [[ "$CONFIG_BASENAME" =~ ^connection-([0-9]+)\.conf$ ]]; then
CONNECTION_ID="${BASH_REMATCH[1]}"
else
echo "Connector-ID konnte nicht aus $CONFIG_FILE ermittelt werden." >&2
exit 1
fi
fi
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
@@ -87,6 +98,7 @@ else
if python3 "$SCRIPT_DIR/lib/activity_gate.py" check \
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT" \
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE" \
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW" \
--quiet "$QUIET_SECONDS" --max-wait "$MAX_WAIT_SECONDS" --full-after "$FULL_SYNC_SECONDS"; then
GATE_RESULT=0
else
@@ -110,19 +122,16 @@ python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--manifest "$MANIFEST" --destination "$GALLERY_ROOT" --state "$MAP_FILE"
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
[[ -r "${PIWIGO_SYNC_PASSWORD_FILE:?}" ]] || {
echo "Piwigo-Passwortdatei fehlt oder ist nicht lesbar." >&2
exit 1
}
perl "$SCRIPT_DIR/lib/piwigo-db-sync.pl" \
--base-url="http://127.0.0.1" \
--username="${PIWIGO_SYNC_USER:?}" \
--password-file="$PIWIGO_SYNC_PASSWORD_FILE"
php "$SCRIPT_DIR/lib/piwigo-sync.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID" \
--base-url="http://127.0.0.1"
fi
python3 "$SCRIPT_DIR/lib/activity_gate.py" commit \
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT" \
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE"
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE" \
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW"
trap - ERR
write_status ok "Synchronisierung erfolgreich"

View File

@@ -29,8 +29,8 @@
</div>
<div class="bratonien-card" style="grid-column:1/-1">
<h4>Piwigo API prüfen</h4>
<p class="bratonien-base-note">Die API-Schlüssel-ID und das Geheimnis werden getrennt eingegeben. Bratonien Tools setzt daraus intern exakt den von Piwigo erwarteten Header <code>ID:Geheimnis</code> zusammen. Beide Werte werden nur für diesen Test verwendet, nicht gespeichert und es wird keine Synchronisation ausgelöst.</p>
<h4>Piwigo API bevorzugter Sync-Zugang</h4>
<p class="bratonien-base-note">Der NC Connector versucht bei jedem produktiven Lauf zuerst die Piwigo-API. Ein erfolgreich geprüfter API-Key wird verschlüsselt gespeichert. Die produktive API-Synchronisierung ist ausdrücklich an die im Plugin freigegebene Piwigo-Version gebunden. Ist diese Version nicht freigegeben oder die API nicht nutzbar, darf nur der Benutzername/Passwort-Fallback übernehmen.</p>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
@@ -39,7 +39,10 @@
<label class="bratonien-label" for="nc_piwigo_api_key_secret">API-Geheimnis</label>
<input id="nc_piwigo_api_key_secret" name="nc_piwigo_api_key_secret" type="password" autocomplete="off" required>
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_test">API prüfen</button></p>
<div class="bratonien-actions">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_test">API prüfen und speichern</button>
<button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_delete" formnovalidate onclick="return confirm('Gespeicherte Piwigo-API-Zugangsdaten wirklich löschen?');">Gespeicherte API löschen</button>
</div>
</form>
{if isset($NC_CONNECTOR.piwigo_api_test) && $NC_CONNECTOR.piwigo_api_test}
@@ -50,20 +53,41 @@
<span class="bratonien-label">Piwigo-Status</span><strong>{$NC_CONNECTOR.piwigo_api_test.status|escape:html}</strong>
<span class="bratonien-label">Administrator/Webmaster</span><strong>{if $NC_CONNECTOR.piwigo_api_test.admin}Ja{else}Nein{/if}</strong>
<span class="bratonien-label">Sichtbare API-Methoden</span><strong>{$NC_CONNECTOR.piwigo_api_test.method_count|escape:html}</strong>
<span class="bratonien-label">Mögliche Sync-/Site-Methoden</span><strong>{if $NC_CONNECTOR.piwigo_api_test.sync_api_detected}Ja{else}Nein{/if}</strong>
<span class="bratonien-label">Bratonien-Sync-API</span><strong>{if $NC_CONNECTOR.piwigo_api_test.sync_api_detected}Bereit{else}Nicht bereit{/if}</strong>
</div>
{if $NC_CONNECTOR.piwigo_api_test.sync_candidates|@count > 0}
<p><strong>Gefundene Kandidaten:</strong></p>
<ul>
{foreach from=$NC_CONNECTOR.piwigo_api_test.sync_candidates item=method}
<li><code>{$method|escape:html}</code></li>
{/foreach}
</ul>
{/if}
<p class="bratonien-base-note"><strong>Bewertung:</strong> {$NC_CONNECTOR.piwigo_api_test.conclusion|escape:html}</p>
{/if}
</div>
<div class="bratonien-card" style="grid-column:1/-1">
<h4>Benutzername/Passwort-Fallback</h4>
<p class="bratonien-base-note">Dieser Zugang wird nur verwendet, wenn die bevorzugte API nicht genutzt werden kann. Zugangsdaten können einmalig für einen manuellen Fallback verwendet, verschlüsselt dauerhaft hinterlegt oder vollständig gelöscht werden.</p>
{if $NC_CONNECTOR.connection_count > 0}
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_fallback_connection">Verbindung</label>
<select id="nc_fallback_connection" name="connection_id" required>
{foreach from=$NC_CONNECTOR.connections item=connection}
<option value="{$connection.id|escape:html}">{$connection.name|escape:html}</option>
{/foreach}
</select>
<label class="bratonien-label" for="nc_fallback_user">Piwigo-Benutzer</label>
<input id="nc_fallback_user" name="nc_fallback_user" type="text" autocomplete="username">
<label class="bratonien-label" for="nc_fallback_password">Piwigo-Passwort</label>
<input id="nc_fallback_password" name="nc_fallback_password" type="password" autocomplete="current-password">
</div>
<div class="bratonien-actions">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_once">Einmalig verwenden</button>
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_save">Fest speichern</button>
<button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_fallback_delete" formnovalidate onclick="return confirm('Gespeicherten Benutzername/Passwort-Fallback wirklich löschen?');">Gespeicherten Fallback löschen</button>
</div>
</form>
{else}
<p class="bratonien-base-note">Noch keine Connector-Verbindung vorhanden.</p>
{/if}
</div>
<div class="bratonien-card" style="grid-column:1/-1">
<h4>Neue lokale Verbindung</h4>
<form method="post">
@@ -78,8 +102,8 @@
<label class="bratonien-label" for="nc_source_view">Source-View</label><input id="nc_source_view" name="nc_source_view" type="text" value="piwigo_showcase_sources" required>
<label class="bratonien-label" for="nc_activity_view">Activity-View</label><input id="nc_activity_view" name="nc_activity_view" type="text" value="piwigo_showcase_activity" required>
<label class="bratonien-label" for="nc_gallery_root">Piwigo-Galeriepfad</label><input id="nc_gallery_root" name="nc_gallery_root" type="text" placeholder="/var/www/piwigo/galleries/nextcloud" required>
<label class="bratonien-label" for="nc_piwigo_user">Piwigo-Sync-Benutzer</label><input id="nc_piwigo_user" name="nc_piwigo_user" type="text" required>
<label class="bratonien-label" for="nc_piwigo_password">Piwigo-Sync-Passwort</label><input id="nc_piwigo_password" name="nc_piwigo_password" type="password" autocomplete="new-password" required>
<label class="bratonien-label" for="nc_piwigo_user">Piwigo-Fallback-Benutzer</label><input id="nc_piwigo_user" name="nc_piwigo_user" type="text" required>
<label class="bratonien-label" for="nc_piwigo_password">Piwigo-Fallback-Passwort</label><input id="nc_piwigo_password" name="nc_piwigo_password" type="password" autocomplete="new-password" required>
<label class="bratonien-label" for="nc_quiet_seconds">Ruhezeit</label><input id="nc_quiet_seconds" name="nc_quiet_seconds" type="number" min="0" value="120">
<label class="bratonien-label" for="nc_max_wait_seconds">Maximale Wartezeit</label><input id="nc_max_wait_seconds" name="nc_max_wait_seconds" type="number" min="60" value="900">
<label class="bratonien-label" for="nc_full_sync_seconds">Vollprüfung nach</label><input id="nc_full_sync_seconds" name="nc_full_sync_seconds" type="number" min="300" value="86400">