mirror of
https://github.com/Terranom674/Piwigo_Bratonien_Tools.git
synced 2026-09-20 18:03:16 +00:00
Compare commits
45 Commits
f41f885105
...
0a300b6cbf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a300b6cbf | ||
|
|
4be078fe07 | ||
|
|
83e71316f0 | ||
|
|
44f6487f94 | ||
|
|
3f5606035e | ||
|
|
f16b01ce18 | ||
|
|
25650ef9e8 | ||
|
|
8216a8c508 | ||
|
|
9ae3c79a0b | ||
|
|
a7a7214fda | ||
|
|
a4083ee15d | ||
|
|
e17c45760a | ||
|
|
983af29aa5 | ||
|
|
04f7ef4758 | ||
|
|
e25e519e7a | ||
|
|
16e4c334bb | ||
|
|
0a341385fc | ||
|
|
ad09e42c40 | ||
|
|
a57be97f48 | ||
|
|
61a3d8daeb | ||
|
|
8e403ccd46 | ||
|
|
d6fbd0ff6d | ||
|
|
e2c46f21df | ||
|
|
b91276f752 | ||
|
|
e28daf5e24 | ||
|
|
76a48b89bd | ||
|
|
b3fc198aea | ||
|
|
b834ffbdbc | ||
|
|
ebd3cc16f9 | ||
|
|
0d02141c16 | ||
|
|
fd8fa89ee0 | ||
|
|
c1ef3c5a2a | ||
|
|
5d67b7e3a6 | ||
|
|
4d7a85436f | ||
|
|
025bf3c4f8 | ||
|
|
be47674d20 | ||
|
|
b56990c827 | ||
|
|
b67300b8e8 | ||
|
|
a980236cef | ||
|
|
da846fd737 | ||
|
|
941e6a375b | ||
|
|
e76b054993 | ||
|
|
7450988ff0 | ||
|
|
5523c107e9 | ||
|
|
69a939637c |
122
CURRENT_STATUS.md
Normal file
122
CURRENT_STATUS.md
Normal 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.
|
||||
31
README.md
31
README.md
@@ -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.
|
||||
13
admin.php
13
admin.php
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
246
include/nc_connector_auth.inc.php
Normal file
246
include/nc_connector_auth.inc.php
Normal 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.');
|
||||
}
|
||||
110
include/nc_connector_create_api.inc.php
Normal file
110
include/nc_connector_create_api.inc.php
Normal 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.'
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
251
include/nc_orphan_ws.inc.php
Normal file
251
include/nc_orphan_ws.inc.php
Normal 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),
|
||||
);
|
||||
}
|
||||
130
include/nc_productive_ws.inc.php
Normal file
130
include/nc_productive_ws.inc.php
Normal 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'] : '',
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
),
|
||||
|
||||
30
main.inc.php
30
main.inc.php
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
329
runtime/lib/piwigo-sync.php
Normal 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);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user