Compare commits

...

96 Commits

Author SHA1 Message Date
Terranom674
add913aab1 Update current status for 0.9.7.1.7 2026-08-20 20:52:44 +02:00
Terranom674
6a0851a30c Update README for 0.9.7.1.7 runtime state 2026-08-20 20:52:12 +02:00
Terranom674
c057f7922e Version 0.9.7.1.7 2026-08-20 20:48:09 +02:00
Terranom674
e3d55a1950 Version 9.7.1.7 2026-08-20 20:47:15 +02:00
Terranom674
84dce41b1c Version 0.9.7.1.6 2026-08-20 20:43:16 +02:00
Terranom674
71577f989f Keep Piwigo placeholder visible for pending picture thumbnails 2026-08-20 20:36:47 +02:00
Terranom674
9c8e44da09 Hide pending WebDAV picture thumbnails 2026-08-20 20:36:08 +02:00
Terranom674
09b6465aad Version 9.7.1.6 2026-08-20 20:26:50 +02:00
Terranom674
17a00eebdb Support Piwigo AJAX thumbnail loader responses 2026-08-20 20:26:32 +02:00
Terranom674
199d954409 Bump version to 9.7.1.5 2026-08-20 20:09:52 +02:00
Terranom674
db21cf2c5e Make WebDAV placeholder source writable by runtime group 2026-08-20 20:09:34 +02:00
Terranom674
f0f7556aaf Bump version to 9.7.1.4 2026-08-20 20:04:57 +02:00
Terranom674
68e4cd876d Fix Piwigo-relative source path resolution for WebDAV preview derivatives 2026-08-20 20:04:37 +02:00
Terranom674
43dc600b98 Bump version to 9.7.1.3 2026-08-20 19:57:40 +02:00
Terranom674
fdae62520e Fix Nextcloud preview endpoint path 2026-08-20 19:57:20 +02:00
Terranom674
8023f7b5d7 Bump version to 9.7.1.2 2026-08-20 19:47:45 +02:00
Terranom674
8a5435ea6b Keep WebDAV mapping readable by Piwigo runtime 2026-08-20 19:47:23 +02:00
Terranom674
880ff2a2b1 Fix WebDAV state permissions for runtime preview access 2026-08-20 19:46:56 +02:00
Terranom674
062d38ab08 Bump version to 9.7.1.1 2026-08-20 19:40:51 +02:00
Terranom674
10065ece46 Make WebDAV mapping lookup robust by webdav_path 2026-08-20 19:40:29 +02:00
Terranom674
bccc5cafda Set current status rebuild baseline to 9.7.1.0 2026-08-20 19:33:28 +02:00
Terranom674
2c52228265 Set README rebuild baseline to 9.7.1.0 2026-08-20 19:33:07 +02:00
Terranom674
7277a8b287 Start connector rebuild at version 9.7.1.0 2026-08-20 19:32:38 +02:00
Terranom674
9155a52290 Refresh README for preview-backed WebDAV derivatives 2026-08-20 19:30:34 +02:00
Terranom674
afa6d02c46 Update current status for preview-backed derivatives 2026-08-20 19:30:02 +02:00
Terranom674
9c94953267 Bump version to 0.9.6.8.25 2026-08-20 19:29:24 +02:00
Terranom674
66d62321d0 Use Nextcloud previews for small WebDAV derivatives 2026-08-20 19:28:55 +02:00
Terranom674
cb9a4b7d2a Expose WebDAV file metadata to materialize runtime 2026-08-20 19:27:48 +02:00
Terranom674
388351c356 Bump version to 0.9.6.8.24 2026-08-20 19:20:32 +02:00
Terranom674
ea71b54c94 Avoid HTTP self-request for WebDAV derivative generation 2026-08-20 19:20:06 +02:00
Terranom674
99edfc84d8 Bump version to 0.9.6.8.23 2026-08-20 19:06:30 +02:00
Terranom674
b224cd40f6 Fix Nextcloud image dimension XML parsing 2026-08-20 19:05:54 +02:00
Terranom674
a437f5d205 Bump version to 0.9.6.8.22 2026-08-20 18:23:34 +02:00
Terranom674
f2b2845387 Remove obsolete WebDAV precache and derivative prebuild 2026-08-20 18:23:11 +02:00
Terranom674
b54cb039fb release: bump to 0.9.6.8.21 2026-08-20 18:11:58 +02:00
Terranom674
419fe56cde webdav: stop downloading originals for dimension metadata 2026-08-20 18:11:34 +02:00
Terranom674
8cb4abf81d webdav: read image dimensions from Nextcloud metadata 2026-08-20 18:11:07 +02:00
Terranom674
ae70e64c70 release: bump plugin version to 0.9.6.8.20 2026-08-20 18:00:08 +02:00
Terranom674
e5557eba24 webdav: sync original dimensions after piwigo import 2026-08-20 17:59:45 +02:00
Terranom674
5b3eabcb55 webdav: sync original dimensions into piwigo 2026-08-20 17:58:57 +02:00
Terranom674
d56aff39a2 webdav: persist original image dimensions 2026-08-20 17:58:43 +02:00
Terranom674
01243ea022 docs: refresh current development status 2026-08-20 17:42:06 +02:00
Terranom674
88f2949903 docs: update repository information for 0.9.6.8.19 2026-08-20 17:41:35 +02:00
Terranom674
136a6c235e Fix admin dashboard album tile for single album 2026-08-20 17:37:50 +02:00
Terranom674
fb211693ff Preserve connector top-level album access without name collision 2026-08-20 17:32:13 +02:00
Terranom674
b0885a0554 Fix duplicate private album access handler 2026-08-20 17:31:52 +02:00
Terranom674
bc798c5d65 Fix access for flattened WebDAV top-level albums 2026-08-20 17:23:26 +02:00
Terranom674
f958eca176 Bump version to 0.9.6.8.15 2026-08-20 16:54:35 +02:00
Terranom674
93af719143 Flatten empty WebDAV root into gallery root 2026-08-20 16:54:06 +02:00
Terranom674
11183f2a41 Bump version to 0.9.6.8.14 2026-08-20 16:41:27 +02:00
Terranom674
26bb454ba4 Preserve empty WebDAV root path in sync 2026-08-20 16:41:10 +02:00
Terranom674
a195421fed Allow empty WebDAV root path in reconcile 2026-08-20 16:40:47 +02:00
Terranom674
ec55220124 Bump plugin version to 0.9.6.8.13 2026-08-20 16:15:08 +02:00
Terranom674
f5d3b97339 Use absolute runtime path for manual WebDAV sync 2026-08-20 16:14:41 +02:00
Terranom674
a5b4a2329e Bump version to 0.9.6.8.12 2026-08-20 16:13:13 +02:00
Terranom674
97340a517b Run manual WebDAV sync directly from plugin 2026-08-20 16:12:52 +02:00
Terranom674
238c06be33 Bump Bratonien Tools to 0.9.6.8.11 2026-08-20 16:09:54 +02:00
Terranom674
792e8eb7d2 Add manual WebDAV sync button to connector status 2026-08-20 16:09:15 +02:00
Terranom674
d489bd7979 Bump version to 0.9.6.8.10 2026-08-20 16:05:58 +02:00
Terranom674
e8a5f5f0f7 Add NC connector run-now button to admin status 2026-08-20 16:05:36 +02:00
Terranom674
ecc7665ed3 Register manual NC connector run 2026-08-20 16:05:04 +02:00
Terranom674
0d3764f043 Add manual NC connector run and clean status text 2026-08-20 16:04:45 +02:00
Terranom674
81b3b31689 Bump plugin version to 0.9.6.8.9 2026-08-20 15:59:14 +02:00
Terranom674
dcd1b9fb66 Make WebDAV shadow directories writable for runtime materialization 2026-08-20 15:58:57 +02:00
Terranom674
72a89a4636 Bump version to 0.9.6.8.8 2026-08-20 15:48:58 +02:00
Terranom674
2e03cc50d8 Add WebDAV derivative gate diagnostics 2026-08-20 15:48:29 +02:00
Terranom674
d2f171718e Bump version to 0.9.6.8.7 2026-08-20 15:34:33 +02:00
Terranom674
403b1df43f Simplify WebDAV gate to materialize real Piwigo source 2026-08-20 15:34:11 +02:00
Terranom674
38e6ebdb19 Switch WebDAV images to on-demand Piwigo materialization 2026-08-20 15:17:05 +02:00
Terranom674
474acf1de3 Use materialization resolver for WebDAV originals 2026-08-20 15:16:51 +02:00
Terranom674
fe3c7fede2 Add production WebDAV derivative materialization gate 2026-08-20 15:16:30 +02:00
Terranom674
ec801b88d2 Add production WebDAV materialization runtime 2026-08-20 15:15:40 +02:00
Terranom674
9eb38d7dcf Bump version to 0.9.6.8.5 2026-08-20 14:48:15 +02:00
Terranom674
1a7e9a13ee Add parallel WebDAV download stress test 2026-08-20 14:47:51 +02:00
Terranom674
e1d2598e22 Bump version to 0.9.6.8.4 2026-08-20 14:40:46 +02:00
Terranom674
29063b296a Add resource metrics to WebDAV derivative test 2026-08-20 14:40:13 +02:00
Terranom674
210c180e49 Bump test version to 0.9.6.8.3 2026-08-20 14:31:54 +02:00
Terranom674
7ef6c24db4 Move WebDAV derivative test source to Piwigo upload area 2026-08-20 14:31:28 +02:00
Terranom674
1e57e6d300 Bump test patch version to 0.9.6.8.2 2026-08-20 14:25:29 +02:00
Terranom674
57eec81801 Use Piwigo derivative dir for parallel test workspace 2026-08-20 14:24:53 +02:00
Terranom674
0ea01d9a26 Bump plugin version to 0.9.6.8.1 2026-08-20 14:22:36 +02:00
Terranom674
b901b4e887 Bump plugin version for parallel derivative test 2026-08-20 14:20:17 +02:00
Terranom674
bc5a94dc10 Make WebDAV derivative test fully parallel 2026-08-20 14:19:43 +02:00
Terranom674
34a473fa07 Bump plugin version for WebDAV derivative root test 2026-08-20 14:08:13 +02:00
Terranom674
7fc038e107 Fix WebDAV root matching in derivative test path 2026-08-20 14:07:41 +02:00
Terranom674
3b2a8306ad Bump version for WebDAV diagnostic endpoint 2026-08-20 14:03:38 +02:00
Terranom674
dae5f9a610 Add isolated WebDAV derivative path diagnostics 2026-08-20 14:03:04 +02:00
Terranom674
929f18a5f5 Fix duplicate private album access function and bump version 2026-08-20 13:49:33 +02:00
Terranom674
0c45576e2f Bump plugin version for WebDAV derivative test 2026-08-20 13:46:25 +02:00
Terranom674
ee01842cb4 Add parallel WebDAV derivative materialization test endpoint 2026-08-20 13:41:17 +02:00
Terranom674
a872f9238c Merge pull request #52 from Terranom674/fix/0963-clean-webdav-orphans
0.9.6.3: verwaiste WebDAV-Bilder einmalig bereinigen
2026-08-20 11:25:39 +02:00
Terranom674
fdcf045f39 0.9.6.3: WebDAV-Pfadfilter exakt begrenzen 2026-08-20 11:24:56 +02:00
Terranom674
7831a9513f Version 0.9.6.3 2026-08-20 11:24:19 +02:00
Terranom674
fed089db4c 0.9.6.3: Altlasten-Reparatur einmalig vor WebDAV-Sync ausführen 2026-08-20 11:23:54 +02:00
Terranom674
77a295d74f 0.9.6.3: einmalige Reparatur verwaister WebDAV-Bilder 2026-08-20 11:23:33 +02:00
Terranom674
ecabf2da2f Merge PR #51: WebDAV-only connector cleanup
Remove obsolete legacy/local connector paths while preserving the WebDAV connector, shadow tree, placeholder mapping and Piwigo sync bridge.
2026-08-20 11:08:55 +02:00
23 changed files with 2736 additions and 133 deletions

View File

@@ -2,61 +2,168 @@
## Version ## Version
**Bratonien Tools 0.9.6.2** **Bratonien Tools 0.9.7.1.7**
Verbindliches Versionsschema: **0.x.x.x.x**.
Zielplattform der produktiven NC-Connector-Synchronisierung ist derzeit **Piwigo 16.4.0**.
## NC Connector ## NC Connector
Der NC Connector besitzt nur noch einen produktiven Verbindungsweg: **Nextcloud per WebDAV**. Der produktive Verbindungsweg ist **Nextcloud per WebDAV**.
### Verbindungsmodell ### Verbindungsmodell
- `adapter=remote` - `adapter=remote`
- `source_mode=webdav-placeholder` - `source_mode=webdav-placeholder`
- Nextcloud-Adresse, Benutzer und Passwort/App-Passwort werden pro Verbindung verschlüsselt gespeichert. - Nextcloud-Adresse, Benutzer und Passwort/App-Passwort werden pro Verbindung verschlüsselt gespeichert.
- Piwigo-API und Benutzername/Passwort-Fallback werden ebenfalls pro Verbindung gespeichert. - Piwigo-API und Benutzername/Passwort-Fallback werden pro Verbindung gespeichert.
- Der Assistent zeigt ausschließlich die Verzeichnisse, auf die der angemeldete Nextcloud-Benutzer per WebDAV zugreifen kann. - Der Assistent zeigt ausschließlich per WebDAV sichtbare Verzeichnisse.
- Mehrere Verzeichnisse können pro Verbindung ausgewählt werden. - Ein leerer `webdav_path` ist ein gültiger Benutzer-Root.
### Root-Verhalten
Bei `webdav_path=""` wird die technische Nextcloud-Benutzerebene nicht als Piwigo-Album angelegt.
Direkte Unterordner des Nextcloud-Benutzerroots werden als Top-Level-Piwigo-Alben gespiegelt. Direkt im Root liegende Einzelbilder verbleiben auf Root-Ebene des verbindungseigenen Shadow Trees und werden durch die Orphan-Logik verarbeitet.
Es gibt keine fest verdrahteten Album-Namen.
### Laufzeit ### Laufzeit
Der gemeinsame Runner verarbeitet ausschließlich WebDAV-Verbindungen: Der gemeinsame Runner verarbeitet WebDAV-Verbindungen in dieser Reihenfolge:
1. `runtime/reconcile-webdav.php` 1. `runtime/reconcile-webdav.php`
2. `runtime/cleanup-webdav-piwigo.php` 2. `runtime/repair-webdav-orphans.php`
3. `runtime/sync-webdav.sh` für jede vorhandene WebDAV-Konfiguration 3. `runtime/cleanup-webdav-piwigo.php`
4. `runtime/sync-webdav.sh` für jede vorhandene WebDAV-Konfiguration
Die verbindungsspezifischen Runtime-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`. Zustandsdaten liegen unter `/var/lib/bratonien-tools/nc-connector/connection-ID`. Verbindungsspezifische Runtime-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`. Zustandsdaten liegen unter `/var/lib/bratonien-tools/nc-connector/connection-ID`.
### Shadow Tree und Piwigo State-Verzeichnisse sind für die `www-data`-Gruppe traversierbar/lesbar. `webdav-map.json` wird auch nach atomaren Rewrites mit passenden Runtime-Rechten gehalten.
`runtime/lib/build_webdav_placeholder_source.py` bildet die ausgewählten Nextcloud-Inhalte als lokale Platzhalterquelle ab. `runtime/lib/shadow_tree.py` erzeugt daraus die Dateisystemstruktur, die Piwigo als physische Site sieht. ### Platzhalterquelle, Metadaten und Shadow Tree
Jede WebDAV-Verbindung besitzt ihre eigene Piwigo-Site. `runtime/lib/piwigo-sync.php` übergibt genau diese Site an `bratonien.nc.syncProductive` und `bratonien.nc.syncOrphans`. Es gibt keinen zusätzlichen Sync auf Site 1. `runtime/lib/build_webdav_placeholder_source.py` bildet die Nextcloud-Inhalte als lokale Platzhalterquelle ab. Für Bilder existieren physische 1×1-Platzhalterdateien; Originale werden nicht dauerhaft heruntergeladen.
Der Shadow Tree dient ausschließlich dazu, Piwigo die Alben und Bilder als Dateisystemstruktur bereitzustellen. Albumlogik, Dateisynchronisierung und die daraus folgenden Piwigo-Datensätze bleiben Aufgabe von Piwigo. Der WebDAV-Scan übernimmt `fileid`, `width`, `height`, MIME-Typ, Größe, ETag und WebDAV-Pfad in `webdav-map.json`. `runtime/lib/sync-webdav-metadata.php` überträgt die Originalmaße nach dem normalen Piwigo-Dateisync in `piwigo_images`.
Die physische Source-Struktur wird für den Runtime-Swap mit `root:www-data` und schreibbaren Gruppenrechten angelegt bzw. nach dem Sync gesetzt. Damit kann der Webserver die Platzhalterquelle während der On-Demand-Erzeugung temporär austauschen und anschließend wiederherstellen.
`runtime/lib/shadow_tree.py` erzeugt atomar die Piwigo-kompatible Dateisystemstruktur. Jede WebDAV-Verbindung besitzt ihre eigene Piwigo-Site.
### Piwigo-Synchronisierung
`runtime/lib/piwigo-sync.php` bevorzugt `bratonien.nc.syncProductive` für Piwigo 16.4.0. Wenn keine Piwigo-API eingerichtet ist, wird der gespeicherte Administrator-/Webmaster-Fallback verwendet und Piwigos nativer `site_update` ausgeführt.
Danach läuft `bratonien.nc.syncOrphans` für die konkrete WebDAV-Site.
Albumdatensätze, Bilddatensätze, Bild-Kategorie-Verknüpfungen und Derivate bleiben Aufgabe von Piwigo.
### On-Demand-Materialisierung und Derivaterzeugung
`include/webdav_materialize_runtime.inc.php` löst aus dem Piwigo-Bildpfad die konkrete Verbindung, den Nextcloud-Root, den relativen WebDAV-Pfad und die Mapping-Metadaten auf.
Für ein noch nicht vorhandenes Derivat läuft `webdav-derivative.php`:
1. Zugriffsprüfung für das Piwigo-Bild;
2. Auswahl einer temporären Nextcloud-Quelle;
3. temporäres Bereitstellen dieser Quelle am exakten von Piwigo erwarteten Quellpfad;
4. Erzeugung des normalen Piwigo-Derivats mit Piwigos eigener `pwg_image`-/Derivative-Logik im selben PHP-Prozess;
5. Wiederherstellung des Shadow-/Platzhalterzustands;
6. Auslieferung des Piwigo-Derivats.
Ein interner HTTP-Self-Request auf `i.php` wird nicht verwendet.
Für `custom:s9999x250` und `standard:square` wird bevorzugt der authentifizierte Nextcloud-Endpunkt **`/index.php/core/preview`** anhand der `fileid` verwendet. Die Preview wird mit ungefähr doppelter benötigter Piwigo-Auflösung und unter Berücksichtigung des Original-Seitenverhältnisses angefordert. Varianten mit Bildrotation werden in dieser Stufe nicht über Preview erzeugt.
Ist keine geeignete Preview planbar oder schlägt der Preview-Abruf fehl, erfolgt der vollständige WebDAV-Originaldownload. Große/originale Anforderungen verwenden weiterhin die Originalquelle.
Die temporäre Preview bzw. das temporäre Original wird nach der Derivaterzeugung entfernt. Das fertige Derivat verbleibt ausschließlich in Piwigos normalem `_data/i`-Cache. Es gibt keinen parallelen permanenten Connector-Derivat-Cache.
### Piwigo-Lazyload
Piwigos nativer Thumbnail-Loader ruft nicht gecachte Derivate mit `ajaxload=true` per AJAX ab und erwartet JSON mit einer fertigen `url`.
`webdav-derivative.php` unterstützt dieses Protokoll für:
- bereits vorhandene Zielderivate;
- `target_created_while_waiting`;
- frisch erzeugte Derivate.
Normale direkte Bildanforderungen liefern weiterhin das Bild selbst. Dadurch kann Piwigos eigener Placeholder nach erfolgreicher Erzeugung ohne Seiten-Reload durch das echte Thumbnail ersetzt werden.
### Bilddetail-Navigation
Die eigene Bildnavigation liegt in `js/picture_navigation.js` und `css/picture_navigation.css`. Der aktuelle Stand enthält die Behandlung des sichtbaren Zwischenzustands für WebDAV-Nachbar-Thumbnails, damit während des Nachladens kein Browser-Symbol für ein kaputtes Bild stehen bleiben soll.
### Orphan-Logik
Direkt im WebDAV-Site-Root liegende Bilder werden nicht künstlich in ein Album gezwungen. Sie werden durch `bratonien.nc.syncOrphans` als Piwigo-Orphans synchronisiert.
`runtime/repair-webdav-orphans.php` läuft vor den verbindungsspezifischen Syncs und repariert historische WebDAV-Orphan-Zustände.
### Private Top-Level-Alben
WebDAV-Unterordner können echte private Top-Level-Piwigo-Alben sein. Beim Connector-Fallback-Sync sorgt `bratonien_tools_preserve_connector_top_level_access()` dafür, dass der verwendete Piwigo-Administrator den Zugriff behält. Nach Änderungen wird der Benutzer-Cache invalidiert.
Die allgemeine Funktion `bratonien_tools_preserve_private_album_access()` in `include/album_shares.inc.php` bleibt davon getrennt.
### Administrations-Dashboard
Für Piwigo 16.4.0 wird ausschließlich die Standardtemplate-Bedingung der Album-Kachel von `NB_ALBUMS > 1` auf `NB_ALBUMS > 0` angepasst. Die Albumzahl selbst stammt unverändert aus Piwigo.
### Bildauslieferung ### Bildauslieferung
Die Dateien im Shadow Tree sind Platzhalter. `include/webdav_image_runtime.inc.php` erkennt die WebDAV-Zuordnung und ersetzt die Bildquelle bei der Auslieferung durch den zugehörigen Nextcloud-Inhalt. `webdav-image.php` streamt die Bilddaten serverseitig, ohne Nextcloud-Zugangsdaten an den Browser weiterzugeben. Fehlende Piwigo-Derivate werden durch den On-Demand-Gate erzeugt. Bereits vorhandene Derivate werden direkt aus Piwigos eigenem Derivat-Cache ausgeliefert.
### Löschen Für direkte Originalanforderungen existiert weiterhin die serverseitige WebDAV-Originalauslieferung. Nextcloud-Zugangsdaten werden dem Browser nicht offengelegt.
Eine WebDAV-Verbindung kann unabhängig von ihrer Reihenfolge gelöscht werden. Beim Löschen werden die zugehörige Piwigo-Site und die zugehörigen Piwigo-Datensätze entfernt. Die Originaldateien in Nextcloud bleiben unangetastet. Verwaiste WebDAV-Runtime- und Piwigo-Daten werden vom gemeinsamen Lauf bereinigt.
### Diagnose ### Diagnose
Der Status wird pro Verbindung geführt. Fehler enthalten Zeitpunkt, betroffene Verbindung, Prozesszustand und technische Details. Die Administration zeigt keine lokalen Datenbank-, Storage- oder Mount-Einstellungen mehr an. Der WebDAV-Derivative-Gate schreibt Request-bezogene `[BRAT-WD ...]`-Diagnoseeinträge. Relevante Ereignisse sind unter anderem `preview_start`, `preview_failed`, `download_done`, `swap_begin`, `swapped`, `generate_start`, `generate_done`, `restored`, `target_created_while_waiting` und `serve`.
## Entfernte Connector-Komponenten ## Self-Updater
Nicht mehr Bestandteil des Plugins sind lokale Nextcloud-Datenbankanbindungen, PostgreSQL-Reader, Source-/Activity-Views, Storage-Mappings, Host-Mounts, lokale Reconcile-/Sync-Skripte, Cutover-/Migrationswerkzeuge und der frühere Simulations-Webservice `bratonien.nc.sync`. `include/self_update.inc.php` liest den Zielstand von GitHub, bindet ihn an einen konkreten Commit, prüft die `main.inc.php` per SHA-256, erstellt vor dem Austausch ein Backup und aktualisiert nur auf eine **höhere** Plugin-Version.
Das verbindliche Versionsschema ist **0.x.x.x.x**.
## Aktuell relevante Dateien
- `main.inc.php` Plugin-Hooks und Version
- `include/self_update.inc.php` integrierter Self-Updater
- `include/webdav_materialize_runtime.inc.php` WebDAV-Quellauflösung und Gate-URL-Erzeugung
- `webdav-derivative.php` On-Demand-Gate, Preview/Original-Fallback, Piwigo-Derivaterzeugung und AJAX-Lazyload-Antwort
- `webdav-image.php` direkte WebDAV-Originalauslieferung
- `include/nc_productive_ws.inc.php` direkter Piwigo-Core-Sync für 16.4.0
- `include/nc_orphan_ws.inc.php` Root-Orphan-Synchronisierung
- `runtime/reconcile-webdav.php` Runtime-Reconcile und State-Berechtigungen
- `runtime/repair-webdav-orphans.php` Orphan-Reparatur
- `runtime/cleanup-webdav-piwigo.php` Bereinigung verwaister Piwigo-Sites
- `runtime/sync-webdav.sh` verbindungsspezifischer Sync und Runtime-/Source-Berechtigungen
- `runtime/run-all.sh` gemeinsamer Runner
- `runtime/lib/build_webdav_placeholder_source.py` WebDAV-Scan, Mapping und Platzhalterquelle
- `runtime/lib/sync-webdav-metadata.php` Übernahme der Originalmaße nach Piwigo
- `runtime/lib/shadow_tree.py` atomarer Shadow Tree
- `runtime/lib/piwigo-sync.php` Piwigo-API/Fallback-Sync
- `js/picture_navigation.js` Bilddetail-Navigation und Thumbnail-Zwischenzustand
- `css/picture_navigation.css` Bilddetail-Navigationsdarstellung
## Nicht mehr das produktive WebDAV-Modell
Nicht mehr maßgeblich sind ein separater permanenter WebDAV-Derivatbestand oder ein interner HTTP-Aufruf von `webdav-derivative.php` zurück auf Piwigos `i.php`.
Maßgeblich ist:
**physischer Platzhalter + temporäre Nextcloud-Preview bzw. Original-Fallback + Piwigos Bildverarbeitung im selben PHP-Prozess + normales Piwigo-Derivat + Wiederherstellung des Platzhalters**.
## Prüfpflicht vor Merge ## Prüfpflicht vor Merge
Jede Änderung wird vor dem Merge mindestens mit folgenden GitHub-Actions-Prüfungen validiert: Die GitHub-Actions-Konfiguration prüft mindestens:
- PHP-Syntax für 8.2, 8.3, 8.4 und 8.5 - PHP-Syntax für 8.2, 8.3, 8.4 und 8.5
- Prüfung auf doppelte globale `bratonien_tools_*`-Funktionen - doppelte globale `bratonien_tools_*`-Funktionen
- JavaScript-Syntax - JavaScript-Syntax
- Python-Syntax/Compile - Python-Syntax/Compile
- Shell-Syntax mit `bash -n` - Shell-Syntax mit `bash -n`

123
README.md
View File

@@ -2,7 +2,7 @@
Modulares Piwigo-Plugin für Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo. Modulares Piwigo-Plugin für Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo.
Aktuelle Plugin-Version: **0.9.6.2** Aktuelle Plugin-Version: **0.9.7.1.7**
## Grundprinzip ## Grundprinzip
@@ -12,18 +12,43 @@ Für die Administration gilt: Der normale Nutzer sieht Aufgaben, Entscheidungen
## NC Connector ## NC Connector
Der NC Connector bindet ausdrücklich ausgewählte Nextcloud-Inhalte ausschließlich per WebDAV an Piwigo an. Der NC Connector bindet ausdrücklich ausgewählte Nextcloud-Inhalte per WebDAV an Piwigo an. Nextcloud bleibt dabei die Quelle der Originaldateien; Piwigo verwaltet Alben, Bilder und seine eigenen Derivate.
### Datenmodell ### Datenmodell
- Nextcloud bleibt die Quelle der Originaldateien. - Nextcloud bleibt die verbindliche Quelle der Originaldateien.
- Originalbilder werden nicht dauerhaft in eine zweite Bibliothek unter Piwigo kopiert. - Originalbilder werden nicht dauerhaft als zweite vollständige Bibliothek unter Piwigo gespeichert.
- Ein Shadow Tree erzeugt die für Piwigo benötigte Ordnerstruktur. - Eine lokale Platzhalterquelle enthält physische 1×1-Dateien, damit Piwigo die Bilder regulär als Dateisysteminhalte erkennen kann.
- Ein Shadow Tree bildet daraus die von Piwigo erwartete Galerie-Ordnerstruktur.
- Ordner werden als physische Piwigo-Alben synchronisiert. - Ordner werden als physische Piwigo-Alben synchronisiert.
- Root-Dateien können als Piwigo-Orphans registriert werden. - Direkt im ausgewählten Nextcloud-Root liegende Einzelbilder werden über die vorhandene Orphan-Logik behandelt.
- Neu importierte physische Connector-Alben werden privat angelegt. - Neu importierte physische Connector-Alben werden privat angelegt.
- Entfernte Quellen verschwinden aus Shadow Tree und Piwigo; die Nextcloud-Originale bleiben erhalten. - Entfernte Quellen verschwinden aus Shadow Tree und Piwigo; die Nextcloud-Originale bleiben erhalten.
### Nextcloud-Root ohne künstliches Benutzeralbum
Ein leerer `webdav_path` ist ein gültiger Root und bedeutet den Dateibereich des authentifizierten Nextcloud-Benutzers.
Dabei wird der Nextcloud-Benutzername nicht als künstliche Album-Ebene angelegt. Beispiel:
```text
Nextcloud
/
├── 400 Auswahl/
├── Zweites Album/
├── einzelbild-1.jpg
└── einzelbild-2.jpg
```
wird in Piwigo logisch als
```text
400 Auswahl
Zweites Album
```
abgebildet. Die direkt im Root liegenden Einzelbilder werden separat durch die Orphan-Synchronisierung erfasst.
### Voraussetzungen ### Voraussetzungen
Benötigt werden nur: Benötigt werden nur:
@@ -35,30 +60,57 @@ Benötigt werden nur:
Nicht benötigt werden Nextcloud-Datenbankzugriff, `occ`-Adminzugriff, Storage-IDs, Backend-Pfade, zusätzliche Host-Mounts, FUSE, davfs oder rclone. Nicht benötigt werden Nextcloud-Datenbankzugriff, `occ`-Adminzugriff, Storage-IDs, Backend-Pfade, zusätzliche Host-Mounts, FUSE, davfs oder rclone.
### WebDAV-Ablauf ### WebDAV-Scan und Shadow Tree
1. Der Assistent prüft Nextcloud und liest die sichtbaren Verzeichnisse per WebDAV. 1. Der Assistent prüft Nextcloud und liest die sichtbaren Verzeichnisse per WebDAV.
2. Ausgewählte Verzeichnisse werden rekursiv per PROPFIND eingelesen. 2. Ausgewählte Verzeichnisse werden rekursiv per PROPFIND eingelesen.
3. Ordner, Dateiname, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst. 3. Ordner, Dateiname, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag, Originalabmessungen und WebDAV-Pfad werden erfasst.
4. `runtime/lib/build_webdav_placeholder_source.py` erzeugt eine lokale Platzhalterquelle ohne dauerhafte Originalkopie. 4. `runtime/lib/build_webdav_placeholder_source.py` erzeugt eine lokale Platzhalterquelle ohne dauerhafte Originalkopie.
5. `runtime/lib/shadow_tree.py` baut daraus den verbindungseigenen Shadow Tree. 5. `runtime/lib/shadow_tree.py` baut atomar den verbindungseigenen Shadow Tree.
6. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID`. 6. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID`.
7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert. 7. Jede WebDAV-Verbindung wird als eigene physische Piwigo-Site registriert.
8. Piwigos Dateisynchronisierung erhält ausschließlich diese verbindungseigene Site. 8. Piwigos Dateisynchronisierung erhält ausschließlich diese verbindungseigene Site.
9. Die WebDAV-Bildzuordnung ersetzt beim Ausliefern Platzhalter-URLs durch die echte Nextcloud-Bildquelle. 9. `runtime/lib/sync-webdav-metadata.php` übernimmt die von Nextcloud gelieferten Originalmaße in Piwigos Bilddatensätze.
10. `webdav-image.php` streamt benötigte Bilder serverseitig; Zugangsdaten werden dem Browser nicht offengelegt.
State-Verzeichnisse und Mapping-Dateien werden so berechtigt, dass der Webserver die Laufzeitdaten lesen kann. Die physische Platzhalterquelle wird mit der für den temporären Source-Swap nötigen `www-data`-Gruppenberechtigung erzeugt bzw. nach dem Sync gesetzt.
### On-Demand-Bildauslieferung und Piwigo-Derivate
`include/webdav_materialize_runtime.inc.php` erkennt anhand des Piwigo-Bildpfads die zugehörige WebDAV-Verbindung, den Nextcloud-Root und den relativen WebDAV-Pfad. Aus dem Mapping stehen zusätzlich `fileid`, Originalbreite/-höhe, MIME-Typ, Dateigröße und ETag zur Verfügung. Der Hook `get_derivative_url` leitet fehlende WebDAV-Derivate an `webdav-derivative.php` um.
Wenn ein benötigtes Piwigo-Derivat noch nicht existiert:
1. `webdav-derivative.php` prüft den Piwigo-Zugriff auf das Bild.
2. Für `custom:s9999x250` und `standard:square` wird bevorzugt eine passende authentifizierte Nextcloud-Preview anhand der `fileid` über `/index.php/core/preview` geladen. Die Preview wird mit ungefähr doppelter benötigter Piwigo-Auflösung und erhaltenem Seitenverhältnis angefordert.
3. Ist keine passende Preview planbar oder schlägt der Preview-Abruf fehl, wird automatisch das vollständige Original per WebDAV geladen. Große bzw. originale Anforderungen verwenden weiterhin die Originalquelle.
4. Die temporäre Bildquelle ersetzt für die Dauer der Erzeugung die physische 1×1-Platzhalterdatei bzw. den Shadow-Link am von Piwigo erwarteten Quellpfad.
5. Piwigos eigene `pwg_image`-/Derivative-Logik erzeugt das normale Piwigo-Derivat direkt im selben PHP-Prozess. Ein interner HTTP-Self-Request auf `i.php` wird nicht verwendet.
6. Danach wird die temporäre Bildquelle entfernt und der Platzhalter-/Shadow-Zustand wiederhergestellt.
7. Das fertige Derivat wird ausgeliefert und künftig normal aus Piwigos `_data/i`-Derivat-Cache verwendet.
Piwigos nativer Thumbnail-Loader verwendet bei nicht gecachten Thumbnails `ajaxload=true` und erwartet eine JSON-Antwort mit der fertigen Derivat-URL. `webdav-derivative.php` unterstützt dieses Protokoll; normale direkte Bildanforderungen werden weiterhin als Bild ausgeliefert. Dadurch kann Piwigos eigener Placeholder nach der Erzeugung ohne Seiten-Reload durch das fertige Thumbnail ersetzt werden.
Damit bleibt Piwigo für Größen, Derivate und Lazy-Loading verantwortlich. Der Connector hält keinen parallelen eigenen vollständigen Derivat-Cache vor.
Für Fälle, in denen Piwigo direkt die Originalquelle benötigt, steht weiterhin die serverseitige WebDAV-Auslieferung zur Verfügung. Nextcloud-Zugangsdaten werden nicht an den Browser weitergegeben.
### Piwigo-Synchronisierung ### Piwigo-Synchronisierung
Der Connector verwendet die vorhandene Piwigo-Dateisynchronisierung für die verbindungseigene WebDAV-Site. Der Shadow Tree ist die Dateisystemdarstellung, die Piwigo für Alben und Bilder benötigt. Der Connector verwendet die vorhandene Piwigo-Dateisynchronisierung für die verbindungseigene WebDAV-Site. Der Shadow Tree ist ausschließlich die Dateisystemdarstellung, die Piwigo für Alben und Bilder benötigt.
`runtime/lib/piwigo-sync.php` ruft dafür `bratonien.nc.syncProductive` auf. Die Methode führt den Piwigo-Core-Sync für die konkrete Site aus. `bratonien.nc.syncOrphans` arbeitet ebenfalls nur auf der übergebenen WebDAV-Site. `runtime/lib/piwigo-sync.php` bevorzugt die Webservice-Methode `bratonien.nc.syncProductive`. Ist keine Piwigo-API für die Verbindung eingerichtet, wird der vorhandene Administrator-/Webmaster-Benutzername/Passwort-Fallback verwendet und Piwigos nativer `site_update` ausgeführt.
Nach der regulären Dateisynchronisierung läuft `bratonien.nc.syncOrphans` für direkt im Site-Root liegende Einzelbilder.
Die produktive Synchronisierung ist aktuell auf **Piwigo 16.4.0** abgestimmt. Die produktive Synchronisierung ist aktuell auf **Piwigo 16.4.0** abgestimmt.
### Piwigo-Zugang ### Private Top-Level-Alben
Der Zugang wird pro Verbindung gespeichert. Bevorzugt wird eine Piwigo-API. Wird keine API eingerichtet, kann ein Administrator-/Webmaster-Benutzer mit Passwort als Fallback verwendet werden. Durch das Überspringen der technischen Nextcloud-Benutzerebene können WebDAV-Unterordner zu echten Top-Level-Piwigo-Alben werden. Für den Connector-Sync wird deshalb der Zugriff des verwendeten Piwigo-Administrators auf neu erzeugte private Top-Level-Alben erhalten und der Benutzer-Cache anschließend invalidiert.
### Administrations-Dashboard
Piwigo 16.4.0 blendet seine Album-Statistik standardmäßig bei exakt einem Album aus (`NB_ALBUMS > 1`). Bratonien Tools passt ausschließlich diese Dashboard-Anzeige auf `NB_ALBUMS > 0` an. Dadurch wird auch ein einzelnes korrekt angelegtes Album in der Verwaltungsübersicht angezeigt.
### Statusanzeige ### Statusanzeige
@@ -78,11 +130,14 @@ Aktive WebDAV-Verbindungen werden über den gemeinsamen Runner verarbeitet:
Verbindungsspezifische Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`, State-Daten unter `/var/lib/bratonien-tools/nc-connector/connection-ID`. Verbindungsspezifische Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`, State-Daten unter `/var/lib/bratonien-tools/nc-connector/connection-ID`.
Der Lauf besteht aus: Der gemeinsame Lauf besteht aktuell aus:
1. WebDAV-Verbindungen reconciliieren; 1. `runtime/reconcile-webdav.php` gespeicherte Verbindungen in Runtime-Konfigurationen überführen;
2. verwaiste WebDAV-Piwigo-Inhalte bereinigen; 2. `runtime/repair-webdav-orphans.php` vorhandene WebDAV-Orphan-Zustände reparieren;
3. jede vorhandene WebDAV-Verbindung synchronisieren. 3. `runtime/cleanup-webdav-piwigo.php` verwaiste WebDAV-Piwigo-Daten bereinigen;
4. `runtime/sync-webdav.sh` jede vorhandene WebDAV-Verbindung synchronisieren.
Der verbindungsspezifische Lauf erzeugt Platzhalterquelle und Shadow Tree, setzt die Laufzeitberechtigungen, führt Piwigos Sync und Orphan-Sync aus und überträgt anschließend die Originalabmessungen aus dem WebDAV-Mapping in Piwigo. Eine vollständige Vorab-Erzeugung von Connector-Derivaten findet nicht statt; fehlende Derivate werden On-Demand erzeugt.
## Bildcache ## Bildcache
@@ -125,34 +180,38 @@ Private Alben können über eigene Freigabelinks geteilt werden, optional mit Pa
## Erweiterte Bildnavigation ## Erweiterte Bildnavigation
Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges Bild, nächstes Bild, Rückkehr zur Übersicht und PhotoSwipe/Vollbild. Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges Bild, nächstes Bild, Rückkehr zur Übersicht und PhotoSwipe/Vollbild. Für WebDAV-Nachbar-Thumbnails wird der sichtbare Ladezustand so behandelt, dass während eines noch laufenden Bildabrufs kein Browser-Symbol für ein kaputtes Bild stehen bleiben soll.
## Selbstaktualisierung ## Selbstaktualisierung
Der integrierte Updater liest den Zielstand aus GitHub, bindet ein Update an einen konkreten Commit, prüft Version und SHA-256, erstellt vor dem Austausch ein Backup und verlangt Webmaster-Rechte sowie die notwendigen Servervoraussetzungen. Der integrierte Updater liest den Zielstand aus GitHub, bindet ein Update an einen konkreten Commit, prüft Version und SHA-256, erstellt vor dem Austausch ein Backup und verlangt Webmaster-Rechte sowie die notwendigen Servervoraussetzungen. Das verbindliche Versionsschema des Plugins ist **0.x.x.x.x**; Updates werden nur auf eine höhere Version angeboten.
## Wichtige Dateien und Verzeichnisse ## Wichtige Dateien und Verzeichnisse
- `main.inc.php` Plugin-Einstieg und Runtime-Hooks - `main.inc.php` Plugin-Einstieg, Hooks, Connector-Sync-Hilfen und Dashboard-Prefilter
- `admin.php` zentraler Admin-Controller - `admin.php` zentraler Admin-Controller
- `include/nc_connector.inc.php` WebDAV-Verbindungsmodell - `include/nc_connector.inc.php` WebDAV-Verbindungsmodell
- `include/nc_connector_wizard.inc.php` Verbindungsassistent - `include/nc_connector_wizard.inc.php` Verbindungsassistent
- `include/nc_connector_wizard_webdav_flow.inc.php` WebDAV-Wizardablauf - `include/nc_connector_wizard_webdav_flow.inc.php` WebDAV-Wizardablauf
- `include/nc_connector_delete_safe.inc.php` Löschen einschließlich WebDAV-Piwigo-Inhalten - `include/nc_connector_delete_safe.inc.php` Löschen einschließlich WebDAV-Piwigo-Inhalten
- `include/nc_productive_ws.inc.php` Piwigo-Core-Dateisync - `include/nc_productive_ws.inc.php` direkter Piwigo-Core-Dateisync für Piwigo 16.4.0
- `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung der konkreten WebDAV-Site - `include/nc_orphan_ws.inc.php` Orphan-Synchronisierung der konkreten WebDAV-Site
- `include/webdav_image_runtime.inc.php` WebDAV-Bildzuordnung und URL-Filter - `include/webdav_materialize_runtime.inc.php` WebDAV-Quellauflösung und On-Demand-Materialisierung einschließlich `fileid` und Originalmaßen
- `webdav-image.php` berechtigungsgeprüfter WebDAV-Bildstream - `include/webdav_image_runtime.inc.php` vorhandene WebDAV-Bildzuordnung/Originalauslieferung
- `runtime/reconcile-webdav.php` WebDAV-Runtime-Reconcile - `webdav-derivative.php` On-Demand-Gate, Nextcloud-Preview/Original-Fallback, Piwigo-AJAX-Loader-Antwort und Piwigo-Derivaterzeugung
- `webdav-image.php` serverseitige WebDAV-Originalauslieferung
- `runtime/reconcile-webdav.php` WebDAV-Runtime-Reconcile, einschließlich gültigem leerem Root und Runtime-Berechtigungen
- `runtime/repair-webdav-orphans.php` Reparatur vorhandener Orphan-Zustände
- `runtime/cleanup-webdav-piwigo.php` Bereinigung verwaister WebDAV-Piwigo-Sites - `runtime/cleanup-webdav-piwigo.php` Bereinigung verwaister WebDAV-Piwigo-Sites
- `runtime/sync-webdav.sh` Ablauf einer WebDAV-Verbindung - `runtime/sync-webdav.sh` Ablauf einer WebDAV-Verbindung einschließlich Mapping-/Source-Berechtigungen
- `runtime/run-all.sh` gemeinsamer WebDAV-Runner - `runtime/run-all.sh` gemeinsamer WebDAV-Runner
- `runtime/lib/build_webdav_placeholder_source.py` rekursiver WebDAV-Scan und Platzhalterquelle - `runtime/lib/build_webdav_placeholder_source.py` rekursiver WebDAV-Scan, Bildmetadaten und physische Platzhalterquelle
- `runtime/lib/shadow_tree.py` atomarer Shadow Tree - `runtime/lib/sync-webdav-metadata.php` Übernahme der Originalabmessungen in Piwigo
- `runtime/lib/shadow_tree.py` atomarer, Piwigo-kompatibler Shadow Tree
- `runtime/lib/piwigo-sync.php` Piwigo-Sync mit API/Fallback - `runtime/lib/piwigo-sync.php` Piwigo-Sync mit API/Fallback
- `main-cache-build.php` allgemeiner Piwigo-Derivat-/Cache-Builder - `main-cache-build.php` allgemeiner Piwigo-Derivat-/Cache-Builder
- `include/self_update.inc.php` Self-Updater - `include/self_update.inc.php` Self-Updater
- `include/album_shares.inc.php` geschützte Albumfreigaben - `include/album_shares.inc.php` geschützte Albumfreigaben und private Albumzugriffe
- `include/public_selection.inc.php` Fotoauswahl - `include/public_selection.inc.php` Fotoauswahl
- `include/batch_titles.inc.php` fortlaufende Titel - `include/batch_titles.inc.php` fortlaufende Titel
- `include/watermark_*.inc.php` Wasserzeichen-Engine - `include/watermark_*.inc.php` Wasserzeichen-Engine
@@ -167,5 +226,7 @@ Der integrierte Updater liest den Zielstand aus GitHub, bindet ein Update an ein
- Wizard-Geheimnisse werden nicht im Browser-Web-Storage persistiert; - Wizard-Geheimnisse werden nicht im Browser-Web-Storage persistiert;
- produktive Piwigo-API ist versionsgebunden; - produktive Piwigo-API ist versionsgebunden;
- Originalbilder werden vom Connector nicht gelöscht; - Originalbilder werden vom Connector nicht gelöscht;
- WebDAV-Originalbilder werden nicht dauerhaft lokal gespeichert; - WebDAV-Originalbilder werden nur bei Bedarf temporär lokal materialisiert;
- kleine Derivate können stattdessen eine temporäre authentifizierte Nextcloud-Preview verwenden;
- temporäre Preview-/Originalquellen werden nach der Piwigo-Derivaterzeugung wieder entfernt;
- Update-Pakete werden an Commit und Hash gebunden. - Update-Pakete werden an Commit und Hash gebunden.

View File

@@ -17,3 +17,12 @@
.bratonien-picture-zone.is-active { .bratonien-picture-zone.is-active {
opacity: 1; opacity: 1;
} }
#theImageAndInfos img[src*="plugins/bratonien_tools/webdav-derivative.php"]:not(#theMainImage):not(.bratonien-webdav-thumbnail-ready),
#theImageAndInfos img.bratonien-webdav-thumbnail-pending {
visibility: hidden;
}
#theImageAndInfos img.bratonien-webdav-thumbnail-ready {
visibility: visible;
}

View File

@@ -4,9 +4,14 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
function bratonien_tools_nc_connector_systemctl_value(array $args) function bratonien_tools_nc_connector_systemctl_run(array $args)
{ {
if (!function_exists('proc_open')) return ''; $result = array('exit'=>-1, 'stdout'=>'', 'stderr'=>'');
if (!function_exists('proc_open'))
{
$result['stderr'] = 'proc_open ist in PHP nicht verfügbar.';
return $result;
}
$command = array_merge(array('/usr/bin/systemctl'), $args); $command = array_merge(array('/usr/bin/systemctl'), $args);
$spec = array( $spec = array(
@@ -16,13 +21,71 @@ function bratonien_tools_nc_connector_systemctl_value(array $args)
); );
$environment = array_merge($_ENV, array('LC_ALL'=>'C','LANG'=>'C')); $environment = array_merge($_ENV, array('LC_ALL'=>'C','LANG'=>'C'));
$process = @proc_open($command, $spec, $pipes, null, $environment); $process = @proc_open($command, $spec, $pipes, null, $environment);
if (!is_resource($process)) return ''; if (!is_resource($process))
$stdout = stream_get_contents($pipes[1]); {
stream_get_contents($pipes[2]); $result['stderr'] = 'systemctl konnte nicht gestartet werden.';
return $result;
}
$result['stdout'] = trim((string)stream_get_contents($pipes[1]));
$result['stderr'] = trim((string)stream_get_contents($pipes[2]));
fclose($pipes[1]); fclose($pipes[1]);
fclose($pipes[2]); fclose($pipes[2]);
$exit = proc_close($process); $result['exit'] = (int)proc_close($process);
return $exit === 0 ? trim((string)$stdout) : ''; return $result;
}
function bratonien_tools_nc_connector_systemctl_value(array $args)
{
$result = bratonien_tools_nc_connector_systemctl_run($args);
return $result['exit'] === 0 ? $result['stdout'] : '';
}
function bratonien_tools_nc_connector_run_now()
{
if (!function_exists('proc_open'))
{
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: proc_open ist in PHP nicht verfügbar.');
}
$runtime_dir = realpath(dirname(__DIR__).'/runtime');
if ($runtime_dir === false || !is_dir($runtime_dir))
{
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: Runtime-Verzeichnis wurde nicht gefunden.');
}
$script = $runtime_dir.'/run-all.sh';
if (!is_file($script) || !is_readable($script))
{
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: runtime/run-all.sh ist nicht lesbar.');
}
$spec = array(
0=>array('file','/dev/null','r'),
1=>array('pipe','w'),
2=>array('pipe','w'),
);
$environment = array_merge($_ENV, array('LC_ALL'=>'C','LANG'=>'C'));
$process = @proc_open(array('/usr/bin/env','bash',$script), $spec, $pipes, $runtime_dir, $environment);
if (!is_resource($process))
{
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: Connector-Prozess konnte nicht geöffnet werden.');
}
$stdout = trim((string)stream_get_contents($pipes[1]));
$stderr = trim((string)stream_get_contents($pipes[2]));
fclose($pipes[1]);
fclose($pipes[2]);
$exit = (int)proc_close($process);
if ($exit !== 0)
{
$detail = $stderr !== '' ? $stderr : $stdout;
if ($detail === '') $detail = 'Exit-Code '.$exit;
throw new RuntimeException('WebDAV-Abgleich fehlgeschlagen: '.$detail);
}
return array('message'=>'WebDAV-Abgleich wurde ausgeführt.');
} }
function bratonien_tools_nc_connector_parse_systemd_time($value) function bratonien_tools_nc_connector_parse_systemd_time($value)
@@ -112,6 +175,15 @@ function bratonien_tools_nc_connector_last_status(array $connections)
return $latest; return $latest;
} }
function bratonien_tools_nc_connector_status_message(array $last)
{
if ((string)($last['state'] ?? '') === 'ok')
{
return 'WebDAV eingelesen und Piwigo synchronisiert';
}
return (string)($last['message'] ?? '');
}
function bratonien_tools_nc_connector_system_status(array $connections = array()) function bratonien_tools_nc_connector_system_status(array $connections = array())
{ {
$timer = 'bratonien-nc-connector.timer'; $timer = 'bratonien-nc-connector.timer';
@@ -143,7 +215,7 @@ function bratonien_tools_nc_connector_system_status(array $connections = array()
'last_run_timestamp'=>(int)$last['timestamp'], 'last_run_timestamp'=>(int)$last['timestamp'],
'last_run_label'=>$last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar', 'last_run_label'=>$last['timestamp'] > 0 ? date('d.m.Y H:i:s', (int)$last['timestamp']) : 'Nicht verfügbar',
'last_run_state'=>(string)$last['state'], 'last_run_state'=>(string)$last['state'],
'last_run_message'=>(string)$last['message'], 'last_run_message'=>bratonien_tools_nc_connector_status_message($last),
'last_run_auth_mode'=>(string)$last['auth_mode'], 'last_run_auth_mode'=>(string)$last['auth_mode'],
'last_run_api_state'=>(string)$last['api_state'], 'last_run_api_state'=>(string)$last['api_state'],
'last_run_api_message'=>(string)$last['api_message'], 'last_run_api_message'=>(string)$last['api_message'],

View File

@@ -50,6 +50,7 @@ function bratonien_tools_get_tools()
'nc_connector_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'), 'nc_connector_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'),
'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'), 'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'),
'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'), 'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
'nc_connector_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_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'), 'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'),
'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'), 'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),

View File

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

View File

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

View File

@@ -216,12 +216,105 @@
resizeTimer = window.setTimeout(updateMap, 60); resizeTimer = window.setTimeout(updateMap, 60);
} }
function isWebdavThumbnail(image) {
if (!image || image.id === 'theMainImage') {
return false;
}
var src = image.getAttribute('src') || '';
var dataSrc = image.getAttribute('data-src') || '';
return src.indexOf('plugins/bratonien_tools/webdav-derivative.php') !== -1 ||
dataSrc.indexOf('plugins/bratonien_tools/webdav-derivative.php') !== -1 ||
image.getAttribute('data-bratonien-webdav-thumbnail') === '1';
}
function setThumbnailPlaceholder(image) {
var parent = image.parentElement;
if (!parent || parent.getAttribute('data-bratonien-placeholder-active') === '1') {
return;
}
parent.setAttribute('data-bratonien-placeholder-active', '1');
parent.setAttribute('data-bratonien-placeholder-background-image', parent.style.backgroundImage || '');
parent.setAttribute('data-bratonien-placeholder-background-repeat', parent.style.backgroundRepeat || '');
parent.setAttribute('data-bratonien-placeholder-background-position', parent.style.backgroundPosition || '');
parent.setAttribute('data-bratonien-placeholder-background-size', parent.style.backgroundSize || '');
parent.style.backgroundImage = 'url("' + absoluteUrl('themes/default/icon/img_small.png') + '")';
parent.style.backgroundRepeat = 'no-repeat';
parent.style.backgroundPosition = 'center';
parent.style.backgroundSize = 'auto';
}
function clearThumbnailPlaceholder(image) {
var parent = image.parentElement;
if (!parent || parent.getAttribute('data-bratonien-placeholder-active') !== '1') {
return;
}
parent.style.backgroundImage = parent.getAttribute('data-bratonien-placeholder-background-image') || '';
parent.style.backgroundRepeat = parent.getAttribute('data-bratonien-placeholder-background-repeat') || '';
parent.style.backgroundPosition = parent.getAttribute('data-bratonien-placeholder-background-position') || '';
parent.style.backgroundSize = parent.getAttribute('data-bratonien-placeholder-background-size') || '';
parent.removeAttribute('data-bratonien-placeholder-active');
parent.removeAttribute('data-bratonien-placeholder-background-image');
parent.removeAttribute('data-bratonien-placeholder-background-repeat');
parent.removeAttribute('data-bratonien-placeholder-background-position');
parent.removeAttribute('data-bratonien-placeholder-background-size');
}
function markThumbnailPending(image) {
image.setAttribute('data-bratonien-webdav-thumbnail', '1');
image.classList.remove('bratonien-webdav-thumbnail-ready');
image.classList.add('bratonien-webdav-thumbnail-pending');
setThumbnailPlaceholder(image);
}
function markThumbnailReady(image) {
image.classList.remove('bratonien-webdav-thumbnail-pending');
image.classList.add('bratonien-webdav-thumbnail-ready');
clearThumbnailPlaceholder(image);
}
function protectWebdavThumbnail(image) {
if (!isWebdavThumbnail(image)) {
return;
}
if (image.getAttribute('data-bratonien-thumbnail-bound') !== '1') {
image.setAttribute('data-bratonien-thumbnail-bound', '1');
image.addEventListener('load', function () {
if (image.naturalWidth > 0 && image.naturalHeight > 0) {
markThumbnailReady(image);
}
});
image.addEventListener('error', function () {
markThumbnailPending(image);
});
}
if (image.complete && image.naturalWidth > 0 && image.naturalHeight > 0) {
markThumbnailReady(image);
} else {
markThumbnailPending(image);
}
}
function protectWebdavThumbnails(root) {
var scope = root || document;
var images = scope.querySelectorAll ? scope.querySelectorAll('img') : [];
for (var i = 0; i < images.length; i += 1) {
protectWebdavThumbnail(images[i]);
}
}
$(function () { $(function () {
var image = document.getElementById('theMainImage'); var image = document.getElementById('theMainImage');
if (!image) { if (!image) {
return; return;
} }
protectWebdavThumbnails(document.getElementById('theImageAndInfos') || document);
if (image.complete) { if (image.complete) {
updateMap(); updateMap();
} else { } else {
@@ -241,6 +334,35 @@
attributes: true, attributes: true,
attributeFilter: ['src', 'srcset', 'usemap', 'class', 'style'] attributeFilter: ['src', 'srcset', 'usemap', 'class', 'style']
}); });
var thumbnailRoot = document.getElementById('theImageAndInfos');
if (thumbnailRoot) {
var thumbnailObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type === 'attributes' && mutation.target.tagName === 'IMG') {
protectWebdavThumbnail(mutation.target);
return;
}
for (var i = 0; i < mutation.addedNodes.length; i += 1) {
var node = mutation.addedNodes[i];
if (node.nodeType !== 1) {
continue;
}
if (node.tagName === 'IMG') {
protectWebdavThumbnail(node);
}
protectWebdavThumbnails(node);
}
});
});
thumbnailObserver.observe(thumbnailRoot, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src', 'data-src']
});
}
} }
}); });
})(jQuery); })(jQuery);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ function decrypt_webdav_credentials($blob, $hexKey)
$iv = base64_decode((string)($payload['iv'] ?? ''), true); $iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true); $tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true); $cipher = base64_decode((string)($payload['data'] ?? ''), true);
if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) fail_webdav_reconcile('Connector-Zugangsdaten sind unvollstaendig.');
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag); $plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false || $plain === '') fail_webdav_reconcile('Connector-Zugangsdaten konnten nicht entschluesselt werden.'); if ($plain === false || $plain === '') fail_webdav_reconcile('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
$decoded = json_decode($plain, true); $decoded = json_decode($plain, true);
@@ -75,11 +76,19 @@ function webdav_source_fingerprint($baseUrl, $user, array $roots)
return hash('sha256', strtolower(rtrim((string)$baseUrl, '/'))."\n".strtolower(trim((string)$user))."\n".json_encode($normalized)); return hash('sha256', strtolower(rtrim((string)$baseUrl, '/'))."\n".strtolower(trim((string)$user))."\n".json_encode($normalized));
} }
function webdav_set_runtime_dir_permissions($dir)
{
if (!is_dir($dir)) return false;
if (!@chgrp($dir, 'www-data')) return false;
return @chmod($dir, 0750);
}
$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'; $configDir = '/etc/bratonien-tools/nc-connector';
$stateRoot = '/var/lib/bratonien-tools/nc-connector'; $stateBase = '/var/lib/bratonien-tools';
$stateRoot = $stateBase.'/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';
@@ -111,7 +120,8 @@ try
if (!is_dir($dir) && !mkdir($dir, $dir === $configDir ? 0700 : 0750, 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); if (!webdav_set_runtime_dir_permissions($stateBase)) fail_webdav_reconcile('Runtime-State-Basis konnte nicht fuer den Webserver freigegeben werden.');
if (!webdav_set_runtime_dir_permissions($stateRoot)) fail_webdav_reconcile('Runtime-State-Verzeichnis konnte nicht fuer den Webserver freigegeben werden.');
@chmod($publicSourceRoot, 0755); @chmod($publicSourceRoot, 0755);
@chmod($publicGalleryRoot, 0755); @chmod($publicGalleryRoot, 0755);
@@ -151,7 +161,7 @@ try
$stateDir = rtrim((string)($config['state_dir'] ?? ''), '/'); $stateDir = 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); if (!webdav_set_runtime_dir_permissions($stateDir)) fail_webdav_reconcile('State-Verzeichnis konnte nicht fuer den Webserver freigegeben werden.');
$galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/'); $galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
$expectedGalleryRoot = $publicGalleryRoot.'/connection-'.$id; $expectedGalleryRoot = $publicGalleryRoot.'/connection-'.$id;
@@ -180,7 +190,7 @@ try
{ {
$path = trim((string)($root['webdav_path'] ?? ''), '/'); $path = trim((string)($root['webdav_path'] ?? ''), '/');
$display = trim((string)($root['display_name'] ?? '')); $display = trim((string)($root['display_name'] ?? ''));
if ($path === '' || $display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.'); if ($display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.');
$rootLines[] = $path."\t".$display; $rootLines[] = $path."\t".$display;
} }
file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX); file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX);

View File

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

View File

@@ -10,6 +10,11 @@ if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then
exit 1 exit 1
fi fi
if ! php "$SCRIPT_DIR/repair-webdav-orphans.php"; then
echo "NC Connector: verwaiste WebDAV-Bilddatensaetze konnten nicht repariert werden." >&2
exit 1
fi
if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then if ! php "$SCRIPT_DIR/cleanup-webdav-piwigo.php"; then
echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2 echo "NC Connector: Piwigo-Inhalte geloeschter WebDAV-Verbindungen konnten nicht bereinigt werden." >&2
exit 1 exit 1

View File

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

View File

@@ -14,6 +14,10 @@
<span class="bratonien-label">Nächster Lauf</span><strong data-nc-next-run>{if $nc_system_available}{$NC_CONNECTOR.system.next_run_label|escape:html}{else}Nicht verfügbar{/if}</strong> <span class="bratonien-label">Nächster Lauf</span><strong data-nc-next-run>{if $nc_system_available}{$NC_CONNECTOR.system.next_run_label|escape:html}{else}Nicht verfügbar{/if}</strong>
</div> </div>
{if $nc_system_available && $NC_CONNECTOR.system.last_run_message}<p class="bratonien-base-note">Letztes Ergebnis: <strong>{$NC_CONNECTOR.system.last_run_message|escape:html}</strong></p>{/if} {if $nc_system_available && $NC_CONNECTOR.system.last_run_message}<p class="bratonien-base-note">Letztes Ergebnis: <strong>{$NC_CONNECTOR.system.last_run_message|escape:html}</strong></p>{/if}
<form method="post" class="bratonien-actions" style="margin-top:.75rem">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="nc_connector_run_now"{if $NC_CONNECTOR.connection_count < 1} disabled{/if}>Jetzt abrufen</button>
</form>
{if $nc_system_available && $NC_CONNECTOR.system.last_run_api_state == 'error'}<p class="bratonien-main-cache__warning"><strong>API:</strong> {$NC_CONNECTOR.system.last_run_api_message|escape:html}</p>{/if} {if $nc_system_available && $NC_CONNECTOR.system.last_run_api_state == 'error'}<p class="bratonien-main-cache__warning"><strong>API:</strong> {$NC_CONNECTOR.system.last_run_api_message|escape:html}</p>{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_fallback_state == 'error'}<p class="bratonien-main-cache__warning"><strong>Fallback:</strong> {$NC_CONNECTOR.system.last_run_fallback_message|escape:html}</p>{/if} {if $nc_system_available && $NC_CONNECTOR.system.last_run_fallback_state == 'error'}<p class="bratonien-main-cache__warning"><strong>Fallback:</strong> {$NC_CONNECTOR.system.last_run_fallback_message|escape:html}</p>{/if}
{if $nc_system_available && $NC_CONNECTOR.system.last_run_error_detail}<details><summary>Technische Fehlerdetails</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.system.last_run_error_detail|escape:html}</p></details>{/if} {if $nc_system_available && $NC_CONNECTOR.system.last_run_error_detail}<details><summary>Technische Fehlerdetails</summary><p class="bratonien-main-cache__warning">{$NC_CONNECTOR.system.last_run_error_detail|escape:html}</p></details>{/if}

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

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

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

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

560
webdav-derivative.php Normal file
View File

@@ -0,0 +1,560 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
define('BRATONIEN_TOOLS_ID', basename(__DIR__));
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
function bratonien_tools_webdav_derivative_debug($request_id, $event, array $fields=array())
{
$parts = array('[BRAT-WD '.$request_id.']', $event);
foreach ($fields as $key => $value)
{
if (is_bool($value)) $value = $value ? '1' : '0';
elseif ($value === null) $value = 'NULL';
elseif (is_array($value) || is_object($value)) $value = json_encode($value);
$parts[] = $key.'='.str_replace(array("\r", "\n"), array('\\r', '\\n'), (string)$value);
}
error_log(implode(' ', $parts));
}
function bratonien_tools_webdav_derivative_abort($status, $message)
{
http_response_code((int)$status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo $message;
exit;
}
function bratonien_tools_webdav_derivative_send($target_path, $after_url='')
{
if (isset($_GET['ajaxload']) && (string)$_GET['ajaxload'] === 'true')
{
if ($after_url === '') bratonien_tools_webdav_derivative_abort(400, 'Lazyload-Ziel fehlt.');
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode(array('url'=>$after_url), JSON_UNESCAPED_SLASHES);
exit;
}
header('Content-Type: '.(function_exists('get_mimetype') ? get_mimetype($target_path) : 'image/jpeg'));
readfile($target_path);
exit;
}
function bratonien_tools_webdav_derivative_decrypt_secret($blob, $hex_key)
{
$hex_key = trim((string)$hex_key);
if (!preg_match('/^[a-f0-9]{64}$/', $hex_key)) return null;
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) return null;
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) return null;
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) return null;
$decoded = json_decode((string)$plain, true);
return is_array($decoded) ? $decoded : null;
}
function bratonien_tools_webdav_derivative_quote_path($path)
{
$parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen'));
return implode('/', array_map('rawurlencode', $parts));
}
function bratonien_tools_webdav_derivative_connection(array $source, &$detail=null)
{
$detail = '';
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$result = pwg_query('SELECT config_json, secret_blob FROM `'.$table.'` WHERE id='.(int)$source['connection_id'].' LIMIT 1');
if (!pwg_db_num_rows($result)) { $detail = 'WebDAV-Verbindung nicht gefunden.'; return null; }
$row = pwg_db_fetch_assoc($result);
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) { $detail = 'WebDAV-Konfiguration ist ungueltig.'; return null; }
$key_result = pwg_query("SELECT value FROM ".$GLOBALS['prefixeTable']."config WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!pwg_db_num_rows($key_result)) { $detail = 'Connector-Schluessel fehlt.'; return null; }
$key_row = pwg_db_fetch_assoc($key_result);
$credentials = bratonien_tools_webdav_derivative_decrypt_secret((string)$row['secret_blob'], (string)$key_row['value']);
if (!is_array($credentials)) { $detail = 'WebDAV-Zugangsdaten konnten nicht gelesen werden.'; return null; }
$base_url = rtrim((string)($config['nextcloud_url'] ?? ''), '/');
$user = trim((string)($credentials['nextcloud_user'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
if ($base_url === '' || $user === '' || $password === '') { $detail = 'WebDAV-Verbindung ist unvollstaendig.'; return null; }
return array('base_url'=>$base_url, 'user'=>$user, 'password'=>$password);
}
function bratonien_tools_webdav_derivative_download_url($url, array $connection, $destination, &$detail=null)
{
$detail = '';
$fp = @fopen($destination, 'xb');
if (!$fp) { $detail = 'Temporaere Quelldatei konnte nicht angelegt werden.'; return false; }
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 180,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $connection['user'].':'.$connection['password'],
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FILE => $fp,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Materialize',
));
$ok = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
if ($ok === false || $errno !== 0 || $http < 200 || $http >= 300)
{
@unlink($destination);
$detail = 'Nextcloud-Download fehlgeschlagen (HTTP '.$http.', cURL '.$errno.($error !== '' ? ': '.$error : '').').';
return false;
}
if (!is_file($destination) || filesize($destination) < 1 || @getimagesize($destination) === false)
{
@unlink($destination);
$detail = 'Nextcloud-Datei ist kein lesbares Bild.';
return false;
}
return true;
}
function bratonien_tools_webdav_derivative_download(array $source, array $connection, $destination, &$detail=null)
{
$webdav_path = trim((string)($source['webdav_path'] ?? ''), '/');
if ($webdav_path === '') { $detail = 'WebDAV-Bildquelle ist unvollstaendig.'; return false; }
$url = $connection['base_url'].'/remote.php/dav/files/'.rawurlencode($connection['user']).'/'.bratonien_tools_webdav_derivative_quote_path($webdav_path);
return bratonien_tools_webdav_derivative_download_url($url, $connection, $destination, $detail);
}
function bratonien_tools_webdav_derivative_preview_plan($variant, array $derivative_size, array $source, array $image_row)
{
if ($variant !== 'custom:s9999x250' && $variant !== 'standard:square') return null;
if ((int)($source['fileid'] ?? 0) < 1) return null;
if ((int)($source['width'] ?? 0) < 1 || (int)($source['height'] ?? 0) < 1) return null;
if ((int)($image_row['rotation'] ?? 0) !== 0) return null;
$target_width = (int)($derivative_size[0] ?? 0);
$target_height = (int)($derivative_size[1] ?? 0);
if ($target_width < 1 || $target_height < 1 || max($target_width, $target_height) > 800) return null;
$source_width = (int)$source['width'];
$source_height = (int)$source['height'];
$required_width = $target_width * 2;
$required_height = $target_height * 2;
$scale = max($required_width / $source_width, $required_height / $source_height);
$preview_width = max(32, (int)ceil($source_width * $scale));
$preview_height = max(32, (int)ceil($source_height * $scale));
if (max($preview_width, $preview_height) > 1600) return null;
return array(
'fileid'=>(int)$source['fileid'],
'width'=>$preview_width,
'height'=>$preview_height,
);
}
function bratonien_tools_webdav_derivative_download_preview(array $plan, array $connection, $destination, &$detail=null)
{
$query = http_build_query(array(
'fileId'=>(int)$plan['fileid'],
'x'=>(int)$plan['width'],
'y'=>(int)$plan['height'],
'a'=>'true',
'forceIcon'=>'false',
'mode'=>'fill',
'mimeFallback'=>'false',
), '', '&', PHP_QUERY_RFC3986);
$url = $connection['base_url'].'/index.php/core/preview?'.$query;
return bratonien_tools_webdav_derivative_download_url($url, $connection, $destination, $detail);
}
function bratonien_tools_webdav_derivative_make_dir($dir)
{
global $conf;
if (!is_dir($dir))
{
$mode = isset($conf['chmod_value']) ? (int)$conf['chmod_value'] : 0755;
$umask = umask(0);
$ok = @mkdir($dir, $mode, true);
umask($umask);
if (!$ok && !is_dir($dir)) return false;
}
if (!is_writable($dir)) return false;
$index = rtrim($dir, '/').'/index.htm';
if (!file_exists($index)) @file_put_contents($index, 'Not allowed!');
return true;
}
function bratonien_tools_webdav_derivative_generate($source_path, $target_path, $params, array $image_row, &$detail=null)
{
global $conf;
$detail = '';
include_once(PHPWG_ROOT_PATH.'admin/include/image.class.php');
if (!is_object($params)) { $detail = 'Piwigo-Derivatparameter fehlen.'; return false; }
if (!is_file($source_path) || !is_readable($source_path)) { $detail = 'Materialisierte Bildquelle ist nicht lesbar.'; return false; }
if (!bratonien_tools_webdav_derivative_make_dir(dirname($target_path))) { $detail = 'Piwigo-Derivatverzeichnis konnte nicht angelegt werden.'; return false; }
$rotation_angle = isset($image_row['rotation'])
? pwg_image::get_rotation_angle_from_code($image_row['rotation'])
: pwg_image::get_rotation_angle($source_path);
$coi = $image_row['coi'] ?? null;
if (($params->type ?? '') === IMG_CUSTOM)
{
$defined = ImageStdParams::get_defined_type_map();
if (count($defined) > 0)
{
$sharpen = 0;
foreach ($defined as $std_params) $sharpen += $std_params->sharpen;
$params->sharpen = round($sharpen / count($defined));
}
}
$image = null;
$wm_image = null;
try
{
$image = new pwg_image($source_path);
$changes = 0;
if (0 != $rotation_angle)
{
$image->rotate($rotation_angle);
$changes++;
}
$o_size = $d_size = array($image->get_width(), $image->get_height());
$crop_rect = null;
$scaled_size = null;
$params->sizing->compute($o_size, $coi, $crop_rect, $scaled_size);
if ($crop_rect)
{
$image->crop($crop_rect->width(), $crop_rect->height(), $crop_rect->l, $crop_rect->t);
$changes++;
}
if ($scaled_size)
{
$image->resize($scaled_size[0], $scaled_size[1]);
$d_size = $scaled_size;
$changes++;
}
if ($params->sharpen)
{
$changes += $image->sharpen($params->sharpen);
}
if ($params->will_watermark($d_size))
{
$wm = ImageStdParams::get_watermark();
$wm_image = new pwg_image(PHPWG_ROOT_PATH.$wm->file);
$wm_size = array($wm_image->get_width(), $wm_image->get_height());
if ($d_size[0] < $wm_size[0] || $d_size[1] < $wm_size[1])
{
$wm_scaling_params = SizingParams::classic($d_size[0], $d_size[1]);
$tmp = null;
$wm_scaled_size = null;
$wm_scaling_params->compute($wm_size, null, $tmp, $wm_scaled_size);
$wm_size = $wm_scaled_size;
$wm_image->resize($wm_scaled_size[0], $wm_scaled_size[1]);
}
$x = round(($wm->xpos / 100) * ($d_size[0] - $wm_size[0]));
$y = round(($wm->ypos / 100) * ($d_size[1] - $wm_size[1]));
if ($image->compose($wm_image, $x, $y, $wm->opacity))
{
$changes++;
if ($wm->xrepeat || $wm->yrepeat)
{
$xpad = $wm_size[0] + max(30, round($wm_size[0] / 4));
$ypad = $wm_size[1] + max(30, 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] < $d_size[0] && $y2 >= 0 && $y2 + $wm_size[1] < $d_size[1])
{
if (!$image->compose($wm_image, $x2, $y2, $wm->opacity)) break;
}
}
}
}
}
$wm_image->destroy();
$wm_image = null;
}
if (!$changes)
{
$detail = 'Piwigo-Derivat benoetigt keine Bildaenderung.';
return false;
}
if ($d_size[0] * $d_size[1] < ($conf['derivatives_strip_metadata_threshold'] ?? PHP_INT_MAX))
{
$image->strip();
}
$image->write($target_path);
@chmod($target_path, 0644);
clearstatcache(true, $target_path);
if (!is_file($target_path) || !is_readable($target_path) || @getimagesize($target_path) === false)
{
$detail = 'Piwigo hat kein gueltiges finales Derivat erzeugt.';
return false;
}
return true;
}
catch (Throwable $e)
{
$detail = 'Piwigo-Derivaterzeugung fehlgeschlagen: '.$e->getMessage();
return false;
}
finally
{
if ($wm_image) $wm_image->destroy();
if ($image) $image->destroy();
}
}
$request_id = substr(bin2hex(random_bytes(8)), 0, 8);
$image_id = (int)($_GET['id'] ?? 0);
$variant = trim((string)($_GET['variant'] ?? ''));
$after_url = '';
if ($image_id < 1 || $variant === '') bratonien_tools_webdav_derivative_abort(400, 'Ungueltige Derivatanforderung.');
bratonien_tools_webdav_derivative_debug($request_id, 'start', array('image_id'=>$image_id, 'variant'=>$variant));
if (!empty($_GET['after']))
{
$raw_after = strtr((string)$_GET['after'], '-_', '+/');
$padding = strlen($raw_after) % 4;
if ($padding) $raw_after .= str_repeat('=', 4 - $padding);
$decoded_after = base64_decode($raw_after, true);
if (is_string($decoded_after)) $after_url = $decoded_after;
}
if ($after_url !== '')
{
$expected = bratonien_tools_webdav_materialize_after_signature($image_id, $variant, $after_url);
if (!hash_equals($expected, (string)($_GET['sig'] ?? ''))) bratonien_tools_webdav_derivative_abort(403, 'Ungueltige Derivatsignatur.');
}
$result = pwg_query('SELECT id, path, width, height, rotation, coi FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result)) bratonien_tools_webdav_derivative_abort(404, 'Bild nicht gefunden.');
$image_row = pwg_db_fetch_assoc($result);
if (function_exists('get_sql_condition_FandF'))
{
$forbidden = get_sql_condition_FandF(array('visible_images'=>'id'), ' AND');
if ($forbidden !== '')
{
$visible = pwg_query('SELECT id FROM '.IMAGES_TABLE.' WHERE id='.$image_id.$forbidden.' LIMIT 1');
if (!pwg_db_num_rows($visible)) bratonien_tools_webdav_derivative_abort(403, 'Kein Zugriff auf das Bild.');
}
}
$source = bratonien_tools_webdav_materialize_source_info($image_id);
if (!$source) bratonien_tools_webdav_derivative_abort(404, 'WebDAV-Bildquelle nicht gefunden.');
bratonien_tools_webdav_derivative_debug($request_id, 'source_resolved', array(
'connection_id'=>$source['connection_id'],
'root_fileid'=>$source['root_fileid'],
'fileid'=>$source['fileid'] ?? 0,
'webdav_path'=>$source['webdav_path'],
));
$params = bratonien_tools_webdav_materialize_params_from_variant($variant);
if (!$params) bratonien_tools_webdav_derivative_abort(400, 'Unbekannte Derivatvariante.');
$src_image = new SrcImage($image_row);
$real_derivative = new DerivativeImage($params, $src_image);
$target_path = $real_derivative->get_path();
$derivative_size = $real_derivative->get_size();
if ($target_path !== '' && is_file($target_path) && is_readable($target_path))
{
bratonien_tools_webdav_derivative_debug($request_id, 'target_exists', array('target'=>$target_path));
bratonien_tools_webdav_derivative_send($target_path, $after_url);
}
$source_logical_path = (string)$image_row['path'];
if (strpos($source_logical_path, '/') !== 0)
{
$source_logical_path = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $source_logical_path), '/');
}
$lock_dir = PHPWG_ROOT_PATH.'upload/bratonien-webdav-materialize';
if (!is_dir($lock_dir)) @mkdir($lock_dir, 0755, true);
$lock_path = $lock_dir.'/image-'.$image_id.'.lock';
$lock_handle = fopen($lock_path, 'c');
if (!$lock_handle) bratonien_tools_webdav_derivative_abort(500, 'Derivat-Lock konnte nicht geoeffnet werden.');
bratonien_tools_webdav_derivative_debug($request_id, 'waiting_lock', array('lock'=>$lock_path, 'is_link'=>is_link($source_logical_path)));
flock($lock_handle, LOCK_EX);
bratonien_tools_webdav_derivative_debug($request_id, 'lock_acquired', array('is_link'=>is_link($source_logical_path)));
clearstatcache(true, $target_path);
if ($target_path !== '' && is_file($target_path) && is_readable($target_path))
{
bratonien_tools_webdav_derivative_debug($request_id, 'target_created_while_waiting', array('target'=>$target_path));
flock($lock_handle, LOCK_UN);
fclose($lock_handle);
bratonien_tools_webdav_derivative_send($target_path, $after_url);
}
$detail = '';
$connection = bratonien_tools_webdav_derivative_connection($source, $detail);
if (!$connection)
{
flock($lock_handle, LOCK_UN);
fclose($lock_handle);
bratonien_tools_webdav_derivative_abort(502, $detail !== '' ? $detail : 'WebDAV-Verbindung konnte nicht geladen werden.');
}
$temp_dir = $lock_dir.'/tmp';
if (!is_dir($temp_dir)) @mkdir($temp_dir, 0755, true);
$temp_source = $temp_dir.'/source-'.$image_id.'-'.$request_id.'.img';
$source_mode = 'original';
$preview_plan = bratonien_tools_webdav_derivative_preview_plan($variant, $derivative_size, $source, $image_row);
if ($preview_plan)
{
bratonien_tools_webdav_derivative_debug($request_id, 'preview_start', array(
'fileid'=>$preview_plan['fileid'],
'width'=>$preview_plan['width'],
'height'=>$preview_plan['height'],
));
$download_start = microtime(true);
if (bratonien_tools_webdav_derivative_download_preview($preview_plan, $connection, $temp_source, $detail))
{
$source_mode = 'preview';
bratonien_tools_webdav_derivative_debug($request_id, 'download_done', array(
'mode'=>$source_mode,
'ms'=>round((microtime(true) - $download_start) * 1000, 1),
'bytes'=>filesize($temp_source),
));
}
else
{
bratonien_tools_webdav_derivative_debug($request_id, 'preview_failed', array('detail'=>$detail));
$detail = '';
}
}
if ($source_mode !== 'preview')
{
@unlink($temp_source);
$download_start = microtime(true);
if (!bratonien_tools_webdav_derivative_download($source, $connection, $temp_source, $detail))
{
flock($lock_handle, LOCK_UN);
fclose($lock_handle);
bratonien_tools_webdav_derivative_abort(502, $detail !== '' ? $detail : 'WebDAV-Original konnte nicht geladen werden.');
}
bratonien_tools_webdav_derivative_debug($request_id, 'download_done', array(
'mode'=>'original',
'ms'=>round((microtime(true) - $download_start) * 1000, 1),
'bytes'=>filesize($temp_source),
));
}
$source_path = realpath($source_logical_path);
if ($source_path === false || !is_file($source_path))
{
@unlink($temp_source);
flock($lock_handle, LOCK_UN);
fclose($lock_handle);
bratonien_tools_webdav_derivative_abort(500, 'Physischer Platzhalter konnte nicht aufgeloest werden.');
}
$placeholder_backup = $source_path.'.bratonien-placeholder-'.$request_id;
$staging_path = $source_logical_path.'.bratonien-source-'.$request_id;
@unlink($placeholder_backup);
@unlink($staging_path);
if (!@copy($temp_source, $staging_path))
{
@unlink($temp_source);
flock($lock_handle, LOCK_UN);
fclose($lock_handle);
bratonien_tools_webdav_derivative_abort(500, 'Temporaere Bildquelle konnte nicht bereitgestellt werden.');
}
$swapped = false;
try
{
bratonien_tools_webdav_derivative_debug($request_id, 'swap_begin', array('placeholder'=>$source_path, 'mode'=>$source_mode));
if (!@rename($source_path, $placeholder_backup)) throw new RuntimeException('Platzhalter konnte nicht gesichert werden.');
if (!@rename($staging_path, $source_path))
{
@rename($placeholder_backup, $source_path);
throw new RuntimeException('Temporaere Bildquelle konnte nicht eingesetzt werden.');
}
$swapped = true;
bratonien_tools_webdav_derivative_debug($request_id, 'swapped', array('is_link'=>is_link($source_logical_path), 'mode'=>$source_mode));
$generate_start = microtime(true);
bratonien_tools_webdav_derivative_debug($request_id, 'generate_start', array('target'=>$target_path, 'mode'=>$source_mode));
$ok = bratonien_tools_webdav_derivative_generate($source_path, $target_path, $params, $image_row, $detail);
bratonien_tools_webdav_derivative_debug($request_id, 'generate_done', array(
'ok'=>$ok,
'mode'=>$source_mode,
'ms'=>round((microtime(true) - $generate_start) * 1000, 1),
'detail'=>$detail,
));
if (!$ok) throw new RuntimeException($detail !== '' ? $detail : 'Piwigo-Derivat konnte nicht erzeugt werden.');
}
catch (Throwable $e)
{
if ($swapped)
{
@unlink($source_path);
@rename($placeholder_backup, $source_path);
}
else
{
@unlink($staging_path);
}
@unlink($temp_source);
flock($lock_handle, LOCK_UN);
fclose($lock_handle);
bratonien_tools_webdav_derivative_abort(500, $e->getMessage());
}
@unlink($source_path);
@rename($placeholder_backup, $source_path);
@unlink($temp_source);
bratonien_tools_webdav_derivative_debug($request_id, 'restored', array('is_link'=>is_link($source_logical_path)));
flock($lock_handle, LOCK_UN);
fclose($lock_handle);
if (!is_file($target_path) || !is_readable($target_path)) bratonien_tools_webdav_derivative_abort(500, 'Erzeugtes Derivat fehlt.');
$final_size = @getimagesize($target_path);
bratonien_tools_webdav_derivative_debug($request_id, 'serve', array(
'target'=>$target_path,
'width'=>(int)($final_size[0] ?? 0),
'height'=>(int)($final_size[1] ?? 0),
'mode'=>$source_mode,
));
bratonien_tools_webdav_derivative_send($target_path, $after_url);

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

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

View File

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