Compare commits

...

113 Commits

Author SHA1 Message Date
Terranom674
a437f5d205 Bump version to 0.9.6.8.22 2026-08-20 18:23:34 +02:00
Terranom674
f2b2845387 Remove obsolete WebDAV precache and derivative prebuild 2026-08-20 18:23:11 +02:00
Terranom674
b54cb039fb release: bump to 0.9.6.8.21 2026-08-20 18:11:58 +02:00
Terranom674
419fe56cde webdav: stop downloading originals for dimension metadata 2026-08-20 18:11:34 +02:00
Terranom674
8cb4abf81d webdav: read image dimensions from Nextcloud metadata 2026-08-20 18:11:07 +02:00
Terranom674
ae70e64c70 release: bump plugin version to 0.9.6.8.20 2026-08-20 18:00:08 +02:00
Terranom674
e5557eba24 webdav: sync original dimensions after piwigo import 2026-08-20 17:59:45 +02:00
Terranom674
5b3eabcb55 webdav: sync original dimensions into piwigo 2026-08-20 17:58:57 +02:00
Terranom674
d56aff39a2 webdav: persist original image dimensions 2026-08-20 17:58:43 +02:00
Terranom674
01243ea022 docs: refresh current development status 2026-08-20 17:42:06 +02:00
Terranom674
88f2949903 docs: update repository information for 0.9.6.8.19 2026-08-20 17:41:35 +02:00
Terranom674
136a6c235e Fix admin dashboard album tile for single album 2026-08-20 17:37:50 +02:00
Terranom674
fb211693ff Preserve connector top-level album access without name collision 2026-08-20 17:32:13 +02:00
Terranom674
b0885a0554 Fix duplicate private album access handler 2026-08-20 17:31:52 +02:00
Terranom674
bc798c5d65 Fix access for flattened WebDAV top-level albums 2026-08-20 17:23:26 +02:00
Terranom674
f958eca176 Bump version to 0.9.6.8.15 2026-08-20 16:54:35 +02:00
Terranom674
93af719143 Flatten empty WebDAV root into gallery root 2026-08-20 16:54:06 +02:00
Terranom674
11183f2a41 Bump version to 0.9.6.8.14 2026-08-20 16:41:27 +02:00
Terranom674
26bb454ba4 Preserve empty WebDAV root path in sync 2026-08-20 16:41:10 +02:00
Terranom674
a195421fed Allow empty WebDAV root path in reconcile 2026-08-20 16:40:47 +02:00
Terranom674
ec55220124 Bump plugin version to 0.9.6.8.13 2026-08-20 16:15:08 +02:00
Terranom674
f5d3b97339 Use absolute runtime path for manual WebDAV sync 2026-08-20 16:14:41 +02:00
Terranom674
a5b4a2329e Bump version to 0.9.6.8.12 2026-08-20 16:13:13 +02:00
Terranom674
97340a517b Run manual WebDAV sync directly from plugin 2026-08-20 16:12:52 +02:00
Terranom674
238c06be33 Bump Bratonien Tools to 0.9.6.8.11 2026-08-20 16:09:54 +02:00
Terranom674
792e8eb7d2 Add manual WebDAV sync button to connector status 2026-08-20 16:09:15 +02:00
Terranom674
d489bd7979 Bump version to 0.9.6.8.10 2026-08-20 16:05:58 +02:00
Terranom674
e8a5f5f0f7 Add NC connector run-now button to admin status 2026-08-20 16:05:36 +02:00
Terranom674
ecc7665ed3 Register manual NC connector run 2026-08-20 16:05:04 +02:00
Terranom674
0d3764f043 Add manual NC connector run and clean status text 2026-08-20 16:04:45 +02:00
Terranom674
81b3b31689 Bump plugin version to 0.9.6.8.9 2026-08-20 15:59:14 +02:00
Terranom674
dcd1b9fb66 Make WebDAV shadow directories writable for runtime materialization 2026-08-20 15:58:57 +02:00
Terranom674
72a89a4636 Bump version to 0.9.6.8.8 2026-08-20 15:48:58 +02:00
Terranom674
2e03cc50d8 Add WebDAV derivative gate diagnostics 2026-08-20 15:48:29 +02:00
Terranom674
d2f171718e Bump version to 0.9.6.8.7 2026-08-20 15:34:33 +02:00
Terranom674
403b1df43f Simplify WebDAV gate to materialize real Piwigo source 2026-08-20 15:34:11 +02:00
Terranom674
38e6ebdb19 Switch WebDAV images to on-demand Piwigo materialization 2026-08-20 15:17:05 +02:00
Terranom674
474acf1de3 Use materialization resolver for WebDAV originals 2026-08-20 15:16:51 +02:00
Terranom674
fe3c7fede2 Add production WebDAV derivative materialization gate 2026-08-20 15:16:30 +02:00
Terranom674
ec801b88d2 Add production WebDAV materialization runtime 2026-08-20 15:15:40 +02:00
Terranom674
9eb38d7dcf Bump version to 0.9.6.8.5 2026-08-20 14:48:15 +02:00
Terranom674
1a7e9a13ee Add parallel WebDAV download stress test 2026-08-20 14:47:51 +02:00
Terranom674
e1d2598e22 Bump version to 0.9.6.8.4 2026-08-20 14:40:46 +02:00
Terranom674
29063b296a Add resource metrics to WebDAV derivative test 2026-08-20 14:40:13 +02:00
Terranom674
210c180e49 Bump test version to 0.9.6.8.3 2026-08-20 14:31:54 +02:00
Terranom674
7ef6c24db4 Move WebDAV derivative test source to Piwigo upload area 2026-08-20 14:31:28 +02:00
Terranom674
1e57e6d300 Bump test patch version to 0.9.6.8.2 2026-08-20 14:25:29 +02:00
Terranom674
57eec81801 Use Piwigo derivative dir for parallel test workspace 2026-08-20 14:24:53 +02:00
Terranom674
0ea01d9a26 Bump plugin version to 0.9.6.8.1 2026-08-20 14:22:36 +02:00
Terranom674
b901b4e887 Bump plugin version for parallel derivative test 2026-08-20 14:20:17 +02:00
Terranom674
bc5a94dc10 Make WebDAV derivative test fully parallel 2026-08-20 14:19:43 +02:00
Terranom674
34a473fa07 Bump plugin version for WebDAV derivative root test 2026-08-20 14:08:13 +02:00
Terranom674
7fc038e107 Fix WebDAV root matching in derivative test path 2026-08-20 14:07:41 +02:00
Terranom674
3b2a8306ad Bump version for WebDAV diagnostic endpoint 2026-08-20 14:03:38 +02:00
Terranom674
dae5f9a610 Add isolated WebDAV derivative path diagnostics 2026-08-20 14:03:04 +02:00
Terranom674
929f18a5f5 Fix duplicate private album access function and bump version 2026-08-20 13:49:33 +02:00
Terranom674
0c45576e2f Bump plugin version for WebDAV derivative test 2026-08-20 13:46:25 +02:00
Terranom674
ee01842cb4 Add parallel WebDAV derivative materialization test endpoint 2026-08-20 13:41:17 +02:00
Terranom674
a872f9238c Merge pull request #52 from Terranom674/fix/0963-clean-webdav-orphans
0.9.6.3: verwaiste WebDAV-Bilder einmalig bereinigen
2026-08-20 11:25:39 +02:00
Terranom674
fdcf045f39 0.9.6.3: WebDAV-Pfadfilter exakt begrenzen 2026-08-20 11:24:56 +02:00
Terranom674
7831a9513f Version 0.9.6.3 2026-08-20 11:24:19 +02:00
Terranom674
fed089db4c 0.9.6.3: Altlasten-Reparatur einmalig vor WebDAV-Sync ausführen 2026-08-20 11:23:54 +02:00
Terranom674
77a295d74f 0.9.6.3: einmalige Reparatur verwaister WebDAV-Bilder 2026-08-20 11:23:33 +02:00
Terranom674
ecabf2da2f Merge PR #51: WebDAV-only connector cleanup
Remove obsolete legacy/local connector paths while preserving the WebDAV connector, shadow tree, placeholder mapping and Piwigo sync bridge.
2026-08-20 11:08:55 +02:00
Terranom674
36349a71b1 Remove takeover state from WebDAV reconcile 2026-08-20 11:06:57 +02:00
Terranom674
df8e1f6cd0 Restore mandatory multi-version and script checks 2026-08-20 11:05:29 +02:00
Terranom674
1a6094f2e2 Record WebDAV-only connector status 2026-08-20 11:02:48 +02:00
Terranom674
40bac7b910 Document WebDAV-only connector architecture 2026-08-20 11:02:25 +02:00
Terranom674
2c10f190c3 Remove legacy connector state from admin controller 2026-08-20 11:01:43 +02:00
Terranom674
d4c195856c Remove legacy fields from WebDAV connection creation 2026-08-20 11:00:52 +02:00
Terranom674
aacbd5af1d Reduce connector model to WebDAV-only state 2026-08-20 11:00:29 +02:00
Terranom674
2e9b3ee2c6 Remove obsolete database credential field from WebDAV secrets 2026-08-20 10:59:50 +02:00
Terranom674
83f44a27b9 Remove obsolete connector WS registration and bump version 2026-08-20 10:59:28 +02:00
Terranom674
1d90754de0 Remove obsolete connector sync simulation endpoint 2026-08-20 10:51:40 +02:00
Terranom674
a4130621d0 Remove legacy local connector install tool 2026-08-20 10:50:58 +02:00
Terranom674
1f1b2dbcf6 Remove legacy connector diagnose tool 2026-08-20 10:50:47 +02:00
Terranom674
173c487474 Remove legacy local connector disable tool 2026-08-20 10:50:34 +02:00
Terranom674
edd86f59f3 Remove legacy connector normalize tool 2026-08-20 10:50:22 +02:00
Terranom674
45ab33d3c4 Remove legacy connector runtime switch 2026-08-20 10:50:14 +02:00
Terranom674
b81207e406 Remove legacy connector cleanup tool 2026-08-20 10:49:59 +02:00
Terranom674
6d33f62599 Remove legacy connector migration tool 2026-08-20 10:49:48 +02:00
Terranom674
5099b154bf Remove legacy connector cutover v2 tool 2026-08-20 10:49:35 +02:00
Terranom674
54616730ca Remove legacy connector cutover tool 2026-08-20 10:49:24 +02:00
Terranom674
09b70ee317 Remove legacy runtime status checks 2026-08-20 10:44:40 +02:00
Terranom674
01f935b769 Keep only WebDAV connection-scoped Piwigo auth helpers 2026-08-20 10:43:46 +02:00
Terranom674
03afa95dc6 Remove legacy connector UI 2026-08-20 10:43:20 +02:00
Terranom674
8588c3c1c2 Remove parallel and legacy reconcile branches 2026-08-20 10:41:14 +02:00
Terranom674
1a47e82997 Make WebDAV the sole connector mode 2026-08-20 10:40:23 +02:00
Terranom674
d2fd72bbc1 Reduce connector model to WebDAV connections 2026-08-20 10:39:58 +02:00
Terranom674
c5331d275e Remove legacy global connector authentication 2026-08-20 10:39:30 +02:00
Terranom674
7c76b4f476 Remove legacy local connector management 2026-08-20 10:39:17 +02:00
Terranom674
edfd91dc29 Remove remaining legacy connector handlers 2026-08-20 10:39:04 +02:00
Terranom674
d44f6c988f Keep connection auth WebDAV-only 2026-08-20 10:38:43 +02:00
Terranom674
471df03c76 Reduce connector wizard to WebDAV only 2026-08-20 10:38:24 +02:00
Terranom674
ea2bac5cfa Remove legacy connector tools 2026-08-20 10:35:47 +02:00
Terranom674
6ba855b76c Remove legacy wizard dispatch branches 2026-08-20 10:35:27 +02:00
Terranom674
792f7bb638 Keep wizard user scope WebDAV-only 2026-08-20 10:35:03 +02:00
Terranom674
557ad16e8a Remove legacy wizard technical flow 2026-08-20 10:34:34 +02:00
Terranom674
76f9089c00 Remove legacy wizard DB bridge 2026-08-20 10:34:21 +02:00
Terranom674
6af48796eb Remove legacy generic DB scope 2026-08-20 10:34:11 +02:00
Terranom674
b331bb087c Remove legacy takeover flow 2026-08-20 10:33:57 +02:00
Terranom674
d4ad87a6df Remove legacy local connector creation 2026-08-20 10:33:48 +02:00
Terranom674
49f30f5f17 Remove legacy Piwigo admin sync helper 2026-08-20 10:33:39 +02:00
Terranom674
ba35da9b68 Remove legacy Piwigo DB checker 2026-08-20 10:33:29 +02:00
Terranom674
d862433bdb Remove legacy selected-fileid DB manifest builder 2026-08-20 10:33:19 +02:00
Terranom674
337b5c6dc7 Remove legacy DB manifest builder 2026-08-20 10:33:04 +02:00
Terranom674
1504e1dc29 Remove legacy DB activity gate 2026-08-20 10:32:43 +02:00
Terranom674
8f0b8aabf8 Remove legacy Nextcloud DB view migration 2026-08-20 10:32:33 +02:00
Terranom674
225542acd9 Remove legacy local runtime cleanup 2026-08-20 10:32:24 +02:00
Terranom674
c152ef9808 Remove legacy local sync runtime 2026-08-20 10:32:13 +02:00
Terranom674
e5b8a01367 Remove legacy local reconcile runtime 2026-08-20 10:31:59 +02:00
Terranom674
247da33e3a Isolate Piwigo sync to WebDAV sites 2026-08-20 10:31:36 +02:00
Terranom674
198b93f4e7 Remove legacy runtime orchestration 2026-08-20 10:30:37 +02:00
57 changed files with 2628 additions and 6829 deletions

View File

@@ -5,8 +5,13 @@ on:
pull_request: pull_request:
jobs: jobs:
syntax: php-syntax:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4', '8.5']
steps: steps:
- name: Repository auschecken - name: Repository auschecken
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -14,7 +19,7 @@ jobs:
- name: PHP installieren - name: PHP installieren
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: '8.2' php-version: ${{ matrix.php }}
coverage: none coverage: none
- name: PHP Syntax pruefen - name: PHP Syntax pruefen
@@ -25,6 +30,43 @@ jobs:
php -l "$file" php -l "$file"
done < <(find . -type f -name '*.php' -print0) 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 - name: Python Syntax pruefen
shell: bash shell: bash
run: | run: |

View File

@@ -1,139 +1,140 @@
# Aktueller Entwicklungsstand # Aktueller Entwicklungsstand
Stand: 19.08.2026 ## Version
## Plugin **Bratonien Tools 0.9.6.8.19**
- Aktuelle Plugin-Version: **0.9.6.1** Zielplattform der produktiven NC-Connector-Synchronisierung ist derzeit **Piwigo 16.4.0**.
- 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.
## Nicht verhandelbare Architekturregeln ## NC Connector
- Nextcloud bleibt die Quelle der Originalbilder. Der NC Connector besitzt einen produktiven Verbindungsweg: **Nextcloud per WebDAV**.
- 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.
## Bestehende Quellenmodi ### Verbindungsmodell
Weiterhin vorhanden: - `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 Verzeichnisse, auf die der angemeldete Nextcloud-Benutzer per WebDAV zugreifen kann.
- Ein leerer `webdav_path` ist ein gültiger Root und bedeutet den Dateibereich des authentifizierten Nextcloud-Benutzers.
- `legacy-view` ### Root-Verhalten
- `user-shares`
- `selected-fileids`
Neuer WebDAV-Modus: Bei `webdav_path=""` wird die technische Nextcloud-Benutzerebene nicht als Piwigo-Album angelegt.
- `webdav-placeholder` Direkte Unterordner des Nextcloud-Benutzerroots werden als Top-Level-Piwigo-Alben gespiegelt. Direkt im Root liegende Einzelbilder verbleiben auf Root-Ebene des verbindungseigenen Shadow Trees und werden durch die bestehende Orphan-Logik behandelt.
Es gibt keine automatische Migration bestehender Verbindungen auf WebDAV. Es gibt keine fest verdrahteten Album-Namen.
## WebDAV-Ablauf in 0.9.6.1 ### Laufzeit
1. Der Assistent prüft Nextcloud über WebDAV und lässt sichtbare Verzeichnisse auswählen. Der gemeinsame Runner verarbeitet WebDAV-Verbindungen in dieser Reihenfolge:
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.
## Piwigo-Synchronisierung 1. `runtime/reconcile-webdav.php`
2. `runtime/repair-webdav-orphans.php`
3. `runtime/cleanup-webdav-piwigo.php`
4. `runtime/sync-webdav.sh` für jede vorhandene WebDAV-Konfiguration
WebDAV verwendet denselben Piwigo-Synchronisationspfad wie der bestehende Connector, aber mit einer verbindungseigenen physischen Site. Die verbindungsspezifischen Runtime-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`. Zustandsdaten liegen unter `/var/lib/bratonien-tools/nc-connector/connection-ID`.
Nach der Dateisynchronisierung laufen die vorhandenen Piwigo-Nacharbeiten, darunter Metadaten-, Integritäts-, Kategorie-, Pfad-, Rang- und Cache-Aktualisierungen sowie der Orphan-Abgleich. Der Reconcile- und Shell-Pfad unterstützt den leeren WebDAV-Root ausdrücklich und gibt ihn als `--root ""` an den Platzhalter-Builder weiter.
Die direkte produktive Synchronisierung ist derzeit ausdrücklich auf **Piwigo 16.4.0** abgestimmt. ### Platzhalterquelle und Shadow Tree
## WebDAV-Bildausgabe und Derivate `runtime/lib/build_webdav_placeholder_source.py` bildet die Nextcloud-Inhalte als lokale Platzhalterquelle ab. Für Bilder existieren physische 1×1-Platzhalterdateien; die Originale werden dabei nicht dauerhaft heruntergeladen.
Wichtige Dateien: `runtime/lib/shadow_tree.py` erzeugt atomar die Dateisystemstruktur, die Piwigo als physische Site sieht. Shadow-Verzeichnisse werden so angelegt, dass der Webserver die für die On-Demand-Materialisierung nötigen Dateisystemoperationen ausführen kann.
- `include/webdav_image_runtime.inc.php` Zuordnung Piwigo-Bild ↔ WebDAV-Quelle, Preview-/Derivat-Helfer und URL-Filter. Jede WebDAV-Verbindung besitzt ihre eigene Piwigo-Site. Es gibt keinen zusätzlichen Sync auf Site 1.
- `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 Frontend-Hook führt keine Bildgenerierung mehr aus. Damit bleibt ein Fehler in GD/Imagick/External ImageMagick auf den Hintergrundlauf begrenzt. ### Piwigo-Synchronisierung
## Löschen einer WebDAV-Verbindung seit 0.9.6.1 `runtime/lib/piwigo-sync.php` bevorzugt `bratonien.nc.syncProductive` für Piwigo 16.4.0. Wenn für die Verbindung keine Piwigo-API eingerichtet ist, wird der gespeicherte Administrator-/Webmaster-Benutzername/Passwort-Fallback verwendet und Piwigos nativer `site_update` ausgeführt.
Eine gelöschte WebDAV-Verbindung darf keine weiterhin sichtbaren oder über alte Piwigo-Datensätze erreichbaren Bilder hinterlassen. Danach wird `bratonien.nc.syncOrphans` für die konkrete WebDAV-Site ausgeführt.
Beim Löschen über die Oberfläche: Der Shadow Tree stellt Piwigo ausschließlich die physische Album-/Bildstruktur bereit. Albumdatensätze, Bilddatensätze, Bild-Kategorie-Verknüpfungen und Piwigo-Derivate bleiben Aufgabe von Piwigo.
- werden die zur Verbindung gehörende Piwigo-Site, ihre Alben und Bilddatensätze entfernt; ### On-Demand-Materialisierung vor `i.php`
- 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. Die produktive WebDAV-Derivaterzeugung sitzt vor Piwigos `i.php`.
## Gemeinsamer Runner `include/webdav_materialize_runtime.inc.php` löst aus dem Piwigo-Bildpfad die konkrete Verbindung, den ausgewählten Nextcloud-Root und den relativen WebDAV-Pfad auf.
`runtime/run-all.sh` verarbeitet die Schritte in kontrollierter Reihenfolge: Für ein noch nicht vorhandenes Derivat läuft `webdav-derivative.php`:
1. lokale Verbindungen reconciliieren; 1. Zugriffsprüfung für das Piwigo-Bild;
2. WebDAV-Verbindungen reconciliieren; 2. Download des Originals aus Nextcloud in eine temporäre Datei;
3. verwaiste WebDAV-Piwigo-Inhalte bereinigen; 3. temporäres Bereitstellen des Originals am exakten von Piwigo erwarteten Quellpfad;
4. verwaiste allgemeine Runtime-Dateien bereinigen; 4. Aufruf des nativen Piwigo-`i.php`;
5. aktive WebDAV-Verbindungen synchronisieren; 5. Piwigo erzeugt sein normales Derivat;
6. aktive lokale Verbindungen synchronisieren. 6. Wiederherstellung des Shadow-/Platzhalterzustands;
7. Auslieferung des Piwigo-Derivats.
Der vorhandene systemd-Timer ruft direkt die Runtime aus dem installierten Plugin auf. Das Original verbleibt in Nextcloud. Es gibt keine dauerhafte vollständige Originalkopie in Piwigo.
## Status- und UI-Regeln ### Orphan-Logik
- Hintergrund-Polling lädt die Admin-Seite nicht neu. Direkt im WebDAV-Site-Root liegende Bilder werden nicht künstlich in ein Album gezwungen. Sie werden durch `bratonien.nc.syncOrphans` als Piwigo-Orphans synchronisiert.
- 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 `runtime/repair-webdav-orphans.php` läuft vor den verbindungsspezifischen Syncs und repariert historische WebDAV-Orphan-Zustände.
- Connector-Zugangsdaten werden verschlüsselt gespeichert. ### Private Top-Level-Alben
- 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 Da die technische Nextcloud-Benutzerebene nicht mehr als Elternalbum existiert, können WebDAV-Unterordner echte private Top-Level-Piwigo-Alben sein. Beim Connector-Fallback-Sync sorgt `bratonien_tools_preserve_connector_top_level_access()` dafür, dass der verwendete Piwigo-Administrator auf diese privaten Top-Level-Alben zugreifen kann. Nach Änderungen wird der Benutzer-Cache invalidiert.
Bereits umgesetzt: Die allgemeine bestehende Funktion `bratonien_tools_preserve_private_album_access()` in `include/album_shares.inc.php` bleibt davon getrennt.
- WebDAV-Verbindungsassistent; ### Administrations-Dashboard
- 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: Piwigo 16.4.0 zeigt die Album-Kachel im Standardtemplate nur bei `NB_ALBUMS > 1` an. Dadurch verschwand die Kachel nach dem korrekten Entfernen der künstlichen Nextcloud-Benutzerebene, sobald nur noch ein echtes Album vorhanden war.
- vollständiger Derivataufbau für alle in der Installation vorkommenden Standard- und Custom-Größen; Bratonien Tools setzt für die Verwaltungsübersicht einen Template-Prefilter und ändert ausschließlich diese Anzeigebedingung auf `NB_ALBUMS > 0`. Die Albumzahl selbst stammt weiterhin unverändert aus Piwigos `get_pwg_general_statitics()`.
- 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 ### Bildauslieferung
Die aktuelle Versionslinie ist **0.9.6.x**. Weitere Sprünge auf eine neue Minor-/Major-Linie erfolgen nur bewusst und ausdrücklich. Fehlende Piwigo-Derivate werden durch den On-Demand-Gate erzeugt. Bereits vorhandene Derivate werden direkt aus Piwigos eigenem Derivat-Cache ausgeliefert.
Für direkte Originalanforderungen existiert weiterhin die serverseitige WebDAV-Originalauslieferung. Nextcloud-Zugangsdaten werden dem Browser nicht offengelegt.
### Löschen
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.
### Diagnose
Der WebDAV-Derivative-Gate schreibt Request-bezogene `[BRAT-WD ...]`-Diagnoseeinträge. Der Verbindungsstatus wird pro Verbindung geführt. Runtime-Probleme lassen sich zusätzlich über `bratonien-nc-connector.service` nachvollziehen.
## Aktuell relevante Dateien
- `main.inc.php` Plugin-Hooks, Connector-Sync-Hilfen, Dashboard-Prefilter
- `include/webdav_materialize_runtime.inc.php` WebDAV-Quellauflösung und Materialisierung
- `webdav-derivative.php` On-Demand-Gate vor Piwigos `i.php`
- `webdav-image.php` direkte WebDAV-Originalauslieferung
- `include/nc_productive_ws.inc.php` direkter Piwigo-Core-Sync für 16.4.0
- `include/nc_orphan_ws.inc.php` Root-Orphan-Synchronisierung
- `runtime/reconcile-webdav.php` Runtime-Reconcile einschließlich leerem WebDAV-Root
- `runtime/repair-webdav-orphans.php` Orphan-Reparatur
- `runtime/cleanup-webdav-piwigo.php` Bereinigung verwaister Piwigo-Sites
- `runtime/sync-webdav.sh` verbindungsspezifischer Sync
- `runtime/run-all.sh` gemeinsamer Runner
- `runtime/lib/build_webdav_placeholder_source.py` WebDAV-Scan und Platzhalterquelle
- `runtime/lib/shadow_tree.py` atomarer Shadow Tree
- `runtime/lib/piwigo-sync.php` Piwigo-API/Fallback-Sync
## Nicht mehr das produktive WebDAV-Modell
Nicht mehr maßgeblich für die produktive WebDAV-Derivaterzeugung ist das frühere Modell, bei dem Bild-URLs einfach auf einen separaten WebDAV-Bildstream umgebogen wurden oder der Connector selbst einen parallelen vollständigen Derivatbestand verwalten sollte.
Maßgeblich ist: **physischer Platzhalter + On-Demand-Original + natives Piwigo-`i.php` + Wiederherstellung des Platzhalters**.
## Prüfpflicht vor Merge
Die GitHub-Actions-Konfiguration prüft mindestens:
- PHP-Syntax für 8.2, 8.3, 8.4 und 8.5
- doppelte globale `bratonien_tools_*`-Funktionen
- JavaScript-Syntax
- Python-Syntax/Compile
- Shell-Syntax mit `bash -n`

206
README.md
View File

@@ -2,7 +2,7 @@
Modulares Piwigo-Plugin für Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo. 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.8.19**
## Grundprinzip ## Grundprinzip
@@ -12,31 +12,44 @@ Für die Administration gilt: Der normale Nutzer sieht Aufgaben, Entscheidungen
## NC Connector ## NC Connector
Der NC Connector synchronisiert ausdrücklich ausgewählte Nextcloud-Inhalte mit Piwigo. Der NC Connector bindet ausdrücklich ausgewählte Nextcloud-Inhalte per WebDAV an Piwigo an. Nextcloud bleibt dabei die Quelle der Originaldateien; Piwigo verwaltet Alben, Bilder und seine eigenen Derivate.
### Datenmodell ### Datenmodell
- Nextcloud bleibt die Quelle der Originaldateien. - Nextcloud bleibt die verbindliche Quelle der Originaldateien.
- Originalbilder werden nicht dauerhaft in eine zweite Bibliothek unter Piwigo kopiert. - Originalbilder werden nicht dauerhaft als zweite vollständige Bibliothek unter Piwigo gespeichert.
- Ein Shadow Tree erzeugt die für Piwigo benötigte Ordnerstruktur. - Eine lokale Platzhalterquelle enthält physische 1×1-Dateien, damit Piwigo die Bilder regulär als Dateisysteminhalte erkennen kann.
- Ein Shadow Tree bildet daraus die von Piwigo erwartete Galerie-Ordnerstruktur.
- Ordner werden als physische Piwigo-Alben synchronisiert. - Ordner werden als physische Piwigo-Alben synchronisiert.
- Root-Dateien können als Piwigo-Orphans registriert werden. - Direkt im ausgewählten Nextcloud-Root liegende Einzelbilder werden über die vorhandene Orphan-Logik behandelt.
- Neu importierte physische Connector-Alben werden privat angelegt. - 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 ### Nextcloud-Root ohne künstliches Benutzeralbum
Der bestehende Connector unterstützt weiterhin: Ein leerer `webdav_path` ist ein gültiger Root und bedeutet den Dateibereich des authentifizierten Nextcloud-Benutzers.
- `legacy-view` Dabei wird der Nextcloud-Benutzername nicht als künstliche Album-Ebene angelegt. Beispiel:
- `user-shares`
- `selected-fileids`
Diese Modi bleiben erhalten. Es gibt keine automatische Migration auf WebDAV. ```text
Nextcloud
/
├── 400 Auswahl/
├── Zweites Album/
├── einzelbild-1.jpg
└── einzelbild-2.jpg
```
### WebDAV-Modus wird in Piwigo logisch als
Der neue Endnutzerpfad verwendet `source_mode=webdav-placeholder` und `adapter=remote`. ```text
400 Auswahl
Zweites Album
```
abgebildet. Die direkt im Root liegenden Einzelbilder werden separat durch die Orphan-Synchronisierung erfasst.
### Voraussetzungen
Benötigt werden nur: Benötigt werden nur:
@@ -45,72 +58,67 @@ Benötigt werden nur:
- normaler WebDAV-Zugriff auf die Inhalte dieses Benutzers; - normaler WebDAV-Zugriff auf die Inhalte dieses Benutzers;
- eine normale Piwigo-Installation mit ihrer vorhandenen PHP-/Bildverarbeitungsumgebung. - eine normale Piwigo-Installation mit ihrer vorhandenen PHP-/Bildverarbeitungsumgebung.
Nicht vorausgesetzt werden: Nicht benötigt werden Nextcloud-Datenbankzugriff, `occ`-Adminzugriff, Storage-IDs, Backend-Pfade, zusätzliche Host-Mounts, FUSE, davfs oder rclone.
- PostgreSQL-Zugriff auf Nextcloud; ### WebDAV-Scan und Shadow Tree
- `occ`-Adminzugriff;
- Storage-IDs oder Backend-Pfade;
- zusätzliche Host-Mounts;
- FUSE;
- davfs;
- rclone.
### WebDAV-Ablauf
1. Der Assistent prüft Nextcloud und liest die sichtbaren Verzeichnisse per WebDAV. 1. Der Assistent prüft Nextcloud und liest die sichtbaren Verzeichnisse per WebDAV.
2. Ausgewählte Verzeichnisse werden rekursiv per PROPFIND eingelesen. 2. Ausgewählte Verzeichnisse werden rekursiv per PROPFIND eingelesen.
3. Ordner, Dateiname, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst. 3. Ordner, Dateiname, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
4. `runtime/lib/build_webdav_placeholder_source.py` erzeugt eine lokale Platzhalterquelle ohne dauerhafte Originalkopie. 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. 5. `runtime/lib/shadow_tree.py` baut atomar den verbindungseigenen Shadow Tree.
6. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID`. 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. 7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert.
8. `runtime/lib/precache-webdav-previews.php` erzeugt vorbereitete Arbeitsbilder als JPEG beziehungsweise PNG mit maximal 4096 px Kantenlänge. 8. Piwigos Dateisynchronisierung erhält ausschließlich diese verbindungseigene Site.
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. ### On-Demand-Bildauslieferung und Piwigo-Derivate
11. Wenn das Original benötigt wird, streamt `webdav-image.php` es serverseitig direkt aus Nextcloud. Zugangsdaten werden dem Browser nicht offengelegt.
Die produktive Bildauslieferung sitzt vor Piwigos `i.php`.
`include/webdav_materialize_runtime.inc.php` erkennt anhand des Piwigo-Bildpfads die zugehörige WebDAV-Verbindung, den Nextcloud-Root und den relativen WebDAV-Pfad. Der Hook `get_derivative_url` leitet fehlende WebDAV-Derivate an `webdav-derivative.php` um.
Wenn ein benötigtes Piwigo-Derivat noch nicht existiert:
1. `webdav-derivative.php` prüft den Piwigo-Zugriff auf das Bild.
2. Das Original wird temporär direkt aus Nextcloud geladen.
3. Die physische 1×1-Platzhalterdatei bzw. der Shadow-Link am von Piwigo erwarteten Quellpfad wird für die Dauer der Erzeugung ersetzt.
4. Piwigos eigenes `i.php` erzeugt das normale Piwigo-Derivat.
5. Danach wird der temporäre Originalzustand entfernt und der Platzhalter-/Shadow-Zustand wiederhergestellt.
6. Das von Piwigo erzeugte Derivat wird ausgeliefert und künftig normal aus Piwigos Derivat-Cache verwendet.
Damit bleibt Piwigo für Größen, Derivate und Lazy-Loading verantwortlich. Der Connector hält keinen parallelen eigenen vollständigen Derivat-Cache vor.
Für Fälle, in denen Piwigo direkt die Originalquelle benötigt, steht weiterhin die serverseitige WebDAV-Auslieferung zur Verfügung. Nextcloud-Zugangsdaten werden nicht an den Browser weitergegeben.
### Piwigo-Synchronisierung ### 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 ausschließlich die Dateisystemdarstellung, die Piwigo für Alben und Bilder benötigt.
Nach dem Dateiabgleich laufen die vorhandenen Nacharbeiten, darunter: `runtime/lib/piwigo-sync.php` bevorzugt die Webservice-Methode `bratonien.nc.syncProductive`. Ist keine Piwigo-API für die Verbindung eingerichtet, wird der vorhandene Administrator-/Webmaster-Benutzername/Passwort-Fallback verwendet und Piwigos nativer `site_update` ausgeführt.
- Metadaten-Synchronisierung; Nach der regulären Dateisynchronisierung läuft `bratonien.nc.syncOrphans` für direkt im Site-Root liegende Einzelbilder.
- Bild- und Kategorieintegrität;
- Uppercats/Kategoriestruktur;
- globale Ränge;
- Pfadpflege;
- Rating-Score;
- Benutzer-Cache-Invalidierung;
- Orphan-Abgleich.
Die direkte produktive Synchronisierung ist aktuell ausdrücklich auf **Piwigo 16.4.0** abgestimmt. Die produktive Synchronisierung ist aktuell auf **Piwigo 16.4.0** abgestimmt.
### Fallback-Zugang ### Private Top-Level-Alben
Piwigo wird API-first angesprochen. Ist für eine Verbindung keine API konfiguriert, kann Benutzername/Passwort als Fallback verwendet werden. Durch das Überspringen der technischen Nextcloud-Benutzerebene können WebDAV-Unterordner zu echten Top-Level-Piwigo-Alben werden. Für den Connector-Sync wird deshalb der Zugriff des verwendeten Piwigo-Administrators auf neu erzeugte private Top-Level-Alben erhalten und der Benutzer-Cache anschließend invalidiert.
Der Assistent prüft Fallback-Zugangsdaten vor dem Speichern. Eine fehlgeschlagene Prüfung schließt oder leert den Assistenten nicht. ### Administrations-Dashboard
Piwigo 16.4.0 blendet seine Album-Statistik standardmäßig bei exakt einem Album aus (`NB_ALBUMS > 1`). Bratonien Tools passt ausschließlich diese Dashboard-Anzeige auf `NB_ALBUMS > 0` an. Dadurch wird auch ein einzelnes korrekt angelegtes Album in der Verwaltungsübersicht angezeigt.
### Statusanzeige ### 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. Die Administration zeigt Laufzeitstatus und Fehler pro Verbindung. Fehlerdetails bleiben der betroffenen Verbindung zugeordnet.
Fehler werden nach Prozessschritt getrennt dargestellt, unter anderem für WebDAV-Einlesen, Piwigo-Synchronisierung, Preview-Erzeugung und Derivat-Erzeugung.
### Löschen einer Verbindung ### 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 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.
- 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.
### Runtime ### 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.timer`
- `bratonien-nc-connector.service` - `bratonien-nc-connector.service`
@@ -118,25 +126,18 @@ 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`. 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 gemeinsame Lauf besteht aktuell aus:
1. lokale Verbindungen reconciliieren; 1. `runtime/reconcile-webdav.php` gespeicherte Verbindungen in Runtime-Konfigurationen überführen;
2. WebDAV-Verbindungen reconciliieren; 2. `runtime/repair-webdav-orphans.php` vorhandene WebDAV-Orphan-Zustände reparieren;
3. verwaiste WebDAV-Piwigo-Inhalte bereinigen; 3. `runtime/cleanup-webdav-piwigo.php` verwaiste WebDAV-Piwigo-Daten bereinigen;
4. allgemeine verwaiste Runtime-Dateien bereinigen; 4. `runtime/sync-webdav.sh` jede vorhandene WebDAV-Verbindung synchronisieren.
5. WebDAV-Verbindungen synchronisieren;
6. lokale Verbindungen synchronisieren. Der verbindungsspezifische Lauf erzeugt Platzhalterquelle und Shadow Tree, führt Piwigos Sync und Orphan-Sync aus und enthält weiterhin die vorhandenen Preview-/Derivative-Schritte. Die produktive Auslieferung fehlender WebDAV-Derivate erfolgt jedoch über den On-Demand-Gate vor `i.php`.
## Bildcache ## Bildcache
Bratonien Tools kann: 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.
- 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.
## Wasserzeichenverwaltung ## Wasserzeichenverwaltung
@@ -171,14 +172,7 @@ Alben können in Bratonien Tools zwischen öffentlich und privat umgeschaltet we
## Geschützte Albumfreigaben ## Geschützte Albumfreigaben
Private Alben können über eigene Freigabelinks geteilt werden: 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.
- 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.
## Erweiterte Bildnavigation ## Erweiterte Bildnavigation
@@ -186,39 +180,33 @@ Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges B
## Selbstaktualisierung ## Selbstaktualisierung
Der integrierte Updater: 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.
- 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.
## Wichtige Dateien und Verzeichnisse ## Wichtige Dateien und Verzeichnisse
- `main.inc.php` Plugin-Einstieg und Runtime-Hooks - `main.inc.php` Plugin-Einstieg, Hooks, Connector-Sync-Hilfen und Dashboard-Prefilter
- `admin.php` zentraler Admin-Controller - `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.inc.php` Verbindungsassistent
- `include/nc_connector_wizard_webdav_flow.inc.php` WebDAV-first-Wizardablauf - `include/nc_connector_wizard_webdav_flow.inc.php` WebDAV-Wizardablauf
- `include/nc_connector_delete_safe.inc.php` sicheres Löschen einschließlich WebDAV-Piwigo-Inhalten - `include/nc_connector_delete_safe.inc.php` Löschen einschließlich WebDAV-Piwigo-Inhalten
- `include/nc_productive_ws.inc.php` produktiver Piwigo-Dateisync - `include/nc_productive_ws.inc.php` direkter Piwigo-Core-Dateisync für Piwigo 16.4.0
- `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung - `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung der konkreten WebDAV-Site
- `include/webdav_image_runtime.inc.php` WebDAV-Bildzuordnung, Preview-/Derivathilfen und URL-Filter - `include/webdav_materialize_runtime.inc.php` WebDAV-Quellauflösung und On-Demand-Materialisierung
- `webdav-image.php` berechtigungsgeprüfter WebDAV-Bildstream - `include/webdav_image_runtime.inc.php` vorhandene WebDAV-Bildzuordnung/Originalauslieferung
- `runtime/reconcile-webdav.php` WebDAV-Runtime-Reconcile - `webdav-derivative.php` Gate vor `i.php` für fehlende WebDAV-Derivate
- `runtime/cleanup-webdav-piwigo.php` Bereinigung gelöschter/verwaister WebDAV-Verbindungen in Piwigo - `webdav-image.php` serverseitige WebDAV-Originalauslieferung
- `runtime/reconcile-webdav.php` WebDAV-Runtime-Reconcile, einschließlich gültigem leerem Root
- `runtime/repair-webdav-orphans.php` Reparatur vorhandener Orphan-Zustände
- `runtime/cleanup-webdav-piwigo.php` Bereinigung verwaister WebDAV-Piwigo-Sites
- `runtime/sync-webdav.sh` Ablauf einer WebDAV-Verbindung - `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/build_webdav_placeholder_source.py` rekursiver WebDAV-Scan und physische Platzhalterquelle
- `runtime/lib/precache-webdav-previews.php` vorbereitete JPEG-/PNG-Arbeitsbilder - `runtime/lib/shadow_tree.py` atomarer, Piwigo-kompatibler Shadow Tree
- `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 - `runtime/lib/piwigo-sync.php` Piwigo-Sync mit API/Fallback
- `main-cache-build.php` allgemeiner Piwigo-Derivat-/Cache-Builder - `main-cache-build.php` allgemeiner Piwigo-Derivat-/Cache-Builder
- `include/self_update.inc.php` Self-Updater - `include/self_update.inc.php` Self-Updater
- `include/album_shares.inc.php` geschützte Albumfreigaben - `include/album_shares.inc.php` geschützte Albumfreigaben und private Albumzugriffe
- `include/public_selection.inc.php` Fotoauswahl - `include/public_selection.inc.php` Fotoauswahl
- `include/batch_titles.inc.php` fortlaufende Titel - `include/batch_titles.inc.php` fortlaufende Titel
- `include/watermark_*.inc.php` Wasserzeichen-Engine - `include/watermark_*.inc.php` Wasserzeichen-Engine
@@ -231,16 +219,8 @@ Der integrierte Updater:
- Connector-Zugangsdaten werden verschlüsselt gespeichert; - Connector-Zugangsdaten werden verschlüsselt gespeichert;
- Nextcloud-Zugangsdaten werden verbindungseigen gespeichert und nur serverseitig verwendet; - Nextcloud-Zugangsdaten werden verbindungseigen gespeichert und nur serverseitig verwendet;
- Wizard-Geheimnisse werden nicht im Browser-Web-Storage persistiert; - Wizard-Geheimnisse werden nicht im Browser-Web-Storage persistiert;
- SQL-View-Namen werden vor Verwendung validiert;
- produktive Piwigo-API ist versionsgebunden; - produktive Piwigo-API ist versionsgebunden;
- Originalbilder werden vom Connector nicht gelöscht; - Originalbilder werden vom Connector nicht gelöscht;
- WebDAV-Originalbilder werden nicht dauerhaft lokal gespeichert; - WebDAV-Originalbilder werden nur bei Bedarf temporär lokal materialisiert;
- temporäre Originale werden nach der Piwigo-Derivaterzeugung wieder entfernt;
- Update-Pakete werden an Commit und Hash gebunden. - 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).

View File

@@ -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['last_sync'] = bratonien_tools_nc_connector_connection_last_status($nc_connection);
$nc_connection['display_name'] = $nc_connection['name']; $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'])) if (!empty($nc_connection['fallback_stored']))
{ {
$nc_connection['display_name'] .= ' · Fallback gespeichert'; $nc_connection['display_name'] .= ' · Fallback gespeichert';
$nc_connection['verification_checks'][] = array(
'name' => 'Piwigo-Fallback',
'ok' => true,
'detail' => 'Benutzername/Passwort verschluesselt gespeichert',
);
} }
} }
unset($nc_connection); unset($nc_connection);
@@ -159,10 +143,6 @@ $nc_system_defaults = array(
'last_run_error_detail' => '', 'last_run_error_detail' => '',
'next_run_timestamp' => 0, 'next_run_timestamp' => 0,
'next_run_label' => 'Nicht verfügbar', '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_connector['system'] = array_merge(
$nc_system_defaults, $nc_system_defaults,

View File

@@ -4,15 +4,6 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); 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() function bratonien_tools_nc_connector_table()
{ {
if (function_exists('bratonien_tools_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, id int(11) NOT NULL AUTO_INCREMENT,
connection_key varchar(64) NOT NULL, connection_key varchar(64) NOT NULL,
name varchar(255) NOT NULL, name varchar(255) NOT NULL,
adapter enum('local','remote') NOT NULL DEFAULT 'local', adapter enum('remote') NOT NULL DEFAULT 'remote',
enabled tinyint(1) NOT NULL DEFAULT 0, enabled tinyint(1) NOT NULL DEFAULT 1,
takeover_state enum('imported','verified','active','disabled') NOT NULL DEFAULT 'imported',
config_json mediumtext NOT NULL, config_json mediumtext NOT NULL,
secret_blob mediumtext DEFAULT NULL, secret_blob mediumtext DEFAULT NULL,
created datetime NOT NULL, created datetime NOT NULL,
@@ -64,139 +54,59 @@ function bratonien_tools_nc_connector_secret_key()
function bratonien_tools_nc_connector_encrypt_secret($plain) function bratonien_tools_nc_connector_encrypt_secret($plain)
{ {
$plain = (string)$plain; $plain = (string)$plain;
if ($plain === '') if ($plain === '') return '';
{ if (!function_exists('openssl_encrypt')) throw new RuntimeException('OpenSSL wird zum sicheren Speichern der Connector-Zugangsdaten benötigt.');
return '';
}
if (!function_exists('openssl_encrypt'))
{
throw new RuntimeException('OpenSSL wird zum sicheren Speichern der Connector-Zugangsdaten benoetigt.');
}
$iv = random_bytes(12); $iv = random_bytes(12);
$tag = ''; $tag = '';
$cipher = openssl_encrypt( $cipher = openssl_encrypt($plain, 'aes-256-gcm', bratonien_tools_nc_connector_secret_key(), OPENSSL_RAW_DATA, $iv, $tag);
$plain, if ($cipher === false) throw new RuntimeException('Connector-Zugangsdaten konnten nicht verschlüsselt werden.');
'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.');
}
return base64_encode(json_encode(array( return base64_encode(json_encode(array(
'v' => 1, 'v'=>1,
'iv' => base64_encode($iv), 'iv'=>base64_encode($iv),
'tag' => base64_encode($tag), 'tag'=>base64_encode($tag),
'data' => base64_encode($cipher), 'data'=>base64_encode($cipher),
))); )));
} }
function bratonien_tools_nc_connector_decrypt_secret($blob) function bratonien_tools_nc_connector_decrypt_secret($blob)
{ {
$blob = trim((string)$blob); $blob = trim((string)$blob);
if ($blob === '') if ($blob === '') return '';
{ if (!function_exists('openssl_decrypt')) throw new RuntimeException('OpenSSL wird zum Lesen der Connector-Zugangsdaten benötigt.');
return '';
}
if (!function_exists('openssl_decrypt'))
{
throw new RuntimeException('OpenSSL wird zum Lesen der Connector-Zugangsdaten benoetigt.');
}
$outer = base64_decode($blob, true); $outer = base64_decode($blob, true);
$payload = is_string($outer) ? json_decode($outer, true) : null; $payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) throw new RuntimeException('Gespeicherte Connector-Zugangsdaten haben ein unbekanntes Format.');
{
throw new RuntimeException('Gespeicherte Connector-Zugangsdaten haben ein unbekanntes Format.');
}
$iv = base64_decode((string)($payload['iv'] ?? ''), true); $iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true); $tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true); $cipher = base64_decode((string)($payload['data'] ?? ''), true);
if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) throw new RuntimeException('Gespeicherte Connector-Zugangsdaten sind beschädigt.');
{
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.');
}
$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; return (string)$plain;
} }
function bratonien_tools_nc_connector_read_config($path) function bratonien_tools_nc_connector_is_webdav(array $row)
{ {
$config = array(); $config = isset($row['config']) && is_array($row['config']) ? $row['config'] : array();
if (!is_readable($path)) return (string)($row['adapter'] ?? '') === 'remote' && (string)($config['source_mode'] ?? '') === 'webdav-placeholder';
{
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;
} }
function bratonien_tools_nc_connector_connections() function bratonien_tools_nc_connector_connections()
{ {
bratonien_tools_nc_connector_ensure_table(); bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_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(); $connections = array();
while ($row = pwg_db_fetch_assoc($result)) while ($row = pwg_db_fetch_assoc($result))
{ {
$config = json_decode((string)$row['config_json'], true); $config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
{
$config = array();
}
$verification = isset($config['verification']) && is_array($config['verification']) ? $config['verification'] : array();
$has_fallback = false; $has_fallback = false;
try try
@@ -205,8 +115,7 @@ function bratonien_tools_nc_connector_connections()
$credentials = json_decode($plain, true); $credentials = json_decode($plain, true);
if (is_array($credentials)) if (is_array($credentials))
{ {
$has_fallback = trim((string)($credentials['piwigo_user'] ?? '')) !== '' $has_fallback = trim((string)($credentials['piwigo_user'] ?? '')) !== '' && (string)($credentials['piwigo_password'] ?? '') !== '';
&& (string)($credentials['piwigo_password'] ?? '') !== '';
} }
} }
catch (Throwable $e) catch (Throwable $e)
@@ -218,17 +127,9 @@ function bratonien_tools_nc_connector_connections()
$row['id'] = (int)$row['id']; $row['id'] = (int)$row['id'];
$row['enabled'] = (bool)$row['enabled']; $row['enabled'] = (bool)$row['enabled'];
$row['config'] = $config; $row['config'] = $config;
$row['host'] = isset($config['host']) ? (string)$config['host'] : ''; $row['user'] = (string)($config['nextcloud_access_user'] ?? $config['access_user'] ?? '');
$row['database'] = isset($config['database']) ? (string)$config['database'] : ''; $row['storage_count'] = isset($config['roots']) && is_array($config['roots']) ? count($config['roots']) : 0;
$row['user'] = isset($config['user']) ? (string)$config['user'] : '';
$row['source_view'] = isset($config['source_view']) ? (string)$config['source_view'] : '';
$row['storage_count'] = isset($config['storages']) && is_array($config['storages']) ? count($config['storages']) : 0;
$row['fallback_stored'] = $has_fallback; $row['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; $connections[] = $row;
} }
@@ -240,233 +141,24 @@ function bratonien_tools_nc_connector_connection($id, $with_secret = false)
bratonien_tools_nc_connector_ensure_table(); bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table(); $table = bratonien_tools_nc_connector_table();
$id = (int)$id; $id = (int)$id;
if ($id <= 0) if ($id <= 0) return null;
{
return null;
}
$columns = 'id, connection_key, name, adapter, enabled, takeover_state, config_json, created, updated'; $columns = 'id, connection_key, name, adapter, enabled, config_json, created, updated';
if ($with_secret) if ($with_secret) $columns .= ', secret_blob';
{ $result = pwg_query("SELECT $columns FROM `$table` WHERE id = $id AND adapter='remote' LIMIT 1");
$columns .= ', secret_blob'; if (!pwg_db_num_rows($result)) return null;
}
$result = pwg_query("SELECT $columns FROM `$table` WHERE id = $id LIMIT 1");
if (!pwg_db_num_rows($result))
{
return null;
}
$row = pwg_db_fetch_assoc($result); $row = pwg_db_fetch_assoc($result);
$row['id'] = (int)$row['id']; $row['id'] = (int)$row['id'];
$row['enabled'] = (bool)$row['enabled']; $row['enabled'] = (bool)$row['enabled'];
$row['config'] = json_decode((string)$row['config_json'], true); $row['config'] = json_decode((string)$row['config_json'], true);
if (!is_array($row['config'])) if (!is_array($row['config']) || (string)($row['config']['source_mode'] ?? '') !== 'webdav-placeholder') return null;
{
$row['config'] = array();
}
return $row; 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() function bratonien_tools_nc_connector_status()
{ {
$connections = bratonien_tools_nc_connector_connections(); $connections = bratonien_tools_nc_connector_connections();
$legacy_config = '/etc/piwigo-sync/piwigo.conf';
$legacy_present = is_readable($legacy_config);
$active_count = 0; $active_count = 0;
foreach ($connections as $connection) foreach ($connections as $connection)
{ {
@@ -474,11 +166,9 @@ function bratonien_tools_nc_connector_status()
} }
return array( return array(
'phase' => 'native-runtime', 'phase'=>'webdav',
'legacy_config_path' => $legacy_config, 'connections'=>$connections,
'legacy_present' => $legacy_present, 'connection_count'=>count($connections),
'connections' => $connections, 'active_count'=>$active_count,
'connection_count' => count($connections),
'active_count' => $active_count,
); );
} }

View File

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

View File

@@ -4,196 +4,52 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); 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) 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'] ?? ''); $plain = bratonien_tools_nc_connector_decrypt_secret($connection['secret_blob'] ?? '');
$decoded = json_decode($plain, true); $decoded = json_decode($plain, true);
if (is_array($decoded) && array_key_exists('db_password', $decoded)) if (!is_array($decoded))
{ {
return array( throw new RuntimeException('WebDAV-Zugangsdaten haben ein unbekanntes Format.');
'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'] ?? ''),
);
} }
return array( return array(
'v'=>3, 'v'=>3,
'db_password'=>(string)$plain, 'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
'piwigo_user'=>'', 'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
'piwigo_password'=>'', 'api_key_id'=>(string)($decoded['api_key_id'] ?? ''),
'api_key_id'=>'', 'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''),
'api_key_secret'=>'', 'nextcloud_user'=>(string)($decoded['nextcloud_user'] ?? ''),
'nextcloud_user'=>'', 'nextcloud_password'=>(string)($decoded['nextcloud_password'] ?? ''),
'nextcloud_password'=>'',
); );
} }
function bratonien_tools_nc_connector_store_scoped_secret($id, array $connection, array $credentials) function bratonien_tools_nc_connector_store_scoped_secret($id, array $connection, array $credentials)
{ {
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array(); $config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
$is_webdav = (string)($config['source_mode'] ?? '') === 'webdav-placeholder'; if ((string)($connection['adapter'] ?? '') !== 'remote' || (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'] ?? '') === '')) 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.'); throw new RuntimeException('Die Nextcloud-Zugangsdaten der WebDAV-Verbindung fehlen.');
} }
$credentials['v'] = 3; $credentials['v'] = 3;
unset($credentials['db_password']);
$payload = json_encode($credentials, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $payload = json_encode($credentials, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($payload)) throw new RuntimeException('Verbindungszugangsdaten konnten nicht serialisiert werden.'); if (!is_string($payload)) throw new RuntimeException('Verbindungszugangsdaten konnten nicht serialisiert werden.');
$blob = bratonien_tools_nc_connector_encrypt_secret($payload); $blob = bratonien_tools_nc_connector_encrypt_secret($payload);
if (array_key_exists('api_enabled', $config)) $config['piwigo_auth'] = 'connection-scoped';
{ $config['api_enabled'] = trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '';
$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); $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.'); 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); $connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.'); 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 = bratonien_tools_nc_connector_scoped_secret($connection);
$credentials['piwigo_user'] = $username; $credentials['piwigo_user'] = $username;
$credentials['piwigo_password'] = $password; $credentials['piwigo_password'] = $password;
bratonien_tools_nc_connector_store_scoped_secret($id, $connection, $credentials); 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() 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); $id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true); $connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.'); if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$credentials = bratonien_tools_nc_connector_scoped_secret($connection); $credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$credentials['piwigo_user'] = ''; $credentials['piwigo_user'] = '';
$credentials['piwigo_password'] = ''; $credentials['piwigo_password'] = '';
bratonien_tools_nc_connector_store_scoped_secret($id, $connection, $credentials); 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.');
} }

View File

@@ -1,128 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_create_local_api_first()
{
$required = array(
'nc_name' => 'Name',
'nc_host' => 'PostgreSQL-Host',
'nc_database' => 'Datenbank',
'nc_user' => 'Reader-Benutzer',
'nc_db_password' => 'Reader-Passwort',
'nc_source_view' => 'Source-View',
'nc_activity_view' => 'Activity-View',
'nc_gallery_root' => 'Galerie-Pfad',
);
foreach ($required as $field => $label)
{
if (trim((string)($_POST[$field] ?? '')) === '') throw new RuntimeException($label.' fehlt.');
}
$fallback_user = trim((string)($_POST['nc_piwigo_user'] ?? ''));
$fallback_password = (string)($_POST['nc_piwigo_password'] ?? '');
if (($fallback_user === '') !== ($fallback_password === ''))
{
throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort muessen entweder beide angegeben oder beide leer gelassen werden.');
}
$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.'
);
}

View File

@@ -1,101 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_finish_generic_scope()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)($state['step'] ?? 0) !== 4 || empty($state['technical_complete']) || trim((string)($state['username'] ?? '')) === '')
{
throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
}
$roots = isset($state['roots']) && is_array($state['roots']) ? $state['roots'] : array();
$storages = isset($state['storages']) && is_array($state['storages']) ? $state['storages'] : array();
if (!$roots) throw new RuntimeException('Es wurden keine Nextcloud-Quellen ausgewählt.');
if (!$storages) throw new RuntimeException('Es wurden keine Storage-Adapter zugeordnet.');
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 ($storages as $storage)
{
$storage_id = trim((string)($storage['storage_id'] ?? ''));
$source_prefix = trim((string)($storage['source_prefix'] ?? ''), '/');
$local_mount = rtrim(trim((string)($storage['local_mount'] ?? '')), '/');
if ($storage_id === '' || $local_mount === '') throw new RuntimeException('Eine Storage-Zuordnung ist unvollständig.');
$key = $storage_id.'|'.$source_prefix.'|'.$local_mount;
$mapping_lines[$key] = $storage_id.' | '.$source_prefix.' | '.$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']='piwigo_connector_files';
$_POST['nc_activity_view']='piwigo_connector_activity';
$_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_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 = is_array($connection['config'] ?? null) ? $connection['config'] : array();
$config['source_mode'] = 'selected-fileids';
$config['source_view'] = 'piwigo_connector_files';
$config['activity_view'] = 'piwigo_connector_activity';
$config['access_user'] = (string)$state['username'];
$config['nextcloud_access_user'] = (string)$state['username'];
$config['storages'] = array_values($storages);
$config['roots'] = array_values(array_map(function($root) {
return array(
'fileid'=>(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;
}

View File

@@ -1,107 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_credentials_from_blob($blob)
{
$plain = bratonien_tools_nc_connector_decrypt_secret($blob);
if ($plain === '') return array('db_password'=>'', '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.');
}

View File

@@ -7,10 +7,7 @@ if (!defined('PHPWG_ROOT_PATH'))
function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node) function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node)
{ {
$children = $node->children(); $children = $node->children();
if (count($children) === 0) if (count($children) === 0) return (string)$node;
{
return (string)$node;
}
$result = array(); $result = array();
foreach ($children as $name => $child) 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); $value = bratonien_tools_nc_connector_xml_value($child);
if (array_key_exists($name, $result)) if (array_key_exists($name, $result))
{ {
if (!is_array($result[$name]) || !array_is_list($result[$name])) if (!is_array($result[$name]) || !array_is_list($result[$name])) $result[$name] = array($result[$name]);
{
$result[$name] = array($result[$name]);
}
$result[$name][] = $value; $result[$name][] = $value;
} }
else else $result[$name] = $value;
{
$result[$name] = $value;
}
} }
return $result; 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 = '') function bratonien_tools_nc_connector_decode_api_response($body, $content_type = '')
{ {
$decoded = json_decode((string)$body, true); $decoded = json_decode((string)$body, true);
if (is_array($decoded)) if (is_array($decoded)) return $decoded;
{
return $decoded;
}
if (!function_exists('simplexml_load_string')) if (!function_exists('simplexml_load_string')) throw new RuntimeException('Piwigo-API lieferte XML, aber SimpleXML ist in PHP nicht verfügbar.');
{
throw new RuntimeException('Piwigo-API lieferte XML, aber SimpleXML ist in PHP nicht verfuegbar.');
}
$previous = libxml_use_internal_errors(true); $previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$body); $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') if ($xml === false || $xml->getName() !== 'rsp')
{ {
$detail = $content_type !== '' ? ' Antworttyp: '.$content_type.'.' : ''; $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']); $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); $value = bratonien_tools_nc_connector_xml_value($child);
if (array_key_exists($name, $response)) if (array_key_exists($name, $response))
{ {
if (!is_array($response[$name]) || !array_is_list($response[$name])) if (!is_array($response[$name]) || !array_is_list($response[$name])) $response[$name] = array($response[$name]);
{
$response[$name] = array($response[$name]);
}
$response[$name][] = $value; $response[$name][] = $value;
} }
else else $response[$name] = $value;
{
$response[$name] = $value;
}
} }
return $response; return $response;
} }
function bratonien_tools_nc_connector_api_payload(array $decoded) function bratonien_tools_nc_connector_api_payload(array $decoded)
{ {
if (array_key_exists('result', $decoded)) if (array_key_exists('result', $decoded)) return $decoded['result'];
{
return $decoded['result'];
}
unset($decoded['stat']); unset($decoded['stat']);
return $decoded; return $decoded;
} }
@@ -91,66 +66,42 @@ function bratonien_tools_nc_connector_collect_method_names($value, array &$metho
if (is_string($value)) if (is_string($value))
{ {
$value = trim($value); $value = trim($value);
if ($value !== '' && preg_match('/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/', $value)) if ($value !== '' && preg_match('/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/', $value)) $methods[$value] = true;
{
$methods[$value] = true;
}
return;
}
if (!is_array($value))
{
return; return;
} }
if (!is_array($value)) return;
if (isset($value['name']) && is_string($value['name'])) if (isset($value['name']) && is_string($value['name']))
{ {
$name = trim($value['name']); $name = trim($value['name']);
if ($name !== '') if ($name !== '') $methods[$name] = true;
{
$methods[$name] = true;
}
}
foreach ($value as $entry)
{
bratonien_tools_nc_connector_collect_method_names($entry, $methods);
} }
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) function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, $method)
{ {
if (!function_exists('curl_init')) if (!function_exists('curl_init')) throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der API-Key-Test kann nicht ausgeführt werden.');
{
throw new RuntimeException('cURL ist in PHP nicht verfuegbar. Der API-Key-Test kann nicht ausgefuehrt werden.');
}
$api_key_id = trim((string)$api_key_id); $api_key_id = trim((string)$api_key_id);
$api_key_secret = trim((string)$api_key_secret); $api_key_secret = trim((string)$api_key_secret);
if ($api_key_id === '' || $api_key_secret === '') if ($api_key_id === '' || $api_key_secret === '') throw new RuntimeException('API-Schlüssel-ID und Geheimnis müssen angegeben werden.');
{
throw new RuntimeException('API-Schluessel-ID und Geheimnis muessen angegeben werden.');
}
$api_key = $api_key_id.':'.$api_key_secret;
$url = rtrim(get_absolute_root_url(true), '/').'/ws.php'; $url = rtrim(get_absolute_root_url(true), '/').'/ws.php';
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt_array($ch, array( curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER=>true,
CURLOPT_POST => true, CURLOPT_POST=>true,
CURLOPT_POSTFIELDS => http_build_query(array( CURLOPT_POSTFIELDS=>http_build_query(array('method'=>(string)$method,'format'=>'json')),
'method' => (string)$method, CURLOPT_HTTPHEADER=>array(
'format' => 'json', 'X-PIWIGO-API: '.$api_key_id.':'.$api_key_secret,
)),
CURLOPT_HTTPHEADER => array(
'X-PIWIGO-API: '.$api_key,
'Accept: application/json, text/xml;q=0.9', 'Accept: application/json, text/xml;q=0.9',
'Content-Type: application/x-www-form-urlencoded', 'Content-Type: application/x-www-form-urlencoded',
), ),
CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_CONNECTTIMEOUT=>10,
CURLOPT_TIMEOUT => 20, CURLOPT_TIMEOUT=>20,
CURLOPT_FOLLOWLOCATION => false, 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); $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); $content_type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch); curl_close($ch);
if ($body === false || $errno !== 0) 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.'.');
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); $decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $content_type);
if (($decoded['stat'] ?? '') !== 'ok') 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.'); $message = (string)($decoded['message'] ?? $decoded['err'] ?? 'API-Aufruf wurde abgelehnt.');
throw new RuntimeException($message); throw new RuntimeException($message);
} }
return bratonien_tools_nc_connector_api_payload($decoded); return bratonien_tools_nc_connector_api_payload($decoded);
} }
function bratonien_tools_nc_connector_validate_fallback_credentials($username, $password) function bratonien_tools_nc_connector_validate_fallback_credentials($username, $password)
{ {
if (!function_exists('curl_init')) if (!function_exists('curl_init')) throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der Piwigo-Fallback kann nicht geprüft werden.');
{
throw new RuntimeException('cURL ist in PHP nicht verfuegbar. Der Piwigo-Fallback kann nicht geprueft werden.');
}
$username = trim((string)$username); $username = trim((string)$username);
$password = (string)$password; $password = (string)$password;
if ($username === '' || $password === '') if ($username === '' || $password === '') throw new RuntimeException('Piwigo-Benutzername und Passwort müssen angegeben werden.');
{
throw new RuntimeException('Piwigo-Benutzername und Passwort muessen angegeben werden.');
}
$cookie_file = tempnam(sys_get_temp_dir(), 'br-pwg-auth-'); $cookie_file = tempnam(sys_get_temp_dir(), 'br-pwg-auth-');
if ($cookie_file === false) if ($cookie_file === false) throw new RuntimeException('Temporäre Piwigo-Sitzung konnte nicht angelegt werden.');
{
throw new RuntimeException('Temporare Piwigo-Sitzung konnte nicht angelegt werden.');
}
try try
{ {
@@ -214,7 +149,7 @@ function bratonien_tools_nc_connector_validate_fallback_credentials($username, $
CURLOPT_CONNECTTIMEOUT=>10, CURLOPT_CONNECTTIMEOUT=>10,
CURLOPT_TIMEOUT=>20, CURLOPT_TIMEOUT=>20,
CURLOPT_FOLLOWLOCATION=>false, 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); $body = curl_exec($ch);
$errno = curl_errno($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); $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE); $type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch); curl_close($ch);
if ($body === false || $errno !== 0) throw new RuntimeException('Piwigo-Fallback konnte nicht geprueft werden: '.$error); 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-Pruefung antwortete mit HTTP '.$http.'.'); 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); $decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $type);
if (($decoded['stat'] ?? '') !== 'ok') 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); 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')); $status = $request(array('method'=>'pwg.session.getStatus'));
if (!is_array($status)) throw new RuntimeException('Piwigo hat keinen auswertbaren Benutzerstatus geliefert.'); if (!is_array($status)) throw new RuntimeException('Piwigo hat keinen auswertbaren Benutzerstatus geliefert.');
$role = strtolower(trim((string)($status['status'] ?? ''))); $role = strtolower(trim((string)($status['status'] ?? '')));
if (!in_array($role, array('admin','webmaster'), true)) 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);
throw new RuntimeException('Der Piwigo-Fallback funktioniert, gehoert aber keinem Administrator/Webmaster.');
}
return array('username'=>(string)($status['username'] ?? $username), 'status'=>$role);
} }
finally finally
{ {
@unlink($cookie_file); @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,
);
}

View File

@@ -4,40 +4,94 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
function bratonien_tools_nc_connector_systemctl_value(array $args) function bratonien_tools_nc_connector_systemctl_run(array $args)
{ {
$result = array('exit'=>-1, 'stdout'=>'', 'stderr'=>'');
if (!function_exists('proc_open')) if (!function_exists('proc_open'))
{ {
return ''; $result['stderr'] = 'proc_open ist in PHP nicht verfügbar.';
return $result;
} }
$command = array_merge(array('/usr/bin/systemctl'), $args); $command = array_merge(array('/usr/bin/systemctl'), $args);
$spec = array( $spec = array(
0 => array('file', '/dev/null', 'r'), 0=>array('file','/dev/null','r'),
1 => array('pipe', 'w'), 1=>array('pipe','w'),
2 => 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); $process = @proc_open($command, $spec, $pipes, null, $environment);
if (!is_resource($process)) if (!is_resource($process))
{ {
return ''; $result['stderr'] = 'systemctl konnte nicht gestartet werden.';
return $result;
} }
$stdout = stream_get_contents($pipes[1]);
stream_get_contents($pipes[2]); $result['stdout'] = trim((string)stream_get_contents($pipes[1]));
$result['stderr'] = trim((string)stream_get_contents($pipes[2]));
fclose($pipes[1]); fclose($pipes[1]);
fclose($pipes[2]); fclose($pipes[2]);
$exit = proc_close($process); $result['exit'] = (int)proc_close($process);
return $exit === 0 ? trim((string)$stdout) : ''; return $result;
}
function bratonien_tools_nc_connector_systemctl_value(array $args)
{
$result = bratonien_tools_nc_connector_systemctl_run($args);
return $result['exit'] === 0 ? $result['stdout'] : '';
}
function bratonien_tools_nc_connector_run_now()
{
if (!function_exists('proc_open'))
{
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: proc_open ist in PHP nicht verfügbar.');
}
$runtime_dir = realpath(dirname(__DIR__).'/runtime');
if ($runtime_dir === false || !is_dir($runtime_dir))
{
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: Runtime-Verzeichnis wurde nicht gefunden.');
}
$script = $runtime_dir.'/run-all.sh';
if (!is_file($script) || !is_readable($script))
{
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: runtime/run-all.sh ist nicht lesbar.');
}
$spec = array(
0=>array('file','/dev/null','r'),
1=>array('pipe','w'),
2=>array('pipe','w'),
);
$environment = array_merge($_ENV, array('LC_ALL'=>'C','LANG'=>'C'));
$process = @proc_open(array('/usr/bin/env','bash',$script), $spec, $pipes, $runtime_dir, $environment);
if (!is_resource($process))
{
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: Connector-Prozess konnte nicht geöffnet werden.');
}
$stdout = trim((string)stream_get_contents($pipes[1]));
$stderr = trim((string)stream_get_contents($pipes[2]));
fclose($pipes[1]);
fclose($pipes[2]);
$exit = (int)proc_close($process);
if ($exit !== 0)
{
$detail = $stderr !== '' ? $stderr : $stdout;
if ($detail === '') $detail = 'Exit-Code '.$exit;
throw new RuntimeException('WebDAV-Abgleich fehlgeschlagen: '.$detail);
}
return array('message'=>'WebDAV-Abgleich wurde ausgeführt.');
} }
function bratonien_tools_nc_connector_parse_systemd_time($value) function bratonien_tools_nc_connector_parse_systemd_time($value)
{ {
$value = trim((string)$value); $value = trim((string)$value);
if ($value === '' || strtolower($value) === 'n/a') if ($value === '' || strtolower($value) === 'n/a') return 0;
{
return 0;
}
$parsed = strtotime($value); $parsed = strtotime($value);
return $parsed === false ? 0 : (int)$parsed; return $parsed === false ? 0 : (int)$parsed;
} }
@@ -45,106 +99,52 @@ function bratonien_tools_nc_connector_parse_systemd_time($value)
function bratonien_tools_nc_connector_monotonic_to_timestamp($value) function bratonien_tools_nc_connector_monotonic_to_timestamp($value)
{ {
$value = trim((string)$value); $value = trim((string)$value);
if ($value === '' || $value === '0' || strtolower($value) === 'n/a') if ($value === '' || $value === '0' || strtolower($value) === 'n/a') return 0;
{ if (!preg_match('/^([0-9]+)(?:us)?$/', $value, $matches)) return 0;
return 0;
}
if (preg_match('/^([0-9]+)(?:us)?$/', $value, $matches))
{
$next_boot_seconds = ((float)$matches[1]) / 1000000;
}
else
{
return 0;
}
$next_boot_seconds = ((float)$matches[1]) / 1000000;
$uptime_raw = @file_get_contents('/proc/uptime'); $uptime_raw = @file_get_contents('/proc/uptime');
if (!is_string($uptime_raw) || !preg_match('/^([0-9]+(?:\.[0-9]+)?)/', trim($uptime_raw), $uptime_match)) if (!is_string($uptime_raw) || !preg_match('/^([0-9]+(?:\.[0-9]+)?)/', trim($uptime_raw), $uptime_match)) return 0;
{
return 0;
}
$remaining = $next_boot_seconds - (float)$uptime_match[1]; $remaining = $next_boot_seconds - (float)$uptime_match[1];
if ($remaining < -1) if ($remaining < -1) return 0;
{
return 0;
}
return (int)round(time() + max(0, $remaining)); return (int)round(time() + max(0, $remaining));
} }
function bratonien_tools_nc_connector_next_from_timer_list($timer) function bratonien_tools_nc_connector_next_from_timer_list($timer)
{ {
$line = bratonien_tools_nc_connector_systemctl_value(array( $line = bratonien_tools_nc_connector_systemctl_value(array('list-timers','--all','--no-pager','--no-legend',$timer));
'list-timers', '--all', '--no-pager', '--no-legend', $timer, if ($line === '') return 0;
));
if ($line === '')
{
return 0;
}
$first_line = trim((string)strtok($line, "\n")); $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)) 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]);
$parsed = strtotime($matches[1]); return $parsed === false ? 0 : (int)$parsed;
return $parsed === false ? 0 : (int)$parsed;
}
return 0;
} }
function bratonien_tools_nc_connector_connection_last_status(array $connection) function bratonien_tools_nc_connector_connection_last_status(array $connection)
{ {
$empty = array( $empty = array(
'timestamp'=>0, 'timestamp'=>0,'label'=>'Nicht verfügbar','state'=>'','message'=>'','auth_mode'=>'',
'label'=>'Nicht verfügbar', 'api_state'=>'','api_message'=>'','fallback_state'=>'','fallback_message'=>'','error_detail'=>'',
'state'=>'',
'message'=>'',
'auth_mode'=>'',
'api_state'=>'',
'api_message'=>'',
'fallback_state'=>'',
'fallback_message'=>'',
'error_detail'=>'',
); );
$connection_id = (int)($connection['id'] ?? 0); $connection_id = (int)($connection['id'] ?? 0);
$candidates = array(); $candidates = array();
if ($connection_id > 0) if ($connection_id > 0) $candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$connection_id.'.json';
{
$candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$connection_id.'.json';
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array(); $config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/'); $state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir === '' && $connection_id > 0) 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';
$state_dir = '/var/lib/bratonien-tools/nc-connector/connection-'.$connection_id;
}
if ($state_dir !== '')
{
$candidates[] = $state_dir.'/connector-status.json';
}
$decoded = null; $decoded = null;
foreach ($candidates as $candidate) foreach ($candidates as $candidate)
{ {
if (!is_readable($candidate)) if (!is_readable($candidate)) continue;
{
continue;
}
$value = json_decode((string)@file_get_contents($candidate), true); $value = json_decode((string)@file_get_contents($candidate), true);
if (is_array($value)) if (is_array($value)) { $decoded = $value; break; }
{
$decoded = $value;
break;
}
}
if (!is_array($decoded))
{
return $empty;
} }
if (!is_array($decoded)) return $empty;
$timestamp = (int)($decoded['timestamp'] ?? 0); $timestamp = (int)($decoded['timestamp'] ?? 0);
$api = isset($decoded['api']) && is_array($decoded['api']) ? $decoded['api'] : array(); $api = isset($decoded['api']) && is_array($decoded['api']) ? $decoded['api'] : array();
@@ -166,73 +166,63 @@ function bratonien_tools_nc_connector_connection_last_status(array $connection)
function bratonien_tools_nc_connector_last_status(array $connections) 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) foreach ($connections as $connection)
{ {
if (empty($connection['enabled']) || (string)($connection['takeover_state'] ?? '') !== 'active')
{
continue;
}
$status = bratonien_tools_nc_connector_connection_last_status($connection); $status = bratonien_tools_nc_connector_connection_last_status($connection);
if ((int)$status['timestamp'] >= (int)$latest['timestamp']) if ((int)$status['timestamp'] >= (int)$latest['timestamp']) $latest = $status;
{
$latest = $status;
}
} }
return $latest; return $latest;
} }
function bratonien_tools_nc_connector_status_message(array $last)
{
if ((string)($last['state'] ?? '') === 'ok')
{
return 'WebDAV eingelesen und Piwigo synchronisiert';
}
return (string)($last['message'] ?? '');
}
function bratonien_tools_nc_connector_system_status(array $connections = array()) function bratonien_tools_nc_connector_system_status(array $connections = array())
{ {
$timer = 'bratonien-nc-connector.timer'; $timer = 'bratonien-nc-connector.timer';
$service = 'bratonien-nc-connector.service'; $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); $next_timestamp = bratonien_tools_nc_connector_parse_systemd_time($next_realtime_raw);
if ($next_timestamp <= 0) 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); $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) $active = bratonien_tools_nc_connector_systemctl_value(array('is-active',$timer));
{ $enabled = bratonien_tools_nc_connector_systemctl_value(array('is-enabled',$timer));
$next_timestamp = bratonien_tools_nc_connector_next_from_timer_list($timer);
}
$active = bratonien_tools_nc_connector_systemctl_value(array('is-active', $timer));
$enabled = bratonien_tools_nc_connector_systemctl_value(array('is-enabled', $timer));
$last = bratonien_tools_nc_connector_last_status($connections); $last = bratonien_tools_nc_connector_last_status($connections);
if ($last['timestamp'] <= 0) 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); $last['timestamp'] = bratonien_tools_nc_connector_parse_systemd_time($last_raw);
} }
return array( return array(
'timer_name' => $timer, 'timer_name'=>$timer,
'timer_active' => $active === 'active', 'timer_active'=>$active === 'active',
'timer_enabled' => $enabled === 'enabled', 'timer_enabled'=>$enabled === 'enabled',
'last_run_timestamp' => (int)$last['timestamp'], '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_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_state'=>(string)$last['state'],
'last_run_message' => (string)$last['message'], 'last_run_message'=>bratonien_tools_nc_connector_status_message($last),
'last_run_auth_mode' => (string)$last['auth_mode'], 'last_run_auth_mode'=>(string)$last['auth_mode'],
'last_run_api_state' => (string)$last['api_state'], 'last_run_api_state'=>(string)$last['api_state'],
'last_run_api_message' => (string)$last['api_message'], 'last_run_api_message'=>(string)$last['api_message'],
'last_run_fallback_state' => (string)$last['fallback_state'], 'last_run_fallback_state'=>(string)$last['fallback_state'],
'last_run_fallback_message' => (string)$last['fallback_message'], 'last_run_fallback_message'=>(string)$last['fallback_message'],
'last_run_error_detail' => (string)$last['error_detail'], 'last_run_error_detail'=>(string)$last['error_detail'],
'next_run_timestamp' => $next_timestamp, 'next_run_timestamp'=>$next_timestamp,
'next_run_label' => $next_timestamp > 0 ? date('d.m.Y H:i:s', $next_timestamp) : 'Nicht verfügbar', 'next_run_label'=>$next_timestamp > 0 ? date('d.m.Y H:i:s', $next_timestamp) : 'Nicht verfügbar',
'legacy_runtime_exists' => is_dir('/opt/piwigo-sync'),
'legacy_config_exists' => is_dir('/etc/piwigo-sync'),
'legacy_service_exists' => is_file('/etc/systemd/system/piwigo-sync.service'),
'legacy_timer_exists' => is_file('/etc/systemd/system/piwigo-sync.timer'),
); );
} }

View File

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

View File

@@ -4,12 +4,6 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); 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() function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
{ {
$state = bratonien_tools_nc_wizard_state(); $state = bratonien_tools_nc_wizard_state();
@@ -24,7 +18,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$password = (string)($state['_password'] ?? ''); $password = (string)($state['_password'] ?? '');
if ($base_url === '' || $username === '' || $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']) $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'] ? $state['directory_selected_fileids']
: array(); : array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; })); $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(); $roots = array();
foreach ($selected as $path) 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 !== ''; $api_enabled = $api_key_id !== '' && $api_key_secret !== '';
$config = array( $config = array(
'origin'=>'native',
'source_mode'=>'webdav-placeholder', 'source_mode'=>'webdav-placeholder',
'transport'=>'webdav', 'transport'=>'webdav',
'nextcloud_url'=>$base_url, 'nextcloud_url'=>$base_url,
@@ -70,10 +63,6 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
'roots'=>$roots, 'roots'=>$roots,
'state_dir'=>'', 'state_dir'=>'',
'status_file'=>'', 'status_file'=>'',
'quiet_seconds'=>120,
'max_wait_seconds'=>900,
'full_sync_seconds'=>86400,
'parallel_test'=>true,
'piwigo_auth'=>'connection-scoped', 'piwigo_auth'=>'connection-scoped',
'api_enabled'=>$api_enabled, 'api_enabled'=>$api_enabled,
); );
@@ -86,7 +75,6 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$secret_payload = json_encode(array( $secret_payload = json_encode(array(
'v'=>3, 'v'=>3,
'db_password'=>'',
'piwigo_user'=>$fallback_user, 'piwigo_user'=>$fallback_user,
'piwigo_password'=>$fallback_password, 'piwigo_password'=>$fallback_password,
'api_key_id'=>$api_key_id, '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)); $connection_key = 'webdav-'.bin2hex(random_bytes(12));
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload); $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($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($config_json)."','"
.pwg_db_real_escape_string($secret_blob)."','" .pwg_db_real_escape_string($secret_blob)."','"
.pwg_db_real_escape_string($now)."','" .pwg_db_real_escape_string($now)."','"
.pwg_db_real_escape_string($now)."')"); .pwg_db_real_escape_string($now)."')");
$id = (int)pwg_db_insert_id(); $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['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file'] = $config['state_dir'].'/connector-status.json'; $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)) if (!is_string($config_json))
{ {
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1"); 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"); pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id." LIMIT 1");
return array( return array(
'connection_id'=>$id, 'connection_id'=>$id,
'message'=>'Parallele WebDAV-Testverbindung wurde deaktiviert angelegt. Bestehende Verbindungen blieben unverändert.', 'message'=>'WebDAV-Verbindung wurde angelegt.',
); );
} }

View File

@@ -17,30 +17,28 @@ function bratonien_tools_nc_wizard_state()
'display_name'=>'', 'display_name'=>'',
'version'=>'', 'version'=>'',
'product'=>'Nextcloud', 'product'=>'Nextcloud',
'users'=>array(),
'can_list_users'=>false,
'showcase_user'=>'',
'connection_name'=>'', 'connection_name'=>'',
'scan_message'=>'', 'scan_message'=>'',
'technical_stage'=>'auto_check', 'source_mode'=>'webdav-placeholder',
'technical_source'=>'Noch nicht geprüft', 'transport'=>'webdav',
'gallery_root'=>'',
'roots'=>array(),
'technical_stage'=>'mounts',
'technical_source'=>'WebDAV',
'technical_error'=>'', 'technical_error'=>'',
'technical_complete'=>false, 'technical_complete'=>false,
'db_host'=>'', 'directory_selection_ready'=>false,
'db_port'=>'5432', 'directory_path'=>'',
'db_database'=>'nextcloud', 'directory_parent'=>'',
'db_user'=>'', 'directory_children'=>array(),
'db_password_set'=>false, 'directory_current_fileid'=>0,
'source_view'=>'piwigo_showcase_sources', 'directory_fileids'=>array(),
'activity_view'=>'piwigo_showcase_activity', 'directory_selected'=>array(),
'gallery_root'=>'', 'directory_selected_fileids'=>array(),
'storages'=>array(),
'storage_candidates'=>array(),
'api_status'=>'pending', 'api_status'=>'pending',
'api_username'=>'', 'api_username'=>'',
'api_error'=>'', 'api_error'=>'',
'_password'=>'', '_password'=>'',
'_db_password'=>'',
'_api_key_id'=>'', '_api_key_id'=>'',
'_api_key_secret'=>'', '_api_key_secret'=>'',
'_fallback_user'=>'', '_fallback_user'=>'',
@@ -102,7 +100,7 @@ function bratonien_tools_nc_wizard_http($url, $username = '', $password = '', ar
CURLOPT_CONNECTTIMEOUT=>8, CURLOPT_CONNECTTIMEOUT=>8,
CURLOPT_TIMEOUT=>15, CURLOPT_TIMEOUT=>15,
CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'), $headers), 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 !== '') 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(); 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() function bratonien_tools_nc_wizard_api_test()
{ {
$state = bratonien_tools_nc_wizard_state(); $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']); $key_id = trim((string)$state['_api_key_id']);
$secret = trim((string)$state['_api_key_secret']); $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.'); if ($key_id === '' || $secret === '') throw new RuntimeException('API-Schlüssel-ID und API-Geheimnis fehlen.');
try 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.'); 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() function bratonien_tools_nc_connector_update_name()
{ {
$id = (int)($_POST['connection_id'] ?? 0); $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); $connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.'); 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(); $table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s'); $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.'); 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.');
}

View File

@@ -1,448 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_apply_known_database_profile(array &$state)
{
if (empty($state['scan_ok']) || trim((string)($state['base_url'] ?? '')) === '') return false;
$base_host = strtolower(trim((string)parse_url((string)$state['base_url'], PHP_URL_HOST)));
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) === (string)$state['base_url'];
}
catch (Throwable $ignored)
{
$match = false;
}
}
elseif ($base_host !== '')
{
$match = strtolower(trim((string)($config['host'] ?? ''))) === $base_host;
}
if (!$match) continue;
$connection = bratonien_tools_nc_connector_connection((int)$candidate['id'], true);
if (!$connection || !is_array($connection['config'] ?? null)) continue;
$known = $connection['config'];
$db_user = trim((string)($known['user'] ?? ''));
if ($db_user === '') continue;
$db_password = '';
try
{
$plain = bratonien_tools_nc_connector_decrypt_secret($connection['secret_blob'] ?? '');
$decoded = json_decode($plain, true);
$db_password = is_array($decoded) && array_key_exists('db_password', $decoded)
? (string)$decoded['db_password']
: (string)$plain;
}
catch (Throwable $ignored)
{
$db_password = '';
}
if ($db_password === '') continue;
$state['db_host'] = trim((string)($known['host'] ?? $state['db_host']));
$state['db_port'] = trim((string)($known['port'] ?? $state['db_port']));
$state['db_database'] = trim((string)($known['database'] ?? $state['db_database']));
$state['db_user'] = $db_user;
$state['_db_password'] = $db_password;
$state['db_password_set'] = true;
$state['source_view'] = trim((string)($known['source_view'] ?? $state['source_view']));
$state['activity_view'] = trim((string)($known['activity_view'] ?? $state['activity_view']));
$state['gallery_root'] = trim((string)($known['gallery_root'] ?? $state['gallery_root']));
$state['storages'] = isset($known['storages']) && is_array($known['storages']) ? $known['storages'] : array();
$state['storage_candidates'] = $state['storages'];
$state['technical_source'] = 'Gespeicherte Reader-Verbindung wird erneut geprüft';
$state['technical_error'] = '';
return true;
}
return false;
}
function bratonien_tools_nc_wizard_prepare_directory_stage(array &$state, array $known_storages)
{
if ((string)($state['technical_stage'] ?? '') === 'database_details') return;
$known_by_storage = array();
foreach ($known_storages as $known)
{
$storage_id = trim((string)($known['storage_id'] ?? ''));
$mount = rtrim(trim((string)($known['local_mount'] ?? '')), '/');
if ($storage_id === '' || $mount === '') continue;
if (!isset($known_by_storage[$storage_id])) $known_by_storage[$storage_id] = $known;
}
$candidates = isset($state['storage_candidates']) && is_array($state['storage_candidates']) ? $state['storage_candidates'] : array();
$all_mapped = !empty($candidates);
foreach ($candidates as &$candidate)
{
$storage_id = trim((string)($candidate['storage_id'] ?? ''));
$known = isset($known_by_storage[$storage_id]) ? $known_by_storage[$storage_id] : null;
if (is_array($known))
{
$candidate['source_prefix'] = trim((string)($known['source_prefix'] ?? $candidate['source_prefix'] ?? ''), '/');
$candidate['local_mount'] = rtrim(trim((string)($known['local_mount'] ?? '')), '/');
}
$mount = trim((string)($candidate['local_mount'] ?? ''));
if ($mount === '' || !is_dir($mount) || !is_readable($mount)) $all_mapped = false;
$candidate['include_prefix'] = '';
}
unset($candidate);
$state['storage_candidates'] = $candidates;
$state['storages'] = array();
$state['technical_complete'] = false;
$state['technical_stage'] = 'mounts';
$state['technical_source'] = $all_mapped
? 'Datenzugriff und Speicherzuordnung erkannt; Verzeichnisauswahl offen'
: 'Datenzugriff erkannt; Speicherzuordnung muss bestätigt werden';
$state['directory_selection_ready'] = $all_mapped;
}
function bratonien_tools_nc_wizard_finish_data_access_for_selection(array &$state, array $known_storages)
{
bratonien_tools_nc_wizard_finish_data_access($state);
bratonien_tools_nc_wizard_prepare_directory_stage($state, $known_storages);
}
function bratonien_tools_nc_wizard_scan_with_known_database()
{
$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'=>'',
'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;
}

View File

@@ -1,10 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_wizard_save_technical_flow()
{
return bratonien_tools_nc_wizard_save_technical_generic();
}

View File

@@ -112,256 +112,6 @@ function bratonien_tools_nc_wizard_refresh_directory_state(array &$state, $path
if (!isset($state['directory_selected_fileids']) || !is_array($state['directory_selected_fileids'])) $state['directory_selected_fileids'] = array(); if (!isset($state['directory_selected_fileids']) || !is_array($state['directory_selected_fileids'])) $state['directory_selected_fileids'] = array();
} }
function bratonien_tools_nc_wizard_generic_db_config(array $state)
{
return array(
'host'=>(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() function bratonien_tools_nc_wizard_directory_browse()
{ {
$state = bratonien_tools_nc_wizard_state(); $state = bratonien_tools_nc_wizard_state();
@@ -400,42 +150,6 @@ function bratonien_tools_nc_wizard_directory_remove()
return array('message'=>'Verzeichnis entfernt.'); 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() function bratonien_tools_nc_wizard_select_current_user()
{ {
$state = bratonien_tools_nc_wizard_state(); $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'])); $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.'); if ($connection_name === '') throw new RuntimeException('Bitte einen Namen für die Verbindung angeben.');
$state['connection_name'] = $connection_name; $state['connection_name'] = $connection_name;
$state['showcase_user'] = (string)$state['username'];
$state['step'] = 3; $state['step'] = 3;
$state['api_error'] = ''; $state['api_error'] = '';
bratonien_tools_nc_wizard_store($state); bratonien_tools_nc_wizard_store($state);
@@ -475,23 +188,10 @@ function bratonien_tools_nc_wizard_back()
$state['directory_selection_ready'] = true; $state['directory_selection_ready'] = true;
bratonien_tools_nc_wizard_refresh_directory_state($state); 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 else
{ {
$state['step'] = 1; $state['step'] = 1;
$state['directory_selection_ready'] = false;
} }
bratonien_tools_nc_wizard_store($state); bratonien_tools_nc_wizard_store($state);

View File

@@ -73,17 +73,12 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''), 'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''),
'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''), 'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''),
'product'=>(string)($status_data['productname'] ?? 'Nextcloud'), 'product'=>(string)($status_data['productname'] ?? 'Nextcloud'),
'users'=>array(),
'can_list_users'=>false,
'showcase_user'=>$resolved_username,
'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud WebDAV', 'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud WebDAV',
'scan_message'=>'Nextcloud und WebDAV-Zugriff wurden bestätigt.', 'scan_message'=>'Nextcloud und WebDAV-Zugriff wurden bestätigt.',
'_password'=>$password, '_password'=>$password,
'source_mode'=>'webdav-placeholder', 'source_mode'=>'webdav-placeholder',
'transport'=>'webdav', 'transport'=>'webdav',
'gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries', 'gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries',
'storages'=>array(),
'storage_candidates'=>array(),
'roots'=>array(), 'roots'=>array(),
'technical_stage'=>'mounts', 'technical_stage'=>'mounts',
'technical_source'=>'WebDAV', 'technical_source'=>'WebDAV',
@@ -97,8 +92,6 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
'directory_fileids'=>array(), 'directory_fileids'=>array(),
'directory_selected'=>array(), 'directory_selected'=>array(),
'directory_selected_fileids'=>array(), 'directory_selected_fileids'=>array(),
'database_prompted'=>false,
'mount_prompted'=>false,
'api_status'=>'pending', 'api_status'=>'pending',
'api_username'=>'', 'api_username'=>'',
'api_error'=>'', 'api_error'=>'',
@@ -115,7 +108,7 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$state = bratonien_tools_nc_wizard_state(); $state = bratonien_tools_nc_wizard_state();
if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder') 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'])) 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; $state['directory_selection_ready'] = false;
bratonien_tools_nc_wizard_store($state); 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() function bratonien_tools_nc_wizard_finish_dispatch()
@@ -166,7 +159,7 @@ function bratonien_tools_nc_wizard_finish_dispatch()
$state = bratonien_tools_nc_wizard_state(); $state = bratonien_tools_nc_wizard_state();
if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder') 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'])) if ((int)($state['step'] ?? 0) !== 4 || empty($state['technical_complete']))

View File

@@ -1,282 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
/**
* Registers the connector-owned filesystem synchronization endpoint.
*
* Authentication and authorization are delegated to Piwigo's Web API.
* The method is administrator-only and POST-only so an API key inherits the
* permissions of its Piwigo user without storing that user's password.
*/
function bratonien_tools_register_ws_methods($arr)
{
$service = &$arr[0];
$service->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'] : '',
);
}

View File

@@ -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_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.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.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_piwigo_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.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_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_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_delete_safe.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php');
@@ -54,13 +47,11 @@ function bratonien_tools_get_tools()
'album_share_create' => array('handler' => 'bratonien_tools_create_album_share'), 'album_share_create' => array('handler' => 'bratonien_tools_create_album_share'),
'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'), 'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'),
'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'), '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_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_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'),
'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'), 'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'), 'nc_connector_run_now' => array('handler' => 'bratonien_tools_nc_connector_run_now'),
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'), 'nc_connector_wizard_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_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_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),
'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'), 'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
@@ -71,15 +62,8 @@ function bratonien_tools_get_tools()
'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_dispatch'), '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_back' => array('handler' => 'bratonien_tools_nc_wizard_back'),
'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'), '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_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_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_check' => array('handler' => 'bratonien_tools_self_update_check'),
'self_update_run' => array('handler' => 'bratonien_tools_self_update_run'), 'self_update_run' => array('handler' => 'bratonien_tools_self_update_run'),
); );

View File

@@ -0,0 +1,274 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_webdav_materialize_source_info($image_id)
{
static $cache = array();
$image_id = (int)$image_id;
if ($image_id < 1) return null;
if (array_key_exists($image_id, $cache)) return $cache[$image_id];
$result = pwg_query('SELECT path, coi FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result)) return $cache[$image_id] = null;
$row = pwg_db_fetch_assoc($result);
$path = (string)($row['path'] ?? '');
if ($path === '') return $cache[$image_id] = null;
$absolute = $path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $absolute), '/');
}
$resolved = realpath($absolute);
if ($resolved === false) return $cache[$image_id] = null;
$normalized = str_replace('\\', '/', $resolved);
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{
return $cache[$image_id] = null;
}
$connection_id = (int)$match[1];
$root_fileid = (int)$match[2];
$relative_path = trim((string)$match[3], '/');
if ($connection_id < 1 || $root_fileid < 1 || $relative_path === '')
{
return $cache[$image_id] = null;
}
$table = defined('BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE')
? BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$connection_result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($connection_result)) return $cache[$image_id] = null;
$connection_row = pwg_db_fetch_assoc($connection_result);
$config = json_decode((string)$connection_row['config_json'], true);
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder')
{
return $cache[$image_id] = null;
}
$root_found = false;
$root_path = '';
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
foreach ($roots as $root)
{
if ((int)($root['fileid'] ?? 0) === $root_fileid)
{
$root_found = true;
$root_path = trim((string)($root['webdav_path'] ?? ''), '/');
break;
}
}
if (!$root_found) return $cache[$image_id] = null;
$webdav_path = trim($root_path === '' ? $relative_path : $root_path.'/'.$relative_path, '/');
if ($webdav_path === '') return $cache[$image_id] = null;
$content_type = '';
$size = 0;
$etag = '';
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '')
{
$mapping_file = $state_dir.'/webdav-map.json';
if (is_readable($mapping_file))
{
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null;
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file')
{
$mapped_path = trim((string)($entry['webdav_path'] ?? ''), '/');
if ($mapped_path !== '') $webdav_path = $mapped_path;
$content_type = (string)($entry['content_type'] ?? '');
$size = (int)($entry['size'] ?? 0);
$etag = (string)($entry['etag'] ?? '');
}
}
}
}
return $cache[$image_id] = array(
'image_id'=>$image_id,
'connection_id'=>$connection_id,
'root_fileid'=>$root_fileid,
'relative_path'=>$relative_path,
'webdav_path'=>$webdav_path,
'content_type'=>$content_type,
'size'=>$size,
'etag'=>$etag,
'coi'=>$row['coi'] ?? null,
);
}
function bratonien_tools_webdav_materialize_image_url($image_id)
{
$info = bratonien_tools_webdav_materialize_source_info($image_id);
if (!$info) return null;
$url = get_root_url().'plugins/'.BRATONIEN_TOOLS_ID.'/webdav-image.php?id='.(int)$image_id;
if ($info['etag'] !== '') $url .= '&v='.rawurlencode(substr(sha1($info['etag']), 0, 12));
return $url;
}
function bratonien_tools_webdav_materialize_custom_params($key)
{
if (!class_exists('DerivativeParams') || !class_exists('SizingParams')) return null;
$tokens = explode('_', trim((string)$key));
if (!$tokens || $tokens[0] === '') return null;
$parse_size = function($value)
{
if (!preg_match('/^[0-9]+(?:x[0-9]+)?$/', (string)$value)) return null;
$parts = explode('x', (string)$value, 2);
$width = (int)$parts[0];
$height = count($parts) === 1 ? $width : (int)$parts[1];
if ($width < 1 || $height < 1 || $width > 20000 || $height > 20000) return null;
return array($width, $height);
};
$token = array_shift($tokens);
$crop = 0;
$min_size = null;
if (isset($token[0]) && $token[0] === 's')
{
$size = $parse_size(substr($token, 1));
}
elseif (isset($token[0]) && $token[0] === 'e')
{
$crop = 1;
$size = $min_size = $parse_size(substr($token, 1));
}
else
{
if (count($tokens) < 2) return null;
$size = $parse_size($token);
$crop_token = (string)array_shift($tokens);
$min_size = $parse_size(array_shift($tokens));
if (!preg_match('/^[a-z]$/', $crop_token) || $min_size === null) return null;
$crop = function_exists('char_to_fraction') ? char_to_fraction($crop_token) : 0;
}
if ($size === null) return null;
$params = new DerivativeParams(new SizingParams($size, $crop, $min_size));
if (class_exists('ImageStdParams')) ImageStdParams::apply_global($params);
return $params;
}
function bratonien_tools_webdav_materialize_variant_from_params($params)
{
if (!is_object($params) || !class_exists('ImageStdParams')) return null;
$type = (string)($params->type ?? '');
$defined = ImageStdParams::get_defined_type_map();
if ($type !== '' && defined('IMG_CUSTOM') && $type !== IMG_CUSTOM && isset($defined[$type]))
{
return 'standard:'.$type;
}
if (!method_exists($params, 'add_url_tokens')) return null;
$tokens = array();
$params->add_url_tokens($tokens);
if (!$tokens) return null;
$key = implode('_', $tokens);
if (!preg_match('/^[A-Za-z0-9_x-]+$/', $key)) return null;
return 'custom:'.$key;
}
function bratonien_tools_webdav_materialize_params_from_variant($variant)
{
$variant = trim((string)$variant);
if ($variant === '' || !class_exists('ImageStdParams')) return null;
if (strpos($variant, 'standard:') === 0)
{
$type = substr($variant, strlen('standard:'));
$defined = ImageStdParams::get_defined_type_map();
return isset($defined[$type]) ? $defined[$type] : null;
}
if (strpos($variant, 'custom:') === 0)
{
$key = substr($variant, strlen('custom:'));
if (!preg_match('/^[A-Za-z0-9_x-]+$/', $key)) return null;
return bratonien_tools_webdav_materialize_custom_params($key);
}
return null;
}
function bratonien_tools_webdav_materialize_b64url_encode($value)
{
return rtrim(strtr(base64_encode((string)$value), '+/', '-_'), '=');
}
function bratonien_tools_webdav_materialize_after_signature($image_id, $variant, $after_url)
{
global $conf;
$key = !empty($conf['secret_key']) ? $conf['secret_key'] : ($conf['db_password'] ?? 'bratonien-tools');
return hash_hmac('sha256', (int)$image_id.'|'.(string)$variant.'|'.(string)$after_url, $key);
}
function bratonien_tools_webdav_materialize_derivative_url($image_id, $variant, array $info, $after_url='')
{
$url = get_root_url().'plugins/'.BRATONIEN_TOOLS_ID.'/webdav-derivative.php?id='.(int)$image_id.'&variant='.rawurlencode($variant);
if ($after_url !== '')
{
$url .= '&after='.rawurlencode(bratonien_tools_webdav_materialize_b64url_encode($after_url));
$url .= '&sig='.rawurlencode(bratonien_tools_webdav_materialize_after_signature($image_id, $variant, $after_url));
}
if (!empty($info['etag'])) $url .= '&v='.rawurlencode(substr(sha1((string)$info['etag']), 0, 12));
return $url;
}
function bratonien_tools_filter_webdav_materialize_src_url($url, $src_image)
{
if (!is_object($src_image) || empty($src_image->id)) return $url;
$webdav_url = bratonien_tools_webdav_materialize_image_url((int)$src_image->id);
return $webdav_url ?: $url;
}
function bratonien_tools_filter_webdav_materialize_derivative_url($url, $params, $src_image, $rel_url)
{
if (!is_object($src_image) || empty($src_image->id)) return $url;
$image_id = (int)$src_image->id;
$info = bratonien_tools_webdav_materialize_source_info($image_id);
if (!$info) return $url;
try
{
$derivative = new DerivativeImage($params, $src_image);
if ($derivative->same_as_source())
{
$source_url = bratonien_tools_webdav_materialize_image_url($image_id);
return $source_url ?: $url;
}
$path = $derivative->get_path();
if ($path !== '' && is_file($path) && is_readable($path)) return $url;
$variant = bratonien_tools_webdav_materialize_variant_from_params($params);
if ($variant === null) return $url;
return bratonien_tools_webdav_materialize_derivative_url($image_id, $variant, $info, (string)$url);
}
catch (Throwable $e)
{
error_log('Bratonien WebDAV materialize derivative #'.$image_id.': '.$e->getMessage());
return $url;
}
}

View File

@@ -1,6 +1,40 @@
(function () { (function () {
'use strict'; 'use strict';
function initNcConnectorRunNow() {
var section = document.getElementById('nc-connector');
if (!section || section.querySelector('[data-nc-run-now]')) return;
var statusCard = section.querySelector('.bratonien-card');
var token = section.querySelector('input[name="pwg_token"]');
if (!statusCard || !token) return;
var actions = document.createElement('div');
actions.className = 'bratonien-actions';
actions.style.marginTop = '1rem';
actions.setAttribute('data-nc-run-now', '1');
var form = document.createElement('form');
form.method = 'post';
var tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = 'pwg_token';
tokenInput.value = token.value;
var button = document.createElement('button');
button.className = 'buttonLike';
button.type = 'submit';
button.name = 'bratonien_tool';
button.value = 'nc_connector_run_now';
button.textContent = 'Jetzt abrufen';
form.appendChild(tokenInput);
form.appendChild(button);
actions.appendChild(form);
statusCard.appendChild(actions);
}
function initBratonienTabs() { function initBratonienTabs() {
var admin = document.querySelector('.bratonien-admin'); var admin = document.querySelector('.bratonien-admin');
if (!admin) return; if (!admin) return;
@@ -113,9 +147,14 @@
activate(initial, false); activate(initial, false);
} }
if (document.readyState === 'loading') { function init() {
document.addEventListener('DOMContentLoaded', initBratonienTabs);
} else {
initBratonienTabs(); initBratonienTabs();
initNcConnectorRunNow();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
} }
})(); })();

View File

@@ -1,7 +1,7 @@
<?php <?php
/* /*
Plugin Name: Bratonien Tools Plugin Name: Bratonien Tools
Version: 0.9.6.1 Version: 0.9.6.8.22
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation. Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien Author: Bratonien
@@ -17,26 +17,27 @@ define('BRATONIEN_TOOLS_PATH', PHPWG_PLUGINS_PATH . BRATONIEN_TOOLS_ID . '/');
require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_runtime.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_image_runtime.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_image_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_materialize_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/public_selection.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/public_selection.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/picture_navigation.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/picture_navigation.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/batch_titles.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/batch_titles.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php');
add_event_handler('get_admin_plugin_menu_links', 'bratonien_tools_admin_menu'); add_event_handler('get_admin_plugin_menu_links', 'bratonien_tools_admin_menu');
add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4); add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4);
add_event_handler('get_src_image_url', 'bratonien_tools_filter_webdav_src_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 2); add_event_handler('get_src_image_url', 'bratonien_tools_filter_webdav_materialize_src_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 2);
add_event_handler('get_derivative_url', 'bratonien_tools_filter_webdav_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 4); add_event_handler('get_derivative_url', 'bratonien_tools_filter_webdav_materialize_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 4);
add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action'); add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action');
add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2); add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EVENT_HANDLER_PRIORITY_NEUTRAL - 30); add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EVENT_HANDLER_PRIORITY_NEUTRAL - 30);
add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', EVENT_HANDLER_PRIORITY_NEUTRAL - 20); add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', EVENT_HANDLER_PRIORITY_NEUTRAL - 20);
add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10); add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10);
add_event_handler('init', 'bratonien_tools_preserve_connector_top_level_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 9);
add_event_handler('init', 'bratonien_tools_album_shares_init'); add_event_handler('init', 'bratonien_tools_album_shares_init');
add_event_handler('loc_end_intro', 'bratonien_tools_fix_admin_album_stat_tile');
add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories'); add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories');
add_event_handler('ws_add_methods', 'bratonien_tools_register_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods'); add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_productive_ws_methods'); add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_productive_ws_methods');
@@ -83,36 +84,108 @@ function bratonien_tools_prepare_private_album_permissions()
return; return;
} }
$result = pwg_query('SELECT status FROM '.CATEGORIES_TABLE.' WHERE id = '.$category_id.' LIMIT 1'); bratonien_tools_grant_private_album_access($category_id, (int)$user['id']);
if (!pwg_db_num_rows($result)) }
function bratonien_tools_preserve_connector_top_level_access()
{
global $user;
if (
!defined('IN_ADMIN')
|| ($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST'
|| (string)($_GET['page'] ?? '') !== 'site_update'
|| (string)($_POST['bratonien_connector'] ?? '') !== '1'
|| empty($user['id'])
)
{ {
return; return;
} }
$category = pwg_db_fetch_assoc($result); $site_id = (int)($_GET['site'] ?? 0);
if ($category['status'] !== 'public') $user_id = (int)$user['id'];
if ($site_id < 1 || $user_id < 1)
{ {
return; return;
} }
$users = isset($_POST['users']) && is_array($_POST['users']) ? $_POST['users'] : array(); register_shutdown_function(function () use ($site_id, $user_id) {
$current_user_id = (int)$user['id']; $query = '\nSELECT id\n FROM '.CATEGORIES_TABLE.'\n WHERE site_id = '.$site_id.'\n AND dir IS NOT NULL\n AND id_uppercat IS NULL\n AND status = \'private\'\n;';
$normalized_user_ids = array_map('intval', $users); $result = pwg_query($query);
$changed = false;
while ($row = pwg_db_fetch_assoc($result))
{
$category_id = (int)$row['id'];
if ($category_id < 1)
{
continue;
}
if (!in_array($current_user_id, $normalized_user_ids, true)) $access_query = '\nSELECT 1\n FROM '.USER_ACCESS_TABLE.'\n WHERE user_id = '.$user_id.'\n AND cat_id = '.$category_id.'\n LIMIT 1\n;';
if (pwg_db_num_rows(pwg_query($access_query)) > 0)
{
continue;
}
bratonien_tools_grant_private_album_access($category_id, $user_id);
$changed = true;
}
if ($changed && function_exists('invalidate_user_cache'))
{
invalidate_user_cache(true);
}
});
}
function bratonien_tools_grant_private_album_access($category_id, $user_id)
{
$category_id = (int)$category_id;
$user_id = (int)$user_id;
if ($category_id < 1 || $user_id < 1)
{ {
$users[] = $current_user_id; return;
} }
$_POST['users'] = $users; $query = '\nSELECT 1\n FROM '.USER_ACCESS_TABLE.'\n WHERE user_id = '.$user_id.'\n AND cat_id = '.$category_id.'\n LIMIT 1\n;';
$result = pwg_query($query);
if (pwg_db_num_rows($result) > 0)
{
return;
}
single_insert(
USER_ACCESS_TABLE,
array(
'user_id' => $user_id,
'cat_id' => $category_id,
)
);
}
function bratonien_tools_fix_admin_album_stat_tile()
{
global $template;
if (!defined('IN_ADMIN'))
{
return;
}
$template->set_prefilter('intro', 'bratonien_tools_prefilter_admin_album_stat_tile');
}
function bratonien_tools_prefilter_admin_album_stat_tile($source)
{
return str_replace('{if $NB_ALBUMS > 1}', '{if $NB_ALBUMS > 0}', $source);
} }
function bratonien_tools_admin_menu($menu) function bratonien_tools_admin_menu($menu)
{ {
$menu[] = array( $menu[] = array(
'NAME' => 'Bratonien Tools', 'NAME' => 'Bratonien Tools',
'URL' => get_root_url() . 'admin.php?page=plugin-' . BRATONIEN_TOOLS_ID, 'URL' => get_root_url() . 'admin.php?page=plugin-' . BRATONIEN_TOOLS_ID,
); );
return $menu; return $menu;
} }
?>

View File

@@ -1,284 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-cutover-v2.php <connection-id>\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_id<TAB>source_prefix<TAB>local_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);
}

View File

@@ -1,374 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: sudo php nc-connector-cutover.php <connection-id>\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_id<TAB>source_prefix<TAB>local_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);
}

View File

@@ -1,110 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-diagnose.php <connection-id>\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";
}

View File

@@ -1,81 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-disable.php <connection-id>\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);
}

View File

@@ -1,188 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-install.php <connection-id>\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_id<TAB>source_prefix<TAB>local_mount<TAB>include_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);
}

View File

@@ -1,183 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-legacy-cleanup.php <connection-id>\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);
}

View File

@@ -1,133 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
$configPath = '/etc/piwigo-sync/piwigo.conf';
$storagePath = '/etc/piwigo-sync/storages.tsv';
$bundlePath = '/tmp/bratonien-tools-nc-import.json';
function readLegacyConfig($path)
{
if (!is_readable($path))
{
throw new RuntimeException('Legacy-Konfiguration nicht lesbar: '.$path);
}
$config = 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);
}
}
$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);
}

View File

@@ -1,149 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-normalize.php <connection-id>\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);
}

View File

@@ -1,226 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "Dieses Hilfsprogramm darf nur auf der Kommandozeile ausgefuehrt werden.\n");
exit(1);
}
if (function_exists('posix_geteuid') && posix_geteuid() !== 0)
{
fwrite(STDERR, "Bitte als root ausfuehren.\n");
exit(1);
}
if ($argc !== 2 || !preg_match('/^[1-9][0-9]*$/', $argv[1]))
{
fwrite(STDERR, "Aufruf: php nc-connector-runtime-switch.php <connection-id>\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);
}

View File

@@ -1,86 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$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';
function cleanup_fail($message)
{
fwrite(STDERR, $message."\n");
exit(1);
}
function cleanup_tree($path)
{
$path = rtrim((string)$path, '/');
if ($path === '' || $path === '/' || !file_exists($path)) return;
if (is_link($path) || is_file($path))
{
@unlink($path);
return;
}
$items = @scandir($path);
if (!is_array($items)) return;
foreach ($items as $item)
{
if ($item === '.' || $item === '..') continue;
cleanup_tree($path.'/'.$item);
}
@rmdir($path);
}
if (!is_readable($dbConfig)) cleanup_fail('Piwigo-Datenbankkonfiguration ist nicht lesbar.');
$conf = array();
$prefixeTable = 'piwigo_';
require $dbConfig;
foreach (array('db_host','db_user','db_password','db_base') as $key)
{
if (!isset($conf[$key])) cleanup_fail('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
}
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->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);

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -25,6 +25,7 @@ from pathlib import Path, PurePosixPath
DAV = "DAV:" DAV = "DAV:"
OC = "http://owncloud.org/ns" OC = "http://owncloud.org/ns"
NC = "http://nextcloud.org/ns"
SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
# 1x1 transparent GIF, 34 bytes. The filename keeps the remote extension; # 1x1 transparent GIF, 34 bytes. The filename keeps the remote extension;
# the placeholder exists only so Piwigo can discover the logical image entry. # the placeholder exists only so Piwigo can discover the logical image entry.
@@ -54,6 +55,27 @@ def safe_local_name(name: str) -> str:
return name return name
def parse_image_dimensions(prop: ET.Element) -> tuple[int, int]:
for property_name in ("file-metadata-size", "metadata-photos-size"):
raw = prop.findtext(f"{{{NC}}}{property_name}", default="").strip()
if not raw:
continue
try:
value = json.loads(raw)
except (TypeError, ValueError, json.JSONDecodeError):
continue
if not isinstance(value, dict):
continue
try:
width = int(value.get("width", 0) or 0)
height = int(value.get("height", 0) or 0)
except (TypeError, ValueError):
continue
if width > 0 and height > 0:
return width, height
return 0, 0
class WebDavClient: class WebDavClient:
def __init__(self, base_url: str, user: str, password: str, timeout: int = 30) -> None: def __init__(self, base_url: str, user: str, password: str, timeout: int = 30) -> None:
self.base_url = base_url.rstrip("/") self.base_url = base_url.rstrip("/")
@@ -77,9 +99,11 @@ class WebDavClient:
url = self.collection_url(relative) url = self.collection_url(relative)
body = ( body = (
'<?xml version="1.0"?>' '<?xml version="1.0"?>'
'<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">' '<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">'
'<d:prop><d:displayname/><d:resourcetype/><d:getcontenttype/>' '<d:prop><d:displayname/><d:resourcetype/><d:getcontenttype/>'
'<d:getcontentlength/><d:getetag/><oc:fileid/></d:prop></d:propfind>' '<d:getcontentlength/><d:getetag/><oc:fileid/>'
'<nc:file-metadata-size/><nc:metadata-photos-size/>'
'</d:prop></d:propfind>'
).encode("utf-8") ).encode("utf-8")
request = urllib.request.Request(url, data=body, method="PROPFIND") request = urllib.request.Request(url, data=body, method="PROPFIND")
request.add_header("Authorization", self.auth_header) request.add_header("Authorization", self.auth_header)
@@ -122,6 +146,7 @@ class WebDavClient:
fileid_text = prop.findtext(f"{{{OC}}}fileid", default="").strip() fileid_text = prop.findtext(f"{{{OC}}}fileid", default="").strip()
resource_type = prop.find(f"{{{DAV}}}resourcetype") resource_type = prop.find(f"{{{DAV}}}resourcetype")
is_dir = resource_type is not None and resource_type.find(f"{{{DAV}}}collection") is not None is_dir = resource_type is not None and resource_type.find(f"{{{DAV}}}collection") is not None
width, height = parse_image_dimensions(prop)
item = { item = {
"display_name": display, "display_name": display,
"fileid": int(fileid_text) if fileid_text.isdigit() else 0, "fileid": int(fileid_text) if fileid_text.isdigit() else 0,
@@ -129,6 +154,8 @@ class WebDavClient:
"content_type": prop.findtext(f"{{{DAV}}}getcontenttype", default=""), "content_type": prop.findtext(f"{{{DAV}}}getcontenttype", default=""),
"size": int(prop.findtext(f"{{{DAV}}}getcontentlength", default="0") or 0), "size": int(prop.findtext(f"{{{DAV}}}getcontentlength", default="0") or 0),
"etag": prop.findtext(f"{{{DAV}}}getetag", default="").strip('"'), "etag": prop.findtext(f"{{{DAV}}}getetag", default="").strip('"'),
"width": width,
"height": height,
} }
if href_path == base_path: if href_path == base_path:
current = item current = item
@@ -197,6 +224,8 @@ def build_root(
"content_type": str(child.get("content_type", "")), "content_type": str(child.get("content_type", "")),
"size": int(child.get("size", 0)), "size": int(child.get("size", 0)),
"etag": str(child.get("etag", "")), "etag": str(child.get("etag", "")),
"width": int(child.get("width", 0) or 0),
"height": int(child.get("height", 0) or 0),
} }
return files, folders, skipped return files, folders, skipped
@@ -269,7 +298,20 @@ def main() -> int:
total_files += files total_files += files
total_folders += folders total_folders += folders
total_skipped += skipped total_skipped += skipped
manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}")
if remote_root == "":
for child in sorted(local_root.iterdir(), key=lambda item: (item.name.casefold(), item.name)):
child_data = mapping.get(str(child))
if not child_data:
continue
child_fileid = int(child_data.get("fileid", 0))
child_display = str(child_data.get("display_name", "")).strip() or child.name
child_type = "folder" if child.is_dir() else "file"
manifest.append(
f"webdav:{child_fileid}\t{child_type}\t{child_display}\t{source_dir / local_name / child.name}"
)
else:
manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}")
if previous.exists(): if previous.exists():
shutil.rmtree(previous) shutil.rmtree(previous)

View File

@@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
if ($argc !== 3) {
fwrite(STDERR, "Usage: piwigo-db-check.php MAP_FILE PIWIGO_ROOT\n");
exit(2);
}
$mapFile = $argv[1];
$piwigoRoot = rtrim($argv[2], '/') . '/';
$databaseConfig = $piwigoRoot . 'local/config/database.inc.php';
if (!is_file($databaseConfig)) {
exit(1);
}
$mapping = json_decode((string) file_get_contents($mapFile), true, 512, JSON_THROW_ON_ERROR);
$expected = [];
foreach ($mapping as $source => $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);

View File

@@ -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";

View File

@@ -50,7 +50,7 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 900, CURLOPT_TIMEOUT => 900,
CURLOPT_FOLLOWLOCATION => false, CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-NC-Connector', CURLOPT_USERAGENT => 'Bratonien-NC-Connector-WebDAV',
); );
if ($headers) $options[CURLOPT_HTTPHEADER] = $headers; if ($headers) $options[CURLOPT_HTTPHEADER] = $headers;
if ($cookie_file !== null) 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) 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'] ?? ''), '/'); $gallery_root = rtrim((string)($connection_config['parallel_gallery_root'] ?? ''), '/');
if ($gallery_root === '') fail_sync('WebDAV-Galeriewurzel fehlt in der Verbindungskonfiguration.'); 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); $escaped = $db->real_escape_string($site_url);
$result = $db->query("SELECT id FROM `{$prefixeTable}sites` WHERE galleries_url='{$escaped}' LIMIT 1"); $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) fail_sync('Piwigo-Site konnte nicht gelesen werden: '.$db->error);
if ($result->num_rows) if ($result->num_rows) return (int)$result->fetch_assoc()['id'];
{
return (int)$result->fetch_assoc()['id'];
}
if (!$db->query("INSERT INTO `{$prefixeTable}sites` (galleries_url) VALUES ('{$escaped}')")) 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); $site_id = ensure_webdav_site($db, $prefixeTable, $piwigo_root, $connection_config, $connection_id);
$api = array('key_id'=>'', 'key_secret'=>''); $api = array('key_id'=>'', 'key_secret'=>'');
$connection_scoped = (string)($connection_config['piwigo_auth'] ?? '') === 'connection-scoped' || array_key_exists('api_enabled', $connection_config); if (!empty($connection_config['api_enabled']))
if ($connection_scoped)
{ {
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'] ?? ''));
$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'] ?? '');
}
}
}
} }
$fallback_user = (string)($connection_credentials['piwigo_user'] ?? ''); $fallback_user = trim((string)($connection_credentials['piwigo_user'] ?? ''));
$fallback_password = (string)($connection_credentials['piwigo_password'] ?? ''); $fallback_password = (string)($connection_credentials['piwigo_password'] ?? '');
$api_error = null;
if ($api['key_id'] !== '' && $api['key_secret'] !== '') if ($api['key_id'] !== '' && $api['key_secret'] !== '')
{ {
try 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)); 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)); $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); $added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Synchronisierung per API erfolgreich (Site $site_id)\n"; echo "Piwigo-Synchronisierung per API erfolgreich (Site $site_id)\n";
@@ -266,14 +237,12 @@ try
} }
catch (Throwable $e) catch (Throwable $e)
{ {
$api_error = $e->getMessage(); fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$e->getMessage()."\n");
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n");
} }
} }
else else
{ {
$api_error = $connection_scoped ? 'Fuer diese Verbindung ist keine API konfiguriert.' : 'Keine gespeicherten API-Zugangsdaten.'; fwrite(STDERR, "Piwigo-API nicht nutzbar: Fuer diese Verbindung ist keine API konfiguriert.\n");
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$api_error."\n");
} }
if ($fallback_user === '' || $fallback_password === '') fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback fuer diese Verbindung vorhanden.'); if ($fallback_user === '' || $fallback_password === '') fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback fuer diese Verbindung vorhanden.');
@@ -291,10 +260,6 @@ try
$cookie_file $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)); $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); $added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich (Site $site_id)\n"; echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich (Site $site_id)\n";

View File

@@ -32,7 +32,7 @@ function fetch_remote_blob($url, $user, $password)
CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user.':'.$password, CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_FAILONERROR => false, CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.6.1', CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.6.8.21',
)); ));
$body = curl_exec($ch); $body = curl_exec($ch);
$errno = curl_errno($ch); $errno = curl_errno($ch);

View File

@@ -12,6 +12,7 @@ from pathlib import Path
ALLOWED = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.") ALLOWED = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.")
GERMAN = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "Ä": "Ae", "Ö": "Oe", "Ü": "Ue", "ß": "ss", "": "SS"}) GERMAN = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "Ä": "Ae", "Ö": "Oe", "Ü": "Ue", "ß": "ss", "": "SS"})
SHADOW_DIR_MODE = 0o777
def safe_name(name: str) -> str: def safe_name(name: str) -> str:
@@ -79,8 +80,13 @@ def preferred_target(source_key: str, raw_name: str, parent_target: Path, old_ma
return safe_name(raw_name) return safe_name(raw_name)
def ensure_shadow_directory(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
path.chmod(SHADOW_DIR_MODE)
def mirror_directory(source: Path, target: Path, source_key: str, target_key: Path, old_map: dict[str, str], new_map: dict[str, str]) -> None: def mirror_directory(source: Path, target: Path, source_key: str, target_key: Path, old_map: dict[str, str], new_map: dict[str, str]) -> None:
target.mkdir(parents=True, exist_ok=True) ensure_shadow_directory(target)
used: set[str] = set() used: set[str] = set()
for child in sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)): for child in sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)):
if child.is_symlink(): if child.is_symlink():
@@ -116,7 +122,7 @@ def build(manifest: Path, destination: Path, state_file: Path) -> None:
remove_tree(previous) remove_tree(previous)
if state_staging.exists(): if state_staging.exists():
state_staging.unlink() state_staging.unlink()
staging.mkdir(parents=True) ensure_shadow_directory(staging)
try: try:
used_roots: set[str] = set() used_roots: set[str] = set()

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$options = getopt('', array('piwigo-root:', 'connection-id:', 'mapping:'));
$piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/');
$connection_id = (int)($options['connection-id'] ?? 0);
$mapping_file = (string)($options['mapping'] ?? '');
if ($piwigo_root === '' || $connection_id < 1 || $mapping_file === '' || !is_readable($mapping_file))
{
fwrite(STDERR, "Parameter --piwigo-root, --connection-id und --mapping werden benoetigt.\n");
exit(1);
}
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (!is_array($mapping) || !isset($mapping['files']) || !is_array($mapping['files']))
{
fwrite(STDERR, "WebDAV-Mapping ist ungueltig.\n");
exit(1);
}
$dimensions = array();
foreach ($mapping['files'] as $path => $entry)
{
if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue;
$width = (int)($entry['width'] ?? 0);
$height = (int)($entry['height'] ?? 0);
if ($width < 1 || $height < 1) continue;
$dimensions[str_replace('\\', '/', (string)$path)] = array($width, $height);
}
if (!$dimensions)
{
fwrite(STDERR, "WebDAV-Mapping enthaelt keine Originalabmessungen.\n");
exit(1);
}
define('PHPWG_ROOT_PATH', $piwigo_root.'/');
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['SERVER_PORT'] = '80';
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/lib/sync-webdav-metadata.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Metadata/0.9.6.8.20';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
$checked = 0;
$updated = 0;
$missing = 0;
$result = pwg_query('SELECT id, path, width, height FROM '.IMAGES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result))
{
$path = (string)($row['path'] ?? '');
if ($path === '') continue;
$absolute = $path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $absolute), '/');
}
$normalized_shadow = str_replace('\\', '/', $absolute);
if (strpos($normalized_shadow, '/nc-webdav-gallery/connection-'.$connection_id.'/') === false) continue;
$checked++;
$resolved = realpath($absolute);
if ($resolved === false)
{
$missing++;
continue;
}
$key = str_replace('\\', '/', $resolved);
if (!isset($dimensions[$key]))
{
$missing++;
continue;
}
list($width, $height) = $dimensions[$key];
if ((int)$row['width'] === $width && (int)$row['height'] === $height) continue;
pwg_query('UPDATE '.IMAGES_TABLE.' SET width='.$width.', height='.$height.' WHERE id='.(int)$row['id']);
$updated++;
}
if ($updated > 0)
{
if (function_exists('update_category')) update_category('all');
if (function_exists('invalidate_user_cache')) invalidate_user_cache(true);
}
echo 'WebDAV-Metadaten: bilder='.$checked.' aktualisiert='.$updated.' ohne_masse='.$missing."\n";
exit($missing > 0 ? 1 : 0);

View File

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

View File

@@ -82,7 +82,6 @@ $configDir = '/etc/bratonien-tools/nc-connector';
$stateRoot = '/var/lib/bratonien-tools/nc-connector'; $stateRoot = '/var/lib/bratonien-tools/nc-connector';
$publicSourceRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-source'; $publicSourceRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-source';
$publicGalleryRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-gallery'; $publicGalleryRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-gallery';
$legacyGalleryRoot = rtrim($piwigoRoot, '/').'/galleries';
try try
{ {
@@ -104,14 +103,15 @@ try
$hexKey = trim((string)$keyResult->fetch_assoc()['value']); $hexKey = trim((string)$keyResult->fetch_assoc()['value']);
$table = $prefixeTable.'bratonien_tools_nc_connections'; $table = $prefixeTable.'bratonien_tools_nc_connections';
$rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id DESC"); $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('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error); 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($configDir, 0700);
@chmod($stateRoot, 0750);
@chmod($publicSourceRoot, 0755); @chmod($publicSourceRoot, 0755);
@chmod($publicGalleryRoot, 0755); @chmod($publicGalleryRoot, 0755);
@@ -123,9 +123,7 @@ try
$id = (int)$row['id']; $id = (int)$row['id'];
$config = json_decode((string)$row['config_json'], true); $config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) $config = array(); if (!is_array($config)) $config = array();
if ((string)$row['adapter'] !== 'remote') continue;
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue; if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
if (empty($config['parallel_test'])) continue;
try try
{ {
@@ -144,7 +142,6 @@ try
{ {
fwrite(STDERR, "NC WebDAV #{$id}: identische Quelle bereits durch Verbindung #".$seenFingerprints[$fingerprint]." abgedeckt; doppelte Runtime wird unterdrueckt.\n"); 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); 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); webdav_remove_generated_tree($publicGalleryRoot.'/connection-'.$id, $publicGalleryRoot);
continue; continue;
} }
@@ -156,18 +153,12 @@ try
if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_webdav_reconcile('State-Verzeichnis konnte nicht angelegt werden.'); if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_webdav_reconcile('State-Verzeichnis konnte nicht angelegt werden.');
@chmod($stateDir, 0750); @chmod($stateDir, 0750);
$legacyDefault = $legacyGalleryRoot.'/bratonien-webdav-'.$id;
$galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/'); $galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
if ($galleryRoot === '' || $galleryRoot === $legacyDefault || strpos($galleryRoot, $legacyGalleryRoot.'/bratonien-webdav-') === 0) $expectedGalleryRoot = $publicGalleryRoot.'/connection-'.$id;
{ if ($galleryRoot === '' || strpos($galleryRoot, $publicGalleryRoot.'/') !== 0) $galleryRoot = $expectedGalleryRoot;
$galleryRoot = $publicGalleryRoot.'/connection-'.$id;
}
if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.'); if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.');
@chmod($galleryRoot, 0755); @chmod($galleryRoot, 0755);
// Alte technische Wrapper unter ./galleries duerfen nicht als Piwigo-Alben auftauchen.
webdav_remove_generated_tree($legacyDefault, $legacyGalleryRoot);
$sourceDir = $publicSourceRoot.'/connection-'.$id; $sourceDir = $publicSourceRoot.'/connection-'.$id;
if (!is_dir($sourceDir) && !mkdir($sourceDir, 0755, true)) fail_webdav_reconcile('WebDAV-Platzhalterquelle konnte nicht angelegt werden.'); if (!is_dir($sourceDir) && !mkdir($sourceDir, 0755, true)) fail_webdav_reconcile('WebDAV-Platzhalterquelle konnte nicht angelegt werden.');
@chmod($sourceDir, 0755); @chmod($sourceDir, 0755);
@@ -189,7 +180,7 @@ try
{ {
$path = trim((string)($root['webdav_path'] ?? ''), '/'); $path = trim((string)($root['webdav_path'] ?? ''), '/');
$display = trim((string)($root['display_name'] ?? '')); $display = trim((string)($root['display_name'] ?? ''));
if ($path === '' || $display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.'); if ($display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.');
$rootLines[] = $path."\t".$display; $rootLines[] = $path."\t".$display;
} }
file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX); file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX);
@@ -215,21 +206,23 @@ try
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
@chmod($configPath, 0600); @chmod($configPath, 0600);
unset($config['parallel_test']);
$config['state_dir'] = $stateDir; $config['state_dir'] = $stateDir;
$config['status_file'] = $statusFile; $config['status_file'] = $statusFile;
$config['parallel_gallery_root'] = $galleryRoot; $config['parallel_gallery_root'] = $galleryRoot;
$config['source_fingerprint'] = $fingerprint; $config['source_fingerprint'] = $fingerprint;
$config['runtime'] = array( $config['runtime'] = array(
'mode'=>'parallel-webdav', 'mode'=>'webdav',
'config'=>$configPath, 'config'=>$configPath,
'piwigo_sync_enabled'=>true, 'piwigo_sync_enabled'=>true,
'reconciled_at'=>date('Y-m-d H:i:s'), 'reconciled_at'=>date('Y-m-d H:i:s'),
); );
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $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); fail_webdav_reconcile('WebDAV-Runtime-Status konnte nicht gespeichert werden: '.$db->error);
$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} LIMIT 1");
} }
} }
catch (Throwable $e) catch (Throwable $e)

View File

@@ -1,336 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
function fail_reconcile($message)
{
throw new RuntimeException($message);
}
function decrypt_reconcile($blob, $hexKey)
{
if (!preg_match('/^[a-f0-9]{64}$/', (string)$hexKey)) fail_reconcile('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_reconcile('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_reconcile('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
$decoded = json_decode($plain, true);
if (!is_array($decoded))
{
return array('db_password'=>$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_id<TAB>source_prefix<TAB>local_mount<TAB>include_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('# fileid<TAB>display_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);
}

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$pluginRoot = dirname(__DIR__);
$piwigoRoot = dirname($pluginRoot, 2);
define('PHPWG_ROOT_PATH', rtrim($piwigoRoot, '/').'/');
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['SERVER_PORT'] = '80';
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/repair-webdav-orphans.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$stateRoot = '/var/lib/bratonien-tools/nc-connector';
$marker = $stateRoot.'/.webdav-orphan-repair-0963.done';
try
{
if (is_file($marker))
{
echo "NC WebDAV Altlasten-Reparatur: bereits abgeschlossen.\n";
exit(0);
}
if (!is_dir($stateRoot) && !mkdir($stateRoot, 0750, true))
{
throw new RuntimeException('State-Verzeichnis konnte nicht angelegt werden: '.$stateRoot);
}
$relativePrefix = './_data/bratonien-tools/nc-webdav-gallery/connection-';
$absolutePrefix = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-webdav-gallery/connection-';
$relativeLength = strlen($relativePrefix);
$absoluteLength = strlen($absolutePrefix);
$query = "SELECT id,path FROM ".IMAGES_TABLE.
" WHERE LEFT(path,".$relativeLength.")='".pwg_db_real_escape_string($relativePrefix)."'".
" OR LEFT(path,".$absoluteLength.")='".pwg_db_real_escape_string($absolutePrefix)."'";
$result = pwg_query($query);
$staleIds = array();
while ($row = pwg_db_fetch_assoc($result))
{
$id = (int)$row['id'];
$storedPath = (string)$row['path'];
if ($id < 1 || $storedPath === '') continue;
if (strpos($storedPath, $relativePrefix) === 0)
{
$filesystemPath = rtrim(PHPWG_ROOT_PATH, '/').'/'.ltrim(substr($storedPath, 2), '/');
}
elseif (strpos($storedPath, $absolutePrefix) === 0)
{
$filesystemPath = $storedPath;
}
else
{
continue;
}
if (!is_file($filesystemPath))
{
$staleIds[] = $id;
}
}
$staleIds = array_values(array_unique(array_map('intval', $staleIds)));
if ($staleIds)
{
delete_elements($staleIds, false);
invalidate_user_cache(true);
}
$payload = date('c').' removed='.count($staleIds)."\n";
if (file_put_contents($marker, $payload, LOCK_EX) === false)
{
throw new RuntimeException('Abschlussmarker konnte nicht geschrieben werden: '.$marker);
}
@chmod($marker, 0640);
echo 'NC WebDAV Altlasten-Reparatur: entfernte verwaiste Bilder='.count($staleIds)."\n";
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, 'NC WebDAV Altlasten-Reparatur: '.$e->getMessage()."\n");
exit(1);
}

View File

@@ -5,83 +5,35 @@ CONFIG_DIR="/etc/bratonien-tools/nc-connector"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
shopt -s nullglob 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 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
if ! php "$SCRIPT_DIR/repair-webdav-orphans.php"; then
echo "NC Connector: verwaiste WebDAV-Bilddatensaetze konnten nicht repariert werden." >&2
exit 1 exit 1
fi 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 if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then
echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2 echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2
exit 1 exit 1
fi 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) webdav_configs=("$CONFIG_DIR"/webdav-connection-*.conf)
if [[ ${#configs[@]} -eq 0 && ${#webdav_configs[@]} -eq 0 ]]; then if [[ ${#webdav_configs[@]} -eq 0 ]]; then
echo "Keine NC-Connector-Verbindungen konfiguriert." echo "Keine WebDAV-Connector-Verbindungen konfiguriert."
exit 0 exit 0
fi fi
result=0 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 for config in "${webdav_configs[@]}"; do
name="$(basename "$config")" 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 if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh"; then
result=1 result=1
fi fi
done 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" exit "$result"

View File

@@ -66,14 +66,18 @@ compact_output() {
failure() { failure() {
local code="$1" command="$2" line="$3" local code="$1" command="$2" line="$3"
trap - ERR trap - ERR
write_status error "WebDAV-Shadow-Tree fehlgeschlagen" "Exit-Code: $code; Zeile: $line; Befehl: $command" write_status error "WebDAV-Synchronisierung fehlgeschlagen" "Exit-Code: $code; Zeile: $line; Befehl: $command"
exit "$code" exit "$code"
} }
trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR
write_status running "WebDAV-Synchronisierung läuft"
ROOT_ARGS=() ROOT_ARGS=()
while IFS=$'\t' read -r path display_name; do while IFS= read -r line; do
[[ -z "$path" || "$path" == \#* ]] && continue [[ -z "$line" || "$line" == \#* ]] && continue
[[ "$line" == *$'\t'* ]] || continue
path="${line%%$'\t'*}"
ROOT_ARGS+=(--root "$path") ROOT_ARGS+=(--root "$path")
done < "$WEBDAV_ROOTS_FILE" done < "$WEBDAV_ROOTS_FILE"
@@ -94,28 +98,6 @@ python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--state "$SHADOW_MAP_FILE" --state "$SHADOW_MAP_FILE"
trap - ERR trap - ERR
PREVIEW_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-preview/connection-$CONNECTION_ID"
PREVIEW_OUTPUT=""
PREVIEW_EXIT=0
if PREVIEW_OUTPUT="$(php "$SCRIPT_DIR/lib/precache-webdav-previews.php" \
--mapping="$WEBDAV_MAPPING_FILE" \
--base-url="$WEBDAV_BASE_URL" \
--user="$WEBDAV_USER" \
--password-file="$WEBDAV_PASSWORD_FILE" \
--cache-dir="$PREVIEW_CACHE" 2>&1)"; then
PREVIEW_EXIT=0
else
PREVIEW_EXIT=$?
fi
[[ -z "$PREVIEW_OUTPUT" ]] || printf '%s\n' "$PREVIEW_OUTPUT"
if [[ "$PREVIEW_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PREVIEW_EXIT"
if [[ -n "$PREVIEW_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PREVIEW_OUTPUT" | compact_output)"
fi
write_status error "WebDAV-Vorschaubilder konnten beim Einlesen nicht erzeugt werden" "$DETAIL"
exit "$PREVIEW_EXIT"
fi
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
PIWIGO_OUTPUT="" PIWIGO_OUTPUT=""
@@ -160,28 +142,29 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
exit "$PIWIGO_EXIT" exit "$PIWIGO_EXIT"
fi fi
DERIVATIVE_OUTPUT="" METADATA_OUTPUT=""
DERIVATIVE_EXIT=0 METADATA_EXIT=0
if DERIVATIVE_OUTPUT="$(php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \ if METADATA_OUTPUT="$(php "$SCRIPT_DIR/lib/sync-webdav-metadata.php" \
--piwigo-root="$PIWIGO_ROOT" \ --piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID" 2>&1)"; then --connection-id="$CONNECTION_ID" \
DERIVATIVE_EXIT=0 --mapping="$WEBDAV_MAPPING_FILE" 2>&1)"; then
METADATA_EXIT=0
else else
DERIVATIVE_EXIT=$? METADATA_EXIT=$?
fi fi
[[ -z "$DERIVATIVE_OUTPUT" ]] || printf '%s\n' "$DERIVATIVE_OUTPUT" [[ -z "$METADATA_OUTPUT" ]] || printf '%s\n' "$METADATA_OUTPUT"
if [[ "$DERIVATIVE_EXIT" -ne 0 ]]; then if [[ "$METADATA_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $DERIVATIVE_EXIT" DETAIL="Exit-Code: $METADATA_EXIT"
if [[ -n "$DERIVATIVE_OUTPUT" ]]; then if [[ -n "$METADATA_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$DERIVATIVE_OUTPUT" | compact_output)" DETAIL+="; Ausgabe: $(printf '%s\n' "$METADATA_OUTPUT" | compact_output)"
fi fi
write_status error "Piwigo-Derivate für WebDAV-Bilder konnten nicht erzeugt werden" "$DETAIL" write_status error "Piwigo-Bildabmessungen konnten nicht mit den WebDAV-Metadaten abgeglichen werden" "$DETAIL"
exit "$DERIVATIVE_EXIT" exit "$METADATA_EXIT"
fi fi
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \ write_status ok \
"WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" \ "WebDAV eingelesen und Piwigo synchronisiert" \
"" \ "" \
"api" \ "api" \
"ok" \ "ok" \
@@ -190,7 +173,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"Fallback wurde nicht benötigt" "Fallback wurde nicht benötigt"
elif grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then elif grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \ write_status ok \
"WebDAV eingelesen, Piwigo über Fallback synchronisiert und Derivate erzeugt" \ "WebDAV eingelesen und Piwigo über Fallback synchronisiert" \
"" \ "" \
"fallback" \ "fallback" \
"not_used" \ "not_used" \
@@ -198,8 +181,8 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"ok" \ "ok" \
"Benutzername/Passwort-Fallback erfolgreich" "Benutzername/Passwort-Fallback erfolgreich"
else else
write_status ok "WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" write_status ok "WebDAV eingelesen und Piwigo synchronisiert"
fi fi
else else
write_status ok "WebDAV eingelesen und Vorschaubilder erzeugt; Registrierung erfolgt über den bestehenden produktiven Piwigo-Sync" write_status ok "WebDAV eingelesen; Registrierung erfolgt über den bestehenden produktiven Piwigo-Sync"
fi fi

View File

@@ -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

View File

@@ -1,6 +1,6 @@
<section class="bratonien-section" id="nc-connector"> <section class="bratonien-section" id="nc-connector">
<h3>NC Connector</h3> <h3>NC Connector</h3>
<p class="bratonien-section__intro">Verbindet Nextcloud mit Piwigo und hält freigegebene Bilder automatisch aktuell.</p> <p class="bratonien-section__intro">Verbindet Nextcloud per WebDAV mit Piwigo und hält die ausgewählten Bilder automatisch aktuell.</p>
{assign var=nc_system_available value=isset($NC_CONNECTOR.system)} {assign var=nc_system_available value=isset($NC_CONNECTOR.system)}
@@ -8,12 +8,16 @@
<div class="bratonien-card"> <div class="bratonien-card">
<h4>Status</h4> <h4>Status</h4>
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<span class="bratonien-label">Verbindungen</span><strong>{$NC_CONNECTOR.connection_count|escape:html}</strong> <span class="bratonien-label">WebDAV-Verbindungen</span><strong>{$NC_CONNECTOR.connection_count|escape:html}</strong>
<span class="bratonien-label">Automatischer Abgleich</span><strong>{if $nc_system_available && $NC_CONNECTOR.system.timer_active && $NC_CONNECTOR.system.timer_enabled}Aktiv{else}Nicht aktiv{/if}</strong> <span class="bratonien-label">Automatischer Abgleich</span><strong>{if $nc_system_available && $NC_CONNECTOR.system.timer_active && $NC_CONNECTOR.system.timer_enabled}Aktiv{else}Nicht aktiv{/if}</strong>
<span class="bratonien-label">Letzter Lauf</span><strong data-nc-last-run>{if $nc_system_available}{$NC_CONNECTOR.system.last_run_label|escape:html}{else}Nicht verfügbar{/if}</strong> <span class="bratonien-label">Letzter Lauf</span><strong data-nc-last-run>{if $nc_system_available}{$NC_CONNECTOR.system.last_run_label|escape:html}{else}Nicht verfügbar{/if}</strong>
<span class="bratonien-label">Nächster Lauf</span><strong data-nc-next-run>{if $nc_system_available}{$NC_CONNECTOR.system.next_run_label|escape:html}{else}Nicht verfügbar{/if}</strong> <span class="bratonien-label">Nächster Lauf</span><strong data-nc-next-run>{if $nc_system_available}{$NC_CONNECTOR.system.next_run_label|escape:html}{else}Nicht verfügbar{/if}</strong>
</div> </div>
{if $nc_system_available && $NC_CONNECTOR.system.last_run_message}<p class="bratonien-base-note">Letztes Ergebnis: <strong>{$NC_CONNECTOR.system.last_run_message|escape:html}</strong></p>{/if} {if $nc_system_available && $NC_CONNECTOR.system.last_run_message}<p class="bratonien-base-note">Letztes Ergebnis: <strong>{$NC_CONNECTOR.system.last_run_message|escape:html}</strong></p>{/if}
<form method="post" class="bratonien-actions" style="margin-top:.75rem">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_run_now"{if $NC_CONNECTOR.connection_count < 1} disabled{/if}>Jetzt abrufen</button>
</form>
{if $nc_system_available && $NC_CONNECTOR.system.last_run_api_state == 'error'}<p class="bratonien-main-cache__warning"><strong>API:</strong> {$NC_CONNECTOR.system.last_run_api_message|escape:html}</p>{/if} {if $nc_system_available && $NC_CONNECTOR.system.last_run_api_state == 'error'}<p class="bratonien-main-cache__warning"><strong>API:</strong> {$NC_CONNECTOR.system.last_run_api_message|escape:html}</p>{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_fallback_state == 'error'}<p class="bratonien-main-cache__warning"><strong>Fallback:</strong> {$NC_CONNECTOR.system.last_run_fallback_message|escape:html}</p>{/if} {if $nc_system_available && $NC_CONNECTOR.system.last_run_fallback_state == 'error'}<p class="bratonien-main-cache__warning"><strong>Fallback:</strong> {$NC_CONNECTOR.system.last_run_fallback_message|escape:html}</p>{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_error_detail}<details><summary>Technische Fehlerdetails</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.system.last_run_error_detail|escape:html}</p></details>{/if} {if $nc_system_available && $NC_CONNECTOR.system.last_run_error_detail}<details><summary>Technische Fehlerdetails</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.system.last_run_error_detail|escape:html}</p></details>{/if}
@@ -21,10 +25,9 @@
<div class="bratonien-card"> <div class="bratonien-card">
<h4>Neue Verbindung</h4> <h4>Neue Verbindung</h4>
<p class="bratonien-base-note">Für die normale Einrichtung empfehlen wir den Assistenten.</p> <p class="bratonien-base-note">Nextcloud wird ausschließlich per WebDAV angebunden.</p>
<div class="bratonien-actions"> <div class="bratonien-actions">
<button class="buttonLike" type="button" id="bratonien-nc-wizard-open">Mit Assistent anlegen</button> <button class="buttonLike" type="button" id="bratonien-nc-wizard-open">WebDAV-Verbindung anlegen</button>
<button class="buttonLike" type="button" id="bratonien-nc-technical-open">Ohne Assistent anlegen</button>
</div> </div>
</div> </div>
</div> </div>
@@ -33,32 +36,21 @@
<div style="padding:1.25rem 1.5rem"> <div style="padding:1.25rem 1.5rem">
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem"> <div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem">
<div> <div>
<h4 style="margin:0">Neue Verbindung</h4> <h4 style="margin:0">Neue WebDAV-Verbindung</h4>
<p class="bratonien-base-note" style="margin:.35rem 0 0"><strong> <p class="bratonien-base-note" style="margin:.35rem 0 0"><strong>
{if $NC_CONNECTOR.wizard.step == 1} {if $NC_CONNECTOR.wizard.step == 1}Anmeldung
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_stage == 'database_details'} {elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_complete}Verbindung benennen
Datenbank prüfen {elseif $NC_CONNECTOR.wizard.step == 3}Piwigo-API
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts' && !$NC_CONNECTOR.wizard.directory_selection_ready} {elseif $NC_CONNECTOR.wizard.step == 4}Abschluss
Speicher zuordnen {else}Einrichtung{/if}
{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}
</strong></p> </strong></p>
</div> </div>
<button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button> <button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button>
</div> </div>
{if $NC_CONNECTOR.wizard.step == 1} {if $NC_CONNECTOR.wizard.step == 1}
<p class="bratonien-base-note">Adresse und Zugang reichen für den ersten Scan. Der Assistent prüft den erreichbaren Web-Zugang automatisch.</p> <p class="bratonien-base-note">Adresse und Nextcloud-Zugang genügen. Der Assistent prüft HTTP/HTTPS und anschließend den WebDAV-Zugriff des angemeldeten Benutzers.</p>
<form method="post" data-bratonien-wizard-form> <form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
@@ -73,113 +65,70 @@
</form> </form>
{elseif $NC_CONNECTOR.wizard.step == 2} {elseif $NC_CONNECTOR.wizard.step == 2}
<p class="bratonien-base-note"><strong>Nextcloud wurde gefunden.</strong> Für den Datenzugriff wird die bekannte Reader-Verbindung verwendet. Verzeichnisse werden ausschließlich mit dem angemeldeten Nextcloud-Benutzer gelesen.</p> <p class="bratonien-base-note"><strong>Nextcloud wurde gefunden.</strong> Angezeigt werden ausschließlich Verzeichnisse des angemeldeten Nextcloud-Benutzers.</p>
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<span class="bratonien-label">Adresse</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong> <span class="bratonien-label">Adresse</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
<span class="bratonien-label">Version</span><strong>{if $NC_CONNECTOR.wizard.version}{$NC_CONNECTOR.wizard.version|escape:html}{else}Nicht gemeldet{/if}</strong> <span class="bratonien-label">Version</span><strong>{if $NC_CONNECTOR.wizard.version}{$NC_CONNECTOR.wizard.version|escape:html}{else}Nicht gemeldet{/if}</strong>
<span class="bratonien-label">Angemeldet als</span><strong>{$NC_CONNECTOR.wizard.username|escape:html}{if $NC_CONNECTOR.wizard.display_name} · {$NC_CONNECTOR.wizard.display_name|escape:html}{/if}</strong> <span class="bratonien-label">Angemeldet als</span><strong>{$NC_CONNECTOR.wizard.username|escape:html}{if $NC_CONNECTOR.wizard.display_name} · {$NC_CONNECTOR.wizard.display_name|escape:html}{/if}</strong>
</div> </div>
{if $NC_CONNECTOR.wizard.technical_stage == 'database_details'} {if $NC_CONNECTOR.wizard.technical_stage == 'mounts' && $NC_CONNECTOR.wizard.directory_selection_ready}
<hr> <hr>
<h5>Datenbank-Adresse prüfen</h5> <h5>Verzeichnisse auswählen</h5>
<p class="bratonien-base-note">Die bekannte Reader-Verbindung konnte mit der gespeicherten Adresse nicht bestätigt werden.</p> <div class="bratonien-form-grid">
{if $NC_CONNECTOR.wizard.technical_error}<details><summary>Technische Details</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.wizard.technical_error|escape:html}</p></details>{/if} <span class="bratonien-label">Ausgewählt</span>
<form method="post" data-bratonien-wizard-form> <div>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> {if $NC_CONNECTOR.wizard.directory_selected|@count > 0}
<div class="bratonien-form-grid"> {foreach from=$NC_CONNECTOR.wizard.directory_selected item=selected_path}
<label class="bratonien-label">Datenbank-Adresse</label><input name="nc_wizard_db_host" type="text" value="{$NC_CONNECTOR.wizard.db_host|escape:html}" required> <form method="post" class="bratonien-actions" style="margin:.25rem 0" data-bratonien-wizard-form>
<label class="bratonien-label">Port</label><input name="nc_wizard_db_port" type="number" min="1" max="65535" value="{$NC_CONNECTOR.wizard.db_port|escape:html}" required>
<label class="bratonien-label">Datenbank</label><input name="nc_wizard_db_database" type="text" value="{$NC_CONNECTOR.wizard.db_database|escape:html}" required>
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_technical">Erneut prüfen</button></p>
</form>
{elseif $NC_CONNECTOR.wizard.technical_stage == 'mounts'}
<hr>
{if $NC_CONNECTOR.wizard.directory_selection_ready}
<h5>Verzeichnisse auswählen</h5>
<p class="bratonien-base-note">Es werden nur Verzeichnisse angezeigt, auf die <strong>{$NC_CONNECTOR.wizard.username|escape:html}</strong> in Nextcloud Zugriff hat. Mehrere Verzeichnisse können hinzugefügt werden. Bleibt die Auswahl leer, wird automatisch das Stammverzeichnis verwendet.</p>
<div class="bratonien-form-grid">
<span class="bratonien-label">Ausgewählt</span>
<div>
{if $NC_CONNECTOR.wizard.directory_selected|@count > 0}
{foreach from=$NC_CONNECTOR.wizard.directory_selected item=selected_path}
<form method="post" class="bratonien-actions" style="margin:.25rem 0" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_remove" value="{$selected_path|escape:html}">
<strong style="flex:1">{if $selected_path}{$selected_path|escape:html}{else}Stammverzeichnis{/if}</strong>
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_remove">Entfernen</button>
</form>
{/foreach}
{else}
<span class="bratonien-base-note">Keine Auswahl Stammverzeichnis wird verwendet.</span>
{/if}
</div>
<span class="bratonien-label">Aktueller Ordner</span><strong>/{if $NC_CONNECTOR.wizard.directory_path}{$NC_CONNECTOR.wizard.directory_path|escape:html}{/if}</strong>
</div>
<div class="bratonien-actions" style="margin:.75rem 0">
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_add">Diesen Ordner hinzufügen</button>
</form>
{if $NC_CONNECTOR.wizard.directory_path}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_path" value="{$NC_CONNECTOR.wizard.directory_parent|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_browse">Eine Ebene hoch</button>
</form>
{/if}
</div>
<div style="margin:.75rem 0">
{if $NC_CONNECTOR.wizard.directory_children|@count > 0}
{foreach from=$NC_CONNECTOR.wizard.directory_children item=directory_name key=directory_path}
<form method="post" style="margin:.3rem 0" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_path" value="{$directory_path|escape:html}"> <input type="hidden" name="nc_wizard_directory_remove" value="{$selected_path|escape:html}">
<button class="buttonLike" style="width:100%;text-align:left" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_browse">📁 {$directory_name|escape:html}</button> <strong style="flex:1">{if $selected_path}{$selected_path|escape:html}{else}Stammverzeichnis{/if}</strong>
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_remove">Entfernen</button>
</form> </form>
{/foreach} {/foreach}
{else} {else}
<p class="bratonien-base-note">Keine Unterordner vorhanden.</p> <span class="bratonien-base-note">Noch kein Verzeichnis ausgewählt.</span>
{/if} {/if}
</div> </div>
<span class="bratonien-label">Aktueller Ordner</span><strong>/{if $NC_CONNECTOR.wizard.directory_path}{$NC_CONNECTOR.wizard.directory_path|escape:html}{/if}</strong>
</div>
<div class="bratonien-actions" style="margin:.75rem 0">
<form method="post" data-bratonien-wizard-form> <form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
{foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index} <button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_add">Diesen Ordner hinzufügen</button>
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}"> </form>
{if $NC_CONNECTOR.wizard.directory_path}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_path" value="{$NC_CONNECTOR.wizard.directory_parent|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_browse">Eine Ebene hoch</button>
</form>
{/if}
</div>
<div style="margin:.75rem 0">
{if $NC_CONNECTOR.wizard.directory_children|@count > 0}
{foreach from=$NC_CONNECTOR.wizard.directory_children item=directory_name key=directory_path}
<form method="post" style="margin:.3rem 0" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="nc_wizard_directory_path" value="{$directory_path|escape:html}">
<button class="buttonLike" style="width:100%;text-align:left" type="submit" name="bratonien_tool" value="nc_connector_wizard_directory_browse">📁 {$directory_name|escape:html}</button>
</form>
{/foreach} {/foreach}
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Verzeichnisse übernehmen</button></p> {else}
</form> <p class="bratonien-base-note">Keine Unterordner vorhanden.</p>
{else} {/if}
<h5>Speicherort bestätigen</h5> </div>
<p class="bratonien-base-note">Die Datenquelle wurde gefunden. Ein technischer Speicherort konnte nicht automatisch zugeordnet werden.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
{foreach from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index}
<span class="bratonien-label">Speicher {$storage_index+1}</span>
{if $storage.local_mount}
<strong>Automatisch erkannt</strong>
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}">
{else}
<input name="nc_wizard_storage_mount[{$storage_index|escape:html}]" type="text" placeholder="Eingebundener Speicherpfad" required>
{/if}
{/foreach}
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Speicher prüfen</button></p>
</form>
{/if}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Verzeichnisse übernehmen</button></p>
</form>
{elseif $NC_CONNECTOR.wizard.technical_complete} {elseif $NC_CONNECTOR.wizard.technical_complete}
<hr> <hr>
<h5>Verbindung benennen</h5> <h5>Verbindung benennen</h5>
<p class="bratonien-base-note">Diese Verbindung verwendet ausschließlich den in Schritt 1 angemeldeten Nextcloud-Benutzer.</p>
<form method="post" data-bratonien-wizard-form> <form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
@@ -196,20 +145,20 @@
</div> </div>
{elseif $NC_CONNECTOR.wizard.step == 3} {elseif $NC_CONNECTOR.wizard.step == 3}
<p class="bratonien-base-note"><strong>Nextcloud ist vorbereitet.</strong> Jetzt wird der bevorzugte Piwigo-Zugang geprüft.</p> <p class="bratonien-base-note"><strong>WebDAV ist vorbereitet.</strong> Jetzt wird der Piwigo-Zugang dieser Verbindung geprüft.</p>
{if $NC_CONNECTOR.wizard.api_error}<p class="bratonien-main-cache__warning"><strong>API-Test fehlgeschlagen:</strong> {$NC_CONNECTOR.wizard.api_error|escape:html}</p>{/if} {if $NC_CONNECTOR.wizard.api_error}<p class="bratonien-main-cache__warning"><strong>API-Test fehlgeschlagen:</strong> {$NC_CONNECTOR.wizard.api_error|escape:html}</p>{/if}
<form method="post" data-bratonien-wizard-form> <form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"> <input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<label class="bratonien-label" for="nc_wizard_api_key_id">API-Schlüssel-ID</label><input id="nc_wizard_api_key_id" name="nc_wizard_api_key_id" type="text" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_id|escape:html}" placeholder="leer = bereits gespeicherten Zugang testen"> <label class="bratonien-label" for="nc_wizard_api_key_id">API-Schlüssel-ID</label><input id="nc_wizard_api_key_id" name="nc_wizard_api_key_id" type="text" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_id|escape:html}">
<label class="bratonien-label" for="nc_wizard_api_key_secret">API-Geheimnis</label><input id="nc_wizard_api_key_secret" name="nc_wizard_api_key_secret" type="password" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_secret|escape:html}" placeholder="leer = bereits gespeicherten Zugang testen"> <label class="bratonien-label" for="nc_wizard_api_key_secret">API-Geheimnis</label><input id="nc_wizard_api_key_secret" name="nc_wizard_api_key_secret" type="password" autocomplete="off" value="{$NC_CONNECTOR.wizard._api_key_secret|escape:html}">
</div> </div>
<div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_test">API testen</button><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_skip" formnovalidate>Überspringen</button></div> <div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_test">API testen</button><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_api_skip" formnovalidate>Überspringen</button></div>
</form> </form>
<form method="post" style="margin-top:1rem" data-bratonien-wizard-form><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_back">Zurück</button></form> <form method="post" style="margin-top:1rem" data-bratonien-wizard-form><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_back">Zurück</button></form>
{elseif $NC_CONNECTOR.wizard.step == 4} {elseif $NC_CONNECTOR.wizard.step == 4}
<p class="bratonien-base-note"><strong>Fast fertig.</strong> Die Verbindung wurde noch nicht angelegt. Erst der letzte Button übernimmt die Einstellungen.</p> <p class="bratonien-base-note"><strong>Fast fertig.</strong> Erst der letzte Button legt die WebDAV-Verbindung an.</p>
<div class="bratonien-form-grid"> <div class="bratonien-form-grid">
<span class="bratonien-label">Verbindung</span><strong>{$NC_CONNECTOR.wizard.connection_name|escape:html}</strong> <span class="bratonien-label">Verbindung</span><strong>{$NC_CONNECTOR.wizard.connection_name|escape:html}</strong>
<span class="bratonien-label">Nextcloud</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong> <span class="bratonien-label">Nextcloud</span><strong>{$NC_CONNECTOR.wizard.base_url|escape:html}</strong>
@@ -232,44 +181,16 @@
</div> </div>
</dialog> </dialog>
<details id="bratonien-nc-technical-create" class="bratonien-card" style="margin-top:1.5rem">
<summary style="display:none">Ohne Assistent anlegen</summary>
<h4>Technische Einrichtung</h4>
<p class="bratonien-main-cache__warning">Nur verwenden, wenn die technische Zuordnung bekannt ist. Falsche Pfade können die Synchronisierung auf den falschen Datenbestand richten.</p>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">Name</label><input name="nc_name" type="text" required>
<label class="bratonien-label">PostgreSQL-Host</label><input name="nc_host" type="text" required>
<label class="bratonien-label">PostgreSQL-Port</label><input name="nc_port" type="number" min="1" max="65535" value="5432" required>
<label class="bratonien-label">Datenbank</label><input name="nc_database" type="text" value="nextcloud" required>
<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" type="text" required>
<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" required>
<label class="bratonien-label">Source-View</label><input name="nc_source_view" type="text" value="piwigo_showcase_sources" required>
<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" type="text" value="piwigo_showcase_activity" required>
<label class="bratonien-label">Piwigo-Galeriepfad</label><input name="nc_gallery_root" type="text" placeholder="/var/www/piwigo/galleries/nextcloud" required>
<label class="bratonien-label">Fallback-Benutzer</label><input name="nc_piwigo_user" type="text">
<label class="bratonien-label">Fallback-Passwort</label><input name="nc_piwigo_password" type="password" autocomplete="new-password">
<label class="bratonien-label">Ruhezeit</label><input name="nc_quiet_seconds" type="number" min="0" value="120">
<label class="bratonien-label">Maximale Wartezeit</label><input name="nc_max_wait_seconds" type="number" min="60" value="900">
<label class="bratonien-label">Vollprüfung nach</label><input name="nc_full_sync_seconds" type="number" min="300" value="86400">
</div>
<p><strong>Storage-Zuordnungen</strong></p><p class="bratonien-base-note">Format: Storage-ID | optionales Quellpräfix | lokaler Speicherpfad</p><textarea name="nc_storages" rows="4" style="width:100%" required></textarea>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_create_local">Verbindung anlegen</button></p>
</form>
</details>
<div class="bratonien-card" style="margin-top:1.5rem"> <div class="bratonien-card" style="margin-top:1.5rem">
<h4>Bestehende Verbindungen</h4> <h4>Bestehende WebDAV-Verbindungen</h4>
{if $NC_CONNECTOR.connection_count > 0} {if $NC_CONNECTOR.connection_count > 0}
{foreach from=$NC_CONNECTOR.connections item=connection} {foreach from=$NC_CONNECTOR.connections item=connection}
<details style="margin:.6rem 0"> <details style="margin:.6rem 0">
<summary><strong>{$connection.display_name|escape:html}</strong> · {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}</summary> <summary><strong>{$connection.display_name|escape:html}</strong> · aktiv{if isset($connection.last_sync) && ($connection.last_sync.state == 'error' || $connection.last_sync.state == 'warning')} · Laufzeitproblem{/if}</summary>
<div style="padding:.75rem 0"> <div style="padding:.75rem 0">
<form method="post" class="bratonien-actions"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><input name="connection_name" type="text" value="{$connection.name|escape:html}" required><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_name">Name speichern</button></form> <form method="post" class="bratonien-actions"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><input name="connection_name" type="text" value="{$connection.name|escape:html}" required><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_name">Name speichern</button></form>
<div class="bratonien-actions" style="margin-top:.6rem"> <div class="bratonien-actions" style="margin-top:.6rem">
{if $connection.adapter == 'local' && !$connection.enabled}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_verify">Verbindung prüfen</button></form>{/if} <form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_delete" onclick="return confirm('WebDAV-Verbindung wirklich löschen? Die zugehörigen Piwigo-Inhalte dieser Verbindung werden entfernt; Nextcloud-Dateien bleiben unverändert.');">Löschen</button></form>
<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}"><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_delete" onclick="return confirm('Verbindung wirklich löschen? Die Verbindung wird sofort aus Bratonien Tools entfernt und beim nächsten Connector-Lauf auch aus der Laufzeit entfernt. Bilder in Nextcloud oder Piwigo werden nicht gelöscht.');">Löschen</button></form>
</div> </div>
{if isset($connection.last_sync) && $connection.last_sync.timestamp > 0} {if isset($connection.last_sync) && $connection.last_sync.timestamp > 0}
<p class="bratonien-base-note"><strong>Letzter Abgleich:</strong> {$connection.last_sync.label|escape:html} · {$connection.last_sync.message|escape:html}</p> <p class="bratonien-base-note"><strong>Letzter Abgleich:</strong> {$connection.last_sync.label|escape:html} · {$connection.last_sync.message|escape:html}</p>
@@ -281,49 +202,21 @@
{else} {else}
<p class="bratonien-base-note"><strong>Laufzeit:</strong> Noch kein Laufstatus für diese Verbindung vorhanden.</p> <p class="bratonien-base-note"><strong>Laufzeit:</strong> Noch kein Laufstatus für diese Verbindung vorhanden.</p>
{/if} {/if}
{if isset($connection.config.api_enabled)} <p class="bratonien-base-note"><strong>Nextcloud:</strong> {$connection.config.nextcloud_url|escape:html}</p>
<p class="bratonien-base-note"><strong>Piwigo-Zugang dieser Verbindung:</strong> {if $connection.config.api_enabled}eigene API{elseif $connection.fallback_stored}Fallback{else}kein Zugang gespeichert{/if}</p> <p class="bratonien-base-note"><strong>Nextcloud-Benutzer:</strong> {$connection.user|escape:html}</p>
{else} <p class="bratonien-base-note"><strong>Ausgewählte Wurzeln:</strong> {$connection.storage_count|escape:html}</p>
<p class="bratonien-base-note"><strong>Piwigo-Zugang:</strong> bestehende Legacy-Konfiguration</p> <p class="bratonien-base-note"><strong>Piwigo-Zugang:</strong> {if $connection.config.api_enabled}eigene API{elseif $connection.fallback_stored}Fallback{else}kein Zugang gespeichert{/if}</p>
{/if}
{if $connection.verification_checks|@count > 0}<details><summary>Letzte Prüfung</summary><ul>{foreach from=$connection.verification_checks item=check}<li>{if $check.ok}{else}{/if} {$check.name|escape:html}: {$check.detail|escape:html}</li>{/foreach}</ul></details>{/if}
<details style="margin-top:.75rem"><summary>Technische Einstellungen</summary>
{if $connection.enabled || $connection.takeover_state == 'active'}<p class="bratonien-main-cache__warning">Aktive Verbindungen können technisch nicht geändert werden.</p>{else}
<p class="bratonien-main-cache__warning">Nur ändern, wenn die technische Zuordnung bekannt ist.</p>
<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><input type="hidden" name="connection_id" value="{$connection.id|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">PostgreSQL-Host</label><input name="nc_host" type="text" value="{$connection.config.host|escape:html}" required>
<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="{$connection.config.port|escape:html}" required>
<label class="bratonien-label">Datenbank</label><input name="nc_database" type="text" value="{$connection.config.database|escape:html}" required>
<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" type="text" value="{$connection.config.user|escape:html}" required>
<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" placeholder="leer = unverändert">
<label class="bratonien-label">Source-View</label><input name="nc_source_view" type="text" value="{$connection.config.source_view|escape:html}" required>
<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" type="text" value="{$connection.config.activity_view|escape:html}" required>
<label class="bratonien-label">Piwigo-Galeriepfad</label><input name="nc_gallery_root" type="text" value="{$connection.config.gallery_root|escape:html}" required>
<label class="bratonien-label">Ruhezeit</label><input name="nc_quiet_seconds" type="number" min="0" value="{$connection.config.quiet_seconds|escape:html}">
<label class="bratonien-label">Maximale Wartezeit</label><input name="nc_max_wait_seconds" type="number" min="60" value="{$connection.config.max_wait_seconds|escape:html}">
<label class="bratonien-label">Vollprüfung nach</label><input name="nc_full_sync_seconds" type="number" min="300" value="{$connection.config.full_sync_seconds|escape:html}">
</div>
<p><strong>Storage-Zuordnungen</strong></p><p class="bratonien-base-note">Format: Storage-ID | optionales Quellpräfix | lokaler Speicherpfad</p><textarea name="nc_storages" rows="4" style="width:100%" required>{$connection.storage_text|escape:html}</textarea>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_technical">Technische Einstellungen speichern</button></p>
</form>
{/if}
</details>
</div> </div>
</details> </details>
{/foreach} {/foreach}
{else}<p class="bratonien-base-note">Noch keine Verbindung vorhanden.</p>{/if} {else}<p class="bratonien-base-note">Noch keine WebDAV-Verbindung vorhanden.</p>{/if}
</div> </div>
<details class="bratonien-card" style="margin-top:1.5rem"><summary>Erweiterte Piwigo-Zugänge</summary> {if $NC_CONNECTOR.connection_count > 0}
<h4>Piwigo API</h4> <details class="bratonien-card" style="margin-top:1.5rem"><summary>Fallback-Zugang einer Verbindung ändern</summary>
<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">API-Schlüssel-ID</label><input name="nc_piwigo_api_key_id" type="text" autocomplete="off" required><label class="bratonien-label">API-Geheimnis</label><input name="nc_piwigo_api_key_secret" type="password" autocomplete="off" required></div><div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_test">API prüfen und speichern</button><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_piwigo_api_delete" formnovalidate>Gespeicherte API löschen</button></div></form> <form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">Verbindung</label><select name="connection_id" required>{foreach from=$NC_CONNECTOR.connections item=connection}<option value="{$connection.id|escape:html}">{$connection.name|escape:html}</option>{/foreach}</select><label class="bratonien-label">Piwigo-Benutzer</label><input name="nc_fallback_user" type="text" autocomplete="username"><label class="bratonien-label">Piwigo-Passwort</label><input name="nc_fallback_password" type="password" autocomplete="current-password"></div><div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_save">Fallback speichern</button><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_fallback_delete" formnovalidate>Fallback löschen</button></div></form>
<h4 style="margin-top:1rem">Fallback speichern</h4>
{if $NC_CONNECTOR.connection_count > 0}<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">Verbindung</label><select name="connection_id" required>{foreach from=$NC_CONNECTOR.connections item=connection}<option value="{$connection.id|escape:html}">{$connection.name|escape:html}</option>{/foreach}</select><label class="bratonien-label">Piwigo-Benutzer</label><input name="nc_fallback_user" type="text" autocomplete="username"><label class="bratonien-label">Piwigo-Passwort</label><input name="nc_fallback_password" type="password" autocomplete="current-password"></div><div class="bratonien-actions"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_save">Fallback speichern</button><button class="buttonLike bratonien-delete-button" type="submit" name="bratonien_tool" value="nc_connector_fallback_delete" formnovalidate>Fallback löschen</button></div></form>{/if}
<h4 style="margin-top:1rem">Einmaliger Fallback-Test</h4>
<p class="bratonien-base-note">Verwendet die eingegebenen Piwigo-Zugangsdaten einmalig und speichert sie nicht.</p>
<form method="post"><input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}"><div class="bratonien-form-grid"><label class="bratonien-label">Piwigo-Benutzer</label><input name="nc_fallback_user" type="text" required><label class="bratonien-label">Piwigo-Passwort</label><input name="nc_fallback_password" type="password" required></div><p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_fallback_once">Einmalig testen</button></p></form>
</details> </details>
{/if}
{literal} {literal}
<script> <script>
@@ -331,8 +224,6 @@
var dialog=document.getElementById('bratonien-nc-wizard-dialog'); var dialog=document.getElementById('bratonien-nc-wizard-dialog');
var openButton=document.getElementById('bratonien-nc-wizard-open'); var openButton=document.getElementById('bratonien-nc-wizard-open');
var closeButton=document.getElementById('bratonien-nc-wizard-close'); var closeButton=document.getElementById('bratonien-nc-wizard-close');
var technicalButton=document.getElementById('bratonien-nc-technical-open');
var technical=document.getElementById('bratonien-nc-technical-create');
var key='bratonienNcWizardOpen'; var key='bratonienNcWizardOpen';
function openWizard(){try{sessionStorage.setItem(key,'1');}catch(e){} if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();else dialog.setAttribute('open','open');} function openWizard(){try{sessionStorage.setItem(key,'1');}catch(e){} if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();else dialog.setAttribute('open','open');}
function closeWizard(){try{sessionStorage.removeItem(key);}catch(e){} if(typeof dialog.close==='function')dialog.close();else dialog.removeAttribute('open');} function closeWizard(){try{sessionStorage.removeItem(key);}catch(e){} if(typeof dialog.close==='function')dialog.close();else dialog.removeAttribute('open');}
@@ -344,8 +235,7 @@
dialog.querySelectorAll('form[data-bratonien-wizard-form]').forEach(function(form){form.addEventListener('submit',function(e){var s=e.submitter;try{if(s&&s.hasAttribute('data-bratonien-wizard-end'))sessionStorage.removeItem(key);else sessionStorage.setItem(key,'1');}catch(x){}});}); dialog.querySelectorAll('form[data-bratonien-wizard-form]').forEach(function(form){form.addEventListener('submit',function(e){var s=e.submitter;try{if(s&&s.hasAttribute('data-bratonien-wizard-end'))sessionStorage.removeItem(key);else sessionStorage.setItem(key,'1');}catch(x){}});});
try{if(sessionStorage.getItem(key)==='1')openWizard();}catch(e){} try{if(sessionStorage.getItem(key)==='1')openWizard();}catch(e){}
} }
if(technicalButton&&technical)technicalButton.addEventListener('click',function(){technical.open=!technical.open;if(technical.open)technical.scrollIntoView(true);});
})(); })();
</script> </script>
{/literal} {/literal}
</section> </section>

183
webdav-derivative-debug.php Normal file
View File

@@ -0,0 +1,183 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
define('BRATONIEN_TOOLS_ID', basename(__DIR__));
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php');
function bratonien_tools_webdav_debug_out($status, array $data)
{
http_response_code((int)$status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
global $user;
if (!isset($user['status']) || !in_array($user['status'], array('admin', 'webmaster'), true))
{
bratonien_tools_webdav_debug_out(403, array('ok'=>false, 'stage'=>'auth', 'error'=>'Nur fuer Administratoren/Webmaster.'));
}
$image_id = (int)($_GET['id'] ?? 0);
if ($image_id < 1)
{
bratonien_tools_webdav_debug_out(400, array('ok'=>false, 'stage'=>'input', 'error'=>'Bild-ID fehlt.'));
}
$debug = array(
'ok'=>false,
'image_id'=>$image_id,
'stage'=>'start',
);
$result = pwg_query('SELECT id, path, coi FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result))
{
$debug['stage'] = 'image-row';
$debug['error'] = 'Bild nicht gefunden.';
bratonien_tools_webdav_debug_out(404, $debug);
}
$row = pwg_db_fetch_assoc($result);
$image_path = (string)($row['path'] ?? '');
$debug['images_path'] = $image_path;
$absolute = $image_path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $absolute), '/');
}
$debug['absolute_path'] = $absolute;
$debug['absolute_exists'] = file_exists($absolute);
$debug['absolute_is_file'] = is_file($absolute);
$debug['absolute_is_link'] = is_link($absolute);
if (is_link($absolute))
{
$debug['link_target'] = readlink($absolute);
}
$resolved = realpath($absolute);
$debug['realpath'] = $resolved === false ? null : $resolved;
if ($resolved === false)
{
$debug['stage'] = 'realpath';
$debug['error'] = 'realpath() konnte images.path nicht aufloesen.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$normalized = str_replace('\\', '/', $resolved);
$debug['normalized_realpath'] = $normalized;
$match = array();
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{
$debug['stage'] = 'source-regex';
$debug['error'] = 'realpath passt nicht zum erwarteten nc-webdav-source-Schema.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$connection_id = (int)$match[1];
$root_fileid = (int)$match[2];
$relative_path = trim((string)$match[3], '/');
$debug['connection_id'] = $connection_id;
$debug['root_fileid'] = $root_fileid;
$debug['relative_path'] = $relative_path;
$table = defined('BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE')
? BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$debug['connections_table'] = $table;
$connection_result = pwg_query('SELECT id, enabled, adapter, config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($connection_result))
{
$debug['stage'] = 'connection-row';
$debug['error'] = 'Erkannte WebDAV-Verbindung existiert nicht in der Connector-Tabelle.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$connection_row = pwg_db_fetch_assoc($connection_result);
$debug['connection_enabled'] = (int)($connection_row['enabled'] ?? 0);
$debug['connection_adapter'] = (string)($connection_row['adapter'] ?? '');
$config = json_decode((string)($connection_row['config_json'] ?? ''), true);
if (!is_array($config))
{
$debug['stage'] = 'config-json';
$debug['error'] = 'config_json ist kein gueltiges JSON-Objekt.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$debug['source_mode'] = (string)($config['source_mode'] ?? '');
$debug['state_dir'] = (string)($config['state_dir'] ?? '');
$debug['parallel_gallery_root'] = (string)($config['parallel_gallery_root'] ?? '');
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
$debug['roots'] = array();
$root_path = '';
foreach ($roots as $root)
{
$entry = array(
'fileid'=>(int)($root['fileid'] ?? 0),
'webdav_path'=>(string)($root['webdav_path'] ?? ''),
'display_name'=>(string)($root['display_name'] ?? ''),
);
$debug['roots'][] = $entry;
if ($entry['fileid'] === $root_fileid)
{
$root_path = trim($entry['webdav_path'], '/');
}
}
$debug['matched_root_path'] = $root_path;
if ($debug['source_mode'] !== 'webdav-placeholder')
{
$debug['stage'] = 'source-mode';
$debug['error'] = 'source_mode ist nicht webdav-placeholder.';
bratonien_tools_webdav_debug_out(200, $debug);
}
if ($root_path === '')
{
$debug['stage'] = 'root-match';
$debug['error'] = 'root_fileid aus dem Dateipfad kommt in config.roots nicht vor.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$mapping_file = rtrim((string)($config['state_dir'] ?? ''), '/').'/webdav-map.json';
$debug['mapping_file'] = $mapping_file;
$debug['mapping_readable'] = is_readable($mapping_file);
$debug['mapping_has_resolved_key'] = false;
$debug['mapping_has_normalized_key'] = false;
$debug['mapping_entry'] = null;
if (is_readable($mapping_file))
{
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{
$debug['mapping_has_resolved_key'] = array_key_exists($resolved, $mapping['files']);
$debug['mapping_has_normalized_key'] = array_key_exists($normalized, $mapping['files']);
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null;
if (is_array($entry))
{
$debug['mapping_entry'] = $entry;
}
}
else
{
$debug['mapping_error'] = 'Mapping-Datei ist ungueltig oder enthaelt kein files-Objekt.';
}
}
$debug['runtime_source_info'] = bratonien_tools_webdav_image_source_info($image_id);
$debug['stage'] = $debug['runtime_source_info'] ? 'ok' : 'runtime-source-info-null';
$debug['ok'] = (bool)$debug['runtime_source_info'];
if (!$debug['ok'])
{
$debug['error'] = 'Die produktive Zuordnungsfunktion liefert trotz der obigen Zwischenergebnisse null.';
}
bratonien_tools_webdav_debug_out(200, $debug);

494
webdav-derivative-test.php Normal file
View File

@@ -0,0 +1,494 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
function bratonien_tools_webdav_derivative_test_abort($status, $message)
{
http_response_code((int)$status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo $message;
exit;
}
function bratonien_tools_webdav_derivative_test_decrypt_secret($blob, $hex_key)
{
$hex_key = trim((string)$hex_key);
if (!preg_match('/^[a-f0-9]{64}$/', $hex_key)) return null;
$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) return null;
$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)) return null;
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) return null;
$decoded = json_decode((string)$plain, true);
return is_array($decoded) ? $decoded : null;
}
function bratonien_tools_webdav_derivative_test_quote_path($path)
{
$parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen'));
return implode('/', array_map('rawurlencode', $parts));
}
function bratonien_tools_webdav_derivative_test_source_info($image_id, array $image_row)
{
$image_id = (int)$image_id;
$path = (string)($image_row['path'] ?? '');
if ($image_id < 1 || $path === '') return null;
$absolute = $path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\\./#', '', $absolute), '/');
}
$resolved = realpath($absolute);
if ($resolved === false) return null;
$normalized = str_replace('\\', '/', $resolved);
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{
return null;
}
$connection_id = (int)$match[1];
$root_fileid = (int)$match[2];
$relative_path = trim((string)$match[3], '/');
if ($connection_id < 1 || $root_fileid < 1 || $relative_path === '') return null;
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$connection_result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($connection_result)) return null;
$connection_row = pwg_db_fetch_assoc($connection_result);
$config = json_decode((string)$connection_row['config_json'], true);
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') return null;
$root_found = false;
$root_path = '';
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
foreach ($roots as $root)
{
if ((int)($root['fileid'] ?? 0) === $root_fileid)
{
$root_found = true;
$root_path = trim((string)($root['webdav_path'] ?? ''), '/');
break;
}
}
if (!$root_found) return null;
$webdav_path = trim($root_path === '' ? $relative_path : $root_path.'/'.$relative_path, '/');
if ($webdav_path === '') return null;
$content_type = '';
$size = 0;
$etag = '';
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '')
{
$mapping_file = $state_dir.'/webdav-map.json';
if (is_readable($mapping_file))
{
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null;
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file')
{
$webdav_path = trim((string)($entry['webdav_path'] ?? $webdav_path), '/');
$content_type = (string)($entry['content_type'] ?? '');
$size = (int)($entry['size'] ?? 0);
$etag = (string)($entry['etag'] ?? '');
}
}
}
}
return array(
'image_id'=>$image_id,
'connection_id'=>$connection_id,
'webdav_path'=>$webdav_path,
'content_type'=>$content_type,
'size'=>$size,
'etag'=>$etag,
'coi'=>$image_row['coi'] ?? null,
);
}
function bratonien_tools_webdav_derivative_test_download(array $source, $destination, &$detail=null)
{
$detail = '';
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$result = pwg_query('SELECT config_json, secret_blob FROM `'.$table.'` WHERE id='.(int)$source['connection_id'].' LIMIT 1');
if (!pwg_db_num_rows($result))
{
$detail = 'WebDAV-Verbindung nicht gefunden.';
return false;
}
$row = pwg_db_fetch_assoc($result);
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config))
{
$detail = 'WebDAV-Konfiguration ist ungueltig.';
return false;
}
$key_result = pwg_query("SELECT value FROM ".$GLOBALS['prefixeTable']."config WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!pwg_db_num_rows($key_result))
{
$detail = 'Connector-Schluessel fehlt.';
return false;
}
$key_row = pwg_db_fetch_assoc($key_result);
$credentials = bratonien_tools_webdav_derivative_test_decrypt_secret((string)$row['secret_blob'], (string)$key_row['value']);
if (!is_array($credentials))
{
$detail = 'WebDAV-Zugangsdaten konnten nicht gelesen werden.';
return false;
}
$base_url = rtrim((string)($config['nextcloud_url'] ?? ''), '/');
$user = trim((string)($credentials['nextcloud_user'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
$webdav_path = trim((string)($source['webdav_path'] ?? ''), '/');
if ($base_url === '' || $user === '' || $password === '' || $webdav_path === '')
{
$detail = 'WebDAV-Bildquelle ist unvollstaendig.';
return false;
}
$fp = @fopen($destination, 'xb');
if (!$fp)
{
$detail = 'Temporaere Quelldatei konnte nicht angelegt werden.';
return false;
}
$url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.bratonien_tools_webdav_derivative_test_quote_path($webdav_path);
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 180,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FILE => $fp,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Derivative-Parallel-Test',
));
$ok = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
if ($ok === false || $errno !== 0 || $http < 200 || $http >= 300)
{
@unlink($destination);
$detail = 'Nextcloud-Download fehlgeschlagen (HTTP '.$http.', cURL '.$errno.($error !== '' ? ': '.$error : '').').';
return false;
}
if (!is_file($destination) || filesize($destination) < 1 || @getimagesize($destination) === false)
{
@unlink($destination);
$detail = 'Nextcloud-Datei ist kein lesbares Bild.';
return false;
}
return true;
}
function bratonien_tools_webdav_derivative_test_inner_url($derivative_path)
{
$derivative_root = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR;
if (strpos($derivative_path, $derivative_root) !== 0) return null;
$location = ltrim(substr($derivative_path, strlen($derivative_root)), '/');
if ($location === '') return null;
$segments = array_map('rawurlencode', explode('/', $location));
return rtrim(get_absolute_root_url(), '/').'/i.php?/'.implode('/', $segments);
}
function bratonien_tools_webdav_derivative_test_call_i($url, &$response_body, &$response_type, &$response_status, &$detail=null)
{
$detail = '';
$response_body = '';
$response_type = 'application/octet-stream';
$response_status = 500;
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 180,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-Derivative-Parallel-Gate',
));
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$content_type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
$response_status = $status;
if ($content_type !== '') $response_type = $content_type;
if (is_string($body)) $response_body = $body;
if ($body === false || $errno !== 0)
{
$detail = 'Interner i.php-Aufruf fehlgeschlagen (cURL '.$errno.($error !== '' ? ': '.$error : '').').';
return false;
}
if ($status < 200 || $status >= 400)
{
$detail = 'i.php antwortete mit HTTP '.$status.'.';
return false;
}
return true;
}
global $user;
if (!isset($user['status']) || !in_array($user['status'], array('admin', 'webmaster'), true))
{
bratonien_tools_webdav_derivative_test_abort(403, 'Dieser Test-Endpunkt ist nur fuer Administratoren verfuegbar.');
}
$metrics_requested = isset($_GET['metrics']) && (string)$_GET['metrics'] === '1';
$metrics_started_at = microtime(true);
$memory_start = memory_get_usage(true);
if (function_exists('memory_reset_peak_usage'))
{
memory_reset_peak_usage();
}
$image_id = (int)($_GET['id'] ?? 0);
if ($image_id < 1)
{
bratonien_tools_webdav_derivative_test_abort(400, 'Bild-ID fehlt.');
}
$type = trim((string)($_GET['type'] ?? IMG_THUMB));
$defined = ImageStdParams::get_defined_type_map();
if (!isset($defined[$type]))
{
bratonien_tools_webdav_derivative_test_abort(400, 'Unbekannter oder deaktivierter Derivat-Typ: '.$type);
}
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result))
{
bratonien_tools_webdav_derivative_test_abort(404, 'Bild nicht gefunden.');
}
$image_row = pwg_db_fetch_assoc($result);
$source = bratonien_tools_webdav_derivative_test_source_info($image_id, $image_row);
if (!$source)
{
bratonien_tools_webdav_derivative_test_abort(404, 'Keine WebDAV-Quelle fuer dieses Bild gefunden.');
}
$upload_dir = rtrim((string)($GLOBALS['conf']['upload_dir'] ?? './upload'), '/');
$test_rel_dir = $upload_dir.'/bratonien-webdav-test';
$test_root = PHPWG_ROOT_PATH.$test_rel_dir;
if (!is_dir($test_root) && !@mkdir($test_root, 0755, true))
{
bratonien_tools_webdav_derivative_test_abort(500, 'Paralleler Testbereich konnte nicht angelegt werden: '.$test_root);
}
if (!is_writable($test_root))
{
bratonien_tools_webdav_derivative_test_abort(500, 'Paralleler Testbereich ist fuer PHP nicht beschreibbar: '.$test_root);
}
$lock_path = $test_root.'/image-'.$image_id.'.lock';
$lock = @fopen($lock_path, 'c');
if (!$lock || !flock($lock, LOCK_EX))
{
if ($lock) fclose($lock);
bratonien_tools_webdav_derivative_test_abort(503, 'Paralleler Test-Lock konnte nicht gesetzt werden.');
}
$extension = strtolower(pathinfo((string)($image_row['path'] ?? ''), PATHINFO_EXTENSION));
if (!in_array($extension, array('jpg', 'jpeg', 'png', 'gif', 'webp'), true))
{
$extension = 'jpg';
}
$token = getmypid().'-'.bin2hex(random_bytes(6));
$source_filename = 'source-'.$image_id.'-'.$token.'.'.$extension;
$source_path = $test_root.'/'.$source_filename;
$source_rel = $test_rel_dir.'/'.$source_filename;
$temp_image_id = 0;
$derivative_path = '';
$cleaned = false;
$cleanup = function() use (&$cleaned, &$temp_image_id, &$derivative_path, $source_path, $lock)
{
if ($cleaned) return;
$cleaned = true;
if ($temp_image_id > 0)
{
@pwg_query('DELETE FROM '.IMAGES_TABLE.' WHERE id='.(int)$temp_image_id.' LIMIT 1');
$temp_image_id = 0;
}
if ($derivative_path !== '' && is_file($derivative_path))
{
@unlink($derivative_path);
}
if (is_file($source_path))
{
@unlink($source_path);
}
@flock($lock, LOCK_UN);
@fclose($lock);
};
register_shutdown_function($cleanup);
try
{
$detail = '';
if (!bratonien_tools_webdav_derivative_test_download($source, $source_path, $detail))
{
bratonien_tools_webdav_derivative_test_abort(502, $detail);
}
$source_bytes = (int)filesize($source_path);
$dimensions = @getimagesize($source_path);
if (!is_array($dimensions) || empty($dimensions[0]) || empty($dimensions[1]))
{
bratonien_tools_webdav_derivative_test_abort(500, 'Paralleles Original besitzt keine gueltigen Bildabmessungen.');
}
$test_row = $image_row;
unset($test_row['id']);
$test_row['path'] = $source_rel;
$test_row['file'] = $source_filename;
if (array_key_exists('width', $test_row)) $test_row['width'] = (int)$dimensions[0];
if (array_key_exists('height', $test_row)) $test_row['height'] = (int)$dimensions[1];
single_insert(IMAGES_TABLE, $test_row);
$temp_image_id = (int)pwg_db_insert_id();
if ($temp_image_id < 1)
{
bratonien_tools_webdav_derivative_test_abort(500, 'Temporaerer Piwigo-Testeintrag konnte nicht angelegt werden.');
}
$test_row['id'] = $temp_image_id;
$test_src = new SrcImage($test_row);
$derivative = new DerivativeImage($defined[$type], $test_src);
if ($derivative->same_as_source())
{
bratonien_tools_webdav_derivative_test_abort(409, 'Dieser Typ ist fuer das parallele Testbild identisch mit der Quelle.');
}
$derivative_path = $derivative->get_path();
if ($derivative_path === '')
{
bratonien_tools_webdav_derivative_test_abort(500, 'Piwigo konnte keinen Derivat-Zielpfad fuer den parallelen Test bestimmen.');
}
if (is_file($derivative_path))
{
@unlink($derivative_path);
clearstatcache(true, $derivative_path);
}
$inner_url = bratonien_tools_webdav_derivative_test_inner_url($derivative_path);
if ($inner_url === null)
{
bratonien_tools_webdav_derivative_test_abort(500, 'Piwigo-i.php-URL konnte fuer den parallelen Test nicht bestimmt werden.');
}
$body = '';
$content_type = 'application/octet-stream';
$status = 500;
if (!bratonien_tools_webdav_derivative_test_call_i($inner_url, $body, $content_type, $status, $detail))
{
bratonien_tools_webdav_derivative_test_abort(502, $detail."\n".$body);
}
clearstatcache(true, $derivative_path);
if (!is_file($derivative_path) || !is_readable($derivative_path))
{
bratonien_tools_webdav_derivative_test_abort(
500,
"i.php antwortete, aber das parallele Piwigo-Derivat wurde nicht erzeugt.\n"
.'Testquelle: '.$source_rel."\n"
.'i.php: '.$inner_url
);
}
$derivative_size = @getimagesize($derivative_path);
if (!is_array($derivative_size) || empty($derivative_size[0]) || empty($derivative_size[1]))
{
bratonien_tools_webdav_derivative_test_abort(500, 'Das von i.php erzeugte parallele Derivat ist keine lesbare Bilddatei.');
}
$generated_width = (int)$derivative_size[0];
$generated_height = (int)$derivative_size[1];
$derivative_bytes = (int)filesize($derivative_path);
$cleanup();
$elapsed_ms = round((microtime(true) - $metrics_started_at) * 1000, 2);
$memory_peak = memory_get_peak_usage(true);
$memory_delta_peak = max(0, $memory_peak - $memory_start);
if ($metrics_requested)
{
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode(array(
'source_bytes'=>$source_bytes,
'derivative_bytes'=>$derivative_bytes,
'elapsed_ms'=>$elapsed_ms,
'memory_start_bytes'=>$memory_start,
'memory_peak_bytes'=>$memory_peak,
'memory_delta_peak_bytes'=>$memory_delta_peak,
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
}
header('X-Bratonien-WebDAV-Test: parallel-i.php-success');
header('X-Bratonien-Resource-Source-Bytes: '.$source_bytes);
header('X-Bratonien-Resource-Derivative-Bytes: '.$derivative_bytes);
header('X-Bratonien-Resource-Elapsed-Ms: '.$elapsed_ms);
header('X-Bratonien-Resource-Memory-Start-Bytes: '.$memory_start);
header('X-Bratonien-Resource-Memory-Peak-Bytes: '.$memory_peak);
header('X-Bratonien-Resource-Memory-Delta-Peak-Bytes: '.$memory_delta_peak);
header('X-Bratonien-WebDAV-Test-Size: '.$generated_width.'x'.$generated_height);
header('Content-Type: '.$content_type);
header('Content-Length: '.strlen($body));
header('Cache-Control: no-store');
http_response_code($status);
echo $body;
}
finally
{
$cleanup();
}

368
webdav-derivative.php Normal file
View File

@@ -0,0 +1,368 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
define('BRATONIEN_TOOLS_ID', basename(__DIR__));
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
function bratonien_tools_webdav_derivative_debug($request_id, $event, array $fields=array())
{
$parts = array('[BRAT-WD '.$request_id.']', $event);
foreach ($fields as $key => $value)
{
if (is_bool($value)) $value = $value ? '1' : '0';
elseif ($value === null) $value = 'NULL';
elseif (is_array($value) || is_object($value)) $value = json_encode($value);
$parts[] = $key.'='.str_replace(array("\r", "\n"), array('\\r', '\\n'), (string)$value);
}
error_log(implode(' ', $parts));
}
function bratonien_tools_webdav_derivative_abort($status, $message)
{
http_response_code((int)$status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo $message;
exit;
}
function bratonien_tools_webdav_derivative_decrypt_secret($blob, $hex_key)
{
$hex_key = trim((string)$hex_key);
if (!preg_match('/^[a-f0-9]{64}$/', $hex_key)) return null;
$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) return null;
$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)) return null;
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) return null;
$decoded = json_decode((string)$plain, true);
return is_array($decoded) ? $decoded : null;
}
function bratonien_tools_webdav_derivative_quote_path($path)
{
$parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen'));
return implode('/', array_map('rawurlencode', $parts));
}
function bratonien_tools_webdav_derivative_download(array $source, $destination, &$detail=null)
{
$detail = '';
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$result = pwg_query('SELECT config_json, secret_blob FROM `'.$table.'` WHERE id='.(int)$source['connection_id'].' LIMIT 1');
if (!pwg_db_num_rows($result)) { $detail = 'WebDAV-Verbindung nicht gefunden.'; return false; }
$row = pwg_db_fetch_assoc($result);
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) { $detail = 'WebDAV-Konfiguration ist ungueltig.'; return false; }
$key_result = pwg_query("SELECT value FROM ".$GLOBALS['prefixeTable']."config WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!pwg_db_num_rows($key_result)) { $detail = 'Connector-Schluessel fehlt.'; return false; }
$key_row = pwg_db_fetch_assoc($key_result);
$credentials = bratonien_tools_webdav_derivative_decrypt_secret((string)$row['secret_blob'], (string)$key_row['value']);
if (!is_array($credentials)) { $detail = 'WebDAV-Zugangsdaten konnten nicht gelesen werden.'; return false; }
$base_url = rtrim((string)($config['nextcloud_url'] ?? ''), '/');
$user = trim((string)($credentials['nextcloud_user'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
$webdav_path = trim((string)($source['webdav_path'] ?? ''), '/');
if ($base_url === '' || $user === '' || $password === '' || $webdav_path === '') { $detail = 'WebDAV-Bildquelle ist unvollstaendig.'; return false; }
$fp = @fopen($destination, 'xb');
if (!$fp) { $detail = 'Temporaere Quelldatei konnte nicht angelegt werden.'; return false; }
$url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.bratonien_tools_webdav_derivative_quote_path($webdav_path);
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 180,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FILE => $fp,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Materialize',
));
$ok = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
if ($ok === false || $errno !== 0 || $http < 200 || $http >= 300)
{
@unlink($destination);
$detail = 'Nextcloud-Download fehlgeschlagen (HTTP '.$http.', cURL '.$errno.($error !== '' ? ': '.$error : '').').';
return false;
}
if (!is_file($destination) || filesize($destination) < 1 || @getimagesize($destination) === false)
{
@unlink($destination);
$detail = 'Nextcloud-Datei ist kein lesbares Bild.';
return false;
}
return true;
}
function bratonien_tools_webdav_derivative_inner_url($derivative_path)
{
$derivative_root = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR;
if (strpos($derivative_path, $derivative_root) !== 0) return null;
$location = ltrim(substr($derivative_path, strlen($derivative_root)), '/');
if ($location === '') return null;
$segments = array_map('rawurlencode', explode('/', $location));
return rtrim(get_absolute_root_url(), '/').'/i.php?/'.implode('/', $segments);
}
function bratonien_tools_webdav_derivative_call_i($url, &$status, &$detail=null)
{
$detail = '';
$status = 500;
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 180,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Materialize-Gate',
CURLOPT_WRITEFUNCTION => function($ch, $data) { return strlen($data); },
));
$ok = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($ok === false || $errno !== 0) { $detail = 'Interner i.php-Aufruf fehlgeschlagen (cURL '.$errno.($error !== '' ? ': '.$error : '').').'; return false; }
if ($status < 200 || $status >= 400) { $detail = 'i.php antwortete mit HTTP '.$status.'.'; return false; }
return true;
}
function bratonien_tools_webdav_derivative_after_url($encoded)
{
$encoded = trim((string)$encoded);
if ($encoded === '') return null;
$raw = strtr($encoded, '-_', '+/');
$padding = strlen($raw) % 4;
if ($padding) $raw .= str_repeat('=', 4 - $padding);
$url = base64_decode($raw, true);
if (!is_string($url) || $url === '' || preg_match('/[\r\n]/', $url)) return null;
if (strpos($url, '//') === 0) return null;
if (preg_match('#^https?://#i', $url))
{
$root_parts = parse_url(get_absolute_root_url());
$url_parts = parse_url($url);
if (!is_array($root_parts) || !is_array($url_parts)) return null;
if (strcasecmp((string)($root_parts['scheme'] ?? ''), (string)($url_parts['scheme'] ?? '')) !== 0) return null;
if (strcasecmp((string)($root_parts['host'] ?? ''), (string)($url_parts['host'] ?? '')) !== 0) return null;
if ((int)($root_parts['port'] ?? 0) !== (int)($url_parts['port'] ?? 0)) return null;
return $url;
}
if (strpos($url, ':') !== false && !preg_match('#^[A-Za-z0-9_./?&=%+-]+$#', $url)) return null;
return $url;
}
function bratonien_tools_webdav_derivative_needs_after_redirect($url)
{
if (!is_string($url) || $url === '') return false;
return strpos($url, '/plugins/'.BRATONIEN_TOOLS_ID.'/watermark.php') !== false
|| strpos($url, 'plugins/'.BRATONIEN_TOOLS_ID.'/watermark.php') === 0;
}
function bratonien_tools_webdav_derivative_serve($path)
{
clearstatcache(true, $path);
if (!is_file($path) || !is_readable($path)) bratonien_tools_webdav_derivative_abort(500, 'Piwigo-Derivat ist nicht lesbar.');
$size = @getimagesize($path);
$content_type = is_array($size) && !empty($size['mime']) ? (string)$size['mime'] : 'application/octet-stream';
header('Content-Type: '.$content_type);
header('Content-Length: '.(int)filesize($path));
header('Cache-Control: public, max-age=31536000');
header('X-Content-Type-Options: nosniff');
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'HEAD') readfile($path);
exit;
}
$image_id = (int)($_GET['id'] ?? 0);
$variant = trim((string)($_GET['variant'] ?? ''));
$request_id = bin2hex(random_bytes(4));
bratonien_tools_webdav_derivative_debug($request_id, 'start', array('image_id'=>$image_id, 'variant'=>$variant));
$after_url = bratonien_tools_webdav_derivative_after_url($_GET['after'] ?? '');
$after_sig = trim((string)($_GET['sig'] ?? ''));
if ($image_id < 1) bratonien_tools_webdav_derivative_abort(400, 'Bild-ID fehlt.');
if ($variant === '') bratonien_tools_webdav_derivative_abort(400, 'Derivat-Variante fehlt.');
if ($after_url !== null)
{
$expected_sig = bratonien_tools_webdav_materialize_after_signature($image_id, $variant, $after_url);
if ($after_sig === '' || !hash_equals($expected_sig, $after_sig)) $after_url = null;
}
$redirect_after = bratonien_tools_webdav_derivative_needs_after_redirect($after_url) ? $after_url : null;
$permission_condition = get_sql_condition_FandF(array('forbidden_categories'=>'category_id'), null, true);
$access_result = pwg_query('SELECT 1 FROM '.IMAGE_CATEGORY_TABLE.' WHERE image_id='.$image_id.' AND '.$permission_condition.' LIMIT 1');
if (!pwg_db_num_rows($access_result)) bratonien_tools_webdav_derivative_abort(403, 'Kein Zugriff auf dieses Bild.');
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result)) bratonien_tools_webdav_derivative_abort(404, 'Bild nicht gefunden.');
$image_row = pwg_db_fetch_assoc($result);
$raw_source_path = (string)($image_row['path'] ?? '');
$absolute_source_path = $raw_source_path;
if ($absolute_source_path !== '' && strpos($absolute_source_path, '/') !== 0)
{
$absolute_source_path = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $absolute_source_path), '/');
}
bratonien_tools_webdav_derivative_debug($request_id, 'source_before_resolve', array(
'path'=>$raw_source_path,
'is_link'=>$absolute_source_path !== '' && is_link($absolute_source_path),
'realpath'=>$absolute_source_path !== '' ? (realpath($absolute_source_path) ?: 'FALSE') : 'EMPTY',
));
$source = bratonien_tools_webdav_materialize_source_info($image_id);
if (!$source)
{
bratonien_tools_webdav_derivative_debug($request_id, 'source_resolve_failed');
bratonien_tools_webdav_derivative_abort(404, 'Keine WebDAV-Quelle fuer dieses Bild gefunden.');
}
bratonien_tools_webdav_derivative_debug($request_id, 'source_resolved', array('connection_id'=>$source['connection_id'] ?? 0, 'root_fileid'=>$source['root_fileid'] ?? 0, 'webdav_path'=>$source['webdav_path'] ?? ''));
$params = bratonien_tools_webdav_materialize_params_from_variant($variant);
if (!$params) bratonien_tools_webdav_derivative_abort(400, 'Unbekannte Derivat-Variante.');
$real_src = new SrcImage($image_row);
$real_derivative = new DerivativeImage($params, $real_src);
if ($real_derivative->same_as_source())
{
$source_url = bratonien_tools_webdav_materialize_image_url($image_id);
if (!$source_url) bratonien_tools_webdav_derivative_abort(404, 'WebDAV-Original konnte nicht bestimmt werden.');
header('Location: '.$source_url, true, 302);
exit;
}
$target_path = $real_derivative->get_path();
$derivative_root = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR;
if ($target_path === '' || strpos($target_path, $derivative_root) !== 0) bratonien_tools_webdav_derivative_abort(500, 'Ungueltiger Piwigo-Derivatpfad.');
if (is_file($target_path) && is_readable($target_path))
{
bratonien_tools_webdav_derivative_debug($request_id, 'target_exists', array('target'=>$target_path));
if ($redirect_after !== null) { header('Location: '.$redirect_after, true, 302); exit; }
bratonien_tools_webdav_derivative_serve($target_path);
}
$shadow_path = $raw_source_path;
if ($shadow_path === '') bratonien_tools_webdav_derivative_abort(500, 'Piwigo-Quellpfad fehlt.');
if (strpos($shadow_path, '/') !== 0) $shadow_path = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $shadow_path), '/');
$shadow_normalized = str_replace('\\', '/', $shadow_path);
if (!preg_match('#/_data/bratonien-tools/nc-webdav-gallery/connection-'.(int)$source['connection_id'].'/#', $shadow_normalized)) bratonien_tools_webdav_derivative_abort(500, 'Piwigo-Quellpfad liegt nicht im WebDAV-Shadow-Baum.');
if (!is_link($shadow_path)) bratonien_tools_webdav_derivative_abort(500, 'WebDAV-Shadow-Quelle ist kein Placeholder-Symlink.');
$placeholder_target = realpath($shadow_path);
$placeholder_normalized = $placeholder_target === false ? '' : str_replace('\\', '/', $placeholder_target);
if ($placeholder_target === false || !preg_match('#/nc-webdav-source/connection-'.(int)$source['connection_id'].'/root-'.(int)$source['root_fileid'].'/#', $placeholder_normalized)) bratonien_tools_webdav_derivative_abort(500, 'WebDAV-Placeholder-Ziel ist ungueltig.');
$shadow_dir = dirname($shadow_path);
if (!is_dir($shadow_dir) || !is_writable($shadow_dir)) bratonien_tools_webdav_derivative_abort(500, 'WebDAV-Shadow-Verzeichnis ist fuer PHP nicht beschreibbar.');
$upload_dir = rtrim((string)($GLOBALS['conf']['upload_dir'] ?? './upload'), '/');
$work_rel_dir = $upload_dir.'/bratonien-webdav-materialize';
$work_root = PHPWG_ROOT_PATH.$work_rel_dir;
if (!is_dir($work_root) && !@mkdir($work_root, 0755, true)) bratonien_tools_webdav_derivative_abort(500, 'WebDAV-Materialisierungsbereich konnte nicht angelegt werden.');
if (!is_writable($work_root)) bratonien_tools_webdav_derivative_abort(500, 'WebDAV-Materialisierungsbereich ist fuer PHP nicht beschreibbar.');
$lock_path = $work_root.'/image-'.$image_id.'.lock';
bratonien_tools_webdav_derivative_debug($request_id, 'waiting_lock', array('lock'=>$lock_path, 'is_link'=>is_link($shadow_path), 'realpath'=>realpath($shadow_path) ?: 'FALSE'));
$lock = @fopen($lock_path, 'c');
if (!$lock || !flock($lock, LOCK_EX))
{
if ($lock) fclose($lock);
bratonien_tools_webdav_derivative_abort(503, 'WebDAV-Materialisierungslock konnte nicht gesetzt werden.');
}
bratonien_tools_webdav_derivative_debug($request_id, 'lock_acquired', array('is_link'=>is_link($shadow_path), 'realpath'=>realpath($shadow_path) ?: 'FALSE'));
clearstatcache(true, $target_path);
if (is_file($target_path) && is_readable($target_path))
{
bratonien_tools_webdav_derivative_debug($request_id, 'target_created_while_waiting', array('target'=>$target_path));
@flock($lock, LOCK_UN); @fclose($lock);
if ($redirect_after !== null) { header('Location: '.$redirect_after, true, 302); exit; }
bratonien_tools_webdav_derivative_serve($target_path);
}
$token = getmypid().'-'.bin2hex(random_bytes(6));
$download_path = $work_root.'/source-'.$image_id.'-'.$token.'.tmp';
$staging_path = $shadow_path.'.bratonien-'.$token.'.tmp';
$placeholder_backup = $shadow_path.'.bratonien-placeholder';
$swapped = false;
$cleaned = false;
$cleanup = function() use (&$cleaned, &$swapped, $shadow_path, $staging_path, $placeholder_backup, $download_path, $lock, $request_id)
{
if ($cleaned) return;
$cleaned = true;
if ($swapped)
{
if (is_file($shadow_path) && !is_link($shadow_path)) @unlink($shadow_path);
if (is_link($placeholder_backup)) @rename($placeholder_backup, $shadow_path);
$swapped = false;
}
if (is_file($staging_path) || is_link($staging_path)) @unlink($staging_path);
if (is_file($download_path)) @unlink($download_path);
bratonien_tools_webdav_derivative_debug($request_id, 'restored', array('is_link'=>is_link($shadow_path), 'realpath'=>realpath($shadow_path) ?: 'FALSE'));
@flock($lock, LOCK_UN);
@fclose($lock);
};
register_shutdown_function($cleanup);
try
{
if (is_link($placeholder_backup)) bratonien_tools_webdav_derivative_abort(503, 'WebDAV-Placeholder-Backup existiert bereits; vorheriger Materialisierungslauf ist nicht sauber abgeschlossen.');
$detail = '';
$download_start = microtime(true);
if (!bratonien_tools_webdav_derivative_download($source, $download_path, $detail)) bratonien_tools_webdav_derivative_abort(502, $detail);
bratonien_tools_webdav_derivative_debug($request_id, 'download_done', array('ms'=>round((microtime(true)-$download_start)*1000, 1), 'bytes'=>@filesize($download_path) ?: 0));
if (!@copy($download_path, $staging_path)) bratonien_tools_webdav_derivative_abort(500, 'Original konnte nicht in den WebDAV-Shadow-Baum kopiert werden.');
@chmod($staging_path, 0644);
if (@getimagesize($staging_path) === false) bratonien_tools_webdav_derivative_abort(500, 'Materialisierte Shadow-Quelle ist kein gueltiges Bild.');
bratonien_tools_webdav_derivative_debug($request_id, 'swap_begin', array('placeholder'=>$placeholder_target));
if (!@rename($shadow_path, $placeholder_backup)) bratonien_tools_webdav_derivative_abort(500, 'WebDAV-Placeholder konnte nicht temporaer gesichert werden.');
if (!@rename($staging_path, $shadow_path))
{
@rename($placeholder_backup, $shadow_path);
bratonien_tools_webdav_derivative_abort(500, 'Materialisiertes Original konnte nicht am Piwigo-Quellpfad aktiviert werden.');
}
$swapped = true;
clearstatcache(true, $shadow_path);
bratonien_tools_webdav_derivative_debug($request_id, 'swapped', array('is_link'=>is_link($shadow_path), 'realpath'=>realpath($shadow_path) ?: 'FALSE'));
if (!is_file($shadow_path) || is_link($shadow_path) || @getimagesize($shadow_path) === false) bratonien_tools_webdav_derivative_abort(500, 'Piwigo-Quellpfad enthaelt nach Materialisierung kein gueltiges Original.');
$inner_url = bratonien_tools_webdav_derivative_inner_url($target_path);
if ($inner_url === null) bratonien_tools_webdav_derivative_abort(500, 'Piwigo-i.php-URL konnte nicht bestimmt werden.');
$inner_status = 500;
$i_start = microtime(true);
bratonien_tools_webdav_derivative_debug($request_id, 'i_start', array('url'=>$inner_url));
$i_ok = bratonien_tools_webdav_derivative_call_i($inner_url, $inner_status, $detail);
bratonien_tools_webdav_derivative_debug($request_id, 'i_done', array('status'=>$inner_status, 'ok'=>$i_ok, 'ms'=>round((microtime(true)-$i_start)*1000, 1), 'detail'=>$detail));
if (!$i_ok) bratonien_tools_webdav_derivative_abort(502, $detail);
clearstatcache(true, $target_path);
$final_size = @getimagesize($target_path);
if (!is_file($target_path) || !is_readable($target_path) || !is_array($final_size)) bratonien_tools_webdav_derivative_abort(500, 'Piwigo hat kein gueltiges finales Derivat erzeugt.');
$cleanup();
bratonien_tools_webdav_derivative_debug($request_id, 'serve', array('target'=>$target_path, 'width'=>$final_size[0] ?? 0, 'height'=>$final_size[1] ?? 0));
if ($redirect_after !== null) { header('Location: '.$redirect_after, true, 302); exit; }
bratonien_tools_webdav_derivative_serve($target_path);
}
finally
{
$cleanup();
}

296
webdav-download-test.php Normal file
View File

@@ -0,0 +1,296 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
function bratonien_tools_webdav_download_test_abort($status, $message)
{
http_response_code((int)$status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo $message;
exit;
}
function bratonien_tools_webdav_download_test_decrypt_secret($blob, $hex_key)
{
$hex_key = trim((string)$hex_key);
if (!preg_match('/^[a-f0-9]{64}$/', $hex_key)) return null;
$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) return null;
$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)) return null;
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) return null;
$decoded = json_decode((string)$plain, true);
return is_array($decoded) ? $decoded : null;
}
function bratonien_tools_webdav_download_test_quote_path($path)
{
$parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen'));
return implode('/', array_map('rawurlencode', $parts));
}
function bratonien_tools_webdav_download_test_source_info($image_id, array $image_row)
{
$path = (string)($image_row['path'] ?? '');
if ($image_id < 1 || $path === '') return null;
$absolute = $path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\\./#', '', $absolute), '/');
}
$resolved = realpath($absolute);
if ($resolved === false) return null;
$normalized = str_replace('\\', '/', $resolved);
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{
return null;
}
$connection_id = (int)$match[1];
$root_fileid = (int)$match[2];
$relative_path = trim((string)$match[3], '/');
if ($connection_id < 1 || $root_fileid < 1 || $relative_path === '') return null;
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($result)) return null;
$row = pwg_db_fetch_assoc($result);
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') return null;
$root_found = false;
$root_path = '';
foreach ((array)($config['roots'] ?? array()) as $root)
{
if ((int)($root['fileid'] ?? 0) === $root_fileid)
{
$root_found = true;
$root_path = trim((string)($root['webdav_path'] ?? ''), '/');
break;
}
}
if (!$root_found) return null;
$webdav_path = trim($root_path === '' ? $relative_path : $root_path.'/'.$relative_path, '/');
if ($webdav_path === '') return null;
$size = 0;
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '')
{
$mapping_file = $state_dir.'/webdav-map.json';
if (is_readable($mapping_file))
{
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null;
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file')
{
$webdav_path = trim((string)($entry['webdav_path'] ?? $webdav_path), '/');
$size = (int)($entry['size'] ?? 0);
}
}
}
}
return array(
'connection_id'=>$connection_id,
'webdav_path'=>$webdav_path,
'size'=>$size,
);
}
function bratonien_tools_webdav_download_test_connection(array $source)
{
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$result = pwg_query('SELECT config_json, secret_blob FROM `'.$table.'` WHERE id='.(int)$source['connection_id'].' LIMIT 1');
if (!pwg_db_num_rows($result)) return null;
$row = pwg_db_fetch_assoc($result);
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) return null;
$key_result = pwg_query("SELECT value FROM ".$GLOBALS['prefixeTable']."config WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!pwg_db_num_rows($key_result)) return null;
$key_row = pwg_db_fetch_assoc($key_result);
$credentials = bratonien_tools_webdav_download_test_decrypt_secret((string)$row['secret_blob'], (string)$key_row['value']);
if (!is_array($credentials)) return null;
$base_url = rtrim((string)($config['nextcloud_url'] ?? ''), '/');
$user = trim((string)($credentials['nextcloud_user'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
if ($base_url === '' || $user === '' || $password === '') return null;
return array('base_url'=>$base_url, 'user'=>$user, 'password'=>$password);
}
global $user;
if (!isset($user['status']) || !in_array($user['status'], array('admin', 'webmaster'), true))
{
bratonien_tools_webdav_download_test_abort(403, 'Dieser Test-Endpunkt ist nur fuer Administratoren verfuegbar.');
}
$image_id = (int)($_GET['id'] ?? 0);
$parallel = (int)($_GET['parallel'] ?? 1);
if ($image_id < 1)
{
bratonien_tools_webdav_download_test_abort(400, 'Bild-ID fehlt.');
}
if ($parallel < 1 || $parallel > 20)
{
bratonien_tools_webdav_download_test_abort(400, 'parallel muss zwischen 1 und 20 liegen.');
}
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result))
{
bratonien_tools_webdav_download_test_abort(404, 'Bild nicht gefunden.');
}
$image_row = pwg_db_fetch_assoc($result);
$source = bratonien_tools_webdav_download_test_source_info($image_id, $image_row);
if (!$source)
{
bratonien_tools_webdav_download_test_abort(404, 'Keine WebDAV-Quelle fuer dieses Bild gefunden.');
}
$connection = bratonien_tools_webdav_download_test_connection($source);
if (!$connection)
{
bratonien_tools_webdav_download_test_abort(500, 'WebDAV-Verbindung konnte nicht geladen werden.');
}
$upload_dir = rtrim((string)($GLOBALS['conf']['upload_dir'] ?? './upload'), '/');
$test_rel_dir = $upload_dir.'/bratonien-webdav-download-test';
$test_root = PHPWG_ROOT_PATH.$test_rel_dir;
if (!is_dir($test_root) && !@mkdir($test_root, 0755, true))
{
bratonien_tools_webdav_download_test_abort(500, 'Download-Testbereich konnte nicht angelegt werden: '.$test_root);
}
if (!is_writable($test_root))
{
bratonien_tools_webdav_download_test_abort(500, 'Download-Testbereich ist fuer PHP nicht beschreibbar: '.$test_root);
}
$url = $connection['base_url'].'/remote.php/dav/files/'.rawurlencode($connection['user']).'/'.bratonien_tools_webdav_download_test_quote_path($source['webdav_path']);
$token = getmypid().'-'.bin2hex(random_bytes(6));
$multi = curl_multi_init();
$items = array();
for ($i = 0; $i < $parallel; $i++)
{
$path = $test_root.'/download-'.$image_id.'-'.$token.'-'.$i.'.tmp';
$fp = @fopen($path, 'xb');
if (!$fp)
{
foreach ($items as $item)
{
@fclose($item['fp']);
@unlink($item['path']);
curl_multi_remove_handle($multi, $item['ch']);
curl_close($item['ch']);
}
curl_multi_close($multi);
bratonien_tools_webdav_download_test_abort(500, 'Temporaere Download-Datei konnte nicht angelegt werden.');
}
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 180,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $connection['user'].':'.$connection['password'],
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FILE => $fp,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Download-Stress-Test',
));
curl_multi_add_handle($multi, $ch);
$items[] = array('ch'=>$ch, 'fp'=>$fp, 'path'=>$path);
}
$started_at = microtime(true);
do
{
$status = curl_multi_exec($multi, $running);
if ($status !== CURLM_OK) break;
if ($running > 0)
{
$selected = curl_multi_select($multi, 1.0);
if ($selected === -1) usleep(10000);
}
}
while ($running > 0);
$elapsed_seconds = microtime(true) - $started_at;
$downloads = array();
$total_bytes = 0;
$successful = 0;
$failed = 0;
foreach ($items as $index => $item)
{
@fflush($item['fp']);
@fclose($item['fp']);
$http = (int)curl_getinfo($item['ch'], CURLINFO_HTTP_CODE);
$time_ms = round((float)curl_getinfo($item['ch'], CURLINFO_TOTAL_TIME) * 1000, 2);
$errno = curl_errno($item['ch']);
$error = curl_error($item['ch']);
$bytes = is_file($item['path']) ? (int)filesize($item['path']) : 0;
$ok = ($errno === 0 && $http >= 200 && $http < 300 && $bytes > 0);
if ($ok) $successful++; else $failed++;
$total_bytes += $bytes;
$downloads[] = array(
'index'=>$index + 1,
'ok'=>$ok,
'http'=>$http,
'bytes'=>$bytes,
'elapsed_ms'=>$time_ms,
'mbps'=>$time_ms > 0 ? round(($bytes * 8) / ($time_ms / 1000) / 1000000, 2) : 0,
'curl_errno'=>$errno,
'curl_error'=>$error,
);
@unlink($item['path']);
curl_multi_remove_handle($multi, $item['ch']);
curl_close($item['ch']);
}
curl_multi_close($multi);
$elapsed_ms = round($elapsed_seconds * 1000, 2);
$aggregate_mbps = $elapsed_seconds > 0 ? round(($total_bytes * 8) / $elapsed_seconds / 1000000, 2) : 0;
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode(array(
'image_id'=>$image_id,
'parallel'=>$parallel,
'same_source_repeated'=>true,
'expected_source_bytes'=>(int)$source['size'],
'successful'=>$successful,
'failed'=>$failed,
'total_bytes'=>$total_bytes,
'peak_temp_disk_bytes'=>$total_bytes,
'elapsed_ms'=>$elapsed_ms,
'aggregate_mbps'=>$aggregate_mbps,
'downloads'=>$downloads,
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

View File

@@ -8,6 +8,7 @@ if (!defined('BRATONIEN_TOOLS_PATH'))
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/'); define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
} }
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php'); require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
function bratonien_tools_webdav_image_abort($status, $message) function bratonien_tools_webdav_image_abort($status, $message)
{ {
@@ -48,7 +49,7 @@ $permission_condition = get_sql_condition_FandF(array('forbidden_categories'=>'c
$access_result = pwg_query('SELECT 1 FROM '.IMAGE_CATEGORY_TABLE.' WHERE image_id='.$image_id.' AND '.$permission_condition.' LIMIT 1'); $access_result = pwg_query('SELECT 1 FROM '.IMAGE_CATEGORY_TABLE.' WHERE image_id='.$image_id.' AND '.$permission_condition.' LIMIT 1');
if (!pwg_db_num_rows($access_result)) bratonien_tools_webdav_image_abort(403, 'Kein Zugriff auf dieses Bild.'); if (!pwg_db_num_rows($access_result)) bratonien_tools_webdav_image_abort(403, 'Kein Zugriff auf dieses Bild.');
$source = bratonien_tools_webdav_image_source_info($image_id); $source = bratonien_tools_webdav_materialize_source_info($image_id);
if (!$source) bratonien_tools_webdav_image_abort(404, 'Keine WebDAV-Quelle für dieses Bild gefunden.'); if (!$source) bratonien_tools_webdav_image_abort(404, 'Keine WebDAV-Quelle für dieses Bild gefunden.');
if (!empty($_GET['preview'])) if (!empty($_GET['preview']))
@@ -120,7 +121,7 @@ $options = array(
CURLOPT_USERPWD => $user.':'.$password, CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false, CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false, CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.6.1', CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.6.8.6',
CURLOPT_HEADERFUNCTION => function($ch, $line) CURLOPT_HEADERFUNCTION => function($ch, $line)
{ {
$length = strlen($line); $length = strlen($line);