Compare commits

..

289 Commits

Author SHA1 Message Date
Terranom674
dbc5e27693 0.9.7.26: Version anheben 2026-08-20 09:21:48 +02:00
Terranom674
91f0d0cac1 0.9.7.26: Fallback ueber denselben Piwigo-Core-Sync fuehren 2026-08-20 09:21:19 +02:00
Terranom674
2c1c7ff499 Merge pull request #47 from Terranom674/fix/09725-clean-stale-site-images
0.9.7.25: WebDAV wieder nur an Piwigo-Core übergeben
2026-08-20 09:15:01 +02:00
Terranom674
3a15c2d03d 0.9.7.25: eigenen Orphan-Sync deaktivieren 2026-08-20 09:13:47 +02:00
Terranom674
2acdbc65a2 0.9.7.25: WebDAV nur noch ueber Piwigo-Core synchronisieren 2026-08-20 09:13:24 +02:00
Terranom674
8510d9faad 0.9.7.25: Piwigo-Core-Synchronisation wiederherstellen 2026-08-20 09:12:01 +02:00
Terranom674
f21f19bd30 Merge pull request #46 from Terranom674/fix/09724-native-ajaxload-placeholder
0.9.7.24: Piwigo-Nachladen für WebDAV-Platzhalter reparieren
2026-08-20 09:02:33 +02:00
Terranom674
29990aee4f 0.9.7.24: Version anheben 2026-08-20 09:00:53 +02:00
Terranom674
9c551c8dfc 0.9.7.24: Piwigo ajaxload fuer WebDAV-Platzhalter bedienen 2026-08-20 09:00:36 +02:00
Terranom674
e5bc260622 Merge pull request #45 from Terranom674/fix/09723-visible-run-status
0.9.7.23: Abgleich sichtbar ausführen und Fehlerdetails anzeigen
2026-08-20 08:34:32 +02:00
Terranom674
5b883706c7 0.9.7.23: Version anheben 2026-08-20 08:33:27 +02:00
Terranom674
66f463422e 0.9.7.23: Konkrete Shadow-Tree-Fehler in Diagnose übernehmen 2026-08-20 08:32:51 +02:00
Terranom674
df9e9e6424 0.9.7.23: Verbindungsbezogenen Live-Status bereitstellen 2026-08-20 08:32:23 +02:00
Terranom674
26b602783f 0.9.7.23: Abgleich sichtbar und ohne Seitenreload ausführen 2026-08-20 08:32:04 +02:00
Terranom674
ae1b47c4c4 Merge pull request #44 from Terranom674/fix/09722-placeholder-dimension-fallback
0.9.7.22: Originalmaße robust aus Nextcloud übernehmen
2026-08-20 08:24:54 +02:00
Terranom674
6f2f39cd37 0.9.7.22: Version anheben 2026-08-20 08:23:51 +02:00
Terranom674
2794719ff3 0.9.7.22: Originalmaße bei fehlenden NC-Metadaten aus Bildkopf lesen 2026-08-20 08:23:11 +02:00
Terranom674
941d0cd7bb Merge pull request #43 from Terranom674/fix/09721-placeholder-original-dimensions
0.9.7.21: Originalmaße direkt in Platzhalter tragen
2026-08-20 08:13:55 +02:00
Terranom674
392038ce7e 0.9.7.21: Version anheben 2026-08-20 08:12:29 +02:00
Terranom674
3aa82b18f4 0.9.7.21: Originalmasse in WebDAV-Platzhalter schreiben 2026-08-20 08:11:49 +02:00
Terranom674
0a76919a21 Merge pull request #42 from Terranom674/fix/09720-restore-pre-ui-image-path
0.9.7.20: NC-Bildpfad auf funktionierenden Pre-UI-Stand zurück
2026-08-20 08:03:49 +02:00
Terranom674
d7dacdae56 0.9.7.20: Medien-Sonderlauf aus WebDAV-Sync entfernen 2026-08-20 08:03:04 +02:00
Terranom674
15b489714b 0.9.7.20: nachgelagerte Medien-Sonderlogik deaktivieren 2026-08-20 08:00:43 +02:00
Terranom674
f3b52c55ef 0.9.7.20: WebDAV-Derivat-URL-Hook wiederherstellen 2026-08-20 08:00:20 +02:00
Terranom674
67c1e814c8 0.9.7.20: funktionierenden NC-Kern vor UI-Umbau wiederherstellen 2026-08-20 07:59:44 +02:00
Terranom674
a5a8856e48 Merge pull request #41 from Terranom674/fix/09719-restore-run-now-and-clean-owned-albums
0.9.7.19: Jetzt abgleichen wiederherstellen
2026-08-20 07:39:47 +02:00
Terranom674
2af46d1820 0.9.7.19: Versionsstand setzen 2026-08-20 07:38:28 +02:00
Terranom674
e5d754fdab 0.9.7.19: Jetzt-abgleichen-Button über normalen Tool-POST wiederherstellen 2026-08-20 07:38:10 +02:00
Terranom674
0adcc5ca54 Merge pull request #40 from Terranom674/fix/09718-restore-pre-ui-behavior
0.9.7.18: konsistente Kernfunktionen und einheitliche NC-UI wiederherstellen
2026-08-20 00:02:17 +02:00
Terranom674
61988db337 Restore single-path NC admin UI 2026-08-19 23:59:56 +02:00
Terranom674
bdfea105cf Keep request-local watermark performance cache 2026-08-19 23:58:13 +02:00
Terranom674
6f64cb9fe6 Restore stable admin tab behavior 2026-08-19 23:57:24 +02:00
Terranom674
65b068814f Unify stale WebDAV cleanup across historical connector paths 2026-08-19 23:55:04 +02:00
Terranom674
e58d2c700a Clean missing images across all connection path generations 2026-08-19 23:54:35 +02:00
Terranom674
afaea3b424 Delete all known path variants for one WebDAV connection 2026-08-19 23:54:17 +02:00
Terranom674
7a763ba62a Reduce WebDAV builder to metadata refresh only 2026-08-19 23:53:38 +02:00
Terranom674
31e29db7c2 Remove obsolete destructive legacy reset 2026-08-19 23:53:11 +02:00
Terranom674
ab116d29c2 Remove destructive legacy reset from updates 2026-08-19 23:49:54 +02:00
Terranom674
46924db618 Remove parallel NC source picker endpoint 2026-08-19 23:49:36 +02:00
Terranom674
6e96b0adc3 Remove parallel NC edit save endpoint 2026-08-19 23:49:28 +02:00
Terranom674
b8f3ed44a3 Remove parallel NC edit data endpoint 2026-08-19 23:49:21 +02:00
Terranom674
cb3b2f258c Disable parallel NC editor layer 2026-08-19 23:49:13 +02:00
Terranom674
ef1c1265a9 0.9.7.18: restore stable non-connector hooks 2026-08-19 23:48:40 +02:00
Terranom674
47d7000cc8 Restore stable watermark settings 2026-08-19 23:48:16 +02:00
Terranom674
c358c157e4 Restore stable watermark profile behavior 2026-08-19 23:48:06 +02:00
Terranom674
1d62dc17e8 Restore stable album watermark rules 2026-08-19 23:47:46 +02:00
Terranom674
ad4c281fdc Restore stable watermark engine 2026-08-19 23:47:31 +02:00
Terranom674
a43a15a9d3 Restore stable watermark runtime 2026-08-19 23:47:13 +02:00
Terranom674
31b8590e4a Merge pull request #39 from Terranom674/fix/09717-remove-missing-webdav-images
0.9.7.17: entfernte Freigaben sicher aus Piwigo bereinigen
2026-08-19 23:28:36 +02:00
Terranom674
c772010137 Version 0.9.7.17 2026-08-19 23:27:41 +02:00
Terranom674
d075d05708 0.9.7.17: Verbindungsloeschung ueber eigene Gallery-Root 2026-08-19 23:27:09 +02:00
Terranom674
a4fb7421f0 0.9.7.17: Bereinigung nur nach erfolgreichem WebDAV-Abruf 2026-08-19 23:26:53 +02:00
Terranom674
9da08b5d8e 0.9.7.17: fehlende Connector-Bilder nach erfolgreichem Abruf bereinigen 2026-08-19 23:26:37 +02:00
Terranom674
f28365b0ca Merge pull request #38 from Terranom674/fix/09716-native-piwigo-source
0.9.7.16: WebDAV-Bilder wieder über Piwigos nativen Derivatpfad laden
2026-08-19 23:07:42 +02:00
Terranom674
fb6c1a03d2 0.9.7.16: vorhandene Preview-Caches ohne Massendownload weiterverwenden 2026-08-19 23:06:11 +02:00
Terranom674
98d0471683 0.9.7.16: WebDAV-Bildendpoint wieder auf Originalauslieferung begrenzen 2026-08-19 23:05:14 +02:00
Terranom674
63eba888f8 0.9.7.16: native Piwigo-Derivatlogik wiederherstellen 2026-08-19 23:04:51 +02:00
Terranom674
748b0e986d 0.9.7.16: widersprüchlichen WebDAV-Derivat-Hook entfernen 2026-08-19 23:04:34 +02:00
Terranom674
c1281ad077 0.9.7.16: eigenen WebDAV-Derivat-Endpunkt entfernen 2026-08-19 23:04:26 +02:00
Terranom674
bc0593642c 0.9.7.16: echte NC-Previews als lokale Piwigo-Quelle verwenden 2026-08-19 23:04:06 +02:00
Terranom674
9052076fd7 Merge pull request #37 from Terranom674/fix/09715-piwigo-ajax-loader
0.9.7.15: Piwigo-AJAX-Lader für WebDAV-Bilder reparieren
2026-08-19 22:59:33 +02:00
Terranom674
5772a59b55 Bump version to 0.9.7.15 2026-08-19 22:58:34 +02:00
Terranom674
84762154d6 Return JSON for Piwigo AJAX image loader 2026-08-19 22:58:17 +02:00
Terranom674
1580c235a8 Fix Piwigo AJAX loader response for WebDAV derivatives 2026-08-19 22:57:56 +02:00
Terranom674
4aac5ca4a2 Merge pull request #36 from Terranom674/fix/09714-cache-permissions
0.9.7.14: WebDAV-Cache-Rechte reparieren
2026-08-19 22:48:56 +02:00
Terranom674
ab1f20beef 0.9.7.14: Einmalige Cache-Rechtereparatur 2026-08-19 22:48:00 +02:00
Terranom674
f94a001006 Version 0.9.7.14 2026-08-19 22:47:45 +02:00
Terranom674
3f64895f8e 0.9.7.14: Connector-Cache-Rechte normalisieren 2026-08-19 22:47:20 +02:00
Terranom674
48f1eacce7 Merge pull request #35 from Terranom674/fix/09713-watermark-album-overview
0.9.7.13: Wasserzeichen in Albumübersicht nach Albumregel
2026-08-19 22:43:07 +02:00
Terranom674
7c8c7ab98a Purge stale watermark cache once on 0.9.7.13 2026-08-19 22:42:24 +02:00
Terranom674
ade38ff2d1 Clear watermark cache after profile changes 2026-08-19 22:42:01 +02:00
Terranom674
6a013fe875 Clear watermark cache after album rule changes 2026-08-19 22:41:36 +02:00
Terranom674
c71112d94e Invalidate watermark cache on rule changes 2026-08-19 22:41:20 +02:00
Terranom674
7d97e1b515 Register album overview watermark context 2026-08-19 22:39:08 +02:00
Terranom674
47afbc7f43 Fix watermark context for album overview covers 2026-08-19 22:38:54 +02:00
Terranom674
706639a57e Merge pull request #34 from Terranom674/fix/09712-ondemand-missing-preview
0.9.7.12: On-Demand-Altbilder nicht mehr auf 404 hängen lassen
2026-08-19 22:36:50 +02:00
Terranom674
c7b14add33 0.9.7.12: Version anheben 2026-08-19 22:35:53 +02:00
Terranom674
f9e87df305 0.9.7.12: fehlende On-Demand-Previews auf echtes WebDAV-Bild zurueckfallen 2026-08-19 22:35:25 +02:00
Terranom674
6ba5eb4d7f Merge pull request #33 from Terranom674/fix/09711-fotorama-ondemand
0.9.7.11: Fotorama-Derivate on demand erzeugen
2026-08-19 22:32:15 +02:00
Terranom674
85369fa15e Version 0.9.7.11 2026-08-19 22:31:13 +02:00
Terranom674
981ba6c18b 0.9.7.11: On-Demand WebDAV Derivat Endpoint 2026-08-19 22:30:52 +02:00
Terranom674
77e20af977 0.9.7.11: Fotorama Derivate erst bei Abruf erzeugen 2026-08-19 22:30:34 +02:00
Terranom674
e187c517da Merge pull request #32 from Terranom674/fix/09710-never-serve-connector-placeholders
0.9.7.10: Connector-Platzhalter nie im Frontend ausliefern
2026-08-19 22:27:47 +02:00
Terranom674
28e34d6c73 0.9.7.10: Version anheben 2026-08-19 22:26:54 +02:00
Terranom674
b8852310bd 0.9.7.10: Connector-Platzhalter nie als Derivat ausliefern 2026-08-19 22:26:37 +02:00
Terranom674
ae96fc2e3e Merge pull request #31 from Terranom674/fix/0979-picture-derivatives
0.9.7.9: Picture-Viewer lädt WebDAV-Derivate lokal
2026-08-19 22:17:54 +02:00
Terranom674
ebf4fc7e74 0.9.7.9: Plugin-Version anheben 2026-08-19 22:16:46 +02:00
Terranom674
d798b82ec7 0.9.7.9: Picture-Derivate lokal aus NC-Preview erzeugen 2026-08-19 22:16:26 +02:00
Terranom674
9e282af1b4 Merge pull request #30 from Terranom674/fix/0978-modus-gallery-derivative
0.9.7.8: Modus-Galeriederivat beim Sync vorbauen
2026-08-19 22:03:53 +02:00
Terranom674
d7628d1dac Version auf 0.9.7.8 setzen 2026-08-19 22:02:59 +02:00
Terranom674
7e9c3ae05f 0.9.7.8: Modus-Galeriederivat vorab erzeugen 2026-08-19 22:02:43 +02:00
Terranom674
b64e46a549 Merge pull request #29 from Terranom674/fix/0974-gallery-derivative-only
0.9.7.7: Connector-Altbestand einmalig zurücksetzen
2026-08-19 21:50:57 +02:00
Terranom674
779ba80dfe Jetzt-abgleichen global und verbindungsbezogen erlauben 2026-08-19 21:50:18 +02:00
Terranom674
ebbc183f87 Globalen Jetzt-abgleichen-Lauf erhalten 2026-08-19 21:49:45 +02:00
Terranom674
f3b5002f4d run-all auf angeforderte Verbindung filtern 2026-08-19 21:48:05 +02:00
Terranom674
6c393bbcfc Native Runner auf einzelne Verbindung begrenzen 2026-08-19 21:47:40 +02:00
Terranom674
829381676d Manuellen Abgleich auf ausgewählte Verbindung begrenzen 2026-08-19 21:47:25 +02:00
Terranom674
6382cee491 Jetzt-abgleichen verbindungsbezogen starten 2026-08-19 21:47:01 +02:00
Terranom674
69c3932e9b CI für 0.9.7.7 auslösen 2026-08-19 21:45:16 +02:00
Terranom674
747a76c066 Altbestand beim Update auf 0.9.7.7 einmalig bereinigen 2026-08-19 21:45:06 +02:00
Terranom674
5024757136 Einmaligen Connector-Altbestand-Reset ergänzen 2026-08-19 21:44:52 +02:00
Terranom674
5125d2e247 0.9.7.7 vorbereiten 2026-08-19 21:44:39 +02:00
Terranom674
6607c4aefe Merge pull request #28 from Terranom674/fix/0974-gallery-derivative-only
0.9.7.6: Galerie-Hotpath entlasten
2026-08-19 21:38:09 +02:00
Terranom674
32f2ceaaec cleanup: temporaeren CI-Trigger entfernen 2026-08-19 21:37:19 +02:00
Terranom674
5a5669d976 ci: trigger 0.9.7.6 2026-08-19 21:36:46 +02:00
Terranom674
e4ab277cfc 0.9.7.6: Wasserzeichen-Engine pro Request nur einmal initialisieren 2026-08-19 21:36:37 +02:00
Terranom674
9e07cd8042 0.9.7.6: Request-Scheduler aus Galeriepfad entfernen 2026-08-19 21:35:50 +02:00
Terranom674
588f1fc100 0.9.7.6: WebDAV-Hotpath pro Request cachen 2026-08-19 21:35:06 +02:00
Terranom674
a23fc81190 Merge pull request #27 from Terranom674/fix/0974-gallery-derivative-only
0.9.7.5: Alt-Derivate reparieren und Galerie beschleunigen
2026-08-19 21:30:24 +02:00
Terranom674
ff5af83e65 Rebuild only mismatched legacy gallery derivatives 2026-08-19 21:29:31 +02:00
Terranom674
c61d8e32cc Use fast WebDAV gallery derivative filter 2026-08-19 21:28:47 +02:00
Terranom674
38c4955f70 Add fast WebDAV gallery derivative path 2026-08-19 21:28:29 +02:00
Terranom674
5c217288a8 Bump version to 0.9.7.5 2026-08-19 21:27:17 +02:00
Terranom674
17c79ce8ab Restore branch to 0.9.7.4 baseline 2026-08-19 21:27:03 +02:00
Terranom674
7746d26c9f Remove accidental temporary file 2026-08-19 21:26:47 +02:00
Terranom674
d05ab28549 noop 2026-08-19 21:26:19 +02:00
Terranom674
55b5be56b6 Merge pull request #26 from Terranom674/fix/0974-gallery-derivative-only
0.9.7.4: nur Galeriederivat beim WebDAV-Import
2026-08-19 21:23:35 +02:00
Terranom674
ba0b5af417 Release 0.9.7.4 2026-08-19 21:12:39 +02:00
Terranom674
315af9641c Build only gallery derivative from WebDAV preview 2026-08-19 21:12:24 +02:00
Terranom674
77ae007a28 Merge pull request #25 from Terranom674/fix/0973-webdav-root-altimports
0.9.7.3: Altimporte aus WebDAV-Stammverzeichnis reparieren
2026-08-19 21:02:13 +02:00
Terranom674
e5be2b3043 Release 0.9.7.3 2026-08-19 21:01:26 +02:00
Terranom674
3fe9527537 Fix root WebDAV alt-import derivative detection 2026-08-19 21:01:00 +02:00
Terranom674
fb1f12d608 0.9.7.2: sichtbarer Abgleich und sicherer WebDAV-Import
0.9.7.2: sichtbarer Abgleich und sicherer WebDAV-Import
2026-08-19 20:53:28 +02:00
Terranom674
017cbdbd3b 0.9.7.2: private Alben nicht von öffentlichen Regeln erben lassen 2026-08-19 20:52:16 +02:00
Terranom674
9aaa1a47b3 0.9.7.2: WebDAV-Sync nicht-destruktiv machen 2026-08-19 20:51:23 +02:00
Terranom674
fc3d855e5e 0.9.7.2: Version anheben 2026-08-19 20:49:18 +02:00
Terranom674
4847d2ff11 0.9.7.2: aktuellen Schedulerstatus in Statuskarte spiegeln 2026-08-19 20:48:48 +02:00
Terranom674
0b405e9efb 0.9.7.2: Abgleich sofort als angefordert markieren 2026-08-19 20:48:25 +02:00
Terranom674
2c302b356d Merge pull request #23 from Terranom674/fix/0971-webdav-public-ip
0.9.7.1: WebDAV über öffentliche Ziel-IP
2026-08-19 20:43:19 +02:00
Terranom674
5c62dbd9be 0.9.7.1: clean pinned WebDAV resolver implementation 2026-08-19 20:41:46 +02:00
Terranom674
5f81cc9ce9 0.9.7.1: bump plugin version 2026-08-19 20:40:57 +02:00
Terranom674
a5be16f88e 0.9.7.1: stream WebDAV originals through resolved target IP 2026-08-19 20:40:40 +02:00
Terranom674
6b082ba8b5 0.9.7.1: fetch WebDAV previews through resolved target IP 2026-08-19 20:40:19 +02:00
Terranom674
5396f50bc0 0.9.7.1: resolve remote Nextcloud target before WebDAV scan 2026-08-19 20:39:54 +02:00
Terranom674
a54df74e67 0.9.7.1: pin WebDAV runtime to resolved target IP 2026-08-19 20:39:27 +02:00
Terranom674
986eb530d7 0.9.7.1: scan remote Nextcloud through resolved IP transport 2026-08-19 20:38:52 +02:00
Terranom674
b9adcbddac 0.9.7.1: use transport-aware WebDAV wizard handlers 2026-08-19 20:38:25 +02:00
Terranom674
bb434b9b02 0.9.7.1: route wizard and WebDAV calls through resolved target IP 2026-08-19 20:37:59 +02:00
Terranom674
aa5c2f7f0c 0.9.7.1: add runtime Nextcloud target resolver 2026-08-19 20:37:23 +02:00
Terranom674
aa803a4149 0.9.7.1: add generic Nextcloud transport resolver 2026-08-19 20:35:40 +02:00
Terranom674
ad39b9f0d2 Merge pull request #22 from Terranom674/fix/09630-native-piwigo-scheduler
0.9.6.30: NC Connector nativ in Piwigo ausführen
2026-08-19 20:18:39 +02:00
Terranom674
aa8beb3794 0.9.6.30: globales Lauf-Lock gegen Parallelstarts 2026-08-19 20:17:47 +02:00
Terranom674
3ad6e9eb8e 0.9.6.30: globales Lauf-Lock an run-all übergeben 2026-08-19 20:17:24 +02:00
Terranom674
21118f6fb3 0.9.6.30: Bildaufbereitung ohne systemd im nativen Modus 2026-08-19 20:16:48 +02:00
Terranom674
c23302197a 0.9.6.30: WebDAV-Runtime unter Piwigo _data ablegen 2026-08-19 20:16:19 +02:00
Terranom674
d54aa482f7 0.9.6.30: Runtime für nativen Piwigo-Scheduler vorbereiten 2026-08-19 20:15:41 +02:00
Terranom674
25133bc895 0.9.6.30: Jetzt-abgleichen auf nativen Scheduler umstellen 2026-08-19 20:15:22 +02:00
Terranom674
62cc1af8bf 0.9.6.30: Status auf nativen Piwigo-Scheduler umstellen 2026-08-19 20:14:56 +02:00
Terranom674
cf300bc5c3 0.9.6.30: Scheduler beim Plugin-Lifecycle initialisieren 2026-08-19 20:14:33 +02:00
Terranom674
0dde87cda0 0.9.6.30: nativen Scheduler beim Plugin laden 2026-08-19 20:14:21 +02:00
Terranom674
17a76c4de2 0.9.6.30: nativen Connector-Runner hinzufügen 2026-08-19 20:14:05 +02:00
Terranom674
2366f6366c 0.9.6.30: nativen Piwigo-Scheduler hinzufügen 2026-08-19 20:13:47 +02:00
Terranom674
43780cb9a3 Merge pull request #21 from Terranom674/fix/09629-stability-rollback
0.9.6.29: Shadowtree stabilisieren
2026-08-19 19:59:15 +02:00
Terranom674
7b915c310a 0.9.6.29: PHP-Abfrage stabilisieren 2026-08-19 19:58:28 +02:00
Terranom674
6df469ab7a 0.9.6.29: Shadowtree stabilisieren und Namensprüfung entfernen 2026-08-19 19:55:32 +02:00
Terranom674
55294643b4 0.9.6.29: Shadowtree- und Sync-Umbau zurückrollen 2026-08-19 19:54:15 +02:00
Terranom674
0f3c78f02c Merge pull request #18 from Terranom674/fix/09628-single-piwigo-structure
0.9.6.28: nur ein Piwigo-Strukturpfad
2026-08-19 19:44:36 +02:00
Terranom674
d80163a704 0.9.6.28: Version fuer Strukturfix anheben 2026-08-19 19:43:48 +02:00
Terranom674
9e57c4ea3d 0.9.6.28: Fallback nutzt denselben Piwigo-Syncpfad 2026-08-19 19:43:30 +02:00
Terranom674
30f1c0af18 Merge pull request #17 from Terranom674/fix/09627-webdav-album-structure
0.9.6.27: WebDAV ohne Parallel-Albumstruktur
2026-08-19 19:39:07 +02:00
Terranom674
00d14db57d 0.9.6.27: WebDAV in bestehende Albumstruktur synchronisieren 2026-08-19 19:37:45 +02:00
Terranom674
6e52ba824c 0.9.6.27: transparentes WebDAV-Stammverzeichnis spiegeln 2026-08-19 19:35:32 +02:00
Terranom674
a1bac43150 0.9.6.27: Nextcloud-Stamm ohne Benutzerwrapper abbilden 2026-08-19 19:35:10 +02:00
Terranom674
06646ea37f 0.9.6.27: einen NC-Anlageweg und Jetzt-abgleichen
0.9.6.27: einen NC-Anlageweg und Jetzt-abgleichen
2026-08-19 19:29:08 +02:00
Terranom674
8f2b22a376 0.9.6.27: CI-Marker entfernen 2026-08-19 19:28:20 +02:00
Terranom674
2122ab88ac 0.9.6.27: CI-Stand markieren 2026-08-19 19:28:11 +02:00
Terranom674
babe95fae3 0.9.6.27: versehentliche Hilfsdatei entfernen 2026-08-19 19:28:03 +02:00
Terranom674
14a83be190 noop 2026-08-19 19:27:53 +02:00
Terranom674
a1d2163aab 0.9.6.27: CI auf einen NC-JavaScript-Pfad zuruecksetzen 2026-08-19 19:27:06 +02:00
Terranom674
a0606b2553 0.9.6.27: paralleles Run-now-JavaScript entfernen 2026-08-19 19:26:51 +02:00
Terranom674
9543af50d8 0.9.6.27: parallelen NC-Anlageweg entfernen 2026-08-19 19:26:37 +02:00
Terranom674
4af1972353 0.9.6.27: einen einzigen NC-Verbindungsweg anzeigen 2026-08-19 19:26:11 +02:00
Terranom674
0e7070fb04 0.9.6.27: parallelen sudoers-Installer entfernen 2026-08-19 19:24:41 +02:00
Terranom674
ba53bfebe9 0.9.6.27: parallele sudoers-Hilfsstruktur entfernen 2026-08-19 19:24:36 +02:00
Terranom674
4b2fc93e3d 0.9.6.27: unnoetige Markerdatei entfernen 2026-08-19 19:24:06 +02:00
Terranom674
e356d43e5b 0.9.6.27: unnoetige Run-now-Hinweisdatei entfernen 2026-08-19 19:23:56 +02:00
Terranom674
980efc6700 0.9.6.27: Run-now-Setup markieren 2026-08-19 19:23:30 +02:00
Terranom674
a814c18d18 0.9.6.27: sudoers-Setup fuer Jetzt-abgleichen vorbereiten 2026-08-19 19:23:14 +02:00
Terranom674
6737c8989d 0.9.6.27: minimale sudoers-Regel fuer manuellen Abgleich dokumentieren 2026-08-19 19:23:04 +02:00
Terranom674
2e308867ad 0.9.6.27: Hinweis zur manuellen Service-Ausloesung 2026-08-19 19:22:54 +02:00
Terranom674
1d3768b051 0.9.6.27: Jetzt-abgleichen-JavaScript in CI pruefen 2026-08-19 19:22:48 +02:00
Terranom674
cfea5546de 0.9.6.27: Version anheben 2026-08-19 19:22:32 +02:00
Terranom674
8c30edc0ee 0.9.6.27: Jetzt-abgleichen-JavaScript laden 2026-08-19 19:22:11 +02:00
Terranom674
76a182af82 0.9.6.27: Jetzt-abgleichen-Button anzeigen 2026-08-19 19:21:41 +02:00
Terranom674
21e046cf37 0.9.6.27: manuellen NC-Abgleich registrieren 2026-08-19 19:21:16 +02:00
Terranom674
7728e368db Merge pull request #15 from Terranom674/fix/remove-nc-status-card-09625
0.9.6.26: Connector-Timer nicht mehr durch Bildaufbereitung blockieren
2026-08-19 18:57:07 +02:00
Terranom674
074aa8d99a 0.9.6.26: Version anheben 2026-08-19 18:56:13 +02:00
Terranom674
030634c39d 0.9.6.26: Connector-Lauf nicht mehr durch Bildaufbereitung blockieren 2026-08-19 18:55:50 +02:00
Terranom674
dea1a0ffb2 0.9.6.26: Bildaufbereitung aus Connector-Hauptlauf auslagern 2026-08-19 18:55:30 +02:00
Terranom674
3a3539e55b 0.9.6.25: NC-Status fuer laufenden Sync korrigieren
0.9.6.25: NC-Status fuer laufenden Sync korrigieren
2026-08-19 18:45:58 +02:00
Terranom674
49e74a019b CI: entfernte NC-Migrations-JavaScriptdatei nicht mehr pruefen 2026-08-19 18:45:22 +02:00
Terranom674
f2f7d39b22 Version 0.9.6.25 2026-08-19 18:44:19 +02:00
Terranom674
2c5bcf6ba2 NC-Status: laufenden Service und naechsten Lauf korrekt anzeigen 2026-08-19 18:43:46 +02:00
Terranom674
2d29cfacfe 0.9.6.24: Migration vollstaendig aus aktivem Connectorpfad entfernen 2026-08-19 18:34:43 +02:00
Terranom674
c5e1b33c2f 0.9.6.24: Uebergabe- und Migrationslogik entfernen 2026-08-19 18:34:21 +02:00
Terranom674
675ccd1df8 0.9.6.24: Migrations-UI-Datei entfernen 2026-08-19 18:34:11 +02:00
Terranom674
aefbe76595 0.9.6.24: WebDAV-Runtime von Migration entkoppeln 2026-08-19 18:33:41 +02:00
Terranom674
65cbc0060f 0.9.6.24: Runtime-Migrationsrouting entfernen 2026-08-19 18:33:04 +02:00
Terranom674
5dce180217 0.9.6.24: Migrationsstatus aus Edit-API entfernen 2026-08-19 18:31:51 +02:00
Terranom674
25ee22b1ef 0.9.6.24: Migrations-UI aus Verbindungseditor entfernen 2026-08-19 18:31:28 +02:00
Terranom674
992ddd3b9b 0.9.6.24: Migrationsfunktionen aus Registry entfernen 2026-08-19 18:30:30 +02:00
Terranom674
0f85958dff 0.9.6.24: WebDAV ohne Migrationspaarung 2026-08-19 18:27:44 +02:00
Terranom674
82af31435e 0.9.6.24: gekoppelte Migrationsloeschung entfernen 2026-08-19 18:25:01 +02:00
Terranom674
462197fc78 0.9.6.23: Versionsnummer aktualisieren 2026-08-19 18:20:30 +02:00
Terranom674
cb445c3f04 0.9.6.23: Status logischer NC-Verbindungen korrekt aggregieren 2026-08-19 18:20:10 +02:00
Terranom674
e1306c110c Version auf 0.9.6.22 anheben 2026-08-19 18:10:19 +02:00
Terranom674
bbae8ac984 Migration als einmaligen Uebergang begrenzen 2026-08-19 18:10:01 +02:00
Terranom674
5a7f72b24e Fallback im NC-Verbindungseditor bearbeitbar machen 2026-08-19 18:07:08 +02:00
Terranom674
09027322ef Piwigo-Fallback im Editor pruefen und speichern 2026-08-19 18:06:34 +02:00
Terranom674
a38152108c Piwigo-Fallback im Verbindungseditor bereitstellen 2026-08-19 18:05:50 +02:00
Terranom674
036f2a51e6 Version auf 0.9.6.21 anheben 2026-08-19 18:04:00 +02:00
Terranom674
8cabeb3079 Wizard-Dialog ueber Fehler und Redirects offen halten 2026-08-19 18:03:38 +02:00
Terranom674
0767c4fd8f Gekoppelte NC-Verbindung als Einheit loeschen 2026-08-19 18:03:04 +02:00
Terranom674
de649235ff NC Connector als logische Verbindungen darstellen 2026-08-19 18:02:44 +02:00
Terranom674
3896933081 Merge pull request #13 from Terranom674/fix/nc-ui-mutation-loop-09620
0.9.6.20: NC-Connector UI-Hänger beheben
2026-08-19 17:38:42 +02:00
Terranom674
72a3267ed3 Bump version to 0.9.6.20 2026-08-19 17:32:12 +02:00
Terranom674
99479d5d03 Fix NC connector MutationObserver loop 2026-08-19 17:31:57 +02:00
Terranom674
51089c3446 Merge 0.9.6.19 migration stabilization
WebDAV migration now routes per connection, preserves exact legacy fallbacks, supports API or connection-scoped fallback auth, and passes the final PHP 8.2-8.5 plus script syntax matrix.
2026-08-19 16:11:18 +02:00
Terranom674
c8d846f289 Bump version to 0.9.6.19 2026-08-19 16:09:24 +02:00
Terranom674
2603a8929c 0.9.6.19 preserve fallback-only migration auth 2026-08-19 16:08:57 +02:00
Terranom674
afd8620859 0.9.6.19 allow migration with connection-scoped fallback 2026-08-19 16:08:24 +02:00
Terranom674
72db8b48eb 0.9.6.19 make migration pairing atomic 2026-08-19 16:06:29 +02:00
Terranom674
1a0f8c61e8 0.9.6.19 test supported PHP branches and all admin JS 2026-08-19 16:05:19 +02:00
Terranom674
d4e3e7faab 0.9.6.19 route WebDAV and legacy per connection 2026-08-19 16:04:27 +02:00
Terranom674
03e92b74a7 0.9.6.19 expose exact migration fallback to runtime 2026-08-19 16:03:50 +02:00
Terranom674
88c787c956 0.9.6.19 preserve Nextcloud root selection 2026-08-19 16:02:02 +02:00
Terranom674
3f5f8dc4b4 0.9.6.19 pair migration with selected legacy connection 2026-08-19 16:01:16 +02:00
Terranom674
495edbbb70 Merge pull request #11 from Terranom674/fix/migration-runtime-09618
0.9.6.18: Migration wirklich abschließen und Root-Runtime erzeugen
2026-08-19 15:31:33 +02:00
Terranom674
3d5e6c82ea 0.9.6.18 bump version for migration runtime fix 2026-08-19 15:29:30 +02:00
Terranom674
ae815bc077 0.9.6.18 make selected path lead directly to final migration confirmation 2026-08-19 15:28:41 +02:00
Terranom674
936a1cf3c4 0.9.6.18 allow Nextcloud root in WebDAV runtime 2026-08-19 15:27:39 +02:00
Terranom674
ff0c1e0bec 0.9.6.18 complete migration after WebDAV source selection 2026-08-19 15:26:54 +02:00
Terranom674
a047fc0d83 Merge pull request #10 from Terranom674/fix/root-selection-09617
0.9.6.17: Nextcloud-Stammverzeichnis als WebDAV-Quelle zulassen
2026-08-19 15:19:39 +02:00
Terranom674
2029059720 0.9.6.17 bump version for root selection fix 2026-08-19 15:18:27 +02:00
Terranom674
1eca236509 0.9.6.17 preserve root selection when creating WebDAV connection 2026-08-19 15:17:57 +02:00
Terranom674
e464668343 0.9.6.17 allow Nextcloud root as WebDAV source 2026-08-19 15:17:23 +02:00
Terranom674
47813b0b0e Merge pull request #9 from Terranom674/fix/edit-webdav-path-picker-09616
0.9.6.16: automatische WebDAV-Pfadauswahl direkt beim Bearbeiten
2026-08-19 15:02:21 +02:00
Terranom674
26af1b3019 0.9.6.16 bump version for automatic WebDAV path picker 2026-08-19 15:01:18 +02:00
Terranom674
b1b75676a8 0.9.6.16 prepare WebDAV source picker from edited legacy connection 2026-08-19 15:00:48 +02:00
Terranom674
12880b6c41 0.9.6.16 add automatic Nextcloud path picker to editor 2026-08-19 15:00:27 +02:00
Terranom674
32abc7250d Merge pull request #8 from Terranom674/fix/webdav-source-selection-09615
0.9.6.15: WebDAV-Quellordner klar von Legacy-SMB trennen
2026-08-19 14:44:34 +02:00
Terranom674
76de2b037c 0.9.6.15 bump version 2026-08-19 14:43:23 +02:00
Terranom674
e2e82162c6 0.9.6.15 load source selection clarification UI 2026-08-19 14:42:54 +02:00
Terranom674
7a9eb1baa6 0.9.6.15 separate legacy storage from WebDAV source selection 2026-08-19 14:42:28 +02:00
Terranom674
e800f11f64 Merge pull request #7 from Terranom674/fix/adapter-ui-detection-09614
0.9.6.14: Verbindungstyp nicht mehr aus UI-Text erraten
2026-08-19 14:34:37 +02:00
Terranom674
4808f08590 0.9.6.14 bump version for adapter-safe edit actions 2026-08-19 14:33:33 +02:00
Terranom674
6fd89df5d0 0.9.6.14 determine connection type from backend, not display text 2026-08-19 14:33:03 +02:00
Terranom674
99126088e7 Remove accidental noop file 2026-08-19 14:31:02 +02:00
Terranom674
e53a3848bb noop 2026-08-19 14:30:49 +02:00
Terranom674
0f0c71acaf Merge pull request #6 from Terranom674/fix/webdav-scheme-fallback-09613
0.9.6.13: Nextcloud-Protokoll automatisch erkennen
2026-08-19 14:21:56 +02:00
Terranom674
9847690f50 0.9.6.13 bump version for WebDAV scheme detection 2026-08-19 14:21:02 +02:00
Terranom674
a2f5dc3919 0.9.6.13 try HTTP when scheme is omitted 2026-08-19 14:20:37 +02:00
Terranom674
cd3bf7c8ee Merge pull request #5 from Terranom674/fix/dialog-diagnostics-09612
0.9.6.12: konkrete Fehlerursachen im Verbindungsdialog
2026-08-19 14:14:55 +02:00
Terranom674
79ea403555 0.9.6.12 bump version for actionable diagnostics 2026-08-19 14:14:03 +02:00
Terranom674
33783dd30f 0.9.6.12 include stage and technical cause in visible error 2026-08-19 14:13:35 +02:00
Terranom674
ca6a5cf96f 0.9.6.12 return actionable connection diagnostics 2026-08-19 14:12:58 +02:00
Terranom674
62ada617fb Merge pull request #4 from Terranom674/fix/dialog-validation-09611
0.9.6.11: Dialog bei Fehler offen halten und Felder markieren
2026-08-19 13:59:01 +02:00
Terranom674
898f9b1bc9 0.9.6.11 bump version for dialog validation 2026-08-19 13:58:08 +02:00
Terranom674
5b48e1f6f2 0.9.6.11 keep edit dialog open and mark invalid fields 2026-08-19 13:57:39 +02:00
Terranom674
fdf4584028 0.9.6.11 keep connection editor open on validation errors 2026-08-19 13:56:59 +02:00
Terranom674
880744a8b7 Merge pull request #3 from Terranom674/fix/connection-editing-09610
0.9.6.10: vollständige Verbindungsdaten und migrationsfähiger WebDAV-Start
2026-08-19 13:49:19 +02:00
Terranom674
2ed84ad089 0.9.6.10 lint standalone connector editor JavaScript 2026-08-19 13:48:26 +02:00
Terranom674
60c0a51f01 0.9.6.10 bump plugin version 2026-08-19 13:48:07 +02:00
Terranom674
84d263b16b 0.9.6.10 load complete connector editor on admin page 2026-08-19 13:47:48 +02:00
Terranom674
94c81789ed 0.9.6.10 rebuild connection editor around complete connection data 2026-08-19 13:46:26 +02:00
Terranom674
e3b1f06be3 0.9.6.10 expose complete safe edit metadata and migration readiness 2026-08-19 13:44:46 +02:00
Terranom674
15a3ceb01c 0.9.6.10 complete legacy edit credentials and migration readiness 2026-08-19 13:44:26 +02:00
Terranom674
0f5812a179 0.9.6.9: Verbindung bearbeiten und Migration trennen
* 0.9.6.9 separate edit and migration flows

* 0.9.6.9 register separate edit and migration actions

* 0.9.6.9 add safe connection edit data endpoint

* 0.9.6.9 make connection editing explicit and in-place

* Bump version to 0.9.6.9

* 0.9.6.9 catch duplicate functions and admin JavaScript syntax
2026-08-19 13:35:12 +02:00
Terranom674
377b29a127 Bump Bratonien Tools to 0.9.6.8 2026-08-19 13:21:11 +02:00
Terranom674
d9696172c5 Fix duplicate connector edit handlers 2026-08-19 13:20:55 +02:00
Terranom674
3f2a85fb0b Bump version to 0.9.6.7 2026-08-19 13:17:34 +02:00
Terranom674
c882ba994b Expose editor for every connector connection 2026-08-19 13:17:13 +02:00
Terranom674
cd2c9e350b Make WebDAV connections editable in place 2026-08-19 13:16:29 +02:00
Terranom674
394f375e91 Register connection editor 2026-08-19 13:16:00 +02:00
Terranom674
3b1ccd99a5 Add end-user connection editor 2026-08-19 13:15:30 +02:00
Terranom674
04d88cd40f Bump version to 0.9.6.6 2026-08-19 13:05:02 +02:00
Terranom674
a4f953551c Report real WebDAV failure cause and missing WebDAV configuration 2026-08-19 13:04:36 +02:00
Terranom674
b4161471ca Bump version to 0.9.6.5 2026-08-19 13:00:28 +02:00
Terranom674
71fdadb7f8 Show WebDAV failure reason in connector status 2026-08-19 13:00:10 +02:00
Terranom674
98134b5ab8 Expose WebDAV failure reason before legacy fallback 2026-08-19 12:59:55 +02:00
Terranom674
5f82f2ad71 Bump version to 0.9.6.4 2026-08-19 12:56:17 +02:00
Terranom674
494f8bf192 Show active connector route in status polling 2026-08-19 12:55:41 +02:00
Terranom674
c46f9bb5bd Bump Bratonien Tools to 0.9.6.3 2026-08-19 12:12:57 +02:00
Terranom674
992cb55714 0.9.6.2 show WebDAV vs legacy fallback 2026-08-19 11:43:12 +02:00
Terranom674
46782ea9a3 0.9.6.2 expose active connector route 2026-08-19 11:42:48 +02:00
Terranom674
d03410c9ef 0.9.6.2 make WebDAV primary and legacy local true fallback 2026-08-19 11:24:59 +02:00
Terranom674
aa2a6e4c02 0.9.6.2 pair WebDAV migration with legacy fallback 2026-08-19 11:11:39 +02:00
Terranom674
ebdd77fb0a Release 0.9.6.2 migration phase one 2026-08-19 11:07:01 +02:00
Terranom674
6c36aaeb8a Begin 0.9.6.2 legacy connector migration 2026-08-19 11:06:07 +02:00
73 changed files with 9089 additions and 3051 deletions

1
.ci-trigger-0977 Normal file
View File

@@ -0,0 +1 @@
0.9.7.7

View File

@@ -6,12 +6,11 @@ on:
jobs:
php-syntax:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4', '8.5']
php-version: ['8.2', '8.3', '8.4', '8.5']
steps:
- name: Repository auschecken
uses: actions/checkout@v4
@@ -19,7 +18,7 @@ jobs:
- name: PHP installieren
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
php-version: ${{ matrix.php-version }}
coverage: none
- name: PHP Syntax pruefen
@@ -34,38 +33,26 @@ jobs:
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
duplicates="$(grep -RhoE 'function[[:space:]]+bratonien_tools_[A-Za-z0-9_]+' --include='*.php' . | awk '{print $2}' | sort | uniq -d || true)"
if [[ -n "$duplicates" ]]; then
echo "Doppelte globale Bratonien-Funktionen gefunden:" >&2
printf '%s\n' "$duplicates" >&2
exit 1
fi
script-syntax:
name: JavaScript, Python, Shell
runs-on: ubuntu-latest
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: JavaScript Syntax pruefen
- name: Admin 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)
sed -n '/<script>/,/<\/script>/p' template/admin_tabs.tpl | sed '1d;$d' > /tmp/bratonien-admin-tabs.js
node --check /tmp/bratonien-admin-tabs.js
node --check js/nc_connector_edit_v2.js
- name: Python Syntax pruefen
shell: bash

View File

@@ -1,62 +1,139 @@
# Aktueller Entwicklungsstand
## Version
Stand: 19.08.2026
**Bratonien Tools 0.9.6.2**
## Plugin
## NC Connector
- Aktuelle Plugin-Version: **0.9.6.1**
- Aktueller Entwicklungsblock: **NC Connector WebDAV-End-to-End-Pfad mit Piwigo-Registrierung, Preview-/Derivatstrecke und sauberem Verbindungs-Lebenszyklus**
- GitHub ist das führende Repository.
- Der bestehende lokale Connector bleibt vollständig erhalten und wird nicht automatisch migriert.
Der NC Connector besitzt nur noch einen produktiven Verbindungsweg: **Nextcloud per WebDAV**.
## Nicht verhandelbare Architekturregeln
### Verbindungsmodell
- Nextcloud bleibt die Quelle der Originalbilder.
- Originalbilder werden **nicht dauerhaft nach Piwigo kopiert**.
- Der bestehende lokale Produktionsweg bleibt funktionsfähig.
- WebDAV benötigt nur Nextcloud-Adresse, Benutzer/App-Passwort und normale WebDAV-Berechtigungen des Benutzers.
- Kein Nextcloud-PostgreSQL-Zugriff, kein `occ`, keine Storage-IDs, keine Host-Mounts, kein FUSE, davfs oder rclone als Voraussetzung.
- WebDAV-Verbindungen werden voneinander isoliert verarbeitet.
- Fehler der WebDAV-Bildverarbeitung dürfen keinen normalen Piwigo-Seitenaufruf zum Absturz bringen.
- `adapter=remote`
- `source_mode=webdav-placeholder`
- Nextcloud-Adresse, Benutzer und Passwort/App-Passwort werden pro Verbindung verschlüsselt gespeichert.
- Piwigo-API und Benutzername/Passwort-Fallback werden ebenfalls pro Verbindung gespeichert.
- Der Assistent zeigt ausschließlich die Verzeichnisse, auf die der angemeldete Nextcloud-Benutzer per WebDAV zugreifen kann.
- Mehrere Verzeichnisse können pro Verbindung ausgewählt werden.
## Bestehende Quellenmodi
### Laufzeit
Weiterhin vorhanden:
Der gemeinsame Runner verarbeitet ausschließlich WebDAV-Verbindungen:
- `legacy-view`
- `user-shares`
- `selected-fileids`
1. `runtime/reconcile-webdav.php`
2. `runtime/cleanup-webdav-piwigo.php`
3. `runtime/sync-webdav.sh` für jede vorhandene WebDAV-Konfiguration
Neuer WebDAV-Modus:
Die verbindungsspezifischen Runtime-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`. Zustandsdaten liegen unter `/var/lib/bratonien-tools/nc-connector/connection-ID`.
- `webdav-placeholder`
### Shadow Tree und Piwigo
Es gibt keine automatische Migration bestehender Verbindungen auf WebDAV.
`runtime/lib/build_webdav_placeholder_source.py` bildet die ausgewählten Nextcloud-Inhalte als lokale Platzhalterquelle ab. `runtime/lib/shadow_tree.py` erzeugt daraus die Dateisystemstruktur, die Piwigo als physische Site sieht.
## WebDAV-Ablauf in 0.9.6.1
Jede WebDAV-Verbindung besitzt ihre eigene Piwigo-Site. `runtime/lib/piwigo-sync.php` übergibt genau diese Site an `bratonien.nc.syncProductive` und `bratonien.nc.syncOrphans`. Es gibt keinen zusätzlichen Sync auf Site 1.
1. Der Assistent prüft Nextcloud über WebDAV und lässt sichtbare Verzeichnisse auswählen.
2. `runtime/reconcile-webdav.php` erzeugt die verbindungseigene Runtime-Konfiguration.
3. `runtime/lib/build_webdav_placeholder_source.py` liest die ausgewählten Verzeichnisse rekursiv per PROPFIND.
4. Ordner, Dateinamen, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
5. Eine lokale Platzhalterquelle wird erzeugt, ohne Originalbilder dauerhaft zu speichern.
6. `runtime/lib/shadow_tree.py` baut daraus den verbindungseigenen Shadow Tree.
7. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID` und wird als eigene physische Piwigo-Site registriert.
8. Dadurch erscheint die ausgewählte Nextcloud-Wurzel als normales Piwigo-Album; technische `bratonien-webdav-ID`-Wrapper werden nicht angezeigt.
9. Vorbereitete Arbeitsbilder liegen unter `_data/bratonien-tools/nc-webdav-preview/connection-ID`.
10. Arbeitsbilder werden als JPEG beziehungsweise PNG erzeugt und maximal auf 4096 px Kantenlänge begrenzt, damit Piwigos Bildbibliothek sie zuverlässig verarbeiten kann.
11. Der CLI-Derivat-Builder korrigiert die Piwigo-Bildabmessungen anhand des Arbeitsbilds und erzeugt die aktuell konfigurierten Standard- und Custom-Derivate.
12. Der normale Frontend-Aufruf erzeugt **keine** Derivate. Fehlt ein Derivat, wird das vorbereitete Bild als sicherer Fallback ausgeliefert.
13. Originalbilder werden bei echtem Originalabruf serverseitig über `webdav-image.php` aus Nextcloud gestreamt; Nextcloud-Zugangsdaten erscheinen nicht im Browser.
Der Shadow Tree dient ausschließlich dazu, Piwigo die Alben und Bilder als Dateisystemstruktur bereitzustellen. Albumlogik, Dateisynchronisierung und die daraus folgenden Piwigo-Datensätze bleiben Aufgabe von Piwigo.
## Piwigo-Synchronisierung
### Bildauslieferung
WebDAV verwendet denselben Piwigo-Synchronisationspfad wie der bestehende Connector, aber mit einer verbindungseigenen physischen Site.
Die Dateien im Shadow Tree sind Platzhalter. `include/webdav_image_runtime.inc.php` erkennt die WebDAV-Zuordnung und ersetzt die Bildquelle bei der Auslieferung durch den zugehörigen Nextcloud-Inhalt. `webdav-image.php` streamt die Bilddaten serverseitig, ohne Nextcloud-Zugangsdaten an den Browser weiterzugeben.
Nach der Dateisynchronisierung laufen die vorhandenen Piwigo-Nacharbeiten, darunter Metadaten-, Integritäts-, Kategorie-, Pfad-, Rang- und Cache-Aktualisierungen sowie der Orphan-Abgleich.
### Löschen
Die direkte produktive Synchronisierung ist derzeit ausdrücklich auf **Piwigo 16.4.0** abgestimmt.
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.
## WebDAV-Bildausgabe und Derivate
### Diagnose
Wichtige Dateien:
Der Status wird pro Verbindung geführt. Fehler enthalten Zeitpunkt, betroffene Verbindung, Prozesszustand und technische Details. Die Administration zeigt keine lokalen Datenbank-, Storage- oder Mount-Einstellungen mehr an.
- `include/webdav_image_runtime.inc.php` Zuordnung Piwigo-Bild ↔ WebDAV-Quelle, Preview-/Derivat-Helfer und URL-Filter.
- `webdav-image.php` berechtigungsgeprüfter serverseitiger Original-/Preview-Abruf.
- `runtime/lib/precache-webdav-previews.php` vorbereitete JPEG-/PNG-Arbeitsbilder ohne dauerhafte Originalkopie.
- `runtime/lib/build-webdav-derivatives.php` CLI-Aufbau der konfigurierten Piwigo-Derivate.
## Entfernte Connector-Komponenten
Der Frontend-Hook führt keine Bildgenerierung mehr aus. Damit bleibt ein Fehler in GD/Imagick/External ImageMagick auf den Hintergrundlauf begrenzt.
Nicht mehr Bestandteil des Plugins sind lokale Nextcloud-Datenbankanbindungen, PostgreSQL-Reader, Source-/Activity-Views, Storage-Mappings, Host-Mounts, lokale Reconcile-/Sync-Skripte, Cutover-/Migrationswerkzeuge und der frühere Simulations-Webservice `bratonien.nc.sync`.
## Löschen einer WebDAV-Verbindung seit 0.9.6.1
## Prüfpflicht vor Merge
Eine gelöschte WebDAV-Verbindung darf keine weiterhin sichtbaren oder über alte Piwigo-Datensätze erreichbaren Bilder hinterlassen.
Jede Änderung wird vor dem Merge mindestens mit folgenden GitHub-Actions-Prüfungen validiert:
Beim Löschen über die Oberfläche:
- PHP-Syntax für 8.2, 8.3, 8.4 und 8.5
- Prüfung auf doppelte globale `bratonien_tools_*`-Funktionen
- JavaScript-Syntax
- Python-Syntax/Compile
- Shell-Syntax mit `bash -n`
- werden die zur Verbindung gehörende Piwigo-Site, ihre Alben und Bilddatensätze entfernt;
- werden zugehörige Piwigo-Derivate entfernt;
- bleiben die Nextcloud-Originaldateien unangetastet;
- werden Laufzeit- und Preview-Daten anschließend bereinigt.
Zusätzlich läuft `runtime/cleanup-webdav-piwigo.php` im gemeinsamen Runner. Er erkennt verwaiste WebDAV-Piwigo-Sites, für die keine Connector-Verbindung mehr existiert, und entfernt deren Piwigo-Inhalte sowie Gallery-, Source-, Preview-, State-, Status- und Runtime-Reste. Damit werden auch bereits vor 0.9.6.1 gelöschte Verbindungen nachträglich bereinigt.
## Gemeinsamer Runner
`runtime/run-all.sh` verarbeitet die Schritte in kontrollierter Reihenfolge:
1. lokale Verbindungen reconciliieren;
2. WebDAV-Verbindungen reconciliieren;
3. verwaiste WebDAV-Piwigo-Inhalte bereinigen;
4. verwaiste allgemeine Runtime-Dateien bereinigen;
5. aktive WebDAV-Verbindungen synchronisieren;
6. aktive lokale Verbindungen synchronisieren.
Der vorhandene systemd-Timer ruft direkt die Runtime aus dem installierten Plugin auf.
## Status- und UI-Regeln
- Hintergrund-Polling lädt die Admin-Seite nicht neu.
- Ein geöffneter Verbindungsassistent bleibt bei Statusaktualisierungen geöffnet.
- Fehlgeschlagene Fallback-/Credential-Prüfungen schließen den Assistenten nicht.
- Laufzeitfehler unterscheiden WebDAV-Lesen, Piwigo-Synchronisierung, Preview- und Derivatfehler.
## Sicherheit
- Connector-Zugangsdaten werden verschlüsselt gespeichert.
- WebDAV-Credentials werden nur serverseitig verwendet.
- Bildzugriffe über `webdav-image.php` werden gegen Piwigos Zugriffsrechte geprüft.
- Nextcloud-Originale werden weder beim Sync noch beim Löschen einer Verbindung verändert.
- Derivate und Arbeitsbilder sind lokale Cache-/Arbeitsdaten und dürfen entfernt beziehungsweise neu aufgebaut werden.
## Aktueller Prüfstand
Bereits umgesetzt:
- WebDAV-Verbindungsassistent;
- Verzeichnisauswahl;
- rekursiver WebDAV-Scan;
- Shadow Tree;
- separate Piwigo-Site je WebDAV-Verbindung;
- sichtbare Albumstruktur ohne technische Wrapper;
- echte Bildausgabe aus Nextcloud;
- lokale vorbereitete Arbeitsbilder;
- CLI-Derivatstrecke;
- Piwigo-Nacharbeiten und Orphan-Abgleich;
- ETag-basierte Wiederverwendung vorbereiteter Bilder;
- Löschen einer Verbindung einschließlich Piwigo-Inhalten;
- nachträgliche Bereinigung bereits verwaister WebDAV-Piwigo-Sites.
Weiter zu prüfen beziehungsweise unter realer Last zu härten:
- vollständiger Derivataufbau für alle in der Installation vorkommenden Standard- und Custom-Größen;
- Verhalten bei großen Bildmengen und vielen gleichzeitig noch nicht vorbereiteten Bildern;
- Fehlerfälle verschiedener Bildformate und Bild-Backends;
- langfristige Änderungserkennung und Performance im produktiven Einsatz.
## Versionsregel
Die aktuelle Versionslinie ist **0.9.6.x**. Weitere Sprünge auf eine neue Minor-/Major-Linie erfolgen nur bewusst und ausdrücklich.

139
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.
Aktuelle Plugin-Version: **0.9.6.2**
Aktuelle Plugin-Version: **0.9.6.1**
## Grundprinzip
@@ -12,7 +12,7 @@ Für die Administration gilt: Der normale Nutzer sieht Aufgaben, Entscheidungen
## NC Connector
Der NC Connector bindet ausdrücklich ausgewählte Nextcloud-Inhalte ausschließlich per WebDAV an Piwigo an.
Der NC Connector synchronisiert ausdrücklich ausgewählte Nextcloud-Inhalte mit Piwigo.
### Datenmodell
@@ -22,9 +22,21 @@ Der NC Connector bindet ausdrücklich ausgewählte Nextcloud-Inhalte ausschließ
- Ordner werden als physische Piwigo-Alben synchronisiert.
- Root-Dateien können als Piwigo-Orphans registriert werden.
- Neu importierte physische Connector-Alben werden privat angelegt.
- Entfernte Quellen verschwinden aus Shadow Tree und Piwigo; die Nextcloud-Originale bleiben erhalten.
- Entfernte Quellen werden aus Shadow Tree und Piwigo entfernt; die Nextcloud-Originale bleiben erhalten.
### Voraussetzungen
### Bestehende lokale Quellenmodi
Der bestehende Connector unterstützt weiterhin:
- `legacy-view`
- `user-shares`
- `selected-fileids`
Diese Modi bleiben erhalten. Es gibt keine automatische Migration auf WebDAV.
### WebDAV-Modus
Der neue Endnutzerpfad verwendet `source_mode=webdav-placeholder` und `adapter=remote`.
Benötigt werden nur:
@@ -33,7 +45,15 @@ Benötigt werden nur:
- normaler WebDAV-Zugriff auf die Inhalte dieses Benutzers;
- eine normale Piwigo-Installation mit ihrer vorhandenen PHP-/Bildverarbeitungsumgebung.
Nicht benötigt werden Nextcloud-Datenbankzugriff, `occ`-Adminzugriff, Storage-IDs, Backend-Pfade, zusätzliche Host-Mounts, FUSE, davfs oder rclone.
Nicht vorausgesetzt werden:
- PostgreSQL-Zugriff auf Nextcloud;
- `occ`-Adminzugriff;
- Storage-IDs oder Backend-Pfade;
- zusätzliche Host-Mounts;
- FUSE;
- davfs;
- rclone.
### WebDAV-Ablauf
@@ -43,34 +63,54 @@ Nicht benötigt werden Nextcloud-Datenbankzugriff, `occ`-Adminzugriff, Storage-I
4. `runtime/lib/build_webdav_placeholder_source.py` erzeugt eine lokale Platzhalterquelle ohne dauerhafte Originalkopie.
5. `runtime/lib/shadow_tree.py` baut daraus den verbindungseigenen Shadow Tree.
6. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID`.
7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert.
8. Piwigos Dateisynchronisierung erhält ausschließlich diese verbindungseigene Site.
9. Die WebDAV-Bildzuordnung ersetzt beim Ausliefern Platzhalter-URLs durch die echte Nextcloud-Bildquelle.
10. `webdav-image.php` streamt benötigte Bilder serverseitig; Zugangsdaten werden dem Browser nicht offengelegt.
7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert. Die ausgewählte Nextcloud-Wurzel erscheint dadurch direkt als Album; technische `bratonien-webdav-ID`-Wrapper bleiben unsichtbar.
8. `runtime/lib/precache-webdav-previews.php` erzeugt vorbereitete Arbeitsbilder als JPEG beziehungsweise PNG mit maximal 4096 px Kantenlänge.
9. `runtime/lib/build-webdav-derivatives.php` korrigiert die Piwigo-Bildabmessungen und erzeugt die konfigurierten Standard- und Custom-Derivate im Hintergrundlauf.
10. Der Frontend-Aufruf erzeugt keine Derivate. Fehlt eines, wird das vorbereitete Arbeitsbild als sicherer Fallback ausgeliefert.
11. Wenn das Original benötigt wird, streamt `webdav-image.php` es serverseitig direkt aus Nextcloud. Zugangsdaten werden dem Browser nicht offengelegt.
### Piwigo-Synchronisierung
Der Connector verwendet die vorhandene Piwigo-Dateisynchronisierung für die verbindungseigene WebDAV-Site. Der Shadow Tree ist die Dateisystemdarstellung, die Piwigo für Alben und Bilder benötigt.
Der WebDAV-Pfad nutzt die vorhandene Piwigo-Synchronisationslogik mit einer verbindungseigenen Site.
`runtime/lib/piwigo-sync.php` ruft dafür `bratonien.nc.syncProductive` auf. Die Methode führt den Piwigo-Core-Sync für die konkrete Site aus. `bratonien.nc.syncOrphans` arbeitet ebenfalls nur auf der übergebenen WebDAV-Site.
Nach dem Dateiabgleich laufen die vorhandenen Nacharbeiten, darunter:
Die produktive Synchronisierung ist aktuell auf **Piwigo 16.4.0** abgestimmt.
- Metadaten-Synchronisierung;
- Bild- und Kategorieintegrität;
- Uppercats/Kategoriestruktur;
- globale Ränge;
- Pfadpflege;
- Rating-Score;
- Benutzer-Cache-Invalidierung;
- Orphan-Abgleich.
### Piwigo-Zugang
Die direkte produktive Synchronisierung ist aktuell ausdrücklich auf **Piwigo 16.4.0** abgestimmt.
Der Zugang wird pro Verbindung gespeichert. Bevorzugt wird eine Piwigo-API. Wird keine API eingerichtet, kann ein Administrator-/Webmaster-Benutzer mit Passwort als Fallback verwendet werden.
### Fallback-Zugang
Piwigo wird API-first angesprochen. Ist für eine Verbindung keine API konfiguriert, kann Benutzername/Passwort als Fallback verwendet werden.
Der Assistent prüft Fallback-Zugangsdaten vor dem Speichern. Eine fehlgeschlagene Prüfung schließt oder leert den Assistenten nicht.
### Statusanzeige
Die Administration zeigt Laufzeitstatus und Fehler pro Verbindung. Fehlerdetails bleiben der betroffenen Verbindung zugeordnet.
Die Administration zeigt den Laufzeitstatus regelmäßig im Hintergrund an. Dafür wird die Admin-Seite nicht neu geladen; offene Dialoge, Assistenten und aufgeklappte Bereiche bleiben erhalten.
Fehler werden nach Prozessschritt getrennt dargestellt, unter anderem für WebDAV-Einlesen, Piwigo-Synchronisierung, Preview-Erzeugung und Derivat-Erzeugung.
### Löschen einer Verbindung
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.
Seit **0.9.6.1** besitzt der WebDAV-Pfad einen vollständigen Lösch-Lebenszyklus:
- Beim Löschen einer WebDAV-Verbindung werden ihre Piwigo-Site, Alben und Bilddatensätze entfernt.
- Zugehörige Piwigo-Derivate werden entfernt.
- Nextcloud-Originale bleiben unverändert.
- Runtime-, Shadowtree-, Source-, Preview- und Statusdaten werden bereinigt.
- `runtime/cleanup-webdav-piwigo.php` erkennt zusätzlich bereits verwaiste WebDAV-Piwigo-Sites, für die keine Connector-Verbindung mehr existiert, und bereinigt sie im gemeinsamen Runner nachträglich.
### Runtime
Aktive WebDAV-Verbindungen werden über den gemeinsamen Runner verarbeitet:
Aktive Verbindungen werden über den gemeinsamen Runner verarbeitet:
- `bratonien-nc-connector.timer`
- `bratonien-nc-connector.service`
@@ -78,15 +118,25 @@ Aktive WebDAV-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`.
Der Lauf besteht aus:
Die Reihenfolge des gemeinsamen Laufs ist:
1. WebDAV-Verbindungen reconciliieren;
2. verwaiste WebDAV-Piwigo-Inhalte bereinigen;
3. jede vorhandene WebDAV-Verbindung synchronisieren.
1. lokale Verbindungen reconciliieren;
2. WebDAV-Verbindungen reconciliieren;
3. verwaiste WebDAV-Piwigo-Inhalte bereinigen;
4. allgemeine verwaiste Runtime-Dateien bereinigen;
5. WebDAV-Verbindungen synchronisieren;
6. lokale Verbindungen synchronisieren.
## Bildcache
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.
Bratonien Tools kann:
- Piwigo-Bildderivate gezielt leeren;
- vorhandene Bildgrößen neu erzeugen;
- den Cache-Aufbau als Worker-Prozess starten und abbrechen;
- die Worker-Zahl automatisch oder manuell konfigurieren.
Originalbilder bleiben unangetastet.
## Wasserzeichenverwaltung
@@ -121,7 +171,14 @@ Alben können in Bratonien Tools zwischen öffentlich und privat umgeschaltet we
## Geschützte Albumfreigaben
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.
Private Alben können über eigene Freigabelinks geteilt werden:
- optionales Passwort;
- optionales Ablaufdatum;
- eigener technischer Piwigo-Benutzer pro Freigabe;
- minimaler Albumzugriff;
- Hash-Speicherung von Passwörtern und Tokens;
- Widerruf und automatische Bereinigung beim Löschen eines Albums.
## Erweiterte Bildnavigation
@@ -129,25 +186,34 @@ Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges B
## Selbstaktualisierung
Der integrierte Updater liest den Zielstand aus GitHub, bindet ein Update an einen konkreten Commit, prüft Version und SHA-256, erstellt vor dem Austausch ein Backup und verlangt Webmaster-Rechte sowie die notwendigen Servervoraussetzungen.
Der integrierte Updater:
- liest den Zielstand aus GitHub;
- bindet ein Update an einen konkreten Commit;
- prüft Version und SHA-256 des Zielstands;
- erstellt vor dem Austausch ein Backup;
- nutzt Post/Redirect/Get;
- verlangt Webmaster-Rechte, `ZipArchive` und ausreichende Schreibrechte.
## Wichtige Dateien und Verzeichnisse
- `main.inc.php` Plugin-Einstieg und Runtime-Hooks
- `admin.php` zentraler Admin-Controller
- `include/nc_connector.inc.php` WebDAV-Verbindungsmodell
- `include/nc_connector.inc.php` Datenmodell und gemeinsame Connector-Funktionen
- `include/nc_connector_wizard.inc.php` Verbindungsassistent
- `include/nc_connector_wizard_webdav_flow.inc.php` WebDAV-Wizardablauf
- `include/nc_connector_delete_safe.inc.php` Löschen einschließlich WebDAV-Piwigo-Inhalten
- `include/nc_productive_ws.inc.php` Piwigo-Core-Dateisync
- `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung der konkreten WebDAV-Site
- `include/webdav_image_runtime.inc.php` WebDAV-Bildzuordnung und URL-Filter
- `include/nc_connector_wizard_webdav_flow.inc.php` WebDAV-first-Wizardablauf
- `include/nc_connector_delete_safe.inc.php` sicheres Löschen einschließlich WebDAV-Piwigo-Inhalten
- `include/nc_productive_ws.inc.php` produktiver Piwigo-Dateisync
- `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung
- `include/webdav_image_runtime.inc.php` WebDAV-Bildzuordnung, Preview-/Derivathilfen und URL-Filter
- `webdav-image.php` berechtigungsgeprüfter WebDAV-Bildstream
- `runtime/reconcile-webdav.php` WebDAV-Runtime-Reconcile
- `runtime/cleanup-webdav-piwigo.php` Bereinigung verwaister WebDAV-Piwigo-Sites
- `runtime/cleanup-webdav-piwigo.php` Bereinigung gelöschter/verwaister WebDAV-Verbindungen in Piwigo
- `runtime/sync-webdav.sh` Ablauf einer WebDAV-Verbindung
- `runtime/run-all.sh` gemeinsamer WebDAV-Runner
- `runtime/run-all.sh` gemeinsamer Multi-Connection-Runner
- `runtime/lib/build_webdav_placeholder_source.py` rekursiver WebDAV-Scan und Platzhalterquelle
- `runtime/lib/precache-webdav-previews.php` vorbereitete JPEG-/PNG-Arbeitsbilder
- `runtime/lib/build-webdav-derivatives.php` CLI-Derivat-Builder
- `runtime/lib/shadow_tree.py` atomarer Shadow Tree
- `runtime/lib/piwigo-sync.php` Piwigo-Sync mit API/Fallback
- `main-cache-build.php` allgemeiner Piwigo-Derivat-/Cache-Builder
@@ -165,7 +231,16 @@ Der integrierte Updater liest den Zielstand aus GitHub, bindet ein Update an ein
- Connector-Zugangsdaten werden verschlüsselt gespeichert;
- Nextcloud-Zugangsdaten werden verbindungseigen gespeichert und nur serverseitig verwendet;
- Wizard-Geheimnisse werden nicht im Browser-Web-Storage persistiert;
- SQL-View-Namen werden vor Verwendung validiert;
- produktive Piwigo-API ist versionsgebunden;
- Originalbilder werden vom Connector nicht gelöscht;
- WebDAV-Originalbilder werden nicht dauerhaft lokal gespeichert;
- Update-Pakete werden an Commit und Hash gebunden.
## Entwicklungsstand
`0.9.6.x` ist die aktuelle Versionslinie.
Mit **0.9.6.1** ist der WebDAV-Pfad einschließlich Piwigo-Registrierung, vorbereiteter Bildquelle, CLI-Derivatstrecke und Lösch-/Cleanup-Lebenszyklus im Repository umgesetzt. Der bestehende lokale Connector bleibt parallel erhalten.
Der jeweils detaillierte Prüf- und Entwicklungsstand steht in [`CURRENT_STATUS.md`](CURRENT_STATUS.md).

115
admin.php
View File

@@ -9,6 +9,74 @@ check_status(ACCESS_ADMINISTRATOR);
require_once(BRATONIEN_TOOLS_PATH . 'include/tool_registry.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_system.inc.php');
function bratonien_tools_nc_connector_admin_connections(array $connections)
{
$by_id = array();
foreach ($connections as $connection)
{
$by_id[(int)$connection['id']] = $connection;
}
$hidden_legacy_ids = array();
$logical_remote = array();
foreach ($connections as $connection)
{
if ((string)($connection['adapter'] ?? '') !== 'remote') continue;
$migration = isset($connection['config']['migration']) && is_array($connection['config']['migration'])
? $connection['config']['migration']
: array();
if ((string)($migration['role'] ?? '') !== 'webdav-primary-candidate') continue;
$legacy_id = (int)($migration['legacy_fallback_connection_id'] ?? 0);
if ($legacy_id < 1 || empty($by_id[$legacy_id]) || (string)$by_id[$legacy_id]['adapter'] !== 'local') continue;
$legacy = $by_id[$legacy_id];
$hidden_legacy_ids[$legacy_id] = true;
$connection['logical_connection'] = true;
$connection['legacy_fallback_connection_id'] = $legacy_id;
$connection['enabled'] = !empty($connection['enabled']) || !empty($legacy['enabled']);
if ($connection['enabled']) $connection['takeover_state'] = 'active';
$connection['fallback_stored'] = !empty($connection['fallback_stored']) || !empty($legacy['fallback_stored']);
if (trim((string)$connection['name']) === '') $connection['name'] = (string)$legacy['name'];
$logical_remote[(int)$connection['id']] = $connection;
}
$visible = array();
foreach ($connections as $connection)
{
$id = (int)$connection['id'];
if (isset($hidden_legacy_ids[$id])) continue;
if (isset($logical_remote[$id])) $connection = $logical_remote[$id];
$visible[] = $connection;
}
return $visible;
}
function bratonien_tools_nc_connector_admin_last_status(array $connection)
{
$latest = bratonien_tools_nc_connector_connection_last_status($connection);
$legacy_id = (int)($connection['legacy_fallback_connection_id'] ?? 0);
if ($legacy_id < 1)
{
return $latest;
}
$legacy = bratonien_tools_nc_connector_connection($legacy_id, false);
if (!$legacy)
{
return $latest;
}
$legacy_status = bratonien_tools_nc_connector_connection_last_status($legacy);
if ((int)($legacy_status['timestamp'] ?? 0) > (int)($latest['timestamp'] ?? 0))
{
return $legacy_status;
}
return $latest;
}
$tools = bratonien_tools_get_tools();
$messages = array();
$errors = array();
@@ -94,6 +162,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bratonien_tool']))
);
$redirect_url = get_root_url().'admin.php?page=plugin-'.BRATONIEN_TOOLS_ID;
$wizard_action = strpos($tool_id, 'nc_connector_wizard_') === 0
|| $tool_id === 'nc_connector_migrate_start'
|| $tool_id === 'nc_connector_edit_start';
if ($wizard_action)
{
$wizard_closed = $tool_id === 'nc_connector_wizard_reset'
|| ($tool_id === 'nc_connector_wizard_finish' && empty($errors));
if (($tool_id === 'nc_connector_migrate_start' || $tool_id === 'nc_connector_edit_start') && !empty($errors))
{
$wizard_closed = true;
}
$redirect_url .= '&nc_wizard='.($wizard_closed ? 'closed' : 'open');
}
if (!headers_sent())
{
header('Location: '.$redirect_url, true, 303);
@@ -115,13 +196,37 @@ $asset_environment = bratonien_tools_get_asset_environment();
$album_shares = bratonien_tools_get_album_shares();
$private_albums = bratonien_tools_get_private_albums();
$nc_connector = bratonien_tools_nc_connector_status();
$nc_connector_runtime_connections = $nc_connector['connections'];
$nc_connector['connections'] = bratonien_tools_nc_connector_admin_connections($nc_connector_runtime_connections);
$nc_connector['connection_count'] = count($nc_connector['connections']);
$nc_connector['active_count'] = 0;
foreach ($nc_connector['connections'] as $visible_connection)
{
if (!empty($visible_connection['enabled'])) $nc_connector['active_count']++;
}
foreach ($nc_connector['connections'] as &$nc_connection)
{
$nc_connection['last_sync'] = bratonien_tools_nc_connector_connection_last_status($nc_connection);
$nc_connection['last_sync'] = bratonien_tools_nc_connector_admin_last_status($nc_connection);
$nc_connection['display_name'] = $nc_connection['name'];
$storage_lines = array();
$storages = isset($nc_connection['config']['storages']) && is_array($nc_connection['config']['storages'])
? $nc_connection['config']['storages']
: array();
foreach ($storages as $storage)
{
$storage_lines[] = (string)($storage['storage_id'] ?? '').' | '.(string)($storage['source_prefix'] ?? '').' | '.(string)($storage['local_mount'] ?? '');
}
$nc_connection['storage_text'] = implode("\n", $storage_lines);
if (!empty($nc_connection['fallback_stored']))
{
$nc_connection['display_name'] .= ' · Fallback gespeichert';
$nc_connection['verification_checks'][] = array(
'name' => 'Piwigo-Fallback',
'ok' => true,
'detail' => 'Benutzername/Passwort verschluesselt gespeichert',
);
}
}
unset($nc_connection);
@@ -143,10 +248,14 @@ $nc_system_defaults = array(
'last_run_error_detail' => '',
'next_run_timestamp' => 0,
'next_run_label' => 'Nicht verfügbar',
'legacy_runtime_exists' => false,
'legacy_config_exists' => false,
'legacy_service_exists' => false,
'legacy_timer_exists' => false,
);
$nc_connector['system'] = array_merge(
$nc_system_defaults,
bratonien_tools_nc_connector_system_status($nc_connector['connections'])
bratonien_tools_nc_connector_system_status($nc_connector_runtime_connections)
);
$album_lock_page_number = isset($_GET['br_album_page']) ? max(1, (int)$_GET['br_album_page']) : 1;
$album_lock_search = isset($_GET['br_album_search']) ? trim((string)$_GET['br_album_search']) : '';
@@ -272,4 +381,4 @@ $admin_content .= $template->parse('asset_manager_admin_content', true);
$admin_content .= $template->parse('album_shares_admin_content', true);
$admin_content .= $template->parse('nc_connector_admin_content', true);
$admin_content .= $template->parse('system_admin_content', true);
$template->assign('ADMIN_CONTENT', $admin_content);
$template->assign('ADMIN_CONTENT', $admin_content);

View File

@@ -4,6 +4,15 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!');
}
/**
* NC Connector migration and verification phase.
*
* The existing /etc/piwigo-sync installation remains the production path.
* Bratonien Tools imports a copy of its configuration into its own connection
* store and verifies that copy independently. Importing and verification never
* change or stop the legacy sync.
*/
function bratonien_tools_nc_connector_table()
{
if (function_exists('bratonien_tools_table'))
@@ -21,8 +30,9 @@ function bratonien_tools_nc_connector_ensure_table()
id int(11) NOT NULL AUTO_INCREMENT,
connection_key varchar(64) NOT NULL,
name varchar(255) NOT NULL,
adapter enum('remote') NOT NULL DEFAULT 'remote',
enabled tinyint(1) NOT NULL DEFAULT 1,
adapter enum('local','remote') NOT NULL DEFAULT 'local',
enabled tinyint(1) NOT NULL DEFAULT 0,
takeover_state enum('imported','verified','active','disabled') NOT NULL DEFAULT 'imported',
config_json mediumtext NOT NULL,
secret_blob mediumtext DEFAULT NULL,
created datetime NOT NULL,
@@ -54,59 +64,139 @@ function bratonien_tools_nc_connector_secret_key()
function bratonien_tools_nc_connector_encrypt_secret($plain)
{
$plain = (string)$plain;
if ($plain === '') return '';
if (!function_exists('openssl_encrypt')) throw new RuntimeException('OpenSSL wird zum sicheren Speichern der Connector-Zugangsdaten benötigt.');
if ($plain === '')
{
return '';
}
if (!function_exists('openssl_encrypt'))
{
throw new RuntimeException('OpenSSL wird zum sicheren Speichern der Connector-Zugangsdaten benoetigt.');
}
$iv = random_bytes(12);
$tag = '';
$cipher = openssl_encrypt($plain, 'aes-256-gcm', bratonien_tools_nc_connector_secret_key(), OPENSSL_RAW_DATA, $iv, $tag);
if ($cipher === false) throw new RuntimeException('Connector-Zugangsdaten konnten nicht verschlüsselt werden.');
$cipher = openssl_encrypt(
$plain,
'aes-256-gcm',
bratonien_tools_nc_connector_secret_key(),
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($cipher === false)
{
throw new RuntimeException('Connector-Zugangsdaten konnten nicht verschluesselt werden.');
}
return base64_encode(json_encode(array(
'v'=>1,
'iv'=>base64_encode($iv),
'tag'=>base64_encode($tag),
'data'=>base64_encode($cipher),
'v' => 1,
'iv' => base64_encode($iv),
'tag' => base64_encode($tag),
'data' => base64_encode($cipher),
)));
}
function bratonien_tools_nc_connector_decrypt_secret($blob)
{
$blob = trim((string)$blob);
if ($blob === '') return '';
if (!function_exists('openssl_decrypt')) throw new RuntimeException('OpenSSL wird zum Lesen der Connector-Zugangsdaten benötigt.');
if ($blob === '')
{
return '';
}
if (!function_exists('openssl_decrypt'))
{
throw new RuntimeException('OpenSSL wird zum Lesen der Connector-Zugangsdaten benoetigt.');
}
$outer = base64_decode($blob, true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) throw new RuntimeException('Gespeicherte Connector-Zugangsdaten haben ein unbekanntes Format.');
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1)
{
throw new RuntimeException('Gespeicherte Connector-Zugangsdaten haben ein unbekanntes Format.');
}
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) throw new RuntimeException('Gespeicherte Connector-Zugangsdaten sind beschädigt.');
if (!is_string($iv) || !is_string($tag) || !is_string($cipher))
{
throw new RuntimeException('Gespeicherte Connector-Zugangsdaten sind beschaedigt.');
}
$plain = openssl_decrypt(
$cipher,
'aes-256-gcm',
bratonien_tools_nc_connector_secret_key(),
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($plain === false)
{
throw new RuntimeException('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
}
$plain = openssl_decrypt($cipher, 'aes-256-gcm', bratonien_tools_nc_connector_secret_key(), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) throw new RuntimeException('Connector-Zugangsdaten konnten nicht entschlüsselt werden.');
return (string)$plain;
}
function bratonien_tools_nc_connector_is_webdav(array $row)
function bratonien_tools_nc_connector_read_config($path)
{
$config = isset($row['config']) && is_array($row['config']) ? $row['config'] : array();
return (string)($row['adapter'] ?? '') === 'remote' && (string)($config['source_mode'] ?? '') === 'webdav-placeholder';
$config = array();
if (!is_readable($path))
{
return $config;
}
$lines = @file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!is_array($lines))
{
return $config;
}
foreach ($lines as $line)
{
$line = trim($line);
if ($line === '' || $line[0] === '#')
{
continue;
}
if (!preg_match('/^([A-Z0-9_]+)=(.*)$/', $line, $matches))
{
continue;
}
$value = trim($matches[2]);
$length = strlen($value);
if ($length >= 2)
{
$first = $value[0];
$last = $value[$length - 1];
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'"))
{
$value = substr($value, 1, -1);
}
}
$config[$matches[1]] = $value;
}
return $config;
}
function bratonien_tools_nc_connector_connections()
{
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
$result = pwg_query("SELECT id, connection_key, name, adapter, enabled, config_json, secret_blob, created, updated FROM `$table` WHERE adapter='remote' ORDER BY id");
$result = pwg_query("SELECT id, connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated FROM `$table` ORDER BY id");
$connections = array();
while ($row = pwg_db_fetch_assoc($result))
{
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
if (!is_array($config))
{
$config = array();
}
$verification = isset($config['verification']) && is_array($config['verification']) ? $config['verification'] : array();
$has_fallback = false;
try
@@ -115,7 +205,8 @@ function bratonien_tools_nc_connector_connections()
$credentials = json_decode($plain, true);
if (is_array($credentials))
{
$has_fallback = trim((string)($credentials['piwigo_user'] ?? '')) !== '' && (string)($credentials['piwigo_password'] ?? '') !== '';
$has_fallback = trim((string)($credentials['piwigo_user'] ?? '')) !== ''
&& (string)($credentials['piwigo_password'] ?? '') !== '';
}
}
catch (Throwable $e)
@@ -127,9 +218,17 @@ function bratonien_tools_nc_connector_connections()
$row['id'] = (int)$row['id'];
$row['enabled'] = (bool)$row['enabled'];
$row['config'] = $config;
$row['user'] = (string)($config['nextcloud_access_user'] ?? $config['access_user'] ?? '');
$row['storage_count'] = isset($config['roots']) && is_array($config['roots']) ? count($config['roots']) : 0;
$row['host'] = isset($config['host']) ? (string)$config['host'] : '';
$row['database'] = isset($config['database']) ? (string)$config['database'] : '';
$row['user'] = isset($config['user']) ? (string)$config['user'] : '';
$row['source_view'] = isset($config['source_view']) ? (string)$config['source_view'] : '';
$row['storage_count'] = isset($config['storages']) && is_array($config['storages']) ? count($config['storages']) : 0;
$row['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;
}
@@ -141,24 +240,233 @@ function bratonien_tools_nc_connector_connection($id, $with_secret = false)
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
$id = (int)$id;
if ($id <= 0) return null;
if ($id <= 0)
{
return null;
}
$columns = 'id, connection_key, name, adapter, enabled, config_json, created, updated';
if ($with_secret) $columns .= ', secret_blob';
$result = pwg_query("SELECT $columns FROM `$table` WHERE id = $id AND adapter='remote' LIMIT 1");
if (!pwg_db_num_rows($result)) return null;
$columns = 'id, connection_key, name, adapter, enabled, takeover_state, config_json, created, updated';
if ($with_secret)
{
$columns .= ', secret_blob';
}
$result = pwg_query("SELECT $columns FROM `$table` WHERE id = $id LIMIT 1");
if (!pwg_db_num_rows($result))
{
return null;
}
$row = pwg_db_fetch_assoc($result);
$row['id'] = (int)$row['id'];
$row['enabled'] = (bool)$row['enabled'];
$row['config'] = json_decode((string)$row['config_json'], true);
if (!is_array($row['config']) || (string)($row['config']['source_mode'] ?? '') !== 'webdav-placeholder') return null;
if (!is_array($row['config']))
{
$row['config'] = array();
}
return $row;
}
function bratonien_tools_nc_connector_import_bundle_path()
{
return '/tmp/bratonien-tools-nc-import.json';
}
function bratonien_tools_nc_connector_import_legacy()
{
$path = bratonien_tools_nc_connector_import_bundle_path();
if (!is_readable($path))
{
throw new RuntimeException('Kein lesbares Migrationspaket gefunden. Fuehre zuerst den angezeigten Connector-Migrationsbefehl im Piwigo-LXC aus.');
}
$raw = @file_get_contents($path);
$bundle = is_string($raw) ? json_decode($raw, true) : null;
if (!is_array($bundle) || (int)($bundle['format'] ?? 0) !== 1 || !is_array($bundle['config'] ?? null))
{
throw new RuntimeException('Das Migrationspaket ist ungueltig oder unvollstaendig.');
}
$legacy = $bundle['config'];
$required = array('NC_DB_HOST', 'NC_DB_PORT', 'NC_DB_NAME', 'NC_DB_USER', 'NC_DB_VIEW');
foreach ($required as $key)
{
if (!isset($legacy[$key]) || trim((string)$legacy[$key]) === '')
{
throw new RuntimeException('Im Migrationspaket fehlt '.$key.'.');
}
}
$password = isset($bundle['db_password']) ? (string)$bundle['db_password'] : '';
if ($password === '')
{
throw new RuntimeException('Das Migrationspaket enthaelt kein Datenbankpasswort.');
}
$config = array(
'host' => (string)$legacy['NC_DB_HOST'],
'port' => (string)$legacy['NC_DB_PORT'],
'database' => (string)$legacy['NC_DB_NAME'],
'user' => (string)$legacy['NC_DB_USER'],
'source_view' => (string)$legacy['NC_DB_VIEW'],
'activity_view' => isset($legacy['NC_ACTIVITY_VIEW']) ? (string)$legacy['NC_ACTIVITY_VIEW'] : 'piwigo_showcase_activity',
'gallery_root' => isset($legacy['GALLERY_ROOT']) ? (string)$legacy['GALLERY_ROOT'] : '',
'state_dir' => isset($legacy['STATE_DIR']) ? (string)$legacy['STATE_DIR'] : '',
'status_file' => isset($legacy['STATUS_FILE']) ? (string)$legacy['STATUS_FILE'] : '',
'quiet_seconds' => isset($legacy['QUIET_SECONDS']) ? (int)$legacy['QUIET_SECONDS'] : 120,
'max_wait_seconds' => isset($legacy['MAX_WAIT_SECONDS']) ? (int)$legacy['MAX_WAIT_SECONDS'] : 900,
'full_sync_seconds' => isset($legacy['FULL_SYNC_SECONDS']) ? (int)$legacy['FULL_SYNC_SECONDS'] : 86400,
'storages' => isset($bundle['storages']) && is_array($bundle['storages']) ? $bundle['storages'] : array(),
'imported_from' => '/etc/piwigo-sync/piwigo.conf',
'legacy_sync_enabled' => isset($legacy['PIWIGO_SYNC_ENABLED']) && (string)$legacy['PIWIGO_SYNC_ENABLED'] === '1',
);
$key_material = $config['host'].'|'.$config['database'].'|'.$config['user'].'|'.$config['source_view'];
$connection_key = 'legacy-'.substr(hash('sha256', $key_material), 0, 24);
$table = bratonien_tools_nc_connector_table();
bratonien_tools_nc_connector_ensure_table();
$now = date('Y-m-d H:i:s');
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($password);
$escaped_key = pwg_db_real_escape_string($connection_key);
$existing = pwg_query("SELECT id FROM `$table` WHERE connection_key = '$escaped_key' LIMIT 1");
if (pwg_db_num_rows($existing))
{
$row = pwg_db_fetch_assoc($existing);
$id = (int)$row['id'];
pwg_query("UPDATE `$table` SET
name = 'Bestehende Nextcloud',
adapter = 'local',
enabled = 0,
takeover_state = 'imported',
config_json = '".pwg_db_real_escape_string($config_json)."',
secret_blob = '".pwg_db_real_escape_string($secret_blob)."',
updated = '".pwg_db_real_escape_string($now)."'
WHERE id = $id");
}
else
{
pwg_query("INSERT INTO `$table`
(connection_key, name, adapter, enabled, takeover_state, config_json, secret_blob, created, updated)
VALUES (
'$escaped_key',
'Bestehende Nextcloud',
'local',
0,
'imported',
'".pwg_db_real_escape_string($config_json)."',
'".pwg_db_real_escape_string($secret_blob)."',
'".pwg_db_real_escape_string($now)."',
'".pwg_db_real_escape_string($now)."'
)");
}
@unlink($path);
return array(
'message' => 'Bestehende Nextcloud-Verbindung wurde in den NC Connector importiert. Der produktive Legacy-Sync bleibt unveraendert aktiv; die importierte Verbindung ist noch nicht aktiviert.',
);
}
function bratonien_tools_nc_connector_view_name($name)
{
$name = trim((string)$name);
if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/', $name))
{
throw new RuntimeException('Ungueltiger PostgreSQL-View-Name in der Connector-Konfiguration.');
}
$parts = explode('.', $name);
$quoted = array();
foreach ($parts as $part)
{
$quoted[] = '"'.str_replace('"', '""', $part).'"';
}
return implode('.', $quoted);
}
function bratonien_tools_nc_connector_psql($config, $password, $query)
{
$psql = '/usr/bin/psql';
if (!is_executable($psql))
{
throw new RuntimeException('PostgreSQL-Client /usr/bin/psql ist nicht installiert oder nicht ausfuehrbar.');
}
if (!function_exists('proc_open'))
{
throw new RuntimeException('PHP-Funktion proc_open ist deaktiviert; Connector-Verifikation kann nicht ausgefuehrt werden.');
}
$command = array(
$psql,
'-X', '-A', '-t',
'-v', 'ON_ERROR_STOP=1',
'-h', (string)$config['host'],
'-p', (string)$config['port'],
'-U', (string)$config['user'],
'-d', (string)$config['database'],
'-c', (string)$query,
);
$descriptors = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$env = array(
'PGPASSWORD' => (string)$password,
'PGCONNECT_TIMEOUT' => '5',
'LC_ALL' => 'C',
);
$process = @proc_open($command, $descriptors, $pipes, null, $env);
if (!is_resource($process))
{
throw new RuntimeException('PostgreSQL-Pruefprozess konnte nicht gestartet werden.');
}
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
$stdout = trim((string)$stdout);
$stderr = trim((string)$stderr);
if ($exit !== 0)
{
$detail = $stderr !== '' ? $stderr : 'psql Exit-Code '.$exit;
throw new RuntimeException('PostgreSQL-Abfrage fehlgeschlagen: '.$detail);
}
return $stdout;
}
function bratonien_tools_nc_connector_mount_points()
{
$mounts = array();
$content = @file_get_contents('/proc/self/mountinfo');
if (!is_string($content))
{
return $mounts;
}
foreach (preg_split('/\r\n|\r|\n/', $content) as $line)
{
if ($line === '') continue;
$parts = explode(' ', $line);
if (count($parts) < 5) continue;
$mount = str_replace(array('\\040','\\011','\\012','\\134'), array(' ',"\t","\n",'\\'), $parts[4]);
$mounts[rtrim($mount, '/')] = true;
}
return $mounts;
}
function bratonien_tools_nc_connector_status()
{
$connections = bratonien_tools_nc_connector_connections();
$legacy_config = '/etc/piwigo-sync/piwigo.conf';
$legacy_present = is_readable($legacy_config);
$active_count = 0;
foreach ($connections as $connection)
{
@@ -166,9 +474,11 @@ function bratonien_tools_nc_connector_status()
}
return array(
'phase'=>'webdav',
'connections'=>$connections,
'connection_count'=>count($connections),
'active_count'=>$active_count,
'phase' => 'native-runtime',
'legacy_config_path' => $legacy_config,
'legacy_present' => $legacy_present,
'connections' => $connections,
'connection_count' => count($connections),
'active_count' => $active_count,
);
}

View File

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

View File

@@ -4,52 +4,196 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_scoped_secret(array $connection)
function bratonien_tools_nc_wizard_finish_connection_scoped()
{
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
if ((string)($connection['adapter'] ?? '') !== 'remote' || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder')
$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)
{
throw new RuntimeException('Die Verbindung ist keine WebDAV-Verbindung.');
$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)
{
$plain = bratonien_tools_nc_connector_decrypt_secret($connection['secret_blob'] ?? '');
$decoded = json_decode($plain, true);
if (!is_array($decoded))
if (is_array($decoded) && array_key_exists('db_password', $decoded))
{
throw new RuntimeException('WebDAV-Zugangsdaten haben ein unbekanntes Format.');
return array(
'v'=>3,
'db_password'=>(string)($decoded['db_password'] ?? ''),
'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
'api_key_id'=>(string)($decoded['api_key_id'] ?? ''),
'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''),
'nextcloud_user'=>(string)($decoded['nextcloud_user'] ?? ''),
'nextcloud_password'=>(string)($decoded['nextcloud_password'] ?? ''),
);
}
return array(
'v'=>3,
'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'] ?? ''),
'db_password'=>(string)$plain,
'piwigo_user'=>'',
'piwigo_password'=>'',
'api_key_id'=>'',
'api_key_secret'=>'',
'nextcloud_user'=>'',
'nextcloud_password'=>'',
);
}
function bratonien_tools_nc_connector_store_scoped_secret($id, array $connection, array $credentials)
{
$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.');
}
if (trim((string)($credentials['nextcloud_user'] ?? '')) === '' || (string)($credentials['nextcloud_password'] ?? '') === '')
$is_webdav = (string)($config['source_mode'] ?? '') === 'webdav-placeholder';
if (!$is_webdav && trim((string)($credentials['db_password'] ?? '')) === '') throw new RuntimeException('Das Datenbankpasswort der Verbindung fehlt.');
if ($is_webdav && (trim((string)($credentials['nextcloud_user'] ?? '')) === '' || (string)($credentials['nextcloud_password'] ?? '') === ''))
{
throw new RuntimeException('Die Nextcloud-Zugangsdaten der WebDAV-Verbindung fehlen.');
}
$credentials['v'] = 3;
unset($credentials['db_password']);
$payload = json_encode($credentials, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($payload)) throw new RuntimeException('Verbindungszugangsdaten konnten nicht serialisiert werden.');
$blob = bratonien_tools_nc_connector_encrypt_secret($payload);
$config['piwigo_auth'] = 'connection-scoped';
$config['api_enabled'] = trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '';
if (array_key_exists('api_enabled', $config))
{
$config['api_enabled'] = trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '';
}
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
@@ -67,13 +211,11 @@ function bratonien_tools_nc_connector_fallback_save_scoped()
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
bratonien_tools_nc_connector_validate_fallback_credentials($username, $password);
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$credentials['piwigo_user'] = $username;
$credentials['piwigo_password'] = $password;
bratonien_tools_nc_connector_store_scoped_secret($id, $connection, $credentials);
return array('message'=>'Piwigo-Benutzername und Passwort wurden als Fallback dieser WebDAV-Verbindung gespeichert.');
return array('message'=>'Piwigo-Benutzername und Passwort wurden als Fallback dieser Verbindung gespeichert.');
}
function bratonien_tools_nc_connector_fallback_delete_scoped()
@@ -81,10 +223,9 @@ function bratonien_tools_nc_connector_fallback_delete_scoped()
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$credentials['piwigo_user'] = '';
$credentials['piwigo_password'] = '';
bratonien_tools_nc_connector_store_scoped_secret($id, $connection, $credentials);
return array('message'=>'Fallback dieser WebDAV-Verbindung wurde gelöscht. Ein vorhandener API-Zugang blieb unverändert.');
return array('message'=>'Fallback dieser Verbindung wurde gelöscht. Ein vorhandener API-Zugang blieb unverändert.');
}

View File

@@ -0,0 +1,133 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_create_local_api_first()
{
throw new RuntimeException('Neue Verbindungen über die alte lokale PostgreSQL-/Storage-Struktur sind ab 0.9.6.2 gesperrt. Bestehende lokale Verbindungen bleiben für die schrittweise Migration weiterhin aktiv. Neue Verbindungen müssen über den WebDAV-Assistenten angelegt werden.');
}
function bratonien_tools_nc_connector_create_local_api_first_legacy()
{
$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

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

View File

@@ -0,0 +1,348 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_migration_state(array $connection)
{
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$missing = array();
if (trim((string)($config['nextcloud_url'] ?? '')) === '') $missing[] = 'Nextcloud-Adresse';
if (trim((string)($credentials['nextcloud_user'] ?? '')) === '') $missing[] = 'Nextcloud-Benutzer';
if ((string)($credentials['nextcloud_password'] ?? '') === '') $missing[] = 'Nextcloud-Passwort';
$api_id = trim((string)($credentials['api_key_id'] ?? ''));
$api_secret = trim((string)($credentials['api_key_secret'] ?? ''));
$fallback_user = trim((string)($credentials['piwigo_user'] ?? ''));
$fallback_password = (string)($credentials['piwigo_password'] ?? '');
$api_complete = $api_id !== '' && $api_secret !== '';
$fallback_complete = $fallback_user !== '' && $fallback_password !== '';
if (($api_id === '') !== ($api_secret === '')) $missing[] = 'vollstaendiger Piwigo-API-Zugang';
if (!$api_complete && !$fallback_complete) $missing[] = 'Piwigo-API oder Benutzer/Passwort-Fallback';
return array(
'ready'=>!$missing,
'missing'=>$missing,
'api_available'=>$api_complete,
'fallback_available'=>$fallback_complete,
);
}
function bratonien_tools_nc_connector_validate_nextcloud_access($base_url, $username, $password)
{
$base_url = bratonien_tools_nc_wizard_normalize_url($base_url);
$username = trim((string)$username);
$password = (string)$password;
if ($username === '' || $password === '') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden benoetigt.');
$status_response = bratonien_tools_nc_wizard_http($base_url.'/status.php');
if ($status_response['status'] < 200 || $status_response['status'] >= 300)
{
throw new RuntimeException('Die Nextcloud-Adresse konnte nicht bestaetigt werden.');
}
$status = json_decode((string)$status_response['body'], true);
if (!is_array($status) || empty($status['installed']))
{
throw new RuntimeException('Unter dieser Adresse wurde keine installierte Nextcloud erkannt.');
}
$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 der Benutzerzugang konnte nicht geprueft werden.');
}
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$resolved = trim((string)($user_data['id'] ?? $username));
return array(
'base_url'=>$base_url,
'username'=>$resolved !== '' ? $resolved : $username,
);
}
function bratonien_tools_nc_connector_validate_scoped_api($api_key_id, $api_key_secret)
{
$api_key_id = trim((string)$api_key_id);
$api_key_secret = trim((string)$api_key_secret);
if ($api_key_id === '' || $api_key_secret === '')
{
throw new RuntimeException('Piwigo-API-Schluessel-ID und API-Geheimnis werden benoetigt.');
}
$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 API-Benutzerstatus geliefert.');
$role = strtolower(trim((string)($status['status'] ?? '')));
if (!in_array($role, array('admin','webmaster'), true))
{
throw new RuntimeException('Der API-Key funktioniert, gehoert aber keinem Piwigo-Administrator/Webmaster.');
}
$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);
$required = array('bratonien.nc.syncProductive', 'bratonien.nc.syncOrphans');
$missing = array_values(array_diff($required, array_keys($method_map)));
if ($missing)
{
throw new RuntimeException('Der API-Key ist gueltig, aber benoetigte Bratonien-Sync-Methoden fehlen: '.implode(', ', $missing).'.');
}
}
function bratonien_tools_nc_connector_prepare_webdav_wizard_from_connection(array $connection, $mode)
{
$id = (int)$connection['id'];
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$base_url = trim((string)($config['nextcloud_url'] ?? ''));
$username = trim((string)($credentials['nextcloud_user'] ?? ''));
if ($username === '')
{
$username = trim((string)($config['nextcloud_access_user'] ?? $config['access_user'] ?? ''));
}
$password = (string)($credentials['nextcloud_password'] ?? '');
$roots = isset($config['roots']) && is_array($config['roots']) ? array_values($config['roots']) : array();
$selected = array();
$selected_ids = array();
foreach ($roots as $root)
{
$path = trim((string)($root['webdav_path'] ?? ''), '/');
$fileid = (int)($root['fileid'] ?? 0);
if ($fileid < 1) continue;
$selected[] = $path;
$selected_ids[$path] = $fileid;
}
$state = bratonien_tools_nc_wizard_state();
$state = array_merge($state, array(
'editing_connection_id'=>$id,
'editing_adapter'=>(string)$connection['adapter'],
'editing_mode'=>(string)$mode,
'connection_name'=>(string)$connection['name'],
'host_input'=>$base_url,
'base_url'=>$base_url,
'username'=>$username,
'_password'=>$password,
'_fallback_user'=>(string)($credentials['piwigo_user'] ?? ''),
'_fallback_password'=>(string)($credentials['piwigo_password'] ?? ''),
'_api_key_id'=>(string)($credentials['api_key_id'] ?? ''),
'_api_key_secret'=>(string)($credentials['api_key_secret'] ?? ''),
'api_status'=>trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '' ? 'ok' : 'pending',
'roots'=>$roots,
'directory_selected'=>$selected,
'directory_selected_fileids'=>$selected_ids,
'source_mode'=>'webdav-placeholder',
'transport'=>'webdav',
));
if ($base_url !== '' && $username !== '' && $password !== '')
{
$state['step'] = 2;
$state['scan_ok'] = true;
$state['technical_stage'] = 'mounts';
$state['technical_source'] = 'WebDAV';
$state['technical_error'] = '';
$state['technical_complete'] = false;
$state['directory_selection_ready'] = true;
$state['directory_path'] = '';
$state['directory_parent'] = '';
$state['directory_children'] = array();
$state['directory_current_fileid'] = 0;
try
{
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
}
catch (Throwable $e)
{
$state['step'] = 1;
$state['scan_ok'] = false;
$state['technical_error'] = $e->getMessage();
}
}
else
{
$state['step'] = 1;
$state['scan_ok'] = false;
}
bratonien_tools_nc_wizard_store($state);
}
function bratonien_tools_nc_connector_edit_start()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$is_webdav = (string)$connection['adapter'] === 'remote'
&& (string)($config['source_mode'] ?? '') === 'webdav-placeholder';
if (!$is_webdav)
{
throw new RuntimeException('Diese Legacy-Verbindung wird direkt bearbeitet. Eine WebDAV-Migration ist eine separate Aktion.');
}
bratonien_tools_nc_connector_prepare_webdav_wizard_from_connection($connection, 'update');
return array('message'=>'Verbindung #'.$id.' wurde zum Bearbeiten geoeffnet.');
}
function bratonien_tools_nc_connector_migrate_start()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
if ((string)$connection['adapter'] !== 'local')
{
throw new RuntimeException('Nur eine Legacy-Verbindung kann auf WebDAV migriert werden.');
}
$migration = bratonien_tools_nc_connector_migration_state($connection);
if (empty($migration['ready']))
{
throw new RuntimeException('Die WebDAV-Migration ist noch nicht bereit. Unter Bearbeiten fehlen: '.implode(', ', $migration['missing']).'.');
}
bratonien_tools_nc_connector_prepare_webdav_wizard_from_connection($connection, 'migrate');
return array('message'=>'Die WebDAV-Migration fuer Verbindung #'.$id.' wurde geoeffnet. Die bestehende Legacy-Verbindung bleibt bis zum erfolgreichen Umstieg unveraendert.');
}
function bratonien_tools_nc_connector_update_local_friendly()
{
$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 Bearbeitung ist nur fuer Legacy-Verbindungen vorgesehen.');
$name = trim((string)($_POST['connection_name'] ?? ''));
if ($name === '') throw new RuntimeException('Bitte einen Namen fuer die Verbindung angeben.');
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$host = trim((string)($_POST['nc_host'] ?? ''));
$port = (int)($_POST['nc_port'] ?? 5432);
$database = trim((string)($_POST['nc_database'] ?? ''));
$user = trim((string)($_POST['nc_user'] ?? ''));
$gallery_root = rtrim(trim((string)($_POST['nc_gallery_root'] ?? '')), '/');
$source_view = trim((string)($_POST['nc_source_view'] ?? ''));
$activity_view = trim((string)($_POST['nc_activity_view'] ?? ''));
if ($host === '') throw new RuntimeException('Datenbank-Server fehlt.');
if ($port < 1 || $port > 65535) throw new RuntimeException('Der Datenbank-Port ist ungueltig.');
if ($database === '') throw new RuntimeException('Datenbankname fehlt.');
if ($user === '') throw new RuntimeException('Reader-Benutzer fehlt.');
if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Piwigo-Galerieordner muss ein absoluter Pfad sein.');
if ($source_view === '' || $activity_view === '') throw new RuntimeException('Die gespeicherten Datenbankansichten duerfen nicht leer sein.');
bratonien_tools_nc_connector_view_name($source_view);
bratonien_tools_nc_connector_view_name($activity_view);
$storage_ids = isset($_POST['nc_storage_id']) && is_array($_POST['nc_storage_id']) ? $_POST['nc_storage_id'] : array();
$storage_prefixes = isset($_POST['nc_source_prefix']) && is_array($_POST['nc_source_prefix']) ? $_POST['nc_source_prefix'] : array();
$storage_mounts = isset($_POST['nc_local_mount']) && is_array($_POST['nc_local_mount']) ? $_POST['nc_local_mount'] : array();
$storages = array();
$count = max(count($storage_ids), count($storage_prefixes), count($storage_mounts));
for ($index = 0; $index < $count; $index++)
{
$storage_id = trim((string)($storage_ids[$index] ?? ''));
$prefix = trim((string)($storage_prefixes[$index] ?? ''), '/');
$mount = rtrim(trim((string)($storage_mounts[$index] ?? '')), '/');
if ($storage_id === '' && $prefix === '' && $mount === '') continue;
if ($storage_id === '') throw new RuntimeException('Bei einem Speicherort fehlt die Storage-ID.');
if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Bei einem Speicherort fehlt ein gueltiger lokaler Pfad.');
$storages[] = array('storage_id'=>$storage_id, 'source_prefix'=>$prefix, 'local_mount'=>$mount);
}
if (!$storages) throw new RuntimeException('Mindestens ein Speicherort muss vorhanden sein.');
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$new_db_password = (string)($_POST['nc_db_password'] ?? '');
if ($new_db_password !== '') $credentials['db_password'] = $new_db_password;
if (trim((string)($credentials['db_password'] ?? '')) === '') throw new RuntimeException('Fuer die Legacy-Verbindung ist kein Datenbankpasswort gespeichert.');
$nextcloud_url = trim((string)($_POST['nc_nextcloud_url'] ?? ($config['nextcloud_url'] ?? '')));
$nextcloud_user = trim((string)($_POST['nc_nextcloud_user'] ?? ($credentials['nextcloud_user'] ?? '')));
$new_nextcloud_password = (string)($_POST['nc_nextcloud_password'] ?? '');
if ($new_nextcloud_password !== '') $credentials['nextcloud_password'] = $new_nextcloud_password;
$nextcloud_password = (string)($credentials['nextcloud_password'] ?? '');
$api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? ($credentials['api_key_id'] ?? '')));
$new_api_secret = trim((string)($_POST['nc_connection_api_key_secret'] ?? ''));
$old_api_id = trim((string)($credentials['api_key_id'] ?? ''));
if ($api_key_id !== $old_api_id && $new_api_secret === '')
{
throw new RuntimeException('Wenn die API-Schluessel-ID geaendert wird, muss auch das API-Geheimnis neu eingegeben werden.');
}
if ($new_api_secret !== '') $credentials['api_key_secret'] = $new_api_secret;
$api_key_secret = trim((string)($credentials['api_key_secret'] ?? ''));
if (($nextcloud_url === '' || $nextcloud_user === '' || $nextcloud_password === '') && ($nextcloud_url !== '' || $nextcloud_user !== '' || $nextcloud_password !== ''))
{
throw new RuntimeException('Nextcloud-Adresse, Nextcloud-Benutzer und Nextcloud-Passwort muessen fuer WebDAV gemeinsam vollstaendig sein.');
}
if (($api_key_id === '') !== ($api_key_secret === ''))
{
throw new RuntimeException('Piwigo-API-Schluessel-ID und API-Geheimnis muessen gemeinsam vollstaendig sein.');
}
if ($nextcloud_url !== '')
{
$validated_nc = bratonien_tools_nc_connector_validate_nextcloud_access($nextcloud_url, $nextcloud_user, $nextcloud_password);
$nextcloud_url = $validated_nc['base_url'];
$nextcloud_user = $validated_nc['username'];
$credentials['nextcloud_user'] = $nextcloud_user;
}
if ($api_key_id !== '')
{
bratonien_tools_nc_connector_validate_scoped_api($api_key_id, $api_key_secret);
$credentials['api_key_id'] = $api_key_id;
}
$config['host'] = $host;
$config['port'] = (string)$port;
$config['database'] = $database;
$config['user'] = $user;
$config['source_view'] = $source_view;
$config['activity_view'] = $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'] = $storages;
if ($nextcloud_url !== '')
{
$config['nextcloud_url'] = $nextcloud_url;
$config['access_user'] = $nextcloud_user;
$config['nextcloud_access_user'] = $nextcloud_user;
}
$config['piwigo_auth'] = 'connection-scoped';
$config['api_enabled'] = $api_key_id !== '' && $api_key_secret !== '';
unset($config['verification']);
$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.');
$secret_payload = json_encode($credentials, 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();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
$updated = bratonien_tools_nc_connector_connection($id, true);
$migration = $updated ? bratonien_tools_nc_connector_migration_state($updated) : array('ready'=>false);
return array(
'message'=>'Verbindung #'.$id.' wurde gespeichert. '.(!empty($migration['ready']) ? 'Die WebDAV-Migration ist jetzt bereit.' : 'Die WebDAV-Migration ist noch nicht vollstaendig vorbereitet.'),
);
}

View File

@@ -0,0 +1,101 @@
<?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

@@ -0,0 +1,107 @@
<?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,7 +7,10 @@ if (!defined('PHPWG_ROOT_PATH'))
function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node)
{
$children = $node->children();
if (count($children) === 0) return (string)$node;
if (count($children) === 0)
{
return (string)$node;
}
$result = array();
foreach ($children as $name => $child)
@@ -15,10 +18,16 @@ function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node)
$value = bratonien_tools_nc_connector_xml_value($child);
if (array_key_exists($name, $result))
{
if (!is_array($result[$name]) || !array_is_list($result[$name])) $result[$name] = array($result[$name]);
if (!is_array($result[$name]) || !array_is_list($result[$name]))
{
$result[$name] = array($result[$name]);
}
$result[$name][] = $value;
}
else $result[$name] = $value;
else
{
$result[$name] = $value;
}
}
return $result;
}
@@ -26,9 +35,15 @@ function bratonien_tools_nc_connector_xml_value(SimpleXMLElement $node)
function bratonien_tools_nc_connector_decode_api_response($body, $content_type = '')
{
$decoded = json_decode((string)$body, true);
if (is_array($decoded)) return $decoded;
if (is_array($decoded))
{
return $decoded;
}
if (!function_exists('simplexml_load_string')) throw new RuntimeException('Piwigo-API lieferte XML, aber SimpleXML ist in PHP nicht verfügbar.');
if (!function_exists('simplexml_load_string'))
{
throw new RuntimeException('Piwigo-API lieferte XML, aber SimpleXML ist in PHP nicht verfuegbar.');
}
$previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$body);
@@ -37,7 +52,7 @@ function bratonien_tools_nc_connector_decode_api_response($body, $content_type =
if ($xml === false || $xml->getName() !== 'rsp')
{
$detail = $content_type !== '' ? ' Antworttyp: '.$content_type.'.' : '';
throw new RuntimeException('Piwigo-API lieferte weder gültiges JSON noch eine gültige XML-Webservice-Antwort.'.$detail);
throw new RuntimeException('Piwigo-API lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.'.$detail);
}
$response = array('stat'=>(string)$xml['stat']);
@@ -46,17 +61,27 @@ function bratonien_tools_nc_connector_decode_api_response($body, $content_type =
$value = bratonien_tools_nc_connector_xml_value($child);
if (array_key_exists($name, $response))
{
if (!is_array($response[$name]) || !array_is_list($response[$name])) $response[$name] = array($response[$name]);
if (!is_array($response[$name]) || !array_is_list($response[$name]))
{
$response[$name] = array($response[$name]);
}
$response[$name][] = $value;
}
else $response[$name] = $value;
else
{
$response[$name] = $value;
}
}
return $response;
}
function bratonien_tools_nc_connector_api_payload(array $decoded)
{
if (array_key_exists('result', $decoded)) return $decoded['result'];
if (array_key_exists('result', $decoded))
{
return $decoded['result'];
}
unset($decoded['stat']);
return $decoded;
}
@@ -66,42 +91,66 @@ function bratonien_tools_nc_connector_collect_method_names($value, array &$metho
if (is_string($value))
{
$value = trim($value);
if ($value !== '' && preg_match('/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/', $value)) $methods[$value] = true;
if ($value !== '' && preg_match('/^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/', $value))
{
$methods[$value] = true;
}
return;
}
if (!is_array($value))
{
return;
}
if (!is_array($value)) return;
if (isset($value['name']) && is_string($value['name']))
{
$name = trim($value['name']);
if ($name !== '') $methods[$name] = true;
if ($name !== '')
{
$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)
{
if (!function_exists('curl_init')) throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der API-Key-Test kann nicht ausgeführt werden.');
if (!function_exists('curl_init'))
{
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_secret = trim((string)$api_key_secret);
if ($api_key_id === '' || $api_key_secret === '') throw new RuntimeException('API-Schlüssel-ID und Geheimnis müssen angegeben werden.');
if ($api_key_id === '' || $api_key_secret === '')
{
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';
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER=>true,
CURLOPT_POST=>true,
CURLOPT_POSTFIELDS=>http_build_query(array('method'=>(string)$method,'format'=>'json')),
CURLOPT_HTTPHEADER=>array(
'X-PIWIGO-API: '.$api_key_id.':'.$api_key_secret,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(array(
'method' => (string)$method,
'format' => 'json',
)),
CURLOPT_HTTPHEADER => array(
'X-PIWIGO-API: '.$api_key,
'Accept: application/json, text/xml;q=0.9',
'Content-Type: application/x-www-form-urlencoded',
),
CURLOPT_CONNECTTIMEOUT=>10,
CURLOPT_TIMEOUT=>20,
CURLOPT_FOLLOWLOCATION=>false,
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-WebDAV/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 20,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-NC-Connector/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
));
$body = curl_exec($ch);
@@ -111,8 +160,14 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s
$content_type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
if ($body === false || $errno !== 0) throw new RuntimeException('Piwigo-API konnte nicht erreicht werden: '.$error);
if ($http_code < 200 || $http_code >= 300) throw new RuntimeException('Piwigo-API antwortete mit HTTP '.$http_code.'.');
if ($body === false || $errno !== 0)
{
throw new RuntimeException('Piwigo-API konnte nicht erreicht werden: '.$error);
}
if ($http_code < 200 || $http_code >= 300)
{
throw new RuntimeException('Piwigo-API antwortete mit HTTP '.$http_code.'.');
}
$decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $content_type);
if (($decoded['stat'] ?? '') !== 'ok')
@@ -120,19 +175,29 @@ function bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_s
$message = (string)($decoded['message'] ?? $decoded['err'] ?? 'API-Aufruf wurde abgelehnt.');
throw new RuntimeException($message);
}
return bratonien_tools_nc_connector_api_payload($decoded);
}
function bratonien_tools_nc_connector_validate_fallback_credentials($username, $password)
{
if (!function_exists('curl_init')) throw new RuntimeException('cURL ist in PHP nicht verfügbar. Der Piwigo-Fallback kann nicht geprüft werden.');
if (!function_exists('curl_init'))
{
throw new RuntimeException('cURL ist in PHP nicht verfuegbar. Der Piwigo-Fallback kann nicht geprueft werden.');
}
$username = trim((string)$username);
$password = (string)$password;
if ($username === '' || $password === '') throw new RuntimeException('Piwigo-Benutzername und Passwort müssen angegeben werden.');
if ($username === '' || $password === '')
{
throw new RuntimeException('Piwigo-Benutzername und Passwort muessen angegeben werden.');
}
$cookie_file = tempnam(sys_get_temp_dir(), 'br-pwg-auth-');
if ($cookie_file === false) throw new RuntimeException('Temporäre Piwigo-Sitzung konnte nicht angelegt werden.');
if ($cookie_file === false)
{
throw new RuntimeException('Temporare Piwigo-Sitzung konnte nicht angelegt werden.');
}
try
{
@@ -149,7 +214,7 @@ function bratonien_tools_nc_connector_validate_fallback_credentials($username, $
CURLOPT_CONNECTTIMEOUT=>10,
CURLOPT_TIMEOUT=>20,
CURLOPT_FOLLOWLOCATION=>false,
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-WebDAV/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Connector/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
));
$body = curl_exec($ch);
$errno = curl_errno($ch);
@@ -157,8 +222,8 @@ function bratonien_tools_nc_connector_validate_fallback_credentials($username, $
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
if ($body === false || $errno !== 0) throw new RuntimeException('Piwigo-Fallback konnte nicht geprüft werden: '.$error);
if ($http < 200 || $http >= 300) throw new RuntimeException('Piwigo-Fallback-Prüfung antwortete mit HTTP '.$http.'.');
if ($body === false || $errno !== 0) throw new RuntimeException('Piwigo-Fallback konnte nicht geprueft werden: '.$error);
if ($http < 200 || $http >= 300) throw new RuntimeException('Piwigo-Fallback-Pruefung antwortete mit HTTP '.$http.'.');
$decoded = bratonien_tools_nc_connector_decode_api_response((string)$body, $type);
if (($decoded['stat'] ?? '') !== 'ok')
{
@@ -168,15 +233,83 @@ function bratonien_tools_nc_connector_validate_fallback_credentials($username, $
return bratonien_tools_nc_connector_api_payload($decoded);
};
$request(array('method'=>'pwg.session.login','username'=>$username,'password'=>$password));
$request(array('method'=>'pwg.session.login', 'username'=>$username, 'password'=>$password));
$status = $request(array('method'=>'pwg.session.getStatus'));
if (!is_array($status)) throw new RuntimeException('Piwigo hat keinen auswertbaren Benutzerstatus geliefert.');
$role = strtolower(trim((string)($status['status'] ?? '')));
if (!in_array($role, array('admin','webmaster'), true)) throw new RuntimeException('Der Piwigo-Fallback funktioniert, gehört aber keinem Administrator/Webmaster.');
return array('username'=>(string)($status['username'] ?? $username),'status'=>$role);
if (!in_array($role, array('admin','webmaster'), true))
{
throw new RuntimeException('Der Piwigo-Fallback funktioniert, gehoert aber keinem Administrator/Webmaster.');
}
return array('username'=>(string)($status['username'] ?? $username), 'status'=>$role);
}
finally
{
@unlink($cookie_file);
}
}
function bratonien_tools_nc_connector_piwigo_api_test()
{
$api_key_id = trim((string)($_POST['nc_piwigo_api_key_id'] ?? ''));
$api_key_secret = trim((string)($_POST['nc_piwigo_api_key_secret'] ?? ''));
if ($api_key_id === '')
{
throw new RuntimeException('Piwigo-API-Schluessel-ID fehlt.');
}
if ($api_key_secret === '')
{
throw new RuntimeException('Piwigo-API-Geheimnis fehlt.');
}
$status = bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, 'pwg.session.getStatus');
if (!is_array($status))
{
throw new RuntimeException('Piwigo hat keinen auswertbaren Benutzerstatus geliefert.');
}
$username = (string)($status['username'] ?? $status['user'] ?? '');
$user_status = strtolower((string)($status['status'] ?? ''));
$is_admin = in_array($user_status, array('admin', 'webmaster'), true);
if (!$is_admin)
{
throw new RuntimeException('Der API-Key funktioniert, gehoert aber keinem Piwigo-Administrator/Webmaster. Er wurde nicht gespeichert.');
}
$method_result = bratonien_tools_nc_connector_piwigo_api_request($api_key_id, $api_key_secret, 'reflection.getMethodList');
$method_map = array();
bratonien_tools_nc_connector_collect_method_names($method_result, $method_map);
$methods = array_keys($method_map);
sort($methods, SORT_STRING);
$required_methods = array('bratonien.nc.syncProductive', 'bratonien.nc.syncOrphans');
$missing = array_values(array_diff($required_methods, $methods));
if ($missing)
{
throw new RuntimeException('Der API-Key ist gueltig, aber die benoetigten Bratonien-Sync-Methoden fehlen: '.implode(', ', $missing).'.');
}
bratonien_tools_nc_api_credentials_store($api_key_id, $api_key_secret);
$sync_candidates = array_values(array_filter($methods, function($method)
{
return preg_match('/sync|synchron|site/i', (string)$method) === 1;
}));
$result = array(
'ok' => true,
'username' => $username !== '' ? $username : 'nicht gemeldet',
'status' => $user_status !== '' ? $user_status : 'nicht gemeldet',
'admin' => true,
'method_count' => count($methods),
'sync_candidates' => $sync_candidates,
'sync_api_detected' => true,
'stored' => true,
'conclusion' => 'Der API-Key ist als bevorzugter Connector-Zugang gespeichert. Produktive API-Synchronisierung bleibt auf die im Plugin freigegebene Piwigo-Version begrenzt; bei einer nicht freigegebenen Version greift nur der konfigurierte Benutzername/Passwort-Fallback.',
);
return array(
'message' => 'Piwigo-API-Key wurde geprueft und verschluesselt als bevorzugter Connector-Zugang gespeichert.',
'nc_piwigo_api_test' => $result,
);
}

View File

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

View File

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

View File

@@ -4,13 +4,17 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!');
}
/**
* Create or update one independent WebDAV connector connection.
* No migration pair, successor or implicit legacy fallback is created.
*/
function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
{
$state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok']) || empty($state['technical_complete']))
{
throw new RuntimeException('Die WebDAV-Verbindung wurde im Assistenten noch nicht vollständig vorbereitet.');
throw new RuntimeException('Die WebDAV-Verbindung wurde im Assistenten noch nicht vollstaendig vorbereitet.');
}
$base_url = rtrim(trim((string)($state['base_url'] ?? '')), '/');
@@ -18,7 +22,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$password = (string)($state['_password'] ?? '');
if ($base_url === '' || $username === '' || $password === '')
{
throw new RuntimeException('Für die WebDAV-Verbindung fehlen Nextcloud-Adresse oder Zugangsdaten.');
throw new RuntimeException('Fuer den WebDAV-Zugang fehlen Nextcloud-Adresse oder Zugangsdaten.');
}
$selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
@@ -27,17 +31,26 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$selected_ids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids'])
? $state['directory_selected_fileids']
: array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
if (!$selected)
{
$selected = array('');
}
$roots = array();
foreach ($selected as $path)
{
$fileid = isset($selected_ids[$path]) ? (int)$selected_ids[$path] : 0;
if ($fileid < 1) throw new RuntimeException('Für ein ausgewähltes Nextcloud-Verzeichnis fehlt die eindeutige Datei-ID.');
if ($fileid < 1)
{
throw new RuntimeException('Fuer ein ausgewaehltes Nextcloud-Verzeichnis fehlt die eindeutige Datei-ID.');
}
$display_name = $path === ''
? (trim((string)($state['display_name'] ?? '')) !== '' ? trim((string)$state['display_name']) : $username)
: basename($path);
$roots[] = array(
'fileid'=>$fileid,
'display_name'=>basename($path),
'display_name'=>$display_name,
'webdav_path'=>$path,
);
}
@@ -53,7 +66,21 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$fallback_password = (string)($state['_fallback_password'] ?? '');
$api_enabled = $api_key_id !== '' && $api_key_secret !== '';
$editing_id = (int)($state['editing_connection_id'] ?? 0);
$editing_mode = (string)($state['editing_mode'] ?? '');
$editing_connection = $editing_id > 0 ? bratonien_tools_nc_connector_connection($editing_id, false) : null;
$editing_remote = $editing_connection
&& (string)$editing_connection['adapter'] === 'remote'
&& (string)($editing_connection['config']['source_mode'] ?? '') === 'webdav-placeholder'
&& $editing_mode === 'update';
if ($editing_mode === 'migrate')
{
throw new RuntimeException('Die Migrationsfunktion wurde entfernt. Bitte eine normale WebDAV-Verbindung anlegen.');
}
$config = array(
'origin'=>'native',
'source_mode'=>'webdav-placeholder',
'transport'=>'webdav',
'nextcloud_url'=>$base_url,
@@ -63,10 +90,26 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
'roots'=>$roots,
'state_dir'=>'',
'status_file'=>'',
'quiet_seconds'=>120,
'max_wait_seconds'=>900,
'full_sync_seconds'=>86400,
'piwigo_auth'=>'connection-scoped',
'api_enabled'=>$api_enabled,
);
if ($editing_remote)
{
$old_config = is_array($editing_connection['config'] ?? null) ? $editing_connection['config'] : array();
foreach (array('state_dir','status_file','parallel_gallery_root','source_fingerprint','runtime') as $preserve)
{
if (array_key_exists($preserve, $old_config)) $config[$preserve] = $old_config[$preserve];
}
foreach (array('quiet_seconds','max_wait_seconds','full_sync_seconds') as $preserve)
{
if (isset($old_config[$preserve])) $config[$preserve] = (int)$old_config[$preserve];
}
}
foreach (array('product'=>'nextcloud_product', 'version'=>'nextcloud_version') as $state_key=>$config_key)
{
$value = trim((string)($state[$state_key] ?? ''));
@@ -75,6 +118,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$secret_payload = json_encode(array(
'v'=>3,
'db_password'=>'',
'piwigo_user'=>$fallback_user,
'piwigo_password'=>$fallback_password,
'api_key_id'=>$api_key_id,
@@ -84,18 +128,34 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($secret_payload)) throw new RuntimeException('WebDAV-Zugangsdaten konnten nicht serialisiert werden.');
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nicht serialisiert werden.');
bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
$connection_key = 'webdav-'.bin2hex(random_bytes(12));
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
pwg_query("INSERT INTO `$table` (connection_key,name,adapter,enabled,config_json,secret_blob,created,updated) VALUES ('"
if ($editing_remote)
{
$config['state_dir'] = trim((string)($config['state_dir'] ?? '')) !== '' ? $config['state_dir'] : '/var/lib/bratonien-tools/nc-connector/connection-'.$editing_id;
$config['status_file'] = trim((string)($config['status_file'] ?? '')) !== '' ? $config['status_file'] : $config['state_dir'].'/connector-status.json';
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nicht serialisiert werden.');
pwg_query("UPDATE `$table` SET name='".pwg_db_real_escape_string($name)."', config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$editing_id." AND adapter='remote' LIMIT 1");
unset($_SESSION['bratonien_nc_wizard']);
return array(
'connection_id'=>$editing_id,
'message'=>'WebDAV-Verbindung wurde gespeichert.',
);
}
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nicht serialisiert werden.');
$connection_key = 'webdav-'.bin2hex(random_bytes(12));
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($name)."','remote',1,'"
.pwg_db_real_escape_string($name)."','remote',0,'disabled','"
.pwg_db_real_escape_string($config_json)."','"
.pwg_db_real_escape_string($secret_blob)."','"
.pwg_db_real_escape_string($now)."','"
@@ -107,12 +167,10 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$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('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");
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nach dem Anlegen nicht serialisiert werden.');
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id." AND adapter='remote' LIMIT 1");
unset($_SESSION['bratonien_nc_wizard']);
return array(
'connection_id'=>$id,

View File

@@ -17,28 +17,30 @@ function bratonien_tools_nc_wizard_state()
'display_name'=>'',
'version'=>'',
'product'=>'Nextcloud',
'users'=>array(),
'can_list_users'=>false,
'showcase_user'=>'',
'connection_name'=>'',
'scan_message'=>'',
'source_mode'=>'webdav-placeholder',
'transport'=>'webdav',
'gallery_root'=>'',
'roots'=>array(),
'technical_stage'=>'mounts',
'technical_source'=>'WebDAV',
'technical_stage'=>'auto_check',
'technical_source'=>'Noch nicht geprüft',
'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(),
'db_host'=>'',
'db_port'=>'5432',
'db_database'=>'nextcloud',
'db_user'=>'',
'db_password_set'=>false,
'source_view'=>'piwigo_showcase_sources',
'activity_view'=>'piwigo_showcase_activity',
'gallery_root'=>'',
'storages'=>array(),
'storage_candidates'=>array(),
'api_status'=>'pending',
'api_username'=>'',
'api_error'=>'',
'_password'=>'',
'_db_password'=>'',
'_api_key_id'=>'',
'_api_key_secret'=>'',
'_fallback_user'=>'',
@@ -100,7 +102,7 @@ function bratonien_tools_nc_wizard_http($url, $username = '', $password = '', ar
CURLOPT_CONNECTTIMEOUT=>8,
CURLOPT_TIMEOUT=>15,
CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'), $headers),
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-WebDAV/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Wizard/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
);
if ($username !== '')
@@ -139,6 +141,352 @@ function bratonien_tools_nc_wizard_ocs_data($body)
return isset($decoded['ocs']['data']) && is_array($decoded['ocs']['data']) ? $decoded['ocs']['data'] : array();
}
function bratonien_tools_nc_wizard_fill_profile(array &$state)
{
$url_host = strtolower((string)parse_url($state['base_url'], PHP_URL_HOST));
$state['db_host'] = $url_host;
$state['db_port'] = '5432';
$state['db_database'] = 'nextcloud';
$state['db_user'] = (string)$state['username'];
$state['_db_password'] = (string)$state['_password'];
$state['db_password_set'] = $state['_db_password'] !== '';
$state['source_view'] = 'piwigo_showcase_sources';
$state['activity_view'] = 'piwigo_showcase_activity';
$state['gallery_root'] = rtrim(PHPWG_ROOT_PATH, '/').'/galleries/nextcloud';
$state['storages'] = array();
$state['storage_candidates'] = array();
$state['technical_stage'] = 'auto_check';
$state['technical_source'] = 'Automatische Prüfung';
$state['technical_error'] = '';
$state['technical_complete'] = false;
foreach (bratonien_tools_nc_connector_connections() as $candidate)
{
$config = isset($candidate['config']) && is_array($candidate['config']) ? $candidate['config'] : array();
$candidate_url = trim((string)($config['nextcloud_url'] ?? ''));
$match = false;
if ($candidate_url !== '')
{
try
{
$match = bratonien_tools_nc_wizard_normalize_url($candidate_url) === $state['base_url'];
}
catch (Throwable $ignored)
{
$match = false;
}
}
if (!$match) continue;
$state['db_host'] = trim((string)($config['host'] ?? $state['db_host']));
$state['db_port'] = trim((string)($config['port'] ?? $state['db_port']));
$state['db_database'] = trim((string)($config['database'] ?? $state['db_database']));
$state['source_view'] = trim((string)($config['source_view'] ?? $state['source_view']));
$state['activity_view'] = trim((string)($config['activity_view'] ?? $state['activity_view']));
$state['gallery_root'] = trim((string)($config['gallery_root'] ?? $state['gallery_root']));
$state['storages'] = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array();
$state['storage_candidates'] = $state['storages'];
$state['technical_source'] = 'Bekannte Verbindungswerte werden erneut geprüft';
break;
}
}
function bratonien_tools_nc_wizard_finish_data_access(array &$state)
{
$config = array(
'host'=>$state['db_host'],
'port'=>$state['db_port'],
'database'=>$state['db_database'],
'user'=>$state['db_user'],
);
$password = (string)$state['_db_password'];
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1');
$source_view = bratonien_tools_nc_connector_view_name($state['source_view']);
$activity_view = bratonien_tools_nc_connector_view_name($state['activity_view']);
bratonien_tools_nc_connector_psql($config, $password, 'SELECT COUNT(*) FROM '.$source_view);
bratonien_tools_nc_connector_psql($config, $password, 'SELECT 1 FROM '.$activity_view.' LIMIT 1');
$rows = bratonien_tools_nc_connector_psql(
$config,
$password,
"SELECT DISTINCT storage_id::text || E'\\t' || COALESCE(source_path, '') FROM ".$source_view." ORDER BY 1"
);
$candidates = array();
foreach (preg_split('/\r\n|\r|\n/', trim($rows)) as $line)
{
if ($line === '') continue;
$parts = explode("\t", $line, 2);
if (count($parts) !== 2) continue;
$storage_id = trim($parts[0]);
$source_path = trim($parts[1], '/');
if ($storage_id === '') continue;
$prefix = $source_path;
if (strpos($source_path, '/') !== false) $prefix = substr($source_path, 0, strpos($source_path, '/'));
if (!isset($candidates[$storage_id]))
{
$candidates[$storage_id] = array(
'storage_id'=>$storage_id,
'source_prefix'=>$prefix,
'local_mount'=>'',
);
}
}
if (!$candidates) throw new RuntimeException('Die Verbindung zur Datenquelle funktioniert, aber es wurden noch keine freigegebenen Speicherorte gefunden.');
$known = array();
foreach (bratonien_tools_nc_connector_connections() as $connection)
{
foreach (($connection['config']['storages'] ?? array()) as $storage)
{
$key = (string)($storage['storage_id'] ?? '').'|'.trim((string)($storage['source_prefix'] ?? ''), '/');
if ($key !== '|') $known[$key] = (string)($storage['local_mount'] ?? '');
}
}
foreach ($candidates as &$candidate)
{
$key = $candidate['storage_id'].'|'.trim($candidate['source_prefix'], '/');
if (!empty($known[$key]) && is_dir($known[$key]) && is_readable($known[$key])) $candidate['local_mount'] = $known[$key];
}
unset($candidate);
$state['storage_candidates'] = array_values($candidates);
$state['storages'] = array_values(array_filter($state['storage_candidates'], function($storage) {
return trim((string)($storage['local_mount'] ?? '')) !== '';
}));
$state['technical_error'] = '';
$state['technical_source'] = 'Datenzugriff erfolgreich geprüft';
$state['technical_complete'] = count($state['storages']) === count($state['storage_candidates']);
$state['technical_stage'] = $state['technical_complete'] ? 'ready' : 'mounts';
}
function bratonien_tools_nc_wizard_scan()
{
$state = bratonien_tools_nc_wizard_state();
$host_input = trim((string)($_POST['nc_wizard_host'] ?? $state['host_input']));
$username = trim((string)($_POST['nc_wizard_user'] ?? $state['username']));
$password = array_key_exists('nc_wizard_password', $_POST) ? (string)$_POST['nc_wizard_password'] : (string)$state['_password'];
$state['step'] = 1;
$state['host_input'] = $host_input;
$state['username'] = $username;
$state['_password'] = $password;
bratonien_tools_nc_wizard_store($state);
if ($username === '' || $password === '') throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
$base_url = '';
$status_data = null;
foreach (bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
{
try
{
$response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
if ($response['status'] < 200 || $response['status'] >= 300) continue;
$candidate_status = json_decode($response['body'], true);
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue;
$base_url = $candidate_url;
$status_data = $candidate_status;
break;
}
catch (Throwable $ignored)
{
}
}
if ($base_url === '' || !is_array($status_data)) throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
$user_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/user?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($user_response['status'] === 401 || $user_response['status'] === 403) throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
if ($user_response['status'] < 200 || $user_response['status'] >= 300) throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
$users = array();
$can_list_users = false;
try
{
$users_response = bratonien_tools_nc_wizard_http(
$base_url.'/ocs/v2.php/cloud/users?format=json',
$username,
$password,
array('OCS-APIRequest: true')
);
if ($users_response['status'] >= 200 && $users_response['status'] < 300)
{
$users_data = bratonien_tools_nc_wizard_ocs_data($users_response['body']);
if (isset($users_data['users']) && is_array($users_data['users']))
{
$users = array_values(array_filter(array_map('strval', $users_data['users'])));
sort($users, SORT_NATURAL | SORT_FLAG_CASE);
$can_list_users = true;
}
}
}
catch (Throwable $ignored)
{
}
$resolved_username = trim((string)($user_data['id'] ?? $username));
if ($resolved_username === '') $resolved_username = $username;
$url_host = (string)parse_url($base_url, PHP_URL_HOST);
$state = array_merge($state, array(
'step'=>2,
'scan_ok'=>true,
'base_url'=>$base_url,
'host_input'=>$host_input,
'username'=>$resolved_username,
'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''),
'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''),
'product'=>(string)($status_data['productname'] ?? 'Nextcloud'),
'users'=>$users,
'can_list_users'=>$can_list_users,
'showcase_user'=>'',
'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud',
'scan_message'=>'Nextcloud wurde erkannt und der Benutzerzugriff wurde bestätigt.',
'_password'=>$password,
'api_status'=>'pending',
'api_username'=>'',
'api_error'=>'',
));
bratonien_tools_nc_wizard_fill_profile($state);
try
{
bratonien_tools_nc_wizard_finish_data_access($state);
}
catch (Throwable $e)
{
$state['technical_complete'] = false;
$state['technical_stage'] = 'database_details';
$state['technical_error'] = $e->getMessage();
}
bratonien_tools_nc_wizard_store($state);
if ($state['technical_stage'] === 'database_details')
{
return array('message'=>'Nextcloud wurde gefunden. Die Datenbank-Adresse muss noch bestätigt werden.');
}
if ($state['technical_stage'] === 'mounts')
{
return array('message'=>'Nextcloud und Datenzugriff wurden bestätigt. Ein Speicherort muss noch zugeordnet werden.');
}
return array('message'=>'Nextcloud und Datenzugriff wurden erfolgreich bestätigt.');
}
function bratonien_tools_nc_wizard_save_technical()
{
$state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
if ((string)$state['username'] === '' || (string)$state['_password'] === '') throw new RuntimeException('Die Zugangsdaten aus dem ersten Schritt sind nicht mehr verfügbar. Bitte den Assistenten neu starten.');
$state['db_user'] = (string)$state['username'];
$state['_db_password'] = (string)$state['_password'];
$state['db_password_set'] = true;
$state['db_host'] = trim((string)($_POST['nc_wizard_db_host'] ?? $state['db_host']));
$state['db_port'] = (string)max(1, min(65535, (int)($_POST['nc_wizard_db_port'] ?? $state['db_port'])));
$state['db_database'] = trim((string)($_POST['nc_wizard_db_database'] ?? $state['db_database']));
$state['technical_stage'] = 'database_details';
bratonien_tools_nc_wizard_store($state);
if ($state['db_host'] === '' || $state['db_database'] === '') throw new RuntimeException('Die Adresse der Datenbank ist noch unvollständig.');
try
{
bratonien_tools_nc_wizard_finish_data_access($state);
bratonien_tools_nc_wizard_store($state);
return array(
'message'=>$state['technical_complete']
? 'Datenzugriff wurde erfolgreich geprüft.'
: 'Datenzugriff funktioniert. Ein Speicherort muss noch bestätigt werden.'
);
}
catch (Throwable $e)
{
$state['technical_error'] = $e->getMessage();
$state['technical_stage'] = 'database_details';
bratonien_tools_nc_wizard_store($state);
throw $e;
}
}
function bratonien_tools_nc_wizard_save_mounts()
{
$state = bratonien_tools_nc_wizard_state();
$candidates = is_array($state['storage_candidates']) ? $state['storage_candidates'] : array();
$mounts = isset($_POST['nc_wizard_storage_mount']) && is_array($_POST['nc_wizard_storage_mount']) ? $_POST['nc_wizard_storage_mount'] : array();
if (!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte erkannt.');
$storages = array();
foreach ($candidates as $index=>$candidate)
{
$mount = trim((string)($candidate['local_mount'] ?? ''));
if ($mount === '') $mount = trim((string)($mounts[$index] ?? ''));
if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.');
$mount = rtrim($mount, '/');
if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount);
$candidate['local_mount'] = $mount;
$storages[] = $candidate;
}
$state['storages'] = $storages;
$state['storage_candidates'] = $storages;
$state['technical_complete'] = true;
$state['technical_stage'] = 'ready';
$state['technical_error'] = '';
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Speicherzuordnung wurde erfolgreich geprüft.');
}
function bratonien_tools_nc_wizard_select_user()
{
$state = bratonien_tools_nc_wizard_state();
if (empty($state['scan_ok'])) throw new RuntimeException('Bitte zuerst Nextcloud erfolgreich scannen.');
if (empty($state['technical_complete'])) throw new RuntimeException('Die Verbindung ist noch nicht vollständig geprüft.');
$showcase_user = trim((string)($_POST['nc_wizard_showcase_user'] ?? ''));
$connection_name = trim((string)($_POST['nc_wizard_connection_name'] ?? $state['connection_name']));
if ($showcase_user === '') throw new RuntimeException('Bitte den Benutzer auswählen, der die Showcase-Freigaben bereitstellt.');
if ($connection_name === '') throw new RuntimeException('Bitte einen Namen für die Verbindung angeben.');
$state['connection_name'] = $connection_name;
$state['showcase_user'] = $showcase_user;
$state['step'] = 3;
$state['api_error'] = '';
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Showcase-Benutzer übernommen. Jetzt folgt der Piwigo-API-Zugang.');
}
function bratonien_tools_nc_wizard_api_test()
{
$state = bratonien_tools_nc_wizard_state();
@@ -152,6 +500,14 @@ function bratonien_tools_nc_wizard_api_test()
$key_id = trim((string)$state['_api_key_id']);
$secret = trim((string)$state['_api_key_secret']);
if ($key_id === '' && $secret === '')
{
$stored = bratonien_tools_nc_api_credentials();
$key_id = trim((string)($stored['key_id'] ?? ''));
$secret = trim((string)($stored['key_secret'] ?? ''));
}
if ($key_id === '' || $secret === '') throw new RuntimeException('API-Schlüssel-ID und API-Geheimnis fehlen.');
try
@@ -198,6 +554,72 @@ function bratonien_tools_nc_wizard_api_skip()
return array('message'=>'Piwigo-API wurde übersprungen. Für diese Verbindung ist deshalb ein Fallback-Zugang erforderlich.');
}
function bratonien_tools_nc_wizard_finish()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 4 || empty($state['technical_complete']) || trim((string)$state['showcase_user']) === '') throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']);
if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password'];
bratonien_tools_nc_wizard_store($state);
$fallback_user = trim((string)$state['_fallback_user']);
$fallback_password = (string)$state['_fallback_password'];
if (($fallback_user === '') !== ($fallback_password === '')) throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.');
if ($state['api_status'] !== 'ok' && $fallback_user === '') throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist ein Fallback-Zugang erforderlich.');
$storage_lines = array();
foreach ($state['storages'] as $storage)
{
$storage_lines[] = (string)$storage['storage_id'].' | '.trim((string)$storage['source_prefix'], '/').' | '.(string)$storage['local_mount'];
}
$_POST['nc_name'] = (string)$state['connection_name'];
$_POST['nc_host'] = (string)$state['db_host'];
$_POST['nc_port'] = (string)$state['db_port'];
$_POST['nc_database'] = (string)$state['db_database'];
$_POST['nc_user'] = (string)$state['db_user'];
$_POST['nc_db_password'] = (string)$state['_db_password'];
$_POST['nc_source_view'] = (string)$state['source_view'];
$_POST['nc_activity_view'] = (string)$state['activity_view'];
$_POST['nc_gallery_root'] = (string)$state['gallery_root'];
$_POST['nc_storages'] = implode("\n", $storage_lines);
$_POST['nc_quiet_seconds'] = '120';
$_POST['nc_max_wait_seconds'] = '900';
$_POST['nc_full_sync_seconds'] = '86400';
$_POST['nc_piwigo_user'] = $fallback_user;
$_POST['nc_piwigo_password'] = $fallback_password;
$_POST['nc_nextcloud_url'] = (string)$state['base_url'];
$_POST['nc_showcase_user'] = (string)$state['showcase_user'];
$_POST['nc_access_user'] = (string)$state['username'];
$_POST['nc_product'] = (string)$state['product'];
$_POST['nc_version'] = (string)$state['version'];
$_POST['nc_api_validated'] = $state['api_status'] === 'ok' ? '1' : '0';
$result = bratonien_tools_nc_connector_create_local_api_first();
$connection_id = (int)($result['connection_id'] ?? 0);
try
{
if ($state['api_status'] === 'ok') bratonien_tools_nc_api_credentials_store((string)$state['_api_key_id'], (string)$state['_api_key_secret']);
}
catch (Throwable $e)
{
if ($connection_id > 0)
{
$table = bratonien_tools_nc_connector_table();
pwg_query("DELETE FROM `$table` WHERE id=".$connection_id." LIMIT 1");
}
throw new RuntimeException('Die Verbindung wurde nicht übernommen, weil der geprüfte API-Zugang nicht gespeichert werden konnte: '.$e->getMessage());
}
unset($_SESSION['bratonien_nc_wizard']);
unset($result['connection_id']);
$result['message'] = 'Verbindung wurde vollständig durch den Assistenten angelegt. '.$result['message'];
return $result;
}
function bratonien_tools_nc_connector_update_name()
{
$id = (int)($_POST['connection_id'] ?? 0);
@@ -206,10 +628,6 @@ function bratonien_tools_nc_connector_update_name()
$connection = bratonien_tools_nc_connector_connection($id, false);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
if ((string)($connection['adapter'] ?? '') !== 'remote' || (string)($connection['config']['source_mode'] ?? '') !== 'webdav-placeholder')
{
throw new RuntimeException('Es können nur WebDAV-Verbindungen bearbeitet werden.');
}
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
@@ -217,3 +635,49 @@ function bratonien_tools_nc_connector_update_name()
return array('message'=>'Verbindungsname wurde aktualisiert.');
}
function bratonien_tools_nc_connector_update_technical()
{
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
if (!empty($connection['enabled']) || (string)$connection['takeover_state'] === 'active') throw new RuntimeException('Technische Einstellungen einer aktiven Verbindung können nicht geändert werden. Bitte zuerst deaktivieren.');
$config = $connection['config'];
$port = (int)($_POST['nc_port'] ?? ($config['port'] ?? 5432));
if ($port < 1 || $port > 65535) throw new RuntimeException('Ungültiger PostgreSQL-Port.');
$gallery_root = rtrim(trim((string)($_POST['nc_gallery_root'] ?? ($config['gallery_root'] ?? ''))), '/');
if ($gallery_root === '' || $gallery_root[0] !== '/') throw new RuntimeException('Der Galerie-Pfad muss ein absoluter Pfad sein.');
$config['host'] = trim((string)($_POST['nc_host'] ?? ($config['host'] ?? '')));
$config['port'] = (string)$port;
$config['database'] = trim((string)($_POST['nc_database'] ?? ($config['database'] ?? '')));
$config['user'] = trim((string)($_POST['nc_user'] ?? ($config['user'] ?? '')));
$config['source_view'] = trim((string)($_POST['nc_source_view'] ?? ($config['source_view'] ?? '')));
$config['activity_view'] = trim((string)($_POST['nc_activity_view'] ?? ($config['activity_view'] ?? '')));
$config['gallery_root'] = $gallery_root;
$config['quiet_seconds'] = max(0, (int)($_POST['nc_quiet_seconds'] ?? ($config['quiet_seconds'] ?? 120)));
$config['max_wait_seconds'] = max(60, (int)($_POST['nc_max_wait_seconds'] ?? ($config['max_wait_seconds'] ?? 900)));
$config['full_sync_seconds'] = max(300, (int)($_POST['nc_full_sync_seconds'] ?? ($config['full_sync_seconds'] ?? 86400)));
$config['storages'] = bratonien_tools_nc_connector_parse_storages($_POST['nc_storages'] ?? '');
unset($config['verification']);
bratonien_tools_nc_connector_view_name($config['source_view']);
bratonien_tools_nc_connector_view_name($config['activity_view']);
$credentials = bratonien_tools_nc_connector_credentials_from_blob($connection['secret_blob'] ?? '');
$db_password = (string)($_POST['nc_db_password'] ?? '');
if ($db_password === '') $db_password = $credentials['db_password'];
if ($db_password === '') throw new RuntimeException('Datenbankpasswort fehlt.');
$secret_blob = bratonien_tools_nc_connector_encrypt_credentials($db_password, $credentials['piwigo_user'], $credentials['piwigo_password']);
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) throw new RuntimeException('Connector-Konfiguration konnte nicht serialisiert werden.');
$table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s');
pwg_query("UPDATE `$table` SET takeover_state='disabled', enabled=0, config_json='".pwg_db_real_escape_string($config_json)."', secret_blob='".pwg_db_real_escape_string($secret_blob)."', updated='".pwg_db_real_escape_string($now)."' WHERE id=".$id." LIMIT 1");
return array('message'=>'Technische Verbindungseinstellungen wurden gespeichert. Die Verbindung muss erneut geprüft werden.');
}

View File

@@ -0,0 +1,448 @@
<?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

@@ -0,0 +1,10 @@
<?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,6 +112,256 @@ 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();
}
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()
{
$state = bratonien_tools_nc_wizard_state();
@@ -150,6 +400,42 @@ function bratonien_tools_nc_wizard_directory_remove()
return array('message'=>'Verzeichnis entfernt.');
}
function bratonien_tools_nc_wizard_save_mounts_server_side()
{
$state = bratonien_tools_nc_wizard_state();
if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts') throw new RuntimeException('Die Speicher-/Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
if (!empty($state['directory_selection_ready']))
{
bratonien_tools_nc_wizard_resolve_selected_roots($state);
bratonien_tools_nc_wizard_store($state);
return array('message'=>!empty($state['technical_complete']) ? 'Verzeichnisse und vorhandene Storage-Adapter wurden übernommen.' : 'Verzeichnisse wurden aufgelöst. Ein neuer Storage-Adapter muss noch zugeordnet werden.');
}
$candidates = isset($state['storage_candidates']) && is_array($state['storage_candidates']) ? $state['storage_candidates'] : array();
if (!$candidates) throw new RuntimeException('Es wurden noch keine Speicherorte aus der Verzeichnisauswahl aufgelöst.');
$mounts = isset($_POST['nc_wizard_storage_mount']) && is_array($_POST['nc_wizard_storage_mount']) ? $_POST['nc_wizard_storage_mount'] : array();
foreach ($candidates as $index=>&$candidate)
{
$mount = rtrim(trim((string)($candidate['local_mount'] ?? '')), '/');
if ($mount === '') $mount = rtrim(trim((string)($mounts[$index] ?? '')), '/');
if ($mount === '' || $mount[0] !== '/') throw new RuntimeException('Für einen Speicherort fehlt ein gültiger lokaler Pfad.');
if (!is_dir($mount) || !is_readable($mount)) throw new RuntimeException('Der angegebene Speicherort ist nicht vorhanden oder nicht lesbar: '.$mount);
$candidate['local_mount'] = $mount;
}
unset($candidate);
$state['storage_candidates'] = array_values($candidates);
$state['storages'] = $state['storage_candidates'];
$state['technical_complete'] = true;
$state['technical_stage'] = 'ready';
$state['technical_error'] = '';
$state['directory_selection_ready'] = false;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Storage-Adapter wurde geprüft. Die Verbindung ist technisch vollständig.');
}
function bratonien_tools_nc_wizard_select_current_user()
{
$state = bratonien_tools_nc_wizard_state();
@@ -158,6 +444,7 @@ function bratonien_tools_nc_wizard_select_current_user()
$connection_name = trim((string)($_POST['nc_wizard_connection_name'] ?? $state['connection_name']));
if ($connection_name === '') throw new RuntimeException('Bitte einen Namen für die Verbindung angeben.');
$state['connection_name'] = $connection_name;
$state['showcase_user'] = (string)$state['username'];
$state['step'] = 3;
$state['api_error'] = '';
bratonien_tools_nc_wizard_store($state);
@@ -188,10 +475,23 @@ function bratonien_tools_nc_wizard_back()
$state['directory_selection_ready'] = true;
bratonien_tools_nc_wizard_refresh_directory_state($state);
}
elseif ((string)($state['technical_stage'] ?? '') === 'mounts' && !empty($state['directory_selection_ready']))
{
if (!empty($state['database_prompted'])) $state['technical_stage'] = 'database_details'; else $state['step'] = 1;
$state['directory_selection_ready'] = false;
}
elseif ((string)($state['technical_stage'] ?? '') === 'mounts')
{
$state['directory_selection_ready'] = true;
bratonien_tools_nc_wizard_refresh_directory_state($state);
}
elseif ((string)($state['technical_stage'] ?? '') === 'database_details')
{
$state['step'] = 1;
}
else
{
$state['step'] = 1;
$state['directory_selection_ready'] = false;
}
bratonien_tools_nc_wizard_store($state);

View File

@@ -28,7 +28,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
{
try
{
$response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
$response = bratonien_tools_nc_transport_http($candidate_url.'/status.php');
if ($response['status'] < 200 || $response['status'] >= 300) continue;
$candidate_status = json_decode($response['body'], true);
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue;
@@ -44,7 +44,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
}
$user_response = bratonien_tools_nc_wizard_http(
$user_response = bratonien_tools_nc_transport_http(
$base_url.'/ocs/v2.php/cloud/user?format=json',
$username,
$password,
@@ -73,12 +73,17 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''),
'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''),
'product'=>(string)($status_data['productname'] ?? 'Nextcloud'),
'users'=>array(),
'can_list_users'=>false,
'showcase_user'=>$resolved_username,
'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud WebDAV',
'scan_message'=>'Nextcloud und WebDAV-Zugriff wurden bestätigt.',
'_password'=>$password,
'source_mode'=>'webdav-placeholder',
'transport'=>'webdav',
'gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries',
'storages'=>array(),
'storage_candidates'=>array(),
'roots'=>array(),
'technical_stage'=>'mounts',
'technical_source'=>'WebDAV',
@@ -92,12 +97,14 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
'directory_fileids'=>array(),
'directory_selected'=>array(),
'directory_selected_fileids'=>array(),
'database_prompted'=>false,
'mount_prompted'=>false,
'api_status'=>'pending',
'api_username'=>'',
'api_error'=>'',
));
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
bratonien_tools_nc_transport_refresh_directory_state($state, '');
bratonien_tools_nc_wizard_store($state);
return array('message'=>'Nextcloud und WebDAV wurden bestätigt. Jetzt können die Verzeichnisse des angemeldeten Benutzers ausgewählt werden.');
@@ -108,7 +115,7 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$state = bratonien_tools_nc_wizard_state();
if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder')
{
throw new RuntimeException('Der Assistent unterstützt ausschließlich WebDAV-Verbindungen.');
return bratonien_tools_nc_wizard_save_mounts_server_side();
}
if ((int)($state['step'] ?? 0) !== 2 || empty($state['directory_selection_ready']))
@@ -119,7 +126,6 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
? array_values(array_unique(array_map(function($path) { return trim((string)$path, '/'); }, $state['directory_selected'])))
: array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected)
{
throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
@@ -136,9 +142,12 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
{
throw new RuntimeException('Für eine Auswahl fehlt die eindeutige Nextcloud-Datei-ID. Bitte das Verzeichnis erneut auswählen.');
}
$display_name = $path === ''
? (trim((string)($state['display_name'] ?? '')) !== '' ? trim((string)$state['display_name']) : (string)$state['username'])
: basename($path);
$roots[] = array(
'fileid'=>$fileid,
'display_name'=>basename($path),
'display_name'=>$display_name,
'webdav_path'=>$path,
);
}
@@ -149,9 +158,35 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$state['technical_source'] = 'WebDAV-Verzeichnisse ausgewählt';
$state['technical_error'] = '';
$state['directory_selection_ready'] = false;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'WebDAV-Verzeichnisse wurden übernommen.');
if ((string)($state['editing_mode'] ?? '') === 'migrate')
{
$editing_id = (int)($state['editing_connection_id'] ?? 0);
$connection = $editing_id > 0 ? bratonien_tools_nc_connector_connection($editing_id, true) : null;
if (!$connection || (string)$connection['adapter'] !== 'local')
{
throw new RuntimeException('Die zu migrierende Legacy-Verbindung ist nicht mehr verfügbar.');
}
$migration = bratonien_tools_nc_connector_migration_state($connection);
if (empty($migration['ready']))
{
throw new RuntimeException('Die WebDAV-Migration kann noch nicht abgeschlossen werden. Es fehlen: '.implode(', ', $migration['missing']).'.');
}
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$state['_api_key_id'] = (string)($credentials['api_key_id'] ?? '');
$state['_api_key_secret'] = (string)($credentials['api_key_secret'] ?? '');
$state['api_status'] = !empty($migration['api_available']) ? 'ok' : 'skipped';
$state['api_error'] = '';
$state['step'] = 4;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'WebDAV-Quelle wurde übernommen. Mit „Migration starten“ wird jetzt der WebDAV-Nachfolger angelegt; die Legacy-Verbindung bleibt als Fallback erhalten.');
}
bratonien_tools_nc_wizard_store($state);
return array('message'=>'WebDAV-Verzeichnisse wurden übernommen. Die Verbindung ist für den nächsten Einrichtungsschritt vorbereitet.');
}
function bratonien_tools_nc_wizard_finish_dispatch()
@@ -159,7 +194,7 @@ function bratonien_tools_nc_wizard_finish_dispatch()
$state = bratonien_tools_nc_wizard_state();
if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder')
{
throw new RuntimeException('Der Assistent unterstützt ausschließlich WebDAV-Verbindungen.');
return bratonien_tools_nc_wizard_finish_generic_scope();
}
if ((int)($state['step'] ?? 0) !== 4 || empty($state['technical_complete']))

View File

@@ -0,0 +1,282 @@
<?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

@@ -18,7 +18,7 @@ function bratonien_tools_register_nc_productive_ws_methods($arr)
'info' => 'Piwigo storage site to synchronize. Default: 1.',
),
),
'Runs the approved direct Bratonien filesystem synchronization for the NC Connector.',
'Runs the approved Piwigo filesystem synchronization for the Bratonien NC Connector.',
null,
array(
'admin_only' => true,
@@ -27,17 +27,9 @@ function bratonien_tools_register_nc_productive_ws_methods($arr)
);
}
function bratonien_tools_nc_productive_error(&$errors, $path, $type)
{
$errors[] = array(
'path' => (string)$path,
'type' => (string)$type,
);
}
function bratonien_tools_ws_nc_sync_productive($params, &$service)
{
global $conf, $user;
global $conf, $user, $template, $page;
$piwigo_version = defined('PHPWG_VERSION') ? (string)PHPWG_VERSION : '';
if ($piwigo_version !== '16.4.0')
@@ -59,385 +51,92 @@ function bratonien_tools_ws_nc_sync_productive($params, &$service)
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.');
}
$site_reader = new LocalSiteReader($site_url);
if (!$site_reader->open())
{
return new PwgError(500, 'Piwigo could not open the configured local site.');
}
list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW()'));
$errors = array();
$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,
'metadata_updated' => 0,
);
$saved_get = $_GET;
$saved_post = $_POST;
$saved_request_method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
$saved_default_status = isset($conf['newcat_default_status']) ? $conf['newcat_default_status'] : null;
$saved_template_dirs = null;
$output = '';
$counts = array();
try
{
$query = 'SELECT id, id_uppercat, 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_rank = array('NULL'=>1);
$result = pwg_query('SELECT id FROM '.CATEGORIES_TABLE);
while ($row = pwg_db_fetch_assoc($result))
if (!defined('IN_ADMIN'))
{
$next_rank[$row['id']] = 1;
}
$result = pwg_query('SELECT id_uppercat, MAX(`rank`)+1 AS next_rank FROM '.CATEGORIES_TABLE.' GROUP BY id_uppercat');
while ($row = pwg_db_fetch_assoc($result))
{
$key = empty($row['id_uppercat']) ? 'NULL' : $row['id_uppercat'];
$next_rank[$key] = (int)$row['next_rank'];
define('IN_ADMIN', true);
}
$next_id = pwg_db_nextval('id', CATEGORIES_TABLE);
$category_inserts = array();
foreach (array_diff($fs_fulldirs, array_keys($db_fulldirs)) as $fulldir)
if (is_object($template) && isset($template->smarty) && method_exists($template, 'set_template_dir'))
{
$dir = basename($fulldir);
if (!preg_match($conf['sync_chars_regex'], $dir))
{
bratonien_tools_nc_productive_error($errors, $fulldir, 'PWG-UPDATE-1');
continue;
}
$insert = array(
'id' => $next_id++,
'dir' => $dir,
'name' => str_replace('_', ' ', $dir),
'site_id' => $site_id,
'commentable' => boolean_to_string($conf['newcat_default_commentable']),
'status' => 'private',
'visible' => boolean_to_string($conf['newcat_default_visible']),
);
$parent_path = dirname($fulldir);
if (isset($db_fulldirs[$parent_path]))
{
$parent = $db_fulldirs[$parent_path];
$insert['id_uppercat'] = $parent;
$insert['uppercats'] = $db_categories[$parent]['uppercats'].','.$insert['id'];
$insert['rank'] = $next_rank[$parent]++;
$insert['global_rank'] = $db_categories[$parent]['global_rank'].'.'.$insert['rank'];
if ((string)$db_categories[$parent]['visible'] === 'false')
{
$insert['visible'] = 'false';
}
}
else
{
$insert['uppercats'] = (string)$insert['id'];
$insert['rank'] = $next_rank['NULL']++;
$insert['global_rank'] = (string)$insert['rank'];
}
$category_inserts[] = $insert;
$db_categories[$insert['id']] = array(
'id' => $insert['id'],
'id_uppercat' => $insert['id_uppercat'] ?? null,
'uppercats' => $insert['uppercats'],
'global_rank' => $insert['global_rank'],
'status' => 'private',
'visible' => $insert['visible'],
);
$db_fulldirs[$fulldir] = $insert['id'];
$next_rank[$insert['id']] = 1;
$saved_template_dirs = $template->smarty->getTemplateDir();
$template->set_template_dir(PHPWG_ROOT_PATH.'admin/themes/default/template');
}
if ($category_inserts)
{
mass_inserts(
CATEGORIES_TABLE,
array('id','dir','name','site_id','id_uppercat','uppercats','commentable','visible','status','rank','global_rank'),
$category_inserts
);
$category_ids = array_map(function ($row) { return (int)$row['id']; }, $category_inserts);
pwg_activity('album', $category_ids, 'add', array('sync'=>true));
add_permission_on_category($category_ids, get_admins());
$counts['new_categories'] = count($category_ids);
}
$conf['newcat_default_status'] = 'private';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_GET = array(
'page' => 'site_update',
'site' => $site_id,
);
$_POST = array(
'sync' => 'files',
'display_info' => 1,
'privacy_level' => 0,
'sync_meta' => 1,
'simulate' => 0,
'subcats-included' => 1,
'bratonien_connector' => 1,
'submit' => 1,
);
$to_delete_categories = array();
foreach (array_diff(array_keys($db_fulldirs), $fs_fulldirs) as $fulldir)
{
$to_delete_categories[] = (int)$db_fulldirs[$fulldir];
unset($db_fulldirs[$fulldir]);
}
if ($to_delete_categories)
{
delete_categories($to_delete_categories);
$counts['del_categories'] = count($to_delete_categories);
}
$fs = $site_reader->get_elements($basedir);
$cat_ids = array_diff(array_keys($db_categories), $to_delete_categories);
$db_elements = array();
if ($cat_ids)
{
$query = 'SELECT id, path FROM '.IMAGES_TABLE.' WHERE storage_category_id IN ('.implode(',', array_map('intval', $cat_ids)).')';
$db_elements = simple_hash_from_query($query, 'id', 'path');
}
$next_element_id = pwg_db_nextval('id', IMAGES_TABLE);
$image_inserts = array();
$image_links = array();
$format_inserts = array();
$new_image_ids = array();
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_nc_productive_error($errors, $path, 'PWG-UPDATE-1');
continue;
}
$id = $next_element_id++;
$image_inserts[] = array(
'id' => $id,
'file' => $filename,
'name' => get_name_from_file($filename),
'date_available' => $dbnow,
'path' => $path,
'representative_ext' => $fs[$path]['representative_ext'],
'storage_category_id' => $db_fulldirs[$dirname],
'added_by' => (int)$user['id'],
);
$image_links[] = array('image_id'=>$id, 'category_id'=>$db_fulldirs[$dirname]);
$new_image_ids[] = $id;
if (!empty($conf['enable_formats']) && !empty($fs[$path]['formats']))
{
foreach ($fs[$path]['formats'] as $ext => $filesize)
{
$format_inserts[] = array('image_id'=>$id, 'ext'=>$ext, 'filesize'=>$filesize);
}
}
}
if ($image_inserts)
{
mass_inserts(IMAGES_TABLE, array_keys($image_inserts[0]), $image_inserts);
mass_inserts(IMAGE_CATEGORY_TABLE, array_keys($image_links[0]), $image_links);
pwg_activity('photo', $new_image_ids, 'add', array('sync'=>true));
$counts['new_elements'] = count($image_inserts);
}
if ($format_inserts)
{
mass_inserts(IMAGE_FORMAT_TABLE, array_keys($format_inserts[0]), $format_inserts);
$counts['new_formats'] += count($format_inserts);
}
if (!empty($conf['enable_formats']) && $db_elements)
{
$db_elements_flip = array_flip($db_elements);
$existing_ids = array();
foreach (array_intersect_key($fs, $db_elements_flip) as $path => $unused)
{
$existing_ids[] = (int)$db_elements_flip[$path];
}
if ($existing_ids)
{
$db_formats = array();
$result = pwg_query('SELECT * FROM '.IMAGE_FORMAT_TABLE.' WHERE image_id IN ('.implode(',', $existing_ids).')');
while ($row = pwg_db_fetch_assoc($result))
{
$db_formats[$row['image_id']][$row['ext']] = $row['format_id'];
}
$formats_to_delete = array();
$formats_to_insert = array();
foreach ($existing_ids as $image_id)
{
$path = $db_elements[$image_id];
$known = $db_formats[$image_id] ?? array();
$present = $fs[$path]['formats'] ?? array();
foreach (array_diff_key($known, $present) as $format_id)
{
$formats_to_delete[] = (int)$format_id;
}
foreach (array_diff_key($present, $known) as $ext => $filesize)
{
$formats_to_insert[] = array('image_id'=>$image_id, 'ext'=>$ext, 'filesize'=>$filesize);
}
}
if ($formats_to_delete)
{
pwg_query('DELETE FROM '.IMAGE_FORMAT_TABLE.' WHERE format_id IN ('.implode(',', $formats_to_delete).')');
$counts['del_formats'] = count($formats_to_delete);
}
if ($formats_to_insert)
{
mass_inserts(IMAGE_FORMAT_TABLE, array_keys($formats_to_insert[0]), $formats_to_insert);
$counts['new_formats'] += count($formats_to_insert);
}
}
}
$to_delete_elements = array();
foreach (array_diff($db_elements, array_keys($fs)) as $path)
{
$id = array_search($path, $db_elements, true);
if ($id !== false)
{
$to_delete_elements[] = (int)$id;
}
}
if ($to_delete_elements)
{
delete_elements($to_delete_elements);
$counts['del_elements'] = count($to_delete_elements);
}
update_category('all');
update_global_rank();
$files = get_filelist('', $site_id, true, false);
$updates = array();
foreach ($files as $id => $file)
{
$data = $site_reader->get_element_update_attributes($file['path']);
if (!is_array($data))
{
continue;
}
$data['id'] = $id;
$updates[] = $data;
}
if ($updates)
{
mass_updates(
IMAGES_TABLE,
array('primary'=>array('id'), 'update'=>$site_reader->get_update_attributes()),
$updates
);
}
$counts['upd_elements'] = count($updates);
$metadata_files = get_filelist('', $site_id, true, true);
$counts['metadata_candidates'] = count($metadata_files);
$metadata_updates = array();
$tags_of = array();
foreach ($metadata_files as $id => $element_infos)
{
$data = $site_reader->get_element_metadata($element_infos);
if (!is_array($data))
{
bratonien_tools_nc_productive_error($errors, $element_infos['path'], 'PWG-ERROR-NO-FS');
continue;
}
$data['date_metadata_update'] = $dbnow;
$data['id'] = $id;
$metadata_updates[] = $data;
foreach (array('keywords','tags') as $key)
{
if (!isset($data[$key]))
{
continue;
}
$tags_of[$id] = $tags_of[$id] ?? array();
foreach (explode(',', $data[$key]) as $tag_name)
{
$tags_of[$id][] = tag_id_from_tag_name($tag_name);
}
}
}
if ($metadata_updates)
{
mass_updates(
IMAGES_TABLE,
array(
'primary'=>array('id'),
'update'=>array_unique(array_merge(
array_diff($site_reader->get_metadata_attributes(), array('keywords','tags')),
array('date_metadata_update')
)),
),
$metadata_updates,
MASS_UPDATES_SKIP_EMPTY
);
}
if ($tags_of)
{
set_tags_of($tags_of);
}
$counts['metadata_updated'] = count($metadata_updates);
// Mirror Piwigo 16.4.0 Maintenance -> "Update albums informations".
// This repairs the derived album hierarchy and counters that the direct
// API sync otherwise bypasses when no admin maintenance page is invoked.
images_integrity();
categories_integrity();
update_uppercats();
update_category('all');
update_global_rank();
invalidate_user_cache(true);
// Mirror Piwigo 16.4.0 Maintenance -> "Update photos information".
// This finalizes physical paths, ratings and derived photo information.
images_integrity();
update_path();
include_once(PHPWG_ROOT_PATH.'include/functions_rate.inc.php');
update_rating_score();
invalidate_user_cache();
ob_start();
include(PHPWG_ROOT_PATH.'admin/site_update.php');
$output = (string)ob_get_clean();
}
catch (Throwable $e)
{
return new PwgError(500, 'Bratonien direct synchronization failed: '.$e->getMessage());
if (ob_get_level() > 0)
{
ob_end_clean();
}
return new PwgError(500, 'Piwigo core synchronization failed: '.$e->getMessage());
}
finally
{
$_GET = $saved_get;
$_POST = $saved_post;
if ($saved_template_dirs !== null && is_object($template) && isset($template->smarty))
{
$template->smarty->setTemplateDir($saved_template_dirs);
}
if ($saved_default_status === null)
{
unset($conf['newcat_default_status']);
}
else
{
$conf['newcat_default_status'] = $saved_default_status;
}
if ($saved_request_method === null)
{
unset($_SERVER['REQUEST_METHOD']);
}
else
{
$_SERVER['REQUEST_METHOD'] = $saved_request_method;
}
}
return array(
'mode' => 'productive',
'engine' => 'bratonien-direct',
'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),
'counts' => is_array($counts) ? $counts : array(),
'database_writes' => true,
'core_output_bytes' => strlen($output),
'username' => isset($user['username']) ? (string)$user['username'] : '',
'status' => isset($user['status']) ? (string)$user['status'] : '',
);

View File

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

View File

@@ -4,6 +4,17 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!');
}
if (isset($GLOBALS['template']) && is_object($GLOBALS['template']) && method_exists($GLOBALS['template'], 'func_combine_script'))
{
$script_version = function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : '0.9.7.1';
$GLOBALS['template']->func_combine_script(array(
'id'=>'bratonien_nc_connector_edit_v2',
'path'=>BRATONIEN_TOOLS_PATH.'js/nc_connector_edit_v2.js',
'load'=>'footer',
'version'=>$script_version,
));
}
require_once(BRATONIEN_TOOLS_PATH . 'tools/image_cache.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_profiles.inc.php');
@@ -16,13 +27,38 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/self_update.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_manage.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_auth.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_db_bridge.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_user_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_flow.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_connection_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_generic_scope.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_edit.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_transport.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_scheduler.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_system.inc.php');
function bratonien_tools_nc_connector_run_now()
{
$connection_id = isset($_POST['connection_id']) ? max(0, (int)$_POST['connection_id']) : 0;
if ($connection_id > 0)
{
$connection = bratonien_tools_nc_connector_connection($connection_id, false);
if (!$connection)
{
throw new RuntimeException('Die ausgewählte Verbindung existiert nicht.');
}
}
$result = bratonien_tools_nc_scheduler_spawn(true, $connection_id);
return array('message'=>(string)($result['message'] ?? ($connection_id > 0 ? 'Abgleich wurde gestartet.' : 'Abgleich für alle Verbindungen wurde gestartet.')));
}
function bratonien_tools_get_tools()
{
@@ -48,11 +84,15 @@ function bratonien_tools_get_tools()
'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'),
'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'),
'nc_connector_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'),
'nc_connector_edit_start' => array('handler' => 'bratonien_tools_nc_transport_edit_start'),
'nc_connector_update_local' => array('handler' => 'bratonien_tools_nc_connector_update_local_friendly'),
'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'),
'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'),
'nc_connector_run_now' => array('handler' => 'bratonien_tools_nc_connector_run_now'),
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'),
'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'),
'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'),
'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_transport_wizard_directory_browse'),
'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),
'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_sources_dispatch'),
@@ -62,8 +102,12 @@ function bratonien_tools_get_tools()
'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_dispatch'),
'nc_connector_wizard_back' => array('handler' => 'bratonien_tools_nc_wizard_back'),
'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'),
'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_connection_scoped'),
'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'),
'nc_connector_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'),
'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'),
'nc_connector_fallback_delete' => array('handler' => 'bratonien_tools_nc_connector_fallback_delete_scoped'),
'nc_connector_fallback_once' => array('handler' => 'bratonien_tools_nc_connector_fallback_once'),
'self_update_check' => array('handler' => 'bratonien_tools_self_update_check'),
'self_update_run' => array('handler' => 'bratonien_tools_self_update_run'),
);

View File

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

View File

@@ -1,274 +0,0 @@
<?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,40 +1,6 @@
(function () {
'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() {
var admin = document.querySelector('.bratonien-admin');
if (!admin) return;
@@ -147,14 +113,9 @@
activate(initial, false);
}
function init() {
initBratonienTabs();
initNcConnectorRunNow();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentLoaded', initBratonienTabs);
} else {
init();
initBratonienTabs();
}
})();

193
js/nc_connector_edit_v2.js Normal file
View File

@@ -0,0 +1,193 @@
(function () {
'use strict';
var statusEndpoint = 'plugins/bratonien_tools/nc-connector-status.php';
function ensureLiveStatus(detail, actions) {
var node = detail.querySelector('[data-nc-run-live]');
if (node) return node;
node = document.createElement('div');
node.setAttribute('data-nc-run-live', '1');
node.className = 'bratonien-base-note';
node.style.marginTop = '.6rem';
node.hidden = true;
actions.parentNode.insertBefore(node, actions.nextSibling);
return node;
}
function renderLiveStatus(node, data) {
var state = String(data && data.state || '');
var message = String(data && data.message || '');
var detail = String(data && data.error_detail || '');
node.hidden = false;
node.innerHTML = '';
var strong = document.createElement('strong');
if (state === 'queued') strong.textContent = 'Angefordert: ';
else if (state === 'running') strong.textContent = 'Läuft: ';
else if (state === 'ok' || state === 'success') strong.textContent = 'Erfolgreich: ';
else if (state === 'error') strong.textContent = 'Fehler: ';
else strong.textContent = 'Status: ';
node.appendChild(strong);
node.appendChild(document.createTextNode(message || state || 'Status wird ermittelt …'));
if (detail) {
var details = document.createElement('details');
details.style.marginTop = '.35rem';
var summary = document.createElement('summary');
summary.textContent = 'Technische Laufzeitdetails';
var pre = document.createElement('pre');
pre.style.whiteSpace = 'pre-wrap';
pre.style.wordBreak = 'break-word';
pre.textContent = detail;
details.appendChild(summary);
details.appendChild(pre);
node.appendChild(details);
}
}
function pollConnection(connectionId, node, button) {
var attempts = 0;
var maxAttempts = 180;
var timer = null;
function stop() {
if (timer) window.clearInterval(timer);
timer = null;
if (button) {
button.disabled = false;
button.textContent = 'Jetzt abgleichen';
}
}
function poll() {
attempts += 1;
fetch(statusEndpoint + '?connection_id=' + encodeURIComponent(connectionId) + '&_=' + Date.now(), {
credentials: 'same-origin',
cache: 'no-store',
headers: {'Accept': 'application/json'}
})
.then(function (response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
})
.then(function (data) {
renderLiveStatus(node, data);
var state = String(data && data.state || '');
if (state === 'ok' || state === 'success' || state === 'error' || attempts >= maxAttempts) stop();
})
.catch(function (error) {
if (attempts >= maxAttempts) {
renderLiveStatus(node, {state: 'error', message: 'Status konnte nicht gelesen werden.', error_detail: error.message});
stop();
}
});
}
poll();
timer = window.setInterval(poll, 1000);
}
function bindRunNow(form, detail, liveNode) {
if (form.dataset.ncRunBound === '1') return;
form.dataset.ncRunBound = '1';
form.addEventListener('submit', function (event) {
event.preventDefault();
detail.open = true;
var button = form.querySelector('button[value="nc_connector_run_now"]');
var connectionInput = form.querySelector('input[name="connection_id"]');
if (!connectionInput) return;
if (button) {
button.disabled = true;
button.textContent = 'Abgleich wird gestartet …';
}
renderLiveStatus(liveNode, {state: 'queued', message: 'Abgleich wird angefordert …'});
fetch(form.action || window.location.href, {
method: 'POST',
credentials: 'same-origin',
cache: 'no-store',
body: new FormData(form)
})
.then(function (response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
renderLiveStatus(liveNode, {state: 'queued', message: 'Abgleich wurde angefordert.'});
pollConnection(connectionInput.value, liveNode, button);
})
.catch(function (error) {
renderLiveStatus(liveNode, {state: 'error', message: 'Abgleich konnte nicht angefordert werden.', error_detail: error.message});
if (button) {
button.disabled = false;
button.textContent = 'Jetzt abgleichen';
}
});
});
}
function restoreRunNowButtons() {
var section = document.getElementById('nc-connector');
if (!section) return;
var connectionCard = null;
var headings = section.querySelectorAll('h4');
for (var i = 0; i < headings.length; i++) {
if ((headings[i].textContent || '').trim() === 'Bestehende Verbindungen') {
connectionCard = headings[i].closest('.bratonien-card');
break;
}
}
if (!connectionCard) return;
var details = connectionCard.querySelectorAll(':scope > details');
details.forEach(function (detail) {
var actions = detail.querySelector('.bratonien-actions');
if (!actions) return;
var connectionInput = detail.querySelector('input[name="connection_id"]');
var tokenInput = detail.querySelector('input[name="pwg_token"]');
if (!connectionInput || !tokenInput) return;
var form = actions.querySelector('form[data-nc-run-now]');
if (!form) {
form = document.createElement('form');
form.method = 'post';
form.setAttribute('data-nc-run-now', '1');
var token = document.createElement('input');
token.type = 'hidden';
token.name = 'pwg_token';
token.value = tokenInput.value;
form.appendChild(token);
var connection = document.createElement('input');
connection.type = 'hidden';
connection.name = 'connection_id';
connection.value = connectionInput.value;
form.appendChild(connection);
var button = document.createElement('button');
button.className = 'buttonLike';
button.type = 'submit';
button.name = 'bratonien_tool';
button.value = 'nc_connector_run_now';
button.textContent = 'Jetzt abgleichen';
form.appendChild(button);
actions.insertBefore(form, actions.firstChild);
}
bindRunNow(form, detail, ensureLiveStatus(detail, actions));
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', restoreRunNowButtons);
} else {
restoreRunNowButtons();
}
})();

View File

@@ -1,7 +1,7 @@
<?php
/*
Plugin Name: Bratonien Tools
Version: 0.9.6.8.15
Version: 0.9.7.26
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien
@@ -17,18 +17,18 @@ 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/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/picture_navigation.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/batch_titles.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_scheduler.inc.php');
add_event_handler('get_admin_plugin_menu_links', 'bratonien_tools_admin_menu');
add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4);
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_materialize_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 4);
add_event_handler('get_src_image_url', 'bratonien_tools_filter_webdav_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('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action');
add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EVENT_HANDLER_PRIORITY_NEUTRAL - 30);
@@ -36,7 +36,7 @@ add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', E
add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10);
add_event_handler('init', 'bratonien_tools_album_shares_init');
add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_productive_ws_methods');
function bratonien_tools_prepare_connector_private_import()
@@ -82,46 +82,36 @@ function bratonien_tools_prepare_private_album_permissions()
return;
}
bratonien_tools_grant_private_album_access($category_id, (int)$user['id']);
}
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)
$result = pwg_query('SELECT status FROM '.CATEGORIES_TABLE.' WHERE id = '.$category_id.' LIMIT 1');
if (!pwg_db_num_rows($result))
{
return;
}
$query = '
SELECT 1
FROM '.USER_ACCESS_TABLE.'
WHERE user_id = '.$user_id.'
AND cat_id = '.$category_id.'
LIMIT 1
;';
$result = pwg_query($query);
if (pwg_db_num_rows($result) > 0)
$category = pwg_db_fetch_assoc($result);
if ($category['status'] !== 'public')
{
return;
}
single_insert(
USER_ACCESS_TABLE,
array(
'user_id' => $user_id,
'cat_id' => $category_id,
)
);
$users = isset($_POST['users']) && is_array($_POST['users']) ? $_POST['users'] : array();
$current_user_id = (int)$user['id'];
$normalized_user_ids = array_map('intval', $users);
if (!in_array($current_user_id, $normalized_user_ids, true))
{
$users[] = $current_user_id;
}
$_POST['users'] = $users;
}
function bratonien_tools_admin_menu($menu)
{
$menu[] = array(
'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;
}
?>

View File

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

284
nc-connector-cutover-v2.php Normal file
View File

@@ -0,0 +1,284 @@
#!/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);
}

374
nc-connector-cutover.php Normal file
View File

@@ -0,0 +1,374 @@
#!/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);
}

110
nc-connector-diagnose.php Normal file
View File

@@ -0,0 +1,110 @@
#!/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";
}

81
nc-connector-disable.php Normal file
View File

@@ -0,0 +1,81 @@
#!/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);
}

188
nc-connector-install.php Normal file
View File

@@ -0,0 +1,188 @@
#!/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

@@ -0,0 +1,183 @@
#!/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);
}

133
nc-connector-migrate.php Normal file
View File

@@ -0,0 +1,133 @@
#!/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);
}

149
nc-connector-normalize.php Normal file
View File

@@ -0,0 +1,149 @@
#!/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

@@ -0,0 +1,226 @@
#!/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

@@ -24,6 +24,7 @@ if (!function_exists('is_admin') || !is_admin())
exit;
}
$requested_connection_id = isset($_GET['connection_id']) ? max(0, (int)$_GET['connection_id']) : 0;
$dir = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-connector-status';
$latest = array(
'state'=>'idle',
@@ -36,11 +37,21 @@ $latest = array(
'api'=>array('state'=>'not_run','message'=>''),
'fallback'=>array('state'=>'not_run','message'=>''),
'error_detail'=>'',
'route'=>'',
'route_label'=>'Noch kein Datenweg erfasst',
'route_timestamp'=>0,
'route_time_label'=>'Nicht verfügbar',
'route_detail'=>'',
'connection_id'=>$requested_connection_id,
);
if (is_dir($dir))
{
foreach (glob($dir.'/connection-*.json') ?: array() as $file)
$files = $requested_connection_id > 0
? array($dir.'/connection-'.$requested_connection_id.'.json')
: (glob($dir.'/connection-*.json') ?: array());
foreach ($files as $file)
{
if (!is_readable($file))
{
@@ -57,11 +68,57 @@ if (is_dir($dir))
$latest['connection_file'] = basename($file);
}
}
}
if ((int)$latest['timestamp'] > 0)
{
$latest['last_run_label'] = date('d.m.Y H:i:s', (int)$latest['timestamp']);
if ($requested_connection_id === 0)
{
$route_file = $dir.'/route-status.json';
if (is_readable($route_file))
{
$route = json_decode((string)@file_get_contents($route_file), true);
if (is_array($route))
{
$route_name = (string)($route['route'] ?? '');
$route_timestamp = (int)($route['timestamp'] ?? 0);
$route_label = (string)($route['label'] ?? '');
if ($route_name === 'webdav')
{
$route_label = 'WEBDAV PRIMÄR';
}
elseif ($route_name === 'legacy_fallback')
{
$route_label = 'LEGACY-FALLBACK AKTIV';
}
elseif ($route_name === 'failed')
{
$route_label = 'FEHLER - KEIN ERFOLGREICHER DATENWEG';
}
elseif ($route_label === '')
{
$route_label = 'UNBEKANNTER DATENWEG';
}
$route_detail = trim((string)($route['detail'] ?? ''));
$latest['route'] = $route_name;
$latest['route_label'] = $route_label;
$latest['route_timestamp'] = $route_timestamp;
$latest['route_time_label'] = $route_timestamp > 0 ? date('d.m.Y H:i:s', $route_timestamp) : 'Nicht verfügbar';
$latest['route_detail'] = $route_detail;
$base_message = trim((string)($latest['message'] ?? ''));
$message_parts = array($route_label);
if ($route_name !== 'webdav' && $route_detail !== '')
{
$message_parts[] = $route_detail;
}
if ($base_message !== '')
{
$message_parts[] = $base_message;
}
$latest['message'] = implode(' · ', $message_parts);
}
}
}
}
$system_file = BRATONIEN_TOOLS_PATH.'include/nc_connector_system.inc.php';
@@ -76,4 +133,27 @@ if (is_readable($system_file))
}
}
$scheduler_file = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-connector-scheduler/state.json';
if ($requested_connection_id > 0 && is_readable($scheduler_file))
{
$scheduler = json_decode((string)@file_get_contents($scheduler_file), true);
if (
is_array($scheduler)
&& (int)($scheduler['connection_id'] ?? 0) === $requested_connection_id
&& in_array((string)($scheduler['state'] ?? ''), array('queued','running'), true)
&& (int)($scheduler['timestamp'] ?? 0) >= (int)($latest['timestamp'] ?? 0)
)
{
$latest['state'] = (string)$scheduler['state'];
$latest['message'] = (string)($scheduler['message'] ?? 'NC-Abgleich läuft.');
$latest['timestamp'] = (int)($scheduler['timestamp'] ?? time());
$latest['error_detail'] = '';
}
}
if ((int)$latest['timestamp'] > 0)
{
$latest['last_run_label'] = date('d.m.Y H:i:s', (int)$latest['timestamp']);
}
echo json_encode($latest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Seit 0.9.7.20 greift der NC Connector nicht mehr in Piwigos Bild-/Derivatlogik ein.
# Der Shadowtree dient nur der Piwigo-Synchronisation; die Bild-URL wird zur Laufzeit
# ueber den bestehenden WebDAV-URL-Hook auf Nextcloud aufgeloest.
exit 0

View File

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

86
runtime/cleanup-stale.php Normal file
View File

@@ -0,0 +1,86 @@
#!/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

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

View File

@@ -0,0 +1,140 @@
#!/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())

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

View File

@@ -0,0 +1,216 @@
#!/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

@@ -0,0 +1,191 @@
#!/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())

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

@@ -1,9 +1,13 @@
#!/usr/bin/env python3
"""Build a placeholder-backed local source tree from Nextcloud WebDAV.
This is intentionally additive: it does not replace the existing local-storage
connector path. It creates only tiny placeholder files plus a metadata mapping;
no Nextcloud original media is downloaded.
This creates only tiny placeholder files plus a metadata mapping; no Nextcloud
original media is stored locally. The authenticated Nextcloud user is never used
as an album name.
Each placeholder carries the original image dimensions. Dimensions are read
from Nextcloud metadata first. If those metadata are unavailable, only the
beginning of the original file is read through WebDAV and parsed locally.
"""
from __future__ import annotations
@@ -14,6 +18,7 @@ import getpass
import json
import os
import shutil
import socket
import ssl
import sys
import tempfile
@@ -21,14 +26,15 @@ import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from contextlib import contextmanager
from pathlib import Path, PurePosixPath
DAV = "DAV:"
OC = "http://owncloud.org/ns"
NC = "http://nextcloud.org/ns"
SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
# 1x1 transparent GIF, 34 bytes. The filename keeps the remote extension;
# the placeholder exists only so Piwigo can discover the logical image entry.
PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
DIMENSION_PROBE_BYTES = 4 * 1024 * 1024
def fail(message: str) -> None:
@@ -54,9 +60,132 @@ def safe_local_name(name: str) -> str:
return name
def parse_dimensions(prop: ET.Element) -> tuple[int, int]:
for property_name in ("file-metadata-size", "metadata-photos-size"):
raw = prop.findtext(f"{{{NC}}}{property_name}", default="").strip()
if not raw:
continue
try:
decoded = json.loads(raw)
except (TypeError, ValueError, json.JSONDecodeError):
continue
if isinstance(decoded, dict) and isinstance(decoded.get("value"), dict):
decoded = decoded["value"]
if not isinstance(decoded, dict):
continue
try:
width = int(decoded.get("width", 0) or 0)
height = int(decoded.get("height", 0) or 0)
except (TypeError, ValueError):
continue
if width > 0 and height > 0:
return width, height
return 0, 0
def parse_image_header_dimensions(data: bytes) -> tuple[int, int]:
if len(data) >= 24 and data.startswith(b"\x89PNG\r\n\x1a\n") and data[12:16] == b"IHDR":
return int.from_bytes(data[16:20], "big"), int.from_bytes(data[20:24], "big")
if len(data) >= 10 and data[:6] in (b"GIF87a", b"GIF89a"):
return int.from_bytes(data[6:8], "little"), int.from_bytes(data[8:10], "little")
if len(data) >= 12 and data[:4] == b"RIFF" and data[8:12] == b"WEBP":
chunk = data[12:16]
if chunk == b"VP8X" and len(data) >= 30:
width = 1 + int.from_bytes(data[24:27], "little")
height = 1 + int.from_bytes(data[27:30], "little")
return width, height
if chunk == b"VP8 " and len(data) >= 30:
payload = 20
if data[payload + 3:payload + 6] == b"\x9d\x01\x2a":
width = int.from_bytes(data[payload + 6:payload + 8], "little") & 0x3FFF
height = int.from_bytes(data[payload + 8:payload + 10], "little") & 0x3FFF
return width, height
if chunk == b"VP8L" and len(data) >= 25 and data[20] == 0x2F:
b1, b2, b3, b4 = data[21:25]
width = 1 + b1 + ((b2 & 0x3F) << 8)
height = 1 + ((b2 & 0xC0) >> 6) + (b3 << 2) + ((b4 & 0x0F) << 10)
return width, height
if len(data) >= 4 and data[:2] == b"\xff\xd8":
sof_markers = {
0xC0, 0xC1, 0xC2, 0xC3,
0xC5, 0xC6, 0xC7,
0xC9, 0xCA, 0xCB,
0xCD, 0xCE, 0xCF,
}
pos = 2
while pos + 4 <= len(data):
if data[pos] != 0xFF:
pos += 1
continue
while pos < len(data) and data[pos] == 0xFF:
pos += 1
if pos >= len(data):
break
marker = data[pos]
pos += 1
if marker in (0xD8, 0xD9) or 0xD0 <= marker <= 0xD7:
continue
if marker == 0xDA:
break
if pos + 2 > len(data):
break
segment_length = int.from_bytes(data[pos:pos + 2], "big")
if segment_length < 2:
break
if marker in sof_markers:
if pos + 7 > len(data):
break
height = int.from_bytes(data[pos + 3:pos + 5], "big")
width = int.from_bytes(data[pos + 5:pos + 7], "big")
return width, height
pos += segment_length
return 0, 0
def placeholder_bytes(width: int, height: int) -> bytes:
if not (1 <= width <= 65535 and 1 <= height <= 65535):
fail(f"unsupported image dimensions for placeholder: {width}x{height}")
data = bytearray(PLACEHOLDER)
data[6:8] = width.to_bytes(2, "little")
data[8:10] = height.to_bytes(2, "little")
return bytes(data)
@contextmanager
def pinned_resolution(host: str, ip: str):
host = host.strip("[]").lower()
ip = ip.strip("[]")
if not host or not ip or host == ip:
yield
return
original = socket.getaddrinfo
def resolve(name, port, family=0, type=0, proto=0, flags=0):
normalized = str(name).strip("[]").lower()
if normalized == host:
return original(ip, port, family, type, proto, flags)
return original(name, port, family, type, proto, flags)
socket.getaddrinfo = resolve
try:
yield
finally:
socket.getaddrinfo = original
class WebDavClient:
def __init__(self, base_url: str, user: str, password: str, timeout: int = 30) -> None:
def __init__(self, base_url: str, user: str, password: str, timeout: int = 30, connect_ip: str = "") -> None:
self.base_url = base_url.rstrip("/")
parsed = urllib.parse.urlparse(self.base_url)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
fail("Nextcloud base URL must use HTTP or HTTPS and contain a host")
self.host = parsed.hostname.strip("[]")
self.connect_ip = connect_ip.strip("[]") or self.host
self.user = user
self.password = password
self.timeout = timeout
@@ -64,31 +193,60 @@ class WebDavClient:
self.auth_header = f"Basic {token}"
self.context = ssl.create_default_context()
def collection_url(self, relative: str) -> str:
def file_url(self, relative: str) -> str:
user = urllib.parse.quote(self.user, safe="")
suffix = quote_path(relative)
url = f"{self.base_url}/remote.php/dav/files/{user}/"
if suffix:
url += suffix + "/"
return f"{self.base_url}/remote.php/dav/files/{user}/{suffix}"
def collection_url(self, relative: str) -> str:
url = self.file_url(relative)
if not url.endswith("/"):
url += "/"
return url
def probe_dimensions(self, relative: str) -> tuple[int, int]:
request = urllib.request.Request(self.file_url(relative), method="GET")
request.add_header("Authorization", self.auth_header)
request.add_header("Range", f"bytes=0-{DIMENSION_PROBE_BYTES - 1}")
try:
with pinned_resolution(self.host, self.connect_ip):
with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response:
if response.status not in (200, 206):
fail(f"Nextcloud image header returned HTTP {response.status}")
data = response.read(DIMENSION_PROBE_BYTES)
except urllib.error.HTTPError as error:
if error.code in {401, 403}:
fail("Nextcloud rejected the WebDAV credentials or file access")
fail(f"Nextcloud image header request failed with HTTP {error.code}")
except urllib.error.URLError as error:
fail(f"Nextcloud WebDAV is unreachable while reading image dimensions: {error.reason}")
width, height = parse_image_header_dimensions(data)
if width < 1 or height < 1:
fail(f"original image dimensions could not be read from Nextcloud file header: {relative}")
return width, height
def list_collection(self, relative: str) -> tuple[dict[str, object], list[dict[str, object]]]:
relative = validate_relative(relative)
url = self.collection_url(relative)
body = (
'<?xml version="1.0"?>'
'<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">'
'<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: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")
request = urllib.request.Request(url, data=body, method="PROPFIND")
request.add_header("Authorization", self.auth_header)
request.add_header("Depth", "1")
request.add_header("Content-Type", "application/xml; charset=utf-8")
try:
with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response:
status = response.status
payload = response.read()
with pinned_resolution(self.host, self.connect_ip):
with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response:
status = response.status
payload = response.read()
except urllib.error.HTTPError as error:
if error.code in {401, 403}:
fail("Nextcloud rejected the WebDAV credentials or directory access")
@@ -98,14 +256,14 @@ class WebDavClient:
if status != 207:
fail(f"Nextcloud PROPFIND returned HTTP {status}")
base_path = urllib.parse.unquote(urllib.parse.urlparse(url).path).rstrip("/")
current: dict[str, object] | None = None
children: list[dict[str, object]] = []
try:
root = ET.fromstring(payload)
except ET.ParseError as error:
raise RuntimeError("Nextcloud returned invalid WebDAV XML") from error
base_path = urllib.parse.unquote(urllib.parse.urlparse(url).path).rstrip("/")
current: dict[str, object] | None = None
children: list[dict[str, object]] = []
for response in root.findall(f"{{{DAV}}}response"):
href = response.findtext(f"{{{DAV}}}href", default="")
href_path = urllib.parse.unquote(urllib.parse.urlparse(href).path).rstrip("/")
@@ -122,6 +280,7 @@ class WebDavClient:
fileid_text = prop.findtext(f"{{{OC}}}fileid", default="").strip()
resource_type = prop.find(f"{{{DAV}}}resourcetype")
is_dir = resource_type is not None and resource_type.find(f"{{{DAV}}}collection") is not None
width, height = parse_dimensions(prop)
item = {
"display_name": display,
"fileid": int(fileid_text) if fileid_text.isdigit() else 0,
@@ -129,6 +288,8 @@ class WebDavClient:
"content_type": prop.findtext(f"{{{DAV}}}getcontenttype", default=""),
"size": int(prop.findtext(f"{{{DAV}}}getcontentlength", default="0") or 0),
"etag": prop.findtext(f"{{{DAV}}}getetag", default="").strip('"'),
"width": width,
"height": height,
}
if href_path == base_path:
current = item
@@ -140,24 +301,17 @@ class WebDavClient:
return current, children
def link_placeholder(seed: Path, target: Path) -> None:
def write_placeholder(target: Path, width: int, height: int) -> None:
target.parent.mkdir(parents=True, exist_ok=True)
try:
os.link(seed, target)
except OSError:
target.write_bytes(PLACEHOLDER)
target.write_bytes(placeholder_bytes(width, height))
os.chmod(target, 0o644)
def build_root(
client: WebDavClient,
remote_root: str,
local_root: Path,
seed: Path,
mapping: dict[str, dict[str, object]],
) -> tuple[int, int, int]:
def build_root(client: WebDavClient, remote_root: str, local_root: Path, mapping: dict[str, dict[str, object]]) -> tuple[int, int, int, int]:
files = 0
folders = 0
skipped = 0
probed = 0
stack: list[tuple[str, Path]] = [(validate_relative(remote_root), local_root)]
visited: set[str] = set()
@@ -187,7 +341,16 @@ def build_root(
if extension not in SUPPORTED_IMAGE_EXTENSIONS:
skipped += 1
continue
link_placeholder(seed, child_local)
width = int(child.get("width", 0) or 0)
height = int(child.get("height", 0) or 0)
dimension_source = "metadata"
if width < 1 or height < 1:
width, height = client.probe_dimensions(child_remote)
dimension_source = "header"
probed += 1
write_placeholder(child_local, width, height)
files += 1
mapping[str(child_local)] = {
"kind": "file",
@@ -197,8 +360,11 @@ def build_root(
"content_type": str(child.get("content_type", "")),
"size": int(child.get("size", 0)),
"etag": str(child.get("etag", "")),
"width": width,
"height": height,
"dimension_source": dimension_source,
}
return files, folders, skipped
return files, folders, skipped, probed
def atomic_json(path: Path, payload: object) -> None:
@@ -221,6 +387,7 @@ def atomic_text(path: Path, text: str) -> None:
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base-url", required=True)
parser.add_argument("--connect-ip", default="", help="IP address used for the TCP connection while preserving the URL host for HTTP Host and TLS SNI")
parser.add_argument("--user", required=True)
parser.add_argument("--password-file", type=Path)
parser.add_argument("--root", action="append", required=True, help="WebDAV path relative to the authenticated user's files root")
@@ -241,48 +408,47 @@ def main() -> int:
source_dir = args.source_dir.resolve()
staging = source_dir.with_name(f".{source_dir.name}.next")
previous = source_dir.with_name(f".{source_dir.name}.previous")
seed = source_dir.parent / ".bratonien-webdav-placeholder.gif"
source_dir.parent.mkdir(parents=True, exist_ok=True)
seed.write_bytes(PLACEHOLDER)
os.chmod(seed, 0o644)
if staging.exists():
shutil.rmtree(staging)
staging.mkdir(parents=True)
client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout))
client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout), args.connect_ip)
mapping: dict[str, dict[str, object]] = {}
manifest: list[str] = []
total_files = total_folders = total_skipped = 0
used_names: set[str] = set()
total_files = total_folders = total_skipped = total_probed = 0
used_fileids: set[int] = set()
for remote_root_raw in args.root:
remote_root = validate_relative(remote_root_raw)
current, _ = client.list_collection(remote_root)
current, root_children = client.list_collection(remote_root)
fileid = int(current["fileid"])
display = str(current.get("display_name", "")).strip() or (PurePosixPath(remote_root).name if remote_root else args.user)
local_name = f"root-{fileid}"
if local_name in used_names:
if fileid in used_fileids:
fail(f"duplicate selected Nextcloud root fileid: {fileid}")
used_names.add(local_name)
used_fileids.add(fileid)
local_name = f"root-{fileid}"
local_root = staging / local_name
files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping)
files, folders, skipped, probed = build_root(client, remote_root, local_root, mapping)
total_files += files
total_folders += folders
total_skipped += skipped
total_probed += probed
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}")
for child in sorted(root_children, key=lambda item: str(item.get("display_name", "")).casefold()):
name = safe_local_name(str(child.get("display_name", "")))
child_fileid = int(child.get("fileid", 0))
if child_fileid < 1:
fail(f"Nextcloud returned no stable fileid for root child {name!r}")
child_path = source_dir / local_name / name
if bool(child.get("is_dir")):
manifest.append(f"webdav:{child_fileid}\tfolder\t{name}\t{child_path}")
elif Path(name).suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS:
manifest.append(f"webdav:{child_fileid}\tfile\t{name}\t{child_path}")
continue
display = str(current.get("display_name", "")).strip() or PurePosixPath(remote_root).name
manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}")
if previous.exists():
shutil.rmtree(previous)
@@ -308,8 +474,9 @@ def main() -> int:
atomic_text(args.manifest, "\n".join(manifest) + "\n")
atomic_json(args.mapping, {
"version": 1,
"version": 3,
"base_url": args.base_url.rstrip("/"),
"connect_ip": client.connect_ip,
"user": args.user,
"files": final_mapping,
})
@@ -318,6 +485,8 @@ def main() -> int:
"files": total_files,
"folders": total_folders,
"skipped": total_skipped,
"dimension_header_probes": total_probed,
"connect_ip": client.connect_ip,
"source_dir": str(source_dir),
"manifest": str(args.manifest),
"mapping": str(args.mapping),

View File

@@ -0,0 +1,53 @@
<?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

@@ -0,0 +1,83 @@
#!/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";

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

@@ -15,27 +15,12 @@ function http_error_detail($body)
{
$body = trim((string)$body);
if ($body === '') return '';
$decoded = json_decode($body, true);
if (is_array($decoded))
{
$detail = (string)($decoded['message'] ?? $decoded['err'] ?? '');
if ($detail !== '') return $detail;
}
if (function_exists('simplexml_load_string'))
{
$previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string($body);
libxml_clear_errors();
libxml_use_internal_errors($previous);
if ($xml !== false)
{
$detail = trim((string)($xml->message ?? $xml->err ?? ''));
if ($detail !== '') return $detail;
}
}
$plain = trim(preg_replace('/\s+/', ' ', strip_tags($body)));
return $plain === '' ? '' : mb_substr($plain, 0, 500);
}
@@ -50,7 +35,7 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 900,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_USERAGENT => 'Bratonien-NC-Connector-WebDAV',
CURLOPT_USERAGENT => 'Bratonien-NC-Connector',
);
if ($headers) $options[CURLOPT_HTTPHEADER] = $headers;
if ($cookie_file !== null)
@@ -74,55 +59,15 @@ function http_request($url, array $fields, array $headers = array(), $cookie_fil
return (string)$body;
}
function xml_value_sync(SimpleXMLElement $node)
{
$children = $node->children();
if (count($children) === 0) return (string)$node;
$result = array();
foreach ($children as $name => $child)
{
$value = xml_value_sync($child);
if (array_key_exists($name, $result))
{
if (!is_array($result[$name]) || !array_is_list($result[$name])) $result[$name] = array($result[$name]);
$result[$name][] = $value;
}
else $result[$name] = $value;
}
return $result;
}
function decode_ws($body)
{
$decoded = json_decode((string)$body, true);
if (!is_array($decoded))
{
if (!function_exists('simplexml_load_string')) fail_sync('Piwigo lieferte XML, aber SimpleXML ist nicht verfuegbar.');
$previous = libxml_use_internal_errors(true);
$xml = simplexml_load_string((string)$body);
libxml_clear_errors();
libxml_use_internal_errors($previous);
if ($xml === false || $xml->getName() !== 'rsp') fail_sync('Piwigo lieferte weder gueltiges JSON noch eine gueltige XML-Webservice-Antwort.');
$decoded = array('stat'=>(string)$xml['stat']);
foreach ($xml->children() as $name => $child)
{
$value = xml_value_sync($child);
if (array_key_exists($name, $decoded))
{
if (!is_array($decoded[$name]) || !array_is_list($decoded[$name])) $decoded[$name] = array($decoded[$name]);
$decoded[$name][] = $value;
}
else $decoded[$name] = $value;
}
}
if (!is_array($decoded)) fail_sync('Piwigo lieferte keine gueltige JSON-Webservice-Antwort.');
if (($decoded['stat'] ?? '') !== 'ok')
{
$message = (string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo-Aufruf wurde abgelehnt.');
fail_sync($message);
fail_sync((string)($decoded['message'] ?? $decoded['err'] ?? 'Piwigo-Aufruf wurde abgelehnt.'));
}
if (array_key_exists('result', $decoded)) return $decoded['result'];
unset($decoded['stat']);
return $decoded;
return $decoded['result'] ?? array();
}
function decrypt_blob($blob, $hex_key)
@@ -134,29 +79,23 @@ function decrypt_blob($blob, $hex_key)
$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)) fail_sync('Gespeicherte Zugangsdaten sind unvollstaendig.');
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) fail_sync('Gespeicherte Zugangsdaten konnten nicht entschluesselt werden.');
return (string)$plain;
$decoded = json_decode((string)$plain, true);
return is_array($decoded) ? $decoded : array();
}
function ensure_webdav_site(mysqli $db, $prefixeTable, $piwigo_root, array $connection_config, $connection_id)
{
if ((string)($connection_config['source_mode'] ?? '') !== 'webdav-placeholder')
{
fail_sync('Verbindung #'.$connection_id.' ist keine WebDAV-Verbindung.');
}
$gallery_root = rtrim((string)($connection_config['parallel_gallery_root'] ?? ''), '/');
if ($gallery_root === '') fail_sync('WebDAV-Galeriewurzel fehlt in der Verbindungskonfiguration.');
$piwigo_root = rtrim((string)$piwigo_root, '/');
if (strpos($gallery_root, $piwigo_root.'/') !== 0)
{
fail_sync('WebDAV-Galeriewurzel liegt ausserhalb der Piwigo-Installation.');
}
if (strpos($gallery_root, $piwigo_root.'/') !== 0) fail_sync('WebDAV-Galeriewurzel liegt ausserhalb der Piwigo-Installation.');
if (!is_dir($gallery_root)) fail_sync('WebDAV-Galeriewurzel existiert nicht: '.$gallery_root);
$relative = ltrim(substr($gallery_root, strlen($piwigo_root)), '/');
if ($relative === '') fail_sync('WebDAV-Galeriewurzel darf nicht dem Piwigo-Hauptverzeichnis entsprechen.');
$site_url = './'.rtrim($relative, '/').'/';
$escaped = $db->real_escape_string($site_url);
$result = $db->query("SELECT id FROM `{$prefixeTable}sites` WHERE galleries_url='{$escaped}' LIMIT 1");
@@ -186,7 +125,6 @@ try
$conf = array();
$prefixeTable = 'piwigo_';
require $db_config;
foreach (array('db_host','db_user','db_password','db_base') as $key) if (!isset($conf[$key])) fail_sync('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
if ($db->connect_errno) fail_sync('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
@@ -202,42 +140,34 @@ try
$connection_row = $connection_result->fetch_assoc();
$connection_config = json_decode((string)$connection_row['config_json'], true);
if (!is_array($connection_config)) $connection_config = array();
$connection_plain = decrypt_blob((string)$connection_row['secret_blob'], $hex_key);
$connection_credentials = json_decode($connection_plain, true);
if (!is_array($connection_credentials)) $connection_credentials = array();
$credentials = decrypt_blob((string)$connection_row['secret_blob'], $hex_key);
$site_id = ensure_webdav_site($db, $prefixeTable, $piwigo_root, $connection_config, $connection_id);
$api = array('key_id'=>'', 'key_secret'=>'');
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_enabled = !empty($connection_config['api_enabled']);
$api_key_id = $api_enabled ? trim((string)($credentials['api_key_id'] ?? '')) : '';
$api_key_secret = $api_enabled ? trim((string)($credentials['api_key_secret'] ?? '')) : '';
$fallback_user = trim((string)($connection_credentials['piwigo_user'] ?? ''));
$fallback_password = (string)($connection_credentials['piwigo_password'] ?? '');
if ($api['key_id'] !== '' && $api['key_secret'] !== '')
if ($api_key_id !== '' && $api_key_secret !== '')
{
try
{
$headers = array(
'X-PIWIGO-API: '.$api['key_id'].':'.$api['key_secret'],
'Accept: application/json, text/xml;q=0.9',
'X-PIWIGO-API: '.$api_key_id.':'.$api_key_secret,
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
);
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));
$added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Synchronisierung per API erfolgreich (Site $site_id)\n";
echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id),
$headers
));
echo "Piwigo-Synchronisierung per API erfolgreich (Piwigo-Core, Site $site_id)\n";
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, "Piwigo-API nicht nutzbar: ".$e->getMessage()."\n");
fwrite(STDERR, 'Piwigo-API nicht nutzbar: '.$e->getMessage()."\n");
}
}
else
@@ -245,6 +175,8 @@ try
fwrite(STDERR, "Piwigo-API nicht nutzbar: Fuer diese Verbindung ist keine API konfiguriert.\n");
}
$fallback_user = trim((string)($credentials['piwigo_user'] ?? ''));
$fallback_password = (string)($credentials['piwigo_password'] ?? '');
if ($fallback_user === '' || $fallback_password === '') fail_sync('Kein gespeicherter Benutzername/Passwort-Fallback fuer diese Verbindung vorhanden.');
$cookie_file = tempnam(sys_get_temp_dir(), 'br-nc-sync-');
@@ -252,18 +184,21 @@ try
try
{
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password), array(), $cookie_file));
http_request(
$base_url.'/admin.php?page=site_update&site='.$site_id,
array('sync'=>'files','display_info'=>1,'privacy_level'=>0,'sync_meta'=>1,'simulate'=>0,'subcats-included'=>1,'bratonien_connector'=>1,'submit'=>1),
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password),
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));
$added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0);
echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich (Site $site_id)\n";
echo "Piwigo-Orphans synchronisiert: +$added / -$deleted\n";
));
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id),
array('Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'),
$cookie_file
));
echo "Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich (Piwigo-Core, Site $site_id)\n";
}
finally
{
@@ -272,6 +207,6 @@ try
}
catch (Throwable $e)
{
fwrite(STDERR, $e->getMessage()."\n");
fwrite(STDERR, 'Piwigo-Synchronisierung fehlgeschlagen: '.$e->getMessage()."\n");
exit(1);
}

View File

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

View File

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

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

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

108
runtime/native-runner.php Normal file
View File

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

View File

@@ -0,0 +1,17 @@
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

@@ -78,10 +78,14 @@ function webdav_source_fingerprint($baseUrl, $user, array $roots)
$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';
$nativeMode = getenv('BRATONIEN_NC_NATIVE') === '1';
$configDir = trim((string)getenv('BRATONIEN_NC_CONFIG_DIR'));
if ($configDir === '') $configDir = $nativeMode ? rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-runtime' : '/etc/bratonien-tools/nc-connector';
$stateRoot = trim((string)getenv('BRATONIEN_NC_STATE_ROOT'));
if ($stateRoot === '') $stateRoot = $nativeMode ? rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-state' : '/var/lib/bratonien-tools/nc-connector';
$publicSourceRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-source';
$publicGalleryRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-gallery';
$legacyGalleryRoot = rtrim($piwigoRoot, '/').'/galleries';
try
{
@@ -103,8 +107,8 @@ try
$hexKey = trim((string)$keyResult->fetch_assoc()['value']);
$table = $prefixeTable.'bratonien_tools_nc_connections';
$rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` WHERE adapter='remote' ORDER BY id DESC");
if (!$rows) fail_webdav_reconcile('WebDAV-Verbindungen konnten nicht gelesen werden: '.$db->error);
$rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id DESC");
if (!$rows) fail_webdav_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
foreach (array($configDir, $stateRoot, $publicSourceRoot, $publicGalleryRoot) as $dir)
{
@@ -123,6 +127,7 @@ try
$id = (int)$row['id'];
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) $config = array();
if ((string)$row['adapter'] !== 'remote') continue;
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
try
@@ -142,22 +147,27 @@ try
{
fwrite(STDERR, "NC WebDAV #{$id}: identische Quelle bereits durch Verbindung #".$seenFingerprints[$fingerprint]." abgedeckt; doppelte Runtime wird unterdrueckt.\n");
foreach (glob($configDir.'/webdav-connection-'.$id.'.*') ?: array() as $stale) @unlink($stale);
webdav_remove_generated_tree($legacyGalleryRoot.'/bratonien-webdav-'.$id, $legacyGalleryRoot);
webdav_remove_generated_tree($publicGalleryRoot.'/connection-'.$id, $publicGalleryRoot);
continue;
}
$seenFingerprints[$fingerprint] = $id;
$known[$id] = true;
$stateDir = rtrim((string)($config['state_dir'] ?? ''), '/');
$stateDir = $nativeMode ? $stateRoot.'/connection-'.$id : rtrim((string)($config['state_dir'] ?? ''), '/');
if ($stateDir === '') $stateDir = $stateRoot.'/connection-'.$id;
if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_webdav_reconcile('State-Verzeichnis konnte nicht angelegt werden.');
@chmod($stateDir, 0750);
$legacyDefault = $legacyGalleryRoot.'/bratonien-webdav-'.$id;
$galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
$expectedGalleryRoot = $publicGalleryRoot.'/connection-'.$id;
if ($galleryRoot === '' || strpos($galleryRoot, $publicGalleryRoot.'/') !== 0) $galleryRoot = $expectedGalleryRoot;
if ($galleryRoot === '' || $galleryRoot === $legacyDefault || strpos($galleryRoot, $legacyGalleryRoot.'/bratonien-webdav-') === 0)
{
$galleryRoot = $publicGalleryRoot.'/connection-'.$id;
}
if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.');
@chmod($galleryRoot, 0755);
webdav_remove_generated_tree($legacyDefault, $legacyGalleryRoot);
$sourceDir = $publicSourceRoot.'/connection-'.$id;
if (!is_dir($sourceDir) && !mkdir($sourceDir, 0755, true)) fail_webdav_reconcile('WebDAV-Platzhalterquelle konnte nicht angelegt werden.');
@@ -180,8 +190,9 @@ try
{
$path = trim((string)($root['webdav_path'] ?? ''), '/');
$display = trim((string)($root['display_name'] ?? ''));
if ($display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.');
$rootLines[] = $path."\t".$display;
$runtimePath = $path === '' ? '/' : $path;
if ($display === '' || preg_match('/[\t\r\n]/', $runtimePath.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.');
$rootLines[] = $runtimePath."\t".$display;
}
file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX);
@chmod($rootsPath, 0600);
@@ -206,13 +217,13 @@ try
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
@chmod($configPath, 0600);
unset($config['parallel_test']);
unset($config['migration'], $config['parallel_test']);
$config['state_dir'] = $stateDir;
$config['status_file'] = $statusFile;
$config['parallel_gallery_root'] = $galleryRoot;
$config['source_fingerprint'] = $fingerprint;
$config['runtime'] = array(
'mode'=>'webdav',
'mode'=>$nativeMode ? 'piwigo-native-webdav' : 'webdav',
'config'=>$configPath,
'piwigo_sync_enabled'=>true,
'reconciled_at'=>date('Y-m-d H:i:s'),
@@ -220,7 +231,7 @@ try
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json)) fail_webdav_reconcile('WebDAV-Runtime-Konfiguration konnte nicht serialisiert werden.');
$escaped = $db->real_escape_string($json);
if (!$db->query("UPDATE `{$table}` SET enabled=1, config_json='{$escaped}' WHERE id={$id} AND adapter='remote' LIMIT 1"))
if (!$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} AND adapter='remote' LIMIT 1"))
{
fail_webdav_reconcile('WebDAV-Runtime-Status konnte nicht gespeichert werden: '.$db->error);
}

336
runtime/reconcile.php Normal file
View File

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

View File

@@ -1,101 +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);
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

@@ -1,39 +1,194 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_DIR="/etc/bratonien-tools/nc-connector"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
PIWIGO_ROOT_DEFAULT="${BRATONIEN_NC_PIWIGO_ROOT:-$(cd -- "$SCRIPT_DIR/../../.." && pwd)}"
CONFIG_DIR="${BRATONIEN_NC_CONFIG_DIR:-/etc/bratonien-tools/nc-connector}"
NATIVE_MODE="${BRATONIEN_NC_NATIVE:-0}"
TARGET_CONNECTION_ID="${BRATONIEN_NC_CONNECTION_ID:-0}"
GLOBAL_LOCK_DIR="${PIWIGO_ROOT_DEFAULT%/}/_data/bratonien-tools/nc-connector-scheduler"
GLOBAL_LOCK_FILE="$GLOBAL_LOCK_DIR/worker.lock"
mkdir -p -- "$GLOBAL_LOCK_DIR"
exec 8>"$GLOBAL_LOCK_FILE"
if ! flock -n 8; then
echo "NC Connector: ein Lauf ist bereits aktiv."
exit 0
fi
shopt -s nullglob
if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then
echo "NC Connector: WebDAV-Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2
if [[ ! "$TARGET_CONNECTION_ID" =~ ^[0-9]+$ ]]; then
echo "NC Connector: ungültige Ziel-Verbindungs-ID: $TARGET_CONNECTION_ID" >&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
fi
if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then
echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2
exit 1
read_config_value() {
local key="$1"
local file="$2"
local value
value="$(sed -n "s/^${key}=//p" "$file" | tail -n 1)"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
printf '%s' "$value"
}
write_route_status() {
local route="$1"
local label="$2"
local detail="$3"
local success="$4"
[[ -n "${ROUTE_STATUS_FILE:-}" ]] || return 0
mkdir -p -- "$(dirname -- "$ROUTE_STATUS_FILE")"
php -r '
$payload = array(
"timestamp" => time(),
"route" => (string)$argv[2],
"label" => (string)$argv[3],
"detail" => (string)$argv[4],
"fallback_used" => false,
"success" => $argv[5] === "1"
);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if (!is_string($json) || file_put_contents($argv[1], $json.PHP_EOL, LOCK_EX) === false) exit(1);
@chmod($argv[1], 0644);
' "$ROUTE_STATUS_FILE" "$route" "$label" "$detail" "$success"
}
if [[ "$NATIVE_MODE" != "1" ]]; then
php "$SCRIPT_DIR/reconcile.php"
fi
php "$SCRIPT_DIR/reconcile-webdav.php"
php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"
if [[ "$NATIVE_MODE" != "1" ]]; then
php "$SCRIPT_DIR/cleanup-stale.php"
fi
configs=("$CONFIG_DIR"/connection-*.conf)
webdav_configs=("$CONFIG_DIR"/webdav-connection-*.conf)
if [[ ${#webdav_configs[@]} -eq 0 ]]; then
echo "Keine WebDAV-Connector-Verbindungen konfiguriert."
if [[ ${#configs[@]} -eq 0 && ${#webdav_configs[@]} -eq 0 ]]; then
echo "Keine NC-Connector-Verbindungen konfiguriert."
exit 0
fi
result=0
route_piwigo_root=""
for candidate in "${webdav_configs[@]}" "${configs[@]}"; do
[[ -f "$candidate" ]] || continue
candidate_id="$(read_config_value CONNECTION_ID "$candidate")"
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$candidate_id" != "$TARGET_CONNECTION_ID" ]]; then
continue
fi
route_piwigo_root="$(read_config_value PIWIGO_ROOT "$candidate")"
[[ -n "$route_piwigo_root" ]] && break
done
[[ -n "$route_piwigo_root" ]] || route_piwigo_root="$PIWIGO_ROOT_DEFAULT"
ROUTE_STATUS_FILE="${route_piwigo_root%/}/_data/bratonien-tools/nc-connector-status/route-status.json"
failure_count=0
webdav_count=0
local_count=0
summary_parts=()
matched_count=0
for config in "${webdav_configs[@]}"; do
[[ -f "$config" ]] || continue
name="$(basename "$config")"
echo "NC Connector WebDAV: $name"
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh"; then
result=1
connection_id="$(read_config_value CONNECTION_ID "$config")"
if [[ ! "$connection_id" =~ ^[0-9]+$ ]] || [[ "$connection_id" -lt 1 ]]; then
echo "NC Connector: $name besitzt keine gueltige Verbindungs-ID." >&2
failure_count=$((failure_count + 1))
summary_parts+=("$name: ungueltige Verbindungs-ID")
continue
fi
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$connection_id" != "$TARGET_CONNECTION_ID" ]]; then
continue
fi
matched_count=$((matched_count + 1))
webdav_count=$((webdav_count + 1))
echo "NC Connector WebDAV #$connection_id: $name"
output=""
if output="$(env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh" 2>&1)"; then
[[ -z "$output" ]] || printf '%s\n' "$output"
summary_parts+=("WebDAV #$connection_id erfolgreich")
else
code=$?
[[ -z "$output" ]] || printf '%s\n' "$output" >&2
failure_count=$((failure_count + 1))
summary_parts+=("WebDAV #$connection_id fehlgeschlagen (Exit $code)")
fi
done
exit "$result"
if [[ "$NATIVE_MODE" != "1" ]]; then
for config in "${configs[@]}"; do
[[ -f "$config" ]] || continue
name="$(basename "$config")"
connection_id="0"
if [[ "$name" =~ ^connection-([0-9]+)\.conf$ ]]; then
connection_id="${BASH_REMATCH[1]}"
fi
if [[ "$connection_id" -lt 1 ]]; then
echo "NC Connector: $name besitzt keine gueltige Verbindungs-ID." >&2
failure_count=$((failure_count + 1))
summary_parts+=("$name: ungueltige Verbindungs-ID")
continue
fi
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$connection_id" != "$TARGET_CONNECTION_ID" ]]; then
continue
fi
matched_count=$((matched_count + 1))
piwigo_root="$(read_config_value PIWIGO_ROOT "$config")"
[[ -n "$piwigo_root" ]] || piwigo_root="$PIWIGO_ROOT_DEFAULT"
tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status"
if [[ -f "$tombstone_dir/deleted-$connection_id" ]]; then
echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt."
rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \
"$CONFIG_DIR/connection-$connection_id.db-password" \
"$CONFIG_DIR/connection-$connection_id.piwigo-password" \
"$CONFIG_DIR/connection-$connection_id.storages.tsv" \
"$CONFIG_DIR/connection-$connection_id.roots.tsv"
rm -f -- "$tombstone_dir/deleted-$connection_id"
continue
fi
local_count=$((local_count + 1))
echo "NC Connector Local #$connection_id: $name"
if env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
summary_parts+=("Local #$connection_id erfolgreich")
else
failure_count=$((failure_count + 1))
summary_parts+=("Local #$connection_id fehlgeschlagen")
fi
done
fi
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$matched_count" -eq 0 ]]; then
write_route_status "failed" "FEHLER - Verbindung #$TARGET_CONNECTION_ID" "Keine Laufzeitkonfiguration für Verbindung #$TARGET_CONNECTION_ID gefunden." "0"
echo "NC Connector: keine Laufzeitkonfiguration für Verbindung #$TARGET_CONNECTION_ID gefunden." >&2
exit 1
fi
summary_detail="$(IFS='; '; printf '%s' "${summary_parts[*]}")"
[[ -n "$summary_detail" ]] || summary_detail="Keine Verbindung wurde ausgefuehrt."
if [[ "$failure_count" -eq 0 ]]; then
if [[ "$webdav_count" -gt 0 && "$local_count" -gt 0 ]]; then
route="mixed"
label="WebDAV + Local"
elif [[ "$webdav_count" -gt 0 ]]; then
route="webdav"
label="WebDAV"
else
route="local"
label="Local"
fi
write_route_status "$route" "$label" "$summary_detail" "1"
echo "NC Connector: angeforderte Verbindung wurde erfolgreich verarbeitet."
exit 0
fi
write_route_status "failed" "FEHLER - angeforderte Verbindung" "$summary_detail" "0"
echo "NC Connector: die angeforderte Verbindung ist fehlgeschlagen." >&2
exit 1

View File

@@ -59,10 +59,6 @@ for target in (status_file, public_file):
PY
}
compact_output() {
tail -n 12 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
}
failure() {
local code="$1" command="$2" line="$3"
trap - ERR
@@ -72,52 +68,64 @@ failure() {
trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR
ROOT_ARGS=()
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
[[ "$line" == *$'\t'* ]] || continue
path="${line%%$'\t'*}"
while IFS=$'\t' read -r path display_name; do
[[ -z "$path" || "$path" == \#* ]] && continue
ROOT_ARGS+=(--root "$path")
done < "$WEBDAV_ROOTS_FILE"
[[ ${#ROOT_ARGS[@]} -gt 0 ]] || { write_status error "Keine WebDAV-Wurzeln konfiguriert"; exit 1; }
python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
WEBDAV_CONNECT_IP="$(php "$SCRIPT_DIR/lib/resolve-nextcloud-target.php" "$WEBDAV_BASE_URL")"
[[ -n "$WEBDAV_CONNECT_IP" ]] || { write_status error "Nextcloud-Zieladresse konnte nicht ermittelt werden"; exit 1; }
PLACEHOLDER_OUTPUT=""
PLACEHOLDER_EXIT=0
if PLACEHOLDER_OUTPUT="$(python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
--base-url "$WEBDAV_BASE_URL" \
--connect-ip "$WEBDAV_CONNECT_IP" \
--user "$WEBDAV_USER" \
--password-file "$WEBDAV_PASSWORD_FILE" \
"${ROOT_ARGS[@]}" \
--source-dir "$WEBDAV_SOURCE_DIR" \
--manifest "$MANIFEST" \
--mapping "$WEBDAV_MAPPING_FILE"
--mapping "$WEBDAV_MAPPING_FILE" 2>&1)"; then
PLACEHOLDER_EXIT=0
else
PLACEHOLDER_EXIT=$?
fi
[[ -z "$PLACEHOLDER_OUTPUT" ]] || printf '%s\n' "$PLACEHOLDER_OUTPUT"
if [[ "$PLACEHOLDER_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PLACEHOLDER_EXIT"
if [[ -n "$PLACEHOLDER_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PLACEHOLDER_OUTPUT" | tail -n 20 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//')"
fi
trap - ERR
write_status error "WebDAV-Shadow-Tree fehlgeschlagen" "$DETAIL"
exit "$PLACEHOLDER_EXIT"
fi
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
SHADOW_OUTPUT=""
SHADOW_EXIT=0
if SHADOW_OUTPUT="$(python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--manifest "$MANIFEST" \
--destination "$GALLERY_ROOT" \
--state "$SHADOW_MAP_FILE"
--state "$SHADOW_MAP_FILE" 2>&1)"; then
SHADOW_EXIT=0
else
SHADOW_EXIT=$?
fi
[[ -z "$SHADOW_OUTPUT" ]] || printf '%s\n' "$SHADOW_OUTPUT"
if [[ "$SHADOW_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $SHADOW_EXIT"
if [[ -n "$SHADOW_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$SHADOW_OUTPUT" | tail -n 20 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//')"
fi
trap - ERR
write_status error "WebDAV-Shadow-Tree fehlgeschlagen" "$DETAIL"
exit "$SHADOW_EXIT"
fi
trap - ERR
PREVIEW_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-preview/connection-$CONNECTION_ID"
PREVIEW_OUTPUT=""
PREVIEW_EXIT=0
if PREVIEW_OUTPUT="$(php "$SCRIPT_DIR/lib/precache-webdav-previews.php" \
--mapping="$WEBDAV_MAPPING_FILE" \
--base-url="$WEBDAV_BASE_URL" \
--user="$WEBDAV_USER" \
--password-file="$WEBDAV_PASSWORD_FILE" \
--cache-dir="$PREVIEW_CACHE" 2>&1)"; then
PREVIEW_EXIT=0
else
PREVIEW_EXIT=$?
fi
[[ -z "$PREVIEW_OUTPUT" ]] || printf '%s\n' "$PREVIEW_OUTPUT"
if [[ "$PREVIEW_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PREVIEW_EXIT"
if [[ -n "$PREVIEW_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PREVIEW_OUTPUT" | compact_output)"
fi
write_status error "WebDAV-Vorschaubilder konnten beim Einlesen nicht erzeugt werden" "$DETAIL"
exit "$PREVIEW_EXIT"
fi
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
PIWIGO_OUTPUT=""
@@ -135,7 +143,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PIWIGO_EXIT"
if [[ -n "$PIWIGO_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | compact_output)"
DETAIL+="; Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | tail -n 12 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//')"
fi
if grep -qi 'Invalid username/password' <<<"$PIWIGO_OUTPUT"; then
@@ -162,28 +170,9 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
exit "$PIWIGO_EXIT"
fi
DERIVATIVE_OUTPUT=""
DERIVATIVE_EXIT=0
if DERIVATIVE_OUTPUT="$(php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID" 2>&1)"; then
DERIVATIVE_EXIT=0
else
DERIVATIVE_EXIT=$?
fi
[[ -z "$DERIVATIVE_OUTPUT" ]] || printf '%s\n' "$DERIVATIVE_OUTPUT"
if [[ "$DERIVATIVE_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $DERIVATIVE_EXIT"
if [[ -n "$DERIVATIVE_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$DERIVATIVE_OUTPUT" | compact_output)"
fi
write_status error "Piwigo-Derivate für WebDAV-Bilder konnten nicht erzeugt werden" "$DETAIL"
exit "$DERIVATIVE_EXIT"
fi
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \
"WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" \
"WebDAV eingelesen und Piwigo synchronisiert" \
"" \
"api" \
"ok" \
@@ -192,7 +181,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"Fallback wurde nicht benötigt"
elif grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \
"WebDAV eingelesen, Piwigo über Fallback synchronisiert und Derivate erzeugt" \
"WebDAV eingelesen und Piwigo über Fallback synchronisiert" \
"" \
"fallback" \
"not_used" \
@@ -200,8 +189,8 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"ok" \
"Benutzername/Passwort-Fallback erfolgreich"
else
write_status ok "WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt"
write_status ok "WebDAV eingelesen und Piwigo synchronisiert"
fi
else
write_status ok "WebDAV eingelesen und Vorschaubilder erzeugt; Registrierung erfolgt über den bestehenden produktiven Piwigo-Sync"
write_status ok "WebDAV eingelesen; Piwigo-Synchronisierung ist für diese Verbindung deaktiviert"
fi

270
runtime/sync.sh Normal file
View File

@@ -0,0 +1,270 @@
#!/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

@@ -48,12 +48,27 @@
var openButton=document.getElementById('bratonien-nc-wizard-open');
var closeButton=document.getElementById('bratonien-nc-wizard-close');
var storageKey='bratonienNcWizardOpen';
var modeKey='bratonienNcWizardMode';
var resetBusy=false;
function token(){var input=section.querySelector('input[name="pwg_token"]');return input?input.value:'';}
function setOpen(value){try{if(value)sessionStorage.setItem(storageKey,'1');else sessionStorage.removeItem(storageKey);}catch(e){}}
function setMode(value){try{if(value)sessionStorage.setItem(modeKey,value);else sessionStorage.removeItem(modeKey);}catch(e){}}
function mode(){try{return sessionStorage.getItem(modeKey)||'';}catch(e){return '';}}
function applyMode(){
if(!dialog)return;
var heading=dialog.querySelector('h4');
var finish=dialog.querySelector('button[value="nc_connector_wizard_finish"]');
if(mode()==='edit'){
if(heading)heading.textContent='Verbindung bearbeiten';
if(finish)finish.textContent='Änderungen speichern';
}else{
if(heading)heading.textContent='Neue Verbindung';
if(finish)finish.textContent='Verbindung anlegen';
}
}
function showWizard(){
setOpen(true);
setOpen(true);applyMode();
if(!dialog)return;
if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();
else dialog.setAttribute('open','open');
@@ -65,13 +80,13 @@
return fetch(window.location.href,{method:'POST',credentials:'same-origin',cache:'no-store',headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:body.toString()});
}
function closeAfterReset(){
if(resetBusy)return;resetBusy=true;setOpen(false);
if(resetBusy)return;resetBusy=true;setOpen(false);setMode('');
resetServer().catch(function(){}).finally(function(){resetBusy=false;if(dialog){if(typeof dialog.close==='function'&&dialog.open)dialog.close();else dialog.removeAttribute('open');}});
}
if(openButton){
openButton.addEventListener('click',function(event){
event.preventDefault();event.stopImmediatePropagation();showWizard();
event.preventDefault();event.stopImmediatePropagation();setMode('');showWizard();
},true);
}
if(closeButton){
@@ -81,14 +96,29 @@
dialog.addEventListener('cancel',function(event){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();},true);
dialog.addEventListener('click',function(event){if(event.target===dialog){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();}},true);
// Every wizard POST explicitly preserves the open state before the
// browser leaves the page. Validation errors therefore return to the
// same wizard step instead of closing/resetting the dialog.
[].slice.call(dialog.querySelectorAll('form[data-bratonien-wizard-form]')).forEach(function(form){
form.addEventListener('submit',function(){setOpen(true);},true);
form.addEventListener('submit',function(event){
var submitter=event.submitter;
setOpen(true);
if(submitter&&submitter.hasAttribute('data-bratonien-wizard-end')){
setOpen(false);
if(submitter.value==='nc_connector_wizard_reset'||submitter.value==='nc_connector_wizard_finish')setMode('');
}
},true);
});
[].slice.call(section.querySelectorAll('form')).forEach(function(form){
var editButton=form.querySelector('button[value="nc_connector_edit_start"]');
if(!editButton)return;
form.addEventListener('submit',function(event){
if(event.submitter&&event.submitter.value==='nc_connector_edit_start'){
setMode('edit');setOpen(true);
}
},true);
});
try{if(sessionStorage.getItem(storageKey)==='1')showWizard();}catch(e){}
applyMode();
}
}

View File

@@ -1,6 +1,6 @@
<section class="bratonien-section" id="nc-connector">
<h3>NC Connector</h3>
<p class="bratonien-section__intro">Verbindet Nextcloud per WebDAV mit Piwigo und hält die ausgewählten Bilder automatisch aktuell.</p>
<p class="bratonien-section__intro">Verbindet Nextcloud mit Piwigo und hält freigegebene Bilder automatisch aktuell.</p>
{assign var=nc_system_available value=isset($NC_CONNECTOR.system)}
@@ -8,16 +8,12 @@
<div class="bratonien-card">
<h4>Status</h4>
<div class="bratonien-form-grid">
<span class="bratonien-label">WebDAV-Verbindungen</span><strong>{$NC_CONNECTOR.connection_count|escape:html}</strong>
<span class="bratonien-label">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">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>
</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}
<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_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}
@@ -25,9 +21,10 @@
<div class="bratonien-card">
<h4>Neue Verbindung</h4>
<p class="bratonien-base-note">Nextcloud wird ausschließlich per WebDAV angebunden.</p>
<p class="bratonien-base-note">Für die normale Einrichtung empfehlen wir den Assistenten.</p>
<div class="bratonien-actions">
<button class="buttonLike" type="button" id="bratonien-nc-wizard-open">WebDAV-Verbindung anlegen</button>
<button class="buttonLike" type="button" id="bratonien-nc-wizard-open">Mit Assistent anlegen</button>
<button class="buttonLike" type="button" id="bratonien-nc-technical-open">Ohne Assistent anlegen</button>
</div>
</div>
</div>
@@ -36,21 +33,32 @@
<div style="padding:1.25rem 1.5rem">
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem">
<div>
<h4 style="margin:0">Neue WebDAV-Verbindung</h4>
<h4 style="margin:0">Neue Verbindung</h4>
<p class="bratonien-base-note" style="margin:.35rem 0 0"><strong>
{if $NC_CONNECTOR.wizard.step == 1}Anmeldung
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts'}Verzeichnisse auswählen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_complete}Verbindung benennen
{elseif $NC_CONNECTOR.wizard.step == 3}Piwigo-API
{elseif $NC_CONNECTOR.wizard.step == 4}Abschluss
{else}Einrichtung{/if}
{if $NC_CONNECTOR.wizard.step == 1}
Anmeldung
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
Datenbank prüfen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts' && !$NC_CONNECTOR.wizard.directory_selection_ready}
Speicher zuordnen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_stage == 'mounts' && $NC_CONNECTOR.wizard.directory_selection_ready}
Verzeichnisse auswählen
{elseif $NC_CONNECTOR.wizard.step == 2 && $NC_CONNECTOR.wizard.technical_complete}
Verbindung benennen
{elseif $NC_CONNECTOR.wizard.step == 3}
Piwigo-API
{elseif $NC_CONNECTOR.wizard.step == 4}
Abschluss
{else}
Einrichtung
{/if}
</strong></p>
</div>
<button class="buttonLike" type="button" id="bratonien-nc-wizard-close">Schließen</button>
</div>
{if $NC_CONNECTOR.wizard.step == 1}
<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>
<p class="bratonien-base-note">Adresse und Zugang reichen für den ersten Scan. Der Assistent prüft den erreichbaren Web-Zugang automatisch.</p>
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
@@ -65,70 +73,113 @@
</form>
{elseif $NC_CONNECTOR.wizard.step == 2}
<p class="bratonien-base-note"><strong>Nextcloud wurde gefunden.</strong> Angezeigt werden ausschließlich Verzeichnisse des angemeldeten Nextcloud-Benutzers.</p>
<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>
<div class="bratonien-form-grid">
<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">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>
{if $NC_CONNECTOR.wizard.technical_stage == 'mounts' && $NC_CONNECTOR.wizard.directory_selection_ready}
{if $NC_CONNECTOR.wizard.technical_stage == 'database_details'}
<hr>
<h5>Verzeichnisse auswählen</h5>
<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>
<h5>Datenbank-Adresse prüfen</h5>
<p class="bratonien-base-note">Die bekannte Reader-Verbindung konnte mit der gespeicherten Adresse nicht bestätigt werden.</p>
{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}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
<label class="bratonien-label">Datenbank-Adresse</label><input name="nc_wizard_db_host" type="text" value="{$NC_CONNECTOR.wizard.db_host|escape:html}" required>
<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="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>
<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}
{else}
<span class="bratonien-base-note">Noch kein Verzeichnis ausgewählt.</span>
<p class="bratonien-base-note">Keine Unterordner vorhanden.</p>
{/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="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 from=$NC_CONNECTOR.wizard.storage_candidates item=storage key=storage_index}
<input type="hidden" name="nc_wizard_storage_mount[{$storage_index|escape:html}]" value="{$storage.local_mount|escape:html}">
{/foreach}
{else}
<p class="bratonien-base-note">Keine Unterordner vorhanden.</p>
{/if}
</div>
<p><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_wizard_save_mounts">Verzeichnisse übernehmen</button></p>
</form>
{else}
<h5>Speicherort bestätigen</h5>
<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}
<hr>
<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>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<div class="bratonien-form-grid">
@@ -145,20 +196,20 @@
</div>
{elseif $NC_CONNECTOR.wizard.step == 3}
<p class="bratonien-base-note"><strong>WebDAV ist vorbereitet.</strong> Jetzt wird der Piwigo-Zugang dieser Verbindung geprüft.</p>
<p class="bratonien-base-note"><strong>Nextcloud ist vorbereitet.</strong> Jetzt wird der bevorzugte Piwigo-Zugang 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}
<form method="post" data-bratonien-wizard-form>
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<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}">
<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}">
<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_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">
</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 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}
<p class="bratonien-base-note"><strong>Fast fertig.</strong> Erst der letzte Button legt die WebDAV-Verbindung an.</p>
<p class="bratonien-base-note"><strong>Fast fertig.</strong> Die Verbindung wurde noch nicht angelegt. Erst der letzte Button übernimmt die Einstellungen.</p>
<div class="bratonien-form-grid">
<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>
@@ -181,16 +232,44 @@
</div>
</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">
<h4>Bestehende WebDAV-Verbindungen</h4>
<h4>Bestehende Verbindungen</h4>
{if $NC_CONNECTOR.connection_count > 0}
{foreach from=$NC_CONNECTOR.connections item=connection}
<details style="margin:.6rem 0">
<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>
<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>
<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>
<div class="bratonien-actions" style="margin-top:.6rem">
<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>
{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('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>
{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>
@@ -202,21 +281,49 @@
{else}
<p class="bratonien-base-note"><strong>Laufzeit:</strong> Noch kein Laufstatus für diese Verbindung vorhanden.</p>
{/if}
<p class="bratonien-base-note"><strong>Nextcloud:</strong> {$connection.config.nextcloud_url|escape:html}</p>
<p class="bratonien-base-note"><strong>Nextcloud-Benutzer:</strong> {$connection.user|escape:html}</p>
<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> {if $connection.config.api_enabled}eigene API{elseif $connection.fallback_stored}Fallback{else}kein Zugang gespeichert{/if}</p>
{if isset($connection.config.api_enabled)}
<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>
{else}
<p class="bratonien-base-note"><strong>Piwigo-Zugang:</strong> bestehende Legacy-Konfiguration</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>
</details>
{/foreach}
{else}<p class="bratonien-base-note">Noch keine WebDAV-Verbindung vorhanden.</p>{/if}
{else}<p class="bratonien-base-note">Noch keine Verbindung vorhanden.</p>{/if}
</div>
{if $NC_CONNECTOR.connection_count > 0}
<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">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>
<details class="bratonien-card" style="margin-top:1.5rem"><summary>Erweiterte Piwigo-Zugänge</summary>
<h4>Piwigo API</h4>
<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>
<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>
{/if}
{literal}
<script>
@@ -224,6 +331,8 @@
var dialog=document.getElementById('bratonien-nc-wizard-dialog');
var openButton=document.getElementById('bratonien-nc-wizard-open');
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';
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');}
@@ -235,7 +344,8 @@
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){}
}
if(technicalButton&&technical)technicalButton.addEventListener('click',function(){technical.open=!technical.open;if(technical.open)technical.scrollIntoView(true);});
})();
</script>
{/literal}
</section>
</section>

View File

@@ -1,183 +0,0 @@
<?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);

View File

@@ -1,494 +0,0 @@
<?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();
}

View File

@@ -1,368 +0,0 @@
<?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();
}

View File

@@ -1,296 +0,0 @@
<?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,7 +8,7 @@ if (!defined('BRATONIEN_TOOLS_PATH'))
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_transport.inc.php');
function bratonien_tools_webdav_image_abort($status, $message)
{
@@ -49,9 +49,21 @@ $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');
if (!pwg_db_num_rows($access_result)) bratonien_tools_webdav_image_abort(403, 'Kein Zugriff auf dieses Bild.');
$source = bratonien_tools_webdav_materialize_source_info($image_id);
$source = bratonien_tools_webdav_image_source_info($image_id);
if (!$source) bratonien_tools_webdav_image_abort(404, 'Keine WebDAV-Quelle für dieses Bild gefunden.');
if (!empty($_GET['ajaxload']))
{
$preview = !empty($_GET['preview']);
$final_url = bratonien_tools_webdav_image_url($image_id, $preview);
if (!$final_url) bratonien_tools_webdav_image_abort(404, 'Keine WebDAV-Bild-URL verfügbar.');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
echo json_encode(array('url'=>$final_url), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
if (!empty($_GET['preview']))
{
$preview = bratonien_tools_webdav_preview_path($source);
@@ -121,7 +133,7 @@ $options = array(
CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.6.8.6',
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.7.24',
CURLOPT_HEADERFUNCTION => function($ch, $line)
{
$length = strlen($line);
@@ -148,6 +160,7 @@ $options = array(
return strlen($data);
},
);
bratonien_tools_nc_transport_apply_curl($options, $url);
if (!empty($_SERVER['HTTP_RANGE']))
{
$range = trim((string)$_SERVER['HTTP_RANGE']);