Compare commits

...

180 Commits

Author SHA1 Message Date
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
Terranom674
2c7cd9e780 Align WebDAV image stream version with 0.9.6.1 2026-08-19 00:13:11 +02:00
Terranom674
6155955eee Align WebDAV preview runtime version with 0.9.6.1 2026-08-19 00:12:39 +02:00
Terranom674
5179c1cc33 Align derivative builder version with 0.9.6.1 2026-08-19 00:12:08 +02:00
Terranom674
facb270b13 Refresh README for 0.9.6.1 2026-08-19 00:11:53 +02:00
Terranom674
b4f56ac04b Update current status for 0.9.6.1 2026-08-19 00:11:27 +02:00
Terranom674
16571f9322 Bump version to 0.9.6.1 2026-08-18 23:46:33 +02:00
Terranom674
d7fd99eb7c Clean stale WebDAV Piwigo content in runner 2026-08-18 23:46:16 +02:00
Terranom674
207c2ae502 Clean stale WebDAV Piwigo sites after connection deletion 2026-08-18 23:46:03 +02:00
Terranom674
4d04f115e1 Remove WebDAV Piwigo content when deleting connection 2026-08-18 23:45:42 +02:00
Terranom674
ed9088a8e2 Keep WebDAV precache on 0.9.5.x 2026-08-18 23:38:22 +02:00
Terranom674
8c7476a5d2 Keep derivative builder on 0.9.5.x 2026-08-18 23:37:54 +02:00
Terranom674
bc9d378bfd Restore 0.9.5.x version line 2026-08-18 23:37:35 +02:00
Terranom674
3da61079f2 Release 0.9.6.0 WebDAV derivative rebuild 2026-08-18 23:33:16 +02:00
Terranom674
f380ee4aec Serve WebDAV previews with their real MIME type 2026-08-18 23:32:01 +02:00
Terranom674
96dda18fc9 Build WebDAV derivatives with validated Piwigo image sources 2026-08-18 23:31:26 +02:00
Terranom674
3767620867 Make WebDAV derivatives safe and Piwigo-compatible 2026-08-18 23:30:56 +02:00
Terranom674
d18e7108dd Rebuild WebDAV previews in Piwigo-compatible formats 2026-08-18 23:30:22 +02:00
Terranom674
450f9a0ac5 Bump version to 0.9.5.22 2026-08-18 23:17:53 +02:00
Terranom674
47182a86a2 Hotfix: never build WebDAV derivatives during page rendering 2026-08-18 23:16:43 +02:00
Terranom674
907fba8c8f Expose WebDAV derivative builder runtime version 2026-08-18 23:15:24 +02:00
38 changed files with 3742 additions and 1235 deletions

View File

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

View File

@@ -1,187 +1,139 @@
# Aktueller Entwicklungsstand # Aktueller Entwicklungsstand
Stand: 18.08.2026 Stand: 19.08.2026
## Plugin ## Plugin
- Aktuelle Plugin-Version: **0.9.5.7** - Aktuelle Plugin-Version: **0.9.6.1**
- Aktueller Entwicklungsblock: **NC Connector WebDAV-basierter Parallelweg bei vollständigem Erhalt des bestehenden produktiven Wegs** - Aktueller Entwicklungsblock: **NC Connector WebDAV-End-to-End-Pfad mit Piwigo-Registrierung, Preview-/Derivatstrecke und sauberem Verbindungs-Lebenszyklus**
- GitHub ist das führende Repository. - GitHub ist das führende Repository.
- Das private Gitea-System bleibt Mirror/Fallback. - Der bestehende lokale Connector bleibt vollständig erhalten und wird nicht automatisch migriert.
## Nicht verhandelbare Migrationsregel ## Nicht verhandelbare Architekturregeln
Der bestehende lokale NC Connector bleibt vollständig erhalten, bis der neue WebDAV-Weg End-to-End funktioniert. - 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.
Bestehende Quellenmodi: ## Bestehende Quellenmodi
Weiterhin vorhanden:
- `legacy-view` - `legacy-view`
- `user-shares` - `user-shares`
- `selected-fileids` - `selected-fileids`
Sie werden nicht automatisch migriert, deaktiviert oder auf den neuen Weg umgestellt. Neuer WebDAV-Modus:
Der neue Weg entsteht zusätzlich als eigener Modus:
- `webdav-placeholder` - `webdav-placeholder`
## Ziel des neuen Wegs Es gibt keine automatische Migration bestehender Verbindungen auf WebDAV.
Benötigt werden sollen nur: ## WebDAV-Ablauf in 0.9.6.1
- normale Piwigo-Installation; 1. Der Assistent prüft Nextcloud über WebDAV und lässt sichtbare Verzeichnisse auswählen.
- bereits vorhandene Linux-/PHP-Umgebung; 2. `runtime/reconcile-webdav.php` erzeugt die verbindungseigene Runtime-Konfiguration.
- Nextcloud-Adresse; 3. `runtime/lib/build_webdav_placeholder_source.py` liest die ausgewählten Verzeichnisse rekursiv per PROPFIND.
- Nextcloud-Benutzer bzw. App-Passwort; 4. Ordner, Dateinamen, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
- WebDAV-Zugriff auf die Inhalte dieses Benutzers. 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.
Nicht vorausgesetzt werden dürfen: ## Piwigo-Synchronisierung
- PostgreSQL-Zugriff auf Nextcloud; WebDAV verwendet denselben Piwigo-Synchronisationspfad wie der bestehende Connector, aber mit einer verbindungseigenen physischen Site.
- `occ`-Adminzugriff;
- Rootzugriff des Betreibers;
- Storage-IDs oder Backend-Pfade;
- zusätzliche Host-Mounts;
- FUSE;
- davfs;
- rclone;
- zusätzliche Connector-Systempakete.
Originalbilder werden nicht dauerhaft nach Piwigo kopiert. Nach der Dateisynchronisierung laufen die vorhandenen Piwigo-Nacharbeiten, darunter Metadaten-, Integritäts-, Kategorie-, Pfad-, Rang- und Cache-Aktualisierungen sowie der Orphan-Abgleich.
## Architektur des WebDAV-Parallelwegs Die direkte produktive Synchronisierung ist derzeit ausdrücklich auf **Piwigo 16.4.0** abgestimmt.
1. Ausgewählte Nextcloud-Verzeichnisse werden über WebDAV/PROPFIND gelesen. ## WebDAV-Bildausgabe und Derivate
2. Das Plugin erfasst Ordner, Dateinamen, Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad.
3. Für jedes Bild wird nur ein winziger lokaler Platzhalter bereitgestellt.
4. Der bestehende Shadow Tree bleibt die physische Piwigo-Quelle.
5. Der Shadow Tree bildet die reale Ordner- und Dateinamensstruktur ab, seine Bildziele zeigen jedoch auf Platzhalter statt auf Originale.
6. Ein separates Mapping verbindet Shadow-Tree-Pfad, Connection-ID, Nextcloud-Datei-ID und WebDAV-Pfad.
7. Piwigo soll Album und Bild über diese Struktur registrieren.
8. Wenn echte Bilddaten benötigt werden, wird das Original bei Bedarf über WebDAV gelesen.
9. Piwigo-Derivate werden normal lokal unter `_data/i/` gecacht.
10. Das Original bleibt ausschließlich in Nextcloud.
## Bereits vorhandene Bausteine Wichtige Dateien:
### `runtime/lib/build_webdav_placeholder_source.py` - `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.
- liest WebDAV rekursiv; Der Frontend-Hook führt keine Bildgenerierung mehr aus. Damit bleibt ein Fehler in GD/Imagick/External ImageMagick auf den Hintergrundlauf begrenzt.
- lädt keine Originalbilder herunter;
- erzeugt eine lokale Platzhalterquelle;
- erzeugt Manifest und WebDAV-Mapping;
- verwendet einen nur wenige Dutzend Byte großen Platzhalter.
### 0.9.5.6 Verbindungsschicht ## Löschen einer WebDAV-Verbindung seit 0.9.6.1
Der Parallelweg wurde als eigener Connection-Typ im Plugin angelegt. Eine gelöschte WebDAV-Verbindung darf keine weiterhin sichtbaren oder über alte Piwigo-Datensätze erreichbaren Bilder hinterlassen.
- `include/nc_connector_webdav.inc.php` Beim Löschen über die Oberfläche:
- Backend-Aktion `nc_connector_create_webdav_parallel`
- `source_mode=webdav-placeholder`
- `adapter=remote`
- ausgewählte WebDAV-Wurzeln werden verbindungseigen gespeichert;
- Nextcloud-Basis-URL und Benutzer werden verbindungseigen gespeichert;
- Secret v3 enthält `nextcloud_user` und `nextcloud_password` rückwärtskompatibel zu älteren Secrets.
## 0.9.5.7 parallele Runtime angelegt - 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.
Der neue Weg besitzt jetzt eine eigene Runtime neben dem bestehenden produktiven Connector. 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.
Neu: ## Gemeinsamer Runner
- `runtime/reconcile-webdav.php` `runtime/run-all.sh` verarbeitet die Schritte in kontrollierter Reihenfolge:
- `runtime/sync-webdav.sh`
- `runtime/run-all.sh` verarbeitet lokale und WebDAV-Verbindungen getrennt.
Wesentliche Regeln: 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.
- `runtime/reconcile.php` für die bestehenden lokalen Modi bleibt unverändert. Der vorhandene systemd-Timer ruft direkt die Runtime aus dem installierten Plugin auf.
- `runtime/sync.sh` für die bestehenden lokalen Modi bleibt unverändert.
- WebDAV-Verbindungen erhalten eigene Runtime-Dateien `webdav-connection-ID.*`.
- Es werden keine PostgreSQL-/View-/Storage-Mapping-Werte für WebDAV verlangt.
- Nextcloud-Zugang wird aus dem verschlüsselten Connection-Secret in eine Runtime-Passwortdatei mit restriktiven Rechten geschrieben.
- WebDAV-Wurzeln werden getrennt als Runtime-Konfiguration gespeichert.
- Der Platzhalter-Builder erzeugt die lokale Platzhalterquelle, Manifest und WebDAV-Mapping.
- Das Manifest wird anschließend durch den bestehenden `shadow_tree.py` verarbeitet.
- Der parallele Shadow Tree liegt absichtlich unter `galleries/bratonien-webdav-ID` und kann deshalb den bestehenden Galeriebaum nicht ersetzen oder überschreiben.
- Die Platzhalterquelle liegt unter Piwigo `_data/bratonien-tools/nc-webdav-source/connection-ID`, damit Piwigo den Symlink-Zielen später folgen kann.
- **Piwigo-Synchronisierung ist in dieser Stufe hart auf `PIWIGO_SYNC_ENABLED=0` gesetzt.** Die Runtime darf aktuell nur den parallelen Shadow Tree bauen. Die Registrierung in Piwigo wird erst nach Sichtprüfung bewusst freigeschaltet.
Damit existieren lokaler Produktivweg und WebDAV-Testweg jetzt gleichzeitig, ohne dass ein Umzug stattgefunden hat. ## Status- und UI-Regeln
## Bestehender produktiver Weg - 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.
Unverändert: ## Sicherheit
- PostgreSQL-/View-/Storage-Mapping-Logik; - Connector-Zugangsdaten werden verschlüsselt gespeichert.
- lokale Symlinks auf bereits vorhandene Originalpfade; - WebDAV-Credentials werden nur serverseitig verwendet.
- `runtime/reconcile.php` für bestehende lokale Adapter; - Bildzugriffe über `webdav-image.php` werden gegen Piwigos Zugriffsrechte geprüft.
- `runtime/sync.sh` mit `legacy-view`, `user-shares` und `selected-fileids`; - Nextcloud-Originale werden weder beim Sync noch beim Löschen einer Verbindung verändert.
- API-first-Piwigo-Sync; - Derivate und Arbeitsbilder sind lokale Cache-/Arbeitsdaten und dürfen entfernt beziehungsweise neu aufgebaut werden.
- gemeinsame Runtime für aktive bestehende Verbindungen.
## Gemessene WebDAV-Performance ## Aktueller Prüfstand
Testbild: 16.091.204 Byte. Bereits umgesetzt:
Intern: - WebDAV-Verbindungsassistent;
- Verzeichnisauswahl;
- 1,829 s - rekursiver WebDAV-Scan;
- 8.796.915 Byte/s
Extern:
- 1,865 s
- 8.627.524 Byte/s
Die externe Verbindung war im Test nur ungefähr 2 % langsamer. Der Ansatz bleibt deshalb für bedarfsweisen Originalzugriff geeignet, solange Piwigo-Derivate lokal gecacht werden.
## Löschverhalten seit 0.9.5.5
Das Löschen einer Connector-Verbindung darf nicht an Dateirechten Root-eigener Runtime-Dateien scheitern.
- Datenbankeintrag wird entfernt;
- verwaiste Runtime-Dateien werden vor einem späteren Sync bereinigt;
- andere Verbindungen bleiben unangetastet;
- Nextcloud-Originale und vorhandene Piwigo-Bilder werden nicht gelöscht.
## Nächster Testschritt
Die parallele Runtime ist jetzt vorbereitet. Der nächste Schritt ist bewusst klein und kontrolliert:
1. Plugin auf 0.9.5.7 aktualisieren.
2. Eine kleine neue WebDAV-Testverbindung mit einem überschaubaren Verzeichnis anlegen.
3. Gemeinsamen Runner einmal ausführen lassen.
4. Prüfen, ob unter `galleries/bratonien-webdav-ID` ausschließlich die erwartete Ordnerstruktur und Platzhalter-Symlinks entstehen.
5. WebDAV-Mapping und Manifest im Connection-State prüfen.
6. Bestehende produktive Galerie und bestehende Verbindungen dabei auf Unverändertheit prüfen.
Erst wenn dieser Test sauber ist, wird `PIWIGO_SYNC_ENABLED` für WebDAV separat freigeschaltet und die Piwigo-Registrierung getestet.
## Danach noch nicht umsetzen
Erst wenn die Registrierung mit Platzhaltern funktioniert:
- echte Bilddaten über WebDAV anfordern;
- Piwigo-Derivate aus echten Originalen erzeugen;
- normalen Derivat-Cache weiterverwenden;
- ETag-basierte Änderungserkennung ergänzen;
- Fehlerfälle und Parallelität testen.
## Voraussetzung vor irgendeinem Umzug
Erfolgreich getestet sein müssen mindestens:
- Verbindungsanlage;
- WebDAV-Verzeichnisauswahl;
- Shadow Tree; - Shadow Tree;
- Piwigo-Registrierung; - separate Piwigo-Site je WebDAV-Verbindung;
- echte Bildausgabe; - sichtbare Albumstruktur ohne technische Wrapper;
- Derivat-Cache; - echte Bildausgabe aus Nextcloud;
- Änderungserkennung; - lokale vorbereitete Arbeitsbilder;
- Löschen/Deaktivieren; - CLI-Derivatstrecke;
- Fehlerbehandlung; - Piwigo-Nacharbeiten und Orphan-Abgleich;
- Verhalten bei vielen noch ungecachten Bildern. - ETag-basierte Wiederverwendung vorbereiteter Bilder;
- Löschen einer Verbindung einschließlich Piwigo-Inhalten;
- nachträgliche Bereinigung bereits verwaister WebDAV-Piwigo-Sites.
Erst danach kann eine freiwillige Migration bestehender Verbindungen überhaupt diskutiert werden. 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.

263
README.md
View File

@@ -2,7 +2,7 @@
Modulares Piwigo-Plugin für Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo. Modulares Piwigo-Plugin für Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo.
Aktuelle Plugin-Version: **0.9.5.6** Aktuelle Plugin-Version: **0.9.6.1**
## Grundprinzip ## Grundprinzip
@@ -17,166 +17,126 @@ Der NC Connector synchronisiert ausdrücklich ausgewählte Nextcloud-Inhalte mit
### Datenmodell ### Datenmodell
- Nextcloud bleibt die Quelle der Originaldateien. - Nextcloud bleibt die Quelle der Originaldateien.
- Originalbilder werden nicht in eine zweite dauerhafte Bibliothek kopiert. - Originalbilder werden nicht dauerhaft in eine zweite Bibliothek unter Piwigo kopiert.
- Ein Shadow Tree erzeugt die für Piwigo benötigte Verzeichnisstruktur. - Ein Shadow Tree erzeugt die für Piwigo benötigte Ordnerstruktur.
- Der bestehende produktive Weg verwendet lokale bereits vorhandene Speicherpfade und Symlinks auf die Originale.
- Ordner werden als physische Piwigo-Alben synchronisiert. - Ordner werden als physische Piwigo-Alben synchronisiert.
- Root-Dateien können als echte Piwigo-Orphans ohne Albumzuordnung registriert werden. - Root-Dateien können als Piwigo-Orphans registriert werden.
- Entfernte Quellen werden aus Shadow Tree und Piwigo entfernt, das Nextcloud-Original bleibt erhalten.
- Neu importierte physische Connector-Alben werden privat angelegt. - Neu importierte physische Connector-Alben werden privat angelegt.
- Entfernte Quellen werden aus Shadow Tree und Piwigo entfernt; die Nextcloud-Originale bleiben erhalten.
### Bestehende produktive Quellenmodi ### Bestehende lokale Quellenmodi
Der bestehende Connector unterstützt weiterhin getrennte Quellenmodi: Der bestehende Connector unterstützt weiterhin:
- `legacy-view` - `legacy-view`
- `user-shares` - `user-shares`
- `selected-fileids` - `selected-fileids`
Diese Modi bleiben während der Entwicklung des neuen WebDAV-Wegs vollständig erhalten. Es gibt keine automatische Migration bestehender Verbindungen. Diese Modi bleiben erhalten. Es gibt keine automatische Migration auf WebDAV.
### Verbindungsassistent ### WebDAV-Modus
Neue Verbindungen werden bevorzugt mit einem Endnutzer-Assistenten angelegt. Der neue Endnutzerpfad verwendet `source_mode=webdav-placeholder` und `adapter=remote`.
Der aktuelle Assistent kann Nextcloud per HTTP/HTTPS und OCS prüfen und Verzeichnisse per WebDAV anzeigen. Für den bestehenden produktiven lokalen Weg existieren weiterhin PostgreSQL-/View-/Storage-Mapping-Schritte, sofern sie benötigt werden. Benötigt werden nur:
Wizard-Geheimnisse werden während der Einrichtung serverseitig in der Sitzung gehalten. Ein fehlgeschlagener Test leert die Eingaben nicht. Ein API-Test verändert die dauerhafte API-Konfiguration erst beim erfolgreichen Abschluss des Assistenten. - Nextcloud-Adresse;
- Nextcloud-Benutzer bzw. App-Passwort;
- normaler WebDAV-Zugriff auf die Inhalte dieses Benutzers;
- eine normale Piwigo-Installation mit ihrer vorhandenen PHP-/Bildverarbeitungsumgebung.
Die vollständige technische Maske bleibt weiterhin über **Ohne Assistent anlegen** sowie bei bestehenden Verbindungen unter **Technische Einstellungen** erreichbar. Nicht vorausgesetzt werden:
### Lokaler produktiver Adapter - PostgreSQL-Zugriff auf Nextcloud;
- `occ`-Adminzugriff;
- Storage-IDs oder Backend-Pfade;
- zusätzliche Host-Mounts;
- FUSE;
- davfs;
- rclone.
Der derzeit produktive Adapter verwendet je nach bestehender Verbindung: ### WebDAV-Ablauf
- PostgreSQL-Reader; 1. Der Assistent prüft Nextcloud und liest die sichtbaren Verzeichnisse per WebDAV.
- Source-/Activity-Views; 2. Ausgewählte Verzeichnisse werden rekursiv per PROPFIND eingelesen.
- explizite Storage-Zuordnungen auf bereits vorhandene lokale Mounts; 3. Ordner, Dateiname, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
- Plugin-eigene Runtime und getrennten State pro Verbindung. 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.
Dieser Weg bleibt erhalten, bis der neue WebDAV-Weg vollständig End-to-End funktioniert. 6. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID`.
7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert. Die ausgewählte Nextcloud-Wurzel erscheint dadurch direkt als Album; technische `bratonien-webdav-ID`-Wrapper bleiben unsichtbar.
### Neuer WebDAV-Weg 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.
Der neue Quellenmodus wird zusätzlich zu den bestehenden Modi entwickelt als `webdav-placeholder`. 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.
Ziel:
- nur normale Nextcloud-Anmeldung bzw. App-Passwort und WebDAV benötigen;
- keine Nextcloud-PostgreSQL-Rechte voraussetzen;
- keine Storage-IDs oder Backend-Pfade voraussetzen;
- keine zusätzlichen Host-Mounts voraussetzen;
- kein FUSE, davfs oder rclone voraussetzen;
- keine Originalbilder dauerhaft nach Piwigo kopieren.
Grundidee:
1. Nextcloud-Verzeichnisse werden über WebDAV/PROPFIND gelesen.
2. Ordner, Dateinamen, Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
3. Für Bilder wird nur eine winzige lokale Platzhalterquelle erzeugt.
4. Der bestehende Shadow Tree bildet daraus die reale Ordner-/Dateinamensstruktur für Piwigo.
5. Ein Mapping verbindet den Piwigo-/Shadow-Tree-Pfad mit der WebDAV-Quelle.
6. Wenn Piwigo echte Bilddaten benötigt, wird das Original bei Bedarf über WebDAV gelesen.
7. Piwigo-Derivate werden normal in `_data/i/` gecacht.
8. Das Original bleibt ausschließlich in Nextcloud.
Vorhandene Bausteine:
- `runtime/lib/build_webdav_placeholder_source.py` rekursiver WebDAV-Scan, Platzhalterquelle, Manifest und Mapping;
- `include/nc_connector_webdav.inc.php` paralleler, zunächst deaktivierter Connection-Typ `webdav-placeholder`;
- Secret-Format v3 mit verbindungseigenem `nextcloud_user` und `nextcloud_password`.
Neue WebDAV-Testverbindungen werden als `adapter=remote` und deaktiviert gespeichert. Sie verändern keine bestehenden Verbindungen und werden noch nicht von der produktiven Runtime aktiviert.
### Gemessene WebDAV-Performance
Ein realer Test mit einem 16.091.204-Byte-Bild ergab vom Piwigo-System:
- interne Nextcloud-Adresse: 1,829 s bei 8.796.915 Byte/s;
- externe Nextcloud-Adresse: 1,865 s bei 8.627.524 Byte/s.
Damit war die externe Verbindung in diesem Test nur ungefähr 2 % langsamer. Der WebDAV-Ansatz wird deshalb weiterverfolgt. Wichtig bleibt, dass Piwigo-Derivate lokal gecacht werden und WebDAV nicht bei jedem Thumbnail-Aufruf erneut verwendet wird.
### Activity Gate
Der bestehende regelmäßige Lauf berücksichtigt:
- Nextcloud-Aktivität;
- Signatur der sichtbaren Quellen;
- Quiet-Time;
- maximale Wartezeit;
- periodischen Full-Sync;
- notwendigen Reparaturlauf bei beschädigtem Shadow Tree oder fehlenden Piwigo-Alben.
Der neue WebDAV-Modus erhält später eine eigene Änderungserkennung, insbesondere über WebDAV-Metadaten wie ETag.
### Piwigo-Synchronisierung ### Piwigo-Synchronisierung
Der produktive Lauf ist API-first: Der WebDAV-Pfad nutzt die vorhandene Piwigo-Synchronisationslogik mit einer verbindungseigenen Site.
1. `bratonien.nc.syncProductive` für physische Alben und Bilder; Nach dem Dateiabgleich laufen die vorhandenen Nacharbeiten, darunter:
2. `bratonien.nc.syncOrphans` für Root-Dateien;
3. Benutzername/Passwort nur als optionaler Fallback.
Die produktive direkte API-Synchronisierung ist aktuell ausdrücklich für **Piwigo 16.4.0** freigegeben. Bei einer anderen Piwigo-Version wird sie nicht stillschweigend als kompatibel angenommen. - Metadaten-Synchronisierung;
- Bild- und Kategorieintegrität;
- Uppercats/Kategoriestruktur;
- globale Ränge;
- Pfadpflege;
- Rating-Score;
- Benutzer-Cache-Invalidierung;
- Orphan-Abgleich.
Der direkte produktive Sync führt außerdem die für den normalen Piwigo-Import relevanten Nacharbeiten aus, darunter Albuminformationen, globale Ränge, Bildinformationen, Pfade und Cache-Invalidierung. Er ruft dafür keine Admin-Seite fernsteuernd auf. Die direkte produktive Synchronisierung ist aktuell ausdrücklich auf **Piwigo 16.4.0** abgestimmt.
### 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 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
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 ### Runtime
Aktive bestehende Verbindungen werden über einen gemeinsamen systemd-Timer verarbeitet: Aktive Verbindungen werden über den gemeinsamen Runner verarbeitet:
- `bratonien-nc-connector.timer` - `bratonien-nc-connector.timer`
- `bratonien-nc-connector.service` - `bratonien-nc-connector.service`
- `runtime/run-all.sh` - `runtime/run-all.sh`
- `runtime/sync.sh`
Verbindungsspezifische Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`, State-Daten unter `/var/lib/bratonien-tools/nc-connector/connection-ID`. Verbindungsspezifische Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`, State-Daten unter `/var/lib/bratonien-tools/nc-connector/connection-ID`.
Der Shadow-Tree-Austausch besitzt einen Rollback: Scheitert der Wechsel auf den neuen Baum, wird der vorherige Galeriebaum wiederhergestellt. Die Reihenfolge des gemeinsamen Laufs ist:
Der WebDAV-Parallelweg wird in der nächsten Bauphase mit einem eigenen Reconcile-/Sync-Zweig ergänzt. Bis dahin bleibt er deaktiviert und kann keine bestehende Runtime beeinflussen. 1. lokale Verbindungen reconciliieren;
2. WebDAV-Verbindungen reconciliieren;
### Statusanzeige 3. verwaiste WebDAV-Piwigo-Inhalte bereinigen;
4. allgemeine verwaiste Runtime-Dateien bereinigen;
Die Administration zeigt letzten und nächsten Lauf. Der Status wird regelmäßig im Hintergrund abgefragt. Dafür wird die Admin-Seite **nicht** neu geladen; offene Dialoge, Assistenten und aufgeklappte Bereiche bleiben erhalten. 5. WebDAV-Verbindungen synchronisieren;
6. lokale Verbindungen synchronisieren.
### Bestehende Verbindungen
Bestehende Verbindungen können:
- umbenannt;
- geprüft;
- technisch bearbeitet, solange sie nicht aktiv sind;
- deaktiviert und anschließend gelöscht werden.
Seit 0.9.5.5 hängt das Löschen einer Verbindung nicht mehr davon ab, dass der Webserver in Root-eigene Runtime-Verzeichnisse schreiben kann. Die Datenbankverbindung wird entfernt; verwaiste Runtime-Dateien werden vor einem späteren Sync bereinigt.
Das Löschen einer Connector-Verbindung entfernt keine Nextcloud-Originale und keine Piwigo-Bilder.
### Migrationsregel für WebDAV
Der bestehende lokale Weg bleibt so lange vollständig erhalten, bis `webdav-placeholder` End-to-End funktioniert.
Vor einer freiwilligen Migration bestehender Verbindungen müssen mindestens erfolgreich getestet sein:
- Verbindungsanlage;
- Verzeichnisauswahl;
- Shadow Tree;
- Piwigo-Registrierung;
- echte Bildausgabe;
- Derivat-Cache;
- Änderungserkennung;
- Löschen/Deaktivieren;
- Fehlerbehandlung.
## Bildcache ## Bildcache
Bratonien Tools kann:
- Piwigo-Bildderivate gezielt leeren; - Piwigo-Bildderivate gezielt leeren;
- vorhandene Bildgrößen neu erzeugen; - vorhandene Bildgrößen neu erzeugen;
- Cache-Aufbau als Worker-Prozess starten und abbrechen; - den Cache-Aufbau als Worker-Prozess starten und abbrechen;
- Worker-Zahl automatisch oder manuell konfigurieren; - die Worker-Zahl automatisch oder manuell konfigurieren.
- Originalbilder bleiben unangetastet.
Originalbilder bleiben unangetastet.
## Wasserzeichenverwaltung ## Wasserzeichenverwaltung
@@ -186,7 +146,7 @@ Vor einer freiwilligen Migration bestehender Verbindungen müssen mindestens erf
- Album-Ausnahmen und Vererbung; - Album-Ausnahmen und Vererbung;
- Position, Transparenz und Skalierung; - Position, Transparenz und Skalierung;
- eigener Runtime-Filter für Piwigo-Derivate; - eigener Runtime-Filter für Piwigo-Derivate;
- bisherige Piwigo-Wasserzeichenkonfiguration wird beim Aktivieren gesichert. - Sicherung der bisherigen Piwigo-Wasserzeichenkonfiguration beim Aktivieren.
## Bilddateien und Pfade ## Bilddateien und Pfade
@@ -229,37 +189,34 @@ Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges B
Der integrierte Updater: Der integrierte Updater:
- liest den Zielstand aus GitHub; - liest den Zielstand aus GitHub;
- bindet ein Update an einen konkreten Commit statt an einen während des Updates beweglichen Branch-Stand; - bindet ein Update an einen konkreten Commit;
- prüft Version und SHA-256 des Zielstands; - prüft Version und SHA-256 des Zielstands;
- erstellt vor dem Austausch ein Backup; - erstellt vor dem Austausch ein Backup;
- nutzt Post/Redirect/Get, damit ein Browser-Reload keine schreibende Aktion erneut ausführt; - nutzt Post/Redirect/Get;
- verlangt Webmaster-Rechte, `ZipArchive` und ausreichende Schreibrechte. - verlangt Webmaster-Rechte, `ZipArchive` und ausreichende Schreibrechte.
## Architektur ## Wichtige Dateien und Verzeichnisse
Wichtige Dateien und Verzeichnisse:
- `main.inc.php` Plugin-Einstieg und Runtime-Hooks - `main.inc.php` Plugin-Einstieg und Runtime-Hooks
- `admin.php` zentraler Admin-Controller mit Post/Redirect/Get - `admin.php` zentraler Admin-Controller
- `include/tool_registry.inc.php` Registry administrativer Aktionen
- `include/nc_connector.inc.php` Datenmodell und gemeinsame Connector-Funktionen - `include/nc_connector.inc.php` Datenmodell und gemeinsame Connector-Funktionen
- `include/nc_connector_wizard.inc.php` Endnutzer-Assistent und Connection-Bearbeitung - `include/nc_connector_wizard.inc.php` Verbindungsassistent
- `include/nc_connector_manage.inc.php` Credential-Format, Storage-Mappings, Verifikation und Löschen - `include/nc_connector_wizard_webdav_flow.inc.php` WebDAV-first-Wizardablauf
- `include/nc_connector_connection_scope.inc.php` verbindungseigene Authentifizierung - `include/nc_connector_delete_safe.inc.php` sicheres Löschen einschließlich WebDAV-Piwigo-Inhalten
- `include/nc_connector_create_api.inc.php` API-first-Verbindungserstellung und Secret v3
- `include/nc_connector_webdav.inc.php` paralleler WebDAV-Connection-Typ
- `include/nc_connector_piwigo_api.inc.php` API-Zugang und Fallback-Verwaltung
- `include/nc_connector_system.inc.php` Timer- und Laufzeitstatus
- `include/nc_productive_ws.inc.php` produktiver Piwigo-Dateisync - `include/nc_productive_ws.inc.php` produktiver Piwigo-Dateisync
- `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung - `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung
- `runtime/lib/activity_gate.py` Activity Gate - `include/webdav_image_runtime.inc.php` WebDAV-Bildzuordnung, Preview-/Derivathilfen und URL-Filter
- `runtime/lib/build_manifest.py` bestehende Auflösung der Nextcloud-Quellen auf Storage-Mounts - `webdav-image.php` berechtigungsgeprüfter WebDAV-Bildstream
- `runtime/lib/build_selected_manifest.py` ausgewählte Nextcloud-Datei-IDs auf bestehende Storage-Pfade auflösen - `runtime/reconcile-webdav.php` WebDAV-Runtime-Reconcile
- `runtime/lib/build_webdav_placeholder_source.py` experimentelle WebDAV-Platzhalterquelle - `runtime/cleanup-webdav-piwigo.php` Bereinigung gelöschter/verwaister WebDAV-Verbindungen in Piwigo
- `runtime/lib/shadow_tree.py` atomarer Shadow Tree mit Rollback - `runtime/sync-webdav.sh` Ablauf einer WebDAV-Verbindung
- `runtime/lib/piwigo-sync.php` API-first-Piwigo-Sync mit Fallback - `runtime/run-all.sh` gemeinsamer Multi-Connection-Runner
- `runtime/run-all.sh` Multi-Connection-Runner und Bereinigung verwaister Runtime-Dateien - `runtime/lib/build_webdav_placeholder_source.py` rekursiver WebDAV-Scan und Platzhalterquelle
- `runtime/sync.sh` Ablauf einer bestehenden lokalen Verbindung - `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
- `include/self_update.inc.php` Self-Updater - `include/self_update.inc.php` Self-Updater
- `include/album_shares.inc.php` geschützte Albumfreigaben - `include/album_shares.inc.php` geschützte Albumfreigaben
- `include/public_selection.inc.php` Fotoauswahl - `include/public_selection.inc.php` Fotoauswahl
@@ -272,20 +229,18 @@ Wichtige Dateien und Verzeichnisse:
- administrative Schreibaktionen verwenden Piwigos CSRF-Schutz; - administrative Schreibaktionen verwenden Piwigos CSRF-Schutz;
- Connector-Zugangsdaten werden verschlüsselt gespeichert; - Connector-Zugangsdaten werden verschlüsselt gespeichert;
- Secret v3 speichert Nextcloud-Zugangsdaten verbindungseigen und verschlüsselt; - Nextcloud-Zugangsdaten werden verbindungseigen gespeichert und nur serverseitig verwendet;
- Wizard-Geheimnisse werden nicht in Browser-Web-Storage persistiert; - Wizard-Geheimnisse werden nicht im Browser-Web-Storage persistiert;
- bestehende lokale Storage-Mounts werden explizit zugeordnet und validiert;
- SQL-View-Namen werden vor Verwendung validiert; - SQL-View-Namen werden vor Verwendung validiert;
- produktive Piwigo-API ist versionsgebunden; - produktive Piwigo-API ist versionsgebunden;
- Originalbilder werden vom Connector nicht gelöscht; - Originalbilder werden vom Connector nicht gelöscht;
- der neue WebDAV-Weg soll Originalbilder nicht dauerhaft lokal speichern; - WebDAV-Originalbilder werden nicht dauerhaft lokal gespeichert;
- Update-Pakete werden an Commit und Hash gebunden; - Update-Pakete werden an Commit und Hash gebunden.
- bestehende Verbindungen bleiben während der WebDAV-Entwicklung unangetastet.
## Entwicklungsstand ## Entwicklungsstand
`0.9.5.x` ist der aktuelle Entwicklungsblock. Der bestehende lokale NC Connector bleibt produktiv und wird nicht durch den experimentellen WebDAV-Weg ersetzt. `0.9.6.x` ist die aktuelle Versionslinie.
Mit 0.9.5.6 ist die parallele Verbindungsschicht angelegt: Secret v3 kann Nextcloud-Zugangsdaten speichern und `webdav-placeholder` existiert als eigener deaktivierter Connection-Typ. Der nächste Schritt ist ein strikt getrennter Reconcile-/Runtime-Zweig für diesen Modus und danach der erste Platzhalter-PoC durch den bestehenden Shadow Tree und Piwigo-Sync. 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.
Den detaillierten aktuellen Plan enthält `CURRENT_STATUS.md`. Der jeweils detaillierte Prüf- und Entwicklungsstand steht in [`CURRENT_STATUS.md`](CURRENT_STATUS.md).

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/tool_registry.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_system.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(); $tools = bratonien_tools_get_tools();
$messages = array(); $messages = array();
$errors = 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; $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()) if (!headers_sent())
{ {
header('Location: '.$redirect_url, true, 303); header('Location: '.$redirect_url, true, 303);
@@ -115,9 +196,17 @@ $asset_environment = bratonien_tools_get_asset_environment();
$album_shares = bratonien_tools_get_album_shares(); $album_shares = bratonien_tools_get_album_shares();
$private_albums = bratonien_tools_get_private_albums(); $private_albums = bratonien_tools_get_private_albums();
$nc_connector = bratonien_tools_nc_connector_status(); $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) 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']; $nc_connection['display_name'] = $nc_connection['name'];
$storage_lines = array(); $storage_lines = array();
@@ -166,7 +255,7 @@ $nc_system_defaults = array(
); );
$nc_connector['system'] = array_merge( $nc_connector['system'] = array_merge(
$nc_system_defaults, $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_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']) : ''; $album_lock_search = isset($_GET['br_album_search']) ? trim((string)$_GET['br_album_search']) : '';

View File

@@ -5,6 +5,11 @@ if (!defined('PHPWG_ROOT_PATH'))
} }
function bratonien_tools_nc_connector_create_local_api_first() 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( $required = array(
'nc_name' => 'Name', 'nc_name' => 'Name',

View File

@@ -4,12 +4,81 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
function bratonien_tools_nc_connector_webdav_site_id(array $connection)
{
$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;
$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($gallery_root, strlen($piwigo_root)), '/');
if ($relative === '') return 0;
$site_url = './'.rtrim($relative, '/').'/';
$result = pwg_query(
"SELECT id FROM ".SITES_TABLE.
" WHERE galleries_url='".pwg_db_real_escape_string($site_url)."' LIMIT 1"
);
if (!pwg_db_num_rows($result)) return 0;
$row = pwg_db_fetch_assoc($result);
return (int)$row['id'];
}
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');
$image_rows = array();
$query = '\nSELECT DISTINCT i.id, i.path, i.representative_ext\n FROM '.IMAGES_TABLE.' AS i\n LEFT JOIN '.CATEGORIES_TABLE.' AS sc ON sc.id = i.storage_category_id\n LEFT JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON ic.image_id = i.id\n LEFT JOIN '.CATEGORIES_TABLE.' AS vc ON vc.id = ic.category_id\n WHERE sc.site_id = '.$site_id.' OR vc.site_id = '.$site_id.'\n;';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
$row['id'] = (int)$row['id'];
$image_rows[$row['id']] = $row;
}
foreach ($image_rows as $row)
{
try
{
delete_element_derivatives($row);
}
catch (Throwable $e)
{
error_log('Bratonien WebDAV delete derivative cleanup #'.(int)$row['id'].': '.$e->getMessage());
}
}
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'
);
if ($remaining)
{
delete_elements(array_map('intval', $remaining), false);
}
}
invalidate_user_cache(true);
return array('site_id'=>$site_id, 'images'=>count($image_rows));
}
/** /**
* Delete a connector connection from Piwigo without depending on the web * Delete exactly the connection selected by the user.
* server being allowed to write into root-owned runtime directories. * No other connector record is implicitly removed.
*
* Runtime files are removed by runtime/cleanup-stale.php on the next runner
* cycle before any connector sync is started.
*/ */
function bratonien_tools_nc_connector_delete_safe() function bratonien_tools_nc_connector_delete_safe()
{ {
@@ -20,28 +89,35 @@ function bratonien_tools_nc_connector_delete_safe()
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.'); throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
} }
$cleanup = array('site_id'=>0, 'images'=>0);
if (
(string)($connection['adapter'] ?? '') === 'remote'
&& (string)($connection['config']['source_mode'] ?? '') === 'webdav-placeholder'
)
{
$cleanup = bratonien_tools_nc_connector_remove_webdav_piwigo_content($connection);
}
$table = bratonien_tools_nc_connector_table(); $table = bratonien_tools_nc_connector_table();
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1"); pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
// Public status data is only cache/runtime information. Remove it when PHP
// has permission, but never make successful DB deletion depend on ownership
// of files previously created by the root-run connector service.
$status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status'; $status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status';
$public_status = $status_dir.'/connection-'.$id.'.json'; $public_status = $status_dir.'/connection-'.$id.'.json';
if (is_file($public_status)) if (is_file($public_status)) @unlink($public_status);
{
@unlink($public_status);
}
// Keep the old tombstone mechanism as a best-effort fast path for systems
// where the web server can write here. cleanup-stale.php is the authoritative
// fallback and does not require this file.
if (is_dir($status_dir) || @mkdir($status_dir, 0755, true)) if (is_dir($status_dir) || @mkdir($status_dir, 0755, true))
{ {
@file_put_contents($status_dir.'/deleted-'.$id, date('c')."\n", LOCK_EX); @file_put_contents($status_dir.'/deleted-'.$id, date('c')."\n", LOCK_EX);
} }
if ((int)$cleanup['site_id'] > 0)
{
return array(
'message'=>'Verbindung wurde gelöscht. Die zugehörigen Piwigo-Alben und '.(int)$cleanup['images'].' Bilder wurden aus Piwigo entfernt. Nextcloud-Dateien blieben unverändert.',
);
}
return array( return array(
'message'=>'Connector-Verbindung wurde gelöscht. Verbliebene Laufzeitdateien werden vor dem nächsten Connector-Lauf automatisch entfernt. Nextcloud- und Piwigo-Bilder blieben unverändert.', 'message'=>'Verbindung wurde gelöscht. Verbliebene Laufzeitdaten werden automatisch bereinigt. Quelldateien 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,165 @@
<?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',
);
}
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_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)
{
$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['queued_at'] = $now;
$state['next_due'] = $now + bratonien_tools_nc_scheduler_interval();
bratonien_tools_nc_scheduler_write_state($state);
$runner = BRATONIEN_TOOLS_PATH.'runtime/native-runner.php';
$command = escapeshellarg($php).' '.escapeshellarg($runner).' >> '.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)
{
throw new RuntimeException('Der native NC-Abgleich konnte nicht gestartet werden.');
}
return array('started'=>true, 'message'=>'NC-Abgleich wurde gestartet.');
}
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);
}
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,109 +4,13 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
function bratonien_tools_nc_connector_systemctl_value(array $args) require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_scheduler.inc.php');
{
if (!function_exists('proc_open'))
{
return '';
}
$command = array_merge(array('/usr/bin/systemctl'), $args);
$spec = array(
0 => array('file', '/dev/null', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w'),
);
$environment = array_merge($_ENV, array('LC_ALL'=>'C', 'LANG'=>'C'));
$process = @proc_open($command, $spec, $pipes, null, $environment);
if (!is_resource($process))
{
return '';
}
$stdout = stream_get_contents($pipes[1]);
stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit = proc_close($process);
return $exit === 0 ? trim((string)$stdout) : '';
}
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))
{
$next_boot_seconds = ((float)$matches[1]) / 1000000;
}
else
{
return 0;
}
$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))
{
$parsed = strtotime($matches[1]);
return $parsed === false ? 0 : (int)$parsed;
}
return 0;
}
function bratonien_tools_nc_connector_connection_last_status(array $connection) function bratonien_tools_nc_connector_connection_last_status(array $connection)
{ {
$empty = array( $empty = array(
'timestamp'=>0, 'timestamp'=>0,'label'=>'Nicht verfügbar','state'=>'','message'=>'','auth_mode'=>'',
'label'=>'Nicht verfügbar', 'api_state'=>'','api_message'=>'','fallback_state'=>'','fallback_message'=>'','error_detail'=>'',
'state'=>'',
'message'=>'',
'auth_mode'=>'',
'api_state'=>'',
'api_message'=>'',
'fallback_state'=>'',
'fallback_message'=>'',
'error_detail'=>'',
); );
$connection_id = (int)($connection['id'] ?? 0); $connection_id = (int)($connection['id'] ?? 0);
@@ -114,42 +18,25 @@ function bratonien_tools_nc_connector_connection_last_status(array $connection)
if ($connection_id > 0) if ($connection_id > 0)
{ {
$candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$connection_id.'.json'; $candidates[] = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$connection_id.'.json';
$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(); $config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/'); $state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir === '' && $connection_id > 0) if ($state_dir !== '') $candidates[] = $state_dir.'/connector-status.json';
{
$state_dir = '/var/lib/bratonien-tools/nc-connector/connection-'.$connection_id;
}
if ($state_dir !== '')
{
$candidates[] = $state_dir.'/connector-status.json';
}
$decoded = null; $decoded = null;
foreach ($candidates as $candidate) foreach (array_unique($candidates) as $candidate)
{ {
if (!is_readable($candidate)) if (!is_readable($candidate)) continue;
{
continue;
}
$value = json_decode((string)@file_get_contents($candidate), true); $value = json_decode((string)@file_get_contents($candidate), true);
if (is_array($value)) if (is_array($value)) { $decoded = $value; break; }
{
$decoded = $value;
break;
}
}
if (!is_array($decoded))
{
return $empty;
} }
if (!is_array($decoded)) return $empty;
$timestamp = (int)($decoded['timestamp'] ?? 0); $timestamp = (int)($decoded['timestamp'] ?? 0);
$api = isset($decoded['api']) && is_array($decoded['api']) ? $decoded['api'] : array(); $api = isset($decoded['api']) && is_array($decoded['api']) ? $decoded['api'] : array();
$fallback = isset($decoded['fallback']) && is_array($decoded['fallback']) ? $decoded['fallback'] : array(); $fallback = isset($decoded['fallback']) && is_array($decoded['fallback']) ? $decoded['fallback'] : array();
return array( return array(
'timestamp'=>$timestamp, 'timestamp'=>$timestamp,
'label'=>$timestamp > 0 ? date('d.m.Y H:i:s', $timestamp) : 'Nicht verfügbar', 'label'=>$timestamp > 0 ? date('d.m.Y H:i:s', $timestamp) : 'Nicht verfügbar',
@@ -166,73 +53,69 @@ function bratonien_tools_nc_connector_connection_last_status(array $connection)
function bratonien_tools_nc_connector_last_status(array $connections) function bratonien_tools_nc_connector_last_status(array $connections)
{ {
$latest = array('timestamp'=>0, 'state'=>'', 'message'=>'', 'auth_mode'=>'', 'api_state'=>'', 'api_message'=>'', 'fallback_state'=>'', 'fallback_message'=>'', 'error_detail'=>''); $latest = array('timestamp'=>0,'state'=>'','message'=>'','auth_mode'=>'','api_state'=>'','api_message'=>'','fallback_state'=>'','fallback_message'=>'','error_detail'=>'');
foreach ($connections as $connection) foreach ($connections as $connection)
{ {
if (empty($connection['enabled']) || (string)($connection['takeover_state'] ?? '') !== 'active') if (empty($connection['enabled'])) continue;
{
continue;
}
$status = bratonien_tools_nc_connector_connection_last_status($connection); $status = bratonien_tools_nc_connector_connection_last_status($connection);
if ((int)$status['timestamp'] >= (int)$latest['timestamp']) if ((int)$status['timestamp'] >= (int)$latest['timestamp']) $latest = $status;
{
$latest = $status;
}
} }
return $latest; return $latest;
} }
function bratonien_tools_nc_connector_system_status(array $connections = array()) function bratonien_tools_nc_connector_system_status(array $connections = array())
{ {
$timer = 'bratonien-nc-connector.timer'; $scheduler = bratonien_tools_nc_scheduler_read_state();
$service = 'bratonien-nc-connector.service'; $enabled = !isset($scheduler['enabled']) || !empty($scheduler['enabled']);
$running = (string)($scheduler['state'] ?? '') === 'running';
$next_realtime_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $timer, '--property=NextElapseUSecRealtime', '--value')); $started = (int)($scheduler['started_at'] ?? 0);
$next_timestamp = bratonien_tools_nc_connector_parse_systemd_time($next_realtime_raw); $next = (int)($scheduler['next_due'] ?? 0);
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));
$last = bratonien_tools_nc_connector_last_status($connections); $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')); $next_label = date('d.m.Y H:i:s', $next).' (beim nächsten Piwigo-Aufruf)';
$last['timestamp'] = bratonien_tools_nc_connector_parse_systemd_time($last_raw); }
else
{
$next_label = 'Beim nächsten Piwigo-Aufruf';
}
if ((int)$last['timestamp'] <= 0 && !empty($scheduler['finished_at']))
{
$last['timestamp'] = (int)$scheduler['finished_at'];
$last['state'] = (string)($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;
}
} }
return array( return array(
'timer_name' => $timer, 'timer_name'=>'Piwigo nativer NC-Scheduler',
'timer_active' => $active === 'active', 'timer_active'=>$enabled,
'timer_enabled' => $enabled === 'enabled', 'timer_enabled'=>$enabled,
'last_run_timestamp' => (int)$last['timestamp'], 'service_active'=>$running,
'last_run_label' => $last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar', 'current_run_timestamp'=>$started,
'last_run_state' => (string)$last['state'], 'current_run_label'=>$running ? ($started > 0 ? 'Läuft seit '.date('d.m.Y H:i:s', $started) : 'Läuft gerade') : 'Kein Lauf aktiv',
'last_run_message' => (string)$last['message'], 'last_run_timestamp'=>(int)$last['timestamp'],
'last_run_auth_mode' => (string)$last['auth_mode'], 'last_run_label'=>(int)$last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar',
'last_run_api_state' => (string)$last['api_state'], 'last_run_state'=>(string)$last['state'],
'last_run_api_message' => (string)$last['api_message'], 'last_run_message'=>(string)$last['message'],
'last_run_fallback_state' => (string)$last['fallback_state'], 'last_run_auth_mode'=>(string)$last['auth_mode'],
'last_run_fallback_message' => (string)$last['fallback_message'], 'last_run_api_state'=>(string)$last['api_state'],
'last_run_error_detail' => (string)$last['error_detail'], 'last_run_api_message'=>(string)$last['api_message'],
'next_run_timestamp' => $next_timestamp, 'last_run_fallback_state'=>(string)$last['fallback_state'],
'next_run_label' => $next_timestamp > 0 ? date('d.m.Y H:i:s', $next_timestamp) : 'Nicht verfügbar', 'last_run_fallback_message'=>(string)$last['fallback_message'],
'legacy_runtime_exists' => is_dir('/opt/piwigo-sync'), 'last_run_error_detail'=>(string)$last['error_detail'],
'legacy_config_exists' => is_dir('/etc/piwigo-sync'), 'next_run_timestamp'=>$next,
'legacy_service_exists' => is_file('/etc/systemd/system/piwigo-sync.service'), 'next_run_label'=>$next_label,
'legacy_timer_exists' => is_file('/etc/systemd/system/piwigo-sync.timer'), 'legacy_runtime_exists'=>is_dir('/opt/piwigo-sync'),
'legacy_config_exists'=>is_dir('/etc/piwigo-sync'),
'legacy_service_exists'=>is_file('/etc/systemd/system/piwigo-sync.service'),
'legacy_timer_exists'=>is_file('/etc/systemd/system/piwigo-sync.timer'),
); );
} }

View File

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

View File

@@ -5,10 +5,8 @@ if (!defined('PHPWG_ROOT_PATH'))
} }
/** /**
* Create a disabled WebDAV-backed connector beside the existing local modes. * Create or update one independent WebDAV connector connection.
* * No migration pair, successor or implicit legacy fallback is created.
* This deliberately does not activate, migrate or modify any existing
* connection. It only persists the data required for the parallel WebDAV path.
*/ */
function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard() function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
{ {
@@ -16,7 +14,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
if (empty($state['scan_ok']) || empty($state['technical_complete'])) 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'] ?? '')), '/'); $base_url = rtrim(trim((string)($state['base_url'] ?? '')), '/');
@@ -24,7 +22,7 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$password = (string)($state['_password'] ?? ''); $password = (string)($state['_password'] ?? '');
if ($base_url === '' || $username === '' || $password === '') if ($base_url === '' || $username === '' || $password === '')
{ {
throw new RuntimeException('Für den WebDAV-Testpfad fehlen Nextcloud-Adresse oder Zugangsdaten.'); throw new RuntimeException('Fuer den WebDAV-Zugang fehlen Nextcloud-Adresse oder Zugangsdaten.');
} }
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) $selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
@@ -33,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']) $selected_ids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids'])
? $state['directory_selected_fileids'] ? $state['directory_selected_fileids']
: array(); : array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis für den WebDAV-Testpfad auswählen.'); if (!$selected)
{
$selected = array('');
}
$roots = array(); $roots = array();
foreach ($selected as $path) foreach ($selected as $path)
{ {
$fileid = isset($selected_ids[$path]) ? (int)$selected_ids[$path] : 0; $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( $roots[] = array(
'fileid'=>$fileid, 'fileid'=>$fileid,
'display_name'=>basename($path), 'display_name'=>$display_name,
'webdav_path'=>$path, 'webdav_path'=>$path,
); );
} }
@@ -59,6 +66,19 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
$fallback_password = (string)($state['_fallback_password'] ?? ''); $fallback_password = (string)($state['_fallback_password'] ?? '');
$api_enabled = $api_key_id !== '' && $api_key_secret !== ''; $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( $config = array(
'origin'=>'native', 'origin'=>'native',
'source_mode'=>'webdav-placeholder', 'source_mode'=>'webdav-placeholder',
@@ -73,11 +93,23 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
'quiet_seconds'=>120, 'quiet_seconds'=>120,
'max_wait_seconds'=>900, 'max_wait_seconds'=>900,
'full_sync_seconds'=>86400, 'full_sync_seconds'=>86400,
'parallel_test'=>true,
'piwigo_auth'=>'connection-scoped', 'piwigo_auth'=>'connection-scoped',
'api_enabled'=>$api_enabled, '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) foreach (array('product'=>'nextcloud_product', 'version'=>'nextcloud_version') as $state_key=>$config_key)
{ {
$value = trim((string)($state[$state_key] ?? '')); $value = trim((string)($state[$state_key] ?? ''));
@@ -96,15 +128,31 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($secret_payload)) throw new RuntimeException('WebDAV-Zugangsdaten konnten nicht serialisiert werden.'); 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(); bratonien_tools_nc_connector_ensure_table();
$table = bratonien_tools_nc_connector_table(); $table = bratonien_tools_nc_connector_table();
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
$connection_key = 'webdav-'.bin2hex(random_bytes(12));
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload); $secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
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_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($connection_key)."','"
.pwg_db_real_escape_string($name)."','remote',0,'disabled','" .pwg_db_real_escape_string($name)."','remote',0,'disabled','"
@@ -114,20 +162,18 @@ function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
.pwg_db_real_escape_string($now)."')"); .pwg_db_real_escape_string($now)."')");
$id = (int)pwg_db_insert_id(); $id = (int)pwg_db_insert_id();
if ($id < 1) throw new RuntimeException('Die WebDAV-Testverbindung konnte nicht eindeutig angelegt werden.'); if ($id < 1) throw new RuntimeException('Die WebDAV-Verbindung konnte nicht eindeutig angelegt werden.');
$config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id; $config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
$config['status_file'] = $config['state_dir'].'/connector-status.json'; $config['status_file'] = $config['state_dir'].'/connector-status.json';
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($config_json)) 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");
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
throw new RuntimeException('Die WebDAV-Testverbindung konnte nach dem Anlegen nicht serialisiert werden.'); unset($_SESSION['bratonien_nc_wizard']);
}
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id." LIMIT 1");
return array( return array(
'connection_id'=>$id, 'connection_id'=>$id,
'message'=>'Parallele WebDAV-Testverbindung wurde deaktiviert angelegt. Bestehende Verbindungen blieben unverändert.', 'message'=>'WebDAV-Verbindung wurde angelegt.',
); );
} }

View File

@@ -28,7 +28,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
{ {
try 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; if ($response['status'] < 200 || $response['status'] >= 300) continue;
$candidate_status = json_decode($response['body'], true); $candidate_status = json_decode($response['body'], true);
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue; 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.'); 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', $base_url.'/ocs/v2.php/cloud/user?format=json',
$username, $username,
$password, $password,
@@ -104,7 +104,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first()
'api_error'=>'', 'api_error'=>'',
)); ));
bratonien_tools_nc_wizard_refresh_directory_state($state, ''); bratonien_tools_nc_transport_refresh_directory_state($state, '');
bratonien_tools_nc_wizard_store($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.'); return array('message'=>'Nextcloud und WebDAV wurden bestätigt. Jetzt können die Verzeichnisse des angemeldeten Benutzers ausgewählt werden.');
@@ -126,7 +126,6 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$selected = isset($state['directory_selected']) && is_array($state['directory_selected']) $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_values(array_unique(array_map(function($path) { return trim((string)$path, '/'); }, $state['directory_selected'])))
: array(); : array();
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
if (!$selected) if (!$selected)
{ {
throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.'); throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
@@ -143,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.'); 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( $roots[] = array(
'fileid'=>$fileid, 'fileid'=>$fileid,
'display_name'=>basename($path), 'display_name'=>$display_name,
'webdav_path'=>$path, 'webdav_path'=>$path,
); );
} }
@@ -156,9 +158,35 @@ function bratonien_tools_nc_wizard_save_sources_dispatch()
$state['technical_source'] = 'WebDAV-Verzeichnisse ausgewählt'; $state['technical_source'] = 'WebDAV-Verzeichnisse ausgewählt';
$state['technical_error'] = ''; $state['technical_error'] = '';
$state['directory_selection_ready'] = false; $state['directory_selection_ready'] = false;
bratonien_tools_nc_wizard_store($state);
return array('message'=>'WebDAV-Verzeichnisse wurden übernommen. Die Verbindung ist für den parallelen Testweg vorbereitet.'); 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() function bratonien_tools_nc_wizard_finish_dispatch()

View File

@@ -18,7 +18,7 @@ function bratonien_tools_register_nc_productive_ws_methods($arr)
'info' => 'Piwigo storage site to synchronize. Default: 1.', 'info' => 'Piwigo storage site to synchronize. Default: 1.',
), ),
), ),
'Runs the approved direct Bratonien filesystem synchronization for the NC Connector.', 'Synchronizes the NC Connector into the existing Piwigo album hierarchy.',
null, null,
array( array(
'admin_only' => true, 'admin_only' => true,
@@ -35,6 +35,99 @@ function bratonien_tools_nc_productive_error(&$errors, $path, $type)
); );
} }
function bratonien_tools_nc_relative_path($basedir, $path)
{
$basedir = rtrim(str_replace('\\', '/', (string)$basedir), '/');
$path = str_replace('\\', '/', (string)$path);
if ($path === $basedir) return '';
if (strpos($path, $basedir.'/') !== 0)
{
throw new RuntimeException('WebDAV-Pfad liegt ausserhalb der Connector-Wurzel: '.$path);
}
return trim(substr($path, strlen($basedir)), '/');
}
function bratonien_tools_nc_find_album($parent_id, $dir, $name, $excluded_site_id)
{
$where_parent = $parent_id === null ? 'id_uppercat IS NULL' : 'id_uppercat='.(int)$parent_id;
$dir_sql = pwg_db_real_escape_string((string)$dir);
$name_sql = pwg_db_real_escape_string((string)$name);
$query = '
SELECT id, dir, name
FROM '.CATEGORIES_TABLE.'
WHERE '.$where_parent.'
AND (site_id IS NULL OR site_id <> '.(int)$excluded_site_id.')
AND (
dir = \''.$dir_sql.'\'
OR LOWER(name) = LOWER(\''.$name_sql.'\')
)
ORDER BY CASE WHEN dir = \''.$dir_sql.'\' THEN 0 ELSE 1 END, id
LIMIT 1
;';
$result = pwg_query($query);
if (!pwg_db_num_rows($result)) return null;
$row = pwg_db_fetch_assoc($result);
return (int)$row['id'];
}
function bratonien_tools_nc_ensure_album_path($relative_dir, $excluded_site_id, array &$cache, array &$created_ids)
{
$relative_dir = trim((string)$relative_dir, '/');
if ($relative_dir === '') return null;
if (isset($cache[$relative_dir])) return $cache[$relative_dir];
$parts = explode('/', $relative_dir);
$parent_id = null;
$path = '';
foreach ($parts as $part)
{
if ($part === '') continue;
$path = $path === '' ? $part : $path.'/'.$part;
if (isset($cache[$path]))
{
$parent_id = $cache[$path];
continue;
}
$display_name = str_replace('_', ' ', $part);
$album_id = bratonien_tools_nc_find_album($parent_id, $part, $display_name, $excluded_site_id);
if ($album_id === null)
{
$created = create_virtual_category($display_name, $parent_id);
if (!is_array($created) || empty($created['id']))
{
$detail = is_array($created) && !empty($created['error']) ? (string)$created['error'] : 'unbekannter Fehler';
throw new RuntimeException('Album "'.$display_name.'" konnte nicht angelegt werden: '.$detail);
}
$album_id = (int)$created['id'];
pwg_query('UPDATE '.CATEGORIES_TABLE." SET status='private' WHERE id=".$album_id.' LIMIT 1');
add_permission_on_category(array($album_id), get_admins());
$created_ids[] = $album_id;
}
$cache[$path] = $album_id;
$parent_id = $album_id;
}
return $parent_id;
}
function bratonien_tools_nc_managed_images($basedir)
{
$prefix = rtrim((string)$basedir, '/').'/';
$escaped = pwg_db_real_escape_string(addcslashes($prefix, '_%\\'));
$query = "SELECT id, path FROM ".IMAGES_TABLE." WHERE path LIKE '".$escaped."%' ESCAPE '\\\\'";
return simple_hash_from_query($query, 'id', 'path');
}
function bratonien_tools_nc_remove_storage_categories($site_id)
{
$ids = query2array('SELECT id FROM '.CATEGORIES_TABLE.' WHERE site_id='.(int)$site_id.' AND dir IS NOT NULL', null, 'id');
if (!$ids) return 0;
delete_categories(array_map('intval', $ids));
return count($ids);
}
function bratonien_tools_ws_nc_sync_productive($params, &$service) function bratonien_tools_ws_nc_sync_productive($params, &$service)
{ {
global $conf, $user; global $conf, $user;
@@ -42,181 +135,110 @@ function bratonien_tools_ws_nc_sync_productive($params, &$service)
$piwigo_version = defined('PHPWG_VERSION') ? (string)PHPWG_VERSION : ''; $piwigo_version = defined('PHPWG_VERSION') ? (string)PHPWG_VERSION : '';
if ($piwigo_version !== '16.4.0') if ($piwigo_version !== '16.4.0')
{ {
return new PwgError( return new PwgError(409, 'Bratonien API synchronization is not approved for Piwigo '.$piwigo_version.'.');
409,
'Bratonien API synchronization is not approved for Piwigo '.$piwigo_version.'. Use the administrator fallback until this Piwigo version has been verified.'
);
} }
if (empty($conf['enable_synchronization'])) if (empty($conf['enable_synchronization']))
{ {
return new PwgError(403, 'Piwigo filesystem synchronization is disabled.'); return new PwgError(403, 'Piwigo filesystem synchronization is disabled.');
} }
$site_id = isset($params['site_id']) ? (int)$params['site_id'] : 1; $site_id = isset($params['site_id']) ? (int)$params['site_id'] : 1;
if ($site_id < 1) if ($site_id < 1) return new PwgError(400, 'Invalid site_id.');
{
return new PwgError(400, 'Invalid site_id.');
}
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php'); include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
include_once(PHPWG_ROOT_PATH.'admin/site_reader_local.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('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.');
if (!pwg_db_num_rows($result))
{
return new PwgError(404, 'Piwigo site does not exist.');
}
list($site_url) = pwg_db_fetch_row($result); list($site_url) = pwg_db_fetch_row($result);
if (url_is_remote($site_url)) if (url_is_remote($site_url)) return new PwgError(400, 'Remote Piwigo sites are not supported.');
{
return new PwgError(400, 'Remote Piwigo sites are not supported by this synchronization method.');
}
$site_reader = new LocalSiteReader($site_url); $site_reader = new LocalSiteReader($site_url);
if (!$site_reader->open()) if (!$site_reader->open()) return new PwgError(500, 'Piwigo could not open the configured local site.');
{
return new PwgError(500, 'Piwigo could not open the configured local site.');
}
list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW()')); $basedir = preg_replace('#/*$#', '', (string)$site_url);
$errors = array(); $errors = array();
$counts = array( $counts = array(
'new_categories' => 0, 'reused_categories'=>0,
'del_categories' => 0, 'new_categories'=>0,
'new_elements' => 0, 'removed_duplicate_categories'=>0,
'del_elements' => 0, 'new_elements'=>0,
'upd_elements' => 0, 'del_elements'=>0,
'new_formats' => 0, 'upd_elements'=>0,
'del_formats' => 0,
'metadata_candidates' => 0,
'metadata_updated' => 0,
); );
try try
{ {
$query = 'SELECT id, id_uppercat, uppercats, global_rank, status, visible FROM '.CATEGORIES_TABLE.' WHERE dir IS NOT NULL AND site_id = '.$site_id; $counts['removed_duplicate_categories'] = bratonien_tools_nc_remove_storage_categories($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); list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW()'));
$result = pwg_query('SELECT id FROM '.CATEGORIES_TABLE); $fs_dirs = $site_reader->get_full_directories($basedir);
while ($row = pwg_db_fetch_assoc($result)) usort($fs_dirs, function($a, $b)
{ {
$next_rank[$row['id']] = 1; return substr_count((string)$a, '/') <=> substr_count((string)$b, '/');
} });
$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'];
}
$next_id = pwg_db_nextval('id', CATEGORIES_TABLE); $album_cache = array();
$category_inserts = array(); $created_ids = array();
$dir_to_album = array();
foreach (array_diff($fs_fulldirs, array_keys($db_fulldirs)) as $fulldir) foreach ($fs_dirs as $full_dir)
{ {
$dir = basename($fulldir); $relative = bratonien_tools_nc_relative_path($basedir, $full_dir);
if (!preg_match($conf['sync_chars_regex'], $dir)) if ($relative === '') continue;
$before = count($created_ids);
$album_id = bratonien_tools_nc_ensure_album_path($relative, $site_id, $album_cache, $created_ids);
if ($album_id !== null)
{ {
bratonien_tools_nc_productive_error($errors, $fulldir, 'PWG-UPDATE-1'); $dir_to_album[$full_dir] = $album_id;
continue; if (count($created_ids) === $before) $counts['reused_categories']++;
} }
$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;
}
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);
}
$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);
} }
$counts['new_categories'] = count($created_ids);
$fs = $site_reader->get_elements($basedir); $fs = $site_reader->get_elements($basedir);
$cat_ids = array_diff(array_keys($db_categories), $to_delete_categories); $db_elements = bratonien_tools_nc_managed_images($basedir);
$db_elements = array(); $db_by_path = array_flip($db_elements);
if ($cat_ids)
$to_delete = array();
foreach ($db_elements as $id=>$path)
{ {
$query = 'SELECT id, path FROM '.IMAGES_TABLE.' WHERE storage_category_id IN ('.implode(',', array_map('intval', $cat_ids)).')'; if (!array_key_exists($path, $fs)) $to_delete[] = (int)$id;
$db_elements = simple_hash_from_query($query, 'id', 'path'); }
if ($to_delete)
{
delete_elements($to_delete, false);
$counts['del_elements'] = count($to_delete);
foreach ($to_delete as $id)
{
if (isset($db_elements[$id])) unset($db_by_path[$db_elements[$id]], $db_elements[$id]);
}
} }
$next_element_id = pwg_db_nextval('id', IMAGES_TABLE); $next_element_id = pwg_db_nextval('id', IMAGES_TABLE);
$image_inserts = array(); $image_inserts = array();
$image_links = array(); $image_links = array();
$format_inserts = array(); $new_ids = array();
$new_image_ids = array(); $all_ids = array();
foreach (array_diff(array_keys($fs), $db_elements) as $path) foreach ($fs as $path=>$file_info)
{ {
$dirname = dirname($path); $dirname = dirname($path);
if (!isset($db_fulldirs[$dirname])) $relative_dir = bratonien_tools_nc_relative_path($basedir, $dirname);
$category_id = null;
if ($relative_dir !== '')
{ {
$category_id = $dir_to_album[$dirname] ?? bratonien_tools_nc_ensure_album_path($relative_dir, $site_id, $album_cache, $created_ids);
}
if (isset($db_by_path[$path]))
{
$id = (int)$db_by_path[$path];
$all_ids[] = $id;
pwg_query('DELETE FROM '.IMAGE_CATEGORY_TABLE.' WHERE image_id='.$id);
if ($category_id !== null)
{
single_insert(IMAGE_CATEGORY_TABLE, array('image_id'=>$id, 'category_id'=>$category_id));
}
continue; continue;
} }
@@ -229,116 +251,40 @@ function bratonien_tools_ws_nc_sync_productive($params, &$service)
$id = $next_element_id++; $id = $next_element_id++;
$image_inserts[] = array( $image_inserts[] = array(
'id' => $id, 'id'=>$id,
'file' => $filename, 'file'=>$filename,
'name' => get_name_from_file($filename), 'name'=>get_name_from_file($filename),
'date_available' => $dbnow, 'date_available'=>$dbnow,
'path' => $path, 'path'=>$path,
'representative_ext' => $fs[$path]['representative_ext'], 'representative_ext'=>$file_info['representative_ext'],
'storage_category_id' => $db_fulldirs[$dirname], 'storage_category_id'=>null,
'added_by' => (int)$user['id'], 'added_by'=>(int)$user['id'],
); );
$image_links[] = array('image_id'=>$id, 'category_id'=>$db_fulldirs[$dirname]); if ($category_id !== null)
$new_image_ids[] = $id;
if (!empty($conf['enable_formats']) && !empty($fs[$path]['formats']))
{ {
foreach ($fs[$path]['formats'] as $ext => $filesize) $image_links[] = array('image_id'=>$id, 'category_id'=>$category_id);
{
$format_inserts[] = array('image_id'=>$id, 'ext'=>$ext, 'filesize'=>$filesize);
}
} }
$new_ids[] = $id;
$all_ids[] = $id;
} }
if ($image_inserts) if ($image_inserts)
{ {
mass_inserts(IMAGES_TABLE, array_keys($image_inserts[0]), $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); if ($image_links) mass_inserts(IMAGE_CATEGORY_TABLE, array_keys($image_links[0]), $image_links);
pwg_activity('photo', $new_image_ids, 'add', array('sync'=>true)); pwg_activity('photo', $new_ids, 'add', array('sync'=>true));
$counts['new_elements'] = count($image_inserts); $counts['new_elements'] = count($new_ids);
}
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(); $updates = array();
foreach ($files as $id => $file) foreach ($all_ids as $id)
{ {
$data = $site_reader->get_element_update_attributes($file['path']); $path_result = pwg_query('SELECT path FROM '.IMAGES_TABLE.' WHERE id='.(int)$id.' LIMIT 1');
if (!is_array($data)) if (!pwg_db_num_rows($path_result)) continue;
{ list($path) = pwg_db_fetch_row($path_result);
continue; $data = $site_reader->get_element_update_attributes($path);
} if (!is_array($data)) continue;
$data['id'] = $id; $data['id'] = (int)$id;
$updates[] = $data; $updates[] = $data;
} }
if ($updates) if ($updates)
@@ -351,94 +297,25 @@ function bratonien_tools_ws_nc_sync_productive($params, &$service)
} }
$counts['upd_elements'] = count($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(); images_integrity();
categories_integrity(); categories_integrity();
update_uppercats(); update_uppercats();
update_category('all'); update_category('all');
update_global_rank(); 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(); invalidate_user_cache();
}
catch (Throwable $e)
{
return new PwgError(500, 'Bratonien direct synchronization failed: '.$e->getMessage());
}
return array( return array(
'mode' => 'productive', 'mode'=>'productive',
'engine' => 'bratonien-direct', 'piwigo_version'=>$piwigo_version,
'approved_piwigo_version' => '16.4.0', 'site_id'=>$site_id,
'piwigo_version' => $piwigo_version, 'site_url'=>$site_url,
'site_id' => $site_id, 'counts'=>$counts,
'site_url' => $site_url, 'errors'=>$errors,
'counts' => $counts, 'database_writes'=>array_sum($counts) > 0,
'errors' => $errors, );
'error_count' => count($errors), }
'database_writes' => true, catch (Throwable $error)
'username' => isset($user['username']) ? (string)$user['username'] : '', {
'status' => isset($user['status']) ? (string)$user['status'] : '', return new PwgError(500, 'Bratonien NC synchronization failed: '.$error->getMessage());
); }
} }

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!'); 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/image_cache.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_profiles.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_profiles.inc.php');
@@ -17,7 +28,6 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_manage.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_manage.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_takeover.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_auth.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_auth.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_create_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php');
@@ -30,6 +40,16 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_generic_scope.inc.php'
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php');
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()
{
$result = bratonien_tools_nc_scheduler_spawn(true);
return array('message'=>(string)($result['message'] ?? 'NC-Abgleich wurde gestartet.'));
}
function bratonien_tools_get_tools() function bratonien_tools_get_tools()
{ {
@@ -54,14 +74,16 @@ function bratonien_tools_get_tools()
'album_share_create' => array('handler' => 'bratonien_tools_create_album_share'), 'album_share_create' => array('handler' => 'bratonien_tools_create_album_share'),
'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'), 'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'),
'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'), 'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'),
'nc_connector_create_local' => array('handler' => 'bratonien_tools_nc_connector_create_local_api_first'),
'nc_connector_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'), 'nc_connector_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'),
'nc_connector_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_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'),
'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'), 'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'), 'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'),
'nc_connector_run_now' => array('handler' => 'bratonien_tools_nc_connector_run_now'),
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'), 'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'),
'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'), 'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'),
'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'), 'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_transport_wizard_directory_browse'),
'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'), 'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),
'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'), 'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_sources_dispatch'), 'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_sources_dispatch'),
@@ -71,10 +93,7 @@ function bratonien_tools_get_tools()
'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_dispatch'), 'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_dispatch'),
'nc_connector_wizard_back' => array('handler' => 'bratonien_tools_nc_wizard_back'), 'nc_connector_wizard_back' => array('handler' => 'bratonien_tools_nc_wizard_back'),
'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'), 'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'),
'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'),
'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_connection_scoped'), 'nc_connector_verify' => array('handler' => 'bratonien_tools_nc_connector_verify_connection_scoped'),
'nc_connector_prepare_takeover' => array('handler' => 'bratonien_tools_nc_connector_prepare_takeover'),
'nc_connector_cancel_takeover' => array('handler' => 'bratonien_tools_nc_connector_cancel_takeover'),
'nc_connector_piwigo_api_test' => array('handler' => 'bratonien_tools_nc_connector_piwigo_api_test'), 'nc_connector_piwigo_api_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_piwigo_api_delete' => array('handler' => 'bratonien_tools_nc_connector_api_delete'),
'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'), 'nc_connector_fallback_save' => array('handler' => 'bratonien_tools_nc_connector_fallback_save_scoped'),

View File

@@ -65,7 +65,6 @@ function bratonien_tools_webdav_image_source_info($image_id)
$size = 0; $size = 0;
$etag = ''; $etag = '';
// Metadata is optional. Routing must not depend on the root-owned runtime map.
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/'); $state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '') if ($state_dir !== '')
{ {
@@ -102,6 +101,7 @@ function bratonien_tools_webdav_image_url($image_id, $preview=false)
{ {
$info = bratonien_tools_webdav_image_source_info($image_id); $info = bratonien_tools_webdav_image_source_info($image_id);
if (!$info) return null; if (!$info) return null;
$url = get_root_url().'plugins/'.BRATONIEN_TOOLS_ID.'/webdav-image.php?id='.(int)$image_id; $url = get_root_url().'plugins/'.BRATONIEN_TOOLS_ID.'/webdav-image.php?id='.(int)$image_id;
if ($preview) $url .= '&preview=1'; if ($preview) $url .= '&preview=1';
if ($info['etag'] !== '') $url .= '&v='.rawurlencode(substr(sha1($info['etag']), 0, 12)); if ($info['etag'] !== '') $url .= '&v='.rawurlencode(substr(sha1($info['etag']), 0, 12));
@@ -113,8 +113,22 @@ function bratonien_tools_webdav_preview_path(array $info)
$connection_id = (int)($info['connection_id'] ?? 0); $connection_id = (int)($info['connection_id'] ?? 0);
$webdav_path = trim((string)($info['webdav_path'] ?? ''), '/'); $webdav_path = trim((string)($info['webdav_path'] ?? ''), '/');
if ($connection_id < 1 || $webdav_path === '') return null; if ($connection_id < 1 || $webdav_path === '') return null;
$path = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-preview/connection-'.$connection_id.'/'.sha1($webdav_path).'.webp';
return is_file($path) && is_readable($path) ? $path : null; $base = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-preview/connection-'.$connection_id.'/'.sha1($webdav_path);
foreach (array('jpg', 'png', 'webp') as $ext)
{
$path = $base.'.'.$ext;
if (is_file($path) && is_readable($path)) return $path;
}
return null;
}
function bratonien_tools_webdav_preview_content_type($path)
{
$ext = strtolower(pathinfo((string)$path, PATHINFO_EXTENSION));
if ($ext === 'png') return 'image/png';
if ($ext === 'webp') return 'image/webp';
return 'image/jpeg';
} }
function bratonien_tools_webdav_custom_derivative_params($key) function bratonien_tools_webdav_custom_derivative_params($key)
@@ -177,13 +191,37 @@ function bratonien_tools_webdav_derivative_variants()
return $variants; return $variants;
} }
function bratonien_tools_webdav_generate_derivative($params, $src_image) function bratonien_tools_webdav_generate_derivative($params, $src_image, &$detail=null)
{ {
if (!is_object($src_image) || empty($src_image->id)) return false; global $conf;
$detail = '';
if (!is_object($src_image) || empty($src_image->id))
{
$detail = 'SrcImage fehlt.';
return false;
}
$info = bratonien_tools_webdav_image_source_info((int)$src_image->id); $info = bratonien_tools_webdav_image_source_info((int)$src_image->id);
if (!$info) return false; if (!$info)
{
$detail = 'WebDAV-Quellzuordnung fehlt.';
return false;
}
$preview = bratonien_tools_webdav_preview_path($info); $preview = bratonien_tools_webdav_preview_path($info);
if (!$preview) return false; if (!$preview)
{
$detail = 'Vorbereitetes WebDAV-Preview fehlt.';
return false;
}
$preview_ext = strtolower(pathinfo($preview, PATHINFO_EXTENSION));
if (!in_array($preview_ext, array('jpg', 'jpeg', 'png', 'gif'), true))
{
$detail = 'Preview-Format '.$preview_ext.' ist für die Piwigo-Bildbibliothek nicht sicher nutzbar.';
return false;
}
if (!class_exists('DerivativeImage')) require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php'); if (!class_exists('DerivativeImage')) require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
if (!class_exists('pwg_image')) require_once(PHPWG_ROOT_PATH.'admin/include/image.class.php'); if (!class_exists('pwg_image')) require_once(PHPWG_ROOT_PATH.'admin/include/image.class.php');
@@ -191,50 +229,177 @@ function bratonien_tools_webdav_generate_derivative($params, $src_image)
$derivative = new DerivativeImage($params, $src_image); $derivative = new DerivativeImage($params, $src_image);
if ($derivative->same_as_source()) if ($derivative->same_as_source())
{ {
$detail = 'Identisch mit Quelle; kein eigenes Derivat erforderlich.';
return true; return true;
} }
$target = $derivative->get_path(); $target = $derivative->get_path();
if ($target === '' || strpos($target, PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR) !== 0) return false; $derivative_root = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR;
if ($target === '' || strpos($target, $derivative_root) !== 0)
{
$detail = 'Ungültiger Derivat-Zielpfad: '.$target;
return false;
}
$preview_mtime = @filemtime($preview) ?: 0; $preview_mtime = @filemtime($preview) ?: 0;
if (is_file($target) && is_readable($target) && (@filemtime($target) ?: 0) >= $preview_mtime) if (is_file($target) && is_readable($target) && (@filemtime($target) ?: 0) >= max($preview_mtime, (int)($params->last_mod_time ?? 0)))
{ {
$detail = 'Bereits vorhanden.';
return true; return true;
} }
$directory = dirname($target); $directory = dirname($target);
if (!is_dir($directory) && !mkdir($directory, 0755, true) && !is_dir($directory)) return false; $dir_ok = function_exists('mkgetdir') ? mkgetdir($directory) : (is_dir($directory) || @mkdir($directory, 0755, true));
if (!$dir_ok || !is_dir($directory))
{
$detail = 'Derivat-Verzeichnis konnte nicht angelegt werden: '.$directory;
return false;
}
$image = new pwg_image($preview); $image = null;
try try
{ {
$original_size = array($image->get_width(), $image->get_height()); $image = new pwg_image($preview);
$original_size = array((int)$image->get_width(), (int)$image->get_height());
if ($original_size[0] < 1 || $original_size[1] < 1)
{
$detail = 'Preview-Abmessungen sind ungültig.';
return false;
}
$crop_rect = null; $crop_rect = null;
$scaled_size = null; $scaled_size = null;
$params->sizing->compute($original_size, $info['coi'] ?? null, $crop_rect, $scaled_size); $params->sizing->compute($original_size, $info['coi'] ?? null, $crop_rect, $scaled_size);
if ($crop_rect) if ($crop_rect)
{ {
$image->crop($crop_rect->width(), $crop_rect->height(), $crop_rect->l, $crop_rect->t); if (!$image->crop($crop_rect->width(), $crop_rect->height(), $crop_rect->l, $crop_rect->t))
{
$detail = 'Crop fehlgeschlagen.';
return false;
}
}
$final_size = $original_size;
if ($crop_rect)
{
$final_size = array($crop_rect->width(), $crop_rect->height());
} }
if ($scaled_size) if ($scaled_size)
{ {
$image->resize($scaled_size[0], $scaled_size[1]); if (!$image->resize($scaled_size[0], $scaled_size[1]))
{
$detail = 'Resize fehlgeschlagen.';
return false;
}
$final_size = array((int)$scaled_size[0], (int)$scaled_size[1]);
} }
if (!empty($params->sharpen))
if (!empty($params->sharpen) && !$image->sharpen($params->sharpen))
{ {
$image->sharpen($params->sharpen); $detail = 'Sharpen fehlgeschlagen.';
return false;
} }
$image->write($target);
if ($params->will_watermark($final_size))
{
$wm = ImageStdParams::get_watermark();
if (!empty($wm->file))
{
$wm_path = PHPWG_ROOT_PATH.$wm->file;
if (!is_file($wm_path) || !is_readable($wm_path))
{
$detail = 'Wasserzeichen-Datei fehlt: '.$wm_path;
return false;
}
$wm_image = new pwg_image($wm_path);
try
{
$wm_size = array((int)$wm_image->get_width(), (int)$wm_image->get_height());
if ($final_size[0] < $wm_size[0] || $final_size[1] < $wm_size[1])
{
$wm_scaling = SizingParams::classic($final_size[0], $final_size[1]);
$tmp = null;
$wm_scaled = null;
$wm_scaling->compute($wm_size, null, $tmp, $wm_scaled);
if ($wm_scaled)
{
$wm_image->resize($wm_scaled[0], $wm_scaled[1]);
$wm_size = array((int)$wm_scaled[0], (int)$wm_scaled[1]);
}
}
$x = (int)round(($wm->xpos / 100) * ($final_size[0] - $wm_size[0]));
$y = (int)round(($wm->ypos / 100) * ($final_size[1] - $wm_size[1]));
$image->compose($wm_image, $x, $y, $wm->opacity);
if ($wm->xrepeat || $wm->yrepeat)
{
$xpad = $wm_size[0] + max(30, (int)round($wm_size[0] / 4));
$ypad = $wm_size[1] + max(30, (int)round($wm_size[1] / 4));
for ($i = -$wm->xrepeat; $i <= $wm->xrepeat; $i++)
{
for ($j = -$wm->yrepeat; $j <= $wm->yrepeat; $j++)
{
if (!$i && !$j) continue;
$x2 = $x + $i * $xpad;
$y2 = $y + $j * $ypad;
if ($x2 >= 0 && $x2 + $wm_size[0] < $final_size[0] && $y2 >= 0 && $y2 + $wm_size[1] < $final_size[1])
{
$image->compose($wm_image, $x2, $y2, $wm->opacity);
}
}
}
}
}
finally
{
$wm_image->destroy();
}
}
}
if (isset($conf['derivatives_strip_metadata_threshold']) && $final_size[0] * $final_size[1] < (int)$conf['derivatives_strip_metadata_threshold'])
{
$image->strip();
}
$quality = (int)ImageStdParams::$quality;
if (isset($params->type) && in_array($params->type, array(IMG_3XLARGE, IMG_4XLARGE), true))
{
$quality = min($quality, 75);
}
$image->set_compression_quality($quality);
$written = $image->write($target);
if ($written === false)
{
$detail = 'Bildbibliothek konnte das Derivat nicht schreiben.';
return false;
}
}
catch (Throwable $e)
{
$detail = get_class($e).': '.$e->getMessage();
return false;
} }
finally finally
{ {
$image->destroy(); if (is_object($image)) $image->destroy();
} }
@chmod($target, 0644); @chmod($target, 0644);
clearstatcache(true, $target); clearstatcache(true, $target);
return is_file($target) && is_readable($target); $size = @getimagesize($target);
if (!is_file($target) || !is_readable($target) || !is_array($size) || empty($size[0]) || empty($size[1]))
{
$detail = 'Derivatdatei wurde nicht gültig erzeugt: '.$target;
return false;
}
$detail = 'Erzeugt: '.$target.' ('.$size[0].'x'.$size[1].').';
return true;
} }
function bratonien_tools_filter_webdav_src_url($url, $src_image) function bratonien_tools_filter_webdav_src_url($url, $src_image)
@@ -252,16 +417,18 @@ function bratonien_tools_filter_webdav_derivative_url($url, $params, $src_image,
try try
{ {
if (bratonien_tools_webdav_generate_derivative($params, $src_image)) $derivative = new DerivativeImage($params, $src_image);
if (!$derivative->same_as_source())
{ {
return $url; $path = $derivative->get_path();
if ($path !== '' && is_file($path) && is_readable($path)) return $url;
} }
} }
catch (Throwable $e) catch (Throwable $e)
{ {
error_log('Bratonien WebDAV derivative #'.(int)$src_image->id.': '.$e->getMessage()); error_log('Bratonien WebDAV derivative lookup #'.(int)$src_image->id.': '.$e->getMessage());
} }
$webdav_url = bratonien_tools_webdav_image_url((int)$src_image->id, true); $preview_url = bratonien_tools_webdav_image_url((int)$src_image->id, true);
return $webdav_url ?: $url; return $preview_url ?: $url;
} }

View File

@@ -1,6 +1,94 @@
(function () { (function () {
'use strict'; 'use strict';
function initNcRouteStatus() {
var section = document.getElementById('nc-connector');
if (!section) return;
var statusCard = section.querySelector('.bratonien-grid .bratonien-card');
if (!statusCard) return;
var grid = statusCard.querySelector('.bratonien-form-grid');
if (!grid) return;
var routeLabel = document.createElement('span');
routeLabel.className = 'bratonien-label';
routeLabel.textContent = 'Aktiver Datenweg';
var routeValue = document.createElement('strong');
routeValue.setAttribute('data-nc-route-status', '');
routeValue.textContent = 'Noch kein Lauf erfasst';
var routeTimeLabel = document.createElement('span');
routeTimeLabel.className = 'bratonien-label';
routeTimeLabel.textContent = 'Datenweg zuletzt';
var routeTimeValue = document.createElement('strong');
routeTimeValue.setAttribute('data-nc-route-time', '');
routeTimeValue.textContent = 'Nicht verfügbar';
grid.appendChild(routeLabel);
grid.appendChild(routeValue);
grid.appendChild(routeTimeLabel);
grid.appendChild(routeTimeValue);
var detail = document.createElement('p');
detail.className = 'bratonien-base-note';
detail.setAttribute('data-nc-route-detail', '');
detail.textContent = 'Der Datenweg wird nach dem nächsten Connector-Lauf angezeigt.';
statusCard.appendChild(detail);
var basePath = window.location.pathname.replace(/admin\.php.*$/, '');
var statusUrl = basePath + '_data/bratonien-tools/nc-connector-status/route-status.json';
function formatTime(timestamp) {
if (!timestamp) return 'Nicht verfügbar';
var date = new Date(Number(timestamp) * 1000);
if (Number.isNaN(date.getTime())) return 'Nicht verfügbar';
return date.toLocaleString('de-DE');
}
function render(data) {
var label = data && data.label ? String(data.label) : 'Unbekannt';
var route = data && data.route ? String(data.route) : '';
routeValue.textContent = label;
routeTimeValue.textContent = formatTime(data && data.timestamp);
if (route === 'webdav') {
routeValue.textContent = 'WEBDAV PRIMÄR';
detail.textContent = 'Portierung läuft über WebDAV. Legacy wurde in diesem Lauf nicht benutzt.';
} else if (route === 'legacy_fallback') {
routeValue.textContent = 'LEGACY-FALLBACK AKTIV';
detail.textContent = 'WebDAV war nicht erfolgreich. Dieser Lauf wurde über die alte Struktur abgefangen.';
} else if (route === 'failed') {
routeValue.textContent = 'FEHLER - KEIN ERFOLGREICHER DATENWEG';
detail.textContent = data && data.detail ? String(data.detail) : 'WebDAV und Fallback sind fehlgeschlagen.';
} else {
detail.textContent = data && data.detail ? String(data.detail) : 'Unbekannter Datenweg.';
}
}
function refresh() {
fetch(statusUrl + '?t=' + Date.now(), {
cache: 'no-store',
credentials: 'same-origin'
})
.then(function (response) {
if (!response.ok) throw new Error('status unavailable');
return response.json();
})
.then(render)
.catch(function () {
routeValue.textContent = 'Noch kein Lauf erfasst';
routeTimeValue.textContent = 'Nicht verfügbar';
detail.textContent = 'Nach dem nächsten Connector-Lauf wird hier WebDAV oder Legacy-Fallback angezeigt.';
});
}
refresh();
window.setInterval(refresh, 10000);
}
function initBratonienTabs() { function initBratonienTabs() {
var admin = document.querySelector('.bratonien-admin'); var admin = document.querySelector('.bratonien-admin');
if (!admin) return; if (!admin) return;
@@ -113,9 +201,14 @@
activate(initial, false); activate(initial, false);
} }
if (document.readyState === 'loading') { function init() {
document.addEventListener('DOMContentLoaded', initBratonienTabs);
} else {
initBratonienTabs(); initBratonienTabs();
initNcRouteStatus();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
} }
})(); })();

236
js/nc_connector_edit_v2.js Normal file
View File

@@ -0,0 +1,236 @@
(function () {
'use strict';
function ready(callback) {
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', callback);
else callback();
}
ready(function () {
var section = document.getElementById('nc-connector');
if (!section) return;
var tokenInput = section.querySelector('input[name="pwg_token"]');
var pwgToken = tokenInput ? tokenInput.value : '';
var technicalButton = document.getElementById('bratonien-nc-technical-open');
if (technicalButton) technicalButton.remove();
var technicalCreate = document.getElementById('bratonien-nc-technical-create');
if (technicalCreate) technicalCreate.remove();
var statusHeading = Array.prototype.find.call(section.querySelectorAll('h4'), function (heading) {
return heading.textContent.trim() === 'Status';
});
var statusCard = statusHeading ? statusHeading.closest('.bratonien-card') : null;
if (statusCard && pwgToken && !statusCard.querySelector('[data-nc-run-now]')) {
var runForm = document.createElement('form');
runForm.method = 'post';
runForm.className = 'bratonien-actions';
runForm.style.marginTop = '1rem';
runForm.setAttribute('data-nc-run-now', '1');
runForm.innerHTML = '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_run_now">Jetzt abgleichen</button>';
runForm.addEventListener('submit', function () {
var button = runForm.querySelector('button');
if (button) {
button.disabled = true;
button.textContent = 'Abgleich wird gestartet …';
}
});
statusCard.appendChild(runForm);
}
function escapeHtml(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c];
});
}
function loadConnection(id) {
return fetch('plugins/bratonien_tools/nc-connector-edit-data.php?connection_id='+encodeURIComponent(id)+'&_='+Date.now(), {
credentials:'same-origin', cache:'no-store', headers:{'Accept':'application/json'}
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok) throw new Error(data.error || ('HTTP '+response.status));
return data;
});
});
}
function createPostForm(label, tool, id) {
var form = document.createElement('form');
form.method = 'post';
form.style.display = 'inline';
form.innerHTML = '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<input type="hidden" name="connection_id" value="'+escapeHtml(id)+'">'
+ '<button class="buttonLike" type="submit" name="bratonien_tool" value="'+escapeHtml(tool)+'">'+escapeHtml(label)+'</button>';
return form;
}
function dialog() {
var node = document.getElementById('bratonien-nc-connection-edit-v2');
if (node) return node;
node = document.createElement('dialog');
node.id = 'bratonien-nc-connection-edit-v2';
node.className = 'bratonien-edit-dialog';
node.innerHTML = '<div class="bratonien-edit-dialog__body">'
+ '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:1rem">'
+ '<div><h4 style="margin:0">Verbindung bearbeiten</h4><p class="bratonien-base-note" style="margin:.35rem 0 0">Die Einstellungen gelten nur für diese Verbindung.</p></div>'
+ '<button type="button" class="buttonLike" data-edit-close>Schließen</button></div>'
+ '<div data-edit-content style="margin-top:1rem"></div></div>';
document.body.appendChild(node);
node.querySelector('[data-edit-close]').addEventListener('click', function () { node.close(); });
node.addEventListener('click', function (event) { if (event.target === node) node.close(); });
return node;
}
function storageRow(storage) {
storage = storage || {};
return '<div class="bratonien-storage-row" data-storage-row>'
+ '<label>Storage-ID<input name="nc_storage_id[]" value="'+escapeHtml(storage.storage_id || '')+'" required></label>'
+ '<label>Quellordner<input name="nc_source_prefix[]" value="'+escapeHtml(storage.source_prefix || '')+'" placeholder="optional"></label>'
+ '<label>Lokaler Speicherpfad<input name="nc_local_mount[]" value="'+escapeHtml(storage.local_mount || '')+'" required></label>'
+ '<button type="button" class="buttonLike" data-remove-storage>Entfernen</button></div>';
}
function showError(form, data) {
var old = form.querySelector('[data-edit-error]');
if (old) old.remove();
var box = document.createElement('div');
box.dataset.editError = '1';
box.className = 'bratonien-main-cache__warning';
box.style.margin = '0 0 1rem';
box.textContent = (data && data.message) ? data.message : 'Die Verbindung konnte nicht gespeichert werden.';
form.insertBefore(box, form.firstChild);
}
function submitLocal(form, editDialog) {
if (!form.reportValidity()) return;
var button = form.querySelector('button[type="submit"]');
if (button) button.disabled = true;
fetch('plugins/bratonien_tools/nc-connector-edit-save.php', {
method:'POST', credentials:'same-origin', cache:'no-store', headers:{'Accept':'application/json'}, body:new FormData(form)
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok || !data.ok) { showError(form, data); return; }
editDialog.close();
window.location.reload();
});
}).catch(function (error) {
showError(form, {message:error.message || String(error)});
}).finally(function () {
if (button) button.disabled = false;
});
}
function openLocalEditor(id) {
var editDialog = dialog();
var content = editDialog.querySelector('[data-edit-content]');
content.innerHTML = '<p class="bratonien-base-note">Verbindung wird geladen …</p>';
if (typeof editDialog.showModal === 'function') editDialog.showModal();
else editDialog.setAttribute('open', 'open');
loadConnection(id).then(function (data) {
if (data.adapter !== 'local') throw new Error('Diese Verbindung wird über den WebDAV-Assistenten bearbeitet.');
var legacy = data.legacy || {};
var webdav = data.webdav || {};
var storages = Array.isArray(legacy.storages) ? legacy.storages : [];
var rows = storages.map(storageRow).join('') || storageRow({});
content.innerHTML = '<form method="post" data-edit-form>'
+ '<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'
+ '<input type="hidden" name="connection_id" value="'+escapeHtml(data.id)+'">'
+ '<h5>Verbindung</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Name</label><input name="connection_name" value="'+escapeHtml(data.name)+'" required>'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Nextcloud</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Nextcloud-Adresse</label><input name="nc_nextcloud_url" value="'+escapeHtml(webdav.nextcloud_url || '')+'">'
+ '<label class="bratonien-label">Nextcloud-Benutzer</label><input name="nc_nextcloud_user" value="'+escapeHtml(webdav.nextcloud_user || '')+'" autocomplete="username">'
+ '<label class="bratonien-label">Nextcloud-Passwort</label><input name="nc_nextcloud_password" type="password" autocomplete="current-password" placeholder="'+(webdav.has_nextcloud_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Piwigo-Zugang</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">API-Schlüssel-ID</label><input name="nc_connection_api_key_id" value="'+escapeHtml(webdav.api_key_id || '')+'" autocomplete="off">'
+ '<label class="bratonien-label">API-Geheimnis</label><input name="nc_connection_api_key_secret" type="password" autocomplete="new-password" placeholder="'+(webdav.has_api_key_secret ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '<label class="bratonien-label">Fallback-Benutzer</label><input name="nc_fallback_user" value="'+escapeHtml(webdav.fallback_user || '')+'" autocomplete="username">'
+ '<label class="bratonien-label">Fallback-Passwort</label><input name="nc_fallback_password" type="password" autocomplete="current-password" placeholder="'+(webdav.has_fallback_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Lokaler Connector</h5><div class="bratonien-form-grid">'
+ '<label class="bratonien-label">Datenbank-Server</label><input name="nc_host" value="'+escapeHtml(legacy.host)+'" required>'
+ '<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="'+escapeHtml(legacy.port || 5432)+'" required>'
+ '<label class="bratonien-label">Datenbank</label><input name="nc_database" value="'+escapeHtml(legacy.database)+'" required>'
+ '<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" value="'+escapeHtml(legacy.user)+'" required>'
+ '<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" placeholder="'+(legacy.has_db_password ? 'gespeichert leer = unverändert' : 'noch nicht gespeichert')+'">'
+ '</div>'
+ '<h5 style="margin-top:1.2rem">Speicherorte</h5><div data-storage-list>'+rows+'</div>'
+ '<button type="button" class="buttonLike" data-add-storage>Speicherort hinzufügen</button>'
+ '<details style="margin-top:1rem"><summary>Erweiterte Einstellungen</summary><div class="bratonien-form-grid" style="margin-top:.75rem">'
+ '<label class="bratonien-label">Source-View</label><input name="nc_source_view" value="'+escapeHtml(legacy.source_view)+'" required>'
+ '<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" value="'+escapeHtml(legacy.activity_view)+'" required>'
+ '<label class="bratonien-label">Piwigo-Galerieordner</label><input name="nc_gallery_root" value="'+escapeHtml(legacy.gallery_root)+'" required>'
+ '<label class="bratonien-label">Ruhezeit (Sek.)</label><input name="nc_quiet_seconds" type="number" min="0" value="'+escapeHtml(legacy.quiet_seconds)+'">'
+ '<label class="bratonien-label">Maximale Wartezeit (Sek.)</label><input name="nc_max_wait_seconds" type="number" min="60" value="'+escapeHtml(legacy.max_wait_seconds)+'">'
+ '<label class="bratonien-label">Vollprüfung nach (Sek.)</label><input name="nc_full_sync_seconds" type="number" min="300" value="'+escapeHtml(legacy.full_sync_seconds)+'">'
+ '</div></details>'
+ '<div class="bratonien-actions" style="margin-top:1rem"><button class="buttonLike" type="submit">Änderungen prüfen und speichern</button><button class="buttonLike" type="button" data-cancel>Abbrechen</button></div>'
+ '</form>';
var form = content.querySelector('[data-edit-form]');
form.querySelector('[data-cancel]').addEventListener('click', function () { editDialog.close(); });
form.querySelector('[data-add-storage]').addEventListener('click', function () {
form.querySelector('[data-storage-list]').insertAdjacentHTML('beforeend', storageRow({}));
});
form.addEventListener('click', function (event) {
var remove = event.target.closest('[data-remove-storage]');
if (remove) {
var row = remove.closest('[data-storage-row]');
if (row) row.remove();
}
});
form.addEventListener('submit', function (event) {
event.preventDefault();
submitLocal(form, editDialog);
});
}).catch(function (error) {
content.innerHTML = '<p class="bratonien-main-cache__warning"><strong>Bearbeiten nicht möglich:</strong> '+escapeHtml(error.message || String(error))+'</p>';
});
}
[].slice.call(section.querySelectorAll('button[value="nc_connector_edit_start"]')).forEach(function (button) {
var form = button.closest('form');
if (form) form.remove();
});
[].slice.call(section.querySelectorAll('button[value="nc_connector_migrate_start"]')).forEach(function (button) {
var form = button.closest('form');
if (form) form.remove();
});
[].slice.call(section.querySelectorAll('button[value="nc_connector_delete"]')).forEach(function (deleteButton) {
var deleteForm = deleteButton.closest('form');
if (!deleteForm || !deleteForm.parentElement) return;
var idInput = deleteForm.querySelector('input[name="connection_id"]');
if (!idInput) return;
var id = idInput.value;
var actions = deleteForm.parentElement;
loadConnection(id).then(function (data) {
var edit;
if (data.adapter === 'local') {
edit = document.createElement('button');
edit.type = 'button';
edit.className = 'buttonLike';
edit.textContent = 'Bearbeiten';
edit.addEventListener('click', function () { openLocalEditor(id); });
} else {
edit = createPostForm('Bearbeiten', 'nc_connector_edit_start', id);
}
actions.insertBefore(edit, deleteForm);
}).catch(function (error) {
var info = document.createElement('span');
info.className = 'bratonien-main-cache__warning';
info.textContent = 'Verbindung konnte nicht geladen werden: '+(error.message || String(error));
actions.insertBefore(info, deleteForm);
});
});
});
})();

View File

@@ -1,7 +1,7 @@
<?php <?php
/* /*
Plugin Name: Bratonien Tools Plugin Name: Bratonien Tools
Version: 0.9.5.21 Version: 0.9.7.1
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation. Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien Author: Bratonien
@@ -24,6 +24,7 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_ws.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php');
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_admin_plugin_menu_links', 'bratonien_tools_admin_menu');
add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4); add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4);
@@ -35,6 +36,7 @@ add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EV
add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', EVENT_HANDLER_PRIORITY_NEUTRAL - 20); add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', EVENT_HANDLER_PRIORITY_NEUTRAL - 20);
add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10); add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10);
add_event_handler('init', 'bratonien_tools_album_shares_init'); add_event_handler('init', 'bratonien_tools_album_shares_init');
add_event_handler('init', 'bratonien_tools_nc_scheduler_tick', EVENT_HANDLER_PRIORITY_NEUTRAL + 100);
add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories'); add_event_handler('delete_categories', 'bratonien_tools_album_shares_on_delete_categories');
add_event_handler('ws_add_methods', 'bratonien_tools_register_ws_methods'); add_event_handler('ws_add_methods', 'bratonien_tools_register_ws_methods');
add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods'); add_event_handler('ws_add_methods', 'bratonien_tools_register_nc_orphan_ws_methods');

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__) . '/include/database.class.php');
require_once(dirname(__FILE__) . '/tools/watermark_profiles.inc.php'); require_once(dirname(__FILE__) . '/tools/watermark_profiles.inc.php');
require_once(dirname(__FILE__) . '/include/dependencies.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 class bratonien_tools_maintain extends PluginMaintain
{ {
@@ -18,9 +19,14 @@ class bratonien_tools_maintain extends PluginMaintain
$dependency_messages = array(); $dependency_messages = array();
bratonien_tools_ensure_dependencies($dependency_messages); 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')) if (function_exists('conf_update_param'))
{ {
conf_update_param('bratonien_nc_scheduler_interval', 60);
conf_update_param('bratonien_dependency_status', json_encode(array( conf_update_param('bratonien_dependency_status', json_encode(array(
'checked_at' => time(), 'checked_at' => time(),
'messages' => $dependency_messages, 'messages' => $dependency_messages,

View File

@@ -0,0 +1,93 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
http_response_code(404);
echo json_encode(array('error'=>'Bratonien Tools ist nicht aktiv.'));
exit;
}
if (!function_exists('is_admin') || !is_admin())
{
http_response_code(403);
echo json_encode(array('error'=>'Administratorrechte erforderlich.'));
exit;
}
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_connection_scope.inc.php');
$id = (int)($_GET['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
http_response_code(404);
echo json_encode(array('error'=>'Connector-Verbindung wurde nicht gefunden.'));
exit;
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = array();
try
{
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
}
catch (Throwable $e)
{
$credentials = array();
}
$storages = array();
foreach ((array)($config['storages'] ?? array()) as $storage)
{
$storages[] = array(
'storage_id'=>(string)($storage['storage_id'] ?? ''),
'source_prefix'=>(string)($storage['source_prefix'] ?? ''),
'local_mount'=>(string)($storage['local_mount'] ?? ''),
);
}
$payload = array(
'id'=>(int)$connection['id'],
'name'=>(string)$connection['name'],
'adapter'=>(string)$connection['adapter'],
'enabled'=>(bool)$connection['enabled'],
'takeover_state'=>(string)$connection['takeover_state'],
'source_mode'=>(string)($config['source_mode'] ?? ''),
'legacy'=>array(
'host'=>(string)($config['host'] ?? ''),
'port'=>(string)($config['port'] ?? '5432'),
'database'=>(string)($config['database'] ?? ''),
'user'=>(string)($config['user'] ?? ''),
'has_db_password'=>trim((string)($credentials['db_password'] ?? '')) !== '',
'source_view'=>(string)($config['source_view'] ?? ''),
'activity_view'=>(string)($config['activity_view'] ?? ''),
'gallery_root'=>(string)($config['gallery_root'] ?? ''),
'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),
'storages'=>$storages,
),
'webdav'=>array(
'nextcloud_url'=>(string)($config['nextcloud_url'] ?? ''),
'nextcloud_user'=>(string)($credentials['nextcloud_user'] ?? $config['nextcloud_access_user'] ?? $config['access_user'] ?? ''),
'has_nextcloud_password'=>(string)($credentials['nextcloud_password'] ?? '') !== '',
'api_key_id'=>(string)($credentials['api_key_id'] ?? ''),
'has_api_key_secret'=>trim((string)($credentials['api_key_secret'] ?? '')) !== '',
'fallback_user'=>(string)($credentials['piwigo_user'] ?? ''),
'has_fallback_password'=>(string)($credentials['piwigo_password'] ?? '') !== '',
),
);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

356
nc-connector-edit-save.php Normal file
View File

@@ -0,0 +1,356 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
function bratonien_tools_nc_edit_json($status, array $payload)
{
http_response_code((int)$status);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function bratonien_tools_nc_edit_fail($message, array $fields = array(), $stage = '', $detail = '')
{
$visible = trim((string)$message);
if ($stage !== '') $visible = 'Bereich: '.$stage.' — '.$visible;
if ($detail !== '') $visible .= ' Technische Ursache: '.$detail;
bratonien_tools_nc_edit_json(422, array(
'ok'=>false,
'message'=>$visible,
'stage'=>(string)$stage,
'detail'=>(string)$detail,
'fields'=>array_values(array_unique($fields)),
));
}
function bratonien_tools_nc_edit_probe($url, $username = '', $password = '', array $headers = array())
{
if (!function_exists('curl_init'))
{
return array('ok'=>false, 'http'=>0, 'errno'=>-1, 'error'=>'cURL ist in PHP nicht verfügbar.', 'body'=>'');
}
$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-Editor/'.(function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : 'dev'),
);
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);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return array(
'ok'=>$body !== false && $errno === 0,
'http'=>$http,
'errno'=>$errno,
'error'=>$error,
'body'=>$body === false ? '' : (string)$body,
);
}
if (!defined('BRATONIEN_TOOLS_PATH'))
{
bratonien_tools_nc_edit_json(404, array('ok'=>false, 'message'=>'Bratonien Tools ist nicht aktiv.', 'fields'=>array()));
}
if (!function_exists('is_admin') || !is_admin())
{
bratonien_tools_nc_edit_json(403, array('ok'=>false, 'message'=>'Administratorrechte erforderlich.', 'fields'=>array()));
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST')
{
bratonien_tools_nc_edit_json(405, array('ok'=>false, 'message'=>'Nur POST ist erlaubt.', 'fields'=>array()));
}
try
{
check_pwg_token();
require_once(BRATONIEN_TOOLS_PATH.'include/tool_registry.inc.php');
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
bratonien_tools_nc_edit_fail('Die zu bearbeitende Verbindung wurde nicht gefunden.', array(), 'Verbindung');
}
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$credentials = bratonien_tools_nc_connector_scoped_secret($connection);
$nextcloud_input = trim((string)($_POST['nc_nextcloud_url'] ?? ($config['nextcloud_url'] ?? '')));
$nextcloud_user = trim((string)($_POST['nc_nextcloud_user'] ?? ($credentials['nextcloud_user'] ?? '')));
$submitted_nc_password = (string)($_POST['nc_nextcloud_password'] ?? '');
$nextcloud_password = $submitted_nc_password !== '' ? $submitted_nc_password : (string)($credentials['nextcloud_password'] ?? '');
$has_any_nextcloud = $nextcloud_input !== '' || $nextcloud_user !== '' || $nextcloud_password !== '';
if ($has_any_nextcloud)
{
$missing = array();
if ($nextcloud_input === '') $missing[] = 'nc_nextcloud_url';
if ($nextcloud_user === '') $missing[] = 'nc_nextcloud_user';
if ($nextcloud_password === '') $missing[] = 'nc_nextcloud_password';
if ($missing)
{
bratonien_tools_nc_edit_fail(
'Die WebDAV-Daten sind unvollständig. Adresse, Benutzer und Passwort werden gemeinsam benötigt.',
$missing,
'Nextcloud / WebDAV'
);
}
try
{
$candidate_urls = bratonien_tools_nc_wizard_candidate_urls($nextcloud_input);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail($e->getMessage(), array('nc_nextcloud_url'), 'Nextcloud / WebDAV');
}
$nextcloud_url = '';
$last_probe = null;
$last_status_url = '';
foreach ($candidate_urls as $candidate_url)
{
$status_url = $candidate_url.'/status.php';
$probe = bratonien_tools_nc_edit_probe($status_url);
$last_probe = $probe;
$last_status_url = $status_url;
if (!$probe['ok'] || $probe['http'] < 200 || $probe['http'] >= 300)
{
continue;
}
$status = json_decode($probe['body'], true);
if (!is_array($status) || empty($status['installed']))
{
continue;
}
$nextcloud_url = $candidate_url;
break;
}
if ($nextcloud_url === '')
{
$attempts = array();
foreach ($candidate_urls as $candidate_url)
{
$attempts[] = $candidate_url.'/status.php';
}
if (is_array($last_probe) && !$last_probe['ok'])
{
$technical = $last_probe['errno'] >= 0
? 'cURL '.$last_probe['errno'].($last_probe['error'] !== '' ? ': '.$last_probe['error'] : '')
: $last_probe['error'];
bratonien_tools_nc_edit_fail(
'Die Nextcloud-Adresse ist vom Piwigo-Server aus nicht erreichbar. Ohne angegebenes Protokoll wurden HTTPS und HTTP geprüft.',
array('nc_nextcloud_url'),
'Nextcloud / WebDAV Erreichbarkeit',
$technical.' · Geprüft: '.implode(' ; ', $attempts)
);
}
bratonien_tools_nc_edit_fail(
'Unter der angegebenen Adresse wurde keine erreichbare installierte Nextcloud gefunden. Ohne angegebenes Protokoll wurden HTTPS und HTTP geprüft.',
array('nc_nextcloud_url'),
'Nextcloud / WebDAV Erkennung',
'Geprüft: '.implode(' ; ', $attempts).($last_probe ? ' · Letzter HTTP-Status: '.$last_probe['http'] : '')
);
}
$_POST['nc_nextcloud_url'] = $nextcloud_url;
$user_url = $nextcloud_url.'/ocs/v2.php/cloud/user?format=json';
$user_probe = bratonien_tools_nc_edit_probe($user_url, $nextcloud_user, $nextcloud_password, array('OCS-APIRequest: true'));
if (!$user_probe['ok'])
{
$technical = $user_probe['errno'] >= 0
? 'cURL '.$user_probe['errno'].($user_probe['error'] !== '' ? ': '.$user_probe['error'] : '')
: $user_probe['error'];
bratonien_tools_nc_edit_fail(
'Die Nextcloud-Anmeldung konnte technisch nicht geprüft werden.',
array('nc_nextcloud_url','nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung',
$technical.' · Ziel: '.$user_url
);
}
if ($user_probe['http'] === 401 || $user_probe['http'] === 403)
{
bratonien_tools_nc_edit_fail(
'Nextcloud hat Benutzername oder Passwort abgelehnt.',
array('nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung',
'HTTP '.$user_probe['http']
);
}
if ($user_probe['http'] < 200 || $user_probe['http'] >= 300)
{
bratonien_tools_nc_edit_fail(
'Nextcloud ist erreichbar, aber die Benutzerprüfung wurde vom Server abgelehnt.',
array('nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung',
'HTTP '.$user_probe['http'].' · Ziel: '.$user_url
);
}
try
{
bratonien_tools_nc_wizard_ocs_data($user_probe['body']);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail(
'Nextcloud antwortet auf die Benutzerprüfung, die Antwort ist aber nicht gültig: '.$e->getMessage(),
array('nc_nextcloud_user','nc_nextcloud_password'),
'Nextcloud / WebDAV Anmeldung'
);
}
}
$api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? ($credentials['api_key_id'] ?? '')));
$submitted_api_secret = trim((string)($_POST['nc_connection_api_key_secret'] ?? ''));
$api_key_secret = $submitted_api_secret !== '' ? $submitted_api_secret : trim((string)($credentials['api_key_secret'] ?? ''));
if (($api_key_id === '') !== ($api_key_secret === ''))
{
$missing = $api_key_id === '' ? array('nc_connection_api_key_id') : array('nc_connection_api_key_secret');
bratonien_tools_nc_edit_fail(
'API-Schlüssel-ID und API-Geheimnis müssen gemeinsam vollständig sein.',
$missing,
'Piwigo API'
);
}
if ($api_key_id !== '')
{
try
{
bratonien_tools_nc_connector_validate_scoped_api($api_key_id, $api_key_secret);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail(
'Der Piwigo-API-Zugang konnte nicht bestätigt werden: '.$e->getMessage(),
array('nc_connection_api_key_id','nc_connection_api_key_secret'),
'Piwigo API'
);
}
}
$fallback_user = trim((string)($_POST['nc_fallback_user'] ?? ($credentials['piwigo_user'] ?? '')));
$submitted_fallback_password = (string)($_POST['nc_fallback_password'] ?? '');
$fallback_password = $submitted_fallback_password !== '' ? $submitted_fallback_password : (string)($credentials['piwigo_password'] ?? '');
if (($fallback_user === '') !== ($fallback_password === ''))
{
bratonien_tools_nc_edit_fail(
'Fallback-Benutzer und Fallback-Passwort müssen gemeinsam vollständig sein.',
array('nc_fallback_user','nc_fallback_password'),
'Piwigo-Fallback'
);
}
if ($fallback_user !== '')
{
try
{
bratonien_tools_nc_connector_validate_fallback_credentials($fallback_user, $fallback_password);
}
catch (Throwable $e)
{
bratonien_tools_nc_edit_fail(
'Der Piwigo-Fallback wurde abgelehnt: '.$e->getMessage(),
array('nc_fallback_user','nc_fallback_password'),
'Piwigo-Fallback'
);
}
}
$result = bratonien_tools_nc_connector_update_local_friendly();
if (array_key_exists('nc_fallback_user', $_POST) || array_key_exists('nc_fallback_password', $_POST))
{
$updated = bratonien_tools_nc_connector_connection($id, true);
if (!$updated) throw new RuntimeException('Die Verbindung konnte nach dem Speichern nicht erneut geladen werden.');
$updated_credentials = bratonien_tools_nc_connector_scoped_secret($updated);
$updated_credentials['piwigo_user'] = $fallback_user;
$updated_credentials['piwigo_password'] = $fallback_password;
bratonien_tools_nc_connector_store_scoped_secret($id, $updated, $updated_credentials);
}
bratonien_tools_nc_edit_json(200, array(
'ok'=>true,
'message'=>(string)($result['message'] ?? 'Verbindung wurde gespeichert.'),
'fields'=>array(),
));
}
catch (Throwable $e)
{
$message = trim($e->getMessage());
if ($message === '') $message = 'Die Verbindung konnte nicht gespeichert werden.';
$fields = array();
$stage = 'Verbindung';
$add = function($name) use (&$fields)
{
if (!in_array($name, $fields, true)) $fields[] = $name;
};
if (stripos($message, 'Name') !== false && stripos($message, 'Datenbankname') === false) $add('connection_name');
if (stripos($message, 'Datenbank-Server') !== false) { $add('nc_host'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Datenbank-Port') !== false) { $add('nc_port'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Datenbankname') !== false) { $add('nc_database'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Reader-Benutzer') !== false) { $add('nc_user'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Datenbankpasswort') !== false) { $add('nc_db_password'); $stage = 'Legacy-Datenbank'; }
if (stripos($message, 'Storage-ID') !== false) { $add('nc_storage_id[]'); $stage = 'Speicherorte'; }
if (stripos($message, 'lokaler Pfad') !== false || stripos($message, 'Speicherort') !== false) { $add('nc_local_mount[]'); $stage = 'Speicherorte'; }
if (stripos($message, 'Galerieordner') !== false) { $add('nc_gallery_root'); $stage = 'Erweiterte Legacy-Einstellungen'; }
if (stripos($message, 'Datenbankansichten') !== false || stripos($message, 'Source-View') !== false) { $add('nc_source_view'); $stage = 'Erweiterte Legacy-Einstellungen'; }
if (stripos($message, 'Datenbankansichten') !== false || stripos($message, 'Activity-View') !== false) { $add('nc_activity_view'); $stage = 'Erweiterte Legacy-Einstellungen'; }
if (stripos($message, 'Nextcloud') !== false)
{
$stage = 'Nextcloud / WebDAV';
if (stripos($message, 'Adresse') !== false) $add('nc_nextcloud_url');
if (stripos($message, 'Benutzer') !== false) $add('nc_nextcloud_user');
if (stripos($message, 'Passwort') !== false) $add('nc_nextcloud_password');
}
if (stripos($message, 'API') !== false)
{
$stage = 'Piwigo API';
$add('nc_connection_api_key_id');
$add('nc_connection_api_key_secret');
}
if (stripos($message, 'Fallback') !== false)
{
$stage = 'Piwigo-Fallback';
$add('nc_fallback_user');
$add('nc_fallback_password');
}
bratonien_tools_nc_edit_fail(
$message,
$fields,
$stage,
'Interne Prüfung: '.get_class($e)
);
}

View File

@@ -0,0 +1,104 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
function bratonien_tools_nc_source_picker_json($status, array $payload)
{
http_response_code((int)$status);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function bratonien_tools_nc_source_picker_fail($message, array $fields = array())
{
bratonien_tools_nc_source_picker_json(422, array(
'ok'=>false,
'message'=>(string)$message,
'fields'=>array_values(array_unique($fields)),
));
}
if (!defined('BRATONIEN_TOOLS_PATH'))
{
bratonien_tools_nc_source_picker_json(404, array('ok'=>false, 'message'=>'Bratonien Tools ist nicht aktiv.', 'fields'=>array()));
}
if (!function_exists('is_admin') || !is_admin())
{
bratonien_tools_nc_source_picker_json(403, array('ok'=>false, 'message'=>'Administratorrechte erforderlich.', 'fields'=>array()));
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST')
{
bratonien_tools_nc_source_picker_json(405, array('ok'=>false, 'message'=>'Nur POST ist erlaubt.', 'fields'=>array()));
}
try
{
check_pwg_token();
require_once(BRATONIEN_TOOLS_PATH.'include/tool_registry.inc.php');
$id = (int)($_POST['connection_id'] ?? 0);
$connection = bratonien_tools_nc_connector_connection($id, true);
if (!$connection)
{
bratonien_tools_nc_source_picker_fail('Die zu bearbeitende Verbindung wurde nicht gefunden.');
}
if ((string)$connection['adapter'] !== 'local')
{
bratonien_tools_nc_source_picker_fail('Die automatische Migrationsauswahl ist nur für die bestehende Legacy-Verbindung vorgesehen.');
}
$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'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
$missing = array();
if ($base_url === '') $missing[] = 'nc_nextcloud_url';
if ($username === '') $missing[] = 'nc_nextcloud_user';
if ($password === '') $missing[] = 'nc_nextcloud_password';
if ($missing)
{
bratonien_tools_nc_source_picker_fail(
'Für die automatische Ordnerauswahl müssen Nextcloud-Adresse, Benutzer und Passwort gespeichert sein.',
$missing
);
}
bratonien_tools_nc_connector_prepare_webdav_wizard_from_connection($connection, 'migrate');
$state = bratonien_tools_nc_wizard_state();
if ((int)($state['step'] ?? 0) !== 2 || empty($state['scan_ok']) || empty($state['directory_selection_ready']))
{
$detail = trim((string)($state['technical_error'] ?? ''));
bratonien_tools_nc_source_picker_fail(
$detail !== '' ? 'Die Nextcloud-Ordner konnten nicht geladen werden: '.$detail : 'Die Nextcloud-Ordner konnten nicht geladen werden.',
array('nc_nextcloud_url','nc_nextcloud_user','nc_nextcloud_password')
);
}
bratonien_tools_nc_source_picker_json(200, array(
'ok'=>true,
'message'=>'Nextcloud-Ordner wurden geladen. Die Auswahl wird geöffnet.',
'fields'=>array(),
));
}
catch (Throwable $e)
{
$message = trim($e->getMessage());
if ($message === '') $message = 'Die Nextcloud-Ordnerauswahl konnte nicht vorbereitet werden.';
bratonien_tools_nc_source_picker_fail(
$message,
array('nc_nextcloud_url','nc_nextcloud_user','nc_nextcloud_password')
);
}

View File

@@ -36,6 +36,11 @@ $latest = array(
'api'=>array('state'=>'not_run','message'=>''), 'api'=>array('state'=>'not_run','message'=>''),
'fallback'=>array('state'=>'not_run','message'=>''), 'fallback'=>array('state'=>'not_run','message'=>''),
'error_detail'=>'', 'error_detail'=>'',
'route'=>'',
'route_label'=>'Noch kein Datenweg erfasst',
'route_timestamp'=>0,
'route_time_label'=>'Nicht verfügbar',
'route_detail'=>'',
); );
if (is_dir($dir)) if (is_dir($dir))
@@ -57,6 +62,54 @@ if (is_dir($dir))
$latest['connection_file'] = basename($file); $latest['connection_file'] = basename($file);
} }
} }
$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);
}
}
} }
if ((int)$latest['timestamp'] > 0) if ((int)$latest['timestamp'] > 0)

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${PIWIGO_CONFIG:-}"
[[ -n "$CONFIG_FILE" && -r "$CONFIG_FILE" ]] || { echo "WebDAV-Konfiguration fehlt: ${CONFIG_FILE:-<leer>}" >&2; exit 1; }
# shellcheck source=/dev/null
source "$CONFIG_FILE"
: "${PIWIGO_ROOT:?PIWIGO_ROOT fehlt}"
: "${CONNECTION_ID:?CONNECTION_ID fehlt}"
: "${WEBDAV_BASE_URL:?WEBDAV_BASE_URL fehlt}"
: "${WEBDAV_USER:?WEBDAV_USER fehlt}"
: "${WEBDAV_PASSWORD_FILE:?WEBDAV_PASSWORD_FILE fehlt}"
: "${WEBDAV_MAPPING_FILE:?WEBDAV_MAPPING_FILE fehlt}"
: "${STATE_DIR:?STATE_DIR fehlt}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
LOCK_FILE="$STATE_DIR/webdav-media.lock"
mkdir -p -- "$STATE_DIR"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
PREVIEW_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-preview/connection-$CONNECTION_ID"
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"
php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID"

View File

@@ -0,0 +1,169 @@
#!/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/cleanup-webdav-piwigo.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');
function bratonien_cleanup_tree($path, $allowed_root)
{
$path = rtrim((string)$path, '/');
$allowed_root = rtrim((string)$allowed_root, '/');
if ($path === '' || $allowed_root === '' || strpos($path, $allowed_root.'/') !== 0 || !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;
bratonien_cleanup_tree($path.'/'.$item, $allowed_root);
}
@rmdir($path);
}
function bratonien_webdav_site_images($site_id)
{
$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))
{
$row['id'] = (int)$row['id'];
$rows[$row['id']] = $row;
}
return $rows;
}
try
{
$connection_table = function_exists('bratonien_tools_nc_connector_table')
? bratonien_tools_nc_connector_table()
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$active = array();
$exists = pwg_query("SHOW TABLES LIKE '".pwg_db_real_escape_string($connection_table)."'");
if (pwg_db_num_rows($exists))
{
$result = pwg_query("SELECT id FROM `".$connection_table."`");
while ($row = pwg_db_fetch_assoc($result))
{
$active[(int)$row['id']] = true;
}
}
$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))
{
$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];
if ($connection_id < 1 || isset($active[$connection_id])) continue;
$images = bratonien_webdav_site_images($site_id);
foreach ($images as $row)
{
try
{
delete_element_derivatives($row);
}
catch (Throwable $e)
{
fwrite(STDERR, 'WebDAV-Cleanup: Derivate von Bild #'.(int)$row['id'].' konnten nicht vollständig entfernt werden: '.$e->getMessage()."\n");
}
}
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++;
$removed_images += count($images);
echo 'NC WebDAV: entferne verwaiste Piwigo-Site '.$site_id.' für gelöschte 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';
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($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)
{
invalidate_user_cache(true);
}
echo 'NC WebDAV Piwigo-Cleanup: sites='.$removed_sites.' bilder='.$removed_images."\n";
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, 'NC WebDAV Piwigo-Cleanup: '.$e->getMessage()."\n");
exit(1);
}

View File

@@ -6,6 +6,8 @@ if (PHP_SAPI !== 'cli')
exit(1); exit(1);
} }
const BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION = '0.9.6.1';
$options = getopt('', array('piwigo-root:', 'connection-id:')); $options = getopt('', array('piwigo-root:', 'connection-id:'));
$piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/'); $piwigo_root = rtrim((string)($options['piwigo-root'] ?? ''), '/');
$connection_id = (int)($options['connection-id'] ?? 0); $connection_id = (int)($options['connection-id'] ?? 0);
@@ -31,7 +33,7 @@ $_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/lib/build-webdav-derivatives.php'; $_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/lib/build-webdav-derivatives.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME']; $_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = ''; $_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Derivative-Builder/0.9.5.21'; $_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Derivative-Builder/'.BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION;
$_SERVER['HTTPS'] = 'off'; $_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php'); require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
@@ -53,10 +55,11 @@ try
} }
$images = 0; $images = 0;
$generated = 0; $generated_or_ready = 0;
$identity = 0; $identity = 0;
$metadata_repaired = 0; $metadata_repaired = 0;
$errors = 0; $errors = 0;
$error_lines = array();
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' ORDER BY id'); $result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result)) while ($row = pwg_db_fetch_assoc($result))
@@ -70,7 +73,15 @@ try
if (!$preview) if (!$preview)
{ {
$errors++; $errors++;
fwrite(STDERR, 'Bild #'.$image_id.": vorbereitetes WebDAV-Preview fehlt.\n"); $error_lines[] = 'Bild #'.$image_id.': vorbereitetes WebDAV-Preview fehlt.';
continue;
}
$preview_ext = strtolower(pathinfo($preview, PATHINFO_EXTENSION));
if (!in_array($preview_ext, array('jpg', 'jpeg', 'png', 'gif'), true))
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.': Preview-Format '.$preview_ext.' ist nicht Piwigo-kompatibel; Precache muss neu erzeugt werden.';
continue; continue;
} }
@@ -78,7 +89,7 @@ try
if (!is_array($size) || empty($size[0]) || empty($size[1])) if (!is_array($size) || empty($size[0]) || empty($size[1]))
{ {
$errors++; $errors++;
fwrite(STDERR, 'Bild #'.$image_id.": Abmessungen des WebDAV-Previews konnten nicht gelesen werden.\n"); $error_lines[] = 'Bild #'.$image_id.': Preview ist kein lesbares Bild: '.$preview;
continue; continue;
} }
@@ -109,20 +120,21 @@ try
continue; continue;
} }
if (bratonien_tools_webdav_generate_derivative($params, $src)) $detail = '';
if (bratonien_tools_webdav_generate_derivative($params, $src, $detail))
{ {
$generated++; $generated_or_ready++;
} }
else else
{ {
$errors++; $errors++;
fwrite(STDERR, 'Bild #'.$image_id.' '.$variant_name.": Derivat konnte nicht erzeugt werden.\n"); $error_lines[] = 'Bild #'.$image_id.' '.$variant_name.': '.($detail !== '' ? $detail : 'Derivat konnte nicht erzeugt werden.');
} }
} }
catch (Throwable $e) catch (Throwable $e)
{ {
$errors++; $errors++;
fwrite(STDERR, 'Bild #'.$image_id.' '.$variant_name.': '.$e->getMessage()."\n"); $error_lines[] = 'Bild #'.$image_id.' '.$variant_name.': '.get_class($e).': '.$e->getMessage();
} }
} }
} }
@@ -133,16 +145,30 @@ try
invalidate_user_cache(true); invalidate_user_cache(true);
} }
echo 'WebDAV-Derivate: bilder='.$images. if ($errors > 0)
{
foreach (array_slice($error_lines, 0, 30) as $line)
{
fwrite(STDERR, $line."\n");
}
if (count($error_lines) > 30)
{
fwrite(STDERR, 'Weitere Fehler: '.(count($error_lines) - 30)."\n");
}
}
echo 'WebDAV-Derivative-Builder: version='.BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION.
' bilder='.$images.
' varianten='.count($variants). ' varianten='.count($variants).
' erzeugt='.$generated. ' bereit='.$generated_or_ready.
' identisch='.$identity. ' identisch='.$identity.
' metadaten_repariert='.$metadata_repaired. ' metadaten_repariert='.$metadata_repaired.
' fehler='.$errors."\n"; ' fehler='.$errors."\n";
exit($errors > 0 ? 1 : 0); exit($errors > 0 ? 1 : 0);
} }
catch (Throwable $e) catch (Throwable $e)
{ {
fwrite(STDERR, 'WebDAV-Derivate: '.$e->getMessage()."\n"); fwrite(STDERR, 'WebDAV-Derivate: '.get_class($e).': '.$e->getMessage()."\n");
exit(1); exit(1);
} }

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

@@ -1,9 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Build a placeholder-backed local source tree from Nextcloud WebDAV. """Build a placeholder-backed local source tree from Nextcloud WebDAV.
This is intentionally additive: it does not replace the existing local-storage This creates only tiny placeholder files plus a metadata mapping; no Nextcloud
connector path. It creates only tiny placeholder files plus a metadata mapping; original media is downloaded. The authenticated Nextcloud user is never used as
no Nextcloud original media is downloaded. an album name.
""" """
from __future__ import annotations from __future__ import annotations
@@ -14,6 +14,7 @@ import getpass
import json import json
import os import os
import shutil import shutil
import socket
import ssl import ssl
import sys import sys
import tempfile import tempfile
@@ -21,13 +22,12 @@ import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from contextlib import contextmanager
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
DAV = "DAV:" DAV = "DAV:"
OC = "http://owncloud.org/ns" OC = "http://owncloud.org/ns"
SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
# 1x1 transparent GIF, 34 bytes. The filename keeps the remote extension;
# the placeholder exists only so Piwigo can discover the logical image entry.
PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==") PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
@@ -54,9 +54,37 @@ def safe_local_name(name: str) -> str:
return name return name
@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: 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("/") 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.user = user
self.password = password self.password = password
self.timeout = timeout self.timeout = timeout
@@ -86,9 +114,10 @@ class WebDavClient:
request.add_header("Depth", "1") request.add_header("Depth", "1")
request.add_header("Content-Type", "application/xml; charset=utf-8") request.add_header("Content-Type", "application/xml; charset=utf-8")
try: try:
with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response: with pinned_resolution(self.host, self.connect_ip):
status = response.status with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response:
payload = response.read() status = response.status
payload = response.read()
except urllib.error.HTTPError as error: except urllib.error.HTTPError as error:
if error.code in {401, 403}: if error.code in {401, 403}:
fail("Nextcloud rejected the WebDAV credentials or directory access") fail("Nextcloud rejected the WebDAV credentials or directory access")
@@ -98,14 +127,14 @@ class WebDavClient:
if status != 207: if status != 207:
fail(f"Nextcloud PROPFIND returned HTTP {status}") 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: try:
root = ET.fromstring(payload) root = ET.fromstring(payload)
except ET.ParseError as error: except ET.ParseError as error:
raise RuntimeError("Nextcloud returned invalid WebDAV XML") from 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"): for response in root.findall(f"{{{DAV}}}response"):
href = response.findtext(f"{{{DAV}}}href", default="") href = response.findtext(f"{{{DAV}}}href", default="")
href_path = urllib.parse.unquote(urllib.parse.urlparse(href).path).rstrip("/") href_path = urllib.parse.unquote(urllib.parse.urlparse(href).path).rstrip("/")
@@ -148,13 +177,7 @@ def link_placeholder(seed: Path, target: Path) -> None:
target.write_bytes(PLACEHOLDER) target.write_bytes(PLACEHOLDER)
def build_root( def build_root(client: WebDavClient, remote_root: str, local_root: Path, seed: Path, mapping: dict[str, dict[str, object]]) -> tuple[int, int, int]:
client: WebDavClient,
remote_root: str,
local_root: Path,
seed: Path,
mapping: dict[str, dict[str, object]],
) -> tuple[int, int, int]:
files = 0 files = 0
folders = 0 folders = 0
skipped = 0 skipped = 0
@@ -221,6 +244,7 @@ def atomic_text(path: Path, text: str) -> None:
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--base-url", required=True) 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("--user", required=True)
parser.add_argument("--password-file", type=Path) 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") parser.add_argument("--root", action="append", required=True, help="WebDAV path relative to the authenticated user's files root")
@@ -249,26 +273,40 @@ def main() -> int:
shutil.rmtree(staging) shutil.rmtree(staging)
staging.mkdir(parents=True) 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]] = {} mapping: dict[str, dict[str, object]] = {}
manifest: list[str] = [] manifest: list[str] = []
total_files = total_folders = total_skipped = 0 total_files = total_folders = total_skipped = 0
used_names: set[str] = set() used_fileids: set[int] = set()
for remote_root_raw in args.root: for remote_root_raw in args.root:
remote_root = validate_relative(remote_root_raw) 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"]) fileid = int(current["fileid"])
display = str(current.get("display_name", "")).strip() or (PurePosixPath(remote_root).name if remote_root else args.user) if fileid in used_fileids:
local_name = f"root-{fileid}"
if local_name in used_names:
fail(f"duplicate selected Nextcloud root fileid: {fileid}") 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 local_root = staging / local_name
files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping) files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping)
total_files += files total_files += files
total_folders += folders total_folders += folders
total_skipped += skipped total_skipped += skipped
if remote_root == "":
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}") manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}")
if previous.exists(): if previous.exists():
@@ -297,6 +335,7 @@ def main() -> int:
atomic_json(args.mapping, { atomic_json(args.mapping, {
"version": 1, "version": 1,
"base_url": args.base_url.rstrip("/"), "base_url": args.base_url.rstrip("/"),
"connect_ip": client.connect_ip,
"user": args.user, "user": args.user,
"files": final_mapping, "files": final_mapping,
}) })
@@ -305,6 +344,7 @@ def main() -> int:
"files": total_files, "files": total_files,
"folders": total_folders, "folders": total_folders,
"skipped": total_skipped, "skipped": total_skipped,
"connect_ip": client.connect_ip,
"source_dir": str(source_dir), "source_dir": str(source_dir),
"manifest": str(args.manifest), "manifest": str(args.manifest),
"mapping": str(args.mapping), "mapping": str(args.mapping),

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

@@ -252,8 +252,6 @@ try
); );
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id), $headers)); decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id), $headers));
$orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), $headers)); $orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), $headers));
// Entfernt alte technische bratonien-webdav-N Wrapper aus Site 1,
// nachdem deren generierte Verzeichnisse beim Reconcile entfernt wurden.
if ($site_id !== 1) if ($site_id !== 1)
{ {
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), $headers)); decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), $headers));
@@ -284,16 +282,31 @@ try
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)); 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, // Der Fallback darf keinen zweiten Strukturpfad benutzen. Auch mit
array('sync'=>'files','display_info'=>1,'privacy_level'=>0,'sync_meta'=>1,'simulate'=>0,'subcats-included'=>1,'bratonien_connector'=>1,'submit'=>1), // Benutzer/Passwort wird exakt derselbe Bratonien-Sync wie mit API-Key
// ausgefuehrt. Dadurch werden alte technische WebDAV-Kategorien entfernt
// und vorhandene Piwigo-Alben wiederverwendet.
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id),
array(), array(),
$cookie_file $cookie_file
); ));
$orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), array(), $cookie_file)); $orphan = decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0),
array(),
$cookie_file
));
if ($site_id !== 1) if ($site_id !== 1)
{ {
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), array(), $cookie_file)); decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0),
array(),
$cookie_file
));
} }
$added = (int)($orphan['added_orphans'] ?? 0); $added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0);

View File

@@ -6,6 +6,12 @@ if (PHP_SAPI !== 'cli')
exit(1); 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;
function fail_preview($message) function fail_preview($message)
{ {
throw new RuntimeException($message); throw new RuntimeException($message);
@@ -20,7 +26,7 @@ function quote_webdav_path($path)
function fetch_remote_blob($url, $user, $password) function fetch_remote_blob($url, $user, $password)
{ {
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt_array($ch, array( $options = array(
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false, CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 10,
@@ -28,72 +34,163 @@ function fetch_remote_blob($url, $user, $password)
CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user.':'.$password, CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_FAILONERROR => false, CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.5.19', CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.7.1',
)); );
bratonien_tools_nc_transport_apply_curl($options, $url);
curl_setopt_array($ch, $options);
$body = curl_exec($ch); $body = curl_exec($ch);
$errno = curl_errno($ch); $errno = curl_errno($ch);
$error = curl_error($ch); $error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
if ($body === false || $errno !== 0) fail_preview('WebDAV-Bild konnte nicht geladen werden: '.$error); if ($body === false || $errno !== 0) fail_preview('WebDAV-Bild konnte nicht geladen werden: '.$error);
if ($http < 200 || $http >= 300) fail_preview('WebDAV-Bild antwortete mit HTTP '.$http.'.'); if ($http < 200 || $http >= 300) fail_preview('WebDAV-Bild antwortete mit HTTP '.$http.'.');
if ($body === '') fail_preview('WebDAV-Bild ist leer.');
return (string)$body; return (string)$body;
} }
function write_preview($blob, $target) 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';
}
return 'jpg';
}
function preview_target_for_entry($cache_dir, $key, array $entry)
{
return $cache_dir.'/'.$key.'.'.preview_extension_for_entry($entry);
}
function remove_other_preview_format($cache_dir, $key, $keep_target)
{
foreach (array('jpg', 'png', 'webp') as $ext)
{
$candidate = $cache_dir.'/'.$key.'.'.$ext;
if ($candidate !== $keep_target && is_file($candidate)) @unlink($candidate);
}
}
function write_preview_imagick($blob, $target, $extension)
{
$image = new Imagick();
try
{
$image->readImageBlob($blob);
if (method_exists($image, 'autoOrientImage')) @$image->autoOrientImage();
$image->setIteratorIndex(0);
$image->thumbnailImage(BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE, BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE, true, true);
$image->stripImage();
if ($extension === 'png')
{
$image->setImageFormat('png');
}
else
{
$image->setImageBackgroundColor('white');
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);
$image->setInterlaceScheme(Imagick::INTERLACE_PLANE);
}
if (!$image->writeImage($target)) fail_preview('Imagick konnte das Preview nicht schreiben.');
}
finally
{
$image->clear();
$image->destroy();
}
}
function write_preview_gd($blob, $target, $extension)
{
$source = @imagecreatefromstring($blob);
if (!$source) fail_preview('GD konnte das Bild nicht dekodieren.');
try
{
$width = imagesx($source);
$height = imagesy($source);
if ($width < 1 || $height < 1) fail_preview('Bildabmessungen sind ungültig.');
$scale = min(1, BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE / $width, BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE / $height);
$target_width = max(1, (int)round($width * $scale));
$target_height = max(1, (int)round($height * $scale));
$preview = imagecreatetruecolor($target_width, $target_height);
if (!$preview) fail_preview('GD konnte die Preview-Arbeitsfläche nicht anlegen.');
try
{
if ($extension === 'png')
{
imagealphablending($preview, false);
imagesavealpha($preview, true);
$transparent = imagecolorallocatealpha($preview, 0, 0, 0, 127);
imagefilledrectangle($preview, 0, 0, $target_width, $target_height, $transparent);
}
else
{
$white = imagecolorallocate($preview, 255, 255, 255);
imagefilledrectangle($preview, 0, 0, $target_width, $target_height, $white);
}
if (!imagecopyresampled($preview, $source, 0, 0, 0, 0, $target_width, $target_height, $width, $height))
{
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.');
}
finally
{
imagedestroy($preview);
}
}
finally
{
imagedestroy($source);
}
}
function write_preview($blob, $target, $extension)
{ {
$dir = dirname($target); $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, 0755, true) && !is_dir($dir)) fail_preview('Preview-Verzeichnis konnte nicht angelegt werden.');
if (class_exists('Imagick')) if (class_exists('Imagick'))
{ {
$image = new Imagick(); write_preview_imagick($blob, $target, $extension);
$image->readImageBlob($blob);
if (method_exists($image, 'autoOrientImage')) @$image->autoOrientImage();
$image->setIteratorIndex(0);
$image->thumbnailImage(1600, 1600, true, true);
$image->setImageFormat('webp');
$image->setImageCompressionQuality(85);
if (!$image->writeImage($target)) fail_preview('Imagick konnte das Preview nicht schreiben.');
$image->clear();
$image->destroy();
@chmod($target, 0644);
return;
} }
elseif (function_exists('imagecreatefromstring') && function_exists('imagejpeg') && function_exists('imagepng'))
if (function_exists('imagecreatefromstring') && function_exists('imagewebp'))
{ {
$source = @imagecreatefromstring($blob); write_preview_gd($blob, $target, $extension);
if (!$source) fail_preview('GD konnte das Bild nicht dekodieren.'); }
$width = imagesx($source); else
$height = imagesy($source); {
if ($width < 1 || $height < 1) fail_preview('Weder Imagick noch GD ist für die Preview-Erzeugung verfügbar.');
{
imagedestroy($source);
fail_preview('Bildabmessungen sind ungültig.');
}
$scale = min(1, 1600 / $width, 1600 / $height);
$target_width = max(1, (int)round($width * $scale));
$target_height = max(1, (int)round($height * $scale));
$preview = imagecreatetruecolor($target_width, $target_height);
imagealphablending($preview, false);
imagesavealpha($preview, true);
$transparent = imagecolorallocatealpha($preview, 0, 0, 0, 127);
imagefilledrectangle($preview, 0, 0, $target_width, $target_height, $transparent);
imagecopyresampled($preview, $source, 0, 0, 0, 0, $target_width, $target_height, $width, $height);
if (!imagewebp($preview, $target, 85))
{
imagedestroy($preview);
imagedestroy($source);
fail_preview('GD konnte das Preview nicht schreiben.');
}
imagedestroy($preview);
imagedestroy($source);
@chmod($target, 0644);
return;
} }
fail_preview('Weder Imagick noch GD/WebP ist für die Preview-Erzeugung verfügbar.'); clearstatcache(true, $target);
$size = @getimagesize($target);
if (!is_array($size) || empty($size[0]) || empty($size[1]) || !is_file($target) || filesize($target) < 64)
{
@unlink($target);
fail_preview('Das erzeugte Preview ist ungültig.');
}
@chmod($target, 0644);
} }
try try
@@ -117,6 +214,7 @@ try
if (!is_dir($cache_dir) && !mkdir($cache_dir, 0755, true) && !is_dir($cache_dir)) fail_preview('Preview-Cache konnte nicht angelegt werden.'); 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); @chmod($cache_dir, 0755);
$state_file = $cache_dir.'/state.json'; $state_file = $cache_dir.'/state.json';
$old_state = array(); $old_state = array();
if (is_readable($state_file)) if (is_readable($state_file))
@@ -135,12 +233,26 @@ try
if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue; if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue;
$webdav_path = trim((string)($entry['webdav_path'] ?? ''), '/'); $webdav_path = trim((string)($entry['webdav_path'] ?? ''), '/');
if ($webdav_path === '') continue; if ($webdav_path === '') continue;
$etag = (string)($entry['etag'] ?? ''); $etag = (string)($entry['etag'] ?? '');
$key = sha1($webdav_path); $key = sha1($webdav_path);
$target = $cache_dir.'/'.$key.'.webp'; $target = preview_target_for_entry($cache_dir, $key, $entry);
$new_state[$key] = array('path'=>$webdav_path, 'etag'=>$etag); $extension = pathinfo($target, PATHINFO_EXTENSION);
$new_state[$key] = array(
'path' => $webdav_path,
'etag' => $etag,
'format' => $extension,
'version' => BRATONIEN_WEBDAV_PREVIEW_VERSION,
);
if (is_file($target) && isset($old_state[$key]) && (string)($old_state[$key]['etag'] ?? '') === $etag) $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++; $cached++;
continue; continue;
@@ -150,7 +262,8 @@ try
{ {
$url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.quote_webdav_path($webdav_path); $url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.quote_webdav_path($webdav_path);
$blob = fetch_remote_blob($url, $user, $password); $blob = fetch_remote_blob($url, $user, $password);
write_preview($blob, $target); write_preview($blob, $target, $extension);
remove_other_preview_format($cache_dir, $key, $target);
$generated++; $generated++;
} }
catch (Throwable $e) catch (Throwable $e)
@@ -160,10 +273,12 @@ try
} }
} }
foreach (glob($cache_dir.'/*.webp') ?: array() as $file) foreach (glob($cache_dir.'/*') ?: array() as $file)
{ {
$key = basename($file, '.webp'); if (!is_file($file) || basename($file) === 'state.json') continue;
if (!isset($new_state[$key])) @unlink($file); $name = basename($file);
if (!preg_match('/^([a-f0-9]{40})\.(jpg|png|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); file_put_contents($state_file, json_encode($new_state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)."\n", LOCK_EX);

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);
}

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

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

@@ -0,0 +1,100 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$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'] = 'NC-Abgleich läuft.';
$state['started_at'] = time();
$state['timestamp'] = time();
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['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 ? 'NC-Abgleich erfolgreich abgeschlossen.' : 'NC-Abgleich fehlgeschlagen.';
$state['timestamp'] = time();
$state['finished_at'] = time();
$state['exit_code'] = $exit;
$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

@@ -78,8 +78,11 @@ function webdav_source_fingerprint($baseUrl, $user, array $roots)
$pluginRoot = dirname(__DIR__); $pluginRoot = dirname(__DIR__);
$piwigoRoot = dirname($pluginRoot, 2); $piwigoRoot = dirname($pluginRoot, 2);
$dbConfig = $piwigoRoot.'/local/config/database.inc.php'; $dbConfig = $piwigoRoot.'/local/config/database.inc.php';
$configDir = '/etc/bratonien-tools/nc-connector'; $nativeMode = getenv('BRATONIEN_NC_NATIVE') === '1';
$stateRoot = '/var/lib/bratonien-tools/nc-connector'; $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'; $publicSourceRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-source';
$publicGalleryRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-gallery'; $publicGalleryRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-gallery';
$legacyGalleryRoot = rtrim($piwigoRoot, '/').'/galleries'; $legacyGalleryRoot = rtrim($piwigoRoot, '/').'/galleries';
@@ -107,11 +110,12 @@ try
$rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id DESC"); $rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id DESC");
if (!$rows) fail_webdav_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error); if (!$rows) fail_webdav_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
foreach (array($configDir=>$configDir, $publicSourceRoot=>$publicSourceRoot, $publicGalleryRoot=>$publicGalleryRoot) as $dir=>$unused) foreach (array($configDir, $stateRoot, $publicSourceRoot, $publicGalleryRoot) as $dir)
{ {
if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0755, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir); if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0750, true)) fail_webdav_reconcile('Runtime-Verzeichnis konnte nicht angelegt werden: '.$dir);
} }
@chmod($configDir, 0700); @chmod($configDir, 0700);
@chmod($stateRoot, 0750);
@chmod($publicSourceRoot, 0755); @chmod($publicSourceRoot, 0755);
@chmod($publicGalleryRoot, 0755); @chmod($publicGalleryRoot, 0755);
@@ -125,7 +129,6 @@ try
if (!is_array($config)) $config = array(); if (!is_array($config)) $config = array();
if ((string)$row['adapter'] !== 'remote') continue; if ((string)$row['adapter'] !== 'remote') continue;
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue; if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
if (empty($config['parallel_test'])) continue;
try try
{ {
@@ -151,7 +154,7 @@ try
$seenFingerprints[$fingerprint] = $id; $seenFingerprints[$fingerprint] = $id;
$known[$id] = true; $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 ($stateDir === '') $stateDir = $stateRoot.'/connection-'.$id;
if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_webdav_reconcile('State-Verzeichnis konnte nicht angelegt werden.'); if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_webdav_reconcile('State-Verzeichnis konnte nicht angelegt werden.');
@chmod($stateDir, 0750); @chmod($stateDir, 0750);
@@ -164,8 +167,6 @@ try
} }
if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.'); if (!is_dir($galleryRoot) && !mkdir($galleryRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Galeriebereich konnte nicht angelegt werden.');
@chmod($galleryRoot, 0755); @chmod($galleryRoot, 0755);
// Alte technische Wrapper unter ./galleries duerfen nicht als Piwigo-Alben auftauchen.
webdav_remove_generated_tree($legacyDefault, $legacyGalleryRoot); webdav_remove_generated_tree($legacyDefault, $legacyGalleryRoot);
$sourceDir = $publicSourceRoot.'/connection-'.$id; $sourceDir = $publicSourceRoot.'/connection-'.$id;
@@ -189,8 +190,9 @@ try
{ {
$path = trim((string)($root['webdav_path'] ?? ''), '/'); $path = trim((string)($root['webdav_path'] ?? ''), '/');
$display = trim((string)($root['display_name'] ?? '')); $display = trim((string)($root['display_name'] ?? ''));
if ($path === '' || $display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.'); $runtimePath = $path === '' ? '/' : $path;
$rootLines[] = $path."\t".$display; 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); file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX);
@chmod($rootsPath, 0600); @chmod($rootsPath, 0600);
@@ -215,21 +217,23 @@ try
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX); file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
@chmod($configPath, 0600); @chmod($configPath, 0600);
unset($config['migration'], $config['parallel_test']);
$config['state_dir'] = $stateDir; $config['state_dir'] = $stateDir;
$config['status_file'] = $statusFile; $config['status_file'] = $statusFile;
$config['parallel_gallery_root'] = $galleryRoot; $config['parallel_gallery_root'] = $galleryRoot;
$config['source_fingerprint'] = $fingerprint; $config['source_fingerprint'] = $fingerprint;
$config['runtime'] = array( $config['runtime'] = array(
'mode'=>'parallel-webdav', 'mode'=>$nativeMode ? 'piwigo-native-webdav' : 'webdav',
'config'=>$configPath, 'config'=>$configPath,
'piwigo_sync_enabled'=>true, 'piwigo_sync_enabled'=>true,
'reconciled_at'=>date('Y-m-d H:i:s'), 'reconciled_at'=>date('Y-m-d H:i:s'),
); );
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (is_string($json)) if (!is_string($json)) fail_webdav_reconcile('WebDAV-Runtime-Konfiguration konnte nicht serialisiert werden.');
$escaped = $db->real_escape_string($json);
if (!$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} AND adapter='remote' LIMIT 1"))
{ {
$escaped = $db->real_escape_string($json); fail_webdav_reconcile('WebDAV-Runtime-Status konnte nicht gespeichert werden: '.$db->error);
$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} LIMIT 1");
} }
} }
catch (Throwable $e) catch (Throwable $e)

View File

@@ -1,23 +1,61 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
CONFIG_DIR="/etc/bratonien-tools/nc-connector"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" 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}"
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 shopt -s nullglob
if ! php "$SCRIPT_DIR/reconcile.php"; then read_config_value() {
echo "NC Connector: gespeicherte lokale Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2 local key="$1"
exit 1 local file="$2"
fi local value
value="$(sed -n "s/^${key}=//p" "$file" | tail -n 1)"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
printf '%s' "$value"
}
if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then write_route_status() {
echo "NC Connector: WebDAV-Testverbindungen konnten nicht mit der parallelen Runtime abgeglichen werden." >&2 local route="$1"
exit 1 local label="$2"
fi 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 ! php "$SCRIPT_DIR/cleanup-stale.php"; then if [[ "$NATIVE_MODE" != "1" ]]; then
echo "NC Connector: verwaiste Laufzeitdateien konnten nicht bereinigt werden." >&2 php "$SCRIPT_DIR/reconcile.php"
exit 1 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 fi
configs=("$CONFIG_DIR"/connection-*.conf) configs=("$CONFIG_DIR"/connection-*.conf)
@@ -28,51 +66,104 @@ if [[ ${#configs[@]} -eq 0 && ${#webdav_configs[@]} -eq 0 ]]; then
exit 0 exit 0
fi fi
result=0 route_piwigo_root=""
for candidate in "${webdav_configs[@]}" "${configs[@]}"; do
# WebDAV zuerst: Der parallele Shadow Tree muss bereits vollständig stehen, [[ -f "$candidate" ]] || continue
# bevor der weiterhin produktive lokale Weg seine normale Piwigo- route_piwigo_root="$(read_config_value PIWIGO_ROOT "$candidate")"
# Dateisynchronisierung ausführt. So wird der neue Baum im selben Minutenlauf [[ -n "$route_piwigo_root" ]] && break
# sichtbar, ohne dass der WebDAV-Zweig selbst eine zweite globale Piwigo-
# Synchronisierung startet.
for config in "${webdav_configs[@]}"; do
name="$(basename "$config")"
echo "NC Connector WebDAV parallel: $name"
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh"; then
result=1
fi
done 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"
for config in "${configs[@]}"; do failure_count=0
webdav_count=0
local_count=0
summary_parts=()
for config in "${webdav_configs[@]}"; do
[[ -f "$config" ]] || continue
name="$(basename "$config")" name="$(basename "$config")"
connection_id="" connection_id="$(read_config_value CONNECTION_ID "$config")"
if [[ "$name" =~ ^connection-([0-9]+)\.conf$ ]]; then if [[ ! "$connection_id" =~ ^[0-9]+$ ]] || [[ "$connection_id" -lt 1 ]]; then
connection_id="${BASH_REMATCH[1]}" echo "NC Connector: $name besitzt keine gueltige Verbindungs-ID." >&2
fi failure_count=$((failure_count + 1))
summary_parts+=("$name: ungueltige Verbindungs-ID")
piwigo_root="$(sed -n 's/^PIWIGO_ROOT=//p' "$config" | tail -n 1)"
piwigo_root="${piwigo_root%\"}"
piwigo_root="${piwigo_root#\"}"
piwigo_root="${piwigo_root%\'}"
piwigo_root="${piwigo_root#\'}"
[[ -n "$piwigo_root" ]] || piwigo_root="/var/www/piwigo"
tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status"
if [[ -n "$connection_id" && -f "$tombstone_dir/deleted-$connection_id" ]]; then
echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt."
rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \
"$CONFIG_DIR/connection-$connection_id.db-password" \
"$CONFIG_DIR/connection-$connection_id.piwigo-password" \
"$CONFIG_DIR/connection-$connection_id.storages.tsv" \
"$CONFIG_DIR/connection-$connection_id.roots.tsv"
rm -f -- "$tombstone_dir/deleted-$connection_id"
continue continue
fi fi
echo "NC Connector lokal: $name" webdav_count=$((webdav_count + 1))
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then echo "NC Connector WebDAV #$connection_id: $name"
result=1 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 fi
done 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
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
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: alle Verbindungen wurden erfolgreich verarbeitet."
exit 0
fi
write_route_status "failed" "FEHLER - mindestens eine Verbindung" "$summary_detail" "0"
echo "NC Connector: mindestens eine Verbindung ist fehlgeschlagen." >&2
exit 1

View File

@@ -59,10 +59,6 @@ for target in (status_file, public_file):
PY PY
} }
compact_output() {
tail -n 12 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
}
failure() { failure() {
local code="$1" command="$2" line="$3" local code="$1" command="$2" line="$3"
trap - ERR trap - ERR
@@ -79,8 +75,12 @@ done < "$WEBDAV_ROOTS_FILE"
[[ ${#ROOT_ARGS[@]} -gt 0 ]] || { write_status error "Keine WebDAV-Wurzeln konfiguriert"; exit 1; } [[ ${#ROOT_ARGS[@]} -gt 0 ]] || { write_status error "Keine WebDAV-Wurzeln konfiguriert"; exit 1; }
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; }
python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \ python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
--base-url "$WEBDAV_BASE_URL" \ --base-url "$WEBDAV_BASE_URL" \
--connect-ip "$WEBDAV_CONNECT_IP" \
--user "$WEBDAV_USER" \ --user "$WEBDAV_USER" \
--password-file "$WEBDAV_PASSWORD_FILE" \ --password-file "$WEBDAV_PASSWORD_FILE" \
"${ROOT_ARGS[@]}" \ "${ROOT_ARGS[@]}" \
@@ -94,28 +94,6 @@ python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--state "$SHADOW_MAP_FILE" --state "$SHADOW_MAP_FILE"
trap - ERR trap - ERR
PREVIEW_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-preview/connection-$CONNECTION_ID"
PREVIEW_OUTPUT=""
PREVIEW_EXIT=0
if PREVIEW_OUTPUT="$(php "$SCRIPT_DIR/lib/precache-webdav-previews.php" \
--mapping="$WEBDAV_MAPPING_FILE" \
--base-url="$WEBDAV_BASE_URL" \
--user="$WEBDAV_USER" \
--password-file="$WEBDAV_PASSWORD_FILE" \
--cache-dir="$PREVIEW_CACHE" 2>&1)"; then
PREVIEW_EXIT=0
else
PREVIEW_EXIT=$?
fi
[[ -z "$PREVIEW_OUTPUT" ]] || printf '%s\n' "$PREVIEW_OUTPUT"
if [[ "$PREVIEW_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PREVIEW_EXIT"
if [[ -n "$PREVIEW_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PREVIEW_OUTPUT" | compact_output)"
fi
write_status error "WebDAV-Vorschaubilder konnten beim Einlesen nicht erzeugt werden" "$DETAIL"
exit "$PREVIEW_EXIT"
fi
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
PIWIGO_OUTPUT="" PIWIGO_OUTPUT=""
@@ -133,7 +111,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PIWIGO_EXIT" DETAIL="Exit-Code: $PIWIGO_EXIT"
if [[ -n "$PIWIGO_OUTPUT" ]]; then 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 fi
if grep -qi 'Invalid username/password' <<<"$PIWIGO_OUTPUT"; then if grep -qi 'Invalid username/password' <<<"$PIWIGO_OUTPUT"; then
@@ -160,28 +138,33 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
exit "$PIWIGO_EXIT" exit "$PIWIGO_EXIT"
fi fi
DERIVATIVE_OUTPUT="" if [[ "${BRATONIEN_NC_NATIVE:-0}" == "1" ]]; then
DERIVATIVE_EXIT=0 if command -v timeout >/dev/null 2>&1; then
if DERIVATIVE_OUTPUT="$(php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \ if ! timeout 30m env PIWIGO_CONFIG="$CONFIG_FILE" bash "$SCRIPT_DIR/build-webdav-media.sh"; then
--piwigo-root="$PIWIGO_ROOT" \ write_status error "Bildaufbereitung ist fehlgeschlagen oder hat das 30-Minuten-Limit erreicht"
--connection-id="$CONNECTION_ID" 2>&1)"; then exit 1
DERIVATIVE_EXIT=0 fi
else elif ! env PIWIGO_CONFIG="$CONFIG_FILE" bash "$SCRIPT_DIR/build-webdav-media.sh"; then
DERIVATIVE_EXIT=$? write_status error "Bildaufbereitung ist fehlgeschlagen"
fi exit 1
[[ -z "$DERIVATIVE_OUTPUT" ]] || printf '%s\n' "$DERIVATIVE_OUTPUT" fi
if [[ "$DERIVATIVE_EXIT" -ne 0 ]]; then else
DETAIL="Exit-Code: $DERIVATIVE_EXIT" MEDIA_UNIT="bratonien-nc-media-${CONNECTION_ID}-$(date +%s)"
if [[ -n "$DERIVATIVE_OUTPUT" ]]; then if ! systemd-run \
DETAIL+="; Ausgabe: $(printf '%s\n' "$DERIVATIVE_OUTPUT" | compact_output)" --quiet \
--collect \
--unit="$MEDIA_UNIT" \
--property=RuntimeMaxSec=30min \
--setenv="PIWIGO_CONFIG=$CONFIG_FILE" \
/usr/bin/env bash "$SCRIPT_DIR/build-webdav-media.sh"; then
write_status error "Bildaufbereitung konnte nicht im Hintergrund gestartet werden"
exit 1
fi fi
write_status error "Piwigo-Derivate für WebDAV-Bilder konnten nicht erzeugt werden" "$DETAIL"
exit "$DERIVATIVE_EXIT"
fi fi
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \ write_status ok \
"WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" \ "WebDAV eingelesen und Piwigo synchronisiert; Bildaufbereitung abgeschlossen" \
"" \ "" \
"api" \ "api" \
"ok" \ "ok" \
@@ -190,7 +173,7 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"Fallback wurde nicht benötigt" "Fallback wurde nicht benötigt"
elif grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then elif grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \ write_status ok \
"WebDAV eingelesen, Piwigo über Fallback synchronisiert und Derivate erzeugt" \ "WebDAV eingelesen und Piwigo über Fallback synchronisiert; Bildaufbereitung abgeschlossen" \
"" \ "" \
"fallback" \ "fallback" \
"not_used" \ "not_used" \
@@ -198,8 +181,8 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
"ok" \ "ok" \
"Benutzername/Passwort-Fallback erfolgreich" "Benutzername/Passwort-Fallback erfolgreich"
else else
write_status ok "WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" write_status ok "WebDAV eingelesen und Piwigo synchronisiert; Bildaufbereitung abgeschlossen"
fi fi
else else
write_status ok "WebDAV eingelesen und Vorschaubilder erzeugt; Registrierung erfolgt über den bestehenden produktiven Piwigo-Sync" write_status ok "WebDAV eingelesen; Piwigo-Synchronisierung ist für diese Verbindung deaktiviert"
fi fi

View File

@@ -9,7 +9,13 @@
.bratonien-tab-panel { width:100%; max-width:none; margin-left:0 !important; margin-right:0 !important; } .bratonien-tab-panel { width:100%; max-width:none; margin-left:0 !important; margin-right:0 !important; }
.bratonien-tab-panel[hidden] { display:none !important; } .bratonien-tab-panel[hidden] { display:none !important; }
.bratonien-card { margin-left:0; margin-right:0; } .bratonien-card { margin-left:0; margin-right:0; }
@media (max-width:760px) { .bratonien-tabs{gap:5px}.bratonien-tab{border-bottom:1px solid rgba(255,255,255,.16);border-radius:4px;margin-bottom:0;padding:9px 11px} } .bratonien-edit-dialog { width:min(920px,calc(100vw - 3rem)); max-height:88vh; overflow:auto; background:#444; color:inherit; border:1px solid #777; border-radius:4px; padding:0; box-shadow:0 18px 60px rgba(0,0,0,.55); }
.bratonien-edit-dialog::backdrop { background:rgba(0,0,0,.55); }
.bratonien-edit-dialog__body { padding:1.25rem 1.5rem; }
.bratonien-storage-row { display:grid; grid-template-columns:minmax(120px,.7fr) minmax(150px,1fr) minmax(240px,1.5fr) auto; gap:.5rem; align-items:end; margin:.5rem 0; }
.bratonien-storage-row label { display:flex; flex-direction:column; gap:.25rem; }
.bratonien-storage-row input { width:100%; box-sizing:border-box; }
@media (max-width:760px) { .bratonien-tabs{gap:5px}.bratonien-tab{border-bottom:1px solid rgba(255,255,255,.16);border-radius:4px;margin-bottom:0;padding:9px 11px}.bratonien-storage-row{grid-template-columns:1fr} }
</style> </style>
<div id="bratonien-tabs-anchor"></div> <div id="bratonien-tabs-anchor"></div>
<script> <script>
@@ -48,12 +54,31 @@
var openButton=document.getElementById('bratonien-nc-wizard-open'); var openButton=document.getElementById('bratonien-nc-wizard-open');
var closeButton=document.getElementById('bratonien-nc-wizard-close'); var closeButton=document.getElementById('bratonien-nc-wizard-close');
var storageKey='bratonienNcWizardOpen'; var storageKey='bratonienNcWizardOpen';
var modeKey='bratonienNcWizardMode';
var resetBusy=false; var resetBusy=false;
function token(){var input=section.querySelector('input[name="pwg_token"]');return input?input.value:'';} 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 setOpen(value){try{if(value)sessionStorage.setItem(storageKey,'1');else sessionStorage.removeItem(storageKey);}catch(e){}}
function mode(){try{return sessionStorage.getItem(modeKey)||'';}catch(e){return '';}}
function clearMode(){try{sessionStorage.removeItem(modeKey);}catch(e){}}
function applyMode(){
if(!dialog)return;
var current=mode();
var heading=dialog.querySelector('h4');
var finish=dialog.querySelector('button[value="nc_connector_wizard_finish"]');
if(current==='edit'){
if(heading)heading.textContent='Verbindung bearbeiten';
if(finish)finish.textContent='Änderungen speichern';
}else if(current==='migrate'){
if(heading)heading.textContent='Auf WebDAV migrieren';
if(finish)finish.textContent='Migration starten';
}else{
if(heading)heading.textContent='Neue Verbindung';
if(finish)finish.textContent='Verbindung anlegen';
}
}
function showWizard(){ function showWizard(){
setOpen(true); setOpen(true);applyMode();
if(!dialog)return; if(!dialog)return;
if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal(); if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();
else dialog.setAttribute('open','open'); else dialog.setAttribute('open','open');
@@ -65,13 +90,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()}); 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(){ function closeAfterReset(){
if(resetBusy)return;resetBusy=true;setOpen(false); if(resetBusy)return;resetBusy=true;setOpen(false);clearMode();
resetServer().catch(function(){}).finally(function(){resetBusy=false;if(dialog){if(typeof dialog.close==='function'&&dialog.open)dialog.close();else dialog.removeAttribute('open');}}); resetServer().catch(function(){}).finally(function(){resetBusy=false;if(dialog){if(typeof dialog.close==='function'&&dialog.open)dialog.close();else dialog.removeAttribute('open');}});
} }
if(openButton){ if(openButton){
openButton.addEventListener('click',function(event){ openButton.addEventListener('click',function(event){
event.preventDefault();event.stopImmediatePropagation();showWizard(); event.preventDefault();event.stopImmediatePropagation();clearMode();showWizard();
},true); },true);
} }
if(closeButton){ if(closeButton){
@@ -80,18 +105,111 @@
if(dialog){ if(dialog){
dialog.addEventListener('cancel',function(event){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();},true); 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); 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){ [].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')clearMode();
}
},true);
}); });
try{if(sessionStorage.getItem(storageKey)==='1')showWizard();}catch(e){} try{if(sessionStorage.getItem(storageKey)==='1')showWizard();}catch(e){}
applyMode();
} }
} }
function initNCConnectionEditing(){
var section=document.getElementById('nc-connector');if(!section)return;
var modeKey='bratonienNcWizardMode';
var pwgTokenInput=section.querySelector('input[name="pwg_token"]');
var pwgToken=pwgTokenInput?pwgTokenInput.value:'';
function setMode(value){try{sessionStorage.setItem(modeKey,value);}catch(e){}}
function makePostButton(label,tool,id,modeValue){
var form=document.createElement('form');form.method='post';form.style.display='inline';
var token=document.createElement('input');token.type='hidden';token.name='pwg_token';token.value=pwgToken;form.appendChild(token);
var connection=document.createElement('input');connection.type='hidden';connection.name='connection_id';connection.value=String(id);form.appendChild(connection);
var button=document.createElement('button');button.type='submit';button.className='buttonLike';button.name='bratonien_tool';button.value=tool;button.textContent=label;
if(modeValue)form.addEventListener('submit',function(){setMode(modeValue);});
form.appendChild(button);return form;
}
function escapeHtml(value){return String(value==null?'':value).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c];});}
function localEditorDialog(){
var dialog=document.getElementById('bratonien-nc-local-edit-dialog');
if(dialog)return dialog;
dialog=document.createElement('dialog');dialog.id='bratonien-nc-local-edit-dialog';dialog.className='bratonien-edit-dialog';
dialog.innerHTML='<div class="bratonien-edit-dialog__body"><div style="display:flex;align-items:center;justify-content:space-between;gap:1rem"><div><h4 style="margin:0">Verbindung bearbeiten</h4><p class="bratonien-base-note" style="margin:.35rem 0 0">Bestehende Legacy-Verbindung. Änderungen werden in derselben Verbindung gespeichert.</p></div><button type="button" class="buttonLike" data-local-edit-close>Schließen</button></div><div data-local-edit-content style="margin-top:1rem"></div></div>';
document.body.appendChild(dialog);
dialog.querySelector('[data-local-edit-close]').addEventListener('click',function(){dialog.close();});
dialog.addEventListener('click',function(event){if(event.target===dialog)dialog.close();});
return dialog;
}
function storageRow(storage){
return '<div class="bratonien-storage-row" data-storage-row>'+
'<label>Storage-ID<input name="nc_storage_id[]" value="'+escapeHtml(storage.storage_id||'')+'" required></label>'+
'<label>Quellordner<input name="nc_source_prefix[]" value="'+escapeHtml(storage.source_prefix||'')+'" placeholder="optional"></label>'+
'<label>Lokaler Speicherpfad<input name="nc_local_mount[]" value="'+escapeHtml(storage.local_mount||'')+'" required></label>'+
'<button type="button" class="buttonLike" data-remove-storage>Entfernen</button></div>';
}
function openLocalEditor(id){
var dialog=localEditorDialog();
var content=dialog.querySelector('[data-local-edit-content]');
content.innerHTML='<p class="bratonien-base-note">Verbindung wird geladen …</p>';
if(typeof dialog.showModal==='function')dialog.showModal();else dialog.setAttribute('open','open');
fetch('plugins/bratonien_tools/nc-connector-edit-data.php?connection_id='+encodeURIComponent(id)+'&_='+Date.now(),{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
.then(function(response){return response.json().then(function(data){if(!response.ok)throw new Error(data.error||('HTTP '+response.status));return data;});})
.then(function(data){
if(data.adapter!=='local')throw new Error('Diese Verbindung ist keine Legacy-Verbindung.');
var legacy=data.legacy||{};var storages=Array.isArray(legacy.storages)?legacy.storages:[];
var rows=storages.map(storageRow).join('');
if(!rows)rows=storageRow({});
content.innerHTML='<form method="post" data-local-edit-form>'+
'<input type="hidden" name="pwg_token" value="'+escapeHtml(pwgToken)+'">'+
'<input type="hidden" name="connection_id" value="'+escapeHtml(data.id)+'">'+
'<div class="bratonien-form-grid">'+
'<label class="bratonien-label">Name</label><input name="connection_name" value="'+escapeHtml(data.name)+'" required>'+
'<label class="bratonien-label">Datenbank-Server</label><input name="nc_host" value="'+escapeHtml(legacy.host)+'" required>'+
'<label class="bratonien-label">Port</label><input name="nc_port" type="number" min="1" max="65535" value="'+escapeHtml(legacy.port||5432)+'" required>'+
'<label class="bratonien-label">Datenbank</label><input name="nc_database" value="'+escapeHtml(legacy.database)+'" required>'+
'<label class="bratonien-label">Reader-Benutzer</label><input name="nc_user" value="'+escapeHtml(legacy.user)+'" required>'+
'<label class="bratonien-label">Reader-Passwort</label><input name="nc_db_password" type="password" autocomplete="new-password" placeholder="leer = unverändert">'+
'</div>'+
'<h5 style="margin-top:1rem">Speicherorte</h5><p class="bratonien-base-note">Jeder Speicherort wird einzeln bearbeitet. Es ist kein Pipe-/Textformat erforderlich.</p><div data-storage-list>'+rows+'</div><button type="button" class="buttonLike" data-add-storage>Speicherort hinzufügen</button>'+
'<details style="margin-top:1rem"><summary>Erweiterte Legacy-Einstellungen</summary><div class="bratonien-form-grid" style="margin-top:.75rem">'+
'<label class="bratonien-label">Source-View</label><input name="nc_source_view" value="'+escapeHtml(legacy.source_view)+'" required>'+
'<label class="bratonien-label">Activity-View</label><input name="nc_activity_view" value="'+escapeHtml(legacy.activity_view)+'" required>'+
'<label class="bratonien-label">Piwigo-Galerieordner</label><input name="nc_gallery_root" value="'+escapeHtml(legacy.gallery_root)+'" required>'+
'<label class="bratonien-label">Ruhezeit (Sek.)</label><input name="nc_quiet_seconds" type="number" min="0" value="'+escapeHtml(legacy.quiet_seconds)+'">'+
'<label class="bratonien-label">Maximale Wartezeit (Sek.)</label><input name="nc_max_wait_seconds" type="number" min="60" value="'+escapeHtml(legacy.max_wait_seconds)+'">'+
'<label class="bratonien-label">Vollprüfung nach (Sek.)</label><input name="nc_full_sync_seconds" type="number" min="300" value="'+escapeHtml(legacy.full_sync_seconds)+'">'+
'</div></details>'+
'<div class="bratonien-actions" style="margin-top:1rem"><button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_update_local">Änderungen speichern</button><button class="buttonLike" type="button" data-local-edit-cancel>Abbrechen</button></div></form>';
var form=content.querySelector('[data-local-edit-form]');
form.querySelector('[data-local-edit-cancel]').addEventListener('click',function(){dialog.close();});
form.querySelector('[data-add-storage]').addEventListener('click',function(){form.querySelector('[data-storage-list]').insertAdjacentHTML('beforeend',storageRow({}));});
form.addEventListener('click',function(event){var remove=event.target.closest('[data-remove-storage]');if(remove){var row=remove.closest('[data-storage-row]');if(row)row.remove();}});
})
.catch(function(error){content.innerHTML='<p class="bratonien-main-cache__warning"><strong>Bearbeiten nicht möglich:</strong> '+escapeHtml(error.message||String(error))+'</p>';});
}
[].slice.call(section.querySelectorAll('button[value="nc_connector_edit_start"]')).forEach(function(button){var form=button.closest('form');if(form)form.remove();});
[].slice.call(section.querySelectorAll('button[value="nc_connector_delete"]')).forEach(function(deleteButton){
var deleteForm=deleteButton.closest('form');var card=deleteButton.closest('details');if(!deleteForm||!card)return;
var idInput=deleteForm.querySelector('input[name="connection_id"]');if(!idInput)return;var id=idInput.value;
var actions=deleteForm.parentElement;if(!actions)return;
var text=card.textContent||'';var isLocal=text.indexOf('bestehende Legacy-Konfiguration')!==-1;
if(isLocal){
var edit=document.createElement('button');edit.type='button';edit.className='buttonLike';edit.textContent='Bearbeiten';edit.addEventListener('click',function(){openLocalEditor(id);});actions.insertBefore(edit,deleteForm);
actions.insertBefore(makePostButton('Auf WebDAV migrieren','nc_connector_migrate_start',id,'migrate'),deleteForm);
}else{
actions.insertBefore(makePostButton('Bearbeiten','nc_connector_edit_start',id,'edit'),deleteForm);
}
});
}
function initNCConnectorPolling(){ function initNCConnectorPolling(){
var section=document.getElementById('nc-connector');if(!section)return; var section=document.getElementById('nc-connector');if(!section)return;
var endpoint='plugins/bratonien_tools/nc-connector-status.php'; var endpoint='plugins/bratonien_tools/nc-connector-status.php';
@@ -144,7 +262,7 @@
poll();schedule(); poll();schedule();
} }
function initAll(){initBratonienTabs();initNCWizardLifecycle();initNCConnectorPolling();} function initAll(){initBratonienTabs();initNCWizardLifecycle();initNCConnectionEditing();initNCConnectorPolling();}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initAll);else initAll(); if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initAll);else initAll();
})(); })();
</script> </script>

View File

@@ -8,6 +8,7 @@ if (!defined('BRATONIEN_TOOLS_PATH'))
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/'); define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
} }
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php'); require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_transport.inc.php');
function bratonien_tools_webdav_image_abort($status, $message) function bratonien_tools_webdav_image_abort($status, $message)
{ {
@@ -58,7 +59,7 @@ if (!empty($_GET['preview']))
{ {
$mtime = @filemtime($preview) ?: time(); $mtime = @filemtime($preview) ?: time();
$etag = sha1($preview.'|'.$mtime.'|'.(@filesize($preview) ?: 0)); $etag = sha1($preview.'|'.$mtime.'|'.(@filesize($preview) ?: 0));
header('Content-Type: image/webp'); header('Content-Type: '.bratonien_tools_webdav_preview_content_type($preview));
header('Content-Length: '.(string)filesize($preview)); header('Content-Length: '.(string)filesize($preview));
header('ETag: "'.$etag.'"'); header('ETag: "'.$etag.'"');
header('Cache-Control: private, max-age=86400, must-revalidate'); header('Cache-Control: private, max-age=86400, must-revalidate');
@@ -69,7 +70,7 @@ if (!empty($_GET['preview']))
http_response_code(304); http_response_code(304);
exit; exit;
} }
if ($_SERVER['REQUEST_METHOD'] !== 'HEAD') readfile($preview); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'HEAD') readfile($preview);
exit; exit;
} }
} }
@@ -120,7 +121,7 @@ $options = array(
CURLOPT_USERPWD => $user.':'.$password, CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false, CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false, CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.5.19', CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.7.1',
CURLOPT_HEADERFUNCTION => function($ch, $line) CURLOPT_HEADERFUNCTION => function($ch, $line)
{ {
$length = strlen($line); $length = strlen($line);
@@ -147,12 +148,13 @@ $options = array(
return strlen($data); return strlen($data);
}, },
); );
bratonien_tools_nc_transport_apply_curl($options, $url);
if (!empty($_SERVER['HTTP_RANGE'])) if (!empty($_SERVER['HTTP_RANGE']))
{ {
$range = trim((string)$_SERVER['HTTP_RANGE']); $range = trim((string)$_SERVER['HTTP_RANGE']);
if (preg_match('/^bytes=(.+)$/i', $range, $m)) $options[CURLOPT_RANGE] = $m[1]; if (preg_match('/^bytes=(.+)$/i', $range, $m)) $options[CURLOPT_RANGE] = $m[1];
} }
if ($_SERVER['REQUEST_METHOD'] === 'HEAD') if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'HEAD')
{ {
$options[CURLOPT_NOBODY] = true; $options[CURLOPT_NOBODY] = true;
} }