Compare commits

..

20 Commits

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

View File

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

263
README.md
View File

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

View File

@@ -4,12 +4,90 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!');
}
function bratonien_tools_nc_connector_webdav_site_id(array $connection)
{
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
if ((string)($connection['adapter'] ?? '') !== 'remote') return 0;
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') return 0;
$gallery_root = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
$piwigo_root = rtrim(PHPWG_ROOT_PATH, '/');
if ($gallery_root === '' || strpos($gallery_root, $piwigo_root.'/') !== 0) return 0;
$relative = ltrim(substr($gallery_root, strlen($piwigo_root)), '/');
if ($relative === '') return 0;
$site_url = './'.rtrim($relative, '/').'/';
$result = pwg_query(
"SELECT id FROM ".SITES_TABLE.
" WHERE galleries_url='".pwg_db_real_escape_string($site_url)."' LIMIT 1"
);
if (!pwg_db_num_rows($result)) return 0;
$row = pwg_db_fetch_assoc($result);
return (int)$row['id'];
}
function bratonien_tools_nc_connector_remove_webdav_piwigo_content(array $connection)
{
$site_id = bratonien_tools_nc_connector_webdav_site_id($connection);
if ($site_id < 1) return array('site_id'=>0, 'images'=>0);
include_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
$image_rows = array();
$query = '
SELECT DISTINCT i.id, i.path, i.representative_ext
FROM '.IMAGES_TABLE.' AS i
LEFT JOIN '.CATEGORIES_TABLE.' AS sc ON sc.id = i.storage_category_id
LEFT JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON ic.image_id = i.id
LEFT JOIN '.CATEGORIES_TABLE.' AS vc ON vc.id = ic.category_id
WHERE sc.site_id = '.$site_id.' OR vc.site_id = '.$site_id.'
;';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
$row['id'] = (int)$row['id'];
$image_rows[$row['id']] = $row;
}
foreach ($image_rows as $row)
{
try
{
delete_element_derivatives($row);
}
catch (Throwable $e)
{
error_log('Bratonien WebDAV delete derivative cleanup #'.(int)$row['id'].': '.$e->getMessage());
}
}
delete_site($site_id);
if ($image_rows)
{
$ids = array_keys($image_rows);
$remaining = query2array(
'SELECT id FROM '.IMAGES_TABLE.' WHERE id IN ('.implode(',', array_map('intval', $ids)).')',
null,
'id'
);
if ($remaining)
{
delete_elements(array_map('intval', $remaining), false);
}
}
invalidate_user_cache(true);
return array('site_id'=>$site_id, 'images'=>count($image_rows));
}
/**
* Delete a connector connection from Piwigo without depending on the web
* server being allowed to write into root-owned runtime directories.
*
* Runtime files are removed by runtime/cleanup-stale.php on the next runner
* cycle before any connector sync is started.
* 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()
{
@@ -20,12 +98,18 @@ function bratonien_tools_nc_connector_delete_safe()
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
}
$cleanup = array('site_id'=>0, 'images'=>0);
if (
(string)($connection['adapter'] ?? '') === 'remote'
&& (string)($connection['config']['source_mode'] ?? '') === 'webdav-placeholder'
)
{
$cleanup = bratonien_tools_nc_connector_remove_webdav_piwigo_content($connection);
}
$table = bratonien_tools_nc_connector_table();
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
// Public status data is only cache/runtime information. Remove it when PHP
// has permission, but never make successful DB deletion depend on ownership
// of files previously created by the root-run connector service.
$status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status';
$public_status = $status_dir.'/connection-'.$id.'.json';
if (is_file($public_status))
@@ -33,15 +117,19 @@ function bratonien_tools_nc_connector_delete_safe()
@unlink($public_status);
}
// Keep the old tombstone mechanism as a best-effort fast path for systems
// where the web server can write here. cleanup-stale.php is the authoritative
// fallback and does not require this file.
if (is_dir($status_dir) || @mkdir($status_dir, 0755, true))
{
@file_put_contents($status_dir.'/deleted-'.$id, date('c')."\n", LOCK_EX);
}
if ((int)$cleanup['site_id'] > 0)
{
return array(
'message'=>'Connector-Verbindung wurde gelöscht. Die zugehörigen Piwigo-Alben und '.(int)$cleanup['images'].' Bilder wurden aus Piwigo entfernt. Nextcloud-Dateien blieben unverändert. Laufzeit- und Vorschaudaten werden automatisch bereinigt.',
);
}
return array(
'message'=>'Connector-Verbindung wurde gelöscht. Verbliebene Laufzeitdateien werden vor dem nächsten Connector-Lauf automatisch entfernt. Nextcloud- und Piwigo-Bilder blieben unverändert.',
'message'=>'Connector-Verbindung wurde gelöscht. Verbliebene Laufzeitdateien werden vor dem nächsten Connector-Lauf automatisch entfernt. Quelldateien blieben unverändert.',
);
}

View File

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

View File

@@ -1,7 +1,7 @@
<?php
/*
Plugin Name: Bratonien Tools
Version: 0.9.5.21
Version: 0.9.6.1
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$pluginRoot = dirname(__DIR__);
$piwigoRoot = dirname($pluginRoot, 2);
define('PHPWG_ROOT_PATH', rtrim($piwigoRoot, '/').'/');
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_ADDR'] = '127.0.0.1';
$_SERVER['SERVER_NAME'] = 'localhost';
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['SERVER_PORT'] = '80';
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/runtime/cleanup-webdav-piwigo.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'admin/include/functions.php');
function bratonien_cleanup_tree($path, $allowed_root)
{
$path = rtrim((string)$path, '/');
$allowed_root = rtrim((string)$allowed_root, '/');
if ($path === '' || $allowed_root === '' || strpos($path, $allowed_root.'/') !== 0 || !file_exists($path)) return;
if (is_link($path) || is_file($path))
{
@unlink($path);
return;
}
$items = @scandir($path);
if (!is_array($items)) return;
foreach ($items as $item)
{
if ($item === '.' || $item === '..') continue;
bratonien_cleanup_tree($path.'/'.$item, $allowed_root);
}
@rmdir($path);
}
function bratonien_webdav_site_images($site_id)
{
$rows = array();
$query = '
SELECT DISTINCT i.id, i.path, i.representative_ext
FROM '.IMAGES_TABLE.' AS i
LEFT JOIN '.CATEGORIES_TABLE.' AS sc ON sc.id = i.storage_category_id
LEFT JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON ic.image_id = i.id
LEFT JOIN '.CATEGORIES_TABLE.' AS vc ON vc.id = ic.category_id
WHERE sc.site_id = '.(int)$site_id.' OR vc.site_id = '.(int)$site_id.'
;';
$result = pwg_query($query);
while ($row = pwg_db_fetch_assoc($result))
{
$row['id'] = (int)$row['id'];
$rows[$row['id']] = $row;
}
return $rows;
}
try
{
$connection_table = function_exists('bratonien_tools_nc_connector_table')
? bratonien_tools_nc_connector_table()
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$active = array();
$exists = pwg_query("SHOW TABLES LIKE '".pwg_db_real_escape_string($connection_table)."'");
if (pwg_db_num_rows($exists))
{
$result = pwg_query("SELECT id FROM `".$connection_table."`");
while ($row = pwg_db_fetch_assoc($result))
{
$active[(int)$row['id']] = true;
}
}
$site_prefix = './_data/bratonien-tools/nc-webdav-gallery/connection-';
$result = pwg_query(
"SELECT id, galleries_url FROM ".SITES_TABLE.
" WHERE galleries_url LIKE '".pwg_db_real_escape_string($site_prefix)."%'"
);
$removed_sites = 0;
$removed_images = 0;
while ($site = pwg_db_fetch_assoc($result))
{
$site_id = (int)$site['id'];
$site_url = (string)$site['galleries_url'];
if (!preg_match('#^\./_data/bratonien-tools/nc-webdav-gallery/connection-([0-9]+)/$#', $site_url, $match)) continue;
$connection_id = (int)$match[1];
if ($connection_id < 1 || isset($active[$connection_id])) continue;
$images = bratonien_webdav_site_images($site_id);
foreach ($images as $row)
{
try
{
delete_element_derivatives($row);
}
catch (Throwable $e)
{
fwrite(STDERR, 'WebDAV-Cleanup: Derivate von Bild #'.(int)$row['id'].' konnten nicht vollständig entfernt werden: '.$e->getMessage()."\n");
}
}
delete_site($site_id);
if ($images)
{
$ids = array_keys($images);
$remaining = query2array(
'SELECT id FROM '.IMAGES_TABLE.' WHERE id IN ('.implode(',', array_map('intval', $ids)).')',
null,
'id'
);
if ($remaining)
{
delete_elements(array_map('intval', $remaining), false);
}
}
$removed_sites++;
$removed_images += count($images);
echo 'NC WebDAV: entferne verwaiste Piwigo-Site '.$site_id.' für gelöschte Verbindung '.$connection_id.".\n";
$gallery_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-gallery';
$source_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-source';
$preview_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-webdav-preview';
bratonien_cleanup_tree($gallery_root.'/connection-'.$connection_id, $gallery_root);
bratonien_cleanup_tree($source_root.'/connection-'.$connection_id, $source_root);
bratonien_cleanup_tree($preview_root.'/connection-'.$connection_id, $preview_root);
$state_root = '/var/lib/bratonien-tools/nc-connector';
bratonien_cleanup_tree($state_root.'/connection-'.$connection_id, $state_root);
foreach (glob('/etc/bratonien-tools/nc-connector/webdav-connection-'.$connection_id.'.*') ?: array() as $file)
{
@unlink($file);
}
$status_root = PHPWG_ROOT_PATH.'_data/bratonien-tools/nc-connector-status';
@unlink($status_root.'/connection-'.$connection_id.'.json');
@unlink($status_root.'/deleted-'.$connection_id);
}
if ($removed_sites > 0)
{
invalidate_user_cache(true);
}
echo 'NC WebDAV Piwigo-Cleanup: sites='.$removed_sites.' bilder='.$removed_images."\n";
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, 'NC WebDAV Piwigo-Cleanup: '.$e->getMessage()."\n");
exit(1);
}

View File

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

View File

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

View File

@@ -15,6 +15,15 @@ if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then
exit 1
fi
# Nach dem Reconcile kennt Piwigo alle noch aktiven WebDAV-Verbindungen. Jetzt
# werden Sites, Alben, Bilddatensaetze und Derivate geloeschter Verbindungen
# entfernt. Das erfasst auch Verbindungen, die bereits vor diesem Update
# geloescht wurden.
if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then
echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2
exit 1
fi
if ! php "$SCRIPT_DIR/cleanup-stale.php"; then
echo "NC Connector: verwaiste Laufzeitdateien konnten nicht bereinigt werden." >&2
exit 1

View File

@@ -58,7 +58,7 @@ if (!empty($_GET['preview']))
{
$mtime = @filemtime($preview) ?: time();
$etag = sha1($preview.'|'.$mtime.'|'.(@filesize($preview) ?: 0));
header('Content-Type: image/webp');
header('Content-Type: '.bratonien_tools_webdav_preview_content_type($preview));
header('Content-Length: '.(string)filesize($preview));
header('ETag: "'.$etag.'"');
header('Cache-Control: private, max-age=86400, must-revalidate');
@@ -69,7 +69,7 @@ if (!empty($_GET['preview']))
http_response_code(304);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'HEAD') readfile($preview);
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'HEAD') readfile($preview);
exit;
}
}
@@ -120,7 +120,7 @@ $options = array(
CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.5.19',
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.6.1',
CURLOPT_HEADERFUNCTION => function($ch, $line)
{
$length = strlen($line);
@@ -152,7 +152,7 @@ if (!empty($_SERVER['HTTP_RANGE']))
$range = trim((string)$_SERVER['HTTP_RANGE']);
if (preg_match('/^bytes=(.+)$/i', $range, $m)) $options[CURLOPT_RANGE] = $m[1];
}
if ($_SERVER['REQUEST_METHOD'] === 'HEAD')
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'HEAD')
{
$options[CURLOPT_NOBODY] = true;
}