Compare commits

...

240 Commits

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

* 0.9.6.9 register separate edit and migration actions

* 0.9.6.9 add safe connection edit data endpoint

* 0.9.6.9 make connection editing explicit and in-place

* Bump version to 0.9.6.9

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

1
.ci-trigger-0977 Normal file
View File

@@ -0,0 +1 @@
0.9.7.7

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

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

@@ -28,25 +28,34 @@ function bratonien_tools_nc_connector_webdav_site_id(array $connection)
return (int)$row['id']; return (int)$row['id'];
} }
function bratonien_tools_nc_connector_gallery_db_prefix(array $connection)
{
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$gallery_root = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
$piwigo_root = rtrim(PHPWG_ROOT_PATH, '/');
if ($gallery_root === '' || strpos($gallery_root, $piwigo_root.'/') !== 0) return '';
$relative = ltrim(substr($gallery_root, strlen($piwigo_root)), '/');
return $relative === '' ? '' : './'.rtrim($relative, '/').'/';
}
function bratonien_tools_nc_connector_remove_webdav_piwigo_content(array $connection) 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'); include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$site_id = bratonien_tools_nc_connector_webdav_site_id($connection);
$db_prefix = bratonien_tools_nc_connector_gallery_db_prefix($connection);
if ($db_prefix === '') return array('site_id'=>$site_id, 'images'=>0);
$escaped = pwg_db_real_escape_string(addcslashes($db_prefix, '_%\\'));
$image_rows = array(); $image_rows = array();
$query = ' $result = pwg_query(
SELECT DISTINCT i.id, i.path, i.representative_ext "SELECT id, path, representative_ext FROM ".IMAGES_TABLE.
FROM '.IMAGES_TABLE.' AS i " WHERE path LIKE '".$escaped."%' ESCAPE '\\\\'"
LEFT JOIN '.CATEGORIES_TABLE.' AS sc ON sc.id = i.storage_category_id );
LEFT JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON ic.image_id = i.id
LEFT JOIN '.CATEGORIES_TABLE.' AS vc ON vc.id = ic.category_id
WHERE sc.site_id = '.$site_id.' OR vc.site_id = '.$site_id.'
;';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result)) while ($row = pwg_db_fetch_assoc($result))
{ {
if (strpos((string)$row['path'], $db_prefix) !== 0) continue;
$row['id'] = (int)$row['id']; $row['id'] = (int)$row['id'];
$image_rows[$row['id']] = $row; $image_rows[$row['id']] = $row;
} }
@@ -63,20 +72,14 @@ SELECT DISTINCT i.id, i.path, i.representative_ext
} }
} }
delete_site($site_id);
if ($image_rows) if ($image_rows)
{ {
$ids = array_keys($image_rows); delete_elements(array_map('intval', array_keys($image_rows)), false);
$remaining = query2array( }
'SELECT id FROM '.IMAGES_TABLE.' WHERE id IN ('.implode(',', array_map('intval', $ids)).')',
null, if ($site_id > 0)
'id' {
); delete_site($site_id);
if ($remaining)
{
delete_elements(array_map('intval', $remaining), false);
}
} }
invalidate_user_cache(true); invalidate_user_cache(true);
@@ -84,10 +87,8 @@ SELECT DISTINCT i.id, i.path, i.representative_ext
} }
/** /**
* 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.
* WebDAV-managed Piwigo records are removed before the connection itself so
* deleted remote content cannot remain visible or addressable in Piwigo.
*/ */
function bratonien_tools_nc_connector_delete_safe() function bratonien_tools_nc_connector_delete_safe()
{ {
@@ -112,24 +113,14 @@ function bratonien_tools_nc_connector_delete_safe()
$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);
}
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'=>'Connector-Verbindung wurde gelöscht. Die zugehörigen Piwigo-Alben und '.(int)$cleanup['images'].' Bilder wurden aus Piwigo entfernt. Nextcloud-Dateien blieben unverändert. Laufzeit- und Vorschaudaten werden automatisch bereinigt.',
);
}
return array( return array(
'message'=>'Connector-Verbindung wurde gelöscht. Verbliebene Laufzeitdateien werden vor dem nächsten Connector-Lauf automatisch entfernt. Quelldateien blieben unverändert.', 'message'=>'Verbindung wurde gelöscht. '.(int)$cleanup['images'].' eindeutig zu dieser Verbindung gehörende Bilder wurden aus Piwigo entfernt. Nextcloud-Dateien blieben unverändert.',
); );
} }

View File

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

View File

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

View File

@@ -4,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,88 @@ 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']);
$scheduler_state = (string)($scheduler['state'] ?? '');
$next_realtime_raw = bratonien_tools_nc_connector_systemctl_value(array('show', $timer, '--property=NextElapseUSecRealtime', '--value')); $running = $scheduler_state === 'running';
$next_timestamp = bratonien_tools_nc_connector_parse_systemd_time($next_realtime_raw); $queued = $scheduler_state === 'queued';
$started = (int)($scheduler['started_at'] ?? 0);
if ($next_timestamp <= 0) $next = (int)($scheduler['next_due'] ?? 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';
}
$scheduler_timestamp = (int)($scheduler['timestamp'] ?? 0);
if ($scheduler_timestamp > (int)$last['timestamp'])
{
$last['timestamp'] = $scheduler_timestamp;
$last['state'] = $scheduler_state;
$last['message'] = (string)($scheduler['message'] ?? '');
if ($scheduler_state === 'error')
{
$detail = trim((string)($scheduler['stderr'] ?? ''));
if ($detail === '') $detail = trim((string)($scheduler['stdout'] ?? ''));
$last['error_detail'] = $detail;
}
}
elseif ((int)$last['timestamp'] <= 0 && !empty($scheduler['finished_at']))
{
$last['timestamp'] = (int)$scheduler['finished_at'];
$last['state'] = $scheduler_state;
$last['message'] = (string)($scheduler['message'] ?? '');
if ((string)$last['state'] === 'error')
{
$detail = trim((string)($scheduler['stderr'] ?? ''));
if ($detail === '') $detail = trim((string)($scheduler['stdout'] ?? ''));
$last['error_detail'] = $detail;
}
}
$current_label = 'Kein Lauf aktiv';
if ($queued) $current_label = 'Abgleich angefordert';
if ($running) $current_label = $started > 0 ? 'Läuft seit '.date('d.m.Y H:i:s', $started) : 'Läuft gerade';
return array( 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 || $queued,
'last_run_label' => $last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar', 'current_run_timestamp'=>$queued ? (int)($scheduler['queued_at'] ?? 0) : $started,
'last_run_state' => (string)$last['state'], 'current_run_label'=>$current_label,
'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

@@ -0,0 +1,115 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_nc_legacy_remove_tree($path, $allowed_root)
{
$path = rtrim(str_replace('\\', '/', (string)$path), '/');
$allowed_root = rtrim(str_replace('\\', '/', (string)$allowed_root), '/');
if ($path === '' || $allowed_root === '' || $path === $allowed_root || strpos($path, $allowed_root.'/') !== 0)
{
return 0;
}
if (!file_exists($path) && !is_link($path)) return 0;
if (is_link($path) || is_file($path))
{
return @unlink($path) ? 1 : 0;
}
$removed = 0;
$items = @scandir($path);
if (is_array($items))
{
foreach ($items as $item)
{
if ($item === '.' || $item === '..') continue;
$removed += bratonien_tools_nc_legacy_remove_tree($path.'/'.$item, $allowed_root);
}
}
if (@rmdir($path)) $removed++;
return $removed;
}
function bratonien_tools_nc_legacy_remove_state_files($state_root)
{
$removed = 0;
foreach (glob(rtrim($state_root, '/').'/connection-*') ?: array() as $connection_dir)
{
if (!is_dir($connection_dir)) continue;
foreach (array('webdav-map.json', 'webdav-manifest.tsv', 'webdav-shadow-map.json') as $name)
{
$path = $connection_dir.'/'.$name;
if (is_file($path) && @unlink($path)) $removed++;
}
}
return $removed;
}
function bratonien_tools_nc_reset_legacy_imports()
{
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$ids = array();
$result = pwg_query(
"SELECT id FROM ".IMAGES_TABLE.
" WHERE path LIKE '%nc-webdav-source/connection-%'"
);
while ($row = pwg_db_fetch_assoc($result))
{
$ids[] = (int)$row['id'];
}
if ($ids)
{
delete_elements($ids, false);
}
$data_root = rtrim(PHPWG_ROOT_PATH, '/').'/_data';
$tool_root = $data_root.'/bratonien-tools';
$removed_files = 0;
foreach (array(
$tool_root.'/nc-webdav-source',
$tool_root.'/nc-webdav-preview',
$tool_root.'/nc-webdav-gallery',
) as $generated_root)
{
$removed_files += bratonien_tools_nc_legacy_remove_tree($generated_root, $data_root);
}
if (defined('PWG_DERIVATIVE_DIR'))
{
$derivative_root = rtrim(PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR, '/');
foreach (array(
$derivative_root.'/_data/bratonien-tools/nc-webdav-source',
$derivative_root.'/_data/bratonien-tools/nc-webdav-gallery',
) as $generated_derivative_root)
{
$removed_files += bratonien_tools_nc_legacy_remove_tree($generated_derivative_root, $derivative_root);
}
}
$removed_state_files = bratonien_tools_nc_legacy_remove_state_files($tool_root.'/nc-connector-state');
if ($ids)
{
update_category('all');
invalidate_user_cache(true);
}
$result = array(
'timestamp'=>time(),
'removed_image_records'=>count($ids),
'removed_generated_entries'=>$removed_files,
'removed_state_files'=>$removed_state_files,
);
if (function_exists('conf_update_param'))
{
conf_update_param('bratonien_nc_legacy_reset_0977', json_encode($result));
}
return $result;
}

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,97 @@ 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 (
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)
{
// Bestehende Piwigo-Alben gehoeren nicht automatisch dem Connector.
// Ohne eindeutige Connector-Eigentumsmarkierung darf hier nichts geloescht werden.
return 0;
}
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 +133,94 @@ 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; list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW()'));
$db_categories = hash_from_query($query, 'id'); $fs_dirs = $site_reader->get_full_directories($basedir);
$db_fulldirs = get_fulldirs(array_keys($db_categories)); usort($fs_dirs, function($a, $b)
$basedir = preg_replace('#/*$#', '', $site_url);
$db_fulldirs = array_flip($db_fulldirs);
$fs_fulldirs = $site_reader->get_full_directories($basedir);
$next_rank = array('NULL'=>1);
$result = pwg_query('SELECT id FROM '.CATEGORIES_TABLE);
while ($row = pwg_db_fetch_assoc($result))
{ {
$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)
{ // Nicht-destruktiver Schutz: Bestehende Piwigo-Bilder werden niemals allein
$query = 'SELECT id, path FROM '.IMAGES_TABLE.' WHERE storage_category_id IN ('.implode(',', array_map('intval', $cat_ids)).')'; // deshalb geloescht, weil sie im aktuellen WebDAV-Scan nicht vorkommen.
$db_elements = simple_hash_from_query($query, 'id', 'path'); // Das Entfernen ist erst wieder zulaessig, wenn Connector-Eigentum eindeutig
} // und verbindungsbezogen gespeichert wird.
$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]))
{
// Altbestand niemals umhaengen. Vorhandene Bild-Album-Zuordnungen bleiben
// exakt bestehen; der Connector darf nur neue Datensaetze ergaenzen.
$all_ids[] = (int)$db_by_path[$path];
continue; continue;
} }
@@ -229,116 +233,42 @@ 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) // Bestehende Bilder werden nicht durch den Connector aktualisiert. Nur neu
{ // angelegte Connector-Bilder erhalten die aus der Quelle ermittelten Attribute.
$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 ($new_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 +281,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,25 @@ 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()
{
$connection_id = isset($_POST['connection_id']) ? max(0, (int)$_POST['connection_id']) : 0;
if ($connection_id > 0)
{
$connection = bratonien_tools_nc_connector_connection($connection_id, false);
if (!$connection)
{
throw new RuntimeException('Die ausgewählte Verbindung existiert nicht.');
}
}
$result = bratonien_tools_nc_scheduler_spawn(true, $connection_id);
return array('message'=>(string)($result['message'] ?? ($connection_id > 0 ? 'Abgleich wurde gestartet.' : 'Abgleich für alle Verbindungen wurde gestartet.')));
}
function bratonien_tools_get_tools() function bratonien_tools_get_tools()
{ {
@@ -54,14 +83,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 +102,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

@@ -8,12 +8,22 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_base.inc.php');
function bratonien_tools_watermark_engine_enabled() function bratonien_tools_watermark_engine_enabled()
{ {
static $initialized = false;
static $enabled = false;
if ($initialized)
{
return $enabled;
}
$config = bratonien_tools_get_watermark_engine_config(); $config = bratonien_tools_get_watermark_engine_config();
$enabled = !empty($config['enabled']); $enabled = !empty($config['enabled']);
$initialized = true;
// Piwigo recalculates use_watermark for custom derivatives from the native // Piwigo recalculates use_watermark for custom derivatives from the native
// watermark file itself. Therefore an active Bratonien engine must keep the // watermark file itself. Therefore an active Bratonien engine must keep the
// native watermark file empty, not only the stored use_watermark flags false. // native watermark file empty, not only the stored use_watermark flags false.
// This initialization is required only once per PHP request.
if ($enabled) if ($enabled)
{ {
bratonien_tools_disable_piwigo_watermarks(); bratonien_tools_disable_piwigo_watermarks();

View File

@@ -27,6 +27,72 @@ function bratonien_tools_runtime_category_id()
return 0; return 0;
} }
function bratonien_tools_watermark_album_cover_category($src_image, $category_id=null)
{
static $map = null;
if ($map === null)
{
$map = new SplObjectStorage();
}
if (!is_object($src_image))
{
return 0;
}
if ($category_id !== null)
{
$category_id = (int)$category_id;
if ($category_id > 0)
{
$map[$src_image] = $category_id;
}
return $category_id;
}
return $map->contains($src_image) ? (int)$map[$src_image] : 0;
}
function bratonien_tools_watermark_prepare_album_overview($thumbnails)
{
if (!is_array($thumbnails))
{
return $thumbnails;
}
foreach ($thumbnails as &$thumbnail)
{
$category_id = (int)($thumbnail['id'] ?? $thumbnail['ID'] ?? 0);
if ($category_id < 1 || empty($thumbnail['representative']['src_image']) || !is_object($thumbnail['representative']['src_image']))
{
continue;
}
// Ein Repraesentantenbild kann fuer mehrere Alben verwendet werden. Piwigo
// reicht in der Albumuebersicht aber nur das SrcImage an get_derivative_url
// weiter. Deshalb bekommt jedes Album-Cover eine eigene SrcImage-Instanz,
// damit seine konkrete Albumregel erhalten bleibt.
$src_image = clone $thumbnail['representative']['src_image'];
$thumbnail['representative']['src_image'] = $src_image;
bratonien_tools_watermark_album_cover_category($src_image, $category_id);
}
unset($thumbnail);
return $thumbnails;
}
function bratonien_tools_runtime_derivative_category_id($src_image)
{
$category_id = bratonien_tools_runtime_category_id();
if ($category_id > 0)
{
return $category_id;
}
return bratonien_tools_watermark_album_cover_category($src_image);
}
function bratonien_tools_runtime_effective_rule($category_id) function bratonien_tools_runtime_effective_rule($category_id)
{ {
static $categories = null; static $categories = null;
@@ -230,7 +296,7 @@ function bratonien_tools_filter_derivative_url($url, $params, $src_image, $rel_u
return $url; return $url;
} }
$rule = bratonien_tools_runtime_effective_rule(bratonien_tools_runtime_category_id()); $rule = bratonien_tools_runtime_effective_rule(bratonien_tools_runtime_derivative_category_id($src_image));
if ($rule['mode'] !== 'profile' || empty($rule['profile_id'])) if ($rule['mode'] !== 'profile' || empty($rule['profile_id']))
{ {
return $url; return $url;

View File

@@ -7,6 +7,9 @@ if (!defined('PHPWG_ROOT_PATH'))
function bratonien_tools_webdav_image_source_info($image_id) function bratonien_tools_webdav_image_source_info($image_id)
{ {
static $cache = array(); static $cache = array();
static $connection_cache = array();
static $mapping_cache = array();
$image_id = (int)$image_id; $image_id = (int)$image_id;
if ($image_id < 1) return null; if ($image_id < 1) return null;
if (array_key_exists($image_id, $cache)) return $cache[$image_id]; if (array_key_exists($image_id, $cache)) return $cache[$image_id];
@@ -20,12 +23,12 @@ function bratonien_tools_webdav_image_source_info($image_id)
$absolute = $path; $absolute = $path;
if (strpos($absolute, '/') !== 0) if (strpos($absolute, '/') !== 0)
{ {
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $absolute), '/'); $absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\\./#', '', $absolute), '/');
} }
$resolved = realpath($absolute); $resolved = realpath($absolute);
if ($resolved === false) return $cache[$image_id] = null; if ($resolved === false) return $cache[$image_id] = null;
$normalized = str_replace('\\', '/', $resolved); $normalized = str_replace('\\\\', '/', $resolved);
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match)) if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{ {
return $cache[$image_id] = null; return $cache[$image_id] = null;
@@ -36,60 +39,82 @@ function bratonien_tools_webdav_image_source_info($image_id)
$relative_path = trim((string)$match[3], '/'); $relative_path = trim((string)$match[3], '/');
if ($relative_path === '') return $cache[$image_id] = null; if ($relative_path === '') return $cache[$image_id] = null;
$table = defined('BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE') if (!array_key_exists($connection_id, $connection_cache))
? BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE {
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections'; $table = defined('BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE')
$connection_result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1'); ? BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE
if (!pwg_db_num_rows($connection_result)) return $cache[$image_id] = null; : $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$connection_row = pwg_db_fetch_assoc($connection_result); $connection_result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
$config = json_decode((string)$connection_row['config_json'], true); if (!pwg_db_num_rows($connection_result))
{
$connection_cache[$connection_id] = null;
}
else
{
$connection_row = pwg_db_fetch_assoc($connection_result);
$decoded = json_decode((string)$connection_row['config_json'], true);
$connection_cache[$connection_id] = is_array($decoded) ? $decoded : null;
}
}
$config = $connection_cache[$connection_id];
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder')
{ {
return $cache[$image_id] = null; return $cache[$image_id] = null;
} }
$root_path = ''; $root_path = '';
$root_found = false;
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array(); $roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
foreach ($roots as $root) foreach ($roots as $root)
{ {
if ((int)($root['fileid'] ?? 0) === $root_fileid) if ((int)($root['fileid'] ?? 0) === $root_fileid)
{ {
$root_path = trim((string)($root['webdav_path'] ?? ''), '/'); $root_path = trim((string)($root['webdav_path'] ?? ''), '/');
$root_found = true;
break; break;
} }
} }
if ($root_path === '') return $cache[$image_id] = null; if (!$root_found) return $cache[$image_id] = null;
$webdav_path = $root_path.'/'.$relative_path; $root_is_base = $root_path === '';
$webdav_path = $root_is_base ? $relative_path : $root_path.'/'.$relative_path;
$content_type = ''; $content_type = '';
$size = 0; $size = 0;
$etag = ''; $etag = '';
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/'); if (!array_key_exists($connection_id, $mapping_cache))
if ($state_dir !== '')
{ {
$mapping_file = $state_dir.'/webdav-map.json'; $mapping_cache[$connection_id] = array();
if (is_readable($mapping_file)) $state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '')
{ {
$mapping = json_decode((string)file_get_contents($mapping_file), true); $mapping_file = $state_dir.'/webdav-map.json';
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files'])) if (is_readable($mapping_file))
{ {
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null; $mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file') if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{ {
$webdav_path = trim((string)($entry['webdav_path'] ?? $webdav_path), '/'); $mapping_cache[$connection_id] = $mapping['files'];
$content_type = (string)($entry['content_type'] ?? '');
$size = (int)($entry['size'] ?? 0);
$etag = (string)($entry['etag'] ?? '');
} }
} }
} }
} }
$entry = $mapping_cache[$connection_id][$resolved] ?? $mapping_cache[$connection_id][$normalized] ?? null;
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file')
{
$webdav_path = trim((string)($entry['webdav_path'] ?? $webdav_path), '/');
$content_type = (string)($entry['content_type'] ?? '');
$size = (int)($entry['size'] ?? 0);
$etag = (string)($entry['etag'] ?? '');
}
return $cache[$image_id] = array( return $cache[$image_id] = array(
'image_id'=>$image_id, 'image_id'=>$image_id,
'connection_id'=>$connection_id, 'connection_id'=>$connection_id,
'webdav_path'=>$webdav_path, 'webdav_path'=>$webdav_path,
'root_is_base'=>$root_is_base,
'content_type'=>$content_type, 'content_type'=>$content_type,
'size'=>$size, 'size'=>$size,
'etag'=>$etag, 'etag'=>$etag,
@@ -412,8 +437,16 @@ function bratonien_tools_filter_webdav_src_url($url, $src_image)
function bratonien_tools_filter_webdav_derivative_url($url, $params, $src_image, $rel_url) function bratonien_tools_filter_webdav_derivative_url($url, $params, $src_image, $rel_url)
{ {
if (!is_object($src_image) || empty($src_image->id)) return $url; if (!is_object($src_image) || empty($src_image->id)) return $url;
$info = bratonien_tools_webdav_image_source_info((int)$src_image->id);
if (!$info) return $url; // Hotpath: Bei einem bereits vorbereiteten WebDAV-Derivat keinerlei
// Connection-DB oder Mapping-Datei mehr anfassen. Der reale Quellpfad
// reicht aus, um Connector-Bilder sicher zu erkennen.
$source_path = $src_image->get_path();
$resolved_source = $source_path !== '' ? realpath($source_path) : false;
if ($resolved_source === false || !preg_match('#/nc-webdav-source/connection-[0-9]+/root-[0-9]+/#', str_replace('\\\\', '/', $resolved_source)))
{
return $url;
}
try try
{ {

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.6.1 Version: 0.9.7.17
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,13 +24,15 @@ 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('loc_end_index_category_thumbnails', 'bratonien_tools_watermark_prepare_album_overview');
add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4); add_event_handler('get_derivative_url', 'bratonien_tools_filter_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL, 4);
add_event_handler('get_src_image_url', 'bratonien_tools_filter_webdav_src_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 2); add_event_handler('get_src_image_url', 'bratonien_tools_filter_webdav_src_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 2);
add_event_handler('get_derivative_url', 'bratonien_tools_filter_webdav_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 4);
add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action'); add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action');
add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2); add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
add_event_handler('init', 'bratonien_tools_watermark_cache_upgrade', EVENT_HANDLER_PRIORITY_NEUTRAL - 40);
add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EVENT_HANDLER_PRIORITY_NEUTRAL - 30); add_event_handler('init', 'bratonien_tools_prepare_connector_private_import', EVENT_HANDLER_PRIORITY_NEUTRAL - 30);
add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', EVENT_HANDLER_PRIORITY_NEUTRAL - 20); add_event_handler('init', 'bratonien_tools_prepare_private_album_permissions', EVENT_HANDLER_PRIORITY_NEUTRAL - 20);
add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10); add_event_handler('init', 'bratonien_tools_preserve_private_album_access', EVENT_HANDLER_PRIORITY_NEUTRAL - 10);

View File

@@ -8,6 +8,8 @@ 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');
require_once(dirname(__FILE__) . '/include/nc_legacy_reset.inc.php');
class bratonien_tools_maintain extends PluginMaintain class bratonien_tools_maintain extends PluginMaintain
{ {
@@ -18,9 +20,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,
@@ -45,6 +52,18 @@ class bratonien_tools_maintain extends PluginMaintain
public function update($old_version, $new_version, &$errors = array()) public function update($old_version, $new_version, &$errors = array())
{ {
$this->prepare($errors); $this->prepare($errors);
if (version_compare((string)$old_version, '0.9.7.7', '<') && version_compare((string)$new_version, '0.9.7.7', '>='))
{
try
{
bratonien_tools_nc_reset_legacy_imports();
}
catch (Throwable $e)
{
$errors[] = 'Connector-Altbestand konnte nicht vollständig bereinigt werden: '.$e->getMessage();
}
}
} }
public function uninstall() public function uninstall()

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,79 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 0002
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"
SOURCE_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-gallery/connection-$CONNECTION_ID"
DERIVATIVE_CACHE="$PIWIGO_ROOT/_data/i/_data/bratonien-tools/nc-webdav-gallery/connection-$CONNECTION_ID"
PIWIGO_DATA="$PIWIGO_ROOT/_data"
normalize_connector_cache_permissions() {
[[ -d "$PIWIGO_DATA" ]] || return 0
local data_uid data_gid current_uid path
data_uid="$(stat -c '%u' "$PIWIGO_DATA")"
data_gid="$(stat -c '%g' "$PIWIGO_DATA")"
current_uid="$(id -u)"
for path in "$SOURCE_CACHE" "$PREVIEW_CACHE" "$DERIVATIVE_CACHE"; do
[[ -e "$path" ]] || continue
if [[ "$current_uid" -eq 0 ]]; then
chown -R "$data_uid:$data_gid" -- "$path"
fi
if ! find "$path" -type d -exec chmod 2775 {} + 2>/dev/null; then
echo "Hinweis: Verzeichnisrechte konnten ohne Root-Rechte nicht vollständig repariert werden: $path" >&2
fi
if ! find "$path" -type f -exec chmod 0664 {} + 2>/dev/null; then
echo "Hinweis: Dateirechte konnten ohne Root-Rechte nicht vollständig repariert werden: $path" >&2
fi
done
}
# Altbestände aus früheren root/systemd-Läufen werden repariert, sobald dieser
# Lauf mit ausreichenden Rechten ausgeführt wird. Bei normalen Webserver-Läufen
# werden zumindest alle eigenen Dateien gruppenschreibbar gehalten.
normalize_connector_cache_permissions
# Dieser Medienlauf wird erst nach einem erfolgreichen WebDAV-Aufbau und einer
# erfolgreichen Piwigo-Synchronisierung gestartet. Deshalb darf erst hier aus
# "lokale Connector-Datei fehlt" auf eine in Nextcloud entfernte Freigabe/Datei
# geschlossen werden. Bei einer nicht erreichbaren Verbindung wird dieser Block
# niemals ausgeführt und der vorhandene Piwigo-Bestand bleibt unverändert.
php "$SCRIPT_DIR/cleanup-missing-webdav-images.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$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"
normalize_connector_cache_permissions

View File

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

View File

@@ -6,7 +6,7 @@ if (PHP_SAPI !== 'cli')
exit(1); exit(1);
} }
const BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION = '0.9.6.1'; const BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION = '0.9.7.8';
$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'] ?? ''), '/');
@@ -46,20 +46,84 @@ if (!function_exists('bratonien_tools_webdav_image_source_info') || !function_ex
exit(1); exit(1);
} }
function bratonien_tools_webdav_modus_config()
{
global $conf;
$modus = $conf['modus_theme'] ?? array();
if (is_string($modus) && $modus !== '')
{
$decoded = @unserialize($modus);
if (is_array($decoded)) $modus = $decoded;
}
if (!is_array($modus)) $modus = array();
return array_merge(
array(
'index_photo_deriv'=>'2small',
'index_photo_deriv_hdpi'=>'xsmall',
),
$modus
);
}
function bratonien_tools_webdav_gallery_base_params()
{
$modus = bratonien_tools_webdav_modus_config();
$type = trim((string)($modus['index_photo_deriv'] ?? '2small'));
$params = $type !== '' ? @ImageStdParams::get_by_type($type) : null;
if ($params) return $params;
$params = ImageStdParams::get_by_type(IMG_THUMB);
if (!$params)
{
throw new RuntimeException('Piwigo-Galeriederivat ist nicht konfiguriert.');
}
return $params;
}
function bratonien_tools_webdav_modus_gallery_params(SrcImage $src, $base_params)
{
$row_height = (int)$base_params->max_height();
$candidates = array($base_params);
foreach (ImageStdParams::get_defined_type_map() as $params)
{
if (
$params->max_height() > $row_height
&& $params->sizing->max_crop == $base_params->sizing->max_crop
)
{
$candidates[] = $params;
if (count($candidates) === 3) break;
}
}
$selected = $base_params;
foreach ($candidates as $params)
{
$selected = $params;
$probe = new DerivativeImage($params, $src);
$size = $probe->get_size();
if ((int)$size[1] >= $row_height - 2) break;
}
return $selected;
}
try try
{ {
$variants = bratonien_tools_webdav_derivative_variants(); $gallery_base_params = bratonien_tools_webdav_gallery_base_params();
if (!$variants) $gallery_base_type = (string)$gallery_base_params->type;
{
throw new RuntimeException('Keine Piwigo-Derivate konfiguriert.');
}
$images = 0; $images = 0;
$generated_or_ready = 0; $generated_or_ready = 0;
$identity = 0; $identity = 0;
$metadata_repaired = 0; $metadata_repaired = 0;
$legacy_derivatives_rebuilt = 0;
$errors = 0; $errors = 0;
$error_lines = array(); $error_lines = array();
$variant_counts = 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))
@@ -109,34 +173,59 @@ try
} }
$src = new SrcImage($row); $src = new SrcImage($row);
foreach ($variants as $variant_name => $params) try
{ {
try $gallery_params = bratonien_tools_webdav_modus_gallery_params($src, $gallery_base_params);
{ $variant = (string)$gallery_params->type;
$probe = new DerivativeImage($params, $src); if ($variant === '') $variant = 'custom';
if ($probe->same_as_source()) $variant_counts[$variant] = ($variant_counts[$variant] ?? 0) + 1;
{
$identity++;
continue;
}
$detail = ''; $probe = new DerivativeImage($gallery_params, $src);
if (bratonien_tools_webdav_generate_derivative($params, $src, $detail)) if ($probe->same_as_source())
{
$identity++;
continue;
}
$target = $probe->get_path();
if ($target !== '' && is_file($target) && is_readable($target))
{
$expected_size = $gallery_params->compute_final_size(array($preview_width, $preview_height));
$existing_size = @getimagesize($target);
if (
is_array($expected_size)
&& isset($expected_size[0], $expected_size[1])
&& is_array($existing_size)
&& isset($existing_size[0], $existing_size[1])
&& ((int)$existing_size[0] !== (int)$expected_size[0] || (int)$existing_size[1] !== (int)$expected_size[1])
)
{ {
$generated_or_ready++; if (!@unlink($target))
} {
else $errors++;
{ $error_lines[] = 'Bild #'.$image_id.' Galerie: falsches Alt-Derivat konnte nicht entfernt werden: '.$target;
$errors++; continue;
$error_lines[] = 'Bild #'.$image_id.' '.$variant_name.': '.($detail !== '' ? $detail : 'Derivat konnte nicht erzeugt werden.'); }
$legacy_derivatives_rebuilt++;
} }
} }
catch (Throwable $e)
$detail = '';
if (bratonien_tools_webdav_generate_derivative($gallery_params, $src, $detail))
{
$generated_or_ready++;
}
else
{ {
$errors++; $errors++;
$error_lines[] = 'Bild #'.$image_id.' '.$variant_name.': '.get_class($e).': '.$e->getMessage(); $error_lines[] = 'Bild #'.$image_id.' Galerie: '.($detail !== '' ? $detail : 'Galeriederivat konnte nicht erzeugt werden.');
} }
} }
catch (Throwable $e)
{
$errors++;
$error_lines[] = 'Bild #'.$image_id.' Galerie: '.get_class($e).': '.$e->getMessage();
}
} }
if ($metadata_repaired > 0) if ($metadata_repaired > 0)
@@ -157,12 +246,22 @@ try
} }
} }
ksort($variant_counts);
$variant_parts = array();
foreach ($variant_counts as $type=>$count)
{
$variant_parts[] = $type.':'.$count;
}
echo 'WebDAV-Derivative-Builder: version='.BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION. echo 'WebDAV-Derivative-Builder: version='.BRATONIEN_WEBDAV_DERIVATIVE_BUILDER_VERSION.
' bilder='.$images. ' bilder='.$images.
' varianten='.count($variants). ' varianten_pro_bild=1'.
' modus_basis='.$gallery_base_type.
' modus_varianten='.implode(',', $variant_parts).
' bereit='.$generated_or_ready. ' bereit='.$generated_or_ready.
' identisch='.$identity. ' identisch='.$identity.
' metadaten_repariert='.$metadata_repaired. ' metadaten_repariert='.$metadata_repaired.
' alt_derivate_neu='.$legacy_derivatives_rebuilt.
' fehler='.$errors."\n"; ' fehler='.$errors."\n";
exit($errors > 0 ? 1 : 0); exit($errors > 0 ? 1 : 0);

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

View File

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

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

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

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

View File

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

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

View File

@@ -1,32 +1,67 @@
#!/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}"
TARGET_CONNECTION_ID="${BRATONIEN_NC_CONNECTION_ID:-0}"
GLOBAL_LOCK_DIR="${PIWIGO_ROOT_DEFAULT%/}/_data/bratonien-tools/nc-connector-scheduler"
GLOBAL_LOCK_FILE="$GLOBAL_LOCK_DIR/worker.lock"
mkdir -p -- "$GLOBAL_LOCK_DIR"
exec 8>"$GLOBAL_LOCK_FILE"
if ! flock -n 8; then
echo "NC Connector: ein Lauf ist bereits aktiv."
exit 0
fi
shopt -s nullglob shopt -s nullglob
if ! php "$SCRIPT_DIR/reconcile.php"; then if [[ ! "$TARGET_CONNECTION_ID" =~ ^[0-9]+$ ]]; then
echo "NC Connector: gespeicherte lokale Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2 echo "NC Connector: ungültige Ziel-Verbindungs-ID: $TARGET_CONNECTION_ID" >&2
exit 1 exit 1
fi fi
if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then read_config_value() {
echo "NC Connector: WebDAV-Testverbindungen konnten nicht mit der parallelen 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"
}
# Nach dem Reconcile kennt Piwigo alle noch aktiven WebDAV-Verbindungen. Jetzt write_route_status() {
# werden Sites, Alben, Bilddatensaetze und Derivate geloeschter Verbindungen local route="$1"
# entfernt. Das erfasst auch Verbindungen, die bereits vor diesem Update local label="$2"
# geloescht wurden. local detail="$3"
if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then local success="$4"
echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2 [[ -n "${ROUTE_STATUS_FILE:-}" ]] || return 0
exit 1 mkdir -p -- "$(dirname -- "$ROUTE_STATUS_FILE")"
fi 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)
@@ -37,51 +72,123 @@ 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- candidate_id="$(read_config_value CONNECTION_ID "$candidate")"
# Dateisynchronisierung ausführt. So wird der neue Baum im selben Minutenlauf if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$candidate_id" != "$TARGET_CONNECTION_ID" ]]; then
# sichtbar, ohne dass der WebDAV-Zweig selbst eine zweite globale Piwigo- continue
# 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 fi
route_piwigo_root="$(read_config_value PIWIGO_ROOT "$candidate")"
[[ -n "$route_piwigo_root" ]] && break
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=()
matched_count=0
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
failure_count=$((failure_count + 1))
summary_parts+=("$name: ungueltige Verbindungs-ID")
continue
fi fi
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$connection_id" != "$TARGET_CONNECTION_ID" ]]; then
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" matched_count=$((matched_count + 1))
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then webdav_count=$((webdav_count + 1))
result=1 echo "NC Connector WebDAV #$connection_id: $name"
output=""
if output="$(env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh" 2>&1)"; then
[[ -z "$output" ]] || printf '%s\n' "$output"
summary_parts+=("WebDAV #$connection_id erfolgreich")
else
code=$?
[[ -z "$output" ]] || printf '%s\n' "$output" >&2
failure_count=$((failure_count + 1))
summary_parts+=("WebDAV #$connection_id fehlgeschlagen (Exit $code)")
fi 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
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$connection_id" != "$TARGET_CONNECTION_ID" ]]; then
continue
fi
matched_count=$((matched_count + 1))
piwigo_root="$(read_config_value PIWIGO_ROOT "$config")"
[[ -n "$piwigo_root" ]] || piwigo_root="$PIWIGO_ROOT_DEFAULT"
tombstone_dir="${piwigo_root%/}/_data/bratonien-tools/nc-connector-status"
if [[ -f "$tombstone_dir/deleted-$connection_id" ]]; then
echo "NC Connector: Verbindung $connection_id wurde geloescht; Laufzeitdateien werden entfernt."
rm -f -- "$CONFIG_DIR/connection-$connection_id.conf" \
"$CONFIG_DIR/connection-$connection_id.db-password" \
"$CONFIG_DIR/connection-$connection_id.piwigo-password" \
"$CONFIG_DIR/connection-$connection_id.storages.tsv" \
"$CONFIG_DIR/connection-$connection_id.roots.tsv"
rm -f -- "$tombstone_dir/deleted-$connection_id"
continue
fi
local_count=$((local_count + 1))
echo "NC Connector Local #$connection_id: $name"
if env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
summary_parts+=("Local #$connection_id erfolgreich")
else
failure_count=$((failure_count + 1))
summary_parts+=("Local #$connection_id fehlgeschlagen")
fi
done
fi
if [[ "$TARGET_CONNECTION_ID" -gt 0 && "$matched_count" -eq 0 ]]; then
write_route_status "failed" "FEHLER - Verbindung #$TARGET_CONNECTION_ID" "Keine Laufzeitkonfiguration für Verbindung #$TARGET_CONNECTION_ID gefunden." "0"
echo "NC Connector: keine Laufzeitkonfiguration für Verbindung #$TARGET_CONNECTION_ID gefunden." >&2
exit 1
fi
summary_detail="$(IFS='; '; printf '%s' "${summary_parts[*]}")"
[[ -n "$summary_detail" ]] || summary_detail="Keine Verbindung wurde ausgefuehrt."
if [[ "$failure_count" -eq 0 ]]; then
if [[ "$webdav_count" -gt 0 && "$local_count" -gt 0 ]]; then
route="mixed"
label="WebDAV + Local"
elif [[ "$webdav_count" -gt 0 ]]; then
route="webdav"
label="WebDAV"
else
route="local"
label="Local"
fi
write_route_status "$route" "$label" "$summary_detail" "1"
echo "NC Connector: angeforderte Verbindung wurde erfolgreich verarbeitet."
exit 0
fi
write_route_status "failed" "FEHLER - angeforderte Verbindung" "$summary_detail" "0"
echo "NC Connector: die angeforderte Verbindung ist fehlgeschlagen." >&2
exit 1

View File

@@ -59,10 +59,6 @@ for target in (status_file, public_file):
PY 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

@@ -61,6 +61,7 @@ function bratonien_tools_save_album_rule()
))); )));
} }
bratonien_tools_clear_watermark_cache();
return array('message'=>'Albumregel gespeichert.'); return array('message'=>'Albumregel gespeichert.');
} }
@@ -93,7 +94,34 @@ function bratonien_tools_resolve_album_rule($category_id, array $categories, arr
$by_id[(int)$category['id']] = $category; $by_id[(int)$category['id']] = $category;
} }
$current = (int)$category_id; $category_id = (int)$category_id;
$root = $by_id[$category_id] ?? null;
$is_private = $root && isset($root['status']) && $root['status'] === 'private';
if ($is_private)
{
if (isset($rules[$category_id]))
{
$rule = $rules[$category_id];
if ($rule['mode'] === 'disabled')
{
return array('mode'=>'disabled','profile_id'=>null,'source'=>'album');
}
if ($rule['mode'] === 'profile')
{
return array('mode'=>'profile','profile_id'=>(int)$rule['profile_id'],'source'=>'album');
}
}
$profile_id = $defaults['private_profile'] ?? null;
if (empty($profile_id))
{
return array('mode'=>'disabled','profile_id'=>null,'source'=>'global');
}
return array('mode'=>'profile','profile_id'=>(int)$profile_id,'source'=>'global');
}
$current = $category_id;
$visited = array(); $visited = array();
while ($current > 0 && isset($by_id[$current]) && !isset($visited[$current])) while ($current > 0 && isset($by_id[$current]) && !isset($visited[$current]))
@@ -116,10 +144,7 @@ function bratonien_tools_resolve_album_rule($category_id, array $categories, arr
$current = (int)($by_id[$current]['id_uppercat'] ?? 0); $current = (int)($by_id[$current]['id_uppercat'] ?? 0);
} }
$root = $by_id[(int)$category_id] ?? null; $profile_id = $defaults['public_profile'] ?? null;
$is_private = $root && isset($root['status']) && $root['status'] === 'private';
$profile_id = $is_private ? ($defaults['private_profile'] ?? null) : ($defaults['public_profile'] ?? null);
if (empty($profile_id)) if (empty($profile_id))
{ {
return array('mode'=>'disabled','profile_id'=>null,'source'=>'global'); return array('mode'=>'disabled','profile_id'=>null,'source'=>'global');

View File

@@ -128,6 +128,7 @@ function bratonien_tools_save_watermark_profile()
mass_inserts($table, array_keys($data), array($data)); mass_inserts($table, array_keys($data), array($data));
} }
bratonien_tools_clear_watermark_cache();
return array('message'=>'Wasserzeichenprofil gespeichert.'); return array('message'=>'Wasserzeichenprofil gespeichert.');
} }
@@ -158,6 +159,7 @@ function bratonien_tools_delete_watermark_profile()
} }
pwg_query('DELETE FROM '.bratonien_tools_table('watermark_profiles').' WHERE id='.$id); pwg_query('DELETE FROM '.bratonien_tools_table('watermark_profiles').' WHERE id='.$id);
bratonien_tools_clear_watermark_cache();
return array('message'=>'Wasserzeichenprofil geloescht.'); return array('message'=>'Wasserzeichenprofil geloescht.');
} }
@@ -174,6 +176,7 @@ function bratonien_tools_duplicate_watermark_profile()
$profile['name'] .= ' (Kopie)'; $profile['name'] .= ' (Kopie)';
$profile['created'] = date('Y-m-d H:i:s'); $profile['created'] = date('Y-m-d H:i:s');
mass_inserts(bratonien_tools_table('watermark_profiles'), array_keys($profile), array($profile)); mass_inserts(bratonien_tools_table('watermark_profiles'), array_keys($profile), array($profile));
bratonien_tools_clear_watermark_cache();
return array('message'=>'Wasserzeichenprofil dupliziert.'); return array('message'=>'Wasserzeichenprofil dupliziert.');
} }

View File

@@ -4,6 +4,59 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
function bratonien_tools_watermark_cache_dir()
{
return PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR.'bratonien-watermark';
}
function bratonien_tools_remove_tree($path)
{
$path = rtrim((string)$path, DIRECTORY_SEPARATOR);
if ($path === '' || !file_exists($path)) return;
if (is_file($path) || is_link($path))
{
@unlink($path);
return;
}
foreach (scandir($path) ?: array() as $entry)
{
if ($entry === '.' || $entry === '..') continue;
bratonien_tools_remove_tree($path.DIRECTORY_SEPARATOR.$entry);
}
@rmdir($path);
}
function bratonien_tools_clear_watermark_cache()
{
$dir = bratonien_tools_watermark_cache_dir();
$derivative_root = realpath(PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR);
$parent = realpath(dirname($dir));
if ($derivative_root === false || $parent === false || $parent !== $derivative_root)
{
throw new RuntimeException('Wasserzeichen-Cachepfad ist ungueltig.');
}
if (is_dir($dir))
{
bratonien_tools_remove_tree($dir);
}
}
function bratonien_tools_watermark_cache_upgrade()
{
$version = '0.9.7.13';
if ((string)conf_get_param('bratonien_watermark_cache_version', '') === $version)
{
return;
}
bratonien_tools_clear_watermark_cache();
conf_update_param('bratonien_watermark_cache_version', $version);
}
function bratonien_tools_get_watermark_defaults() function bratonien_tools_get_watermark_defaults()
{ {
$defaults = conf_get_param('bratonien_watermark_defaults', null); $defaults = conf_get_param('bratonien_watermark_defaults', null);
@@ -40,6 +93,7 @@ function bratonien_tools_save_watermark_defaults()
); );
conf_update_param('bratonien_watermark_defaults', json_encode($config)); conf_update_param('bratonien_watermark_defaults', json_encode($config));
bratonien_tools_clear_watermark_cache();
return array('message'=>'Globale Wasserzeichenregeln gespeichert.'); return array('message'=>'Globale Wasserzeichenregeln gespeichert.');
} }

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)
{ {
@@ -120,7 +121,7 @@ $options = array(
CURLOPT_USERPWD => $user.':'.$password, CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false, CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false, CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.6.1', CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.7.16',
CURLOPT_HEADERFUNCTION => function($ch, $line) CURLOPT_HEADERFUNCTION => function($ch, $line)
{ {
$length = strlen($line); $length = strlen($line);
@@ -147,6 +148,7 @@ $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']);