diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b6b0575..857db62 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,8 +5,13 @@ on: pull_request: jobs: - syntax: + php-syntax: + name: PHP ${{ matrix.php }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3', '8.4', '8.5'] steps: - name: Repository auschecken uses: actions/checkout@v4 @@ -14,7 +19,7 @@ jobs: - name: PHP installieren uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: ${{ matrix.php }} coverage: none - name: PHP Syntax pruefen @@ -25,6 +30,43 @@ jobs: php -l "$file" done < <(find . -type f -name '*.php' -print0) + - name: Doppelte Bratonien-Funktionen pruefen + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + import pathlib + import re + import sys + + found = {} + for path in pathlib.Path('.').rglob('*.php'): + text = path.read_text(encoding='utf-8', errors='ignore') + for name in re.findall(r'\bfunction\s+(bratonien_tools_[A-Za-z0-9_]+)\s*\(', text): + found.setdefault(name, []).append(str(path)) + + duplicates = {name: paths for name, paths in found.items() if len(paths) > 1} + if duplicates: + for name, paths in sorted(duplicates.items()): + print(f'{name}: {", ".join(paths)}', file=sys.stderr) + sys.exit(1) + PY + + script-syntax: + name: JavaScript, Python, Shell + runs-on: ubuntu-latest + steps: + - name: Repository auschecken + uses: actions/checkout@v4 + + - name: JavaScript Syntax pruefen + shell: bash + run: | + set -euo pipefail + while IFS= read -r -d '' file; do + node --check "$file" + done < <(find . -type f -name '*.js' -print0) + - name: Python Syntax pruefen shell: bash run: | diff --git a/CURRENT_STATUS.md b/CURRENT_STATUS.md index 112706a..f582131 100644 --- a/CURRENT_STATUS.md +++ b/CURRENT_STATUS.md @@ -1,139 +1,62 @@ # Aktueller Entwicklungsstand -Stand: 19.08.2026 +## Version -## Plugin +**Bratonien Tools 0.9.6.2** -- Aktuelle Plugin-Version: **0.9.6.1** -- Aktueller Entwicklungsblock: **NC Connector – WebDAV-End-to-End-Pfad mit Piwigo-Registrierung, Preview-/Derivatstrecke und sauberem Verbindungs-Lebenszyklus** -- GitHub ist das führende Repository. -- Der bestehende lokale Connector bleibt vollständig erhalten und wird nicht automatisch migriert. +## NC Connector -## Nicht verhandelbare Architekturregeln +Der NC Connector besitzt nur noch einen produktiven Verbindungsweg: **Nextcloud per WebDAV**. -- Nextcloud bleibt die Quelle der Originalbilder. -- Originalbilder werden **nicht dauerhaft nach Piwigo kopiert**. -- Der bestehende lokale Produktionsweg bleibt funktionsfähig. -- WebDAV benötigt nur Nextcloud-Adresse, Benutzer/App-Passwort und normale WebDAV-Berechtigungen des Benutzers. -- Kein Nextcloud-PostgreSQL-Zugriff, kein `occ`, keine Storage-IDs, keine Host-Mounts, kein FUSE, davfs oder rclone als Voraussetzung. -- WebDAV-Verbindungen werden voneinander isoliert verarbeitet. -- Fehler der WebDAV-Bildverarbeitung dürfen keinen normalen Piwigo-Seitenaufruf zum Absturz bringen. +### Verbindungsmodell -## Bestehende Quellenmodi +- `adapter=remote` +- `source_mode=webdav-placeholder` +- Nextcloud-Adresse, Benutzer und Passwort/App-Passwort werden pro Verbindung verschlüsselt gespeichert. +- Piwigo-API und Benutzername/Passwort-Fallback werden ebenfalls pro Verbindung gespeichert. +- Der Assistent zeigt ausschließlich die Verzeichnisse, auf die der angemeldete Nextcloud-Benutzer per WebDAV zugreifen kann. +- Mehrere Verzeichnisse können pro Verbindung ausgewählt werden. -Weiterhin vorhanden: +### Laufzeit -- `legacy-view` -- `user-shares` -- `selected-fileids` +Der gemeinsame Runner verarbeitet ausschließlich WebDAV-Verbindungen: -Neuer WebDAV-Modus: +1. `runtime/reconcile-webdav.php` +2. `runtime/cleanup-webdav-piwigo.php` +3. `runtime/sync-webdav.sh` für jede vorhandene WebDAV-Konfiguration -- `webdav-placeholder` +Die verbindungsspezifischen Runtime-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`. Zustandsdaten liegen unter `/var/lib/bratonien-tools/nc-connector/connection-ID`. -Es gibt keine automatische Migration bestehender Verbindungen auf WebDAV. +### Shadow Tree und Piwigo -## WebDAV-Ablauf in 0.9.6.1 +`runtime/lib/build_webdav_placeholder_source.py` bildet die ausgewählten Nextcloud-Inhalte als lokale Platzhalterquelle ab. `runtime/lib/shadow_tree.py` erzeugt daraus die Dateisystemstruktur, die Piwigo als physische Site sieht. -1. Der Assistent prüft Nextcloud über WebDAV und lässt sichtbare Verzeichnisse auswählen. -2. `runtime/reconcile-webdav.php` erzeugt die verbindungseigene Runtime-Konfiguration. -3. `runtime/lib/build_webdav_placeholder_source.py` liest die ausgewählten Verzeichnisse rekursiv per PROPFIND. -4. Ordner, Dateinamen, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst. -5. Eine lokale Platzhalterquelle wird erzeugt, ohne Originalbilder dauerhaft zu speichern. -6. `runtime/lib/shadow_tree.py` baut daraus den verbindungseigenen Shadow Tree. -7. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID` und wird als eigene physische Piwigo-Site registriert. -8. Dadurch erscheint die ausgewählte Nextcloud-Wurzel als normales Piwigo-Album; technische `bratonien-webdav-ID`-Wrapper werden nicht angezeigt. -9. Vorbereitete Arbeitsbilder liegen unter `_data/bratonien-tools/nc-webdav-preview/connection-ID`. -10. Arbeitsbilder werden als JPEG beziehungsweise PNG erzeugt und maximal auf 4096 px Kantenlänge begrenzt, damit Piwigos Bildbibliothek sie zuverlässig verarbeiten kann. -11. Der CLI-Derivat-Builder korrigiert die Piwigo-Bildabmessungen anhand des Arbeitsbilds und erzeugt die aktuell konfigurierten Standard- und Custom-Derivate. -12. Der normale Frontend-Aufruf erzeugt **keine** Derivate. Fehlt ein Derivat, wird das vorbereitete Bild als sicherer Fallback ausgeliefert. -13. Originalbilder werden bei echtem Originalabruf serverseitig über `webdav-image.php` aus Nextcloud gestreamt; Nextcloud-Zugangsdaten erscheinen nicht im Browser. +Jede WebDAV-Verbindung besitzt ihre eigene Piwigo-Site. `runtime/lib/piwigo-sync.php` übergibt genau diese Site an `bratonien.nc.syncProductive` und `bratonien.nc.syncOrphans`. Es gibt keinen zusätzlichen Sync auf Site 1. -## Piwigo-Synchronisierung +Der Shadow Tree dient ausschließlich dazu, Piwigo die Alben und Bilder als Dateisystemstruktur bereitzustellen. Albumlogik, Dateisynchronisierung und die daraus folgenden Piwigo-Datensätze bleiben Aufgabe von Piwigo. -WebDAV verwendet denselben Piwigo-Synchronisationspfad wie der bestehende Connector, aber mit einer verbindungseigenen physischen Site. +### Bildauslieferung -Nach der Dateisynchronisierung laufen die vorhandenen Piwigo-Nacharbeiten, darunter Metadaten-, Integritäts-, Kategorie-, Pfad-, Rang- und Cache-Aktualisierungen sowie der Orphan-Abgleich. +Die Dateien im Shadow Tree sind Platzhalter. `include/webdav_image_runtime.inc.php` erkennt die WebDAV-Zuordnung und ersetzt die Bildquelle bei der Auslieferung durch den zugehörigen Nextcloud-Inhalt. `webdav-image.php` streamt die Bilddaten serverseitig, ohne Nextcloud-Zugangsdaten an den Browser weiterzugeben. -Die direkte produktive Synchronisierung ist derzeit ausdrücklich auf **Piwigo 16.4.0** abgestimmt. +### Löschen -## WebDAV-Bildausgabe und Derivate +Eine WebDAV-Verbindung kann unabhängig von ihrer Reihenfolge gelöscht werden. Beim Löschen werden die zugehörige Piwigo-Site und die zugehörigen Piwigo-Datensätze entfernt. Die Originaldateien in Nextcloud bleiben unangetastet. Verwaiste WebDAV-Runtime- und Piwigo-Daten werden vom gemeinsamen Lauf bereinigt. -Wichtige Dateien: +### Diagnose -- `include/webdav_image_runtime.inc.php` – Zuordnung Piwigo-Bild ↔ WebDAV-Quelle, Preview-/Derivat-Helfer und URL-Filter. -- `webdav-image.php` – berechtigungsgeprüfter serverseitiger Original-/Preview-Abruf. -- `runtime/lib/precache-webdav-previews.php` – vorbereitete JPEG-/PNG-Arbeitsbilder ohne dauerhafte Originalkopie. -- `runtime/lib/build-webdav-derivatives.php` – CLI-Aufbau der konfigurierten Piwigo-Derivate. +Der Status wird pro Verbindung geführt. Fehler enthalten Zeitpunkt, betroffene Verbindung, Prozesszustand und technische Details. Die Administration zeigt keine lokalen Datenbank-, Storage- oder Mount-Einstellungen mehr an. -Der Frontend-Hook führt keine Bildgenerierung mehr aus. Damit bleibt ein Fehler in GD/Imagick/External ImageMagick auf den Hintergrundlauf begrenzt. +## Entfernte Connector-Komponenten -## Löschen einer WebDAV-Verbindung seit 0.9.6.1 +Nicht mehr Bestandteil des Plugins sind lokale Nextcloud-Datenbankanbindungen, PostgreSQL-Reader, Source-/Activity-Views, Storage-Mappings, Host-Mounts, lokale Reconcile-/Sync-Skripte, Cutover-/Migrationswerkzeuge und der frühere Simulations-Webservice `bratonien.nc.sync`. -Eine gelöschte WebDAV-Verbindung darf keine weiterhin sichtbaren oder über alte Piwigo-Datensätze erreichbaren Bilder hinterlassen. +## Prüfpflicht vor Merge -Beim Löschen über die Oberfläche: +Jede Änderung wird vor dem Merge mindestens mit folgenden GitHub-Actions-Prüfungen validiert: -- werden die zur Verbindung gehörende Piwigo-Site, ihre Alben und Bilddatensätze entfernt; -- werden zugehörige Piwigo-Derivate entfernt; -- bleiben die Nextcloud-Originaldateien unangetastet; -- werden Laufzeit- und Preview-Daten anschließend bereinigt. - -Zusätzlich läuft `runtime/cleanup-webdav-piwigo.php` im gemeinsamen Runner. Er erkennt verwaiste WebDAV-Piwigo-Sites, für die keine Connector-Verbindung mehr existiert, und entfernt deren Piwigo-Inhalte sowie Gallery-, Source-, Preview-, State-, Status- und Runtime-Reste. Damit werden auch bereits vor 0.9.6.1 gelöschte Verbindungen nachträglich bereinigt. - -## Gemeinsamer Runner - -`runtime/run-all.sh` verarbeitet die Schritte in kontrollierter Reihenfolge: - -1. lokale Verbindungen reconciliieren; -2. WebDAV-Verbindungen reconciliieren; -3. verwaiste WebDAV-Piwigo-Inhalte bereinigen; -4. verwaiste allgemeine Runtime-Dateien bereinigen; -5. aktive WebDAV-Verbindungen synchronisieren; -6. aktive lokale Verbindungen synchronisieren. - -Der vorhandene systemd-Timer ruft direkt die Runtime aus dem installierten Plugin auf. - -## Status- und UI-Regeln - -- Hintergrund-Polling lädt die Admin-Seite nicht neu. -- Ein geöffneter Verbindungsassistent bleibt bei Statusaktualisierungen geöffnet. -- Fehlgeschlagene Fallback-/Credential-Prüfungen schließen den Assistenten nicht. -- Laufzeitfehler unterscheiden WebDAV-Lesen, Piwigo-Synchronisierung, Preview- und Derivatfehler. - -## Sicherheit - -- Connector-Zugangsdaten werden verschlüsselt gespeichert. -- WebDAV-Credentials werden nur serverseitig verwendet. -- Bildzugriffe über `webdav-image.php` werden gegen Piwigos Zugriffsrechte geprüft. -- Nextcloud-Originale werden weder beim Sync noch beim Löschen einer Verbindung verändert. -- Derivate und Arbeitsbilder sind lokale Cache-/Arbeitsdaten und dürfen entfernt beziehungsweise neu aufgebaut werden. - -## Aktueller Prüfstand - -Bereits umgesetzt: - -- WebDAV-Verbindungsassistent; -- Verzeichnisauswahl; -- rekursiver WebDAV-Scan; -- Shadow Tree; -- separate Piwigo-Site je WebDAV-Verbindung; -- sichtbare Albumstruktur ohne technische Wrapper; -- echte Bildausgabe aus Nextcloud; -- lokale vorbereitete Arbeitsbilder; -- CLI-Derivatstrecke; -- Piwigo-Nacharbeiten und Orphan-Abgleich; -- ETag-basierte Wiederverwendung vorbereiteter Bilder; -- Löschen einer Verbindung einschließlich Piwigo-Inhalten; -- nachträgliche Bereinigung bereits verwaister WebDAV-Piwigo-Sites. - -Weiter zu prüfen beziehungsweise unter realer Last zu härten: - -- vollständiger Derivataufbau für alle in der Installation vorkommenden Standard- und Custom-Größen; -- Verhalten bei großen Bildmengen und vielen gleichzeitig noch nicht vorbereiteten Bildern; -- Fehlerfälle verschiedener Bildformate und Bild-Backends; -- langfristige Änderungserkennung und Performance im produktiven Einsatz. - -## Versionsregel - -Die aktuelle Versionslinie ist **0.9.6.x**. Weitere Sprünge auf eine neue Minor-/Major-Linie erfolgen nur bewusst und ausdrücklich. +- PHP-Syntax für 8.2, 8.3, 8.4 und 8.5 +- Prüfung auf doppelte globale `bratonien_tools_*`-Funktionen +- JavaScript-Syntax +- Python-Syntax/Compile +- Shell-Syntax mit `bash -n` diff --git a/README.md b/README.md index 5ebb132..04dcf45 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Modulares Piwigo-Plugin für Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo. -Aktuelle Plugin-Version: **0.9.6.1** +Aktuelle Plugin-Version: **0.9.6.2** ## Grundprinzip @@ -12,7 +12,7 @@ Für die Administration gilt: Der normale Nutzer sieht Aufgaben, Entscheidungen ## NC Connector -Der NC Connector synchronisiert ausdrücklich ausgewählte Nextcloud-Inhalte mit Piwigo. +Der NC Connector bindet ausdrücklich ausgewählte Nextcloud-Inhalte ausschließlich per WebDAV an Piwigo an. ### Datenmodell @@ -22,21 +22,9 @@ Der NC Connector synchronisiert ausdrücklich ausgewählte Nextcloud-Inhalte mit - Ordner werden als physische Piwigo-Alben synchronisiert. - Root-Dateien können als Piwigo-Orphans registriert werden. - Neu importierte physische Connector-Alben werden privat angelegt. -- Entfernte Quellen werden aus Shadow Tree und Piwigo entfernt; die Nextcloud-Originale bleiben erhalten. +- Entfernte Quellen verschwinden aus Shadow Tree und Piwigo; die Nextcloud-Originale bleiben erhalten. -### Bestehende lokale Quellenmodi - -Der bestehende Connector unterstützt weiterhin: - -- `legacy-view` -- `user-shares` -- `selected-fileids` - -Diese Modi bleiben erhalten. Es gibt keine automatische Migration auf WebDAV. - -### WebDAV-Modus - -Der neue Endnutzerpfad verwendet `source_mode=webdav-placeholder` und `adapter=remote`. +### Voraussetzungen Benötigt werden nur: @@ -45,15 +33,7 @@ Benötigt werden nur: - normaler WebDAV-Zugriff auf die Inhalte dieses Benutzers; - eine normale Piwigo-Installation mit ihrer vorhandenen PHP-/Bildverarbeitungsumgebung. -Nicht vorausgesetzt werden: - -- PostgreSQL-Zugriff auf Nextcloud; -- `occ`-Adminzugriff; -- Storage-IDs oder Backend-Pfade; -- zusätzliche Host-Mounts; -- FUSE; -- davfs; -- rclone. +Nicht benötigt werden Nextcloud-Datenbankzugriff, `occ`-Adminzugriff, Storage-IDs, Backend-Pfade, zusätzliche Host-Mounts, FUSE, davfs oder rclone. ### WebDAV-Ablauf @@ -63,54 +43,34 @@ Nicht vorausgesetzt werden: 4. `runtime/lib/build_webdav_placeholder_source.py` erzeugt eine lokale Platzhalterquelle ohne dauerhafte Originalkopie. 5. `runtime/lib/shadow_tree.py` baut daraus den verbindungseigenen Shadow Tree. 6. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID`. -7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert. Die ausgewählte Nextcloud-Wurzel erscheint dadurch direkt als Album; technische `bratonien-webdav-ID`-Wrapper bleiben unsichtbar. -8. `runtime/lib/precache-webdav-previews.php` erzeugt vorbereitete Arbeitsbilder als JPEG beziehungsweise PNG mit maximal 4096 px Kantenlänge. -9. `runtime/lib/build-webdav-derivatives.php` korrigiert die Piwigo-Bildabmessungen und erzeugt die konfigurierten Standard- und Custom-Derivate im Hintergrundlauf. -10. Der Frontend-Aufruf erzeugt keine Derivate. Fehlt eines, wird das vorbereitete Arbeitsbild als sicherer Fallback ausgeliefert. -11. Wenn das Original benötigt wird, streamt `webdav-image.php` es serverseitig direkt aus Nextcloud. Zugangsdaten werden dem Browser nicht offengelegt. +7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert. +8. Piwigos Dateisynchronisierung erhält ausschließlich diese verbindungseigene Site. +9. Die WebDAV-Bildzuordnung ersetzt beim Ausliefern Platzhalter-URLs durch die echte Nextcloud-Bildquelle. +10. `webdav-image.php` streamt benötigte Bilder serverseitig; Zugangsdaten werden dem Browser nicht offengelegt. ### Piwigo-Synchronisierung -Der WebDAV-Pfad nutzt die vorhandene Piwigo-Synchronisationslogik mit einer verbindungseigenen Site. +Der Connector verwendet die vorhandene Piwigo-Dateisynchronisierung für die verbindungseigene WebDAV-Site. Der Shadow Tree ist die Dateisystemdarstellung, die Piwigo für Alben und Bilder benötigt. -Nach dem Dateiabgleich laufen die vorhandenen Nacharbeiten, darunter: +`runtime/lib/piwigo-sync.php` ruft dafür `bratonien.nc.syncProductive` auf. Die Methode führt den Piwigo-Core-Sync für die konkrete Site aus. `bratonien.nc.syncOrphans` arbeitet ebenfalls nur auf der übergebenen WebDAV-Site. -- Metadaten-Synchronisierung; -- Bild- und Kategorieintegrität; -- Uppercats/Kategoriestruktur; -- globale Ränge; -- Pfadpflege; -- Rating-Score; -- Benutzer-Cache-Invalidierung; -- Orphan-Abgleich. +Die produktive Synchronisierung ist aktuell auf **Piwigo 16.4.0** abgestimmt. -Die direkte produktive Synchronisierung ist aktuell ausdrücklich auf **Piwigo 16.4.0** abgestimmt. +### Piwigo-Zugang -### Fallback-Zugang - -Piwigo wird API-first angesprochen. Ist für eine Verbindung keine API konfiguriert, kann Benutzername/Passwort als Fallback verwendet werden. - -Der Assistent prüft Fallback-Zugangsdaten vor dem Speichern. Eine fehlgeschlagene Prüfung schließt oder leert den Assistenten nicht. +Der Zugang wird pro Verbindung gespeichert. Bevorzugt wird eine Piwigo-API. Wird keine API eingerichtet, kann ein Administrator-/Webmaster-Benutzer mit Passwort als Fallback verwendet werden. ### Statusanzeige -Die Administration zeigt den Laufzeitstatus regelmäßig im Hintergrund an. Dafür wird die Admin-Seite nicht neu geladen; offene Dialoge, Assistenten und aufgeklappte Bereiche bleiben erhalten. - -Fehler werden nach Prozessschritt getrennt dargestellt, unter anderem für WebDAV-Einlesen, Piwigo-Synchronisierung, Preview-Erzeugung und Derivat-Erzeugung. +Die Administration zeigt Laufzeitstatus und Fehler pro Verbindung. Fehlerdetails bleiben der betroffenen Verbindung zugeordnet. ### Löschen einer Verbindung -Seit **0.9.6.1** besitzt der WebDAV-Pfad einen vollständigen Lösch-Lebenszyklus: - -- Beim Löschen einer WebDAV-Verbindung werden ihre Piwigo-Site, Alben und Bilddatensätze entfernt. -- Zugehörige Piwigo-Derivate werden entfernt. -- Nextcloud-Originale bleiben unverändert. -- Runtime-, Shadowtree-, Source-, Preview- und Statusdaten werden bereinigt. -- `runtime/cleanup-webdav-piwigo.php` erkennt zusätzlich bereits verwaiste WebDAV-Piwigo-Sites, für die keine Connector-Verbindung mehr existiert, und bereinigt sie im gemeinsamen Runner nachträglich. +Beim Löschen einer WebDAV-Verbindung werden ihre zugehörige Piwigo-Site und die dazugehörigen Piwigo-Datensätze entfernt. Nextcloud-Dateien bleiben unverändert. Runtime-, Shadowtree-, Source-, Preview- und Statusdaten werden bereinigt. ### Runtime -Aktive Verbindungen werden über den gemeinsamen Runner verarbeitet: +Aktive WebDAV-Verbindungen werden über den gemeinsamen Runner verarbeitet: - `bratonien-nc-connector.timer` - `bratonien-nc-connector.service` @@ -118,25 +78,15 @@ Aktive Verbindungen werden über den gemeinsamen Runner verarbeitet: Verbindungsspezifische Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`, State-Daten unter `/var/lib/bratonien-tools/nc-connector/connection-ID`. -Die Reihenfolge des gemeinsamen Laufs ist: +Der Lauf besteht aus: -1. lokale Verbindungen reconciliieren; -2. WebDAV-Verbindungen reconciliieren; -3. verwaiste WebDAV-Piwigo-Inhalte bereinigen; -4. allgemeine verwaiste Runtime-Dateien bereinigen; -5. WebDAV-Verbindungen synchronisieren; -6. lokale Verbindungen synchronisieren. +1. WebDAV-Verbindungen reconciliieren; +2. verwaiste WebDAV-Piwigo-Inhalte bereinigen; +3. jede vorhandene WebDAV-Verbindung synchronisieren. ## Bildcache -Bratonien Tools kann: - -- Piwigo-Bildderivate gezielt leeren; -- vorhandene Bildgrößen neu erzeugen; -- den Cache-Aufbau als Worker-Prozess starten und abbrechen; -- die Worker-Zahl automatisch oder manuell konfigurieren. - -Originalbilder bleiben unangetastet. +Bratonien Tools kann Piwigo-Bildderivate gezielt leeren, vorhandene Bildgrößen neu erzeugen und den Cache-Aufbau als Worker-Prozess starten bzw. abbrechen. Originalbilder bleiben unangetastet. ## Wasserzeichenverwaltung @@ -171,14 +121,7 @@ Alben können in Bratonien Tools zwischen öffentlich und privat umgeschaltet we ## Geschützte Albumfreigaben -Private Alben können über eigene Freigabelinks geteilt werden: - -- optionales Passwort; -- optionales Ablaufdatum; -- eigener technischer Piwigo-Benutzer pro Freigabe; -- minimaler Albumzugriff; -- Hash-Speicherung von Passwörtern und Tokens; -- Widerruf und automatische Bereinigung beim Löschen eines Albums. +Private Alben können über eigene Freigabelinks geteilt werden, optional mit Passwort und Ablaufdatum. Die Freigaben verwenden einen technischen Piwigo-Benutzer mit minimalem Albumzugriff und können widerrufen werden. ## Erweiterte Bildnavigation @@ -186,34 +129,25 @@ Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges B ## Selbstaktualisierung -Der integrierte Updater: - -- liest den Zielstand aus GitHub; -- bindet ein Update an einen konkreten Commit; -- prüft Version und SHA-256 des Zielstands; -- erstellt vor dem Austausch ein Backup; -- nutzt Post/Redirect/Get; -- verlangt Webmaster-Rechte, `ZipArchive` und ausreichende Schreibrechte. +Der integrierte Updater liest den Zielstand aus GitHub, bindet ein Update an einen konkreten Commit, prüft Version und SHA-256, erstellt vor dem Austausch ein Backup und verlangt Webmaster-Rechte sowie die notwendigen Servervoraussetzungen. ## Wichtige Dateien und Verzeichnisse - `main.inc.php` – Plugin-Einstieg und Runtime-Hooks - `admin.php` – zentraler Admin-Controller -- `include/nc_connector.inc.php` – Datenmodell und gemeinsame Connector-Funktionen +- `include/nc_connector.inc.php` – WebDAV-Verbindungsmodell - `include/nc_connector_wizard.inc.php` – Verbindungsassistent -- `include/nc_connector_wizard_webdav_flow.inc.php` – WebDAV-first-Wizardablauf -- `include/nc_connector_delete_safe.inc.php` – sicheres Löschen einschließlich WebDAV-Piwigo-Inhalten -- `include/nc_productive_ws.inc.php` – produktiver Piwigo-Dateisync -- `include/nc_orphan_ws.inc.php` – Orphan-Synchronisierung -- `include/webdav_image_runtime.inc.php` – WebDAV-Bildzuordnung, Preview-/Derivathilfen und URL-Filter +- `include/nc_connector_wizard_webdav_flow.inc.php` – WebDAV-Wizardablauf +- `include/nc_connector_delete_safe.inc.php` – Löschen einschließlich WebDAV-Piwigo-Inhalten +- `include/nc_productive_ws.inc.php` – Piwigo-Core-Dateisync +- `include/nc_orphan_ws.inc.php` – Orphan-Synchronisierung der konkreten WebDAV-Site +- `include/webdav_image_runtime.inc.php` – WebDAV-Bildzuordnung und URL-Filter - `webdav-image.php` – berechtigungsgeprüfter WebDAV-Bildstream - `runtime/reconcile-webdav.php` – WebDAV-Runtime-Reconcile -- `runtime/cleanup-webdav-piwigo.php` – Bereinigung gelöschter/verwaister WebDAV-Verbindungen in Piwigo +- `runtime/cleanup-webdav-piwigo.php` – Bereinigung verwaister WebDAV-Piwigo-Sites - `runtime/sync-webdav.sh` – Ablauf einer WebDAV-Verbindung -- `runtime/run-all.sh` – gemeinsamer Multi-Connection-Runner +- `runtime/run-all.sh` – gemeinsamer WebDAV-Runner - `runtime/lib/build_webdav_placeholder_source.py` – rekursiver WebDAV-Scan und Platzhalterquelle -- `runtime/lib/precache-webdav-previews.php` – vorbereitete JPEG-/PNG-Arbeitsbilder -- `runtime/lib/build-webdav-derivatives.php` – CLI-Derivat-Builder - `runtime/lib/shadow_tree.py` – atomarer Shadow Tree - `runtime/lib/piwigo-sync.php` – Piwigo-Sync mit API/Fallback - `main-cache-build.php` – allgemeiner Piwigo-Derivat-/Cache-Builder @@ -231,16 +165,7 @@ Der integrierte Updater: - Connector-Zugangsdaten werden verschlüsselt gespeichert; - Nextcloud-Zugangsdaten werden verbindungseigen gespeichert und nur serverseitig verwendet; - Wizard-Geheimnisse werden nicht im Browser-Web-Storage persistiert; -- SQL-View-Namen werden vor Verwendung validiert; - produktive Piwigo-API ist versionsgebunden; - Originalbilder werden vom Connector nicht gelöscht; - WebDAV-Originalbilder werden nicht dauerhaft lokal gespeichert; - Update-Pakete werden an Commit und Hash gebunden. - -## Entwicklungsstand - -`0.9.6.x` ist die aktuelle Versionslinie. - -Mit **0.9.6.1** ist der WebDAV-Pfad einschließlich Piwigo-Registrierung, vorbereiteter Bildquelle, CLI-Derivatstrecke und Lösch-/Cleanup-Lebenszyklus im Repository umgesetzt. Der bestehende lokale Connector bleibt parallel erhalten. - -Der jeweils detaillierte Prüf- und Entwicklungsstand steht in [`CURRENT_STATUS.md`](CURRENT_STATUS.md). diff --git a/admin.php b/admin.php index c151e1c..1dfece5 100644 --- a/admin.php +++ b/admin.php @@ -119,25 +119,9 @@ foreach ($nc_connector['connections'] as &$nc_connection) { $nc_connection['last_sync'] = bratonien_tools_nc_connector_connection_last_status($nc_connection); $nc_connection['display_name'] = $nc_connection['name']; - - $storage_lines = array(); - $storages = isset($nc_connection['config']['storages']) && is_array($nc_connection['config']['storages']) - ? $nc_connection['config']['storages'] - : array(); - foreach ($storages as $storage) - { - $storage_lines[] = (string)($storage['storage_id'] ?? '').' | '.(string)($storage['source_prefix'] ?? '').' | '.(string)($storage['local_mount'] ?? ''); - } - $nc_connection['storage_text'] = implode("\n", $storage_lines); - if (!empty($nc_connection['fallback_stored'])) { $nc_connection['display_name'] .= ' · Fallback gespeichert'; - $nc_connection['verification_checks'][] = array( - 'name' => 'Piwigo-Fallback', - 'ok' => true, - 'detail' => 'Benutzername/Passwort verschluesselt gespeichert', - ); } } unset($nc_connection); @@ -159,10 +143,6 @@ $nc_system_defaults = array( 'last_run_error_detail' => '', 'next_run_timestamp' => 0, 'next_run_label' => 'Nicht verfügbar', - 'legacy_runtime_exists' => false, - 'legacy_config_exists' => false, - 'legacy_service_exists' => false, - 'legacy_timer_exists' => false, ); $nc_connector['system'] = array_merge( $nc_system_defaults, diff --git a/include/nc_connector.inc.php b/include/nc_connector.inc.php index b28bfca..f84f8c5 100644 --- a/include/nc_connector.inc.php +++ b/include/nc_connector.inc.php @@ -4,15 +4,6 @@ if (!defined('PHPWG_ROOT_PATH')) die('Hacking attempt!'); } -/** - * NC Connector migration and verification phase. - * - * The existing /etc/piwigo-sync installation remains the production path. - * Bratonien Tools imports a copy of its configuration into its own connection - * store and verifies that copy independently. Importing and verification never - * change or stop the legacy sync. - */ - function bratonien_tools_nc_connector_table() { if (function_exists('bratonien_tools_table')) @@ -30,9 +21,8 @@ function bratonien_tools_nc_connector_ensure_table() id int(11) NOT NULL AUTO_INCREMENT, connection_key varchar(64) NOT NULL, name varchar(255) NOT NULL, - adapter enum('local','remote') NOT NULL DEFAULT 'local', - enabled tinyint(1) NOT NULL DEFAULT 0, - takeover_state enum('imported','verified','active','disabled') NOT NULL DEFAULT 'imported', + adapter enum('remote') NOT NULL DEFAULT 'remote', + enabled tinyint(1) NOT NULL DEFAULT 1, config_json mediumtext NOT NULL, secret_blob mediumtext DEFAULT NULL, created datetime NOT NULL, @@ -64,139 +54,59 @@ function bratonien_tools_nc_connector_secret_key() function bratonien_tools_nc_connector_encrypt_secret($plain) { $plain = (string)$plain; - if ($plain === '') - { - return ''; - } - if (!function_exists('openssl_encrypt')) - { - throw new RuntimeException('OpenSSL wird zum sicheren Speichern der Connector-Zugangsdaten benoetigt.'); - } + if ($plain === '') return ''; + if (!function_exists('openssl_encrypt')) throw new RuntimeException('OpenSSL wird zum sicheren Speichern der Connector-Zugangsdaten benötigt.'); $iv = random_bytes(12); $tag = ''; - $cipher = openssl_encrypt( - $plain, - 'aes-256-gcm', - bratonien_tools_nc_connector_secret_key(), - OPENSSL_RAW_DATA, - $iv, - $tag - ); - if ($cipher === false) - { - throw new RuntimeException('Connector-Zugangsdaten konnten nicht verschluesselt werden.'); - } + $cipher = openssl_encrypt($plain, 'aes-256-gcm', bratonien_tools_nc_connector_secret_key(), OPENSSL_RAW_DATA, $iv, $tag); + if ($cipher === false) throw new RuntimeException('Connector-Zugangsdaten konnten nicht verschlüsselt werden.'); return base64_encode(json_encode(array( - 'v' => 1, - 'iv' => base64_encode($iv), - 'tag' => base64_encode($tag), - 'data' => base64_encode($cipher), + 'v'=>1, + 'iv'=>base64_encode($iv), + 'tag'=>base64_encode($tag), + 'data'=>base64_encode($cipher), ))); } function bratonien_tools_nc_connector_decrypt_secret($blob) { $blob = trim((string)$blob); - if ($blob === '') - { - return ''; - } - if (!function_exists('openssl_decrypt')) - { - throw new RuntimeException('OpenSSL wird zum Lesen der Connector-Zugangsdaten benoetigt.'); - } + if ($blob === '') return ''; + if (!function_exists('openssl_decrypt')) throw new RuntimeException('OpenSSL wird zum Lesen der Connector-Zugangsdaten benötigt.'); $outer = base64_decode($blob, true); $payload = is_string($outer) ? json_decode($outer, true) : null; - if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) - { - throw new RuntimeException('Gespeicherte Connector-Zugangsdaten haben ein unbekanntes Format.'); - } + if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) throw new RuntimeException('Gespeicherte Connector-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); - if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) - { - throw new RuntimeException('Gespeicherte Connector-Zugangsdaten sind beschaedigt.'); - } - - $plain = openssl_decrypt( - $cipher, - 'aes-256-gcm', - bratonien_tools_nc_connector_secret_key(), - OPENSSL_RAW_DATA, - $iv, - $tag - ); - if ($plain === false) - { - throw new RuntimeException('Connector-Zugangsdaten konnten nicht entschluesselt werden.'); - } + if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) throw new RuntimeException('Gespeicherte Connector-Zugangsdaten sind beschädigt.'); + $plain = openssl_decrypt($cipher, 'aes-256-gcm', bratonien_tools_nc_connector_secret_key(), OPENSSL_RAW_DATA, $iv, $tag); + if ($plain === false) throw new RuntimeException('Connector-Zugangsdaten konnten nicht entschlüsselt werden.'); return (string)$plain; } -function bratonien_tools_nc_connector_read_config($path) +function bratonien_tools_nc_connector_is_webdav(array $row) { - $config = array(); - if (!is_readable($path)) - { - return $config; - } - - $lines = @file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - if (!is_array($lines)) - { - return $config; - } - - foreach ($lines as $line) - { - $line = trim($line); - if ($line === '' || $line[0] === '#') - { - continue; - } - if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches)) - { - continue; - } - - $value = trim($matches[2]); - $length = strlen($value); - if ($length >= 2) - { - $first = $value[0]; - $last = $value[$length - 1]; - if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) - { - $value = substr($value, 1, -1); - } - } - $config[$matches[1]] = $value; - } - - return $config; + $config = isset($row['config']) && is_array($row['config']) ? $row['config'] : array(); + return (string)($row['adapter'] ?? '') === 'remote' && (string)($config['source_mode'] ?? '') === 'webdav-placeholder'; } 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, secret_blob, created, updated FROM `$table` ORDER BY id"); + $result = pwg_query("SELECT id, connection_key, name, adapter, enabled, config_json, secret_blob, created, updated FROM `$table` WHERE adapter='remote' ORDER BY id"); $connections = array(); while ($row = pwg_db_fetch_assoc($result)) { $config = json_decode((string)$row['config_json'], true); - if (!is_array($config)) - { - $config = array(); - } - $verification = isset($config['verification']) && is_array($config['verification']) ? $config['verification'] : array(); + if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue; $has_fallback = false; try @@ -205,8 +115,7 @@ function bratonien_tools_nc_connector_connections() $credentials = json_decode($plain, true); if (is_array($credentials)) { - $has_fallback = trim((string)($credentials['piwigo_user'] ?? '')) !== '' - && (string)($credentials['piwigo_password'] ?? '') !== ''; + $has_fallback = trim((string)($credentials['piwigo_user'] ?? '')) !== '' && (string)($credentials['piwigo_password'] ?? '') !== ''; } } catch (Throwable $e) @@ -218,17 +127,9 @@ function bratonien_tools_nc_connector_connections() $row['id'] = (int)$row['id']; $row['enabled'] = (bool)$row['enabled']; $row['config'] = $config; - $row['host'] = isset($config['host']) ? (string)$config['host'] : ''; - $row['database'] = isset($config['database']) ? (string)$config['database'] : ''; - $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['user'] = (string)($config['nextcloud_access_user'] ?? $config['access_user'] ?? ''); + $row['storage_count'] = isset($config['roots']) && is_array($config['roots']) ? count($config['roots']) : 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'] : ''; - $row['source_count'] = isset($verification['source_count']) ? (int)$verification['source_count'] : null; - $row['verification_checks'] = isset($verification['checks']) && is_array($verification['checks']) ? $verification['checks'] : array(); $connections[] = $row; } @@ -240,233 +141,24 @@ function bratonien_tools_nc_connector_connection($id, $with_secret = false) bratonien_tools_nc_connector_ensure_table(); $table = bratonien_tools_nc_connector_table(); $id = (int)$id; - if ($id <= 0) - { - return null; - } + if ($id <= 0) return null; - $columns = 'id, connection_key, name, adapter, enabled, takeover_state, config_json, created, updated'; - if ($with_secret) - { - $columns .= ', secret_blob'; - } - $result = pwg_query("SELECT $columns FROM `$table` WHERE id = $id LIMIT 1"); - if (!pwg_db_num_rows($result)) - { - return null; - } + $columns = 'id, connection_key, name, adapter, enabled, config_json, created, updated'; + if ($with_secret) $columns .= ', secret_blob'; + $result = pwg_query("SELECT $columns FROM `$table` WHERE id = $id AND adapter='remote' LIMIT 1"); + if (!pwg_db_num_rows($result)) return null; $row = pwg_db_fetch_assoc($result); $row['id'] = (int)$row['id']; $row['enabled'] = (bool)$row['enabled']; $row['config'] = json_decode((string)$row['config_json'], true); - if (!is_array($row['config'])) - { - $row['config'] = array(); - } + if (!is_array($row['config']) || (string)($row['config']['source_mode'] ?? '') !== 'webdav-placeholder') return null; return $row; } -function bratonien_tools_nc_connector_import_bundle_path() -{ - return '/tmp/bratonien-tools-nc-import.json'; -} - -function bratonien_tools_nc_connector_import_legacy() -{ - $path = bratonien_tools_nc_connector_import_bundle_path(); - if (!is_readable($path)) - { - throw new RuntimeException('Kein lesbares Migrationspaket gefunden. Fuehre zuerst den angezeigten Connector-Migrationsbefehl im Piwigo-LXC aus.'); - } - - $raw = @file_get_contents($path); - $bundle = is_string($raw) ? json_decode($raw, true) : null; - if (!is_array($bundle) || (int)($bundle['format'] ?? 0) !== 1 || !is_array($bundle['config'] ?? null)) - { - throw new RuntimeException('Das Migrationspaket ist ungueltig oder unvollstaendig.'); - } - - $legacy = $bundle['config']; - $required = array('NC_DB_HOST', 'NC_DB_PORT', 'NC_DB_NAME', 'NC_DB_USER', 'NC_DB_VIEW'); - foreach ($required as $key) - { - if (!isset($legacy[$key]) || trim((string)$legacy[$key]) === '') - { - throw new RuntimeException('Im Migrationspaket fehlt '.$key.'.'); - } - } - - $password = isset($bundle['db_password']) ? (string)$bundle['db_password'] : ''; - if ($password === '') - { - throw new RuntimeException('Das Migrationspaket enthaelt kein Datenbankpasswort.'); - } - - $config = array( - 'host' => (string)$legacy['NC_DB_HOST'], - 'port' => (string)$legacy['NC_DB_PORT'], - 'database' => (string)$legacy['NC_DB_NAME'], - 'user' => (string)$legacy['NC_DB_USER'], - 'source_view' => (string)$legacy['NC_DB_VIEW'], - 'activity_view' => isset($legacy['NC_ACTIVITY_VIEW']) ? (string)$legacy['NC_ACTIVITY_VIEW'] : 'piwigo_showcase_activity', - 'gallery_root' => isset($legacy['GALLERY_ROOT']) ? (string)$legacy['GALLERY_ROOT'] : '', - 'state_dir' => isset($legacy['STATE_DIR']) ? (string)$legacy['STATE_DIR'] : '', - 'status_file' => isset($legacy['STATUS_FILE']) ? (string)$legacy['STATUS_FILE'] : '', - 'quiet_seconds' => isset($legacy['QUIET_SECONDS']) ? (int)$legacy['QUIET_SECONDS'] : 120, - 'max_wait_seconds' => isset($legacy['MAX_WAIT_SECONDS']) ? (int)$legacy['MAX_WAIT_SECONDS'] : 900, - 'full_sync_seconds' => isset($legacy['FULL_SYNC_SECONDS']) ? (int)$legacy['FULL_SYNC_SECONDS'] : 86400, - 'storages' => isset($bundle['storages']) && is_array($bundle['storages']) ? $bundle['storages'] : array(), - 'imported_from' => '/etc/piwigo-sync/piwigo.conf', - 'legacy_sync_enabled' => isset($legacy['PIWIGO_SYNC_ENABLED']) && (string)$legacy['PIWIGO_SYNC_ENABLED'] === '1', - ); - - $key_material = $config['host'].'|'.$config['database'].'|'.$config['user'].'|'.$config['source_view']; - $connection_key = 'legacy-'.substr(hash('sha256', $key_material), 0, 24); - $table = bratonien_tools_nc_connector_table(); - bratonien_tools_nc_connector_ensure_table(); - $now = date('Y-m-d H:i:s'); - $config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $secret_blob = bratonien_tools_nc_connector_encrypt_secret($password); - - $escaped_key = pwg_db_real_escape_string($connection_key); - $existing = pwg_query("SELECT id FROM `$table` WHERE connection_key = '$escaped_key' LIMIT 1"); - if (pwg_db_num_rows($existing)) - { - $row = pwg_db_fetch_assoc($existing); - $id = (int)$row['id']; - pwg_query("UPDATE `$table` SET - name = 'Bestehende Nextcloud', - adapter = 'local', - enabled = 0, - takeover_state = 'imported', - config_json = '".pwg_db_real_escape_string($config_json)."', - secret_blob = '".pwg_db_real_escape_string($secret_blob)."', - updated = '".pwg_db_real_escape_string($now)."' - WHERE id = $id"); - } - else - { - pwg_query("INSERT INTO `$table` - (connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated) - VALUES ( - '$escaped_key', - 'Bestehende Nextcloud', - 'local', - 0, - 'imported', - '".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)."' - )"); - } - - @unlink($path); - - return array( - 'message' => 'Bestehende Nextcloud-Verbindung wurde in den NC Connector importiert. Der produktive Legacy-Sync bleibt unveraendert aktiv; die importierte Verbindung ist noch nicht aktiviert.', - ); -} - -function bratonien_tools_nc_connector_view_name($name) -{ - $name = trim((string)$name); - if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/', $name)) - { - throw new RuntimeException('Ungueltiger PostgreSQL-View-Name in der Connector-Konfiguration.'); - } - - $parts = explode('.', $name); - $quoted = array(); - foreach ($parts as $part) - { - $quoted[] = '"'.str_replace('"', '""', $part).'"'; - } - return implode('.', $quoted); -} - -function bratonien_tools_nc_connector_psql($config, $password, $query) -{ - $psql = '/usr/bin/psql'; - if (!is_executable($psql)) - { - throw new RuntimeException('PostgreSQL-Client /usr/bin/psql ist nicht installiert oder nicht ausfuehrbar.'); - } - if (!function_exists('proc_open')) - { - throw new RuntimeException('PHP-Funktion proc_open ist deaktiviert; Connector-Verifikation kann nicht ausgefuehrt werden.'); - } - - $command = array( - $psql, - '-X', '-A', '-t', - '-v', 'ON_ERROR_STOP=1', - '-h', (string)$config['host'], - '-p', (string)$config['port'], - '-U', (string)$config['user'], - '-d', (string)$config['database'], - '-c', (string)$query, - ); - $descriptors = array( - 0 => array('pipe', 'r'), - 1 => array('pipe', 'w'), - 2 => array('pipe', 'w'), - ); - $env = array( - 'PGPASSWORD' => (string)$password, - 'PGCONNECT_TIMEOUT' => '5', - 'LC_ALL' => 'C', - ); - - $process = @proc_open($command, $descriptors, $pipes, null, $env); - if (!is_resource($process)) - { - throw new RuntimeException('PostgreSQL-Pruefprozess konnte nicht gestartet werden.'); - } - - fclose($pipes[0]); - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[1]); - fclose($pipes[2]); - $exit = proc_close($process); - - $stdout = trim((string)$stdout); - $stderr = trim((string)$stderr); - if ($exit !== 0) - { - $detail = $stderr !== '' ? $stderr : 'psql Exit-Code '.$exit; - throw new RuntimeException('PostgreSQL-Abfrage fehlgeschlagen: '.$detail); - } - - return $stdout; -} - -function bratonien_tools_nc_connector_mount_points() -{ - $mounts = array(); - $content = @file_get_contents('/proc/self/mountinfo'); - if (!is_string($content)) - { - return $mounts; - } - foreach (preg_split('/\r\n|\r|\n/', $content) as $line) - { - if ($line === '') continue; - $parts = explode(' ', $line); - 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_status() { $connections = bratonien_tools_nc_connector_connections(); - $legacy_config = '/etc/piwigo-sync/piwigo.conf'; - $legacy_present = is_readable($legacy_config); $active_count = 0; foreach ($connections as $connection) { @@ -474,11 +166,9 @@ function bratonien_tools_nc_connector_status() } return array( - 'phase' => 'native-runtime', - 'legacy_config_path' => $legacy_config, - 'legacy_present' => $legacy_present, - 'connections' => $connections, - 'connection_count' => count($connections), - 'active_count' => $active_count, + 'phase'=>'webdav', + 'connections'=>$connections, + 'connection_count'=>count($connections), + 'active_count'=>$active_count, ); } diff --git a/include/nc_connector_auth.inc.php b/include/nc_connector_auth.inc.php deleted file mode 100644 index 6f925f4..0000000 --- a/include/nc_connector_auth.inc.php +++ /dev/null @@ -1,246 +0,0 @@ -'', '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.'); -} diff --git a/include/nc_connector_connection_scope.inc.php b/include/nc_connector_connection_scope.inc.php index 8661fb8..839628d 100644 --- a/include/nc_connector_connection_scope.inc.php +++ b/include/nc_connector_connection_scope.inc.php @@ -4,196 +4,52 @@ if (!defined('PHPWG_ROOT_PATH')) die('Hacking attempt!'); } -function bratonien_tools_nc_wizard_finish_connection_scoped() -{ - $state = bratonien_tools_nc_wizard_state(); - if ((int)$state['step'] !== 4 || empty($state['technical_complete']) || trim((string)$state['showcase_user']) === '') throw new RuntimeException('Der Assistent ist noch nicht vollständig.'); - - if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']); - if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password']; - bratonien_tools_nc_wizard_store($state); - - $fallback_user = trim((string)$state['_fallback_user']); - $fallback_password = (string)$state['_fallback_password']; - if (($fallback_user === '') !== ($fallback_password === '')) throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.'); - if ($state['api_status'] !== 'ok' && $fallback_user === '') throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist für diese Verbindung ein Fallback-Zugang erforderlich.'); - - $mapping_lines = array(); - foreach ((array)$state['storages'] as $storage) - { - $key = (string)$storage['storage_id'].'|'.trim((string)$storage['source_prefix'], '/').'|'.(string)$storage['local_mount']; - $mapping_lines[$key] = (string)$storage['storage_id'].' | '.trim((string)$storage['source_prefix'], '/').' | '.(string)$storage['local_mount']; - } - - $_POST['nc_name']=(string)$state['connection_name']; - $_POST['nc_host']=(string)$state['db_host']; - $_POST['nc_port']=(string)$state['db_port']; - $_POST['nc_database']=(string)$state['db_database']; - $_POST['nc_user']=(string)$state['db_user']; - $_POST['nc_db_password']=(string)$state['_db_password']; - $_POST['nc_source_view']=(string)$state['source_view']; - $_POST['nc_activity_view']=(string)$state['activity_view']; - $_POST['nc_gallery_root']=(string)$state['gallery_root']; - $_POST['nc_storages']=implode("\n",array_values($mapping_lines)); - $_POST['nc_quiet_seconds']='120'; - $_POST['nc_max_wait_seconds']='900'; - $_POST['nc_full_sync_seconds']='86400'; - $_POST['nc_piwigo_user']=$fallback_user; - $_POST['nc_piwigo_password']=$fallback_password; - $_POST['nc_nextcloud_url']=(string)$state['base_url']; - $_POST['nc_showcase_user']=(string)$state['showcase_user']; - $_POST['nc_access_user']=(string)$state['username']; - $_POST['nc_nextcloud_user']=(string)$state['username']; - $_POST['nc_nextcloud_password']=(string)$state['_password']; - $_POST['nc_product']=(string)$state['product']; - $_POST['nc_version']=(string)$state['version']; - $_POST['nc_api_validated']=$state['api_status']==='ok'?'1':'0'; - $_POST['nc_connection_api_key_id']=$state['api_status']==='ok'?(string)$state['_api_key_id']:''; - $_POST['nc_connection_api_key_secret']=$state['api_status']==='ok'?(string)$state['_api_key_secret']:''; - - $result = bratonien_tools_nc_connector_create_local_api_first(); - $connection_id = (int)($result['connection_id'] ?? 0); - if ($connection_id < 1) throw new RuntimeException('Die neue Verbindung konnte nicht gespeichert werden.'); - - $connection = bratonien_tools_nc_connector_connection($connection_id, false); - if (!$connection) throw new RuntimeException('Die neue Verbindung konnte nach dem Anlegen nicht gelesen werden.'); - $config = $connection['config']; - $config['storages'] = array_values($state['storages']); - $config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($config_json)) throw new RuntimeException('Die Verzeichnisauswahl konnte nicht serialisiert werden.'); - $table = bratonien_tools_nc_connector_table(); - pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$connection_id." LIMIT 1"); - - unset($_SESSION['bratonien_nc_wizard']); - unset($result['connection_id']); - $result['message'] = 'Verbindung wurde vollständig mit eigener Authentifizierung angelegt. '.$result['message']; - return $result; -} - -function bratonien_tools_nc_connector_delete_any() -{ - $id = (int)($_POST['connection_id'] ?? 0); - $connection = bratonien_tools_nc_connector_connection($id, false); - if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.'); - - $status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status'; - if (!is_dir($status_dir) && !@mkdir($status_dir, 0755, true) && !is_dir($status_dir)) throw new RuntimeException('Der Connector-Statusordner konnte nicht angelegt werden.'); - - $tombstone = $status_dir.'/deleted-'.$id; - if (@file_put_contents($tombstone, date('c')."\n", LOCK_EX) === false) throw new RuntimeException('Die Verbindung konnte nicht sicher für die Laufzeit deaktiviert werden.'); - - $public_status = $status_dir.'/connection-'.$id.'.json'; - if (is_file($public_status)) @unlink($public_status); - - $config = is_array($connection['config'] ?? null) ? $connection['config'] : array(); - $state_dir = rtrim((string)($config['state_dir'] ?? ''), '/'); - if ($state_dir !== '' && strpos($state_dir, '/var/lib/bratonien-tools/nc-connector/connection-'.$id) === 0 && file_exists($state_dir)) - { - bratonien_tools_nc_connector_remove_tree($state_dir); - } - - $table = bratonien_tools_nc_connector_table(); - pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1"); - return array('message'=>'Connector-Verbindung wurde gelöscht und für weitere Laufzeitabrufe gesperrt. Nextcloud- und Piwigo-Bilder blieben unverändert.'); -} - -function bratonien_tools_nc_connector_verify_connection_scoped() -{ - $result = bratonien_tools_nc_connector_verify_managed(); - $id = (int)($_POST['connection_id'] ?? 0); - $connection = bratonien_tools_nc_connector_connection($id, true); - if (!$connection) return $result; - - $config = is_array($connection['config'] ?? null) ? $connection['config'] : array(); - if ((string)($config['origin'] ?? '') !== 'native' || array_key_exists('api_enabled', $config)) return $result; - - $credentials = bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob'] ?? ''); - $db_password = (string)($credentials['db_password'] ?? ''); - $fallback_user = (string)($credentials['piwigo_user'] ?? ''); - $fallback_password = (string)($credentials['piwigo_password'] ?? ''); - if ($db_password === '') return $result; - - $api_key_id = ''; - $api_key_secret = ''; - if ($fallback_user === '') - { - $legacy_api = bratonien_tools_nc_api_credentials(); - $api_key_id = trim((string)($legacy_api['key_id'] ?? '')); - $api_key_secret = trim((string)($legacy_api['key_secret'] ?? '')); - } - - $payload = json_encode(array( - 'v'=>3, - 'db_password'=>$db_password, - 'piwigo_user'=>$fallback_user, - 'piwigo_password'=>$fallback_password, - 'api_key_id'=>$api_key_id, - 'api_key_secret'=>$api_key_secret, - 'nextcloud_user'=>'', - 'nextcloud_password'=>'', - ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($payload)) throw new RuntimeException('Verbindungsauthentifizierung konnte nicht migriert werden.'); - - $config['piwigo_auth'] = 'connection-scoped'; - $config['api_enabled'] = $api_key_id !== ''; - $config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht migriert werden.'); - - $blob = bratonien_tools_nc_connector_encrypt_secret($payload); - $table = bratonien_tools_nc_connector_table(); - pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($blob)."' WHERE id=".$id." LIMIT 1"); - return $result; -} - function bratonien_tools_nc_connector_scoped_secret(array $connection) { + $config = is_array($connection['config'] ?? null) ? $connection['config'] : array(); + if ((string)($connection['adapter'] ?? '') !== 'remote' || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') + { + throw new RuntimeException('Die Verbindung ist keine WebDAV-Verbindung.'); + } + $plain = bratonien_tools_nc_connector_decrypt_secret($connection['secret_blob'] ?? ''); $decoded = json_decode($plain, true); - if (is_array($decoded) && array_key_exists('db_password', $decoded)) + if (!is_array($decoded)) { - return array( - 'v'=>3, - 'db_password'=>(string)($decoded['db_password'] ?? ''), - 'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''), - 'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''), - 'api_key_id'=>(string)($decoded['api_key_id'] ?? ''), - 'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''), - 'nextcloud_user'=>(string)($decoded['nextcloud_user'] ?? ''), - 'nextcloud_password'=>(string)($decoded['nextcloud_password'] ?? ''), - ); + throw new RuntimeException('WebDAV-Zugangsdaten haben ein unbekanntes Format.'); } return array( 'v'=>3, - 'db_password'=>(string)$plain, - 'piwigo_user'=>'', - 'piwigo_password'=>'', - 'api_key_id'=>'', - 'api_key_secret'=>'', - 'nextcloud_user'=>'', - 'nextcloud_password'=>'', + 'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''), + 'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''), + 'api_key_id'=>(string)($decoded['api_key_id'] ?? ''), + 'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''), + 'nextcloud_user'=>(string)($decoded['nextcloud_user'] ?? ''), + 'nextcloud_password'=>(string)($decoded['nextcloud_password'] ?? ''), ); } function bratonien_tools_nc_connector_store_scoped_secret($id, array $connection, array $credentials) { $config = is_array($connection['config'] ?? null) ? $connection['config'] : array(); - $is_webdav = (string)($config['source_mode'] ?? '') === 'webdav-placeholder'; - if (!$is_webdav && trim((string)($credentials['db_password'] ?? '')) === '') throw new RuntimeException('Das Datenbankpasswort der Verbindung fehlt.'); - if ($is_webdav && (trim((string)($credentials['nextcloud_user'] ?? '')) === '' || (string)($credentials['nextcloud_password'] ?? '') === '')) + if ((string)($connection['adapter'] ?? '') !== 'remote' || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') + { + throw new RuntimeException('Die Verbindung ist keine WebDAV-Verbindung.'); + } + if (trim((string)($credentials['nextcloud_user'] ?? '')) === '' || (string)($credentials['nextcloud_password'] ?? '') === '') { throw new RuntimeException('Die Nextcloud-Zugangsdaten der WebDAV-Verbindung fehlen.'); } $credentials['v'] = 3; + unset($credentials['db_password']); $payload = json_encode($credentials, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (!is_string($payload)) throw new RuntimeException('Verbindungszugangsdaten konnten nicht serialisiert werden.'); $blob = bratonien_tools_nc_connector_encrypt_secret($payload); - if (array_key_exists('api_enabled', $config)) - { - $config['api_enabled'] = trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== ''; - } + $config['piwigo_auth'] = 'connection-scoped'; + $config['api_enabled'] = trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== ''; $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.'); @@ -211,11 +67,13 @@ function bratonien_tools_nc_connector_fallback_save_scoped() $connection = bratonien_tools_nc_connector_connection($id, true); if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.'); + bratonien_tools_nc_connector_validate_fallback_credentials($username, $password); + $credentials = bratonien_tools_nc_connector_scoped_secret($connection); $credentials['piwigo_user'] = $username; $credentials['piwigo_password'] = $password; bratonien_tools_nc_connector_store_scoped_secret($id, $connection, $credentials); - return array('message'=>'Piwigo-Benutzername und Passwort wurden als Fallback dieser Verbindung gespeichert.'); + return array('message'=>'Piwigo-Benutzername und Passwort wurden als Fallback dieser WebDAV-Verbindung gespeichert.'); } function bratonien_tools_nc_connector_fallback_delete_scoped() @@ -223,9 +81,10 @@ function bratonien_tools_nc_connector_fallback_delete_scoped() $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_scoped_secret($connection); $credentials['piwigo_user'] = ''; $credentials['piwigo_password'] = ''; bratonien_tools_nc_connector_store_scoped_secret($id, $connection, $credentials); - return array('message'=>'Fallback dieser Verbindung wurde gelöscht. Ein vorhandener API-Zugang blieb unverändert.'); + return array('message'=>'Fallback dieser WebDAV-Verbindung wurde gelöscht. Ein vorhandener API-Zugang blieb unverändert.'); } diff --git a/include/nc_connector_create_api.inc.php b/include/nc_connector_create_api.inc.php deleted file mode 100644 index e0ed67f..0000000 --- a/include/nc_connector_create_api.inc.php +++ /dev/null @@ -1,128 +0,0 @@ - '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.'); - } - - $nextcloud_user = trim((string)($_POST['nc_nextcloud_user'] ?? $_POST['nc_access_user'] ?? '')); - $nextcloud_password = (string)($_POST['nc_nextcloud_password'] ?? ''); - if (($nextcloud_user === '') !== ($nextcloud_password === '')) - { - throw new RuntimeException('Nextcloud-Benutzer und Nextcloud-Passwort muessen entweder beide vorhanden oder beide leer sein.'); - } - - $wizard_auth_present = array_key_exists('nc_api_validated', $_POST); - $validated_pending_api = (string)($_POST['nc_api_validated'] ?? '') === '1'; - $api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? '')); - $api_key_secret = trim((string)($_POST['nc_connection_api_key_secret'] ?? '')); - if (($api_key_id === '') !== ($api_key_secret === '')) - { - throw new RuntimeException('API-Schluessel-ID und API-Geheimnis muessen entweder beide vorhanden oder beide leer sein.'); - } - if (($validated_pending_api || (!$wizard_auth_present && $fallback_user === '')) && $api_key_id === '') - { - $legacy_api = bratonien_tools_nc_api_credentials(); - $api_key_id = trim((string)($legacy_api['key_id'] ?? '')); - $api_key_secret = trim((string)($legacy_api['key_secret'] ?? '')); - } - if ($api_key_id === '' && $fallback_user === '') - { - throw new RuntimeException('Fuer diese Verbindung ist weder ein eigener gepruefter 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 (strpos($gallery_root, './') === 0) - { - $piwigo_root = realpath(PHPWG_ROOT_PATH); - if ($piwigo_root === false) throw new RuntimeException('Piwigo-Stammverzeichnis konnte nicht aufgeloest werden.'); - $gallery_root = rtrim($piwigo_root, '/').'/'.ltrim(substr($gallery_root, 2), '/'); - } - 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'=>'connection-scoped', - 'api_enabled'=>$api_key_id !== '', - ); - foreach (array('nextcloud_url'=>'nc_nextcloud_url','showcase_user'=>'nc_showcase_user','nextcloud_access_user'=>'nc_access_user','nextcloud_product'=>'nc_product','nextcloud_version'=>'nc_version') as $config_key=>$post_key) - { - $value=trim((string)($_POST[$post_key] ?? '')); if($value!=='') $config[$config_key]=$value; - } - bratonien_tools_nc_connector_view_name($config['source_view']); - bratonien_tools_nc_connector_view_name($config['activity_view']); - - $secret_payload = json_encode(array( - 'v'=>3, - 'db_password'=>(string)$_POST['nc_db_password'], - 'piwigo_user'=>$fallback_user, - 'piwigo_password'=>$fallback_password, - 'api_key_id'=>$api_key_id, - 'api_key_secret'=>$api_key_secret, - 'nextcloud_user'=>$nextcloud_user, - 'nextcloud_password'=>$nextcloud_password, - ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($secret_payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.'); - $secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload); - - $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)); - $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(); - if($id<1) throw new RuntimeException('Die Connector-Verbindung konnte nicht eindeutig angelegt werden.'); - $config['state_dir']='/var/lib/bratonien-tools/nc-connector/connection-'.$id; - $config['status_file']=$config['state_dir'].'/connector-status.json'; - $config_json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - if(!is_string($config_json)) - { - pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1"); - throw new RuntimeException('Connector-Konfiguration konnte nach dem Anlegen nicht serialisiert werden.'); - } - pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id); - - return array( - 'connection_id'=>$id, - 'message'=>'Nextcloud-Verbindung wurde mit verbindungseigener Piwigo-Authentifizierung angelegt.' - ); -} diff --git a/include/nc_connector_generic_scope.inc.php b/include/nc_connector_generic_scope.inc.php deleted file mode 100644 index 71f1200..0000000 --- a/include/nc_connector_generic_scope.inc.php +++ /dev/null @@ -1,101 +0,0 @@ -(int)($root['fileid'] ?? 0), - 'display_name'=>(string)($root['display_name'] ?? ''), - 'webdav_path'=>(string)($root['webdav_path'] ?? ''), - ); - }, $roots)); - unset($config['showcase_user']); - - foreach ($config['roots'] as $root) - { - if ((int)$root['fileid'] < 1 || trim((string)$root['display_name']) === '') throw new RuntimeException('Eine ausgewählte Nextcloud-Quelle ist unvollständig.'); - } - - $config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($config_json)) throw new RuntimeException('Die Quellenkonfiguration konnte nicht serialisiert werden.'); - $table = bratonien_tools_nc_connector_table(); - pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$connection_id." LIMIT 1"); - - unset($_SESSION['bratonien_nc_wizard']); - unset($result['connection_id']); - $result['message'] = 'Verbindung wurde mit generischer Nextcloud-Quellenauswahl angelegt. '.$result['message']; - return $result; -} diff --git a/include/nc_connector_manage.inc.php b/include/nc_connector_manage.inc.php deleted file mode 100644 index 0e398c0..0000000 --- a/include/nc_connector_manage.inc.php +++ /dev/null @@ -1,107 +0,0 @@ -'', 'piwigo_user'=>'', 'piwigo_password'=>''); - $decoded = json_decode($plain, true); - if (is_array($decoded) && isset($decoded['db_password'])) - { - return array( - 'db_password'=>(string)($decoded['db_password'] ?? ''), - 'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''), - 'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''), - ); - } - return array('db_password'=>$plain, 'piwigo_user'=>'', 'piwigo_password'=>''); -} - -function bratonien_tools_nc_connector_encrypt_credentials($db_password, $piwigo_user, $piwigo_password) -{ - $payload=json_encode(array('v'=>1,'db_password'=>(string)$db_password,'piwigo_user'=>(string)$piwigo_user,'piwigo_password'=>(string)$piwigo_password),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - if(!is_string($payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.'); - return bratonien_tools_nc_connector_encrypt_secret($payload); -} - -function bratonien_tools_nc_connector_parse_storages($raw) -{ - $storages=array(); - $lines=preg_split('/\r\n|\r|\n/',trim((string)$raw)); - foreach($lines as $line) - { - if(trim($line)==='') continue; - $parts=array_map('trim',explode('|',$line,3)); - if(count($parts)!==3 || $parts[0]==='' || $parts[2]==='') - { - throw new RuntimeException('Eine Speicherzuordnung ist unvollständig. Benötigt werden Storage-ID und lokaler Speicherpfad.'); - } - if($parts[2][0]!=='/') throw new RuntimeException('Der lokale Speicherpfad muss ein absoluter Pfad sein.'); - $storages[]=array( - 'storage_id'=>$parts[0], - 'source_prefix'=>trim($parts[1],'/'), - 'local_mount'=>rtrim($parts[2],'/'), - ); - } - if(!$storages) throw new RuntimeException('Mindestens ein Speicherort muss zugeordnet werden.'); - return $storages; -} - -function bratonien_tools_nc_connector_create_local() -{ - $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','nc_piwigo_user'=>'Piwigo-Benutzer','nc_piwigo_password'=>'Piwigo-Passwort'); - foreach($required as $field=>$label)if(trim((string)($_POST[$field]??''))==='')throw new RuntimeException($label.' fehlt.'); - $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'); - 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'],trim((string)$_POST['nc_piwigo_user']),(string)$_POST['nc_piwigo_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'=>'Nextcloud-Verbindung wurde angelegt. Bitte zuerst technisch pruefen und danach im LXC aktivieren.'); -} - -function bratonien_tools_nc_connector_verify_managed() -{ - $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((string)$connection['adapter']!=='local')throw new RuntimeException('Diese Verifikation ist derzeit nur fuer lokale Connector-Verbindungen verfuegbar.'); - $config=$connection['config'];foreach(array('host','port','database','user','source_view','activity_view') as $key)if(trim((string)($config[$key]??''))==='')throw new RuntimeException('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.'); - $credentials=bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob']??'');if($credentials['db_password']==='')throw new RuntimeException('Fuer diese Verbindung ist kein Datenbankpasswort gespeichert.'); - $checks=array();$ok=true;$source_count=null; - try{bratonien_tools_nc_connector_psql($config,$credentials['db_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{$source_view=bratonien_tools_nc_connector_view_name($config['source_view']);$value=bratonien_tools_nc_connector_psql($config,$credentials['db_password'],'SELECT COUNT(*) FROM '.$source_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_view=bratonien_tools_nc_connector_view_name($config['activity_view']);bratonien_tools_nc_connector_psql($config,$credentials['db_password'],'SELECT 1 FROM '.$activity_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(!$storages){$checks[]=array('name'=>'Storage-Mounts','ok'=>false,'detail'=>'Keine Storage-Zuordnung gespeichert');$ok=false;} - else foreach($storages as $index=>$storage){$path=rtrim((string)($storage['local_mount']??''),'/');$exists=$path!==''&&is_dir($path);$readable=$exists&&is_readable($path);$mounted=$path!==''&&isset($mount_points[$path]);$storage_ok=$exists&&$readable&&$mounted;$storage_id=(string)($storage['storage_id']??('#'.($index+1)));$detail=$storage_id.' -> '.($path!==''?$path:'(kein Pfad)');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':((string)$connection['takeover_state']==='disabled'?'disabled':'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=".$id); - if(!$ok)throw new RuntimeException('Connector-Verifikation ist fehlgeschlagen. Die Verbindung bleibt deaktiviert; Details stehen im NC Connector.');return array('message'=>'Connector-Verbindung wurde erfolgreich verifiziert. PostgreSQL, Views und Storage-Mounts sind erreichbar.'); -} - -function bratonien_tools_nc_connector_remove_tree($path) -{ - $path=rtrim((string)$path,'/');if($path===''||$path==='/'||!file_exists($path))return true; - if(is_link($path)||is_file($path))return @unlink($path); - $items=@scandir($path);if(!is_array($items))return false; - foreach($items as $item){if($item==='.'||$item==='..')continue;if(!bratonien_tools_nc_connector_remove_tree($path.'/'.$item))return false;} - return @rmdir($path); -} - -function bratonien_tools_nc_connector_delete() -{ - $id=(int)($_POST['connection_id']??0);$connection=bratonien_tools_nc_connector_connection($id,false);if(!$connection)throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');if(!empty($connection['enabled'])||(string)$connection['takeover_state']==='active')throw new RuntimeException('Eine aktive Verbindung kann nicht geloescht werden. Sie muss zuerst deaktiviert werden.'); - $config=is_array($connection['config'])?$connection['config']:array(); - $public_status=rtrim(PHPWG_ROOT_PATH,'/').'/_data/bratonien-tools/nc-connector-status/connection-'.$id.'.json';if(is_file($public_status))@unlink($public_status); - $state_dir=rtrim((string)($config['state_dir']??''),'/'); - if($state_dir!==''&&strpos($state_dir,'/var/lib/bratonien-tools/nc-connector/connection-'.$id)===0&&file_exists($state_dir)) - { - bratonien_tools_nc_connector_remove_tree($state_dir); - } - $table=bratonien_tools_nc_connector_table();pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1"); - return array('message'=>'Connector-Verbindung und erreichbare Connector-eigene Statusdaten wurden geloescht. Nextcloud- und Piwigo-Bilder blieben unverändert.'); -} diff --git a/include/nc_connector_piwigo_api.inc.php b/include/nc_connector_piwigo_api.inc.php index 26f411e..87fdb55 100644 --- a/include/nc_connector_piwigo_api.inc.php +++ b/include/nc_connector_piwigo_api.inc.php @@ -7,10 +7,7 @@ if (!defined('PHPWG_ROOT_PATH')) function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node) { $children = $node->children(); - if (count($children) === 0) - { - return (string)$node; - } + if (count($children) === 0) return (string)$node; $result = array(); foreach ($children as $name => $child) @@ -18,16 +15,10 @@ function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node) $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]); - } + if (!is_array($result[$name]) || !array_is_list($result[$name])) $result[$name] = array($result[$name]); $result[$name][] = $value; } - else - { - $result[$name] = $value; - } + else $result[$name] = $value; } return $result; } @@ -35,15 +26,9 @@ function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node) function bratonien_tools_nc_connector_decode_api_response($body, $content_type = '') { $decoded = json_decode((string)$body, true); - if (is_array($decoded)) - { - return $decoded; - } + 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.'); - } + if (!function_exists('simplexml_load_string')) throw new RuntimeException('Piwigo-API lieferte XML, aber SimpleXML ist in PHP nicht verfügbar.'); $previous = libxml_use_internal_errors(true); $xml = simplexml_load_string((string)$body); @@ -52,7 +37,7 @@ function bratonien_tools_nc_connector_decode_api_response($body, $content_type = 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); + throw new RuntimeException('Piwigo-API lieferte weder gültiges JSON noch eine gültige XML-Webservice-Antwort.'.$detail); } $response = array('stat'=>(string)$xml['stat']); @@ -61,27 +46,17 @@ function bratonien_tools_nc_connector_decode_api_response($body, $content_type = $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]); - } + if (!is_array($response[$name]) || !array_is_list($response[$name])) $response[$name] = array($response[$name]); $response[$name][] = $value; } - else - { - $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']; - } - + if (array_key_exists('result', $decoded)) return $decoded['result']; unset($decoded['stat']); return $decoded; } @@ -91,66 +66,42 @@ function bratonien_tools_nc_connector_collect_method_names($value, array &$metho if (is_string($value)) { $value = trim($value); - if ($value !== '' && preg_match('/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/', $value)) - { - $methods[$value] = true; - } - return; - } - - if (!is_array($value)) - { + if ($value !== '' && preg_match('/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/', $value)) $methods[$value] = true; return; } + if (!is_array($value)) return; if (isset($value['name']) && is_string($value['name'])) { $name = trim($value['name']); - if ($name !== '') - { - $methods[$name] = true; - } - } - - foreach ($value as $entry) - { - bratonien_tools_nc_connector_collect_method_names($entry, $methods); + if ($name !== '') $methods[$name] = true; } + foreach ($value as $entry) bratonien_tools_nc_connector_collect_method_names($entry, $methods); } function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, $method) { - if (!function_exists('curl_init')) - { - throw new RuntimeException('cURL ist in PHP nicht verfuegbar. Der API-Key-Test kann nicht ausgefuehrt werden.'); - } + if (!function_exists('curl_init')) throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der API-Key-Test kann nicht ausgeführt werden.'); $api_key_id = trim((string)$api_key_id); $api_key_secret = trim((string)$api_key_secret); - if ($api_key_id === '' || $api_key_secret === '') - { - throw new RuntimeException('API-Schluessel-ID und Geheimnis muessen angegeben werden.'); - } + if ($api_key_id === '' || $api_key_secret === '') throw new RuntimeException('API-Schlüssel-ID und Geheimnis müssen angegeben werden.'); - $api_key = $api_key_id.':'.$api_key_secret; $url = rtrim(get_absolute_root_url(true), '/').'/ws.php'; $ch = curl_init($url); curl_setopt_array($ch, array( - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => http_build_query(array( - 'method' => (string)$method, - 'format' => 'json', - )), - CURLOPT_HTTPHEADER => array( - 'X-PIWIGO-API: '.$api_key, + CURLOPT_RETURNTRANSFER=>true, + CURLOPT_POST=>true, + CURLOPT_POSTFIELDS=>http_build_query(array('method'=>(string)$method,'format'=>'json')), + CURLOPT_HTTPHEADER=>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', ), - CURLOPT_CONNECTTIMEOUT => 10, - CURLOPT_TIMEOUT => 20, - CURLOPT_FOLLOWLOCATION => false, - CURLOPT_USERAGENT => 'Bratonien-Tools-NC-Connector/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'), + CURLOPT_CONNECTTIMEOUT=>10, + CURLOPT_TIMEOUT=>20, + CURLOPT_FOLLOWLOCATION=>false, + CURLOPT_USERAGENT=>'Bratonien-Tools-NC-WebDAV/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'), )); $body = curl_exec($ch); @@ -160,14 +111,8 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s $content_type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); - if ($body === false || $errno !== 0) - { - throw new RuntimeException('Piwigo-API konnte nicht erreicht werden: '.$error); - } - if ($http_code < 200 || $http_code >= 300) - { - throw new RuntimeException('Piwigo-API antwortete mit HTTP '.$http_code.'.'); - } + if ($body === false || $errno !== 0) throw new RuntimeException('Piwigo-API konnte nicht erreicht werden: '.$error); + if ($http_code < 200 || $http_code >= 300) throw new RuntimeException('Piwigo-API antwortete mit HTTP '.$http_code.'.'); $decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $content_type); if (($decoded['stat'] ?? '') !== 'ok') @@ -175,29 +120,19 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s $message = (string)($decoded['message'] ?? $decoded['err'] ?? 'API-Aufruf wurde abgelehnt.'); throw new RuntimeException($message); } - return bratonien_tools_nc_connector_api_payload($decoded); } function bratonien_tools_nc_connector_validate_fallback_credentials($username, $password) { - if (!function_exists('curl_init')) - { - throw new RuntimeException('cURL ist in PHP nicht verfuegbar. Der Piwigo-Fallback kann nicht geprueft werden.'); - } + if (!function_exists('curl_init')) throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der Piwigo-Fallback kann nicht geprüft werden.'); $username = trim((string)$username); $password = (string)$password; - if ($username === '' || $password === '') - { - throw new RuntimeException('Piwigo-Benutzername und Passwort muessen angegeben werden.'); - } + if ($username === '' || $password === '') throw new RuntimeException('Piwigo-Benutzername und Passwort müssen angegeben werden.'); $cookie_file = tempnam(sys_get_temp_dir(), 'br-pwg-auth-'); - if ($cookie_file === false) - { - throw new RuntimeException('Temporare Piwigo-Sitzung konnte nicht angelegt werden.'); - } + if ($cookie_file === false) throw new RuntimeException('Temporäre Piwigo-Sitzung konnte nicht angelegt werden.'); try { @@ -214,7 +149,7 @@ function bratonien_tools_nc_connector_validate_fallback_credentials($username, $ CURLOPT_CONNECTTIMEOUT=>10, CURLOPT_TIMEOUT=>20, CURLOPT_FOLLOWLOCATION=>false, - CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Connector/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'), + CURLOPT_USERAGENT=>'Bratonien-Tools-NC-WebDAV/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'), )); $body = curl_exec($ch); $errno = curl_errno($ch); @@ -222,8 +157,8 @@ function bratonien_tools_nc_connector_validate_fallback_credentials($username, $ $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); - if ($body === false || $errno !== 0) throw new RuntimeException('Piwigo-Fallback konnte nicht geprueft werden: '.$error); - if ($http < 200 || $http >= 300) throw new RuntimeException('Piwigo-Fallback-Pruefung antwortete mit HTTP '.$http.'.'); + if ($body === false || $errno !== 0) throw new RuntimeException('Piwigo-Fallback konnte nicht geprüft werden: '.$error); + if ($http < 200 || $http >= 300) throw new RuntimeException('Piwigo-Fallback-Prüfung antwortete mit HTTP '.$http.'.'); $decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $type); if (($decoded['stat'] ?? '') !== 'ok') { @@ -233,83 +168,15 @@ function bratonien_tools_nc_connector_validate_fallback_credentials($username, $ return bratonien_tools_nc_connector_api_payload($decoded); }; - $request(array('method'=>'pwg.session.login', 'username'=>$username, 'password'=>$password)); + $request(array('method'=>'pwg.session.login','username'=>$username,'password'=>$password)); $status = $request(array('method'=>'pwg.session.getStatus')); if (!is_array($status)) throw new RuntimeException('Piwigo hat keinen auswertbaren Benutzerstatus geliefert.'); $role = strtolower(trim((string)($status['status'] ?? ''))); - if (!in_array($role, array('admin','webmaster'), true)) - { - throw new RuntimeException('Der Piwigo-Fallback funktioniert, gehoert aber keinem Administrator/Webmaster.'); - } - return array('username'=>(string)($status['username'] ?? $username), 'status'=>$role); + if (!in_array($role, array('admin','webmaster'), true)) throw new RuntimeException('Der Piwigo-Fallback funktioniert, gehört aber keinem Administrator/Webmaster.'); + return array('username'=>(string)($status['username'] ?? $username),'status'=>$role); } finally { @unlink($cookie_file); } } - -function bratonien_tools_nc_connector_piwigo_api_test() -{ - $api_key_id = trim((string)($_POST['nc_piwigo_api_key_id'] ?? '')); - $api_key_secret = trim((string)($_POST['nc_piwigo_api_key_secret'] ?? '')); - if ($api_key_id === '') - { - throw new RuntimeException('Piwigo-API-Schluessel-ID fehlt.'); - } - if ($api_key_secret === '') - { - throw new RuntimeException('Piwigo-API-Geheimnis fehlt.'); - } - - $status = bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, 'pwg.session.getStatus'); - if (!is_array($status)) - { - throw new RuntimeException('Piwigo hat keinen auswertbaren Benutzerstatus geliefert.'); - } - - $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'); - $method_map = array(); - bratonien_tools_nc_connector_collect_method_names($method_result, $method_map); - $methods = array_keys($method_map); - sort($methods, SORT_STRING); - - $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; - })); - - $result = array( - 'ok' => true, - 'username' => $username !== '' ? $username : 'nicht gemeldet', - 'status' => $user_status !== '' ? $user_status : 'nicht gemeldet', - 'admin' => true, - 'method_count' => count($methods), - 'sync_candidates' => $sync_candidates, - '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.', - ); - - return array( - 'message' => 'Piwigo-API-Key wurde geprueft und verschluesselt als bevorzugter Connector-Zugang gespeichert.', - 'nc_piwigo_api_test' => $result, - ); -} diff --git a/include/nc_connector_system.inc.php b/include/nc_connector_system.inc.php index 18e2d33..26f3bce 100644 --- a/include/nc_connector_system.inc.php +++ b/include/nc_connector_system.inc.php @@ -6,23 +6,17 @@ if (!defined('PHPWG_ROOT_PATH')) function bratonien_tools_nc_connector_systemctl_value(array $args) { - if (!function_exists('proc_open')) - { - return ''; - } + if (!function_exists('proc_open')) return ''; $command = array_merge(array('/usr/bin/systemctl'), $args); $spec = array( - 0 => array('file', '/dev/null', 'r'), - 1 => array('pipe', 'w'), - 2 => array('pipe', 'w'), + 0=>array('file','/dev/null','r'), + 1=>array('pipe','w'), + 2=>array('pipe','w'), ); - $environment = array_merge($_ENV, array('LC_ALL'=>'C', 'LANG'=>'C')); + $environment = array_merge($_ENV, array('LC_ALL'=>'C','LANG'=>'C')); $process = @proc_open($command, $spec, $pipes, null, $environment); - if (!is_resource($process)) - { - return ''; - } + if (!is_resource($process)) return ''; $stdout = stream_get_contents($pipes[1]); stream_get_contents($pipes[2]); fclose($pipes[1]); @@ -34,10 +28,7 @@ function bratonien_tools_nc_connector_systemctl_value(array $args) function bratonien_tools_nc_connector_parse_systemd_time($value) { $value = trim((string)$value); - if ($value === '' || strtolower($value) === 'n/a') - { - return 0; - } + if ($value === '' || strtolower($value) === 'n/a') return 0; $parsed = strtotime($value); return $parsed === false ? 0 : (int)$parsed; } @@ -45,106 +36,52 @@ function bratonien_tools_nc_connector_parse_systemd_time($value) function bratonien_tools_nc_connector_monotonic_to_timestamp($value) { $value = trim((string)$value); - if ($value === '' || $value === '0' || strtolower($value) === 'n/a') - { - return 0; - } - - if (preg_match('/^([0-9]+)(?:us)?$/', $value, $matches)) - { - $next_boot_seconds = ((float)$matches[1]) / 1000000; - } - else - { - return 0; - } + if ($value === '' || $value === '0' || strtolower($value) === 'n/a') return 0; + if (!preg_match('/^([0-9]+)(?:us)?$/', $value, $matches)) return 0; + $next_boot_seconds = ((float)$matches[1]) / 1000000; $uptime_raw = @file_get_contents('/proc/uptime'); - if (!is_string($uptime_raw) || !preg_match('/^([0-9]+(?:\.[0-9]+)?)/', trim($uptime_raw), $uptime_match)) - { - return 0; - } + if (!is_string($uptime_raw) || !preg_match('/^([0-9]+(?:\.[0-9]+)?)/', trim($uptime_raw), $uptime_match)) return 0; $remaining = $next_boot_seconds - (float)$uptime_match[1]; - if ($remaining < -1) - { - return 0; - } - + if ($remaining < -1) return 0; return (int)round(time() + max(0, $remaining)); } function bratonien_tools_nc_connector_next_from_timer_list($timer) { - $line = bratonien_tools_nc_connector_systemctl_value(array( - 'list-timers', '--all', '--no-pager', '--no-legend', $timer, - )); - if ($line === '') - { - return 0; - } - + $line = bratonien_tools_nc_connector_systemctl_value(array('list-timers','--all','--no-pager','--no-legend',$timer)); + if ($line === '') return 0; $first_line = trim((string)strtok($line, "\n")); - if (preg_match('/^(\S+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+\S+)/', $first_line, $matches)) - { - $parsed = strtotime($matches[1]); - return $parsed === false ? 0 : (int)$parsed; - } - - return 0; + if (!preg_match('/^(\S+\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+\S+)/', $first_line, $matches)) return 0; + $parsed = strtotime($matches[1]); + return $parsed === false ? 0 : (int)$parsed; } function bratonien_tools_nc_connector_connection_last_status(array $connection) { $empty = array( - 'timestamp'=>0, - 'label'=>'Nicht verfügbar', - 'state'=>'', - 'message'=>'', - 'auth_mode'=>'', - 'api_state'=>'', - 'api_message'=>'', - 'fallback_state'=>'', - 'fallback_message'=>'', - 'error_detail'=>'', + 'timestamp'=>0,'label'=>'Nicht verfügbar','state'=>'','message'=>'','auth_mode'=>'', + 'api_state'=>'','api_message'=>'','fallback_state'=>'','fallback_message'=>'','error_detail'=>'', ); $connection_id = (int)($connection['id'] ?? 0); $candidates = array(); - if ($connection_id > 0) - { - $candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$connection_id.'.json'; - } + if ($connection_id > 0) $candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$connection_id.'.json'; $config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array(); $state_dir = rtrim((string)($config['state_dir'] ?? ''), '/'); - if ($state_dir === '' && $connection_id > 0) - { - $state_dir = '/var/lib/bratonien-tools/nc-connector/connection-'.$connection_id; - } - if ($state_dir !== '') - { - $candidates[] = $state_dir.'/connector-status.json'; - } + if ($state_dir === '' && $connection_id > 0) $state_dir = '/var/lib/bratonien-tools/nc-connector/connection-'.$connection_id; + if ($state_dir !== '') $candidates[] = $state_dir.'/connector-status.json'; $decoded = null; foreach ($candidates as $candidate) { - if (!is_readable($candidate)) - { - continue; - } + if (!is_readable($candidate)) continue; $value = json_decode((string)@file_get_contents($candidate), true); - if (is_array($value)) - { - $decoded = $value; - break; - } - } - if (!is_array($decoded)) - { - return $empty; + if (is_array($value)) { $decoded = $value; break; } } + if (!is_array($decoded)) return $empty; $timestamp = (int)($decoded['timestamp'] ?? 0); $api = isset($decoded['api']) && is_array($decoded['api']) ? $decoded['api'] : array(); @@ -166,22 +103,12 @@ function bratonien_tools_nc_connector_connection_last_status(array $connection) function bratonien_tools_nc_connector_last_status(array $connections) { - $latest = array('timestamp'=>0, 'state'=>'', 'message'=>'', 'auth_mode'=>'', 'api_state'=>'', 'api_message'=>'', 'fallback_state'=>'', 'fallback_message'=>'', 'error_detail'=>''); - + $latest = array('timestamp'=>0,'state'=>'','message'=>'','auth_mode'=>'','api_state'=>'','api_message'=>'','fallback_state'=>'','fallback_message'=>'','error_detail'=>''); foreach ($connections as $connection) { - if (empty($connection['enabled']) || (string)($connection['takeover_state'] ?? '') !== 'active') - { - continue; - } - $status = bratonien_tools_nc_connector_connection_last_status($connection); - if ((int)$status['timestamp'] >= (int)$latest['timestamp']) - { - $latest = $status; - } + if ((int)$status['timestamp'] >= (int)$latest['timestamp']) $latest = $status; } - return $latest; } @@ -190,49 +117,40 @@ function bratonien_tools_nc_connector_system_status(array $connections = array() $timer = 'bratonien-nc-connector.timer'; $service = 'bratonien-nc-connector.service'; - $next_realtime_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $timer, '--property=NextElapseUSecRealtime', '--value')); + $next_realtime_raw = bratonien_tools_nc_connector_systemctl_value(array('show',$timer,'--property=NextElapseUSecRealtime','--value')); $next_timestamp = bratonien_tools_nc_connector_parse_systemd_time($next_realtime_raw); - if ($next_timestamp <= 0) { - $next_monotonic_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $timer, '--property=NextElapseUSecMonotonic', '--value')); + $next_monotonic_raw = bratonien_tools_nc_connector_systemctl_value(array('show',$timer,'--property=NextElapseUSecMonotonic','--value')); $next_timestamp = bratonien_tools_nc_connector_monotonic_to_timestamp($next_monotonic_raw); } + if ($next_timestamp <= 0) $next_timestamp = bratonien_tools_nc_connector_next_from_timer_list($timer); - if ($next_timestamp <= 0) - { - $next_timestamp = bratonien_tools_nc_connector_next_from_timer_list($timer); - } - - $active = bratonien_tools_nc_connector_systemctl_value(array('is-active', $timer)); - $enabled = bratonien_tools_nc_connector_systemctl_value(array('is-enabled', $timer)); + $active = bratonien_tools_nc_connector_systemctl_value(array('is-active',$timer)); + $enabled = bratonien_tools_nc_connector_systemctl_value(array('is-enabled',$timer)); $last = bratonien_tools_nc_connector_last_status($connections); if ($last['timestamp'] <= 0) { - $last_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $service, '--property=ExecMainExitTimestamp', '--value')); + $last_raw = bratonien_tools_nc_connector_systemctl_value(array('show',$service,'--property=ExecMainExitTimestamp','--value')); $last['timestamp'] = bratonien_tools_nc_connector_parse_systemd_time($last_raw); } return array( - 'timer_name' => $timer, - 'timer_active' => $active === 'active', - 'timer_enabled' => $enabled === 'enabled', - 'last_run_timestamp' => (int)$last['timestamp'], - 'last_run_label' => $last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar', - 'last_run_state' => (string)$last['state'], - 'last_run_message' => (string)$last['message'], - 'last_run_auth_mode' => (string)$last['auth_mode'], - 'last_run_api_state' => (string)$last['api_state'], - 'last_run_api_message' => (string)$last['api_message'], - 'last_run_fallback_state' => (string)$last['fallback_state'], - 'last_run_fallback_message' => (string)$last['fallback_message'], - 'last_run_error_detail' => (string)$last['error_detail'], - 'next_run_timestamp' => $next_timestamp, - 'next_run_label' => $next_timestamp > 0 ? date('d.m.Y H:i:s', $next_timestamp) : 'Nicht verfügbar', - 'legacy_runtime_exists' => is_dir('/opt/piwigo-sync'), - 'legacy_config_exists' => is_dir('/etc/piwigo-sync'), - 'legacy_service_exists' => is_file('/etc/systemd/system/piwigo-sync.service'), - 'legacy_timer_exists' => is_file('/etc/systemd/system/piwigo-sync.timer'), + 'timer_name'=>$timer, + 'timer_active'=>$active === 'active', + 'timer_enabled'=>$enabled === 'enabled', + 'last_run_timestamp'=>(int)$last['timestamp'], + 'last_run_label'=>$last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar', + 'last_run_state'=>(string)$last['state'], + 'last_run_message'=>(string)$last['message'], + 'last_run_auth_mode'=>(string)$last['auth_mode'], + 'last_run_api_state'=>(string)$last['api_state'], + 'last_run_api_message'=>(string)$last['api_message'], + 'last_run_fallback_state'=>(string)$last['fallback_state'], + 'last_run_fallback_message'=>(string)$last['fallback_message'], + 'last_run_error_detail'=>(string)$last['error_detail'], + 'next_run_timestamp'=>$next_timestamp, + 'next_run_label'=>$next_timestamp > 0 ? date('d.m.Y H:i:s', $next_timestamp) : 'Nicht verfügbar', ); } diff --git a/include/nc_connector_takeover.inc.php b/include/nc_connector_takeover.inc.php deleted file mode 100644 index 1bedf01..0000000 --- a/include/nc_connector_takeover.inc.php +++ /dev/null @@ -1,135 +0,0 @@ - date('Y-m-d H:i:s'), - 'legacy_sync_untouched' => true, - 'connector_enabled' => false, - 'first_run' => array( - 'status' => 'pending', - 'finished_at' => null, - 'detail' => '', - ), - ); - $config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($config_json)) - { - throw new RuntimeException('Uebergabestatus konnte nicht gespeichert werden.'); - } - - bratonien_tools_nc_connector_ensure_takeover_state(); - $table = bratonien_tools_nc_connector_table(); - $now = date('Y-m-d H:i:s'); - pwg_query("UPDATE `$table` SET - takeover_state = 'ready', - enabled = 0, - config_json = '".pwg_db_real_escape_string($config_json)."', - updated = '".pwg_db_real_escape_string($now)."' - WHERE id = ".(int)$connection['id']); - - return array( - 'message' => 'Die verifizierte Nextcloud-Verbindung ist jetzt fuer die kontrollierte Uebergabe vorbereitet. Der Connector wurde noch nicht aktiviert und der Legacy-Sync bleibt unveraendert Produktionsverbindung.', - ); -} - -function bratonien_tools_nc_connector_cancel_takeover() -{ - $id = isset($_POST['connection_id']) ? (int)$_POST['connection_id'] : 0; - $connection = bratonien_tools_nc_connector_connection($id, false); - if (!$connection) - { - throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.'); - } - if ($connection['takeover_state'] !== 'ready') - { - throw new RuntimeException('Diese Verbindung befindet sich nicht in der Uebergabevorbereitung.'); - } - - $config = $connection['config']; - if (isset($config['takeover'])) - { - unset($config['takeover']); - } - $config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($config_json)) - { - throw new RuntimeException('Uebergabestatus konnte nicht zurueckgesetzt werden.'); - } - - bratonien_tools_nc_connector_ensure_takeover_state(); - $table = bratonien_tools_nc_connector_table(); - $now = date('Y-m-d H:i:s'); - pwg_query("UPDATE `$table` SET - takeover_state = 'verified', - enabled = 0, - config_json = '".pwg_db_real_escape_string($config_json)."', - updated = '".pwg_db_real_escape_string($now)."' - WHERE id = ".(int)$connection['id']); - - return array( - 'message' => 'Die Uebergabevorbereitung wurde zurueckgenommen. Die Verbindung bleibt verifiziert und deaktiviert; der Legacy-Sync bleibt unveraendert aktiv.', - ); -} - -/** - * Normalize the outcome of a Connector sync run during takeover. - * - * Both "changed" and "no_changes" are successful results. Only "error" - * represents a failed run and may trigger rollback of a controlled handover. - */ -function bratonien_tools_nc_connector_takeover_result($status, $detail = '') -{ - $status = trim((string)$status); - if (!in_array($status, array('changed', 'no_changes', 'error'), true)) - { - throw new RuntimeException('Unbekannter Connector-Laufstatus: '.$status); - } - - return array( - 'status' => $status, - 'success' => $status !== 'error', - 'changed' => $status === 'changed', - 'finished_at' => date('Y-m-d H:i:s'), - 'detail' => (string)$detail, - ); -} diff --git a/include/nc_connector_webdav.inc.php b/include/nc_connector_webdav.inc.php index fb6908c..ddb3480 100644 --- a/include/nc_connector_webdav.inc.php +++ b/include/nc_connector_webdav.inc.php @@ -4,12 +4,6 @@ if (!defined('PHPWG_ROOT_PATH')) die('Hacking attempt!'); } -/** - * Create a disabled WebDAV-backed connector beside the existing local modes. - * - * This deliberately does not activate, migrate or modify any existing - * connection. It only persists the data required for the parallel WebDAV path. - */ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() { $state = bratonien_tools_nc_wizard_state(); @@ -24,7 +18,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() $password = (string)($state['_password'] ?? ''); if ($base_url === '' || $username === '' || $password === '') { - throw new RuntimeException('Für den WebDAV-Testpfad fehlen Nextcloud-Adresse oder Zugangsdaten.'); + throw new RuntimeException('Für die WebDAV-Verbindung fehlen Nextcloud-Adresse oder Zugangsdaten.'); } $selected = isset($state['directory_selected']) && is_array($state['directory_selected']) @@ -34,7 +28,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() ? $state['directory_selected_fileids'] : array(); $selected = array_values(array_filter($selected, function($path) { return $path !== ''; })); - if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis für den WebDAV-Testpfad auswählen.'); + if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.'); $roots = array(); foreach ($selected as $path) @@ -60,7 +54,6 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() $api_enabled = $api_key_id !== '' && $api_key_secret !== ''; $config = array( - 'origin'=>'native', 'source_mode'=>'webdav-placeholder', 'transport'=>'webdav', 'nextcloud_url'=>$base_url, @@ -70,10 +63,6 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() 'roots'=>$roots, 'state_dir'=>'', 'status_file'=>'', - 'quiet_seconds'=>120, - 'max_wait_seconds'=>900, - 'full_sync_seconds'=>86400, - 'parallel_test'=>true, 'piwigo_auth'=>'connection-scoped', 'api_enabled'=>$api_enabled, ); @@ -86,7 +75,6 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() $secret_payload = json_encode(array( 'v'=>3, - 'db_password'=>'', 'piwigo_user'=>$fallback_user, 'piwigo_password'=>$fallback_password, 'api_key_id'=>$api_key_id, @@ -105,16 +93,16 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() $connection_key = 'webdav-'.bin2hex(random_bytes(12)); $secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload); - pwg_query("INSERT INTO `$table` (connection_key,name,adapter,enabled,takeover_state,config_json,secret_blob,created,updated) VALUES ('" + pwg_query("INSERT INTO `$table` (connection_key,name,adapter,enabled,config_json,secret_blob,created,updated) VALUES ('" .pwg_db_real_escape_string($connection_key)."','" - .pwg_db_real_escape_string($name)."','remote',0,'disabled','" + .pwg_db_real_escape_string($name)."','remote',1,'" .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(); - if ($id < 1) throw new RuntimeException('Die WebDAV-Testverbindung konnte nicht eindeutig angelegt werden.'); + if ($id < 1) throw new RuntimeException('Die WebDAV-Verbindung konnte nicht eindeutig angelegt werden.'); $config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id; $config['status_file'] = $config['state_dir'].'/connector-status.json'; @@ -122,12 +110,12 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() if (!is_string($config_json)) { pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1"); - throw new RuntimeException('Die WebDAV-Testverbindung konnte nach dem Anlegen nicht serialisiert werden.'); + throw new RuntimeException('Die WebDAV-Verbindung konnte nach dem Anlegen nicht serialisiert werden.'); } pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id." LIMIT 1"); return array( 'connection_id'=>$id, - 'message'=>'Parallele WebDAV-Testverbindung wurde deaktiviert angelegt. Bestehende Verbindungen blieben unverändert.', + 'message'=>'WebDAV-Verbindung wurde angelegt.', ); } diff --git a/include/nc_connector_wizard.inc.php b/include/nc_connector_wizard.inc.php index 7ec2726..82733a2 100644 --- a/include/nc_connector_wizard.inc.php +++ b/include/nc_connector_wizard.inc.php @@ -17,30 +17,28 @@ function bratonien_tools_nc_wizard_state() 'display_name'=>'', 'version'=>'', 'product'=>'Nextcloud', - 'users'=>array(), - 'can_list_users'=>false, - 'showcase_user'=>'', 'connection_name'=>'', 'scan_message'=>'', - 'technical_stage'=>'auto_check', - 'technical_source'=>'Noch nicht geprüft', + 'source_mode'=>'webdav-placeholder', + 'transport'=>'webdav', + 'gallery_root'=>'', + 'roots'=>array(), + 'technical_stage'=>'mounts', + 'technical_source'=>'WebDAV', 'technical_error'=>'', 'technical_complete'=>false, - 'db_host'=>'', - 'db_port'=>'5432', - 'db_database'=>'nextcloud', - 'db_user'=>'', - 'db_password_set'=>false, - 'source_view'=>'piwigo_showcase_sources', - 'activity_view'=>'piwigo_showcase_activity', - 'gallery_root'=>'', - 'storages'=>array(), - 'storage_candidates'=>array(), + 'directory_selection_ready'=>false, + 'directory_path'=>'', + 'directory_parent'=>'', + 'directory_children'=>array(), + 'directory_current_fileid'=>0, + 'directory_fileids'=>array(), + 'directory_selected'=>array(), + 'directory_selected_fileids'=>array(), 'api_status'=>'pending', 'api_username'=>'', 'api_error'=>'', '_password'=>'', - '_db_password'=>'', '_api_key_id'=>'', '_api_key_secret'=>'', '_fallback_user'=>'', @@ -102,7 +100,7 @@ function bratonien_tools_nc_wizard_http($url, $username = '', $password = '', ar CURLOPT_CONNECTTIMEOUT=>8, CURLOPT_TIMEOUT=>15, CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'), $headers), - CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Wizard/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'), + CURLOPT_USERAGENT=>'Bratonien-Tools-NC-WebDAV/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'), ); if ($username !== '') @@ -141,352 +139,6 @@ function bratonien_tools_nc_wizard_ocs_data($body) return isset($decoded['ocs']['data']) && is_array($decoded['ocs']['data']) ? $decoded['ocs']['data'] : array(); } -function bratonien_tools_nc_wizard_fill_profile(array &$state) -{ - $url_host = strtolower((string)parse_url($state['base_url'], PHP_URL_HOST)); - - $state['db_host'] = $url_host; - $state['db_port'] = '5432'; - $state['db_database'] = 'nextcloud'; - $state['db_user'] = (string)$state['username']; - $state['_db_password'] = (string)$state['_password']; - $state['db_password_set'] = $state['_db_password'] !== ''; - $state['source_view'] = 'piwigo_showcase_sources'; - $state['activity_view'] = 'piwigo_showcase_activity'; - $state['gallery_root'] = rtrim(PHPWG_ROOT_PATH, '/').'/galleries/nextcloud'; - $state['storages'] = array(); - $state['storage_candidates'] = array(); - $state['technical_stage'] = 'auto_check'; - $state['technical_source'] = 'Automatische Prüfung'; - $state['technical_error'] = ''; - $state['technical_complete'] = false; - - foreach (bratonien_tools_nc_connector_connections() as $candidate) - { - $config = isset($candidate['config']) && is_array($candidate['config']) ? $candidate['config'] : array(); - $candidate_url = trim((string)($config['nextcloud_url'] ?? '')); - $match = false; - - if ($candidate_url !== '') - { - try - { - $match = bratonien_tools_nc_wizard_normalize_url($candidate_url) === $state['base_url']; - } - catch (Throwable $ignored) - { - $match = false; - } - } - - if (!$match) continue; - - $state['db_host'] = trim((string)($config['host'] ?? $state['db_host'])); - $state['db_port'] = trim((string)($config['port'] ?? $state['db_port'])); - $state['db_database'] = trim((string)($config['database'] ?? $state['db_database'])); - $state['source_view'] = trim((string)($config['source_view'] ?? $state['source_view'])); - $state['activity_view'] = trim((string)($config['activity_view'] ?? $state['activity_view'])); - $state['gallery_root'] = trim((string)($config['gallery_root'] ?? $state['gallery_root'])); - $state['storages'] = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array(); - $state['storage_candidates'] = $state['storages']; - $state['technical_source'] = 'Bekannte Verbindungswerte werden erneut geprüft'; - break; - } -} - -function bratonien_tools_nc_wizard_finish_data_access(array &$state) -{ - $config = array( - 'host'=>$state['db_host'], - 'port'=>$state['db_port'], - 'database'=>$state['db_database'], - 'user'=>$state['db_user'], - ); - $password = (string)$state['_db_password']; - - bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1'); - - $source_view = bratonien_tools_nc_connector_view_name($state['source_view']); - $activity_view = bratonien_tools_nc_connector_view_name($state['activity_view']); - bratonien_tools_nc_connector_psql($config, $password, 'SELECT COUNT(*) FROM '.$source_view); - bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1 FROM '.$activity_view.' LIMIT 1'); - - $rows = bratonien_tools_nc_connector_psql( - $config, - $password, - "SELECT DISTINCT storage_id::text || E'\\t' || COALESCE(source_path, '') FROM ".$source_view." ORDER BY 1" - ); - - $candidates = array(); - foreach (preg_split('/\r\n|\r|\n/', trim($rows)) as $line) - { - if ($line === '') continue; - - $parts = explode("\t", $line, 2); - if (count($parts) !== 2) continue; - - $storage_id = trim($parts[0]); - $source_path = trim($parts[1], '/'); - if ($storage_id === '') continue; - - $prefix = $source_path; - if (strpos($source_path, '/') !== false) $prefix = substr($source_path, 0, strpos($source_path, '/')); - - if (!isset($candidates[$storage_id])) - { - $candidates[$storage_id] = array( - 'storage_id'=>$storage_id, - 'source_prefix'=>$prefix, - 'local_mount'=>'', - ); - } - } - - if (!$candidates) throw new RuntimeException('Die Verbindung zur Datenquelle funktioniert, aber es wurden noch keine freigegebenen Speicherorte gefunden.'); - - $known = array(); - foreach (bratonien_tools_nc_connector_connections() as $connection) - { - foreach (($connection['config']['storages'] ?? array()) as $storage) - { - $key = (string)($storage['storage_id'] ?? '').'|'.trim((string)($storage['source_prefix'] ?? ''), '/'); - if ($key !== '|') $known[$key] = (string)($storage['local_mount'] ?? ''); - } - } - - foreach ($candidates as &$candidate) - { - $key = $candidate['storage_id'].'|'.trim($candidate['source_prefix'], '/'); - if (!empty($known[$key]) && is_dir($known[$key]) && is_readable($known[$key])) $candidate['local_mount'] = $known[$key]; - } - unset($candidate); - - $state['storage_candidates'] = array_values($candidates); - $state['storages'] = array_values(array_filter($state['storage_candidates'], function($storage) { - return trim((string)($storage['local_mount'] ?? '')) !== ''; - })); - $state['technical_error'] = ''; - $state['technical_source'] = 'Datenzugriff erfolgreich geprüft'; - $state['technical_complete'] = count($state['storages']) === count($state['storage_candidates']); - $state['technical_stage'] = $state['technical_complete'] ? 'ready' : 'mounts'; -} - -function bratonien_tools_nc_wizard_scan() -{ - $state = bratonien_tools_nc_wizard_state(); - $host_input = trim((string)($_POST['nc_wizard_host'] ?? $state['host_input'])); - $username = trim((string)($_POST['nc_wizard_user'] ?? $state['username'])); - $password = array_key_exists('nc_wizard_password', $_POST) ? (string)$_POST['nc_wizard_password'] : (string)$state['_password']; - - $state['step'] = 1; - $state['host_input'] = $host_input; - $state['username'] = $username; - $state['_password'] = $password; - bratonien_tools_nc_wizard_store($state); - - if ($username === '' || $password === '') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.'); - - $base_url = ''; - $status_data = null; - foreach (bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url) - { - try - { - $response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php'); - if ($response['status'] < 200 || $response['status'] >= 300) continue; - - $candidate_status = json_decode($response['body'], true); - if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue; - - $base_url = $candidate_url; - $status_data = $candidate_status; - break; - } - catch (Throwable $ignored) - { - } - } - - if ($base_url === '' || !is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.'); - - $user_response = bratonien_tools_nc_wizard_http( - $base_url.'/ocs/v2.php/cloud/user?format=json', - $username, - $password, - array('OCS-APIRequest: true') - ); - - if ($user_response['status'] === 401 || $user_response['status'] === 403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.'); - if ($user_response['status'] < 200 || $user_response['status'] >= 300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.'); - - $user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']); - - $users = array(); - $can_list_users = false; - try - { - $users_response = bratonien_tools_nc_wizard_http( - $base_url.'/ocs/v2.php/cloud/users?format=json', - $username, - $password, - array('OCS-APIRequest: true') - ); - - if ($users_response['status'] >= 200 && $users_response['status'] < 300) - { - $users_data = bratonien_tools_nc_wizard_ocs_data($users_response['body']); - if (isset($users_data['users']) && is_array($users_data['users'])) - { - $users = array_values(array_filter(array_map('strval', $users_data['users']))); - sort($users, SORT_NATURAL | SORT_FLAG_CASE); - $can_list_users = true; - } - } - } - catch (Throwable $ignored) - { - } - - $resolved_username = trim((string)($user_data['id'] ?? $username)); - if ($resolved_username === '') $resolved_username = $username; - $url_host = (string)parse_url($base_url, PHP_URL_HOST); - - $state = array_merge($state, array( - 'step'=>2, - 'scan_ok'=>true, - 'base_url'=>$base_url, - 'host_input'=>$host_input, - 'username'=>$resolved_username, - 'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''), - 'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''), - 'product'=>(string)($status_data['productname'] ?? 'Nextcloud'), - 'users'=>$users, - 'can_list_users'=>$can_list_users, - 'showcase_user'=>'', - 'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud', - 'scan_message'=>'Nextcloud wurde erkannt und der Benutzerzugriff wurde bestätigt.', - '_password'=>$password, - 'api_status'=>'pending', - 'api_username'=>'', - 'api_error'=>'', - )); - - bratonien_tools_nc_wizard_fill_profile($state); - - try - { - bratonien_tools_nc_wizard_finish_data_access($state); - } - catch (Throwable $e) - { - $state['technical_complete'] = false; - $state['technical_stage'] = 'database_details'; - $state['technical_error'] = $e->getMessage(); - } - - bratonien_tools_nc_wizard_store($state); - - if ($state['technical_stage'] === 'database_details') - { - return array('message'=>'Nextcloud wurde gefunden. Die Datenbank-Adresse muss noch bestätigt werden.'); - } - if ($state['technical_stage'] === 'mounts') - { - return array('message'=>'Nextcloud und Datenzugriff wurden bestätigt. Ein Speicherort muss noch zugeordnet werden.'); - } - - return array('message'=>'Nextcloud und Datenzugriff wurden erfolgreich bestätigt.'); -} - -function bratonien_tools_nc_wizard_save_technical() -{ - $state = bratonien_tools_nc_wizard_state(); - if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.'); - if ((string)$state['username'] === '' || (string)$state['_password'] === '') throw new RuntimeException('Die Zugangsdaten aus dem ersten Schritt sind nicht mehr verfügbar. Bitte den Assistenten neu starten.'); - - $state['db_user'] = (string)$state['username']; - $state['_db_password'] = (string)$state['_password']; - $state['db_password_set'] = true; - $state['db_host'] = trim((string)($_POST['nc_wizard_db_host'] ?? $state['db_host'])); - $state['db_port'] = (string)max(1, min(65535, (int)($_POST['nc_wizard_db_port'] ?? $state['db_port']))); - $state['db_database'] = trim((string)($_POST['nc_wizard_db_database'] ?? $state['db_database'])); - $state['technical_stage'] = 'database_details'; - bratonien_tools_nc_wizard_store($state); - - if ($state['db_host'] === '' || $state['db_database'] === '') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.'); - - try - { - bratonien_tools_nc_wizard_finish_data_access($state); - bratonien_tools_nc_wizard_store($state); - - return array( - 'message'=>$state['technical_complete'] - ? 'Datenzugriff wurde erfolgreich geprüft.' - : 'Datenzugriff funktioniert. Ein Speicherort muss noch bestätigt werden.' - ); - } - catch (Throwable $e) - { - $state['technical_error'] = $e->getMessage(); - $state['technical_stage'] = 'database_details'; - bratonien_tools_nc_wizard_store($state); - throw $e; - } -} - -function bratonien_tools_nc_wizard_save_mounts() -{ - $state = bratonien_tools_nc_wizard_state(); - $candidates = is_array($state['storage_candidates']) ? $state['storage_candidates'] : array(); - $mounts = isset($_POST['nc_wizard_storage_mount']) && is_array($_POST['nc_wizard_storage_mount']) ? $_POST['nc_wizard_storage_mount'] : array(); - - if (!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte erkannt.'); - - $storages = array(); - foreach ($candidates as $index=>$candidate) - { - $mount = trim((string)($candidate['local_mount'] ?? '')); - if ($mount === '') $mount = trim((string)($mounts[$index] ?? '')); - if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.'); - - $mount = rtrim($mount, '/'); - if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount); - - $candidate['local_mount'] = $mount; - $storages[] = $candidate; - } - - $state['storages'] = $storages; - $state['storage_candidates'] = $storages; - $state['technical_complete'] = true; - $state['technical_stage'] = 'ready'; - $state['technical_error'] = ''; - bratonien_tools_nc_wizard_store($state); - - return array('message'=>'Speicherzuordnung wurde erfolgreich geprüft.'); -} - -function bratonien_tools_nc_wizard_select_user() -{ - $state = bratonien_tools_nc_wizard_state(); - if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.'); - if (empty($state['technical_complete'])) throw new RuntimeException('Die Verbindung ist noch nicht vollständig geprüft.'); - - $showcase_user = trim((string)($_POST['nc_wizard_showcase_user'] ?? '')); - $connection_name = trim((string)($_POST['nc_wizard_connection_name'] ?? $state['connection_name'])); - if ($showcase_user === '') throw new RuntimeException('Bitte den Benutzer auswählen, der die Showcase-Freigaben bereitstellt.'); - if ($connection_name === '') throw new RuntimeException('Bitte einen Namen für die Verbindung angeben.'); - - $state['connection_name'] = $connection_name; - $state['showcase_user'] = $showcase_user; - $state['step'] = 3; - $state['api_error'] = ''; - bratonien_tools_nc_wizard_store($state); - - return array('message'=>'Showcase-Benutzer übernommen. Jetzt folgt der Piwigo-API-Zugang.'); -} - function bratonien_tools_nc_wizard_api_test() { $state = bratonien_tools_nc_wizard_state(); @@ -500,14 +152,6 @@ function bratonien_tools_nc_wizard_api_test() $key_id = trim((string)$state['_api_key_id']); $secret = trim((string)$state['_api_key_secret']); - - if ($key_id === '' && $secret === '') - { - $stored = bratonien_tools_nc_api_credentials(); - $key_id = trim((string)($stored['key_id'] ?? '')); - $secret = trim((string)($stored['key_secret'] ?? '')); - } - if ($key_id === '' || $secret === '') throw new RuntimeException('API-Schlüssel-ID und API-Geheimnis fehlen.'); try @@ -554,72 +198,6 @@ function bratonien_tools_nc_wizard_api_skip() return array('message'=>'Piwigo-API wurde übersprungen. Für diese Verbindung ist deshalb ein Fallback-Zugang erforderlich.'); } -function bratonien_tools_nc_wizard_finish() -{ - $state = bratonien_tools_nc_wizard_state(); - if ((int)$state['step'] !== 4 || empty($state['technical_complete']) || trim((string)$state['showcase_user']) === '') throw new RuntimeException('Der Assistent ist noch nicht vollständig.'); - - if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']); - if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password']; - bratonien_tools_nc_wizard_store($state); - - $fallback_user = trim((string)$state['_fallback_user']); - $fallback_password = (string)$state['_fallback_password']; - if (($fallback_user === '') !== ($fallback_password === '')) throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.'); - if ($state['api_status'] !== 'ok' && $fallback_user === '') throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist ein Fallback-Zugang erforderlich.'); - - $storage_lines = array(); - foreach ($state['storages'] as $storage) - { - $storage_lines[] = (string)$storage['storage_id'].' | '.trim((string)$storage['source_prefix'], '/').' | '.(string)$storage['local_mount']; - } - - $_POST['nc_name'] = (string)$state['connection_name']; - $_POST['nc_host'] = (string)$state['db_host']; - $_POST['nc_port'] = (string)$state['db_port']; - $_POST['nc_database'] = (string)$state['db_database']; - $_POST['nc_user'] = (string)$state['db_user']; - $_POST['nc_db_password'] = (string)$state['_db_password']; - $_POST['nc_source_view'] = (string)$state['source_view']; - $_POST['nc_activity_view'] = (string)$state['activity_view']; - $_POST['nc_gallery_root'] = (string)$state['gallery_root']; - $_POST['nc_storages'] = implode("\n", $storage_lines); - $_POST['nc_quiet_seconds'] = '120'; - $_POST['nc_max_wait_seconds'] = '900'; - $_POST['nc_full_sync_seconds'] = '86400'; - $_POST['nc_piwigo_user'] = $fallback_user; - $_POST['nc_piwigo_password'] = $fallback_password; - $_POST['nc_nextcloud_url'] = (string)$state['base_url']; - $_POST['nc_showcase_user'] = (string)$state['showcase_user']; - $_POST['nc_access_user'] = (string)$state['username']; - $_POST['nc_product'] = (string)$state['product']; - $_POST['nc_version'] = (string)$state['version']; - $_POST['nc_api_validated'] = $state['api_status'] === 'ok' ? '1' : '0'; - - $result = bratonien_tools_nc_connector_create_local_api_first(); - $connection_id = (int)($result['connection_id'] ?? 0); - - try - { - if ($state['api_status'] === 'ok') bratonien_tools_nc_api_credentials_store((string)$state['_api_key_id'], (string)$state['_api_key_secret']); - } - catch (Throwable $e) - { - if ($connection_id > 0) - { - $table = bratonien_tools_nc_connector_table(); - pwg_query("DELETE FROM `$table` WHERE id=".$connection_id." LIMIT 1"); - } - throw new RuntimeException('Die Verbindung wurde nicht übernommen, weil der geprüfte API-Zugang nicht gespeichert werden konnte: '.$e->getMessage()); - } - - unset($_SESSION['bratonien_nc_wizard']); - unset($result['connection_id']); - $result['message'] = 'Verbindung wurde vollständig durch den Assistenten angelegt. '.$result['message']; - - return $result; -} - function bratonien_tools_nc_connector_update_name() { $id = (int)($_POST['connection_id'] ?? 0); @@ -628,6 +206,10 @@ function bratonien_tools_nc_connector_update_name() $connection = bratonien_tools_nc_connector_connection($id, false); if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.'); + if ((string)($connection['adapter'] ?? '') !== 'remote' || (string)($connection['config']['source_mode'] ?? '') !== 'webdav-placeholder') + { + throw new RuntimeException('Es können nur WebDAV-Verbindungen bearbeitet werden.'); + } $table = bratonien_tools_nc_connector_table(); $now = date('Y-m-d H:i:s'); @@ -635,49 +217,3 @@ function bratonien_tools_nc_connector_update_name() return array('message'=>'Verbindungsname wurde aktualisiert.'); } - -function bratonien_tools_nc_connector_update_technical() -{ - $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 (!empty($connection['enabled']) || (string)$connection['takeover_state'] === 'active') throw new RuntimeException('Technische Einstellungen einer aktiven Verbindung können nicht geändert werden. Bitte zuerst deaktivieren.'); - - $config = $connection['config']; - $port = (int)($_POST['nc_port'] ?? ($config['port'] ?? 5432)); - if ($port < 1 || $port > 65535) throw new RuntimeException('Ungültiger PostgreSQL-Port.'); - - $gallery_root = rtrim(trim((string)($_POST['nc_gallery_root'] ?? ($config['gallery_root'] ?? ''))), '/'); - if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.'); - - $config['host'] = trim((string)($_POST['nc_host'] ?? ($config['host'] ?? ''))); - $config['port'] = (string)$port; - $config['database'] = trim((string)($_POST['nc_database'] ?? ($config['database'] ?? ''))); - $config['user'] = trim((string)($_POST['nc_user'] ?? ($config['user'] ?? ''))); - $config['source_view'] = trim((string)($_POST['nc_source_view'] ?? ($config['source_view'] ?? ''))); - $config['activity_view'] = trim((string)($_POST['nc_activity_view'] ?? ($config['activity_view'] ?? ''))); - $config['gallery_root'] = $gallery_root; - $config['quiet_seconds'] = max(0, (int)($_POST['nc_quiet_seconds'] ?? ($config['quiet_seconds'] ?? 120))); - $config['max_wait_seconds'] = max(60, (int)($_POST['nc_max_wait_seconds'] ?? ($config['max_wait_seconds'] ?? 900))); - $config['full_sync_seconds'] = max(300, (int)($_POST['nc_full_sync_seconds'] ?? ($config['full_sync_seconds'] ?? 86400))); - $config['storages'] = bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? ''); - unset($config['verification']); - - bratonien_tools_nc_connector_view_name($config['source_view']); - bratonien_tools_nc_connector_view_name($config['activity_view']); - - $credentials = bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob'] ?? ''); - $db_password = (string)($_POST['nc_db_password'] ?? ''); - if ($db_password === '') $db_password = $credentials['db_password']; - if ($db_password === '') throw new RuntimeException('Datenbankpasswort fehlt.'); - - $secret_blob = bratonien_tools_nc_connector_encrypt_credentials($db_password, $credentials['piwigo_user'], $credentials['piwigo_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.'); - - $table = bratonien_tools_nc_connector_table(); - $now = date('Y-m-d H:i:s'); - pwg_query("UPDATE `$table` SET takeover_state='disabled', enabled=0, config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1"); - - return array('message'=>'Technische Verbindungseinstellungen wurden gespeichert. Die Verbindung muss erneut geprüft werden.'); -} diff --git a/include/nc_connector_wizard_db_bridge.inc.php b/include/nc_connector_wizard_db_bridge.inc.php deleted file mode 100644 index 75dd12f..0000000 --- a/include/nc_connector_wizard_db_bridge.inc.php +++ /dev/null @@ -1,448 +0,0 @@ -= 300) continue; - $candidate_status = json_decode($response['body'], true); - if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue; - $base_url = $candidate_url; - $status_data = $candidate_status; - break; - } - catch (Throwable $ignored) - { - } - } - - if ($base_url === '' || !is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.'); - - $user_response = bratonien_tools_nc_wizard_http( - $base_url.'/ocs/v2.php/cloud/user?format=json', - $username, - $password, - array('OCS-APIRequest: true') - ); - if ($user_response['status'] === 401 || $user_response['status'] === 403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.'); - if ($user_response['status'] < 200 || $user_response['status'] >= 300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.'); - - $user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']); - $users = array(); - $can_list_users = false; - try - { - $users_response = bratonien_tools_nc_wizard_http( - $base_url.'/ocs/v2.php/cloud/users?format=json', - $username, - $password, - array('OCS-APIRequest: true') - ); - if ($users_response['status'] >= 200 && $users_response['status'] < 300) - { - $users_data = bratonien_tools_nc_wizard_ocs_data($users_response['body']); - if (isset($users_data['users']) && is_array($users_data['users'])) - { - $users = array_values(array_filter(array_map('strval', $users_data['users']))); - sort($users, SORT_NATURAL | SORT_FLAG_CASE); - $can_list_users = true; - } - } - } - catch (Throwable $ignored) - { - } - - $resolved_username = trim((string)($user_data['id'] ?? $username)); - if ($resolved_username === '') $resolved_username = $username; - $url_host = (string)parse_url($base_url, PHP_URL_HOST); - - $state = array_merge($state, array( - 'step'=>2, - 'scan_ok'=>true, - 'base_url'=>$base_url, - 'host_input'=>$host_input, - 'username'=>$resolved_username, - 'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''), - 'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''), - 'product'=>(string)($status_data['productname'] ?? 'Nextcloud'), - 'users'=>$users, - 'can_list_users'=>$can_list_users, - 'showcase_user'=>'', - 'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud', - 'scan_message'=>'Nextcloud wurde erkannt und der Benutzerzugriff wurde bestätigt.', - '_password'=>$password, - 'api_status'=>'pending','api_username'=>'','api_error'=>'', - 'db_host'=>strtolower($url_host),'db_port'=>'5432','db_database'=>'nextcloud','db_user'=>'','db_password_set'=>false,'_db_password'=>'', - 'source_view'=>'piwigo_showcase_sources','activity_view'=>'piwigo_showcase_activity', - 'gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries/nextcloud', - 'storages'=>array(),'storage_candidates'=>array(), - 'technical_stage'=>'auto_check','technical_source'=>'Automatische Prüfung','technical_error'=>'','technical_complete'=>false, - 'directory_selection_ready'=>false, - )); - - if (!bratonien_tools_nc_wizard_apply_known_database_profile($state)) - { - $state['technical_stage'] = 'database_details'; - $state['technical_source'] = 'Keine bekannte Reader-Verbindung gefunden'; - $state['technical_error'] = 'Für diese Nextcloud ist noch keine bekannte Datenbank-Reader-Verbindung gespeichert. Die Nextcloud-Anmeldedaten werden nicht als PostgreSQL-Zugang verwendet.'; - bratonien_tools_nc_wizard_store($state); - return array('message'=>'Nextcloud wurde gefunden. Für den Datenzugriff wird eine separate Reader-Verbindung benötigt.'); - } - - $known_storages = isset($state['storages']) && is_array($state['storages']) ? $state['storages'] : array(); - try - { - bratonien_tools_nc_wizard_finish_data_access_for_selection($state, $known_storages); - } - catch (Throwable $e) - { - $state['technical_complete'] = false; - $state['technical_stage'] = 'database_details'; - $state['technical_error'] = $e->getMessage(); - } - bratonien_tools_nc_wizard_store($state); - - if ($state['technical_stage'] === 'database_details') return array('message'=>'Nextcloud wurde gefunden. Die bekannte Reader-Verbindung konnte noch nicht vollständig bestätigt werden.'); - return array('message'=>'Nextcloud und Datenzugriff wurden bestätigt. Jetzt können die gewünschten Verzeichnisse gewählt werden.'); -} - -function bratonien_tools_nc_wizard_save_technical_with_known_database() -{ - $state = bratonien_tools_nc_wizard_state(); - if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.'); - if (trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') bratonien_tools_nc_wizard_apply_known_database_profile($state); - if (trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') throw new RuntimeException('Für diese Nextcloud sind noch keine Datenbank-Reader-Zugangsdaten bekannt. Nextcloud-Benutzer und -Passwort werden dafür bewusst nicht verwendet.'); - - $known_storages = isset($state['storages']) && is_array($state['storages']) ? $state['storages'] : array(); - $state['db_host'] = trim((string)($_POST['nc_wizard_db_host'] ?? $state['db_host'])); - $state['db_port'] = (string)max(1, min(65535, (int)($_POST['nc_wizard_db_port'] ?? $state['db_port']))); - $state['db_database'] = trim((string)($_POST['nc_wizard_db_database'] ?? $state['db_database'])); - if ($state['db_host'] === '' || $state['db_database'] === '') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.'); - - try - { - bratonien_tools_nc_wizard_finish_data_access_for_selection($state, $known_storages); - bratonien_tools_nc_wizard_store($state); - return array('message'=>'Datenzugriff wurde erfolgreich geprüft. Jetzt können die gewünschten Verzeichnisse gewählt werden.'); - } - catch (Throwable $e) - { - $state['technical_error'] = $e->getMessage(); - $state['technical_stage'] = 'database_details'; - bratonien_tools_nc_wizard_store($state); - throw $e; - } -} - -function bratonien_tools_nc_wizard_directory_options(array $state) -{ - if (empty($state['scan_ok']) || trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') return array(); - - $config = array('host'=>$state['db_host'],'port'=>$state['db_port'],'database'=>$state['db_database'],'user'=>$state['db_user']); - $source_view = bratonien_tools_nc_connector_view_name($state['source_view']); - $rows = bratonien_tools_nc_connector_psql($config, (string)$state['_db_password'], "SELECT DISTINCT storage_id::text || E'\\t' || COALESCE(source_path, '') FROM ".$source_view." ORDER BY 1"); - - $paths = array(); - foreach (preg_split('/\r\n|\r|\n/', trim($rows)) as $line) - { - if ($line === '') continue; - $parts = explode("\t", $line, 2); - if (count($parts) !== 2) continue; - $storage_id = trim($parts[0]); - $source_path = trim($parts[1], '/'); - if ($storage_id === '') continue; - if (!isset($paths[$storage_id])) $paths[$storage_id] = array(); - $paths[$storage_id][] = $source_path; - } - - $result = array(); - foreach ((array)($state['storage_candidates'] ?? array()) as $index=>$candidate) - { - $storage_id = trim((string)($candidate['storage_id'] ?? '')); - $mapping_prefix = trim((string)($candidate['source_prefix'] ?? ''), '/'); - $options = array(''=>'Stammverzeichnis'); - - foreach (($paths[$storage_id] ?? array()) as $source_path) - { - $relative = $source_path; - if ($mapping_prefix !== '') - { - if ($source_path === $mapping_prefix) $relative = ''; - elseif (strpos($source_path, $mapping_prefix.'/') === 0) $relative = substr($source_path, strlen($mapping_prefix) + 1); - else continue; - } - $relative = trim($relative, '/'); - if ($relative === '') continue; - $segments = explode('/', $relative); - $current = ''; - foreach ($segments as $segment) - { - if ($segment === '') continue; - $current = $current === '' ? $segment : $current.'/'.$segment; - $options[$current] = $current; - } - } - - if (count($options) > 1) - { - $root = $options['']; - unset($options['']); - natcasesort($options); - $options = array(''=>$root) + $options; - } - - $result[] = array( - 'index'=>(int)$index, - 'storage_id'=>$storage_id, - 'mount'=>(string)($candidate['local_mount'] ?? ''), - 'ready'=>trim((string)($candidate['local_mount'] ?? '')) !== '', - 'options'=>$options, - ); - } - return $result; -} - -function bratonien_tools_nc_wizard_save_mounts_with_directories() -{ - $state = bratonien_tools_nc_wizard_state(); - $candidates = isset($state['storage_candidates']) && is_array($state['storage_candidates']) ? $state['storage_candidates'] : array(); - if (!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte erkannt.'); - - $mounts = isset($_POST['nc_wizard_storage_mount']) && is_array($_POST['nc_wizard_storage_mount']) ? $_POST['nc_wizard_storage_mount'] : array(); - $directories = isset($_POST['nc_wizard_directory']) && is_array($_POST['nc_wizard_directory']) ? $_POST['nc_wizard_directory'] : array(); - $storages = array(); - - foreach ($candidates as $index=>$candidate) - { - $mount = rtrim(trim((string)($candidate['local_mount'] ?? '')), '/'); - if ($mount === '') $mount = rtrim(trim((string)($mounts[$index] ?? '')), '/'); - if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.'); - if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount); - - $selected = isset($directories[$index]) && is_array($directories[$index]) ? $directories[$index] : array(); - $normalized = array(); - foreach ($selected as $directory) - { - $directory = trim((string)$directory, '/'); - if ($directory === '') continue; - if ($directory[0] === '/' || preg_match('#(^|/)\.\.(/|$)#', $directory)) throw new RuntimeException('Ungültige Verzeichnisauswahl.'); - $normalized[$directory] = true; - } - if (!$normalized) $normalized[''] = true; - - foreach (array_keys($normalized) as $include_prefix) - { - $storages[] = array( - 'storage_id'=>(string)($candidate['storage_id'] ?? ''), - 'source_prefix'=>trim((string)($candidate['source_prefix'] ?? ''), '/'), - 'local_mount'=>$mount, - 'include_prefix'=>$include_prefix, - ); - } - } - - $state['storages'] = $storages; - $state['technical_complete'] = true; - $state['technical_stage'] = 'ready'; - $state['technical_error'] = ''; - $state['directory_selection_ready'] = false; - bratonien_tools_nc_wizard_store($state); - return array('message'=>'Verzeichnisauswahl wurde übernommen.'); -} - -function bratonien_tools_nc_wizard_finish_with_directories() -{ - $state = bratonien_tools_nc_wizard_state(); - if ((int)$state['step'] !== 4 || empty($state['technical_complete']) || trim((string)$state['showcase_user']) === '') throw new RuntimeException('Der Assistent ist noch nicht vollständig.'); - - if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']); - if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password']; - bratonien_tools_nc_wizard_store($state); - - $fallback_user = trim((string)$state['_fallback_user']); - $fallback_password = (string)$state['_fallback_password']; - if (($fallback_user === '') !== ($fallback_password === '')) throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.'); - if ($state['api_status'] !== 'ok' && $fallback_user === '') throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist ein Fallback-Zugang erforderlich.'); - - $mapping_lines = array(); - foreach ((array)$state['storages'] as $storage) - { - $key = (string)$storage['storage_id'].'|'.trim((string)$storage['source_prefix'], '/').'|'.(string)$storage['local_mount']; - $mapping_lines[$key] = (string)$storage['storage_id'].' | '.trim((string)$storage['source_prefix'], '/').' | '.(string)$storage['local_mount']; - } - - $_POST['nc_name']=(string)$state['connection_name'];$_POST['nc_host']=(string)$state['db_host'];$_POST['nc_port']=(string)$state['db_port'];$_POST['nc_database']=(string)$state['db_database'];$_POST['nc_user']=(string)$state['db_user'];$_POST['nc_db_password']=(string)$state['_db_password']; - $_POST['nc_source_view']=(string)$state['source_view'];$_POST['nc_activity_view']=(string)$state['activity_view'];$_POST['nc_gallery_root']=(string)$state['gallery_root'];$_POST['nc_storages']=implode("\n",array_values($mapping_lines)); - $_POST['nc_quiet_seconds']='120';$_POST['nc_max_wait_seconds']='900';$_POST['nc_full_sync_seconds']='86400';$_POST['nc_piwigo_user']=$fallback_user;$_POST['nc_piwigo_password']=$fallback_password; - $_POST['nc_nextcloud_url']=(string)$state['base_url'];$_POST['nc_showcase_user']=(string)$state['showcase_user'];$_POST['nc_access_user']=(string)$state['username'];$_POST['nc_product']=(string)$state['product'];$_POST['nc_version']=(string)$state['version'];$_POST['nc_api_validated']=$state['api_status']==='ok'?'1':'0'; - - $result = bratonien_tools_nc_connector_create_local_api_first(); - $connection_id = (int)($result['connection_id'] ?? 0); - if ($connection_id < 1) throw new RuntimeException('Die neue Verbindung konnte nicht gespeichert werden.'); - - $connection = bratonien_tools_nc_connector_connection($connection_id, false); - if (!$connection) throw new RuntimeException('Die neue Verbindung konnte nach dem Anlegen nicht gelesen werden.'); - $config = $connection['config']; - $config['storages'] = array_values($state['storages']); - $config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($config_json)) throw new RuntimeException('Die Verzeichnisauswahl konnte nicht serialisiert werden.'); - $table = bratonien_tools_nc_connector_table(); - pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$connection_id." LIMIT 1"); - - try - { - if ($state['api_status'] === 'ok') bratonien_tools_nc_api_credentials_store((string)$state['_api_key_id'], (string)$state['_api_key_secret']); - } - catch (Throwable $e) - { - pwg_query("DELETE FROM `$table` WHERE id=".$connection_id." LIMIT 1"); - throw new RuntimeException('Die Verbindung wurde nicht übernommen, weil der geprüfte API-Zugang nicht gespeichert werden konnte: '.$e->getMessage()); - } - - unset($_SESSION['bratonien_nc_wizard']); - unset($result['connection_id']); - $result['message'] = 'Verbindung wurde vollständig durch den Assistenten angelegt. '.$result['message']; - return $result; -} diff --git a/include/nc_connector_wizard_flow.inc.php b/include/nc_connector_wizard_flow.inc.php deleted file mode 100644 index f810625..0000000 --- a/include/nc_connector_wizard_flow.inc.php +++ /dev/null @@ -1,10 +0,0 @@ -(string)$state['db_host'], - 'port'=>(string)$state['db_port'], - 'database'=>(string)$state['db_database'], - 'user'=>(string)$state['db_user'], - ); -} - -function bratonien_tools_nc_wizard_verify_generic_data_access(array &$state) -{ - $config = bratonien_tools_nc_wizard_generic_db_config($state); - $password = (string)$state['_db_password']; - if (trim((string)$config['user']) === '' || $password === '') throw new RuntimeException('Die gespeicherte Reader-Verbindung ist unvollständig.'); - - bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1'); - bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1 FROM piwigo_connector_files LIMIT 1'); - bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1 FROM piwigo_connector_activity LIMIT 1'); - - $state['source_view'] = 'piwigo_connector_files'; - $state['activity_view'] = 'piwigo_connector_activity'; - $state['source_mode'] = 'selected-fileids'; - $state['storages'] = array(); - $state['storage_candidates'] = array(); - $state['roots'] = array(); - $state['technical_complete'] = false; - $state['technical_stage'] = 'mounts'; - $state['technical_source'] = 'Generische Reader-Schnittstelle geprüft'; - $state['technical_error'] = ''; - $state['directory_selection_ready'] = true; -} - -function bratonien_tools_nc_wizard_known_adapter($storage_id, $source_path) -{ - $storage_id = trim((string)$storage_id); - $source_path = trim((string)$source_path, '/'); - $matches = array(); - - foreach (bratonien_tools_nc_connector_connections() as $connection) - { - foreach ((array)($connection['config']['storages'] ?? array()) as $storage) - { - if ((string)($storage['storage_id'] ?? '') !== $storage_id) continue; - $prefix = trim((string)($storage['source_prefix'] ?? ''), '/'); - if ($prefix !== '' && $source_path !== $prefix && strpos($source_path, $prefix.'/') !== 0) continue; - $mount = rtrim(trim((string)($storage['local_mount'] ?? '')), '/'); - if ($mount === '' || !is_dir($mount) || !is_readable($mount)) continue; - $key = $prefix.'|'.$mount; - $matches[$key] = array('storage_id'=>$storage_id,'source_prefix'=>$prefix,'local_mount'=>$mount); - } - } - - if (!$matches) return null; - $max = -1; - foreach ($matches as $match) $max = max($max, strlen((string)$match['source_prefix'])); - $best = array_values(array_filter($matches, function($match) use ($max) { return strlen((string)$match['source_prefix']) === $max; })); - return count($best) === 1 ? $best[0] : null; -} - -function bratonien_tools_nc_wizard_resolve_selected_roots(array &$state) -{ - $selected = isset($state['directory_selected']) && is_array($state['directory_selected']) ? $state['directory_selected'] : array(); - $selected_fileids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids']) ? $state['directory_selected_fileids'] : array(); - if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.'); - - $ids = array(); - foreach ($selected as $path) - { - if (!isset($selected_fileids[$path]) || (int)$selected_fileids[$path] < 1) throw new RuntimeException('Für eine Auswahl fehlt die eindeutige Nextcloud-Datei-ID. Bitte das Verzeichnis erneut auswählen.'); - $ids[(int)$selected_fileids[$path]] = (string)$path; - } - - $config = bratonien_tools_nc_wizard_generic_db_config($state); - $id_list = implode(',', array_map('intval', array_keys($ids))); - $rows = bratonien_tools_nc_connector_psql( - $config, - (string)$state['_db_password'], - "SELECT fileid::text || E'\\t' || storage_id || E'\\t' || COALESCE(source_path, '') FROM piwigo_connector_files WHERE fileid IN (".$id_list.") ORDER BY fileid" - ); - - $resolved = array(); - foreach (preg_split('/\r\n|\r|\n/', trim($rows)) as $line) - { - if ($line === '') continue; - $parts = explode("\t", $line, 3); - if (count($parts) !== 3 || !ctype_digit($parts[0])) continue; - $resolved[(int)$parts[0]] = array('storage_id'=>(string)$parts[1], 'source_path'=>trim((string)$parts[2], '/')); - } - - $roots = array(); - $candidates = array(); - foreach ($ids as $fileid=>$path) - { - if (!isset($resolved[$fileid])) throw new RuntimeException('Das ausgewählte Nextcloud-Verzeichnis mit Datei-ID '.$fileid.' konnte nicht mehr aufgelöst werden.'); - $storage_id = trim((string)$resolved[$fileid]['storage_id']); - $source_path = trim((string)$resolved[$fileid]['source_path'], '/'); - if ($storage_id === '') throw new RuntimeException('Nextcloud hat für Datei-ID '.$fileid.' keine Storage-ID geliefert.'); - - $display_name = $path === '' ? ((string)($state['display_name'] ?? '') !== '' ? (string)$state['display_name'] : 'Nextcloud') : basename($path); - $roots[] = array( - 'fileid'=>(int)$fileid, - 'display_name'=>$display_name, - 'webdav_path'=>$path, - 'storage_id'=>$storage_id, - 'source_path'=>$source_path, - ); - - $adapter = bratonien_tools_nc_wizard_known_adapter($storage_id, $source_path); - if (!$adapter) $adapter = array('storage_id'=>$storage_id,'source_prefix'=>'','local_mount'=>''); - $key = $adapter['storage_id'].'|'.$adapter['source_prefix'].'|'.$adapter['local_mount']; - $candidates[$key] = $adapter; - } - - $state['roots'] = array_values($roots); - $state['storage_candidates'] = array_values($candidates); - $state['storages'] = $state['storage_candidates']; - - $all_mapped = !empty($state['storage_candidates']); - foreach ($state['storage_candidates'] as $candidate) - { - $mount = rtrim(trim((string)($candidate['local_mount'] ?? '')), '/'); - if ($mount === '' || !is_dir($mount) || !is_readable($mount)) $all_mapped = false; - } - - if ($all_mapped) - { - $state['technical_complete'] = true; - $state['technical_stage'] = 'ready'; - $state['directory_selection_ready'] = false; - return; - } - - $state['technical_complete'] = false; - $state['technical_stage'] = 'mounts'; - $state['directory_selection_ready'] = false; - $state['mount_prompted'] = true; -} - -function bratonien_tools_nc_wizard_scan_user_scoped() -{ - $state = bratonien_tools_nc_wizard_state(); - $host_input = trim((string)($_POST['nc_wizard_host'] ?? $state['host_input'])); - $username = trim((string)($_POST['nc_wizard_user'] ?? $state['username'])); - $password = array_key_exists('nc_wizard_password', $_POST) ? (string)$_POST['nc_wizard_password'] : (string)$state['_password']; - - $state['step'] = 1; - $state['host_input'] = $host_input; - $state['username'] = $username; - $state['_password'] = $password; - bratonien_tools_nc_wizard_store($state); - if ($username === '' || $password === '') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.'); - - $base_url = ''; - $status_data = null; - foreach (bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url) - { - try - { - $response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php'); - if ($response['status'] < 200 || $response['status'] >= 300) continue; - $candidate_status = json_decode($response['body'], true); - if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue; - $base_url = $candidate_url; - $status_data = $candidate_status; - break; - } - catch (Throwable $ignored) {} - } - if ($base_url === '' || !is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.'); - - $user_response = bratonien_tools_nc_wizard_http($base_url.'/ocs/v2.php/cloud/user?format=json', $username, $password, array('OCS-APIRequest: true')); - if ($user_response['status'] === 401 || $user_response['status'] === 403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.'); - if ($user_response['status'] < 200 || $user_response['status'] >= 300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.'); - - $user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']); - $resolved_username = trim((string)($user_data['id'] ?? $username)); - if ($resolved_username === '') $resolved_username = $username; - $url_host = (string)parse_url($base_url, PHP_URL_HOST); - - $state = array_merge($state, array( - 'step'=>2,'scan_ok'=>true,'base_url'=>$base_url,'host_input'=>$host_input,'username'=>$resolved_username, - 'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''), - 'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''),'product'=>(string)($status_data['productname'] ?? 'Nextcloud'), - 'users'=>array(),'can_list_users'=>false,'showcase_user'=>$resolved_username,'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud', - 'scan_message'=>'Nextcloud wurde erkannt und der Benutzerzugriff wurde bestätigt.','_password'=>$password, - 'api_status'=>'pending','api_username'=>'','api_error'=>'','db_host'=>strtolower($url_host),'db_port'=>'5432','db_database'=>'nextcloud','db_user'=>'','db_password_set'=>false,'_db_password'=>'', - 'source_view'=>'piwigo_connector_files','activity_view'=>'piwigo_connector_activity','source_mode'=>'selected-fileids','gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries/nextcloud', - 'storages'=>array(),'storage_candidates'=>array(),'roots'=>array(),'technical_stage'=>'auto_check','technical_source'=>'Automatische Prüfung','technical_error'=>'','technical_complete'=>false, - 'directory_selection_ready'=>false,'directory_path'=>'','directory_parent'=>'','directory_children'=>array(),'directory_current_fileid'=>0,'directory_fileids'=>array(),'directory_selected'=>array(),'directory_selected_fileids'=>array(), - 'database_prompted'=>false,'mount_prompted'=>false, - )); - - if (!bratonien_tools_nc_wizard_apply_known_database_profile($state)) - { - $state['technical_stage'] = 'database_details'; - $state['database_prompted'] = true; - $state['technical_source'] = 'Keine bekannte Reader-Verbindung gefunden'; - $state['technical_error'] = 'Für diese Nextcloud ist noch keine bekannte Datenbank-Reader-Verbindung gespeichert.'; - bratonien_tools_nc_wizard_store($state); - return array('message'=>'Nextcloud wurde gefunden. Für den Datenzugriff wird eine separate Reader-Verbindung benötigt.'); - } - - try - { - bratonien_tools_nc_wizard_verify_generic_data_access($state); - bratonien_tools_nc_wizard_refresh_directory_state($state, ''); - } - catch (Throwable $e) - { - $state['technical_complete'] = false; - $state['technical_stage'] = 'database_details'; - $state['database_prompted'] = true; - $state['technical_error'] = $e->getMessage().' Die generischen Connector-Views müssen in Nextcloud eingerichtet sein.'; - } - bratonien_tools_nc_wizard_store($state); - - if ($state['technical_stage'] === 'database_details') return array('message'=>'Nextcloud wurde gefunden. Die generische Reader-Schnittstelle konnte noch nicht bestätigt werden.'); - return array('message'=>'Nextcloud und generischer Datenzugriff wurden bestätigt. Jetzt können die Verzeichnisse des angemeldeten Benutzers gewählt werden.'); -} - -function bratonien_tools_nc_wizard_save_technical_generic() -{ - $state = bratonien_tools_nc_wizard_state(); - if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.'); - if (trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') bratonien_tools_nc_wizard_apply_known_database_profile($state); - if (trim((string)($state['db_user'] ?? '')) === '' || (string)($state['_db_password'] ?? '') === '') throw new RuntimeException('Für diese Nextcloud sind noch keine Datenbank-Reader-Zugangsdaten bekannt.'); - - $state['db_host'] = trim((string)($_POST['nc_wizard_db_host'] ?? $state['db_host'])); - $state['db_port'] = (string)max(1, min(65535, (int)($_POST['nc_wizard_db_port'] ?? $state['db_port']))); - $state['db_database'] = trim((string)($_POST['nc_wizard_db_database'] ?? $state['db_database'])); - if ($state['db_host'] === '' || $state['db_database'] === '') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.'); - - try - { - bratonien_tools_nc_wizard_verify_generic_data_access($state); - bratonien_tools_nc_wizard_refresh_directory_state($state, ''); - bratonien_tools_nc_wizard_store($state); - return array('message'=>'Generischer Datenzugriff wurde erfolgreich geprüft. Jetzt können Verzeichnisse ausgewählt werden.'); - } - catch (Throwable $e) - { - $state['technical_error'] = $e->getMessage(); - $state['technical_stage'] = 'database_details'; - $state['database_prompted'] = true; - bratonien_tools_nc_wizard_store($state); - throw $e; - } -} - function bratonien_tools_nc_wizard_directory_browse() { $state = bratonien_tools_nc_wizard_state(); @@ -400,42 +150,6 @@ function bratonien_tools_nc_wizard_directory_remove() return array('message'=>'Verzeichnis entfernt.'); } -function bratonien_tools_nc_wizard_save_mounts_server_side() -{ - $state = bratonien_tools_nc_wizard_state(); - if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts') throw new RuntimeException('Die Speicher-/Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.'); - - if (!empty($state['directory_selection_ready'])) - { - bratonien_tools_nc_wizard_resolve_selected_roots($state); - bratonien_tools_nc_wizard_store($state); - return array('message'=>!empty($state['technical_complete']) ? 'Verzeichnisse und vorhandene Storage-Adapter wurden übernommen.' : 'Verzeichnisse wurden aufgelöst. Ein neuer Storage-Adapter muss noch zugeordnet werden.'); - } - - $candidates = isset($state['storage_candidates']) && is_array($state['storage_candidates']) ? $state['storage_candidates'] : array(); - if (!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte aus der Verzeichnisauswahl aufgelöst.'); - $mounts = isset($_POST['nc_wizard_storage_mount']) && is_array($_POST['nc_wizard_storage_mount']) ? $_POST['nc_wizard_storage_mount'] : array(); - - foreach ($candidates as $index=>&$candidate) - { - $mount = rtrim(trim((string)($candidate['local_mount'] ?? '')), '/'); - if ($mount === '') $mount = rtrim(trim((string)($mounts[$index] ?? '')), '/'); - if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.'); - if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount); - $candidate['local_mount'] = $mount; - } - unset($candidate); - - $state['storage_candidates'] = array_values($candidates); - $state['storages'] = $state['storage_candidates']; - $state['technical_complete'] = true; - $state['technical_stage'] = 'ready'; - $state['technical_error'] = ''; - $state['directory_selection_ready'] = false; - bratonien_tools_nc_wizard_store($state); - return array('message'=>'Storage-Adapter wurde geprüft. Die Verbindung ist technisch vollständig.'); -} - function bratonien_tools_nc_wizard_select_current_user() { $state = bratonien_tools_nc_wizard_state(); @@ -444,7 +158,6 @@ function bratonien_tools_nc_wizard_select_current_user() $connection_name = trim((string)($_POST['nc_wizard_connection_name'] ?? $state['connection_name'])); if ($connection_name === '') throw new RuntimeException('Bitte einen Namen für die Verbindung angeben.'); $state['connection_name'] = $connection_name; - $state['showcase_user'] = (string)$state['username']; $state['step'] = 3; $state['api_error'] = ''; bratonien_tools_nc_wizard_store($state); @@ -475,23 +188,10 @@ function bratonien_tools_nc_wizard_back() $state['directory_selection_ready'] = true; bratonien_tools_nc_wizard_refresh_directory_state($state); } - elseif ((string)($state['technical_stage'] ?? '') === 'mounts' && !empty($state['directory_selection_ready'])) - { - if (!empty($state['database_prompted'])) $state['technical_stage'] = 'database_details'; else $state['step'] = 1; - $state['directory_selection_ready'] = false; - } - elseif ((string)($state['technical_stage'] ?? '') === 'mounts') - { - $state['directory_selection_ready'] = true; - bratonien_tools_nc_wizard_refresh_directory_state($state); - } - elseif ((string)($state['technical_stage'] ?? '') === 'database_details') - { - $state['step'] = 1; - } else { $state['step'] = 1; + $state['directory_selection_ready'] = false; } bratonien_tools_nc_wizard_store($state); diff --git a/include/nc_connector_wizard_webdav_flow.inc.php b/include/nc_connector_wizard_webdav_flow.inc.php index d8b7f6e..c60cb30 100644 --- a/include/nc_connector_wizard_webdav_flow.inc.php +++ b/include/nc_connector_wizard_webdav_flow.inc.php @@ -73,17 +73,12 @@ function bratonien_tools_nc_wizard_scan_webdav_first() 'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''), 'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''), 'product'=>(string)($status_data['productname'] ?? 'Nextcloud'), - 'users'=>array(), - 'can_list_users'=>false, - 'showcase_user'=>$resolved_username, 'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud WebDAV', 'scan_message'=>'Nextcloud und WebDAV-Zugriff wurden bestätigt.', '_password'=>$password, 'source_mode'=>'webdav-placeholder', 'transport'=>'webdav', 'gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries', - 'storages'=>array(), - 'storage_candidates'=>array(), 'roots'=>array(), 'technical_stage'=>'mounts', 'technical_source'=>'WebDAV', @@ -97,8 +92,6 @@ function bratonien_tools_nc_wizard_scan_webdav_first() 'directory_fileids'=>array(), 'directory_selected'=>array(), 'directory_selected_fileids'=>array(), - 'database_prompted'=>false, - 'mount_prompted'=>false, 'api_status'=>'pending', 'api_username'=>'', 'api_error'=>'', @@ -115,7 +108,7 @@ function bratonien_tools_nc_wizard_save_sources_dispatch() $state = bratonien_tools_nc_wizard_state(); if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder') { - return bratonien_tools_nc_wizard_save_mounts_server_side(); + throw new RuntimeException('Der Assistent unterstützt ausschließlich WebDAV-Verbindungen.'); } if ((int)($state['step'] ?? 0) !== 2 || empty($state['directory_selection_ready'])) @@ -158,7 +151,7 @@ function bratonien_tools_nc_wizard_save_sources_dispatch() $state['directory_selection_ready'] = false; bratonien_tools_nc_wizard_store($state); - return array('message'=>'WebDAV-Verzeichnisse wurden übernommen. Die Verbindung ist für den parallelen Testweg vorbereitet.'); + return array('message'=>'WebDAV-Verzeichnisse wurden übernommen.'); } function bratonien_tools_nc_wizard_finish_dispatch() @@ -166,7 +159,7 @@ function bratonien_tools_nc_wizard_finish_dispatch() $state = bratonien_tools_nc_wizard_state(); if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder') { - return bratonien_tools_nc_wizard_finish_generic_scope(); + throw new RuntimeException('Der Assistent unterstützt ausschließlich WebDAV-Verbindungen.'); } if ((int)($state['step'] ?? 0) !== 4 || empty($state['technical_complete'])) diff --git a/include/nc_connector_ws.inc.php b/include/nc_connector_ws.inc.php deleted file mode 100644 index d989ff6..0000000 --- a/include/nc_connector_ws.inc.php +++ /dev/null @@ -1,282 +0,0 @@ -addMethod( - 'bratonien.nc.sync', - 'bratonien_tools_ws_nc_sync', - array( - 'site_id' => array( - 'default' => 1, - 'type' => WS_TYPE_ID, - 'info' => 'Piwigo storage site to synchronize. Default: 1.', - ), - 'simulate' => array( - 'default' => true, - 'type' => WS_TYPE_BOOL, - 'info' => 'Simulation only. Productive API synchronization is disabled until parity has been verified.', - ), - ), - 'Simulates the local Piwigo filesystem synchronization used by the Bratonien NC Connector.', - null, - array( - 'admin_only' => true, - 'post_only' => true, - ) - ); -} - -function bratonien_tools_ws_nc_sync_error(&$errors, $path, $type) -{ - $errors[] = array( - 'path' => (string)$path, - 'type' => (string)$type, - ); -} - -/** - * Read-only parity test for Piwigo 16.4.0 admin/site_update.php with the - * connector's fixed settings: - * sync=files, privacy_level=0, sync_meta=1, subcats-included=1, - * no cat, no caddie, no meta_all, no meta_empty_overrides. - * - * The simulation deliberately performs no INSERT/UPDATE/DELETE and triggers - * no Piwigo activity/events. It only calculates what the matching core admin - * synchronization would attempt to change. - */ -function bratonien_tools_ws_nc_sync($params, &$service) -{ - global $conf, $user, $logger; - - 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 API synchronization is not approved for Piwigo '.$piwigo_version.'. Use the administrator fallback until this Piwigo version has been verified.' - ); - } - - $simulate = !isset($params['simulate']) || (bool)$params['simulate']; - if (!$simulate) - { - return new PwgError( - 409, - 'Productive Bratonien API synchronization is not enabled yet. Run the simulation and verify parity with Piwigo admin synchronization first.' - ); - } - - $site_id = isset($params['site_id']) ? (int)$params['site_id'] : 1; - if ($site_id < 1) - { - return new PwgError(400, 'Invalid site_id.'); - } - - include_once(PHPWG_ROOT_PATH.'admin/include/functions.php'); - include_once(PHPWG_ROOT_PATH.'admin/site_reader_local.php'); - - $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 by this synchronization method.'); - } - - $errors = array(); - $site_reader = new LocalSiteReader($site_url); - if (!$site_reader->open()) - { - return new PwgError(500, 'Piwigo could not open the configured local site.'); - } - - $counts = array( - 'new_categories' => 0, - 'del_categories' => 0, - 'new_elements' => 0, - 'del_elements' => 0, - 'upd_elements' => 0, - 'new_formats' => 0, - 'del_formats' => 0, - 'metadata_candidates' => 0, - ); - - // ------------------------------------------------------------------- - // Directories / physical categories - // ------------------------------------------------------------------- - $query = ' -SELECT id, uppercats, global_rank, status, visible - FROM '.CATEGORIES_TABLE.' - WHERE dir IS NOT NULL - AND site_id = '.$site_id; - $db_categories = hash_from_query($query, 'id'); - $db_fulldirs = get_fulldirs(array_keys($db_categories)); - $basedir = preg_replace('#/*$#', '', $site_url); - $db_fulldirs = array_flip($db_fulldirs); - - $fs_fulldirs = $site_reader->get_full_directories($basedir); - $next_id = pwg_db_nextval('id', CATEGORIES_TABLE); - - foreach (array_diff($fs_fulldirs, array_keys($db_fulldirs)) as $fulldir) - { - $dir = basename($fulldir); - if (!preg_match($conf['sync_chars_regex'], $dir)) - { - bratonien_tools_ws_nc_sync_error($errors, $fulldir, 'PWG-UPDATE-1'); - continue; - } - - $virtual_id = $next_id++; - $db_fulldirs[$fulldir] = $virtual_id; - $db_categories[$virtual_id] = array('id'=>$virtual_id); - $counts['new_categories']++; - } - - $to_delete = array(); - foreach (array_diff(array_keys($db_fulldirs), $fs_fulldirs) as $fulldir) - { - $to_delete[] = $db_fulldirs[$fulldir]; - } - $counts['del_categories'] = count($to_delete); - - // ------------------------------------------------------------------- - // Files / images / formats - // ------------------------------------------------------------------- - $fs = $site_reader->get_elements($basedir); - $cat_ids = array_diff(array_keys($db_categories), $to_delete); - $db_elements = array(); - - if (count($cat_ids) > 0) - { - $query = ' -SELECT id, path - FROM '.IMAGES_TABLE.' - WHERE storage_category_id IN ('.wordwrap(implode(', ', $cat_ids), 160, "\n").')'; - $db_elements = simple_hash_from_query($query, 'id', 'path'); - } - - foreach (array_diff(array_keys($fs), $db_elements) as $path) - { - $dirname = dirname($path); - if (!isset($db_fulldirs[$dirname])) - { - continue; - } - - $filename = basename($path); - if (!preg_match($conf['sync_chars_regex'], $filename)) - { - bratonien_tools_ws_nc_sync_error($errors, $path, 'PWG-UPDATE-1'); - continue; - } - - $counts['new_elements']++; - if (!empty($conf['enable_formats']) && isset($fs[$path]['formats']) && is_array($fs[$path]['formats'])) - { - $counts['new_formats'] += count($fs[$path]['formats']); - } - } - - if (!empty($conf['enable_formats']) && count($db_elements) > 0) - { - $db_elements_flip = array_flip($db_elements); - $existing_ids = array(); - foreach (array_intersect_key($fs, $db_elements_flip) as $path => $unused) - { - $existing_ids[] = $db_elements_flip[$path]; - } - - if (count($existing_ids) > 0) - { - $db_formats = array(); - $query = ' -SELECT * - FROM '.IMAGE_FORMAT_TABLE.' - WHERE image_id IN ('.implode(',', $existing_ids).')'; - $result = pwg_query($query); - while ($row = pwg_db_fetch_assoc($result)) - { - if (!isset($db_formats[$row['image_id']])) - { - $db_formats[$row['image_id']] = array(); - } - $db_formats[$row['image_id']][$row['ext']] = $row['format_id']; - } - - foreach ($db_formats as $image_id => $formats) - { - $path = $db_elements[$image_id]; - $filesystem_formats = isset($fs[$path]['formats']) && is_array($fs[$path]['formats']) ? $fs[$path]['formats'] : array(); - $counts['del_formats'] += count(array_diff_key($formats, $filesystem_formats)); - } - - foreach ($existing_ids as $image_id) - { - $path = $db_elements[$image_id]; - $known_formats = isset($db_formats[$image_id]) ? $db_formats[$image_id] : array(); - $filesystem_formats = isset($fs[$path]['formats']) && is_array($fs[$path]['formats']) ? $fs[$path]['formats'] : array(); - $counts['new_formats'] += count(array_diff_key($filesystem_formats, $known_formats)); - } - } - } - - $counts['del_elements'] = count(array_diff($db_elements, array_keys($fs))); - - $files = get_filelist('', $site_id, true, false); - $update_count = 0; - foreach ($files as $id => $file) - { - $data = $site_reader->get_element_update_attributes($file['path']); - if (is_array($data)) - { - $update_count++; - } - } - $counts['upd_elements'] = $update_count; - - $metadata_files = get_filelist('', $site_id, true, true); - $counts['metadata_candidates'] = count($metadata_files); - - return array( - 'mode' => 'simulation', - 'approved_piwigo_version' => '16.4.0', - 'piwigo_version' => $piwigo_version, - 'site_id' => $site_id, - 'site_url' => $site_url, - 'counts' => $counts, - 'errors' => $errors, - 'error_count' => count($errors), - 'database_writes' => false, - 'fallback' => 'administrator', - 'connected_with' => isset($_SESSION['connected_with']) ? (string)$_SESSION['connected_with'] : '', - 'username' => isset($user['username']) ? (string)$user['username'] : '', - 'status' => isset($user['status']) ? (string)$user['status'] : '', - ); -} diff --git a/include/tool_registry.inc.php b/include/tool_registry.inc.php index 7399cdb..91eb9fa 100644 --- a/include/tool_registry.inc.php +++ b/include/tool_registry.inc.php @@ -16,17 +16,10 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/self_update.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector.inc.php'); -require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_manage.inc.php'); -require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_takeover.inc.php'); -require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_auth.inc.php'); -require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.inc.php'); -require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_db_bridge.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_user_scope.inc.php'); -require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_flow.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_connection_scope.inc.php'); -require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_generic_scope.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php'); @@ -54,13 +47,10 @@ function bratonien_tools_get_tools() 'album_share_create' => array('handler' => 'bratonien_tools_create_album_share'), 'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'), 'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'), - 'nc_connector_create_local' => array('handler' => 'bratonien_tools_nc_connector_create_local_api_first'), 'nc_connector_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'), 'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'), 'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'), - 'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'), 'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'), - 'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'), 'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'), 'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'), 'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'), @@ -71,15 +61,8 @@ function bratonien_tools_get_tools() 'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_dispatch'), 'nc_connector_wizard_back' => array('handler' => 'bratonien_tools_nc_wizard_back'), 'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'), - 'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'), - 'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_connection_scoped'), - 'nc_connector_prepare_takeover' => array('handler' => 'bratonien_tools_nc_connector_prepare_takeover'), - 'nc_connector_cancel_takeover' => array('handler' => 'bratonien_tools_nc_connector_cancel_takeover'), - 'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'), - 'nc_connector_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'), 'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'), 'nc_connector_fallback_delete' => array('handler' => 'bratonien_tools_nc_connector_fallback_delete_scoped'), - 'nc_connector_fallback_once' => array('handler' => 'bratonien_tools_nc_connector_fallback_once'), 'self_update_check' => array('handler' => 'bratonien_tools_self_update_check'), 'self_update_run' => array('handler' => 'bratonien_tools_self_update_run'), ); diff --git a/main.inc.php b/main.inc.php index d8bd9d5..f4e95cc 100644 --- a/main.inc.php +++ b/main.inc.php @@ -1,7 +1,7 @@ \n"); - exit(1); -} - -$connectionId = (int)$argv[1]; -$piwigoRoot = dirname(__DIR__, 2); -$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; -$legacyConfig = '/etc/piwigo-sync/piwigo.conf'; -$legacyRuntime = '/opt/piwigo-sync/sync.sh'; -$newTimer = 'bratonien-nc-connector.timer'; -$newService = 'bratonien-nc-connector.service'; -$legacyTimer = 'piwigo-sync.timer'; - -function fail($message) -{ - throw new RuntimeException($message); -} - -function readKeyValueFile($path) -{ - if (!is_readable($path)) fail('Konfiguration nicht lesbar: '.$path); - $result = array(); - foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) - { - $line = trim($line); - if ($line === '' || $line[0] === '#') continue; - if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches)) continue; - $value = trim($matches[2]); - if (strlen($value) >= 2) - { - $first = $value[0]; $last = $value[strlen($value)-1]; - if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) $value = substr($value, 1, -1); - } - $result[$matches[1]] = $value; - } - return $result; -} - -function runCommand(array $command, $allowFailure = false) -{ - $spec = array(0=>array('file','/dev/null','r'),1=>array('pipe','w'),2=>array('pipe','w')); - $process = proc_open($command, $spec, $pipes); - if (!is_resource($process)) fail('Prozess konnte nicht gestartet werden: '.implode(' ', $command)); - $stdout = stream_get_contents($pipes[1]); $stderr = stream_get_contents($pipes[2]); - fclose($pipes[1]); fclose($pipes[2]); - $exit = proc_close($process); - if ($exit !== 0 && !$allowFailure) - { - $detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout); - fail('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : '')); - } - return array('exit'=>$exit,'stdout'=>(string)$stdout,'stderr'=>(string)$stderr); -} - -function decryptConnectorSecret($blob, $hexKey) -{ - if (!preg_match('/^[a-f0-9]{64}$/', $hexKey)) fail('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('Connector-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($hexKey), OPENSSL_RAW_DATA, $iv, $tag); - if ($plain === false || $plain === '') fail('Connector-Zugangsdaten konnten nicht entschluesselt werden.'); - - $decoded = json_decode($plain, true); - if (is_array($decoded) && array_key_exists('db_password', $decoded)) - { - $password = (string)$decoded['db_password']; - if ($password === '') fail('Datenbankpasswort fehlt in den Connector-Zugangsdaten.'); - return $password; - } - - // Backward compatibility for older imported connections that stored only - // the database password as plaintext inside the encrypted envelope. - return (string)$plain; -} - -function sqlEscape(mysqli $db, $value) -{ - return $db->real_escape_string((string)$value); -} - -function saveTakeoverResult(mysqli $db, $table, $connectionId, array $config, $state, $enabled) -{ - $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($json)) fail('Connector-Status konnte nicht serialisiert werden.'); - $now = date('Y-m-d H:i:s'); - $sql = "UPDATE `".$table."` SET takeover_state='".sqlEscape($db, $state)."', enabled=".($enabled ? 1 : 0).", config_json='".sqlEscape($db, $json)."', updated='".sqlEscape($db, $now)."' WHERE id=".(int)$connectionId; - if (!$db->query($sql)) fail('Connector-Status konnte nicht gespeichert werden: '.$db->error); -} - -$legacyTimerWasEnabled = false; -$newTimerInstalled = false; -$db = null; -$table = ''; -$config = array(); - -try -{ - if (!is_readable($dbConfig)) fail('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig); - if (!is_executable($legacyRuntime)) fail('Bestehende Sync-Runtime fehlt: '.$legacyRuntime); - - $conf = array(); $prefixeTable = 'piwigo_'; require $dbConfig; - foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.'); - - $db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); - if ($db->connect_errno) fail('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error); - $db->set_charset('utf8mb4'); - - $table = $prefixeTable.'bratonien_tools_nc_connections'; - $result = $db->query("SELECT id, takeover_state, enabled, config_json, secret_blob FROM `".$table."` WHERE id=".$connectionId." LIMIT 1"); - if (!$result || !$result->num_rows) fail('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.'); - $row = $result->fetch_assoc(); - if ((string)$row['takeover_state'] !== 'ready' || (int)$row['enabled'] !== 0) fail('Connector-Verbindung muss im Zustand ready und deaktiviert sein.'); - $config = json_decode((string)$row['config_json'], true); - if (!is_array($config) || empty($config['verification']['ok'])) fail('Connector-Verbindung besitzt keine erfolgreiche Verifikation.'); - - $keyResult = $db->query("SELECT value FROM `".$prefixeTable."config` WHERE param='bratonien_nc_connector_secret' LIMIT 1"); - if (!$keyResult || !$keyResult->num_rows) fail('Connector-Schluessel wurde in Piwigo nicht gefunden.'); - $keyRow = $keyResult->fetch_assoc(); - $dbPassword = decryptConnectorSecret($row['secret_blob'], (string)$keyRow['value']); - - $legacy = readKeyValueFile($legacyConfig); - $piwigoUser = trim((string)($legacy['PIWIGO_SYNC_USER'] ?? '')); - $piwigoPasswordFile = trim((string)($legacy['PIWIGO_SYNC_PASSWORD_FILE'] ?? '/etc/piwigo-sync/piwigo-password')); - if ($piwigoUser === '' || !is_readable($piwigoPasswordFile)) fail('Legacy-Piwigo-Sync-Zugangsdaten konnten fuer den einmaligen Cutover nicht gelesen werden.'); - $piwigoPassword = trim((string)file_get_contents($piwigoPasswordFile)); - if ($piwigoPassword === '') fail('Legacy-Piwigo-Sync-Passwort ist leer.'); - - foreach (array('host','port','database','user','source_view','gallery_root','state_dir') as $key) - { - if (!isset($config[$key]) || trim((string)$config[$key]) === '') fail('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.'); - } - - $runtimeDir = '/etc/bratonien-tools/nc-connector'; - if (!is_dir($runtimeDir) && !mkdir($runtimeDir, 0700, true)) fail('Connector-Laufzeitverzeichnis konnte nicht angelegt werden.'); - chmod($runtimeDir, 0700); - - $base = $runtimeDir.'/connection-'.$connectionId; - $dbPasswordPath = $base.'.db-password'; - $piwigoPasswordPath = $base.'.piwigo-password'; - $storagePath = $base.'.storages.tsv'; - $configPath = $base.'.conf'; - $statusPath = rtrim((string)$config['state_dir'], '/').'/connector-status.json'; - - file_put_contents($dbPasswordPath, $dbPassword."\n", LOCK_EX); - file_put_contents($piwigoPasswordPath, $piwigoPassword."\n", LOCK_EX); - chmod($dbPasswordPath, 0600); chmod($piwigoPasswordPath, 0600); - - $storageLines = array('# storage_idsource_prefixlocal_mount'); - foreach ((array)($config['storages'] ?? array()) as $storage) - { - $storageLines[] = (string)($storage['storage_id'] ?? '')."\t".trim((string)($storage['source_prefix'] ?? ''), '/')."\t".(string)($storage['local_mount'] ?? ''); - } - file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); chmod($storagePath, 0600); - - $piwigoRootConfigured = isset($legacy['PIWIGO_ROOT']) && trim((string)$legacy['PIWIGO_ROOT']) !== '' ? trim((string)$legacy['PIWIGO_ROOT']) : $piwigoRoot; - $lines = array( - 'PIWIGO_ROOT='.$piwigoRootConfigured, - 'GALLERY_ROOT='.(string)$config['gallery_root'], - 'STATE_DIR='.(string)$config['state_dir'], - 'STATUS_FILE='.$statusPath, - 'NC_DB_HOST='.(string)$config['host'], - 'NC_DB_PORT='.(string)$config['port'], - 'NC_DB_NAME='.(string)$config['database'], - 'NC_DB_USER='.(string)$config['user'], - 'NC_DB_VIEW='.(string)$config['source_view'], - 'NC_DB_PASSWORD_FILE='.$dbPasswordPath, - 'STORAGE_CONFIG='.$storagePath, - 'QUIET_SECONDS='.(int)($config['quiet_seconds'] ?? 120), - 'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900), - 'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400), - 'PIWIGO_SYNC_ENABLED=1', - 'PIWIGO_SYNC_USER='.$piwigoUser, - 'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath, - ); - file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); chmod($configPath, 0600); - - $enabledCheck = runCommand(array('systemctl', 'is-enabled', $legacyTimer), true); - $legacyTimerWasEnabled = $enabledCheck['exit'] === 0; - - echo "Stoppe Legacy-Timer...\n"; - runCommand(array('systemctl', 'stop', $legacyTimer)); - - $deadline = time() + 120; - do - { - $active = runCommand(array('systemctl', 'is-active', '--quiet', 'piwigo-sync.service'), true); - if ($active['exit'] !== 0) break; - if (time() >= $deadline) fail('Ein laufender Legacy-Sync wurde nach 120 Sekunden nicht beendet.'); - sleep(2); - } - while (true); - - @unlink($statusPath); - echo "Fuehre ersten Connector-Lauf aus...\n"; - $firstRun = runCommand(array('env', 'PIWIGO_CONFIG='.$configPath, $legacyRuntime), true); - if ($firstRun['exit'] !== 0) - { - $detail = trim($firstRun['stderr']) !== '' ? trim($firstRun['stderr']) : trim($firstRun['stdout']); - fail('Erster Connector-Lauf ist technisch fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.')); - } - - $runResult = 'no_changes'; $statusState = ''; $statusMessage = ''; - if (is_readable($statusPath)) - { - $status = json_decode((string)file_get_contents($statusPath), true); - if (is_array($status)) - { - $statusState = trim((string)($status['state'] ?? '')); - $statusMessage = trim((string)($status['message'] ?? '')); - } - $legacyNoChangeMessage = 'Synchronisierung fehlgeschlagen; bestehende Galerie blieb unverändert'; - if ($statusState === 'error' && $statusMessage !== $legacyNoChangeMessage) fail('Erster Connector-Lauf meldete einen technischen Fehler'.($statusMessage !== '' ? ': '.$statusMessage : '.')); - if ($statusState === 'ok') $runResult = 'changed'; - elseif ($statusState === 'error' && $statusMessage === $legacyNoChangeMessage) - { - $runResult = 'no_changes'; - echo "Hinweis: Legacy-Runtime hat den erwarteten Activity-Gate-No-Op faelschlich als error markiert; Exit-Code 0 bestaetigt den erfolgreichen No-Change-Lauf.\n"; - } - } - - echo 'Erster Lauf Exit-Code: '.$firstRun['exit']."\n"; - echo 'Connector-Status: '.($statusState !== '' ? $statusState : 'kein Abschlussstatus').($statusMessage !== '' ? ' - '.$statusMessage : '')."\n"; - echo 'Bewertung: '.$runResult."\n"; - - $servicePath = '/etc/systemd/system/'.$newService; - $timerPath = '/etc/systemd/system/'.$newTimer; - $service = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=PIWIGO_CONFIG=".$configPath."\nExecStart=".$legacyRuntime."\n\n"; - $timer = "[Unit]\nDescription=Bratonien NC Connector regelmaessig pruefen\n\n[Timer]\nOnBootSec=3min\nOnUnitActiveSec=1min\nRandomizedDelaySec=15s\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n"; - file_put_contents($servicePath, $service, LOCK_EX); file_put_contents($timerPath, $timer, LOCK_EX); chmod($servicePath,0644); chmod($timerPath,0644); $newTimerInstalled=true; - - runCommand(array('systemctl','daemon-reload')); - runCommand(array('systemctl','disable',$legacyTimer),true); - runCommand(array('systemctl','enable','--now',$newTimer)); - - $config['takeover']['cutover_at']=date('Y-m-d H:i:s'); - $config['takeover']['legacy_timer_disabled']=true; - $config['takeover']['connector_timer']=$newTimer; - $config['takeover']['runtime']='legacy-runtime-transition'; - $config['takeover']['first_run']=array('state'=>'success','result'=>$runResult,'status_state'=>$statusState,'status_message'=>$statusMessage,'checked_at'=>date('Y-m-d H:i:s')); - saveTakeoverResult($db,$table,$connectionId,$config,'active',true); - - echo "Cutover erfolgreich.\n"; - echo "Erster Connector-Lauf: ".$runResult."\n"; - echo "Legacy-Timer ist deaktiviert; ".$newTimer." ist aktiv.\n"; - echo "Die bisherige Sync-Runtime bleibt fuer diesen Uebergangsschritt noch als Laufzeit erhalten.\n"; -} -catch (Throwable $e) -{ - if ($db instanceof mysqli && $table !== '' && $config) - { - try - { - $config['takeover']['first_run']=array('state'=>'error','result'=>'error','checked_at'=>date('Y-m-d H:i:s'),'message'=>substr($e->getMessage(),0,500)); - saveTakeoverResult($db,$table,$connectionId,$config,'ready',false); - } - catch (Throwable $ignored){} - } - if ($newTimerInstalled) runCommand(array('systemctl','disable','--now',$newTimer),true); - if ($legacyTimerWasEnabled) runCommand(array('systemctl','enable','--now',$legacyTimer),true); - else runCommand(array('systemctl','start',$legacyTimer),true); - fwrite(STDERR, "Cutover fehlgeschlagen: ".$e->getMessage()."\n"); - fwrite(STDERR, "Legacy-Timer wurde wieder aktiviert/gestartet.\n"); - exit(1); -} diff --git a/nc-connector-cutover.php b/nc-connector-cutover.php deleted file mode 100644 index 9c330b3..0000000 --- a/nc-connector-cutover.php +++ /dev/null @@ -1,374 +0,0 @@ -#!/usr/bin/env php -\n"); - exit(1); -} - -$connectionId = (int)$argv[1]; -$piwigoRoot = dirname(__DIR__, 2); -$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; -$legacyConfig = '/etc/piwigo-sync/piwigo.conf'; -$legacyRuntime = '/opt/piwigo-sync/sync.sh'; -$newTimer = 'bratonien-nc-connector.timer'; -$newService = 'bratonien-nc-connector.service'; -$legacyTimer = 'piwigo-sync.timer'; - -function fail($message) -{ - throw new RuntimeException($message); -} - -function readKeyValueFile($path) -{ - if (!is_readable($path)) - { - fail('Konfiguration nicht lesbar: '.$path); - } - $result = array(); - foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) - { - $line = trim($line); - if ($line === '' || $line[0] === '#') - { - continue; - } - if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches)) - { - continue; - } - $value = trim($matches[2]); - if (strlen($value) >= 2) - { - $first = $value[0]; - $last = $value[strlen($value)-1]; - if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) - { - $value = substr($value, 1, -1); - } - } - $result[$matches[1]] = $value; - } - return $result; -} - -function runCommand(array $command, $allowFailure = false) -{ - $spec = array( - 0 => array('file', '/dev/null', 'r'), - 1 => array('pipe', 'w'), - 2 => array('pipe', 'w'), - ); - $process = proc_open($command, $spec, $pipes); - if (!is_resource($process)) - { - fail('Prozess konnte nicht gestartet werden: '.implode(' ', $command)); - } - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[1]); - fclose($pipes[2]); - $exit = proc_close($process); - if ($exit !== 0 && !$allowFailure) - { - $detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout); - fail('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : '')); - } - return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr); -} - -function decryptConnectorSecret($blob, $hexKey) -{ - if (!preg_match('/^[a-f0-9]{64}$/', $hexKey)) - { - fail('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('Connector-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($hexKey), OPENSSL_RAW_DATA, $iv, $tag); - if ($plain === false || $plain === '') - { - fail('Connector-Zugangsdaten konnten nicht entschluesselt werden.'); - } - return (string)$plain; -} - -function sqlEscape(mysqli $db, $value) -{ - return $db->real_escape_string((string)$value); -} - -function saveTakeoverResult(mysqli $db, $table, $connectionId, array $config, $state, $enabled) -{ - $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($json)) - { - fail('Connector-Status konnte nicht serialisiert werden.'); - } - $now = date('Y-m-d H:i:s'); - $sql = "UPDATE `".$table."` SET takeover_state='".sqlEscape($db, $state)."', enabled=".($enabled ? 1 : 0).", config_json='".sqlEscape($db, $json)."', updated='".sqlEscape($db, $now)."' WHERE id=".(int)$connectionId; - if (!$db->query($sql)) - { - fail('Connector-Status konnte nicht gespeichert werden: '.$db->error); - } -} - -$legacyTimerWasEnabled = false; -$newTimerInstalled = false; -$db = null; -$table = ''; -$config = array(); - -try -{ - if (!is_readable($dbConfig)) - { - fail('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig); - } - if (!is_executable($legacyRuntime)) - { - fail('Bestehende Sync-Runtime fehlt: '.$legacyRuntime); - } - - $conf = array(); - $prefixeTable = 'piwigo_'; - require $dbConfig; - foreach (array('db_host','db_user','db_password','db_base') as $key) - { - if (!isset($conf[$key])) - { - fail('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.'); - } - } - - $db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); - if ($db->connect_errno) - { - fail('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error); - } - $db->set_charset('utf8mb4'); - - $table = $prefixeTable.'bratonien_tools_nc_connections'; - $result = $db->query("SELECT id, takeover_state, enabled, config_json, secret_blob FROM `".$table."` WHERE id=".$connectionId." LIMIT 1"); - if (!$result || !$result->num_rows) - { - fail('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.'); - } - $row = $result->fetch_assoc(); - if ((string)$row['takeover_state'] !== 'ready' || (int)$row['enabled'] !== 0) - { - fail('Connector-Verbindung muss im Zustand ready und deaktiviert sein.'); - } - $config = json_decode((string)$row['config_json'], true); - if (!is_array($config) || empty($config['verification']['ok'])) - { - fail('Connector-Verbindung besitzt keine erfolgreiche Verifikation.'); - } - - $keyResult = $db->query("SELECT value FROM `".$prefixeTable."config` WHERE param='bratonien_nc_connector_secret' LIMIT 1"); - if (!$keyResult || !$keyResult->num_rows) - { - fail('Connector-Schluessel wurde in Piwigo nicht gefunden.'); - } - $keyRow = $keyResult->fetch_assoc(); - $dbPassword = decryptConnectorSecret($row['secret_blob'], (string)$keyRow['value']); - - $legacy = readKeyValueFile($legacyConfig); - $piwigoUser = trim((string)($legacy['PIWIGO_SYNC_USER'] ?? '')); - $piwigoPasswordFile = trim((string)($legacy['PIWIGO_SYNC_PASSWORD_FILE'] ?? '/etc/piwigo-sync/piwigo-password')); - if ($piwigoUser === '' || !is_readable($piwigoPasswordFile)) - { - fail('Legacy-Piwigo-Sync-Zugangsdaten konnten fuer den einmaligen Cutover nicht gelesen werden.'); - } - $piwigoPassword = trim((string)file_get_contents($piwigoPasswordFile)); - if ($piwigoPassword === '') - { - fail('Legacy-Piwigo-Sync-Passwort ist leer.'); - } - - foreach (array('host','port','database','user','source_view','gallery_root','state_dir') as $key) - { - if (!isset($config[$key]) || trim((string)$config[$key]) === '') - { - fail('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.'); - } - } - - $runtimeDir = '/etc/bratonien-tools/nc-connector'; - if (!is_dir($runtimeDir) && !mkdir($runtimeDir, 0700, true)) - { - fail('Connector-Laufzeitverzeichnis konnte nicht angelegt werden.'); - } - chmod($runtimeDir, 0700); - - $base = $runtimeDir.'/connection-'.$connectionId; - $dbPasswordPath = $base.'.db-password'; - $piwigoPasswordPath = $base.'.piwigo-password'; - $storagePath = $base.'.storages.tsv'; - $configPath = $base.'.conf'; - $statusPath = rtrim((string)$config['state_dir'], '/').'/connector-status.json'; - - file_put_contents($dbPasswordPath, $dbPassword."\n", LOCK_EX); - file_put_contents($piwigoPasswordPath, $piwigoPassword."\n", LOCK_EX); - chmod($dbPasswordPath, 0600); - chmod($piwigoPasswordPath, 0600); - - $storageLines = array('# storage_idsource_prefixlocal_mount'); - foreach ((array)($config['storages'] ?? array()) as $storage) - { - $storageLines[] = (string)($storage['storage_id'] ?? '')."\t".(string)($storage['source_prefix'] ?? '')."\t".(string)($storage['local_mount'] ?? ''); - } - file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); - chmod($storagePath, 0600); - - $piwigoRootConfigured = isset($legacy['PIWIGO_ROOT']) && trim((string)$legacy['PIWIGO_ROOT']) !== '' ? trim((string)$legacy['PIWIGO_ROOT']) : $piwigoRoot; - $lines = array( - 'PIWIGO_ROOT='.$piwigoRootConfigured, - 'GALLERY_ROOT='.(string)$config['gallery_root'], - 'STATE_DIR='.(string)$config['state_dir'], - 'STATUS_FILE='.$statusPath, - 'NC_DB_HOST='.(string)$config['host'], - 'NC_DB_PORT='.(string)$config['port'], - 'NC_DB_NAME='.(string)$config['database'], - 'NC_DB_USER='.(string)$config['user'], - 'NC_DB_VIEW='.(string)$config['source_view'], - 'NC_DB_PASSWORD_FILE='.$dbPasswordPath, - 'STORAGE_CONFIG='.$storagePath, - 'QUIET_SECONDS='.(int)($config['quiet_seconds'] ?? 120), - 'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900), - 'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400), - 'PIWIGO_SYNC_ENABLED=1', - 'PIWIGO_SYNC_USER='.$piwigoUser, - 'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath, - ); - file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); - chmod($configPath, 0600); - - $enabledCheck = runCommand(array('systemctl', 'is-enabled', $legacyTimer), true); - $legacyTimerWasEnabled = $enabledCheck['exit'] === 0; - - echo "Stoppe Legacy-Timer...\n"; - runCommand(array('systemctl', 'stop', $legacyTimer)); - - $deadline = time() + 120; - do - { - $active = runCommand(array('systemctl', 'is-active', '--quiet', 'piwigo-sync.service'), true); - if ($active['exit'] !== 0) - { - break; - } - if (time() >= $deadline) - { - fail('Ein laufender Legacy-Sync wurde nach 120 Sekunden nicht beendet.'); - } - sleep(2); - } - while (true); - - @unlink($statusPath); - echo "Fuehre ersten Connector-Lauf aus...\n"; - $firstRun = runCommand(array('env', 'PIWIGO_CONFIG='.$configPath, $legacyRuntime), true); - if ($firstRun['exit'] !== 0) - { - $detail = trim($firstRun['stderr']) !== '' ? trim($firstRun['stderr']) : trim($firstRun['stdout']); - fail('Erster Connector-Lauf ist technisch fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.')); - } - - $runResult = 'no_changes'; - if (is_readable($statusPath)) - { - $status = json_decode((string)file_get_contents($statusPath), true); - if (!is_array($status) || (string)($status['state'] ?? '') !== 'ok') - { - fail('Erster Connector-Lauf lieferte keinen gueltigen Erfolgsstatus.'); - } - $runResult = 'changed'; - } - - $servicePath = '/etc/systemd/system/'.$newService; - $timerPath = '/etc/systemd/system/'.$newTimer; - $service = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=PIWIGO_CONFIG=".$configPath."\nExecStart=".$legacyRuntime."\n\n"; - $timer = "[Unit]\nDescription=Bratonien NC Connector regelmaessig pruefen\n\n[Timer]\nOnBootSec=3min\nOnUnitActiveSec=1min\nRandomizedDelaySec=15s\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n"; - file_put_contents($servicePath, $service, LOCK_EX); - file_put_contents($timerPath, $timer, LOCK_EX); - chmod($servicePath, 0644); - chmod($timerPath, 0644); - $newTimerInstalled = true; - - runCommand(array('systemctl', 'daemon-reload')); - runCommand(array('systemctl', 'disable', $legacyTimer), true); - runCommand(array('systemctl', 'enable', '--now', $newTimer)); - - $config['takeover']['cutover_at'] = date('Y-m-d H:i:s'); - $config['takeover']['legacy_timer_disabled'] = true; - $config['takeover']['connector_timer'] = $newTimer; - $config['takeover']['runtime'] = 'legacy-runtime-transition'; - $config['takeover']['first_run'] = array( - 'state' => 'success', - 'result' => $runResult, - 'checked_at' => date('Y-m-d H:i:s'), - ); - saveTakeoverResult($db, $table, $connectionId, $config, 'active', true); - - echo "Cutover erfolgreich.\n"; - echo "Erster Connector-Lauf: ".$runResult."\n"; - echo "Legacy-Timer ist deaktiviert; ".$newTimer." ist aktiv.\n"; - echo "Die bisherige Sync-Runtime bleibt fuer diesen Uebergangsschritt noch als Laufzeit erhalten.\n"; -} -catch (Throwable $e) -{ - if ($db instanceof mysqli && $table !== '' && $config) - { - try - { - $config['takeover']['first_run'] = array( - 'state' => 'error', - 'result' => 'error', - 'checked_at' => date('Y-m-d H:i:s'), - 'message' => substr($e->getMessage(), 0, 500), - ); - saveTakeoverResult($db, $table, $connectionId, $config, 'ready', false); - } - catch (Throwable $ignored) - { - } - } - - if ($newTimerInstalled) - { - runCommand(array('systemctl', 'disable', '--now', $newTimer), true); - } - if ($legacyTimerWasEnabled) - { - runCommand(array('systemctl', 'enable', '--now', $legacyTimer), true); - } - else - { - runCommand(array('systemctl', 'start', $legacyTimer), true); - } - - fwrite(STDERR, "Cutover fehlgeschlagen: ".$e->getMessage()."\n"); - fwrite(STDERR, "Legacy-Timer wurde wieder aktiviert/gestartet.\n"); - exit(1); -} diff --git a/nc-connector-diagnose.php b/nc-connector-diagnose.php deleted file mode 100644 index f3a6751..0000000 --- a/nc-connector-diagnose.php +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env php -\n"); - exit(1); -} - -$connectionId = (int)$argv[1]; -$configPath = '/etc/bratonien-tools/nc-connector/connection-'.$connectionId.'.conf'; -$runtime = '/opt/piwigo-sync/sync.sh'; -$legacyTimer = 'piwigo-sync.timer'; -$legacyService = 'piwigo-sync.service'; - -function runCommand(array $command, $allowFailure = false) -{ - $spec = array( - 0 => array('file', '/dev/null', 'r'), - 1 => array('pipe', 'w'), - 2 => array('pipe', 'w'), - ); - $process = proc_open($command, $spec, $pipes); - if (!is_resource($process)) - { - throw new RuntimeException('Prozess konnte nicht gestartet werden: '.implode(' ', $command)); - } - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[1]); - fclose($pipes[2]); - $exit = proc_close($process); - if ($exit !== 0 && !$allowFailure) - { - throw new RuntimeException('Befehl fehlgeschlagen: '.implode(' ', $command)); - } - return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr); -} - -$timerWasEnabled = false; - -try -{ - if (!is_readable($configPath)) - { - throw new RuntimeException('Connector-Konfiguration fehlt oder ist nicht lesbar: '.$configPath.'. Fuehre zuerst den Cutover-Versuch mindestens einmal aus, damit die Laufzeitkonfiguration erzeugt wird.'); - } - if (!is_executable($runtime)) - { - throw new RuntimeException('Sync-Runtime fehlt: '.$runtime); - } - - $enabled = runCommand(array('systemctl', 'is-enabled', $legacyTimer), true); - $timerWasEnabled = $enabled['exit'] === 0; - - echo "Stoppe Legacy-Timer fuer die Diagnose...\n"; - runCommand(array('systemctl', 'stop', $legacyTimer), true); - - $deadline = time() + 120; - while (true) - { - $active = runCommand(array('systemctl', 'is-active', '--quiet', $legacyService), true); - if ($active['exit'] !== 0) - { - break; - } - if (time() >= $deadline) - { - throw new RuntimeException('Ein laufender Legacy-Sync wurde nach 120 Sekunden nicht beendet.'); - } - sleep(2); - } - - echo "Fuehre Connector-Lauf im Diagnosemodus aus...\n\n"; - $result = runCommand(array('env', 'PIWIGO_CONFIG='.$configPath, 'bash', '-x', $runtime), true); - - echo "===== STDOUT =====\n"; - echo trim($result['stdout'])."\n"; - echo "===== STDERR / TRACE =====\n"; - echo trim($result['stderr'])."\n"; - echo "===== ENDE =====\n"; - echo 'Exit-Code: '.$result['exit']."\n"; -} -catch (Throwable $e) -{ - fwrite(STDERR, 'Diagnose fehlgeschlagen: '.$e->getMessage()."\n"); -} -finally -{ - if ($timerWasEnabled) - { - runCommand(array('systemctl', 'enable', '--now', $legacyTimer), true); - } - else - { - runCommand(array('systemctl', 'start', $legacyTimer), true); - } - echo "Legacy-Timer wurde nach der Diagnose wieder aktiviert/gestartet.\n"; -} diff --git a/nc-connector-disable.php b/nc-connector-disable.php deleted file mode 100644 index 2ed1d33..0000000 --- a/nc-connector-disable.php +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env php -\n"); - exit(1); -} - -$id = (int)$argv[1]; -$piwigoRoot = dirname(__DIR__, 2); -$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; -$configDir = '/etc/bratonien-tools/nc-connector'; - -try -{ - $conf = array(); - $prefixeTable = 'piwigo_'; - if (!is_readable($dbConfig)) throw new RuntimeException('Piwigo-Datenbankkonfiguration nicht lesbar.'); - require $dbConfig; - $db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); - if ($db->connect_errno) throw new RuntimeException('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error); - $db->set_charset('utf8mb4'); - $table = $prefixeTable.'bratonien_tools_nc_connections'; - $result = $db->query("SELECT id, enabled, takeover_state, config_json FROM `".$table."` WHERE id=".$id." LIMIT 1"); - if (!$result || !$result->num_rows) throw new RuntimeException('Connector-Verbindung #'.$id.' wurde nicht gefunden.'); - $row = $result->fetch_assoc(); - if ((int)$row['enabled'] !== 1 || (string)$row['takeover_state'] !== 'active') - { - throw new RuntimeException('Die Verbindung ist nicht aktiv.'); - } - - passthru('systemctl stop bratonien-nc-connector.timer', $stopExit); - if ($stopExit !== 0) throw new RuntimeException('Connector-Timer konnte nicht gestoppt werden.'); - - $base = $configDir.'/connection-'.$id; - foreach (array('.conf','.storages.tsv','.db-password','.piwigo-password') as $suffix) - { - $path = $base.$suffix; - if (is_file($path) && !unlink($path)) throw new RuntimeException('Datei konnte nicht entfernt werden: '.$path); - } - - $config = json_decode((string)$row['config_json'], true); - if (!is_array($config)) $config = array(); - unset($config['runtime']); - $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $now = date('Y-m-d H:i:s'); - $jsonEsc = $db->real_escape_string((string)$json); - $nowEsc = $db->real_escape_string($now); - if (!$db->query("UPDATE `".$table."` SET enabled=0, takeover_state='disabled', config_json='".$jsonEsc."', updated='".$nowEsc."' WHERE id=".$id)) - { - throw new RuntimeException('Connector-Status konnte nicht gespeichert werden: '.$db->error); - } - - $remaining = glob($configDir.'/connection-*.conf') ?: array(); - if ($remaining) - { - passthru('systemctl start bratonien-nc-connector.timer', $startExit); - if ($startExit !== 0) throw new RuntimeException('Connector-Timer konnte nicht wieder gestartet werden.'); - echo "Verbindung #".$id." deaktiviert. Andere Connector-Verbindungen bleiben aktiv.\n"; - } - else - { - passthru('systemctl disable bratonien-nc-connector.timer', $disableExit); - echo "Verbindung #".$id." deaktiviert. Es gibt keine weitere aktive Connector-Verbindung; der Timer bleibt gestoppt.\n"; - } -} -catch (Throwable $e) -{ - fwrite(STDERR, $e->getMessage()."\n"); - exit(1); -} diff --git a/nc-connector-install.php b/nc-connector-install.php deleted file mode 100644 index e2ae3f6..0000000 --- a/nc-connector-install.php +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env php -\n"); - exit(1); -} - -$id = (int)$argv[1]; -$pluginRoot = __DIR__; -$piwigoRoot = dirname(__DIR__, 2); -$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; -$configDir = '/etc/bratonien-tools/nc-connector'; -$stateRoot = '/var/lib/bratonien-tools/nc-connector'; -$servicePath = '/etc/systemd/system/bratonien-nc-connector.service'; -$timerPath = '/etc/systemd/system/bratonien-nc-connector.timer'; - -function fail_install($message) { throw new RuntimeException($message); } - -function run_install(array $command, $allowFailure = false) -{ - $spec = array(0=>array('file','/dev/null','r'),1=>array('pipe','w'),2=>array('pipe','w')); - $process = proc_open($command, $spec, $pipes); - if (!is_resource($process)) fail_install('Prozess konnte nicht gestartet werden: '.implode(' ', $command)); - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[1]); fclose($pipes[2]); - $exit = proc_close($process); - if ($exit !== 0 && !$allowFailure) - { - $detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout); - fail_install('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : '')); - } - return array('exit'=>$exit,'stdout'=>(string)$stdout,'stderr'=>(string)$stderr); -} - -function decrypt_install_credentials($blob, $hexKey) -{ - if (!preg_match('/^[a-f0-9]{64}$/', $hexKey)) fail_install('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_install('Connector-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($hexKey), OPENSSL_RAW_DATA, $iv, $tag); - if ($plain === false || $plain === '') fail_install('Connector-Zugangsdaten konnten nicht entschluesselt werden.'); - $decoded = json_decode($plain, true); - if (!is_array($decoded)) - { - return array('db_password'=>$plain,'piwigo_user'=>'','piwigo_password'=>''); - } - if (empty($decoded['db_password'])) fail_install('Datenbankpasswort fehlt in den Connector-Zugangsdaten.'); - return array( - 'db_password'=>(string)$decoded['db_password'], - 'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''), - 'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''), - ); -} - -function sql_install(mysqli $db, $value) { return $db->real_escape_string((string)$value); } - -try -{ - if (!is_readable($dbConfig)) fail_install('Piwigo-Datenbankkonfiguration nicht lesbar: '.$dbConfig); - $conf = array(); $prefixeTable = 'piwigo_'; require $dbConfig; - foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail_install('Piwigo-Datenbankkonfiguration enthaelt '.$key.' nicht.'); - - $db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); - if ($db->connect_errno) fail_install('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error); - $db->set_charset('utf8mb4'); - - $table = $prefixeTable.'bratonien_tools_nc_connections'; - $result = $db->query("SELECT id, name, adapter, enabled, takeover_state, config_json, secret_blob FROM `".$table."` WHERE id=".$id." LIMIT 1"); - if (!$result || !$result->num_rows) fail_install('Connector-Verbindung #'.$id.' wurde nicht gefunden.'); - $row = $result->fetch_assoc(); - if ((string)$row['adapter'] !== 'local') fail_install('Native Aktivierung ist derzeit nur fuer lokale Verbindungen verfuegbar.'); - if ((string)$row['takeover_state'] !== 'verified' || (int)$row['enabled'] !== 0) fail_install('Die Verbindung muss zuerst erfolgreich verifiziert und deaktiviert sein.'); - - $config = json_decode((string)$row['config_json'], true); - if (!is_array($config) || empty($config['verification']['ok'])) fail_install('Erfolgreiche Verifikation fehlt.'); - foreach (array('host','port','database','user','source_view','activity_view','gallery_root') as $key) if (trim((string)($config[$key] ?? '')) === '') fail_install('Connector-Konfiguration ist unvollstaendig: '.$key.' fehlt.'); - - $keyResult = $db->query("SELECT value FROM `".$prefixeTable."config` WHERE param='bratonien_nc_connector_secret' LIMIT 1"); - if (!$keyResult || !$keyResult->num_rows) fail_install('Connector-Schluessel wurde in Piwigo nicht gefunden.'); - $credentials = decrypt_install_credentials((string)$row['secret_blob'], (string)$keyResult->fetch_assoc()['value']); - - $apiAvailable = false; - $apiResult = $db->query("SELECT value FROM `".$prefixeTable."config` WHERE param='bratonien_nc_piwigo_api' LIMIT 1"); - if ($apiResult && $apiResult->num_rows) $apiAvailable = trim((string)$apiResult->fetch_assoc()['value']) !== ''; - $fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== ''; - if (!$apiAvailable && !$fallbackAvailable) fail_install('Weder Piwigo-API noch Fallback-Zugang sind gespeichert.'); - - $stateDir = $stateRoot.'/connection-'.$id; - $statusFile = $stateDir.'/connector-status.json'; - if (!is_dir($configDir) && !mkdir($configDir, 0700, true)) fail_install('Konfigurationsverzeichnis konnte nicht angelegt werden.'); - if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_install('State-Verzeichnis konnte nicht angelegt werden.'); - chmod($configDir, 0700); chmod($stateDir, 0750); - - $base = $configDir.'/connection-'.$id; - $dbPasswordPath = $base.'.db-password'; - $piwigoPasswordPath = $base.'.piwigo-password'; - $storagePath = $base.'.storages.tsv'; - $configPath = $base.'.conf'; - - file_put_contents($dbPasswordPath, (string)$credentials['db_password']."\n", LOCK_EX); chmod($dbPasswordPath, 0600); - if ($fallbackAvailable) - { - file_put_contents($piwigoPasswordPath, (string)$credentials['piwigo_password']."\n", LOCK_EX); chmod($piwigoPasswordPath, 0600); - } - else - { - @unlink($piwigoPasswordPath); - } - - $storageLines = array('# storage_idsource_prefixlocal_mountinclude_prefix'); - foreach ((array)($config['storages'] ?? array()) as $storage) - { - $storageLines[] = (string)($storage['storage_id'] ?? '')."\t" - .trim((string)($storage['source_prefix'] ?? ''), '/')."\t" - .(string)($storage['local_mount'] ?? '')."\t" - .trim((string)($storage['include_prefix'] ?? ''), '/'); - } - if (count($storageLines) === 1) fail_install('Keine Storage-Zuordnungen gespeichert.'); - file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); chmod($storagePath, 0600); - - $lines = array( - 'PIWIGO_ROOT='.$piwigoRoot, - 'GALLERY_ROOT='.(string)$config['gallery_root'], - 'STATE_DIR='.$stateDir, - 'STATUS_FILE='.$statusFile, - 'NC_DB_HOST='.(string)$config['host'], - 'NC_DB_PORT='.(string)$config['port'], - 'NC_DB_NAME='.(string)$config['database'], - 'NC_DB_USER='.(string)$config['user'], - 'NC_DB_VIEW='.(string)$config['source_view'], - 'NC_ACTIVITY_VIEW='.(string)$config['activity_view'], - 'NC_DB_PASSWORD_FILE='.$dbPasswordPath, - 'STORAGE_CONFIG='.$storagePath, - 'QUIET_SECONDS='.(int)($config['quiet_seconds'] ?? 120), - 'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900), - 'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400), - 'PIWIGO_SYNC_ENABLED=1', - ); - if ($fallbackAvailable) - { - $lines[] = 'PIWIGO_SYNC_USER='.(string)$credentials['piwigo_user']; - $lines[] = 'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath; - } - file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); chmod($configPath, 0600); - - echo "Teste Verbindung mit Plugin-Runtime...\n"; - $test = run_install(array('env', 'PIWIGO_CONFIG='.$configPath, 'bash', $pluginRoot.'/runtime/sync.sh'), true); - if ($test['exit'] !== 0) - { - @unlink($configPath); @unlink($storagePath); @unlink($dbPasswordPath); @unlink($piwigoPasswordPath); - $detail = trim($test['stderr']) !== '' ? trim($test['stderr']) : trim($test['stdout']); - fail_install('Runtime-Test fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.')); - } - - $service = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nExecStart=/usr/bin/env bash ".$pluginRoot."/runtime/run-all.sh\n\n"; - $timer = "[Unit]\nDescription=Bratonien NC Connector regelmaessig pruefen\n\n[Timer]\nOnBootSec=3min\nOnUnitActiveSec=1min\nRandomizedDelaySec=15s\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n"; - file_put_contents($servicePath, $service, LOCK_EX); file_put_contents($timerPath, $timer, LOCK_EX); chmod($servicePath,0644); chmod($timerPath,0644); - - $config['state_dir']=$stateDir;$config['status_file']=$statusFile;$config['runtime']=array('mode'=>'plugin-runtime','config'=>$configPath); - $json=json_encode($config,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);$now=date('Y-m-d H:i:s'); - if (!$db->query("UPDATE `".$table."` SET enabled=1, takeover_state='active', config_json='".sql_install($db,$json)."', updated='".sql_install($db,$now)."' WHERE id=".$id)) fail_install('Connector-Status konnte nicht gespeichert werden: '.$db->error); - - run_install(array('systemctl','daemon-reload')); run_install(array('systemctl','enable','--now','bratonien-nc-connector.timer')); - echo "Aktivierung erfolgreich.\n"; - echo "Verbindung #".$id." verwendet jetzt den nativen Bratonien-Tools-State unter ".$stateDir.".\n"; - echo "Der gemeinsame Connector-Timer verarbeitet alle installierten connection-*.conf Dateien.\n"; -} -catch (Throwable $e) -{ - fwrite(STDERR, $e->getMessage()."\n"); - exit(1); -} diff --git a/nc-connector-legacy-cleanup.php b/nc-connector-legacy-cleanup.php deleted file mode 100644 index 49180f3..0000000 --- a/nc-connector-legacy-cleanup.php +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env php -\n"); - exit(1); -} - -$connectionId = (int)$argv[1]; -$piwigoRoot = dirname(__DIR__, 2); -$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; -$pluginRuntime = __DIR__.'/runtime/sync.sh'; -$configPath = '/etc/bratonien-tools/nc-connector/connection-'.$connectionId.'.conf'; -$connectorService = '/etc/systemd/system/bratonien-nc-connector.service'; -$connectorTimer = 'bratonien-nc-connector.timer'; -$legacyServiceName = 'piwigo-sync.service'; -$legacyTimerName = 'piwigo-sync.timer'; -$legacyServicePath = '/etc/systemd/system/'.$legacyServiceName; -$legacyTimerPath = '/etc/systemd/system/'.$legacyTimerName; - -function cleanupFail($message) -{ - throw new RuntimeException($message); -} - -function cleanupRun(array $command, $allowFailure = false) -{ - $spec = array( - 0 => array('file', '/dev/null', 'r'), - 1 => array('pipe', 'w'), - 2 => array('pipe', 'w'), - ); - $process = proc_open($command, $spec, $pipes); - if (!is_resource($process)) - { - cleanupFail('Prozess konnte nicht gestartet werden: '.implode(' ', $command)); - } - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[1]); - fclose($pipes[2]); - $exit = proc_close($process); - if ($exit !== 0 && !$allowFailure) - { - $detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout); - cleanupFail('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : '')); - } - return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr); -} - -function cleanupRemoveTree($path) -{ - if (!file_exists($path) && !is_link($path)) - { - return; - } - if (is_link($path) || is_file($path)) - { - if (!@unlink($path)) - { - cleanupFail('Datei konnte nicht entfernt werden: '.$path); - } - return; - } - $items = scandir($path); - if (!is_array($items)) - { - cleanupFail('Verzeichnis konnte nicht gelesen werden: '.$path); - } - foreach ($items as $item) - { - if ($item === '.' || $item === '..') - { - continue; - } - cleanupRemoveTree($path.'/'.$item); - } - if (!@rmdir($path)) - { - cleanupFail('Verzeichnis konnte nicht entfernt werden: '.$path); - } -} - -try -{ - foreach (array($dbConfig, $configPath, $connectorService, $pluginRuntime) as $required) - { - if (!is_readable($required)) - { - cleanupFail('Erforderliche Connector-Datei fehlt oder ist nicht lesbar: '.$required); - } - } - - $serviceDefinition = (string)file_get_contents($connectorService); - if (strpos($serviceDefinition, $pluginRuntime) === false) - { - cleanupFail('Der aktive Connector-Service verwendet noch nicht die Plugin-eigene Runtime. Legacy-Bestand wird nicht entfernt.'); - } - - $conf = array(); - $prefixeTable = 'piwigo_'; - require $dbConfig; - $db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); - if ($db->connect_errno) - { - cleanupFail('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error); - } - $db->set_charset('utf8mb4'); - $table = $prefixeTable.'bratonien_tools_nc_connections'; - $result = $db->query("SELECT takeover_state, enabled, config_json FROM `".$table."` WHERE id=".$connectionId." LIMIT 1"); - if (!$result || !$result->num_rows) - { - cleanupFail('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.'); - } - $row = $result->fetch_assoc(); - $config = json_decode((string)$row['config_json'], true); - if ((string)$row['takeover_state'] !== 'active' || (int)$row['enabled'] !== 1 || !is_array($config)) - { - cleanupFail('Legacy-Cleanup ist nur fuer eine aktive Connector-Verbindung erlaubt.'); - } - if (($config['takeover']['runtime'] ?? '') !== 'plugin-runtime') - { - cleanupFail('Connector-Status bestaetigt die Plugin-Runtime noch nicht. Legacy-Bestand wird nicht entfernt.'); - } - - $active = cleanupRun(array('systemctl', 'is-active', $connectorTimer), true); - if ($active['exit'] !== 0) - { - cleanupFail('Der Connector-Timer ist nicht aktiv. Vor dem Cleanup muss der produktive Connector laufen.'); - } - - echo "Deaktiviere verbliebene Legacy-Units...\n"; - cleanupRun(array('systemctl', 'disable', '--now', $legacyTimerName), true); - cleanupRun(array('systemctl', 'stop', $legacyServiceName), true); - - echo "Entferne alte systemd-Units...\n"; - foreach (array($legacyTimerPath, $legacyServicePath) as $path) - { - if (is_file($path) || is_link($path)) - { - @unlink($path); - } - } - - echo "Entferne /opt/piwigo-sync...\n"; - cleanupRemoveTree('/opt/piwigo-sync'); - - echo "Entferne /etc/piwigo-sync...\n"; - cleanupRemoveTree('/etc/piwigo-sync'); - - cleanupRun(array('systemctl', 'daemon-reload')); - cleanupRun(array('systemctl', 'reset-failed', $legacyServiceName), true); - - $config['takeover']['legacy_cleaned_at'] = date('Y-m-d H:i:s'); - $config['takeover']['legacy_cleanup'] = true; - $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (is_string($json)) - { - $now = date('Y-m-d H:i:s'); - $escapedJson = $db->real_escape_string($json); - $escapedNow = $db->real_escape_string($now); - $db->query("UPDATE `".$table."` SET config_json='".$escapedJson."', updated='".$escapedNow."' WHERE id=".$connectionId); - } - - echo "Legacy-Cleanup erfolgreich.\n"; - echo "Entfernt: /opt/piwigo-sync, /etc/piwigo-sync sowie piwigo-sync.service/timer.\n"; - echo "/var/lib/piwigo-sync bleibt bestehen, weil dort der aktive Connector seinen Laufzeitstatus, Name-Map und Activity-State fuehrt.\n"; -} -catch (Throwable $e) -{ - fwrite(STDERR, "Legacy-Cleanup abgebrochen: ".$e->getMessage()."\n"); - exit(1); -} diff --git a/nc-connector-migrate.php b/nc-connector-migrate.php deleted file mode 100644 index 83aaf52..0000000 --- a/nc-connector-migrate.php +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env php -= 2) - { - $first = $value[0]; - $last = $value[strlen($value)-1]; - if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) - { - $value = substr($value, 1, -1); - } - } - $config[$matches[1]] = $value; - } - - return $config; -} - -function readStorages($path) -{ - $storages = array(); - if (!is_readable($path)) - { - return $storages; - } - - foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) - { - if ($line === '' || $line[0] === '#') - { - continue; - } - $parts = explode("\t", $line); - if (count($parts) !== 3) - { - continue; - } - $storages[] = array( - 'storage_id' => $parts[0], - 'source_prefix' => $parts[1], - 'local_mount' => $parts[2], - ); - } - - return $storages; -} - -try -{ - $config = readLegacyConfig($configPath); - $passwordPath = isset($config['NC_DB_PASSWORD_FILE']) ? $config['NC_DB_PASSWORD_FILE'] : '/etc/piwigo-sync/nextcloud-db-password'; - if (!is_readable($passwordPath)) - { - throw new RuntimeException('Nextcloud-Datenbankpasswort nicht lesbar: '.$passwordPath); - } - - $password = trim((string)file_get_contents($passwordPath)); - if ($password === '') - { - throw new RuntimeException('Nextcloud-Datenbankpasswort ist leer.'); - } - - $bundle = array( - 'format' => 1, - 'created_at' => gmdate('c'), - 'config' => $config, - 'db_password' => $password, - 'storages' => readStorages($storagePath), - ); - - $json = json_encode($bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($json)) - { - throw new RuntimeException('Migrationspaket konnte nicht erzeugt werden.'); - } - - if (file_put_contents($bundlePath, $json."\n", LOCK_EX) === false) - { - throw new RuntimeException('Migrationspaket konnte nicht geschrieben werden: '.$bundlePath); - } - - @chmod($bundlePath, 0600); - if (!@chown($bundlePath, 'www-data')) - { - @unlink($bundlePath); - throw new RuntimeException('Migrationspaket konnte nicht sicher an www-data uebergeben werden.'); - } - @chgrp($bundlePath, 'www-data'); - - echo "Migrationspaket wurde bereitgestellt.\n"; - echo "Jetzt in Piwigo -> Bratonien Tools -> NC Connector wechseln und 'Bestehende Verbindung importieren' ausfuehren.\n"; - echo "Der laufende piwigo-sync wurde nicht veraendert oder gestoppt.\n"; -} -catch (Throwable $e) -{ - fwrite(STDERR, 'Fehler: '.$e->getMessage()."\n"); - exit(1); -} diff --git a/nc-connector-normalize.php b/nc-connector-normalize.php deleted file mode 100644 index 1391d9a..0000000 --- a/nc-connector-normalize.php +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env php -\n"); - exit(1); -} - -$id = (int)$argv[1]; -$pluginRoot = __DIR__; -$piwigoRoot = dirname(__DIR__, 2); -$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; -$configPath = '/etc/bratonien-tools/nc-connector/connection-'.$id.'.conf'; -$newState = '/var/lib/bratonien-tools/nc-connector/connection-'.$id; -$servicePath = '/etc/systemd/system/bratonien-nc-connector.service'; -$timer = 'bratonien-nc-connector.timer'; -$service = 'bratonien-nc-connector.service'; - -function normalize_run(array $command, $allowFailure = false) -{ - $spec = array(0=>array('file','/dev/null','r'), 1=>array('pipe','w'), 2=>array('pipe','w')); - $process = proc_open($command, $spec, $pipes); - if (!is_resource($process)) throw new RuntimeException('Prozess konnte nicht gestartet werden.'); - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[1]); fclose($pipes[2]); - $exit = proc_close($process); - if ($exit !== 0 && !$allowFailure) - { - $detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout); - throw new RuntimeException('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : '')); - } - return array('exit'=>$exit, 'stdout'=>$stdout, 'stderr'=>$stderr); -} - -function remove_tree($path) -{ - if (!is_dir($path)) return; - $items = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($items as $item) - { - if ($item->isDir()) @rmdir($item->getPathname()); else @unlink($item->getPathname()); - } - @rmdir($path); -} - -try -{ - $conf = array(); - $prefixeTable = 'piwigo_'; - if (!is_readable($dbConfig)) throw new RuntimeException('Piwigo-Datenbankkonfiguration nicht lesbar.'); - require $dbConfig; - $db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); - if ($db->connect_errno) throw new RuntimeException('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error); - $db->set_charset('utf8mb4'); - $table = $prefixeTable.'bratonien_tools_nc_connections'; - $result = $db->query("SELECT id, enabled, takeover_state, config_json FROM `".$table."` WHERE id=".$id." LIMIT 1"); - if (!$result || !$result->num_rows) throw new RuntimeException('Connector-Verbindung #'.$id.' wurde nicht gefunden.'); - $row = $result->fetch_assoc(); - if ((int)$row['enabled'] !== 1 || (string)$row['takeover_state'] !== 'active') throw new RuntimeException('Die Verbindung muss aktiv sein.'); - $config = json_decode((string)$row['config_json'], true); - if (!is_array($config)) throw new RuntimeException('Connector-Konfiguration ist ungueltig.'); - $oldState = rtrim((string)($config['state_dir'] ?? ''), '/'); - if ($oldState === '') throw new RuntimeException('Bisheriger State-Pfad fehlt.'); - if ($oldState === $newState) - { - echo "Verbindung #".$id." verwendet bereits den nativen State-Pfad.\n"; - exit(0); - } - if (!is_readable($configPath)) throw new RuntimeException('Aktive Runtime-Konfiguration fehlt: '.$configPath); - - echo "Stoppe Connector-Timer...\n"; - normalize_run(array('systemctl','stop',$timer)); - $deadline = time() + 120; - while (normalize_run(array('systemctl','is-active','--quiet',$service), true)['exit'] === 0) - { - if (time() >= $deadline) throw new RuntimeException('Laufender Connector-Lauf wurde nach 120 Sekunden nicht beendet.'); - sleep(2); - } - - if (!is_dir(dirname($newState)) && !mkdir(dirname($newState), 0750, true)) throw new RuntimeException('Neues State-Wurzelverzeichnis konnte nicht angelegt werden.'); - if (!is_dir($newState) && !mkdir($newState, 0750, true)) throw new RuntimeException('Neues State-Verzeichnis konnte nicht angelegt werden.'); - if (is_dir($oldState)) normalize_run(array('cp','-a',$oldState.'/.',$newState.'/')); - chmod($newState, 0750); - - $originalConfig = file_get_contents($configPath); - if (!is_string($originalConfig)) throw new RuntimeException('Runtime-Konfiguration konnte nicht gelesen werden.'); - $newStatus = $newState.'/connector-status.json'; - $updatedConfig = preg_replace('/^STATE_DIR=.*$/m', 'STATE_DIR='.$newState, $originalConfig, 1); - $updatedConfig = preg_replace('/^STATUS_FILE=.*$/m', 'STATUS_FILE='.$newStatus, $updatedConfig, 1); - if (!is_string($updatedConfig) || $updatedConfig === $originalConfig) throw new RuntimeException('Runtime-Konfiguration konnte nicht auf nativen State umgestellt werden.'); - file_put_contents($configPath, $updatedConfig, LOCK_EX); - chmod($configPath, 0600); - - echo "Teste Plugin-Runtime mit neuem State...\n"; - $test = normalize_run(array('env','PIWIGO_CONFIG='.$configPath,'bash',$pluginRoot.'/runtime/sync.sh'), true); - if ($test['exit'] !== 0) - { - file_put_contents($configPath, $originalConfig, LOCK_EX); - remove_tree($newState); - normalize_run(array('systemctl','start',$timer), true); - $detail = trim($test['stderr']) !== '' ? trim($test['stderr']) : trim($test['stdout']); - throw new RuntimeException('Normalisierung abgebrochen; alter Zustand wiederhergestellt'.($detail !== '' ? ': '.$detail : '.')); - } - - $serviceContent = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nExecStart=/usr/bin/env bash ".$pluginRoot."/runtime/run-all.sh\n\n"; - file_put_contents($servicePath, $serviceContent, LOCK_EX); - chmod($servicePath, 0644); - - $config['state_dir'] = $newState; - $config['status_file'] = $newStatus; - $config['origin'] = 'native'; - $config['runtime'] = array('mode'=>'plugin-runtime', 'config'=>$configPath); - if (isset($config['takeover']) && is_array($config['takeover'])) $config['takeover']['runtime'] = 'plugin-runtime'; - $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $jsonEsc = $db->real_escape_string((string)$json); - $nowEsc = $db->real_escape_string(date('Y-m-d H:i:s')); - if (!$db->query("UPDATE `".$table."` SET config_json='".$jsonEsc."', updated='".$nowEsc."' WHERE id=".$id)) throw new RuntimeException('Connector-Datenbankstatus konnte nicht aktualisiert werden: '.$db->error); - - normalize_run(array('systemctl','daemon-reload')); - normalize_run(array('systemctl','enable','--now',$timer)); - - if ($oldState !== '/var/lib' && $oldState !== '/' && strpos($oldState, '/var/lib/') === 0) - { - remove_tree($oldState); - } - - echo "Normalisierung erfolgreich.\n"; - echo "State: ".$newState."\n"; - echo "Service: gemeinsamer Multi-Connection-Runner aus Bratonien Tools.\n"; -} -catch (Throwable $e) -{ - fwrite(STDERR, $e->getMessage()."\n"); - exit(1); -} diff --git a/nc-connector-runtime-switch.php b/nc-connector-runtime-switch.php deleted file mode 100644 index 98c235f..0000000 --- a/nc-connector-runtime-switch.php +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env php -\n"); - exit(1); -} - -$connectionId = (int)$argv[1]; -$piwigoRoot = dirname(__DIR__, 2); -$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; -$pluginRuntime = __DIR__.'/runtime/sync.sh'; -$configPath = '/etc/bratonien-tools/nc-connector/connection-'.$connectionId.'.conf'; -$serviceName = 'bratonien-nc-connector.service'; -$timerName = 'bratonien-nc-connector.timer'; -$servicePath = '/etc/systemd/system/'.$serviceName; - -function failRuntimeSwitch($message) -{ - throw new RuntimeException($message); -} - -function runRuntimeSwitch(array $command, $allowFailure = false) -{ - $spec = array( - 0 => array('file', '/dev/null', 'r'), - 1 => array('pipe', 'w'), - 2 => array('pipe', 'w'), - ); - $process = proc_open($command, $spec, $pipes); - if (!is_resource($process)) - { - failRuntimeSwitch('Prozess konnte nicht gestartet werden: '.implode(' ', $command)); - } - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[1]); - fclose($pipes[2]); - $exit = proc_close($process); - if ($exit !== 0 && !$allowFailure) - { - $detail = trim($stderr) !== '' ? trim($stderr) : trim($stdout); - failRuntimeSwitch('Befehl fehlgeschlagen ('.$exit.'): '.implode(' ', $command).($detail !== '' ? "\n".$detail : '')); - } - return array('exit'=>$exit, 'stdout'=>(string)$stdout, 'stderr'=>(string)$stderr); -} - -function parseRuntimeConfig($path) -{ - if (!is_readable($path)) - { - failRuntimeSwitch('Connector-Konfiguration nicht lesbar: '.$path); - } - $result = array(); - foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) - { - $line = trim($line); - if ($line === '' || $line[0] === '#') - { - continue; - } - if (preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches)) - { - $result[$matches[1]] = trim($matches[2]); - } - } - return $result; -} - -function dbEscapeRuntime(mysqli $db, $value) -{ - return $db->real_escape_string((string)$value); -} - -$oldService = null; -$db = null; -$table = ''; -$config = array(); -$timerStopped = false; - -try -{ - if (!is_file($pluginRuntime) || !is_readable($pluginRuntime)) - { - failRuntimeSwitch('Plugin-Runtime fehlt: '.$pluginRuntime); - } - if (!is_readable($dbConfig)) - { - failRuntimeSwitch('Piwigo-Datenbankkonfiguration nicht lesbar.'); - } - if (!is_readable($servicePath)) - { - failRuntimeSwitch('Bestehender Connector-Service wurde nicht gefunden: '.$servicePath); - } - - $conf = array(); - $prefixeTable = 'piwigo_'; - require $dbConfig; - $db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']); - if ($db->connect_errno) - { - failRuntimeSwitch('Piwigo-Datenbank nicht erreichbar: '.$db->connect_error); - } - $db->set_charset('utf8mb4'); - $table = $prefixeTable.'bratonien_tools_nc_connections'; - $result = $db->query("SELECT takeover_state, enabled, config_json FROM `".$table."` WHERE id=".$connectionId." LIMIT 1"); - if (!$result || !$result->num_rows) - { - failRuntimeSwitch('Connector-Verbindung #'.$connectionId.' wurde nicht gefunden.'); - } - $row = $result->fetch_assoc(); - if ((string)$row['takeover_state'] !== 'active' || (int)$row['enabled'] !== 1) - { - failRuntimeSwitch('Runtime-Switch ist nur fuer eine aktive Connector-Verbindung erlaubt.'); - } - $config = json_decode((string)$row['config_json'], true); - if (!is_array($config)) - { - failRuntimeSwitch('Connector-Konfiguration ist ungueltig.'); - } - - $runtimeConfig = parseRuntimeConfig($configPath); - $statusPath = isset($runtimeConfig['STATUS_FILE']) ? (string)$runtimeConfig['STATUS_FILE'] : ''; - if ($statusPath === '') - { - failRuntimeSwitch('STATUS_FILE fehlt in der Connector-Konfiguration.'); - } - - $oldService = file_get_contents($servicePath); - if (!is_string($oldService) || $oldService === '') - { - failRuntimeSwitch('Bestehende Service-Definition konnte nicht gesichert werden.'); - } - - echo "Stoppe Connector-Timer fuer den Runtime-Test...\n"; - runRuntimeSwitch(array('systemctl', 'stop', $timerName)); - $timerStopped = true; - - $deadline = time() + 120; - do - { - $active = runRuntimeSwitch(array('systemctl', 'is-active', '--quiet', $serviceName), true); - if ($active['exit'] !== 0) - { - break; - } - if (time() >= $deadline) - { - failRuntimeSwitch('Ein laufender Connector-Sync wurde nach 120 Sekunden nicht beendet.'); - } - sleep(2); - } - while (true); - - @unlink($statusPath); - echo "Teste Plugin-eigene Sync-Runtime...\n"; - $test = runRuntimeSwitch(array('env', 'PIWIGO_CONFIG='.$configPath, '/bin/bash', $pluginRuntime), true); - if ($test['exit'] !== 0) - { - $detail = trim($test['stderr']) !== '' ? trim($test['stderr']) : trim($test['stdout']); - failRuntimeSwitch('Plugin-Runtime-Test fehlgeschlagen'.($detail !== '' ? ': '.$detail : '.')); - } - - $status = is_readable($statusPath) ? json_decode((string)file_get_contents($statusPath), true) : null; - $state = is_array($status) ? trim((string)($status['state'] ?? '')) : ''; - $message = is_array($status) ? trim((string)($status['message'] ?? '')) : ''; - if ($state === 'error') - { - failRuntimeSwitch('Plugin-Runtime meldete einen Fehler'.($message !== '' ? ': '.$message : '.')); - } - - echo 'Runtime-Test erfolgreich: '.($message !== '' ? $message : 'Exit-Code 0')."\n"; - - $service = "[Unit]\nDescription=Bratonien NC Connector Sync\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=PIWIGO_CONFIG=".$configPath."\nExecStart=/bin/bash ".$pluginRuntime."\n\n"; - if (file_put_contents($servicePath, $service, LOCK_EX) === false) - { - failRuntimeSwitch('Neue Service-Definition konnte nicht geschrieben werden.'); - } - chmod($servicePath, 0644); - runRuntimeSwitch(array('systemctl', 'daemon-reload')); - runRuntimeSwitch(array('systemctl', 'enable', '--now', $timerName)); - $timerStopped = false; - - $config['takeover']['runtime'] = 'plugin-runtime'; - $config['takeover']['runtime_switched_at'] = date('Y-m-d H:i:s'); - $config['takeover']['runtime_path'] = $pluginRuntime; - $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($json)) - { - failRuntimeSwitch('Connector-Status konnte nicht serialisiert werden.'); - } - $now = date('Y-m-d H:i:s'); - $sql = "UPDATE `".$table."` SET config_json='".dbEscapeRuntime($db, $json)."', updated='".dbEscapeRuntime($db, $now)."' WHERE id=".$connectionId; - if (!$db->query($sql)) - { - failRuntimeSwitch('Connector-Status konnte nicht aktualisiert werden: '.$db->error); - } - - echo "Runtime-Switch erfolgreich.\n"; - echo "Der Connector verwendet jetzt ausschliesslich die Plugin-eigene Sync-Runtime.\n"; -} -catch (Throwable $e) -{ - if (is_string($oldService) && $oldService !== '') - { - @file_put_contents($servicePath, $oldService, LOCK_EX); - runRuntimeSwitch(array('systemctl', 'daemon-reload'), true); - } - if ($timerStopped) - { - runRuntimeSwitch(array('systemctl', 'enable', '--now', $timerName), true); - } - fwrite(STDERR, "Runtime-Switch fehlgeschlagen: ".$e->getMessage()."\n"); - fwrite(STDERR, "Die bisherige Connector-Runtime wurde beibehalten/wiederhergestellt.\n"); - exit(1); -} diff --git a/runtime/cleanup-stale.php b/runtime/cleanup-stale.php deleted file mode 100644 index 7f343a2..0000000 --- a/runtime/cleanup-stale.php +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env php -connect_errno) cleanup_fail('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error); -$db->set_charset('utf8mb4'); - -$table = $prefixeTable.'bratonien_tools_nc_connections'; -$escapedTable = $db->real_escape_string($table); -$exists = $db->query("SHOW TABLES LIKE '{$escapedTable}'"); -if (!$exists) cleanup_fail('Connector-Tabelle konnte nicht geprüft werden: '.$db->error); -if ($exists->num_rows === 0) exit(0); - -$activeIds = array(); -$result = $db->query("SELECT id FROM `{$table}`"); -if (!$result) cleanup_fail('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error); -while ($row = $result->fetch_assoc()) -{ - $activeIds[(int)$row['id']] = true; -} - -if (!is_dir($configDir)) exit(0); -foreach (glob($configDir.'/connection-*.conf') ?: array() as $configPath) -{ - if (!preg_match('/connection-([0-9]+)\.conf$/', $configPath, $matches)) continue; - $id = (int)$matches[1]; - if (isset($activeIds[$id])) continue; - - echo "NC Connector: entferne verwaiste Laufzeitdateien fuer Verbindung {$id}.\n"; - foreach (array('.conf','.db-password','.piwigo-password','.storages.tsv','.roots.tsv') as $suffix) - { - @unlink($configDir.'/connection-'.$id.$suffix); - } - cleanup_tree($stateRoot.'/connection-'.$id); - - $statusDir = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-status'; - @unlink($statusDir.'/connection-'.$id.'.json'); - @unlink($statusDir.'/deleted-'.$id); -} - -exit(0); diff --git a/runtime/lib/activity_gate.py b/runtime/lib/activity_gate.py deleted file mode 100644 index 23184ab..0000000 --- a/runtime/lib/activity_gate.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -"""Debounce Nextcloud activity for one connector scope.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import re -import subprocess -import sys -import tempfile -import time -from pathlib import Path - -IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$") - - -def validate_view(name: str) -> str: - value = str(name).strip() - if not IDENTIFIER.fullmatch(value): - raise ValueError(f"invalid SQL view name: {value!r}") - return value - - -def sql_literal(value: str) -> str: - return "'" + str(value).replace("'", "''") + "'" - - -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 defaults - data = json.loads(path.read_text(encoding="utf-8")) - 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, 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: - json.dump(state, handle, sort_keys=True) - handle.write("\n") - Path(name).replace(path) - - -def query(args: argparse.Namespace, sql: str) -> str: - env = os.environ.copy() - env["PGPASSWORD"] = args.password_file.read_text(encoding="utf-8").strip() - command = [ - "psql", "-XAt", "-h", args.host, "-p", str(args.port), - "-U", args.user, "-d", args.database, "-v", "ON_ERROR_STOP=1", "-c", sql, - ] - return subprocess.run(command, env=env, check=True, text=True, capture_output=True).stdout - - -def user_where(args: argparse.Namespace) -> str: - if not args.access_user: - return "" - return " WHERE lower(access_user) = lower(" + sql_literal(args.access_user) + ")" - - -def latest(args: argparse.Namespace) -> int: - view = validate_view(args.view) - return int(query(args, f"SELECT COALESCE(MAX(activity_id), 0) FROM {view}{user_where(args)}").strip()) - - -def source_signature(args: argparse.Namespace) -> str: - if args.roots_config: - return hashlib.sha256(args.roots_config.read_bytes()).hexdigest() - if not args.source_view: - return "" - view = validate_view(args.source_view) - payload = query(args, f"SELECT share_id, display_name, storage_id, source_path FROM {view}{user_where(args)} ORDER BY share_id") - return hashlib.sha256(payload.encode("utf-8")).hexdigest() - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("action", choices=("check", "commit")) - parser.add_argument("--state", required=True, type=Path) - parser.add_argument("--host", required=True) - parser.add_argument("--port", type=int, default=5432) - parser.add_argument("--database", required=True) - parser.add_argument("--user", required=True) - parser.add_argument("--password-file", required=True, type=Path) - parser.add_argument("--view", required=True) - parser.add_argument("--source-view", default="") - parser.add_argument("--roots-config", type=Path) - parser.add_argument("--access-user", 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) - args = parser.parse_args() - try: - validate_view(args.view) - if args.source_view: - validate_view(args.source_view) - 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, source_signature=current_source) - save(args.state, state) - return 0 - if not int(state["last_full"]): - return 0 - 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"] = int(state["pending_since"]) or now - save(args.state, state) - if now - int(state["last_full"]) >= args.full_after: - return 0 - if int(state["observed"]) <= int(state["processed"]): - return 3 - 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: - print(f"activity-gate: {error}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/runtime/lib/build_manifest.py b/runtime/lib/build_manifest.py deleted file mode 100644 index 70222e2..0000000 --- a/runtime/lib/build_manifest.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 -"""Resolve Nextcloud share rows through configured storage adapters.""" - -from __future__ import annotations - -import argparse -import csv -import json -import os -import re -import subprocess -import sys -import tempfile -from pathlib import Path, PurePosixPath - -IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$") - - -def validate_view(name: str) -> str: - value = str(name).strip() - if not IDENTIFIER.fullmatch(value): - raise ValueError(f"invalid SQL view name: {value!r}") - return value - - -def sql_literal(value: str) -> str: - return "'" + str(value).replace("'", "''") + "'" - - -def read_config(path: Path) -> dict[str, list[tuple[str, Path, str]]]: - result: dict[str, list[tuple[str, Path, str]]] = {} - with path.open(encoding="utf-8") as handle: - for number, line in enumerate(handle, 1): - line = line.rstrip("\n") - if not line or line.startswith("#"): - continue - fields = line.split("\t") - if len(fields) not in {3, 4}: - raise ValueError(f"{path}:{number}: expected storage_id, source_prefix, local_mount and optional include_prefix") - storage_id, prefix, mount = fields[:3] - include_prefix = fields[3] if len(fields) == 4 else "" - storage_id = storage_id.strip() - prefix = prefix.strip("/") - include_prefix = include_prefix.strip("/") - if not storage_id: - raise ValueError(f"{path}:{number}: storage_id is empty") - if ".." in PurePosixPath(prefix).parts or ".." in PurePosixPath(include_prefix).parts: - raise ValueError(f"{path}:{number}: unsafe prefix") - result.setdefault(storage_id, []).append((prefix, Path(mount), include_prefix)) - return result - - -def run_query(args: argparse.Namespace, env: dict[str, str], sql: str) -> subprocess.CompletedProcess[str]: - command = [ - "psql", "-X", "-A", "-F", "\t", "-t", "-v", "ON_ERROR_STOP=1", - "-h", args.host, "-p", str(args.port), "-U", args.user, "-d", args.database, "-c", sql, - ] - 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 - view = validate_view(args.view) - where = "" - if args.access_user: - where = " WHERE lower(access_user) = lower(" + sql_literal(args.access_user) + ")" - modern_sql = f"SELECT share_id, item_type, display_name, storage_id, source_path FROM {view}{where} ORDER BY share_id" - completed = run_query(args, env, modern_sql) - if completed.returncode == 0: - return list(csv.reader(completed.stdout.splitlines(), delimiter="\t")) - if args.access_user: - raise RuntimeError(completed.stderr.strip() or "Nextcloud user-filtered source query failed") - 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 {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: - parts = PurePosixPath(relative).parts - if relative.startswith("/") or ".." in parts: - raise ValueError(f"unsafe source path: {relative}") - return root.joinpath(*parts) - - -def matches_prefix(path: str, prefix: str) -> bool: - return not prefix or path == prefix or path.startswith(prefix + "/") - - -def resolve_adapter(adapters: dict[str, list[tuple[str, Path, str]]], storage_id: str, source_path: str) -> tuple[str, Path, str] | None: - relative = source_path.strip("/") - matches: list[tuple[str, Path, str]] = [] - for prefix, mount, include_prefix in adapters.get(storage_id, []): - if not matches_prefix(relative, prefix): - continue - mapped_relative = relative[len(prefix):].lstrip("/") if prefix else relative - if not matches_prefix(mapped_relative, include_prefix): - continue - matches.append((prefix, mount, include_prefix)) - if not matches: - return None - matches.sort(key=lambda item: (len(item[0]), len(item[2])), reverse=True) - best_score = (len(matches[0][0]), len(matches[0][2])) - best = {(item[0], str(item[1]), item[2]): item for item in matches if (len(item[0]), len(item[2])) == best_score} - if len(best) != 1: - raise RuntimeError(f"storage adapter is ambiguous for {storage_id}") - return next(iter(best.values())) - - -def build(args: argparse.Namespace) -> dict[str, object]: - validate_view(args.view) - adapters = read_config(args.storage_config) - rows = query_rows(args) - if not rows and not args.allow_empty: - raise RuntimeError("Nextcloud returned no matching sources; refusing an empty manifest") - manifest: list[str] = [] - errors: list[str] = [] - folder_count = 0 - file_count = 0 - for row in rows: - if len(row) != 5: - errors.append(f"invalid database row with {len(row)} columns") - continue - share_id, item_type, display_name, storage_id, source_path = row - item_type = item_type.strip().lower() - relative = source_path.strip("/") - if item_type and item_type not in {"folder", "file"}: - errors.append(f"source {share_id}: unsupported item_type {item_type!r}") - continue - try: - adapter = resolve_adapter(adapters, storage_id, relative) - except RuntimeError as error: - errors.append(f"source {share_id}: {error}") - continue - if not adapter: - continue - prefix, mount, _include_prefix = adapter - if prefix: - relative = relative[len(prefix):].lstrip("/") - source = contained_join(mount, relative) - if not mount.is_mount(): - errors.append(f"source {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"source {share_id}: source unavailable: {source}") - continue - if item_type == "folder": - if not source.is_dir(): - errors.append(f"source {share_id}: source directory unavailable: {source}") - continue - folder_count += 1 - else: - if not source.is_file(): - errors.append(f"source {share_id}: source file unavailable: {source}") - continue - file_count += 1 - if any(char in display_name for char in ("\t", "\n", "\r")): - errors.append(f"source {share_id}: display name contains unsupported control characters") - continue - source_text = str(source) - if any(char in source_text for char in ("\t", "\n", "\r")): - errors.append(f"source {share_id}: source path contains unsupported control characters") - continue - manifest.append(f"{share_id}\t{item_type}\t{display_name.lstrip('/')}\t{source}") - if errors: - raise RuntimeError("; ".join(errors)) - if not manifest and rows and not args.allow_empty: - raise RuntimeError("no Nextcloud sources match the configured storage adapters") - args.output.parent.mkdir(parents=True, exist_ok=True) - with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=args.output.parent, delete=False) as handle: - handle.write("\n".join(manifest) + ("\n" if manifest else "")) - temporary = Path(handle.name) - temporary.replace(args.output) - return {"sources": len(manifest), "folders": folder_count, "files": file_count, "manifest": str(args.output)} - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--host", required=True) - parser.add_argument("--port", type=int, default=5432) - parser.add_argument("--database", required=True) - parser.add_argument("--user", required=True) - parser.add_argument("--password-file", required=True, type=Path) - parser.add_argument("--view", required=True) - parser.add_argument("--access-user", default="") - parser.add_argument("--storage-config", required=True, type=Path) - parser.add_argument("--output", required=True, type=Path) - parser.add_argument("--allow-empty", action="store_true") - args = parser.parse_args() - try: - print(json.dumps(build(args), ensure_ascii=False)) - except Exception as error: - print(f"manifest: {error}", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/runtime/lib/build_selected_manifest.py b/runtime/lib/build_selected_manifest.py deleted file mode 100644 index acf32bc..0000000 --- a/runtime/lib/build_selected_manifest.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -"""Build a manifest from WebDAV-authorized Nextcloud file IDs. - -The selected roots are resolved through a generic Nextcloud file view and then -mapped through configured storage adapters. No user name, path layout or storage -protocol is assumed here. -""" - -from __future__ import annotations - -import argparse -import csv -import json -import os -import re -import subprocess -import sys -import tempfile -from pathlib import Path, PurePosixPath - -IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$") - - -def validate_view(name: str) -> str: - value = str(name).strip() - if not IDENTIFIER.fullmatch(value): - raise ValueError(f"invalid SQL view name: {value!r}") - return value - - -def safe_relative(value: str) -> str: - value = str(value).strip("/") - if ".." in PurePosixPath(value).parts: - raise ValueError(f"unsafe relative path: {value!r}") - return value - - -def read_roots(path: Path) -> dict[int, str]: - roots: dict[int, str] = {} - with path.open(encoding="utf-8") as handle: - for number, raw in enumerate(handle, 1): - line = raw.rstrip("\n") - if not line or line.startswith("#"): - continue - fields = line.split("\t") - if len(fields) != 2: - raise ValueError(f"{path}:{number}: expected fileid and display_name") - fileid_text, display_name = fields - if not fileid_text.isdigit() or int(fileid_text) < 1: - raise ValueError(f"{path}:{number}: invalid fileid") - if any(char in display_name for char in ("\t", "\n", "\r")): - raise ValueError(f"{path}:{number}: invalid display name") - roots[int(fileid_text)] = display_name or f"Element_{fileid_text}" - if not roots: - raise ValueError("no selected Nextcloud roots configured") - return roots - - -def read_adapters(path: Path) -> dict[str, list[tuple[str, Path]]]: - result: dict[str, list[tuple[str, Path]]] = {} - with path.open(encoding="utf-8") as handle: - for number, raw in enumerate(handle, 1): - line = raw.rstrip("\n") - if not line or line.startswith("#"): - continue - fields = line.split("\t") - if len(fields) not in {3, 4}: - raise ValueError(f"{path}:{number}: expected storage_id, source_prefix, local_mount and optional include_prefix") - storage_id, source_prefix, local_mount = fields[:3] - storage_id = storage_id.strip() - source_prefix = safe_relative(source_prefix) - mount = Path(local_mount) - if not storage_id: - raise ValueError(f"{path}:{number}: empty storage_id") - if not mount.is_absolute(): - raise ValueError(f"{path}:{number}: local_mount must be absolute") - result.setdefault(storage_id, []).append((source_prefix, mount)) - if not result: - raise ValueError("no storage adapters configured") - return result - - -def matches_prefix(path: str, prefix: str) -> bool: - return not prefix or path == prefix or path.startswith(prefix + "/") - - -def resolve_adapter(adapters: dict[str, list[tuple[str, Path]]], storage_id: str, source_path: str) -> tuple[str, Path]: - relative = safe_relative(source_path) - matches = [(prefix, mount) for prefix, mount in adapters.get(storage_id, []) if matches_prefix(relative, prefix)] - if not matches: - raise RuntimeError(f"no storage adapter configured for {storage_id}") - matches.sort(key=lambda item: len(item[0]), reverse=True) - best_length = len(matches[0][0]) - best = {(prefix, str(mount)): (prefix, mount) for prefix, mount in matches if len(prefix) == best_length} - if len(best) != 1: - raise RuntimeError(f"storage adapter is ambiguous for {storage_id}") - return next(iter(best.values())) - - -def query_rows(args: argparse.Namespace, roots: dict[int, str]) -> dict[int, tuple[str, str]]: - password = args.password_file.read_text(encoding="utf-8").strip() - env = os.environ.copy() - env["PGPASSWORD"] = password - ids = ",".join(str(fileid) for fileid in sorted(roots)) - view = validate_view(args.view) - sql = f"SELECT fileid, storage_id, source_path FROM {view} WHERE fileid IN ({ids}) ORDER BY fileid" - command = [ - "psql", "-X", "-A", "-F", "\t", "-t", "-v", "ON_ERROR_STOP=1", - "-h", args.host, "-p", str(args.port), "-U", args.user, "-d", args.database, "-c", sql, - ] - completed = subprocess.run(command, env=env, check=False, text=True, capture_output=True) - if completed.returncode != 0: - raise RuntimeError(completed.stderr.strip() or "Nextcloud file view query failed") - result: dict[int, tuple[str, str]] = {} - for row in csv.reader(completed.stdout.splitlines(), delimiter="\t"): - if len(row) != 3 or not row[0].isdigit(): - raise RuntimeError("invalid row returned by Nextcloud file view") - result[int(row[0])] = (row[1], row[2]) - missing = sorted(set(roots) - set(result)) - if missing: - raise RuntimeError("selected Nextcloud file IDs are no longer resolvable: " + ", ".join(map(str, missing))) - return result - - -def contained_join(root: Path, relative: str) -> Path: - relative = safe_relative(relative) - candidate = root.joinpath(*PurePosixPath(relative).parts) if relative else root - root_real = root.resolve() - candidate_real = candidate.resolve() - try: - candidate_real.relative_to(root_real) - except ValueError as error: - raise ValueError(f"resolved source escapes configured mount: {candidate}") from error - return candidate - - -def build(args: argparse.Namespace) -> dict[str, object]: - roots = read_roots(args.roots_config) - adapters = read_adapters(args.storage_config) - rows = query_rows(args, roots) - manifest: list[str] = [] - - for fileid, display_name in roots.items(): - storage_id, source_path = rows[fileid] - prefix, mount = resolve_adapter(adapters, storage_id, source_path) - if not mount.is_mount(): - raise RuntimeError(f"storage mount unavailable: {mount}") - relative = safe_relative(source_path) - if prefix: - relative = relative[len(prefix):].lstrip("/") - source = contained_join(mount, relative) - if source.is_dir(): - item_type = "folder" - elif source.is_file(): - item_type = "file" - else: - raise RuntimeError(f"selected source unavailable: {source}") - if any(char in str(source) for char in ("\t", "\n", "\r")): - raise ValueError("resolved source path contains unsupported control characters") - manifest.append(f"fileid:{fileid}\t{item_type}\t{display_name}\t{source}") - - args.output.parent.mkdir(parents=True, exist_ok=True) - with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=args.output.parent, delete=False) as handle: - handle.write("\n".join(manifest) + "\n") - temporary = Path(handle.name) - temporary.replace(args.output) - return {"roots": len(manifest), "manifest": str(args.output)} - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--host", required=True) - parser.add_argument("--port", type=int, default=5432) - parser.add_argument("--database", required=True) - parser.add_argument("--user", required=True) - parser.add_argument("--password-file", required=True, type=Path) - parser.add_argument("--view", required=True) - parser.add_argument("--storage-config", required=True, type=Path) - parser.add_argument("--roots-config", required=True, type=Path) - parser.add_argument("--output", required=True, type=Path) - args = parser.parse_args() - try: - print(json.dumps(build(args), ensure_ascii=False)) - except Exception as error: - print(f"selected-manifest: {error}", file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/runtime/lib/piwigo-db-check.php b/runtime/lib/piwigo-db-check.php deleted file mode 100644 index 2e3d061..0000000 --- a/runtime/lib/piwigo-db-check.php +++ /dev/null @@ -1,53 +0,0 @@ - $target) { - if (str_starts_with((string) $source, 'share:') && !str_contains((string) $source, '/')) { - $expected[] = basename((string) $target); - } -} -if ($expected === []) { - exit(1); -} - -$conf = []; -$prefixeTable = ''; -require $databaseConfig; - -$database = new mysqli( - (string) $conf['db_host'], - (string) $conf['db_user'], - (string) $conf['db_password'], - (string) $conf['db_base'] -); -$database->set_charset('utf8mb4'); -$table = $database->real_escape_string((string) $prefixeTable) . 'categories'; -$result = $database->query( - 'SELECT dir FROM `' . $table . '` WHERE site_id = 1 AND dir IS NOT NULL' -); -$existing = []; -while ($row = $result->fetch_assoc()) { - $existing[(string) $row['dir']] = true; -} - -foreach ($expected as $directory) { - if (!isset($existing[$directory])) { - exit(1); - } -} -exit(0); diff --git a/runtime/lib/piwigo-db-sync.pl b/runtime/lib/piwigo-db-sync.pl deleted file mode 100644 index 7590e93..0000000 --- a/runtime/lib/piwigo-db-sync.pl +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use JSON::PP qw(decode_json); -use LWP::UserAgent; -use Getopt::Long; - -my %opt; -GetOptions( - \%opt, - 'base-url=s', - 'username=s', - 'password-file=s', -) or die "Ungültige Parameter\n"; - -for my $required (qw(base-url username password-file)) { - die "Parameter --$required fehlt\n" if !defined $opt{$required} || $opt{$required} eq ''; -} - -open my $password_handle, '<', $opt{'password-file'} - or die "Passwortdatei kann nicht gelesen werden: $!\n"; -my $password = <$password_handle>; -close $password_handle; -defined $password or die "Passwortdatei ist leer\n"; -chomp $password; - -my $ua = LWP::UserAgent->new(timeout => 900); -$ua->agent('Bratonien-NC-Connector/1.0'); -$ua->cookie_jar({}); - -my $login = $ua->post( - $opt{'base-url'}.'/ws.php?format=json', - { - method => 'pwg.session.login', - username => $opt{username}, - password => $password, - }, -); -die "Piwigo-Anmeldung fehlgeschlagen: ".$login->status_line."\n" - if !$login->is_success; - -my $login_result = eval { decode_json($login->decoded_content) }; -die "Piwigo-Anmeldung wurde abgelehnt\n" - if !$login_result || ($login_result->{stat} // '') ne 'ok'; - -my $sync = $ua->post( - $opt{'base-url'}.'/admin.php?page=site_update&site=1', - { - sync => 'files', - display_info => 1, - privacy_level => 0, - 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"; diff --git a/runtime/lib/piwigo-sync.php b/runtime/lib/piwigo-sync.php index 3e1650d..7c85001 100644 --- a/runtime/lib/piwigo-sync.php +++ b/runtime/lib/piwigo-sync.php @@ -50,7 +50,7 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 900, CURLOPT_FOLLOWLOCATION => false, - CURLOPT_USERAGENT => 'Bratonien-NC-Connector', + CURLOPT_USERAGENT => 'Bratonien-NC-Connector-WebDAV', ); if ($headers) $options[CURLOPT_HTTPHEADER] = $headers; if ($cookie_file !== null) @@ -141,7 +141,10 @@ function decrypt_blob($blob, $hex_key) function ensure_webdav_site(mysqli $db, $prefixeTable, $piwigo_root, array $connection_config, $connection_id) { - if ((string)($connection_config['source_mode'] ?? '') !== 'webdav-placeholder') return 1; + if ((string)($connection_config['source_mode'] ?? '') !== 'webdav-placeholder') + { + fail_sync('Verbindung #'.$connection_id.' ist keine WebDAV-Verbindung.'); + } $gallery_root = rtrim((string)($connection_config['parallel_gallery_root'] ?? ''), '/'); if ($gallery_root === '') fail_sync('WebDAV-Galeriewurzel fehlt in der Verbindungskonfiguration.'); @@ -158,10 +161,7 @@ function ensure_webdav_site(mysqli $db, $prefixeTable, $piwigo_root, array $conn $escaped = $db->real_escape_string($site_url); $result = $db->query("SELECT id FROM `{$prefixeTable}sites` WHERE galleries_url='{$escaped}' LIMIT 1"); if (!$result) fail_sync('Piwigo-Site konnte nicht gelesen werden: '.$db->error); - if ($result->num_rows) - { - return (int)$result->fetch_assoc()['id']; - } + if ($result->num_rows) return (int)$result->fetch_assoc()['id']; if (!$db->query("INSERT INTO `{$prefixeTable}sites` (galleries_url) VALUES ('{$escaped}')")) { @@ -209,38 +209,15 @@ try $site_id = ensure_webdav_site($db, $prefixeTable, $piwigo_root, $connection_config, $connection_id); $api = array('key_id'=>'', 'key_secret'=>''); - $connection_scoped = (string)($connection_config['piwigo_auth'] ?? '') === 'connection-scoped' || array_key_exists('api_enabled', $connection_config); - if ($connection_scoped) + if (!empty($connection_config['api_enabled'])) { - if (!empty($connection_config['api_enabled'])) - { - $api['key_id'] = trim((string)($connection_credentials['api_key_id'] ?? '')); - $api['key_secret'] = trim((string)($connection_credentials['api_key_secret'] ?? '')); - } - } - else - { - $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'] ?? ''); - } - } - } + $api['key_id'] = trim((string)($connection_credentials['api_key_id'] ?? '')); + $api['key_secret'] = trim((string)($connection_credentials['api_key_secret'] ?? '')); } - $fallback_user = (string)($connection_credentials['piwigo_user'] ?? ''); + $fallback_user = trim((string)($connection_credentials['piwigo_user'] ?? '')); $fallback_password = (string)($connection_credentials['piwigo_password'] ?? ''); - $api_error = null; if ($api['key_id'] !== '' && $api['key_secret'] !== '') { try @@ -252,12 +229,6 @@ try ); decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id), $headers)); $orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), $headers)); - // Entfernt alte technische bratonien-webdav-N Wrapper aus Site 1, - // nachdem deren generierte Verzeichnisse beim Reconcile entfernt wurden. - if ($site_id !== 1) - { - decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), $headers)); - } $added = (int)($orphan['added_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0); echo "Piwigo-Synchronisierung per API erfolgreich (Site $site_id)\n"; @@ -266,14 +237,12 @@ try } catch (Throwable $e) { - $api_error = $e->getMessage(); - fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n"); + fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$e->getMessage()."\n"); } } else { - $api_error = $connection_scoped ? 'Fuer diese Verbindung ist keine API konfiguriert.' : 'Keine gespeicherten API-Zugangsdaten.'; - fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n"); + fwrite(STDERR, "Piwigo-API nicht nutzbar: Fuer diese Verbindung ist keine API konfiguriert.\n"); } if ($fallback_user === '' || $fallback_password === '') fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback fuer diese Verbindung vorhanden.'); @@ -291,10 +260,6 @@ try $cookie_file ); $orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), array(), $cookie_file)); - if ($site_id !== 1) - { - decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), array(), $cookie_file)); - } $added = (int)($orphan['added_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0); echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich (Site $site_id)\n"; diff --git a/runtime/nextcloud/update-showcase-source-view.sql b/runtime/nextcloud/update-showcase-source-view.sql deleted file mode 100644 index 469a068..0000000 --- a/runtime/nextcloud/update-showcase-source-view.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE OR REPLACE VIEW piwigo_showcase_sources AS -SELECT - s.id AS share_id, - s.file_target AS display_name, - st.id AS storage_id, - f.path AS source_path, - s.accepted, - s.permissions, - s.item_type AS item_type -FROM oc_share AS s -JOIN oc_filecache AS f ON f.fileid = s.file_source -JOIN oc_storages AS st ON st.numeric_id = f.storage -WHERE lower(s.share_with) = 'showcase' - AND s.item_type IN ('folder', 'file') - AND s.accepted = 1; - -GRANT SELECT ON piwigo_showcase_sources TO piwigo_reader; diff --git a/runtime/reconcile-webdav.php b/runtime/reconcile-webdav.php index f6c4262..2ebfd7f 100644 --- a/runtime/reconcile-webdav.php +++ b/runtime/reconcile-webdav.php @@ -82,7 +82,6 @@ $configDir = '/etc/bratonien-tools/nc-connector'; $stateRoot = '/var/lib/bratonien-tools/nc-connector'; $publicSourceRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-source'; $publicGalleryRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-gallery'; -$legacyGalleryRoot = rtrim($piwigoRoot, '/').'/galleries'; try { @@ -104,14 +103,15 @@ try $hexKey = trim((string)$keyResult->fetch_assoc()['value']); $table = $prefixeTable.'bratonien_tools_nc_connections'; - $rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id DESC"); - if (!$rows) fail_webdav_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error); + $rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` WHERE adapter='remote' ORDER BY id DESC"); + if (!$rows) fail_webdav_reconcile('WebDAV-Verbindungen konnten nicht gelesen werden: '.$db->error); - foreach (array($configDir=>$configDir, $publicSourceRoot=>$publicSourceRoot, $publicGalleryRoot=>$publicGalleryRoot) as $dir=>$unused) + foreach (array($configDir, $stateRoot, $publicSourceRoot, $publicGalleryRoot) as $dir) { - if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0755, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir); + if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0750, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir); } @chmod($configDir, 0700); + @chmod($stateRoot, 0750); @chmod($publicSourceRoot, 0755); @chmod($publicGalleryRoot, 0755); @@ -123,9 +123,7 @@ try $id = (int)$row['id']; $config = json_decode((string)$row['config_json'], true); if (!is_array($config)) $config = array(); - if ((string)$row['adapter'] !== 'remote') continue; if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue; - if (empty($config['parallel_test'])) continue; try { @@ -144,7 +142,6 @@ try { fwrite(STDERR, "NC WebDAV #{$id}: identische Quelle bereits durch Verbindung #".$seenFingerprints[$fingerprint]." abgedeckt; doppelte Runtime wird unterdrueckt.\n"); foreach (glob($configDir.'/webdav-connection-'.$id.'.*') ?: array() as $stale) @unlink($stale); - webdav_remove_generated_tree($legacyGalleryRoot.'/bratonien-webdav-'.$id, $legacyGalleryRoot); webdav_remove_generated_tree($publicGalleryRoot.'/connection-'.$id, $publicGalleryRoot); continue; } @@ -156,18 +153,12 @@ try if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_webdav_reconcile('State-Verzeichnis konnte nicht angelegt werden.'); @chmod($stateDir, 0750); - $legacyDefault = $legacyGalleryRoot.'/bratonien-webdav-'.$id; $galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/'); - if ($galleryRoot === '' || $galleryRoot === $legacyDefault || strpos($galleryRoot, $legacyGalleryRoot.'/bratonien-webdav-') === 0) - { - $galleryRoot = $publicGalleryRoot.'/connection-'.$id; - } + $expectedGalleryRoot = $publicGalleryRoot.'/connection-'.$id; + if ($galleryRoot === '' || strpos($galleryRoot, $publicGalleryRoot.'/') !== 0) $galleryRoot = $expectedGalleryRoot; if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.'); @chmod($galleryRoot, 0755); - // Alte technische Wrapper unter ./galleries duerfen nicht als Piwigo-Alben auftauchen. - webdav_remove_generated_tree($legacyDefault, $legacyGalleryRoot); - $sourceDir = $publicSourceRoot.'/connection-'.$id; if (!is_dir($sourceDir) && !mkdir($sourceDir, 0755, true)) fail_webdav_reconcile('WebDAV-Platzhalterquelle konnte nicht angelegt werden.'); @chmod($sourceDir, 0755); @@ -215,21 +206,23 @@ try file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); @chmod($configPath, 0600); + unset($config['parallel_test']); $config['state_dir'] = $stateDir; $config['status_file'] = $statusFile; $config['parallel_gallery_root'] = $galleryRoot; $config['source_fingerprint'] = $fingerprint; $config['runtime'] = array( - 'mode'=>'parallel-webdav', + 'mode'=>'webdav', 'config'=>$configPath, 'piwigo_sync_enabled'=>true, 'reconciled_at'=>date('Y-m-d H:i:s'), ); $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (is_string($json)) + if (!is_string($json)) fail_webdav_reconcile('WebDAV-Runtime-Konfiguration konnte nicht serialisiert werden.'); + $escaped = $db->real_escape_string($json); + if (!$db->query("UPDATE `{$table}` SET enabled=1, config_json='{$escaped}' WHERE id={$id} AND adapter='remote' LIMIT 1")) { - $escaped = $db->real_escape_string($json); - $db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} LIMIT 1"); + fail_webdav_reconcile('WebDAV-Runtime-Status konnte nicht gespeichert werden: '.$db->error); } } catch (Throwable $e) diff --git a/runtime/reconcile.php b/runtime/reconcile.php deleted file mode 100644 index 8d7062e..0000000 --- a/runtime/reconcile.php +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env php -$plain,'piwigo_user'=>'','piwigo_password'=>'','api_key_id'=>'','api_key_secret'=>''); - } - return array( - 'db_password'=>(string)($decoded['db_password'] ?? ''), - 'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''), - 'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''), - 'api_key_id'=>(string)($decoded['api_key_id'] ?? ''), - 'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''), - ); -} - -function encrypt_reconcile(array $credentials, $hexKey) -{ - $plain = json_encode(array( - 'v'=>2, - 'db_password'=>(string)$credentials['db_password'], - 'piwigo_user'=>(string)$credentials['piwigo_user'], - 'piwigo_password'=>(string)$credentials['piwigo_password'], - 'api_key_id'=>(string)$credentials['api_key_id'], - 'api_key_secret'=>(string)$credentials['api_key_secret'], - ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($plain)) fail_reconcile('Connector-Zugangsdaten konnten nicht serialisiert werden.'); - $iv = random_bytes(12); - $tag = ''; - $cipher = openssl_encrypt($plain, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag); - if ($cipher === false) fail_reconcile('Connector-Zugangsdaten konnten nicht verschluesselt werden.'); - return base64_encode(json_encode(array( - 'v'=>1, - 'iv'=>base64_encode($iv), - 'tag'=>base64_encode($tag), - 'data'=>base64_encode($cipher), - ))); -} - -function write_runtime_status($piwigoRoot, $id, $stateDir, $message) -{ - $payload = json_encode(array( - 'state'=>'error', - 'message'=>'Runtime-Vorbereitung fehlgeschlagen', - 'timestamp'=>time(), - 'auth_mode'=>'failed', - 'api'=>array('state'=>'not_run','message'=>''), - 'fallback'=>array('state'=>'not_run','message'=>''), - 'error_detail'=>(string)$message, - ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($payload)) return; - $targets = array(rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$id.'.json'); - if ($stateDir !== '') $targets[] = rtrim($stateDir, '/').'/connector-status.json'; - foreach ($targets as $target) - { - $dir = dirname($target); - if (!is_dir($dir)) @mkdir($dir, 0755, true); - @file_put_contents($target, $payload."\n", LOCK_EX); - @chmod($target, 0644); - } -} - -function sql_reconcile(mysqli $db, $value) -{ - return $db->real_escape_string((string)$value); -} - -function nextcloud_view_available(array $config, $password, $view) -{ - if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', (string)$view)) return false; - $spec = array(0=>array('file','/dev/null','r'),1=>array('pipe','w'),2=>array('pipe','w')); - $command = array( - 'psql','-XAt','-v','ON_ERROR_STOP=1', - '-h',(string)$config['host'],'-p',(string)$config['port'],'-U',(string)$config['user'],'-d',(string)$config['database'], - '-c','SELECT 1 FROM '.$view.' LIMIT 1' - ); - $env = $_ENV; - $env['PGPASSWORD'] = (string)$password; - $process = @proc_open($command, $spec, $pipes, null, $env); - if (!is_resource($process)) return false; - stream_get_contents($pipes[1]); - stream_get_contents($pipes[2]); - fclose($pipes[1]); fclose($pipes[2]); - return proc_close($process) === 0; -} - -function configured_access_user(array $config) -{ - foreach (array('access_user','nextcloud_access_user','showcase_user') as $key) - { - $value = trim((string)($config[$key] ?? '')); - if ($value !== '') return $value; - } - return ''; -} - -$pluginRoot = dirname(__DIR__); -$piwigoRoot = dirname($pluginRoot, 2); -$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; -$configDir = '/etc/bratonien-tools/nc-connector'; -$stateRoot = '/var/lib/bratonien-tools/nc-connector'; - -try -{ - if (!is_readable($dbConfig)) fail_reconcile('Piwigo-Datenbankkonfiguration ist nicht lesbar: '.$dbConfig); - $conf = array(); - $prefixeTable = 'piwigo_'; - require $dbConfig; - foreach (array('db_host','db_user','db_password','db_base') as $key) - { - if (!isset($conf[$key])) fail_reconcile('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_reconcile('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error); - $db->set_charset('utf8mb4'); - - $keyResult = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1"); - if (!$keyResult || !$keyResult->num_rows) fail_reconcile('Connector-Schluessel wurde nicht gefunden.'); - $hexKey = trim((string)$keyResult->fetch_assoc()['value']); - - $table = $prefixeTable.'bratonien_tools_nc_connections'; - $rows = $db->query("SELECT id,name,adapter,enabled,takeover_state,config_json,secret_blob FROM `{$table}` ORDER BY id"); - if (!$rows) fail_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error); - - if (!is_dir($configDir) && !mkdir($configDir, 0700, true)) fail_reconcile('Runtime-Konfigurationsverzeichnis konnte nicht angelegt werden.'); - chmod($configDir, 0700); - - while ($row = $rows->fetch_assoc()) - { - $id = (int)$row['id']; - $config = json_decode((string)$row['config_json'], true); - if (!is_array($config)) $config = array(); - $stateDir = rtrim((string)($config['state_dir'] ?? ''), '/'); - if ($stateDir === '') $stateDir = $stateRoot.'/connection-'.$id; - - try - { - if ((string)$row['adapter'] !== 'local') continue; - $isActive = (int)$row['enabled'] === 1 && (string)$row['takeover_state'] === 'active'; - $accessUser = configured_access_user($config); - $wizardConnection = (string)($config['origin'] ?? '') === 'native' - && trim((string)($config['nextcloud_url'] ?? '')) !== '' - && $accessUser !== ''; - $verification = isset($config['verification']) && is_array($config['verification']) ? $config['verification'] : null; - $verificationFailed = is_array($verification) && empty($verification['ok']); - if (!$isActive && (!$wizardConnection || $verificationFailed)) continue; - - foreach (array('host','port','database','user','source_view','activity_view','gallery_root') as $key) - { - if (trim((string)($config[$key] ?? '')) === '') fail_reconcile('Konfiguration unvollstaendig: '.$key.' fehlt.'); - } - $storages = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array(); - if (!$storages) fail_reconcile('Keine Storage-Zuordnungen gespeichert.'); - - $credentials = decrypt_reconcile((string)$row['secret_blob'], $hexKey); - if ($credentials['db_password'] === '') fail_reconcile('Datenbankpasswort fehlt.'); - - $sourceMode = trim((string)($config['source_mode'] ?? '')); - if ($sourceMode === 'user-filesystem') - { - @unlink($configDir.'/connection-'.$id.'.conf'); - fail_reconcile('Diese Verbindung verwendet den verworfenen experimentellen Benutzerpfad-Modus. Bitte die Verbindung mit dem aktuellen Assistenten neu anlegen.'); - } - - if ($sourceMode === '' || $sourceMode === 'legacy-view') - { - $canMigrate = $accessUser !== '' - && nextcloud_view_available($config, $credentials['db_password'], 'piwigo_connector_shares') - && nextcloud_view_available($config, $credentials['db_password'], 'piwigo_connector_activity'); - if ($canMigrate) - { - $sourceMode = 'user-shares'; - $config['source_mode'] = $sourceMode; - $config['source_view'] = 'piwigo_connector_shares'; - $config['activity_view'] = 'piwigo_connector_activity'; - $config['access_user'] = $accessUser; - $config['nextcloud_access_user'] = $accessUser; - unset($config['showcase_user']); - echo "NC Connector: Verbindung #{$id} auf generische benutzergefilterte Quellen migriert.\n"; - } - else - { - $sourceMode = 'legacy-view'; - $config['source_mode'] = $sourceMode; - } - } - - if (!in_array($sourceMode, array('legacy-view','user-shares','selected-fileids'), true)) fail_reconcile('Unbekannter Quellenmodus: '.$sourceMode); - if ($sourceMode !== 'legacy-view' && $accessUser === '') fail_reconcile('Für die verbindungsbezogene Quelle fehlt der Nextcloud-Benutzer.'); - - $roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array(); - if ($sourceMode === 'selected-fileids' && !$roots) fail_reconcile('Für die Verbindung sind keine ausgewählten Nextcloud-Datei-IDs gespeichert.'); - - if (!$isActive && !array_key_exists('api_enabled', $config)) - { - $hasFallback = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== ''; - if (!$hasFallback) fail_reconcile('Die Verbindung besitzt weder einen eigenen API-Zugang noch einen eigenen Fallback.'); - $credentials['api_key_id'] = ''; - $credentials['api_key_secret'] = ''; - $config['piwigo_auth'] = 'connection-scoped'; - $config['api_enabled'] = false; - $row['secret_blob'] = encrypt_reconcile($credentials, $hexKey); - } - - $connectionScoped = (string)($config['piwigo_auth'] ?? '') === 'connection-scoped' || array_key_exists('api_enabled', $config); - if ($connectionScoped) - { - $apiAvailable = !empty($config['api_enabled']) && $credentials['api_key_id'] !== '' && $credentials['api_key_secret'] !== ''; - $fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== ''; - if (!$apiAvailable && !$fallbackAvailable) fail_reconcile('Kein verbindungseigener Piwigo-Zugang gespeichert.'); - } - - if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_reconcile('State-Verzeichnis konnte nicht angelegt werden.'); - chmod($stateDir, 0750); - - $base = $configDir.'/connection-'.$id; - $dbPasswordPath = $base.'.db-password'; - $piwigoPasswordPath = $base.'.piwigo-password'; - $storagePath = $base.'.storages.tsv'; - $rootsPath = $base.'.roots.tsv'; - $configPath = $base.'.conf'; - $statusFile = $stateDir.'/connector-status.json'; - - file_put_contents($dbPasswordPath, $credentials['db_password']."\n", LOCK_EX); - chmod($dbPasswordPath, 0600); - - $fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== ''; - if ($fallbackAvailable) - { - file_put_contents($piwigoPasswordPath, $credentials['piwigo_password']."\n", LOCK_EX); - chmod($piwigoPasswordPath, 0600); - } - else @unlink($piwigoPasswordPath); - - $storageLines = array('# storage_idsource_prefixlocal_mountinclude_prefix'); - foreach ($storages as $storage) - { - $storageLines[] = (string)($storage['storage_id'] ?? '')."\t" - .trim((string)($storage['source_prefix'] ?? ''), '/')."\t" - .(string)($storage['local_mount'] ?? '')."\t" - .trim((string)($storage['include_prefix'] ?? ''), '/'); - } - file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX); - chmod($storagePath, 0600); - - if ($sourceMode === 'selected-fileids') - { - $rootLines = array('# fileiddisplay_name'); - foreach ($roots as $root) - { - $fileid = (int)($root['fileid'] ?? 0); - $display = trim((string)($root['display_name'] ?? '')); - if ($fileid < 1 || $display === '' || preg_match('/[\t\r\n]/', $display)) fail_reconcile('Eine gespeicherte Nextcloud-Quelle ist ungueltig.'); - $rootLines[] = $fileid."\t".$display; - } - file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX); - chmod($rootsPath, 0600); - } - else @unlink($rootsPath); - - $lines = array( - 'PIWIGO_ROOT='.$piwigoRoot, - 'GALLERY_ROOT='.(string)$config['gallery_root'], - 'STATE_DIR='.$stateDir, - 'STATUS_FILE='.$statusFile, - 'NC_DB_HOST='.(string)$config['host'], - 'NC_DB_PORT='.(string)$config['port'], - 'NC_DB_NAME='.(string)$config['database'], - 'NC_DB_USER='.(string)$config['user'], - 'NC_DB_VIEW='.(string)$config['source_view'], - 'NC_ACTIVITY_VIEW='.(string)$config['activity_view'], - 'NC_DB_PASSWORD_FILE='.$dbPasswordPath, - 'STORAGE_CONFIG='.$storagePath, - 'SOURCE_MODE='.$sourceMode, - 'QUIET_SECONDS='.(int)($config['quiet_seconds'] ?? 120), - 'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900), - 'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400), - 'PIWIGO_SYNC_ENABLED=1', - ); - if ($sourceMode !== 'legacy-view') $lines[] = 'ACCESS_USER='.escapeshellarg($accessUser); - if ($sourceMode === 'selected-fileids') $lines[] = 'ROOTS_CONFIG='.$rootsPath; - if ($fallbackAvailable) - { - $lines[] = 'PIWIGO_SYNC_USER='.$credentials['piwigo_user']; - $lines[] = 'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath; - } - file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); - chmod($configPath, 0600); - - $config['state_dir'] = $stateDir; - $config['status_file'] = $statusFile; - $config['runtime'] = array('mode'=>'shared-runner','config'=>$configPath,'reconciled_at'=>date('Y-m-d H:i:s')); - $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($json)) fail_reconcile('Connector-Konfiguration konnte nicht serialisiert werden.'); - $now = date('Y-m-d H:i:s'); - $sql = "UPDATE `{$table}` SET enabled=1,takeover_state='active',config_json='".sql_reconcile($db,$json)."',secret_blob='".sql_reconcile($db,$row['secret_blob'])."',updated='".sql_reconcile($db,$now)."' WHERE id=".$id; - if (!$db->query($sql)) fail_reconcile('Runtime-Status konnte nicht gespeichert werden: '.$db->error); - if (!$isActive) echo "NC Connector: Verbindung #{$id} ({$row['name']}) automatisch in die gemeinsame Runtime uebernommen.\n"; - } - catch (Throwable $e) - { - write_runtime_status($piwigoRoot, $id, $stateDir, $e->getMessage()); - fwrite(STDERR, "NC Connector #{$id}: ".$e->getMessage()."\n"); - } - } - - exit(0); -} -catch (Throwable $e) -{ - fwrite(STDERR, "NC Connector Reconcile: ".$e->getMessage()."\n"); - exit(1); -} diff --git a/runtime/run-all.sh b/runtime/run-all.sh index b2a25c5..33aeb96 100644 --- a/runtime/run-all.sh +++ b/runtime/run-all.sh @@ -5,83 +5,30 @@ CONFIG_DIR="/etc/bratonien-tools/nc-connector" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" shopt -s nullglob -if ! php "$SCRIPT_DIR/reconcile.php"; then - echo "NC Connector: gespeicherte lokale Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2 - exit 1 -fi - if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then - echo "NC Connector: WebDAV-Testverbindungen konnten nicht mit der parallelen Runtime abgeglichen werden." >&2 + echo "NC Connector: WebDAV-Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2 exit 1 fi -# Nach dem Reconcile kennt Piwigo alle noch aktiven WebDAV-Verbindungen. Jetzt -# werden Sites, Alben, Bilddatensaetze und Derivate geloeschter Verbindungen -# entfernt. Das erfasst auch Verbindungen, die bereits vor diesem Update -# geloescht wurden. if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2 exit 1 fi -if ! php "$SCRIPT_DIR/cleanup-stale.php"; then - echo "NC Connector: verwaiste Laufzeitdateien konnten nicht bereinigt werden." >&2 - exit 1 -fi - -configs=("$CONFIG_DIR"/connection-*.conf) webdav_configs=("$CONFIG_DIR"/webdav-connection-*.conf) -if [[ ${#configs[@]} -eq 0 && ${#webdav_configs[@]} -eq 0 ]]; then - echo "Keine NC-Connector-Verbindungen konfiguriert." +if [[ ${#webdav_configs[@]} -eq 0 ]]; then + echo "Keine WebDAV-Connector-Verbindungen konfiguriert." exit 0 fi result=0 - -# WebDAV zuerst: Der parallele Shadow Tree muss bereits vollständig stehen, -# bevor der weiterhin produktive lokale Weg seine normale Piwigo- -# Dateisynchronisierung ausführt. So wird der neue Baum im selben Minutenlauf -# sichtbar, ohne dass der WebDAV-Zweig selbst eine zweite globale Piwigo- -# Synchronisierung startet. for config in "${webdav_configs[@]}"; do name="$(basename "$config")" - echo "NC Connector WebDAV parallel: $name" + echo "NC Connector WebDAV: $name" if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh"; then result=1 fi done -for config in "${configs[@]}"; do - name="$(basename "$config")" - connection_id="" - if [[ "$name" =~ ^connection-([0-9]+)\.conf$ ]]; then - connection_id="${BASH_REMATCH[1]}" - fi - - piwigo_root="$(sed -n 's/^PIWIGO_ROOT=//p' "$config" | tail -n 1)" - piwigo_root="${piwigo_root%\"}" - piwigo_root="${piwigo_root#\"}" - piwigo_root="${piwigo_root%\'}" - piwigo_root="${piwigo_root#\'}" - [[ -n "$piwigo_root" ]] || piwigo_root="/var/www/piwigo" - tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status" - - if [[ -n "$connection_id" && -f "$tombstone_dir/deleted-$connection_id" ]]; then - echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt." - rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \ - "$CONFIG_DIR/connection-$connection_id.db-password" \ - "$CONFIG_DIR/connection-$connection_id.piwigo-password" \ - "$CONFIG_DIR/connection-$connection_id.storages.tsv" \ - "$CONFIG_DIR/connection-$connection_id.roots.tsv" - rm -f -- "$tombstone_dir/deleted-$connection_id" - continue - fi - - echo "NC Connector lokal: $name" - if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then - result=1 - fi -done - exit "$result" diff --git a/runtime/sync.sh b/runtime/sync.sh deleted file mode 100644 index 68c14a4..0000000 --- a/runtime/sync.sh +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env bash -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; } - -PIWIGO_SYNC_OVERRIDE_VALUE="${PIWIGO_SYNC_OVERRIDE-}" -# shellcheck source=/dev/null -source "$CONFIG_FILE" - -: "${NC_ACTIVITY_VIEW:?NC_ACTIVITY_VIEW fehlt}" -: "${NC_DB_VIEW:?NC_DB_VIEW fehlt}" -SOURCE_MODE="${SOURCE_MODE:-legacy-view}" -ACCESS_USER="${ACCESS_USER:-}" -ROOTS_CONFIG="${ROOTS_CONFIG:-}" - -case "$SOURCE_MODE" in - legacy-view|user-shares|selected-fileids) ;; - *) echo "Unbekannter SOURCE_MODE: $SOURCE_MODE" >&2; exit 1 ;; -esac -if [[ "$SOURCE_MODE" != "legacy-view" && -z "$ACCESS_USER" ]]; then - echo "ACCESS_USER fehlt fuer die verbindungsbezogene Datenquelle." >&2 - exit 1 -fi -if [[ "$SOURCE_MODE" == "selected-fileids" && ( -z "$ROOTS_CONFIG" || ! -r "$ROOTS_CONFIG" ) ]]; then - echo "ROOTS_CONFIG fehlt fuer die ausgewaehlten Nextcloud-Quellen." >&2 - exit 1 -fi - -if [[ -n "$PIWIGO_SYNC_OVERRIDE_VALUE" ]]; then - case "$PIWIGO_SYNC_OVERRIDE_VALUE" in - 0|1) PIWIGO_SYNC_ENABLED="$PIWIGO_SYNC_OVERRIDE_VALUE" ;; - *) echo "PIWIGO_SYNC_OVERRIDE muss 0 oder 1 sein." >&2; exit 1 ;; - esac -fi - -install -d -m 0750 "$STATE_DIR" -MANIFEST="$STATE_DIR/manifest.tsv" -MAP_FILE="$STATE_DIR/name-map.json" -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 - -PUBLIC_STATUS_DIR="${PIWIGO_ROOT%/}/_data/bratonien-tools/nc-connector-status" -PUBLIC_STATUS_FILE="$PUBLIC_STATUS_DIR/connection-$CONNECTION_ID.json" -install -d -m 0755 "${PIWIGO_ROOT%/}/_data/bratonien-tools" "$PUBLIC_STATUS_DIR" - -exec 9>"$LOCK_FILE" -flock -n 9 || exit 0 - -AUTH_MODE="" -API_STATE="not_run" -API_MESSAGE="" -FALLBACK_STATE="not_run" -FALLBACK_MESSAGE="" -ERROR_DETAIL="" -ERROR_STAGE="Vorbereitung" -ERROR_MESSAGE="Synchronisierung fehlgeschlagen" - -compact_output() { - tail -n 8 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//' -} - -write_status() { - local state="$1" message="$2" - local error_detail="$ERROR_DETAIL" - [[ "$state" == "error" ]] || error_detail="" - python3 - "$STATUS_FILE" "$PUBLIC_STATUS_FILE" "$state" "$message" "$AUTH_MODE" "$API_STATE" "$API_MESSAGE" "$FALLBACK_STATE" "$FALLBACK_MESSAGE" "$error_detail" <<'PY' -import json, os, sys, tempfile, time -(path, public_path, state, message, auth_mode, api_state, api_message, - fallback_state, fallback_message, error_detail) = sys.argv[1:] -payload = { - "state": state, - "message": message, - "timestamp": int(time.time()), - "auth_mode": auth_mode, - "api": {"state": api_state, "message": api_message}, - "fallback": {"state": fallback_state, "message": fallback_message}, - "error_detail": error_detail, -} - -def write_json(target): - os.makedirs(os.path.dirname(target), exist_ok=True) - fd, temporary = tempfile.mkstemp(dir=os.path.dirname(target)) - with os.fdopen(fd, "w", encoding="utf-8") as handle: - json.dump(payload, handle, ensure_ascii=False) - handle.write("\n") - os.chmod(temporary, 0o644) - os.replace(temporary, target) - -write_json(path) -write_json(public_path) -PY -} - -failure() { - local exit_code="${1:-1}" - local failed_command="${2:-unbekannt}" - local failed_line="${3:-?}" - trap - ERR - if [[ -z "$ERROR_DETAIL" ]]; then - ERROR_DETAIL="Schritt: $ERROR_STAGE. Exit-Code: $exit_code. Zeile: $failed_line. Fehlgeschlagener Befehl: $failed_command" - fi - write_status error "$ERROR_MESSAGE" - exit "$exit_code" -} -trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR - -run_stage() { - local stage="$1" message="$2" - shift 2 - local output="" exit_code=0 - ERROR_STAGE="$stage" - ERROR_MESSAGE="$message" - if output="$("$@" 2>&1)"; then exit_code=0; else exit_code=$?; fi - [[ -z "$output" ]] || printf '%s\n' "$output" - if [[ "$exit_code" -ne 0 ]]; then - ERROR_DETAIL="Schritt: $stage. Exit-Code: $exit_code." - [[ -z "$output" ]] || ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$output" | compact_output)" - trap - ERR - write_status error "$message" - exit "$exit_code" - fi -} - -ERROR_STAGE="Lokalen Zustand prüfen" -ERROR_MESSAGE="Lokaler Connector-Zustand konnte nicht geprüft werden" -NEEDS_LOCAL_REPAIR=0 -if [[ ! -s "$MAP_FILE" ]]; then - NEEDS_LOCAL_REPAIR=1 -else - set +e - python3 - "$MAP_FILE" "$GALLERY_ROOT" <<'PY' -import json, sys -from pathlib import Path -mapping = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) -gallery = Path(sys.argv[2]) -roots = [target for source, target in mapping.items() if source.startswith("share:") and "/" not in source] -raise SystemExit(0 if roots and all((gallery / target).is_dir() for target in roots) else 1) -PY - ROOTS_INTACT=$? - set -e - [[ "$ROOTS_INTACT" == "0" ]] || NEEDS_LOCAL_REPAIR=1 -fi - -if [[ "$NEEDS_LOCAL_REPAIR" == "0" && "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then - set +e - php "$SCRIPT_DIR/lib/piwigo-db-check.php" "$MAP_FILE" "$PIWIGO_ROOT" - PIWIGO_ALBUMS_INTACT=$? - set -e - [[ "$PIWIGO_ALBUMS_INTACT" == "0" ]] || NEEDS_LOCAL_REPAIR=1 -fi - -GATE_ARGS=( - --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" -) -if [[ "$SOURCE_MODE" != "legacy-view" ]]; then GATE_ARGS+=(--access-user "$ACCESS_USER"); fi -if [[ "$SOURCE_MODE" == "selected-fileids" ]]; then GATE_ARGS+=(--roots-config "$ROOTS_CONFIG"); fi - -if [[ "$NEEDS_LOCAL_REPAIR" == "1" ]]; then - GATE_RESULT=0 -else - ERROR_STAGE="Nextcloud-Aktivitaet pruefen" - ERROR_MESSAGE="Nextcloud-Aktivitaet konnte nicht geprueft werden" - if GATE_OUTPUT="$(python3 "$SCRIPT_DIR/lib/activity_gate.py" check "${GATE_ARGS[@]}" 2>&1)"; then GATE_RESULT=0; else GATE_RESULT=$?; fi - [[ -z "$GATE_OUTPUT" ]] || printf '%s\n' "$GATE_OUTPUT" -fi - -if [[ "$GATE_RESULT" == "3" ]]; then - trap - ERR - write_status ok "Keine Aenderungen gefunden" - exit 0 -fi -if [[ "$GATE_RESULT" != "0" ]]; then - ERROR_DETAIL="Schritt: Nextcloud-Aktivitaet pruefen. Exit-Code: $GATE_RESULT." - [[ -z "${GATE_OUTPUT:-}" ]] || ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$GATE_OUTPUT" | compact_output)" - trap - ERR - write_status error "Nextcloud-Aktivitaet konnte nicht geprueft werden" - exit "$GATE_RESULT" -fi - -if [[ "$SOURCE_MODE" == "selected-fileids" ]]; then - run_stage "Ausgewaehlte Nextcloud-Quellen aufloesen" "Ausgewaehlte Nextcloud-Quellen konnten nicht aufgeloest werden" \ - python3 "$SCRIPT_DIR/lib/build_selected_manifest.py" \ - --host "$NC_DB_HOST" --port "$NC_DB_PORT" --database "$NC_DB_NAME" --user "$NC_DB_USER" \ - --password-file "$NC_DB_PASSWORD_FILE" --view "$NC_DB_VIEW" \ - --storage-config "$STORAGE_CONFIG" --roots-config "$ROOTS_CONFIG" --output "$MANIFEST" -else - MANIFEST_ARGS=( - --host "$NC_DB_HOST" --port "$NC_DB_PORT" --database "$NC_DB_NAME" --user "$NC_DB_USER" - --password-file "$NC_DB_PASSWORD_FILE" --view "$NC_DB_VIEW" - --storage-config "$STORAGE_CONFIG" --output "$MANIFEST" - ) - if [[ "$SOURCE_MODE" == "user-shares" ]]; then MANIFEST_ARGS+=(--access-user "$ACCESS_USER"); fi - run_stage "Nextcloud-Dateiliste lesen" "Dateiliste aus Nextcloud konnte nicht erstellt werden" \ - python3 "$SCRIPT_DIR/lib/build_manifest.py" "${MANIFEST_ARGS[@]}" -fi - -run_stage "Lokalen Galeriebaum aktualisieren" "Lokaler Galeriebaum konnte nicht aktualisiert werden" \ - python3 "$SCRIPT_DIR/lib/shadow_tree.py" --manifest "$MANIFEST" --destination "$GALLERY_ROOT" --state "$MAP_FILE" - -if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then - ERROR_STAGE="Piwigo synchronisieren" - ERROR_MESSAGE="Piwigo-Synchronisierung fehlgeschlagen" - if PIWIGO_OUTPUT="$(php "$SCRIPT_DIR/lib/piwigo-sync.php" --piwigo-root="$PIWIGO_ROOT" --connection-id="$CONNECTION_ID" --base-url="http://127.0.0.1" 2>&1)"; then PIWIGO_EXIT=0; else PIWIGO_EXIT=$?; fi - printf '%s\n' "$PIWIGO_OUTPUT" - - if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then - AUTH_MODE="api"; API_STATE="ok"; API_MESSAGE="API-Synchronisierung erfolgreich" - FALLBACK_STATE="not_needed"; FALLBACK_MESSAGE="Fallback wurde nicht benoetigt" - else - API_LINE="$(grep -m1 '^Piwigo-API nicht nutzbar:' <<<"$PIWIGO_OUTPUT" || true)" - API_STATE="error" - if [[ -n "$API_LINE" ]]; then API_MESSAGE="${API_LINE#Piwigo-API nicht nutzbar: }"; else API_MESSAGE="API-Synchronisierung war nicht erfolgreich"; fi - if grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then - AUTH_MODE="fallback"; FALLBACK_STATE="ok"; FALLBACK_MESSAGE="Benutzername/Passwort-Fallback erfolgreich" - elif [[ "$PIWIGO_EXIT" -ne 0 ]]; then - AUTH_MODE="failed"; FALLBACK_STATE="error"; FALLBACK_MESSAGE="$(tail -n 1 <<<"$PIWIGO_OUTPUT")" - fi - fi - if [[ "$PIWIGO_EXIT" -ne 0 ]]; then - ERROR_DETAIL="Schritt: Piwigo synchronisieren. Exit-Code: $PIWIGO_EXIT. Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | compact_output)" - trap - ERR - write_status error "Piwigo-Synchronisierung fehlgeschlagen" - exit "$PIWIGO_EXIT" - fi -fi - -COMMIT_ARGS=( - --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" -) -if [[ "$SOURCE_MODE" != "legacy-view" ]]; then COMMIT_ARGS+=(--access-user "$ACCESS_USER"); fi -if [[ "$SOURCE_MODE" == "selected-fileids" ]]; then COMMIT_ARGS+=(--roots-config "$ROOTS_CONFIG"); fi - -ERROR_STAGE="Aktivitaetsstand speichern" -ERROR_MESSAGE="Aktivitaetsstand konnte nicht gespeichert werden" -if COMMIT_OUTPUT="$(python3 "$SCRIPT_DIR/lib/activity_gate.py" commit "${COMMIT_ARGS[@]}" 2>&1)"; then COMMIT_EXIT=0; else COMMIT_EXIT=$?; fi -[[ -z "$COMMIT_OUTPUT" ]] || printf '%s\n' "$COMMIT_OUTPUT" -if [[ "$COMMIT_EXIT" -ne 0 ]]; then - ERROR_DETAIL="Schritt: Aktivitaetsstand speichern. Exit-Code: $COMMIT_EXIT." - [[ -z "$COMMIT_OUTPUT" ]] || ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$COMMIT_OUTPUT" | compact_output)" - trap - ERR - write_status error "Aktivitaetsstand konnte nicht gespeichert werden" - exit "$COMMIT_EXIT" -fi - -trap - ERR -if [[ "$AUTH_MODE" == "fallback" ]]; then - write_status warning "Synchronisierung erfolgreich ueber Fallback; API war nicht nutzbar" -elif [[ "$AUTH_MODE" == "api" ]]; then - write_status ok "Synchronisierung erfolgreich ueber API" -else - write_status ok "Synchronisierung erfolgreich" -fi diff --git a/template/nc_connector_admin.tpl b/template/nc_connector_admin.tpl index 32fccda..c696b30 100644 --- a/template/nc_connector_admin.tpl +++ b/template/nc_connector_admin.tpl @@ -1,6 +1,6 @@

NC Connector

-

Verbindet Nextcloud mit Piwigo und hält freigegebene Bilder automatisch aktuell.

+

Verbindet Nextcloud per WebDAV mit Piwigo und hält die ausgewählten Bilder automatisch aktuell.

{assign var=nc_system_available value=isset($NC_CONNECTOR.system)} @@ -8,7 +8,7 @@

Status

- Verbindungen{$NC_CONNECTOR.connection_count|escape:html} + WebDAV-Verbindungen{$NC_CONNECTOR.connection_count|escape:html} Automatischer Abgleich{if $nc_system_available && $NC_CONNECTOR.system.timer_active && $NC_CONNECTOR.system.timer_enabled}Aktiv{else}Nicht aktiv{/if} Letzter Lauf{if $nc_system_available}{$NC_CONNECTOR.system.last_run_label|escape:html}{else}Nicht verfügbar{/if} Nächster Lauf{if $nc_system_available}{$NC_CONNECTOR.system.next_run_label|escape:html}{else}Nicht verfügbar{/if} @@ -21,10 +21,9 @@

Neue Verbindung

-

Für die normale Einrichtung empfehlen wir den Assistenten.

+

Nextcloud wird ausschließlich per WebDAV angebunden.

- - +
@@ -33,32 +32,21 @@
-

Neue Verbindung

+

Neue WebDAV-Verbindung

- {if $NC_CONNECTOR.wizard.step == 1} - Anmeldung - {elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'database_details'} - Datenbank prüfen - {elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts' && !$NC_CONNECTOR.wizard.directory_selection_ready} - Speicher zuordnen - {elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts' && $NC_CONNECTOR.wizard.directory_selection_ready} - Verzeichnisse auswählen - {elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_complete} - Verbindung benennen - {elseif $NC_CONNECTOR.wizard.step == 3} - Piwigo-API - {elseif $NC_CONNECTOR.wizard.step == 4} - Abschluss - {else} - Einrichtung - {/if} + {if $NC_CONNECTOR.wizard.step == 1}Anmeldung + {elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts'}Verzeichnisse auswählen + {elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_complete}Verbindung benennen + {elseif $NC_CONNECTOR.wizard.step == 3}Piwigo-API + {elseif $NC_CONNECTOR.wizard.step == 4}Abschluss + {else}Einrichtung{/if}

{if $NC_CONNECTOR.wizard.step == 1} -

Adresse und Zugang reichen für den ersten Scan. Der Assistent prüft den erreichbaren Web-Zugang automatisch.

+

Adresse und Nextcloud-Zugang genügen. Der Assistent prüft HTTP/HTTPS und anschließend den WebDAV-Zugriff des angemeldeten Benutzers.

@@ -73,113 +61,70 @@ {elseif $NC_CONNECTOR.wizard.step == 2} -

Nextcloud wurde gefunden. Für den Datenzugriff wird die bekannte Reader-Verbindung verwendet. Verzeichnisse werden ausschließlich mit dem angemeldeten Nextcloud-Benutzer gelesen.

+

Nextcloud wurde gefunden. Angezeigt werden ausschließlich Verzeichnisse des angemeldeten Nextcloud-Benutzers.

Adresse{$NC_CONNECTOR.wizard.base_url|escape:html} Version{if $NC_CONNECTOR.wizard.version}{$NC_CONNECTOR.wizard.version|escape:html}{else}Nicht gemeldet{/if} Angemeldet als{$NC_CONNECTOR.wizard.username|escape:html}{if $NC_CONNECTOR.wizard.display_name} · {$NC_CONNECTOR.wizard.display_name|escape:html}{/if}
- {if $NC_CONNECTOR.wizard.technical_stage == 'database_details'} + {if $NC_CONNECTOR.wizard.technical_stage == 'mounts' && $NC_CONNECTOR.wizard.directory_selection_ready}
-
Datenbank-Adresse prüfen
-

Die bekannte Reader-Verbindung konnte mit der gespeicherten Adresse nicht bestätigt werden.

- {if $NC_CONNECTOR.wizard.technical_error}
Technische Details

{$NC_CONNECTOR.wizard.technical_error|escape:html}

{/if} -
- -
- - - -
-

-
- - {elseif $NC_CONNECTOR.wizard.technical_stage == 'mounts'} -
- {if $NC_CONNECTOR.wizard.directory_selection_ready} -
Verzeichnisse auswählen
-

Es werden nur Verzeichnisse angezeigt, auf die {$NC_CONNECTOR.wizard.username|escape:html} in Nextcloud Zugriff hat. Mehrere Verzeichnisse können hinzugefügt werden. Bleibt die Auswahl leer, wird automatisch das Stammverzeichnis verwendet.

- -
- Ausgewählt -
- {if $NC_CONNECTOR.wizard.directory_selected|@count > 0} - {foreach from=$NC_CONNECTOR.wizard.directory_selected item=selected_path} -
- - - {if $selected_path}{$selected_path|escape:html}{else}Stammverzeichnis{/if} - -
- {/foreach} - {else} - Keine Auswahl – Stammverzeichnis wird verwendet. - {/if} -
- - Aktueller Ordner/{if $NC_CONNECTOR.wizard.directory_path}{$NC_CONNECTOR.wizard.directory_path|escape:html}{/if} -
- -
-
- - -
- {if $NC_CONNECTOR.wizard.directory_path} -
- - - -
- {/if} -
- -
- {if $NC_CONNECTOR.wizard.directory_children|@count > 0} - {foreach from=$NC_CONNECTOR.wizard.directory_children item=directory_name key=directory_path} -
+
Verzeichnisse auswählen
+
+ Ausgewählt +
+ {if $NC_CONNECTOR.wizard.directory_selected|@count > 0} + {foreach from=$NC_CONNECTOR.wizard.directory_selected item=selected_path} + - - + + {if $selected_path}{$selected_path|escape:html}{else}Stammverzeichnis{/if} + {/foreach} {else} -

Keine Unterordner vorhanden.

+ Noch kein Verzeichnis ausgewählt. {/if}
+ Aktueller Ordner/{if $NC_CONNECTOR.wizard.directory_path}{$NC_CONNECTOR.wizard.directory_path|escape:html}{/if} +
+
- {foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index} - + +
+ {if $NC_CONNECTOR.wizard.directory_path} +
+ + + +
+ {/if} +
+ +
+ {if $NC_CONNECTOR.wizard.directory_children|@count > 0} + {foreach from=$NC_CONNECTOR.wizard.directory_children item=directory_name key=directory_path} +
+ + + +
{/foreach} -

- - {else} -
Speicherort bestätigen
-

Die Datenquelle wurde gefunden. Ein technischer Speicherort konnte nicht automatisch zugeordnet werden.

-
- -
- {foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index} - Speicher {$storage_index+1} - {if $storage.local_mount} - Automatisch erkannt - - {else} - - {/if} - {/foreach} -
-

-
- {/if} + {else} +

Keine Unterordner vorhanden.

+ {/if} +
+
+ +

+
{elseif $NC_CONNECTOR.wizard.technical_complete}
Verbindung benennen
-

Diese Verbindung verwendet ausschließlich den in Schritt 1 angemeldeten Nextcloud-Benutzer.

@@ -196,20 +141,20 @@
{elseif $NC_CONNECTOR.wizard.step == 3} -

Nextcloud ist vorbereitet. Jetzt wird der bevorzugte Piwigo-Zugang geprüft.

+

WebDAV ist vorbereitet. Jetzt wird der Piwigo-Zugang dieser Verbindung geprüft.

{if $NC_CONNECTOR.wizard.api_error}

API-Test fehlgeschlagen: {$NC_CONNECTOR.wizard.api_error|escape:html}

{/if}
- - + +
{elseif $NC_CONNECTOR.wizard.step == 4} -

Fast fertig. Die Verbindung wurde noch nicht angelegt. Erst der letzte Button übernimmt die Einstellungen.

+

Fast fertig. Erst der letzte Button legt die WebDAV-Verbindung an.

Verbindung{$NC_CONNECTOR.wizard.connection_name|escape:html} Nextcloud{$NC_CONNECTOR.wizard.base_url|escape:html} @@ -232,44 +177,16 @@
-
- Ohne Assistent anlegen -

Technische Einrichtung

-

Nur verwenden, wenn die technische Zuordnung bekannt ist. Falsche Pfade können die Synchronisierung auf den falschen Datenbestand richten.

-
- -
- - - - - - - - - - - - - - -
-

Storage-Zuordnungen

Format: Storage-ID | optionales Quellpräfix | lokaler Speicherpfad

-

-
-
-
-

Bestehende Verbindungen

+

Bestehende WebDAV-Verbindungen

{if $NC_CONNECTOR.connection_count > 0} {foreach from=$NC_CONNECTOR.connections item=connection}
- {$connection.display_name|escape:html} · {if $connection.enabled}aktiv{else}{$connection.takeover_state|escape:html}{/if}{if isset($connection.last_sync) && ($connection.last_sync.state == 'error' || $connection.last_sync.state == 'warning')} · Laufzeitproblem{/if} + {$connection.display_name|escape:html} · aktiv{if isset($connection.last_sync) && ($connection.last_sync.state == 'error' || $connection.last_sync.state == 'warning')} · Laufzeitproblem{/if}
- {if $connection.adapter == 'local' && !$connection.enabled}
{/if} -
+
{if isset($connection.last_sync) && $connection.last_sync.timestamp > 0}

Letzter Abgleich: {$connection.last_sync.label|escape:html} · {$connection.last_sync.message|escape:html}

@@ -281,49 +198,21 @@ {else}

Laufzeit: Noch kein Laufstatus für diese Verbindung vorhanden.

{/if} - {if isset($connection.config.api_enabled)} -

Piwigo-Zugang dieser Verbindung: {if $connection.config.api_enabled}eigene API{elseif $connection.fallback_stored}Fallback{else}kein Zugang gespeichert{/if}

- {else} -

Piwigo-Zugang: bestehende Legacy-Konfiguration

- {/if} - {if $connection.verification_checks|@count > 0}
Letzte Prüfung
    {foreach from=$connection.verification_checks item=check}
  • {if $check.ok}✓{else}✗{/if} {$check.name|escape:html}: {$check.detail|escape:html}
  • {/foreach}
{/if} -
Technische Einstellungen - {if $connection.enabled || $connection.takeover_state == 'active'}

Aktive Verbindungen können technisch nicht geändert werden.

{else} -

Nur ändern, wenn die technische Zuordnung bekannt ist.

-
-
- - - - - - - - - - - -
-

Storage-Zuordnungen

Format: Storage-ID | optionales Quellpräfix | lokaler Speicherpfad

-

-
- {/if} -
+

Nextcloud: {$connection.config.nextcloud_url|escape:html}

+

Nextcloud-Benutzer: {$connection.user|escape:html}

+

Ausgewählte Wurzeln: {$connection.storage_count|escape:html}

+

Piwigo-Zugang: {if $connection.config.api_enabled}eigene API{elseif $connection.fallback_stored}Fallback{else}kein Zugang gespeichert{/if}

{/foreach} - {else}

Noch keine Verbindung vorhanden.

{/if} + {else}

Noch keine WebDAV-Verbindung vorhanden.

{/if}
-
Erweiterte Piwigo-Zugänge -

Piwigo API

-
-

Fallback speichern

- {if $NC_CONNECTOR.connection_count > 0}
{/if} -

Einmaliger Fallback-Test

-

Verwendet die eingegebenen Piwigo-Zugangsdaten einmalig und speichert sie nicht.

-

+ {if $NC_CONNECTOR.connection_count > 0} +
Fallback-Zugang einer Verbindung ändern +
+ {/if} {literal} {/literal} -
\ No newline at end of file +