Compare commits

..
Author SHA1 Message Date
Terranom674 3113fc1973 Disable automatic workflow triggers; manual runs only 2026-10-19 20:28:33 +02:00
62 changed files with 281 additions and 9062 deletions

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
name: Syntax Check
on:
workflow_dispatch:
jobs:
php-syntax:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3', '8.4', '8.5']
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: PHP installieren
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- name: PHP Syntax pruefen
shell: bash
run: |
set -euo pipefail
while IFS= read -r -d '' file; do
php -l "$file"
done < <(find . -type f -name '*.php' -print0)
- name: Doppelte Bratonien-Funktionen pruefen
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
import pathlib
import re
import sys
found = {}
for path in pathlib.Path('.').rglob('*.php'):
text = path.read_text(encoding='utf-8', errors='ignore')
for name in re.findall(r'\bfunction\s+(bratonien_tools_[A-Za-z0-9_]+)\s*\(', text):
found.setdefault(name, []).append(str(path))
duplicates = {name: paths for name, paths in found.items() if len(paths) > 1}
if duplicates:
for name, paths in sorted(duplicates.items()):
print(f'{name}: {", ".join(paths)}', file=sys.stderr)
sys.exit(1)
PY
script-syntax:
name: JavaScript, Python, Shell
runs-on: ubuntu-latest
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: JavaScript Syntax pruefen
shell: bash
run: |
set -euo pipefail
while IFS= read -r -d '' file; do
node --check "$file"
done < <(find . -type f -name '*.js' -print0)
- name: Python Syntax pruefen
shell: bash
run: |
set -euo pipefail
python3 -m compileall -q runtime
- name: Shell Syntax pruefen
shell: bash
run: |
set -euo pipefail
while IFS= read -r -d '' file; do
bash -n "$file"
done < <(find . -type f -name '*.sh' -print0)
+22 -129
View File
@@ -2,168 +2,61 @@
## Version ## Version
**Bratonien Tools 0.9.7.1.7** **Bratonien Tools 0.9.6.2**
Verbindliches Versionsschema: **0.x.x.x.x**.
Zielplattform der produktiven NC-Connector-Synchronisierung ist derzeit **Piwigo 16.4.0**.
## NC Connector ## NC Connector
Der produktive Verbindungsweg ist **Nextcloud per WebDAV**. Der NC Connector besitzt nur noch einen produktiven Verbindungsweg: **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 pro Verbindung gespeichert. - Piwigo-API und Benutzername/Passwort-Fallback werden ebenfalls pro Verbindung gespeichert.
- Der Assistent zeigt ausschließlich per WebDAV sichtbare Verzeichnisse. - Der Assistent zeigt ausschließlich die Verzeichnisse, auf die der angemeldete Nextcloud-Benutzer per WebDAV zugreifen kann.
- Ein leerer `webdav_path` ist ein gültiger Benutzer-Root. - Mehrere Verzeichnisse können pro Verbindung ausgewählt werden.
### 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 WebDAV-Verbindungen in dieser Reihenfolge: Der gemeinsame Runner verarbeitet ausschließlich WebDAV-Verbindungen:
1. `runtime/reconcile-webdav.php` 1. `runtime/reconcile-webdav.php`
2. `runtime/repair-webdav-orphans.php` 2. `runtime/cleanup-webdav-piwigo.php`
3. `runtime/cleanup-webdav-piwigo.php` 3. `runtime/sync-webdav.sh` für jede vorhandene WebDAV-Konfiguration
4. `runtime/sync-webdav.sh` für jede vorhandene WebDAV-Konfiguration
Verbindungsspezifische Runtime-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`. Zustandsdaten liegen unter `/var/lib/bratonien-tools/nc-connector/connection-ID`. Die verbindungsspezifischen Runtime-Konfigurationen liegen unter `/etc/bratonien-tools/nc-connector/`. Zustandsdaten liegen unter `/var/lib/bratonien-tools/nc-connector/connection-ID`.
State-Verzeichnisse sind für die `www-data`-Gruppe traversierbar/lesbar. `webdav-map.json` wird auch nach atomaren Rewrites mit passenden Runtime-Rechten gehalten. ### Shadow Tree und Piwigo
### Platzhalterquelle, Metadaten und Shadow Tree `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.
`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. 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.
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`. 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.
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
Fehlende Piwigo-Derivate werden durch den On-Demand-Gate erzeugt. Bereits vorhandene Derivate werden direkt aus Piwigos eigenem Derivat-Cache ausgeliefert. 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.
Für direkte Originalanforderungen existiert weiterhin die serverseitige WebDAV-Originalauslieferung. Nextcloud-Zugangsdaten werden dem Browser nicht offengelegt. ### Löschen
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 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`. 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.
## Self-Updater ## Entfernte Connector-Komponenten
`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. 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`.
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
Die GitHub-Actions-Konfiguration prüft mindestens: Jede Änderung wird vor dem Merge mindestens mit folgenden GitHub-Actions-Prüfungen validiert:
- 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
- doppelte globale `bratonien_tools_*`-Funktionen - Prüfung auf 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`
+31 -92
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.7.1.7** Aktuelle Plugin-Version: **0.9.6.2**
## Grundprinzip ## Grundprinzip
@@ -12,43 +12,18 @@ 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 per WebDAV an Piwigo an. Nextcloud bleibt dabei die Quelle der Originaldateien; Piwigo verwaltet Alben, Bilder und seine eigenen Derivate. Der NC Connector bindet ausdrücklich ausgewählte Nextcloud-Inhalte ausschließlich per WebDAV an Piwigo an.
### Datenmodell ### Datenmodell
- Nextcloud bleibt die verbindliche Quelle der Originaldateien. - Nextcloud bleibt die Quelle der Originaldateien.
- Originalbilder werden nicht dauerhaft als zweite vollständige Bibliothek unter Piwigo gespeichert. - Originalbilder werden nicht dauerhaft in eine zweite Bibliothek unter Piwigo kopiert.
- Eine lokale Platzhalterquelle enthält physische 1×1-Dateien, damit Piwigo die Bilder regulär als Dateisysteminhalte erkennen kann. - Ein Shadow Tree erzeugt die für Piwigo benötigte Ordnerstruktur.
- 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.
- Direkt im ausgewählten Nextcloud-Root liegende Einzelbilder werden über die vorhandene Orphan-Logik behandelt. - Root-Dateien können als Piwigo-Orphans registriert werden.
- 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:
@@ -60,57 +35,30 @@ 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-Scan und Shadow Tree ### WebDAV-Ablauf
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, Originalabmessungen und WebDAV-Pfad werden erfasst. 3. Ordner, Dateiname, Nextcloud-Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
4. `runtime/lib/build_webdav_placeholder_source.py` erzeugt eine lokale Platzhalterquelle ohne dauerhafte Originalkopie. 4. `runtime/lib/build_webdav_placeholder_source.py` erzeugt eine lokale Platzhalterquelle ohne dauerhafte Originalkopie.
5. `runtime/lib/shadow_tree.py` baut atomar den verbindungseigenen Shadow Tree. 5. `runtime/lib/shadow_tree.py` baut daraus den verbindungseigenen Shadow Tree.
6. Der WebDAV-Galeriebaum liegt unter `_data/bratonien-tools/nc-webdav-gallery/connection-ID`. 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. `runtime/lib/sync-webdav-metadata.php` übernimmt die von Nextcloud gelieferten Originalmaße in Piwigos Bilddatensätze. 9. Die WebDAV-Bildzuordnung ersetzt beim Ausliefern Platzhalter-URLs durch die echte Nextcloud-Bildquelle.
10. `webdav-image.php` streamt benötigte Bilder serverseitig; Zugangsdaten werden dem Browser nicht offengelegt.
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 ausschließlich 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 die Dateisystemdarstellung, die Piwigo für Alben und Bilder benötigt.
`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. `runtime/lib/piwigo-sync.php` ruft dafür `bratonien.nc.syncProductive` auf. Die Methode führt den Piwigo-Core-Sync für die konkrete Site aus. `bratonien.nc.syncOrphans` arbeitet ebenfalls nur auf der übergebenen WebDAV-Site.
Nach 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.
### Private Top-Level-Alben ### Piwigo-Zugang
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. 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.
### 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
@@ -130,14 +78,11 @@ 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 gemeinsame Lauf besteht aktuell aus: Der Lauf besteht aus:
1. `runtime/reconcile-webdav.php` – gespeicherte Verbindungen in Runtime-Konfigurationen überführen; 1. WebDAV-Verbindungen reconciliieren;
2. `runtime/repair-webdav-orphans.php` – vorhandene WebDAV-Orphan-Zustände reparieren; 2. verwaiste WebDAV-Piwigo-Inhalte bereinigen;
3. `runtime/cleanup-webdav-piwigo.php` – verwaiste WebDAV-Piwigo-Daten bereinigen; 3. jede vorhandene WebDAV-Verbindung synchronisieren.
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
@@ -180,38 +125,34 @@ 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. 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. Die Piwigo-Bilddetailseite erhält responsive Navigationszonen für vorheriges Bild, nächstes Bild, Rückkehr zur Übersicht und PhotoSwipe/Vollbild.
## 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. Das verbindliche Versionsschema des Plugins ist **0.x.x.x.x**; Updates werden nur auf eine höhere Version angeboten. 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.
## Wichtige Dateien und Verzeichnisse ## Wichtige Dateien und Verzeichnisse
- `main.inc.php` – Plugin-Einstieg, Hooks, Connector-Sync-Hilfen und Dashboard-Prefilter - `main.inc.php` – Plugin-Einstieg und Runtime-Hooks
- `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` – direkter Piwigo-Core-Dateisync für Piwigo 16.4.0 - `include/nc_productive_ws.inc.php` – Piwigo-Core-Dateisync
- `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_materialize_runtime.inc.php` – WebDAV-Quellauflösung und On-Demand-Materialisierung einschließlich `fileid` und Originalmaßen - `include/webdav_image_runtime.inc.php` – WebDAV-Bildzuordnung und URL-Filter
- `include/webdav_image_runtime.inc.php` – vorhandene WebDAV-Bildzuordnung/Originalauslieferung - `webdav-image.php` – berechtigungsgeprüfter WebDAV-Bildstream
- `webdav-derivative.php` – On-Demand-Gate, Nextcloud-Preview/Original-Fallback, Piwigo-AJAX-Loader-Antwort und Piwigo-Derivaterzeugung - `runtime/reconcile-webdav.php` – WebDAV-Runtime-Reconcile
- `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 einschließlich Mapping-/Source-Berechtigungen - `runtime/sync-webdav.sh` – Ablauf einer WebDAV-Verbindung
- `runtime/run-all.sh` – gemeinsamer WebDAV-Runner - `runtime/run-all.sh` – gemeinsamer WebDAV-Runner
- `runtime/lib/build_webdav_placeholder_source.py` – rekursiver WebDAV-Scan, Bildmetadaten und physische Platzhalterquelle - `runtime/lib/build_webdav_placeholder_source.py` – rekursiver WebDAV-Scan und Platzhalterquelle
- `runtime/lib/sync-webdav-metadata.php` – Übernahme der Originalabmessungen in Piwigo - `runtime/lib/shadow_tree.py` – atomarer Shadow Tree
- `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 und private Albumzugriffe - `include/album_shares.inc.php` – geschützte Albumfreigaben
- `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
@@ -226,7 +167,5 @@ 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 nur bei Bedarf temporär lokal materialisiert; - WebDAV-Originalbilder werden nicht dauerhaft lokal gespeichert;
- 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.
-46
View File
@@ -110,47 +110,6 @@ $engine = bratonien_tools_get_watermark_engine_config();
$cache_workers = bratonien_tools_get_cache_worker_settings(); $cache_workers = bratonien_tools_get_cache_worker_settings();
$public_selection = bratonien_tools_get_public_selection_settings(); $public_selection = bratonien_tools_get_public_selection_settings();
$public_selection_groups = bratonien_tools_get_piwigo_groups(); $public_selection_groups = bratonien_tools_get_piwigo_groups();
$customer_qr = bratonien_tools_customer_qr_admin_data();
$customer_qr['friendship_codes'] = bratonien_tools_friendship_code_admin_data();
$qr_present = array();
foreach ($customer_qr['uploads'] as $qr_upload)
{
$qr_year = (int)$qr_upload['year'];
$qr_number = (int)$qr_upload['number'];
if ($qr_year < 1 || $qr_number < 1)
{
continue;
}
if (!isset($qr_present[$qr_year]))
{
$qr_present[$qr_year] = array();
}
$qr_present[$qr_year][$qr_number] = $qr_upload;
}
$qr_batch_base = get_absolute_root_url(true).'plugins/'.BRATONIEN_TOOLS_ID.'/customer-qr-admin-batch.php';
$customer_qr['batch_download_available'] = class_exists('ZipArchive');
$customer_qr['batch_download_url'] = $qr_batch_base;
foreach ($customer_qr['years'] as &$qr_year)
{
$year = (int)$qr_year['year'];
$capacity = (int)$qr_year['capacity'];
$qr_year['batch_download_url'] = $qr_batch_base.'?year='.$year;
$qr_year['slots'] = array();
for ($number = 1; $number <= $capacity; $number++)
{
$present = isset($qr_present[$year][$number]);
$qr_year['slots'][] = array(
'number' => $number,
'present' => $present,
'preview_url' => $present ? (string)$qr_present[$year][$number]['preview_url'] : '',
);
}
}
unset($qr_year);
$assets = bratonien_tools_get_assets(); $assets = bratonien_tools_get_assets();
$asset_environment = bratonien_tools_get_asset_environment(); $asset_environment = bratonien_tools_get_asset_environment();
$album_shares = bratonien_tools_get_album_shares(); $album_shares = bratonien_tools_get_album_shares();
@@ -285,7 +244,6 @@ $template->assign(array(
'CACHE_WORKERS' => $cache_workers, 'CACHE_WORKERS' => $cache_workers,
'PUBLIC_SELECTION' => $public_selection, 'PUBLIC_SELECTION' => $public_selection,
'PUBLIC_SELECTION_GROUPS' => $public_selection_groups, 'PUBLIC_SELECTION_GROUPS' => $public_selection_groups,
'CUSTOMER_QR' => $customer_qr,
'BRATONIEN_ASSETS' => $assets, 'BRATONIEN_ASSETS' => $assets,
'ASSET_ENV' => $asset_environment, 'ASSET_ENV' => $asset_environment,
'BRATONIEN_ALBUM_SHARES' => $album_shares, 'BRATONIEN_ALBUM_SHARES' => $album_shares,
@@ -301,9 +259,7 @@ $template->assign(array(
$template->set_filename('admin_tabs_content', BRATONIEN_TOOLS_PATH . 'template/admin_tabs.tpl'); $template->set_filename('admin_tabs_content', BRATONIEN_TOOLS_PATH . 'template/admin_tabs.tpl');
$template->set_filename('plugin_admin_content', BRATONIEN_TOOLS_PATH . 'template/admin.tpl'); $template->set_filename('plugin_admin_content', BRATONIEN_TOOLS_PATH . 'template/admin.tpl');
$template->set_filename('webdav_warmup_admin_content', BRATONIEN_TOOLS_PATH . 'template/webdav_warmup_admin.tpl');
$template->set_filename('public_selection_admin_content', BRATONIEN_TOOLS_PATH . 'template/public_selection_admin.tpl'); $template->set_filename('public_selection_admin_content', BRATONIEN_TOOLS_PATH . 'template/public_selection_admin.tpl');
$template->set_filename('customer_qr_admin_content', BRATONIEN_TOOLS_PATH . 'template/customer_qr_admin.tpl');
$template->set_filename('asset_manager_admin_content', BRATONIEN_TOOLS_PATH . 'template/asset_manager_admin.tpl'); $template->set_filename('asset_manager_admin_content', BRATONIEN_TOOLS_PATH . 'template/asset_manager_admin.tpl');
$template->set_filename('album_shares_admin_content', BRATONIEN_TOOLS_PATH . 'template/album_shares_admin.tpl'); $template->set_filename('album_shares_admin_content', BRATONIEN_TOOLS_PATH . 'template/album_shares_admin.tpl');
$template->set_filename('nc_connector_admin_content', BRATONIEN_TOOLS_PATH . 'template/nc_connector_admin.tpl'); $template->set_filename('nc_connector_admin_content', BRATONIEN_TOOLS_PATH . 'template/nc_connector_admin.tpl');
@@ -311,9 +267,7 @@ $template->set_filename('system_admin_content', BRATONIEN_TOOLS_PATH . 'template
$admin_content = $template->parse('admin_tabs_content', true); $admin_content = $template->parse('admin_tabs_content', true);
$admin_content .= $template->parse('plugin_admin_content', true); $admin_content .= $template->parse('plugin_admin_content', true);
$admin_content .= $template->parse('webdav_warmup_admin_content', true);
$admin_content .= $template->parse('public_selection_admin_content', true); $admin_content .= $template->parse('public_selection_admin_content', true);
$admin_content .= $template->parse('customer_qr_admin_content', true);
$admin_content .= $template->parse('asset_manager_admin_content', true); $admin_content .= $template->parse('asset_manager_admin_content', true);
$admin_content .= $template->parse('album_shares_admin_content', true); $admin_content .= $template->parse('album_shares_admin_content', true);
$admin_content .= $template->parse('nc_connector_admin_content', true); $admin_content .= $template->parse('nc_connector_admin_content', true);
-9
View File
@@ -17,12 +17,3 @@
.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;
}
-122
View File
@@ -1,122 +0,0 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
http_response_code(404);
exit;
}
check_status(ACCESS_ADMINISTRATOR);
require_once(BRATONIEN_TOOLS_PATH.'include/customer_qr_upload.inc.php');
if (!class_exists('ZipArchive'))
{
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
exit('ZIP-Unterstützung ist auf diesem Server nicht verfügbar.');
}
bratonien_tools_customer_qr_ensure_storage();
$limits = bratonien_tools_customer_qr_year_limits();
$table = bratonien_tools_customer_qr_table();
$requested_year = isset($_GET['year']) && $_GET['year'] !== '' ? bratonien_tools_customer_qr_year($_GET['year']) : 0;
$query = 'SELECT upload_year, code_number, stored_name FROM `'.$table.'`';
if ($requested_year > 0)
{
$query .= ' WHERE upload_year='.(int)$requested_year;
}
$query .= ' ORDER BY upload_year ASC, CAST(code_number AS UNSIGNED) ASC, id ASC';
$result = pwg_query($query);
$files = array();
$root = bratonien_tools_customer_qr_storage_root();
while ($row = pwg_db_fetch_assoc($result))
{
$year = (int)$row['upload_year'];
if (!isset($limits[$year]))
{
continue;
}
$number = bratonien_tools_customer_qr_number_for_year($year, $row['code_number']);
$stored_name = (string)$row['stored_name'];
if ($stored_name === '' || basename($stored_name) !== $stored_name)
{
continue;
}
$path = $root.DIRECTORY_SEPARATOR.$year.DIRECTORY_SEPARATOR.$stored_name;
if (!is_file($path) || !is_readable($path))
{
continue;
}
$extension = strtolower((string)pathinfo($stored_name, PATHINFO_EXTENSION));
if (!in_array($extension, array('png', 'jpg', 'jpeg', 'webp', 'gif'), true))
{
continue;
}
$width = max(2, strlen((string)$limits[$year]));
$archive_name = 'qr-'.str_pad($number, $width, '0', STR_PAD_LEFT).'.'.$extension;
if ($requested_year === 0)
{
$archive_name = $year.'/'.$archive_name;
}
$files[] = array(
'path' => $path,
'archive_name' => $archive_name,
);
}
if (empty($files))
{
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
exit('Keine QR-Codes für den Batch-Download vorhanden.');
}
$temp = tempnam(sys_get_temp_dir(), 'bratonien-qr-');
if ($temp === false)
{
http_response_code(500);
exit;
}
$zip = new ZipArchive();
if ($zip->open($temp, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true)
{
@unlink($temp);
http_response_code(500);
exit;
}
foreach ($files as $file)
{
$zip->addFile($file['path'], $file['archive_name']);
}
$zip->close();
$download_name = $requested_year > 0
? 'qr-codes-'.$requested_year.'.zip'
: 'qr-codes-alle-jahre.zip';
header('Content-Type: application/zip');
header('Content-Length: '.filesize($temp));
header('Content-Disposition: attachment; filename="'.$download_name.'"');
header('Cache-Control: private, no-store, max-age=0');
header('X-Content-Type-Options: nosniff');
readfile($temp);
@unlink($temp);
exit;
-70
View File
@@ -1,70 +0,0 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
http_response_code(404);
exit;
}
check_status(ACCESS_ADMINISTRATOR);
require_once(BRATONIEN_TOOLS_PATH.'include/customer_qr_upload.inc.php');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id < 1)
{
http_response_code(404);
exit;
}
$table = bratonien_tools_customer_qr_table();
$result = pwg_query('SELECT id, upload_year, code_number, stored_name, mime_type FROM `'.$table.'` WHERE id='.$id.' LIMIT 1');
$row = pwg_db_fetch_assoc($result);
if (!$row)
{
http_response_code(404);
exit;
}
$year = (int)$row['upload_year'];
$number = (string)$row['code_number'];
$stored_name = (string)$row['stored_name'];
$mime = strtolower((string)$row['mime_type']);
$allowed_mimes = array('image/png', 'image/jpeg', 'image/webp', 'image/gif');
if (
$stored_name === ''
|| basename($stored_name) !== $stored_name
|| !in_array($mime, $allowed_mimes, true)
)
{
http_response_code(404);
exit;
}
$path = bratonien_tools_customer_qr_storage_root().DIRECTORY_SEPARATOR.$year.DIRECTORY_SEPARATOR.$stored_name;
if (!is_file($path) || !is_readable($path))
{
http_response_code(404);
exit;
}
$extension = strtolower((string)pathinfo($stored_name, PATHINFO_EXTENSION));
$download = isset($_GET['download']) && (string)$_GET['download'] === '1';
$filename = 'qr-'.$year.'-'.$number.'.'.$extension;
header('Content-Type: '.$mime);
header('Content-Length: '.filesize($path));
header('Cache-Control: private, no-store, max-age=0');
header('X-Content-Type-Options: nosniff');
header('Content-Disposition: '.($download ? 'attachment' : 'inline').'; filename="'.$filename.'"');
readfile($path);
exit;
-584
View File
@@ -1,584 +0,0 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
http_response_code(404);
exit('Bratonien Tools ist nicht aktiv.');
}
require_once(BRATONIEN_TOOLS_PATH.'include/customer_qr_upload.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/friendship_code_upload.inc.php');
$settings = bratonien_tools_customer_qr_settings();
if (empty($settings['enabled']))
{
http_response_code(404);
header('Content-Type: text/html; charset=utf-8');
echo '<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>QR-Code Upload</title></head><body><main style="max-width:720px;margin:48px auto;padding:24px;font-family:system-ui,sans-serif"><h1>QR-Code Upload</h1><p>Dieses Formular ist derzeit nicht aktiviert.</p></main></body></html>';
exit;
}
bratonien_tools_customer_qr_ensure_storage();
bratonien_tools_friendship_code_ensure_storage();
$year_limits = bratonien_tools_customer_qr_year_limits();
$action = (string)($_GET['action'] ?? '');
function bratonien_tools_customer_qr_public_inventory($year)
{
$year = bratonien_tools_customer_qr_year($year);
$limits = bratonien_tools_customer_qr_year_limits();
$limit = (int)$limits[$year];
$present = array();
$table = bratonien_tools_customer_qr_table();
$result = pwg_query(
'SELECT code_number FROM `'.$table.'` '
.'WHERE upload_year='.(int)$year.' '
.'ORDER BY CAST(code_number AS UNSIGNED) ASC'
);
while ($row = pwg_db_fetch_assoc($result))
{
$number = (int)$row['code_number'];
if ($number >= 1 && $number <= $limit)
{
$present[$number] = true;
}
}
$slots = array();
for ($number = 1; $number <= $limit; $number++)
{
$slots[] = array(
'number' => $number,
'present' => isset($present[$number]),
);
}
$present_count = count($present);
return array(
'year' => $year,
'limit' => $limit,
'present_count' => $present_count,
'missing_count' => max(0, $limit - $present_count),
'slots' => $slots,
);
}
if ($action === 'inventory')
{
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
try
{
$inventory = bratonien_tools_customer_qr_public_inventory($_GET['year'] ?? '');
echo json_encode(array(
'ok' => true,
'inventory' => $inventory,
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
catch (Throwable $e)
{
http_response_code(400);
echo json_encode(array('ok' => false, 'message' => $e->getMessage()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
exit;
}
if ($action === 'check')
{
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
try
{
$year = bratonien_tools_customer_qr_year($_GET['year'] ?? '');
$number = bratonien_tools_customer_qr_number_for_year($year, $_GET['number'] ?? '');
$exists = bratonien_tools_customer_qr_exists($year, $number);
echo json_encode(array(
'ok' => true,
'available' => !$exists,
'year' => $year,
'number' => $number,
'limit' => (int)$year_limits[$year],
'message' => $exists
? 'Diese QR-Code-Nummer ist für '.$year.' bereits vorhanden.'
: 'Nummer ist für '.$year.' frei.',
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
catch (Throwable $e)
{
http_response_code(400);
echo json_encode(array('ok' => false, 'message' => $e->getMessage()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
exit;
}
if ($action === 'check_friendship')
{
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
try
{
$name = bratonien_tools_friendship_code_name($_GET['name'] ?? '');
$exists = bratonien_tools_friendship_code_exists_for_name($name);
echo json_encode(array(
'ok' => true,
'available' => !$exists,
'name' => $name,
'message' => $exists
? 'Für diesen Namen ist bereits ein Freundschaftscode hinterlegt.'
: 'Für diesen Namen kann ein Freundschaftscode hinterlegt werden.',
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
catch (Throwable $e)
{
http_response_code(400);
echo json_encode(array('ok' => false, 'message' => $e->getMessage()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
$results = array();
$year = bratonien_tools_customer_qr_default_year();
$upload_type = (string)($_POST['upload_type'] ?? 'qr');
try
{
if (
empty($_POST)
&& empty($_FILES)
&& !empty($_SERVER['CONTENT_LENGTH'])
)
{
throw new RuntimeException('Der Upload überschreitet das PHP-Limit post_max_size ('.ini_get('post_max_size').').');
}
if (function_exists('check_pwg_token'))
{
check_pwg_token();
}
if ($upload_type === 'friendship')
{
$name = bratonien_tools_friendship_code_name($_POST['friend_name'] ?? '');
if (empty($_FILES['friend_file']))
{
throw new RuntimeException('Es wurde keine Freundschaftscode-Datei ausgewählt.');
}
$results[] = bratonien_tools_friendship_code_process_upload($name, $_FILES['friend_file']);
}
else
{
$upload_type = 'qr';
$year = bratonien_tools_customer_qr_year($_POST['upload_year'] ?? '');
if (empty($_FILES['qr_files']))
{
throw new RuntimeException('Es wurden keine QR-Code-Dateien ausgewählt.');
}
$numbers = isset($_POST['qr_numbers']) && is_array($_POST['qr_numbers'])
? array_values($_POST['qr_numbers'])
: array();
$results = bratonien_tools_customer_qr_process_uploads($year, $_FILES['qr_files'], $numbers);
}
}
catch (Throwable $e)
{
$results[] = array(
'status' => 'error',
'kind' => $upload_type === 'friendship' ? 'friendship' : 'qr',
'file' => '',
'name' => $upload_type === 'friendship' ? trim((string)($_POST['friend_name'] ?? '')) : '',
'year' => $year,
'number' => '',
'message' => $e->getMessage(),
);
}
$_SESSION['bratonien_customer_qr_flash'] = array(
'type' => $upload_type,
'year' => $year,
'results' => $results,
);
$target = strtok($_SERVER['REQUEST_URI'], '?');
header('Location: '.$target, true, 303);
exit;
}
$flash = array();
if (!empty($_SESSION['bratonien_customer_qr_flash']) && is_array($_SESSION['bratonien_customer_qr_flash']))
{
$flash = $_SESSION['bratonien_customer_qr_flash'];
unset($_SESSION['bratonien_customer_qr_flash']);
}
$selected_year = isset($flash['year']) ? (int)$flash['year'] : bratonien_tools_customer_qr_default_year();
if (!isset($year_limits[$selected_year]))
{
$selected_year = bratonien_tools_customer_qr_default_year();
}
$selected_limit = (int)$year_limits[$selected_year];
$inventory = bratonien_tools_customer_qr_public_inventory($selected_year);
$results = isset($flash['results']) && is_array($flash['results']) ? $flash['results'] : array();
$token = function_exists('get_pwg_token') ? get_pwg_token() : '';
$max_files = max(1, (int)ini_get('max_file_uploads'));
$endpoint = strtok($_SERVER['REQUEST_URI'], '?');
header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-store, max-age=0');
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>QR-Code Upload</title>
<style>
:root{color-scheme:light dark;--bg:#111214;--card:#1a1c20;--text:#f2f2f2;--muted:#a9adb5;--border:#383c44;--accent:#d99a43;--ok:#67b779;--bad:#d46b6b;--wait:#c7a65a}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
main{max-width:860px;margin:0 auto;padding:34px 18px 60px}
h1{font-size:clamp(1.8rem,5vw,2.6rem);margin:0 0 8px}
h2{font-size:1.12rem;margin:0 0 12px}
p{margin:0 0 16px}.muted{color:var(--muted)}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:20px;margin-top:18px}
.grid{display:grid;grid-template-columns:minmax(0,180px) minmax(0,1fr);gap:14px 18px;align-items:center}
label{font-weight:650}.control{min-width:0}
select,input[type="text"],input[type="file"]{width:100%;font:inherit;color:inherit;background:#121418;border:1px solid var(--border);border-radius:9px;padding:11px 12px}
input[type="file"]{padding:9px}
input:focus,select:focus,button:focus{outline:2px solid var(--accent);outline-offset:2px}
.file-list{display:grid;gap:10px;margin-top:16px}
.file-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,180px) minmax(170px,220px);gap:10px;align-items:center;padding:12px;border:1px solid var(--border);border-radius:10px;background:#15171b}
.file-name{overflow-wrap:anywhere}.status{font-size:.92rem;color:var(--muted)}.status.ok{color:var(--ok)}.status.bad{color:var(--bad)}.status.wait{color:var(--wait)}
.actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:18px}
button{border:0;border-radius:9px;padding:12px 18px;background:var(--accent);color:#1a140d;font:inherit;font-weight:750;cursor:pointer}
button:disabled{opacity:.45;cursor:not-allowed}
.result{padding:12px 14px;border-radius:9px;margin-top:8px;border:1px solid var(--border)}
.result.ok{border-color:color-mix(in srgb,var(--ok),transparent 45%)}.result.duplicate,.result.error{border-color:color-mix(in srgb,var(--bad),transparent 45%)}
.result strong{display:block}.meta{color:var(--muted);font-size:.92rem}
.inventory{margin-top:20px;padding-top:18px;border-top:1px solid var(--border)}
.inventory-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:10px}
.inventory-head h2{margin:0}
.inventory-legend{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:12px;font-size:.92rem;color:var(--muted)}
.inventory-legend span{display:inline-flex;align-items:center;gap:6px}
.inventory-dot{width:14px;height:14px;border-radius:3px;border:1px solid}
.inventory-dot.present{background:rgba(62,170,94,.30);border-color:#58c878}
.inventory-dot.missing{background:rgba(205,72,72,.28);border-color:#db6d6d}
.inventory-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(42px,1fr));gap:6px}
.inventory-slot{min-height:38px;display:flex;align-items:center;justify-content:center;border:1px solid;border-radius:6px;font-weight:750}
.inventory-slot.present{background:rgba(62,170,94,.30);border-color:#58c878;color:#dff7e6}
.inventory-slot.missing{background:rgba(205,72,72,.28);border-color:#db6d6d;color:#ffdede}
@media(max-width:680px){.grid{grid-template-columns:1fr}.file-row{grid-template-columns:1fr}.card{padding:16px}.inventory-grid{grid-template-columns:repeat(auto-fill,minmax(38px,1fr))}}
</style>
</head>
<body>
<main>
<header>
<h1>QR-Code Upload</h1>
<p class="muted">QR-Codes und persönliche Freundschaftscodes hochladen.</p>
</header>
<?php if (!empty($results)): ?>
<section class="card" aria-labelledby="upload-result-title">
<h2 id="upload-result-title">Ergebnis</h2>
<?php foreach ($results as $result): ?>
<?php
$status = in_array(($result['status'] ?? ''), array('ok','duplicate','error'), true) ? $result['status'] : 'error';
$kind = (string)($result['kind'] ?? 'qr');
if ($status === 'ok') $headline = 'Gespeichert';
elseif ($status === 'duplicate') $headline = $kind === 'friendship' ? 'Nicht gespeichert: bereits vorhanden' : 'Nicht gespeichert: Nummer bereits vergeben';
else $headline = 'Nicht gespeichert';
?>
<div class="result <?php echo htmlspecialchars($status, ENT_QUOTES, 'UTF-8'); ?>">
<strong><?php echo htmlspecialchars($headline, ENT_QUOTES, 'UTF-8'); ?></strong>
<?php if (!empty($result['file'])): ?><div><?php echo htmlspecialchars($result['file'], ENT_QUOTES, 'UTF-8'); ?></div><?php endif; ?>
<?php if ($kind === 'friendship' && !empty($result['name'])): ?><div class="meta">Name: <?php echo htmlspecialchars($result['name'], ENT_QUOTES, 'UTF-8'); ?></div><?php endif; ?>
<?php if ($kind !== 'friendship' && !empty($result['number'])): ?><div class="meta">Jahr <?php echo (int)$result['year']; ?> · QR-Code #<?php echo htmlspecialchars($result['number'], ENT_QUOTES, 'UTF-8'); ?></div><?php endif; ?>
<div class="meta"><?php echo htmlspecialchars((string)($result['message'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></div>
</div>
<?php endforeach; ?>
</section>
<?php endif; ?>
<form class="card" method="post" enctype="multipart/form-data" id="qr-upload-form" novalidate>
<input type="hidden" name="pwg_token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="upload_type" value="qr">
<h2>QR-Code</h2>
<div class="grid">
<label for="upload-year">Jahr</label>
<div class="control">
<select id="upload-year" name="upload_year" required>
<?php foreach ($year_limits as $year => $limit): ?>
<option value="<?php echo (int)$year; ?>"<?php echo (int)$year === $selected_year ? ' selected' : ''; ?>><?php echo (int)$year; ?></option>
<?php endforeach; ?>
</select>
<div id="year-limit-note" class="muted" style="margin-top:6px">Für <?php echo $selected_year; ?> sind QR-Code-Nummern 1 bis <?php echo $selected_limit; ?> vorgesehen.</div>
</div>
<label for="qr-files">QR-Code-Datei(en)</label>
<div class="control">
<input id="qr-files" name="qr_files[]" type="file" accept="image/png,image/jpeg,image/webp,image/gif" multiple required>
<div class="muted" style="margin-top:6px">PNG, JPG, WEBP oder GIF. Batch-Upload bis zum Serverlimit von <?php echo $max_files; ?> Dateien pro Anfrage.</div>
</div>
</div>
<section class="inventory" aria-labelledby="inventory-title">
<div class="inventory-head">
<h2 id="inventory-title">QR-Code-Bestand <span id="inventory-year"><?php echo (int)$inventory['year']; ?></span></h2>
<span id="inventory-count" class="muted"><?php echo (int)$inventory['present_count']; ?> vorhanden · <?php echo (int)$inventory['missing_count']; ?> fehlen</span>
</div>
<div class="inventory-legend" aria-hidden="true">
<span><i class="inventory-dot present"></i>Vorhanden</span>
<span><i class="inventory-dot missing"></i>Fehlt</span>
</div>
<div id="inventory-grid" class="inventory-grid" aria-live="polite">
<?php foreach ($inventory['slots'] as $slot): ?>
<span class="inventory-slot <?php echo !empty($slot['present']) ? 'present' : 'missing'; ?>" title="QR-Code #<?php echo (int)$slot['number']; ?> <?php echo !empty($slot['present']) ? 'vorhanden' : 'fehlt'; ?>"><?php echo (int)$slot['number']; ?></span>
<?php endforeach; ?>
</div>
</section>
<div id="file-list" class="file-list" aria-live="polite"></div>
<div class="actions">
<button id="submit-button" type="submit" disabled>QR-Code(s) hochladen</button>
<span class="muted">Eine Nummer kann innerhalb eines Jahres nur einmal verwendet werden.</span>
</div>
</form>
<form class="card" method="post" enctype="multipart/form-data" id="friendship-upload-form" novalidate>
<input type="hidden" name="pwg_token" value="<?php echo htmlspecialchars($token, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="upload_type" value="friendship">
<h2>Persönlicher Freundschaftscode</h2>
<div class="grid">
<label for="friend-name">Name</label>
<div class="control">
<input id="friend-name" name="friend_name" type="text" maxlength="120" autocomplete="name" required>
<div id="friend-name-status" class="status" style="margin-top:6px">Bitte Namen eingeben.</div>
</div>
<label for="friend-file">Freundschaftscode</label>
<div class="control">
<input id="friend-file" name="friend_file" type="file" accept="image/png,image/jpeg,image/webp,image/gif" required>
<div class="muted" style="margin-top:6px">Ein Bild pro Person. PNG, JPG, WEBP oder GIF.</div>
</div>
</div>
<div class="actions">
<button id="friend-submit-button" type="submit" disabled>Freundschaftscode hochladen</button>
<span class="muted">Pro Name wird genau ein persönlicher Freundschaftscode gespeichert.</span>
</div>
</form>
</main>
<script>
(function(){
'use strict';
var form=document.getElementById('qr-upload-form');
var fileInput=document.getElementById('qr-files');
var yearInput=document.getElementById('upload-year');
var yearNote=document.getElementById('year-limit-note');
var list=document.getElementById('file-list');
var submit=document.getElementById('submit-button');
var inventoryYear=document.getElementById('inventory-year');
var inventoryCount=document.getElementById('inventory-count');
var inventoryGrid=document.getElementById('inventory-grid');
var endpoint=<?php echo json_encode($endpoint, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
var limits=<?php echo json_encode($year_limits, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
var timers=new WeakMap();
function yearLimit(){return Number(limits[String(yearInput.value)]||limits[yearInput.value]||0);}
function canonical(value){
value=String(value||'').trim();
if(!/^\d+$/.test(value))return '';
value=value.replace(/^0+(?=\d)/,'');
if(value==='0')return '';
return value;
}
function rows(){return Array.prototype.slice.call(list.querySelectorAll('.file-row'));}
function setStatus(row,text,kind){
var node=row.querySelector('.status');
node.textContent=text;
node.className='status'+(kind?' '+kind:'');
row.dataset.valid=kind==='ok'?'1':'0';
updateSubmit();
}
function updateSubmit(){
var all=rows();
submit.disabled=!all.length||all.some(function(row){return row.dataset.valid!=='1';});
}
function renderInventory(inventory){
inventoryYear.textContent=String(inventory.year);
inventoryCount.textContent=String(inventory.present_count)+' vorhanden · '+String(inventory.missing_count)+' fehlen';
inventoryGrid.textContent='';
(inventory.slots||[]).forEach(function(slot){
var node=document.createElement('span');
node.className='inventory-slot '+(slot.present?'present':'missing');
node.textContent=String(slot.number);
node.title='QR-Code #'+slot.number+' '+(slot.present?'vorhanden':'fehlt');
inventoryGrid.appendChild(node);
});
}
function loadInventory(){
inventoryCount.textContent='Bestand wird geladen …';
var requestedYear=yearInput.value;
var url=endpoint+'?action=inventory&year='+encodeURIComponent(requestedYear)+'&_='+Date.now();
fetch(url,{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
.then(function(response){return response.json().then(function(data){return {response:response,data:data};});})
.then(function(result){
if(!result.response.ok||!result.data.ok||!result.data.inventory)throw new Error(result.data.message||'Bestand konnte nicht geladen werden.');
if(String(yearInput.value)!==String(requestedYear))return;
renderInventory(result.data.inventory);
})
.catch(function(error){inventoryCount.textContent=error.message||'Bestand konnte nicht geladen werden.';});
}
function markBatchDuplicates(){
var grouped={};
rows().forEach(function(row){
var input=row.querySelector('input[name="qr_numbers[]"]');
var value=canonical(input.value);
if(!value)return;
if(!grouped[value])grouped[value]=[];
grouped[value].push(row);
});
Object.keys(grouped).forEach(function(key){
if(grouped[key].length>1){
grouped[key].forEach(function(row){setStatus(row,'Nummer ist in diesem Batch doppelt.','bad');});
}
});
return grouped;
}
function checkRow(row){
var input=row.querySelector('input[name="qr_numbers[]"]');
var value=canonical(input.value);
var limit=yearLimit();
if(!value){setStatus(row,'Bitte eine QR-Code-Nummer ab 1 eingeben.','bad');return;}
if(!limit||Number(value)>limit){setStatus(row,'Für '+yearInput.value+' sind nur die Nummern 1 bis '+limit+' vorgesehen.','bad');return;}
var grouped=markBatchDuplicates();
if(grouped[value]&&grouped[value].length>1)return;
setStatus(row,'Nummer wird geprüft …','wait');
var url=endpoint+'?action=check&year='+encodeURIComponent(yearInput.value)+'&number='+encodeURIComponent(value)+'&_='+Date.now();
fetch(url,{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
.then(function(response){return response.json().then(function(data){return {response:response,data:data};});})
.then(function(result){
if(!result.response.ok||!result.data.ok)throw new Error(result.data.message||'Prüfung fehlgeschlagen.');
if(result.data.available)setStatus(row,result.data.message,'ok');
else setStatus(row,result.data.message,'bad');
})
.catch(function(error){setStatus(row,error.message||'Prüfung fehlgeschlagen.','bad');});
}
function scheduleCheck(row){
var old=timers.get(row);if(old)window.clearTimeout(old);
timers.set(row,window.setTimeout(function(){checkRow(row);},250));
}
function scheduleAll(){rows().forEach(function(row){scheduleCheck(row);});}
function updateYearNote(){
yearNote.textContent='Für '+yearInput.value+' sind QR-Code-Nummern 1 bis '+yearLimit()+' vorgesehen.';
}
function rebuild(){
list.textContent='';
Array.prototype.slice.call(fileInput.files||[]).forEach(function(file,index){
var row=document.createElement('div');row.className='file-row';row.dataset.valid='0';
var name=document.createElement('div');name.className='file-name';name.textContent=file.name;
var input=document.createElement('input');input.type='text';input.name='qr_numbers[]';input.inputMode='numeric';input.autocomplete='off';input.placeholder='QR-Nummer';input.setAttribute('aria-label','QR-Code-Nummer für '+file.name);input.maxLength=3;input.required=true;
var status=document.createElement('div');status.className='status';status.textContent='Nummer fehlt.';
row.appendChild(name);row.appendChild(input);row.appendChild(status);list.appendChild(row);
input.addEventListener('input',scheduleAll);
if(index===0)window.setTimeout(function(){input.focus();},0);
});
updateSubmit();
}
fileInput.addEventListener('change',rebuild);
yearInput.addEventListener('change',function(){updateYearNote();scheduleAll();loadInventory();});
form.addEventListener('submit',function(event){
var invalid=rows().some(function(row){return row.dataset.valid!=='1';});
if(invalid){event.preventDefault();scheduleAll();}
else{submit.disabled=true;submit.textContent='Upload läuft …';}
});
})();
(function(){
'use strict';
var form=document.getElementById('friendship-upload-form');
var nameInput=document.getElementById('friend-name');
var fileInput=document.getElementById('friend-file');
var status=document.getElementById('friend-name-status');
var submit=document.getElementById('friend-submit-button');
var endpoint=<?php echo json_encode($endpoint, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); ?>;
var timer=null;
var nameValid=false;
function updateSubmit(){
submit.disabled=!nameValid||!fileInput.files||fileInput.files.length!==1;
}
function setStatus(text,kind){
status.textContent=text;
status.className='status'+(kind?' '+kind:'');
nameValid=kind==='ok';
updateSubmit();
}
function checkName(){
var name=String(nameInput.value||'').trim().replace(/\s+/g,' ');
if(!name){setStatus('Bitte Namen eingeben.','bad');return;}
setStatus('Name wird geprüft …','wait');
var url=endpoint+'?action=check_friendship&name='+encodeURIComponent(name)+'&_='+Date.now();
fetch(url,{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
.then(function(response){return response.json().then(function(data){return {response:response,data:data};});})
.then(function(result){
if(!result.response.ok||!result.data.ok)throw new Error(result.data.message||'Prüfung fehlgeschlagen.');
if(result.data.available)setStatus(result.data.message,'ok');
else setStatus(result.data.message,'bad');
})
.catch(function(error){setStatus(error.message||'Prüfung fehlgeschlagen.','bad');});
}
nameInput.addEventListener('input',function(){
nameValid=false;updateSubmit();
if(timer)window.clearTimeout(timer);
timer=window.setTimeout(checkName,250);
});
fileInput.addEventListener('change',updateSubmit);
form.addEventListener('submit',function(event){
if(!nameValid||!fileInput.files||fileInput.files.length!==1){event.preventDefault();checkName();return;}
submit.disabled=true;submit.textContent='Upload läuft …';
});
})();
</script>
</body>
</html>
-111
View File
@@ -1,111 +0,0 @@
# HANDOFF – Piwigo / WebDAV Cache-Worker – 2026-09-03
## Warum hier gestoppt wurde
Die aktuelle Cache-Worker-Entwicklung ist in einen Architektur-/Debugging-Kreis geraten. Der beobachtete produktive Zustand ist eindeutig falsch: Nach dem Leeren des Bildcaches meldet der manuelle Cache-Aufbau praktisch sofort "fertig", obwohl der eigentliche Piwigo-Bildcache nicht aufgebaut wurde. Weitere Patchversuche an Anzeige, Statusaggregation oder Legacy-Worker-Zählung würden den Fehler nur weiter überdecken.
Ab hier **nicht weiterpatchen**, bevor der reale Ausführungspfad von Button bis Piwigo-Derivaterzeugung vollständig nachvollzogen ist.
## Gesicherte Architekturvorgaben
- Nextcloud ist die autoritative Quelle der Originalbilder.
- Piwigo arbeitet produktiv mit einem Shadow-Tree und 1x1-Placeholdern.
- Der Worker darf Piwigo nicht nachbauen und selbst keine Derivate rendern.
- Richtiger Ablauf: Worker erkennt Arbeit -> Original temporär materialisieren -> Piwigo nativen Derivatpfad aufrufen -> Ergebnis prüfen -> Original wieder entfernen / Placeholder restaurieren.
- Piwigo allein bestimmt Dimensionen, Crop, Sharpening, Wasserzeichen, Qualität, Format und Cachepfad.
- On-Demand bleibt Fallback.
- Materialisierung erfolgt batchweise, Standard 10 Originale pro Batch.
- Stage 1 vor Stage 2; neue Sync-Arbeit darf Stage 2 zwischen Batches priorisieren.
- Connector/Shadow-Tree bleibt Wahrheit darüber, welche Quellen existieren.
## Neuer Quellenindex – Konzept weiterhin sinnvoll
Die Trennung des Worker-Entscheidungszustands vom Piwigo-Cache ist weiterhin sinnvoll:
- eigener kleiner Quellenindex je WebDAV-Verbindung
- Identität primär über `fileid`, Fallback Root/Pfad
- Vergleich über `etag`, Dateigröße, MIME/Format, Pfad, Breite/Höhe
- Quelle neu/geändert -> Worker darf Piwigo beauftragen
- Quelle unverändert -> normaler periodischer Lauf tut nichts
- gelöschte Quelle -> beim Abgleich aus Worker-Index entfernen; eigentliche Quellenentfernung bleibt Connector/Shadow-Tree-Aufgabe
- manueller Full-Rebuild darf den Quellenindex bewusst ignorieren und alle aktuellen Quellen erneut an Piwigo reichen
Datei dafür aktuell:
`include/webdav_source_index.inc.php`
## Aktueller problematischer Stand
### Version
Aktuell zuletzt auf `0.9.7.1.16` erhöht.
### Relevante Dateien
- `runtime/lib/webdav-cache-warmup.php`
- `runtime/webdav-warmup-dispatch.php`
- `include/webdav_source_index.inc.php`
- `include/webdav_warmup_settings.inc.php`
- `main-cache-build.php`
- `main-cache-status.php`
- `tools/image_cache.inc.php`
- `template/admin.tpl`
- `include/tool_registry.inc.php`
### Letzte Architekturänderungen
1. Legacy `main-cache-build.php` zählt WebDAV-Bilder nicht mehr als normale lokale Cachearbeit.
2. Eigener WebDAV-Quellenindex wurde eingeführt.
3. Warmup-Auswahl wurde auf diesen Quellenindex umgestellt.
4. Kombinierter Admin-Button startet lokalen Piwigo-Teil plus WebDAV-Rebuild.
5. Statusanzeige wurde zusammengeführt.
6. Bei reiner WebDAV-Installation sollte der lokale Legacy-Worker gar nicht mehr gestartet werden.
Trotzdem beobachtet der Nutzer nach Cache-Leeren weiterhin einen praktisch sofort fertigen Aufbau. Das beweist: **Der tatsächliche WebDAV-Rebuild-Pfad erzeugt die erwarteten Piwigo-Derivate nicht bzw. läuft nicht so, wie die Architektur es annimmt.**
## Wichtige letzte Commits
- `ac6353b050cdf30db79ab20716e66e6a6c841abb` – eigener WebDAV-Quellenindex
- `6783974c7dfab9d11a0f1e43419de84e92141fd5` – Warmup auf Quellenindex umgestellt
- `9f80c4aa974da01d606292c479487bce13b403fa` – Quelle erst nach beiden Stufen vollständig
- `030d0c1be7ef8cafdab3f3d46e01ec90d5fd3bdc` – kombinierte Statusanzeige
- `e831301650ee2a80c969e6c9b783d37a3c4f593d` – Syntaxkorrektur Status
- `479cdbc4734605db3861908286426c76468d12a3` – Quellenindex-Baseline blockiert Aktivierung nicht mehr
- `116b710d5d35311eeae261e5ae3cf405c5f51ce3` – Legacy-Worker bei reiner WebDAV-Installation nicht mehr starten
- `81e0a9e185a14c08230b4f8fa0f36809c5b2b9a7` – Version 0.9.7.1.16
## NICHT weiter tun
- Keine weiteren UI-/Statuspatches, bevor der echte Workerpfad bewiesen ist.
- Nicht wieder Cache-Dateiexistenz als Quelle für "neu/geändert" verwenden.
- Nicht den Placeholder selbst rendern.
- Nicht den Legacy-Full-Cache-Builder für WebDAV reaktivieren.
- Nicht weiter an Symptomen wie 45-Sekunden-Stall, "übersprungen" oder Prozentanzeige herumoptimieren.
## Nächster Einstieg – zwingend
Der nächste Chat/Entwickler soll **zuerst den tatsächlichen Rebuild-Ausführungspfad beweisen**:
1. Admin-Handler `image_cache_build` verfolgen.
2. `bratonien_tools_start_combined_image_cache_build()` verfolgen.
3. Start des `runtime/webdav-warmup-dispatch.php --mode=rebuild` beweisen.
4. Verifizieren, dass Dispatcher die aktive WebDAV-Verbindung findet.
5. Verifizieren, dass `runtime/lib/webdav-cache-warmup.php --mode=rebuild` wirklich gestartet wird.
6. Verifizieren, wie viele Quellen `bratonien_tools_cache_warmup_scan()` tatsächlich liefert.
7. Verifizieren, wie viele Varianten Stage 1 und Stage 2 pro Bild tatsächlich liefern.
8. Erst an **einem einzigen Bild** nachweisen:
- Original wird aus Nextcloud heruntergeladen.
- Placeholder wird korrekt gesichert.
- Original wird am produktiven Quellpfad eingesetzt.
- `piwigo-derivative-call.php` ruft wirklich Piwigos nativen `i.php`-Pfad auf.
- Piwigo erzeugt tatsächlich eine konkrete Datei unter `_data/i`.
- strikter Validator erkennt sie als gültig.
- Placeholder wird exakt restauriert.
9. Erst wenn dieser Ein-Bild-Test nachweislich funktioniert, Batchbetrieb und UI wieder anfassen.
## Entscheidender Verdacht
Der derzeit wichtigste Verdacht ist **nicht mehr die Anzeige**, sondern dass der Rebuild intern sehr früh keine Arbeit findet oder Piwigo über den aufgerufenen `i.php`-Pfad keine Derivate erzeugt, obwohl der Worker dies erwartet. Genau diese Stelle muss mit realem Lauf/Log belegt werden.
## Produktionsvalidierung
Von ChatGPT wurde kein echter Serverlauf durchgeführt. Ohne reale Serverausgaben darf nicht behauptet werden, dass Pfadaudit, Materialisierung, Piwigo-Aufruf oder Cache-Erzeugung funktionieren.
-68
View File
@@ -1,68 +0,0 @@
# WebDAV-Cache-Warmup – Patch 0.9.7.1.8
## Ziel
Fehlende Piwigo-Derivate für neue Nextcloud-Inhalte im Hintergrund vorbereiten, ohne einen zweiten dauerhaften Bildbestand anzulegen und ohne Piwigo-Datenbankeinträge, Albumzuordnungen oder Bildpfade durch Bratonien Tools zu verändern.
## Zuständigkeiten
Bratonien Tools übernimmt ausschließlich Orchestrierung:
1. neue Alben bzw. neue/geänderte Bilder erkennen;
2. Originale in konfigurierbaren Batches temporär aus Nextcloud laden;
3. die bereits bestehende Piwigo-Quellzuordnung validieren;
4. die lokale Quelle für genau ein Bild atomar bereitstellen;
5. Piwigos eigenes `i.php` für die gewünschten, von Piwigo definierten Derivate aufrufen;
6. den ursprünglichen Placeholder exakt wiederherstellen und verifizieren;
7. temporäre Batch-Dateien löschen.
Piwigo bleibt allein zuständig für Derivatgrößen, Bildverarbeitung, Qualität, Format und Cache-Dateien.
## Trigger
- Neues Album: unmittelbar nach vollständig erfolgreichem Connector-Sync dieser Verbindung (`sync`-Modus).
- Einzelne neue/geänderte Bilder in bekannten Alben: periodische Eingangsprüfung, Standard 12 Stunden (`periodic`-Modus).
- Administration: `Jetzt auf neue Bilder prüfen` (`manual`-Modus).
- On-Demand bleibt parallel aktiv und wird nicht durch den Warmup ersetzt.
## Batches und Stufen
- Batchgröße ist konfigurierbar, Standard **10 Bilder**.
- Ein Batch wird zuerst vollständig aus Nextcloud geladen.
- Anschließend werden die Bilder einzeln am bereits bestehenden Piwigo-Pfad bereitgestellt und von Piwigo verarbeitet.
- Stufe 1: von Piwigo definierte Standard-/Custom-Derivate bis einschließlich 1920 px längster Kante.
- Erst wenn Stufe 1 abgearbeitet ist, folgt Stufe 2 mit den übrigen von Piwigo definierten Derivaten.
## Schutz produktiver Daten
Der Warmup darf keine Piwigo-Bild-/Albumdatensätze schreiben oder neue Piwigo-Sites/Zuordnungen anlegen.
Vor jedem Lauf wird die bestehende Pfadkette von Piwigo aus validiert:
`Piwigo image.id -> images.path -> realpath() -> nc-webdav-source/connection-ID/root-fileid -> webdav-map.json`
Unsichere oder widersprüchliche Zuordnungen führen zum Abbruch, nicht zu einer Rekonstruktion auf Verdacht.
Zusätzliche Sicherungen:
- vorhandener per-image Lock `upload/bratonien-webdav-materialize/image-ID.lock` wird auch vom Warmup benutzt;
- `webdav-sync.lock` wird für den gesamten Warmup-Lauf geteilt gehalten; der Connector-Sync benötigt denselben Lock exklusiv;
- zwei Piwigo-IDs auf derselben physischen Connector-Quelle führen zum Sicherheitsabbruch;
- Placeholder wird nie in-place überschrieben;
- Swap erfolgt über Dateien im selben Verzeichnis und `rename()`;
- Größe und SHA-1 des Placeholders werden vor dem Swap erfasst;
- nach jedem Restore müssen Größe und SHA-1 exakt dem Ausgangszustand entsprechen;
- Restore-Fehler beendet den Worker sofort als fatal;
- bestehende produktive Daten werden beim ersten Lauf nur als Baseline erfasst und nicht automatisch neu aufgebaut.
## Patchphase
Die automatische Warmup-Funktion ist in 0.9.7.1.8 standardmäßig **deaktiviert**. Vor Aktivierung sind vorgesehen:
1. Plugin aktualisieren;
2. schreibgeschützten `Produktive Pfade prüfen`-Audit ausführen;
3. ersten manuellen Lauf durchführen, der den vorhandenen Bestand nur als Baseline erfasst;
4. gezielten Test mit neuem Testalbum / neuen Bildern;
5. erst nach bestätigtem Restore- und Piwigo-Cache-Verhalten Automatik aktivieren.
Die Entwicklung bleibt bis zum vollständig validierten Endstand in Patch-Versionen. Die geplante Finalversion ist **0.10.1**.
-74
View File
@@ -1,74 +0,0 @@
<?php
$piwigo_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false)
{
http_response_code(500);
exit;
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
http_response_code(404);
exit;
}
check_status(ACCESS_ADMINISTRATOR);
require_once(BRATONIEN_TOOLS_PATH.'include/friendship_code_upload.inc.php');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id < 1)
{
http_response_code(404);
exit;
}
$table = bratonien_tools_friendship_code_table();
$result = pwg_query('SELECT id, person_name, stored_name, mime_type FROM `'.$table.'` WHERE id='.$id.' LIMIT 1');
$row = pwg_db_fetch_assoc($result);
if (!$row)
{
http_response_code(404);
exit;
}
$stored_name = (string)$row['stored_name'];
$mime = strtolower((string)$row['mime_type']);
$allowed_mimes = array('image/png', 'image/jpeg', 'image/webp', 'image/gif');
if ($stored_name === '' || basename($stored_name) !== $stored_name || !in_array($mime, $allowed_mimes, true))
{
http_response_code(404);
exit;
}
$path = bratonien_tools_friendship_code_storage_root().DIRECTORY_SEPARATOR.$stored_name;
if (!is_file($path) || !is_readable($path))
{
http_response_code(404);
exit;
}
$extension = strtolower((string)pathinfo($stored_name, PATHINFO_EXTENSION));
if ($extension === '')
{
$extension = 'png';
}
header('Content-Type: '.$mime);
header('Content-Length: '.filesize($path));
header('Cache-Control: private, no-store, max-age=0');
header('X-Content-Type-Options: nosniff');
if (!empty($_GET['download']))
{
header('Content-Disposition: attachment; filename="freundschaftscode-'.$id.'.'.$extension.'"');
}
else
{
header('Content-Disposition: inline; filename="freundschaftscode-'.$id.'.'.$extension.'"');
}
readfile($path);
exit;
-342
View File
@@ -1,342 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_atomic_cache_remove_tree($root, array &$failed)
{
if (!file_exists($root) && !is_link($root)) return;
// Niemals einem Symlink folgen. Auch ein Link auf ein Verzeichnis ist nur
// ein einzelner Eintrag des ausgelagerten Cachebaums und wird per unlink()
// entfernt. So kann das Cleanup kein Ziel ausserhalb des Cachebaums beruehren.
if (is_link($root))
{
if (!@unlink($root)) $failed[] = $root;
return;
}
if (is_file($root))
{
if (!@unlink($root)) $failed[] = $root;
return;
}
if (!is_dir($root))
{
$failed[] = $root;
return;
}
$entries = @scandir($root);
if ($entries === false)
{
$failed[] = $root;
return;
}
foreach ($entries as $entry)
{
if ($entry === '.' || $entry === '..') continue;
$path = $root.DIRECTORY_SEPARATOR.$entry;
if (is_link($path) || is_file($path))
{
if (!@unlink($path)) $failed[] = $path;
continue;
}
if (is_dir($path))
{
bratonien_tools_atomic_cache_remove_tree($path, $failed);
continue;
}
// Sonderdateien werden wie einzelne Cacheeintraege behandelt. unlink()
// ist hier sicherer als ein rekursiver Iterator, der Verweise interpretieren
// koennte.
if (!@unlink($path)) $failed[] = $path;
}
if (!@rmdir($root) && is_dir($root)) $failed[] = $root;
}
function bratonien_tools_atomic_cache_acquire_lock($path, $label, $timeout_seconds=10.0)
{
$directory = dirname($path);
if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory))
{
throw new RuntimeException($label.'-Lockverzeichnis konnte nicht angelegt werden.');
}
$handle = @fopen($path, 'c');
if (!$handle)
{
throw new RuntimeException($label.'-Lock konnte nicht geöffnet werden: '.$path);
}
$deadline = microtime(true) + max(0.1, (float)$timeout_seconds);
do
{
if (@flock($handle, LOCK_EX | LOCK_NB))
{
return $handle;
}
usleep(100000);
}
while (microtime(true) < $deadline);
fclose($handle);
throw new RuntimeException($label.' ist noch aktiv. Der Cache wurde nicht angefasst; bitte den laufenden Vorgang kurz abschließen lassen und erneut leeren.');
}
function bratonien_tools_atomic_cache_release_locks(array &$locks)
{
foreach (array_reverse($locks) as $handle)
{
if (!is_resource($handle)) continue;
@flock($handle, LOCK_UN);
fclose($handle);
}
$locks = array();
}
function bratonien_tools_atomic_cache_prepare_webdav_guards(array &$locks, array &$cancel_files)
{
if (!function_exists('bratonien_tools_webdav_warmup_connections')) return 0;
$connections = bratonien_tools_webdav_warmup_connections();
if (!$connections) return 0;
ksort($connections, SORT_NUMERIC);
// Jeder aktive WebDAV-Worker bekommt zuerst sein reguläres Abbruchsignal.
// Ein bereits gestarteter 10er-Batch darf sauber fertig werden; ein neuer
// Batch darf danach nicht mehr beginnen.
if (function_exists('bratonien_tools_request_webdav_cache_cancel'))
{
bratonien_tools_request_webdav_cache_cancel();
}
// Das Signal wird für alle Verbindungen gesetzt, auch wenn deren Statusdatei
// veraltet sein sollte. Sobald wir anschließend den Prozess-Lock besitzen,
// kann garantiert kein WebDAV-Worker während des Cache-Leerens schreiben.
foreach ($connections as $connection_id=>$runtime)
{
$cancel = function_exists('bratonien_tools_webdav_warmup_cancel_file_for_connection')
? bratonien_tools_webdav_warmup_cancel_file_for_connection($connection_id)
: PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.cancel-'.(int)$connection_id;
if (@file_put_contents($cancel, (string)time()."\n", LOCK_EX) === false)
{
throw new RuntimeException('WebDAV-Abbruchsignal für Verbindung #'.(int)$connection_id.' konnte nicht geschrieben werden.');
}
@chmod($cancel, 0664);
$cancel_files[] = $cancel;
}
foreach ($connections as $connection_id=>$runtime)
{
$state_dir = rtrim((string)($runtime['state_dir'] ?? ''), '/');
if ($state_dir === '')
{
throw new RuntimeException('WebDAV-State-Verzeichnis für Verbindung #'.(int)$connection_id.' fehlt.');
}
$locks[] = bratonien_tools_atomic_cache_acquire_lock(
$state_dir.'/webdav-cache-warmup.lock',
'WebDAV-Cache-Worker #'.(int)$connection_id,
10.0
);
}
return count($connections);
}
function bratonien_tools_atomic_cache_prepare_presentation_guard(array &$locks)
{
if (!function_exists('bratonien_tools_presentation_refresh_queue_dir')) return false;
$queue_dir = bratonien_tools_presentation_refresh_queue_dir();
if (!is_dir($queue_dir)) return false;
// Der Presentation-Worker erzeugt Bratonien-Wasserzeichen-Vorschauen im
// selben Derivatbaum. Deshalb muss auch er während des atomaren Leerens
// vollständig draußen bleiben.
$locks[] = bratonien_tools_atomic_cache_acquire_lock(
rtrim($queue_dir, '/').'/.worker.lock',
'Vorschau-Aktualisierung',
10.0
);
return true;
}
function bratonien_tools_clear_image_cache_atomic()
{
global $conf;
if (!defined('PWG_DERIVATIVE_DIR'))
{
throw new RuntimeException('PWG_DERIVATIVE_DIR ist nicht definiert.');
}
$held_locks = array();
$webdav_cancel_files = array();
$webdav_guards_ready = false;
try
{
// 1. Lokalen Cache-Builder sauber anhalten und seinen Prozess-Lock selbst
// halten. Damit kann zwischen Prüfung und atomarem Rename kein neuer
// lokaler Builder starten.
if (bratonien_tools_main_cache_process_active() || bratonien_tools_main_cache_is_running())
{
bratonien_tools_request_main_cache_cancel();
if (!bratonien_tools_wait_main_cache_stopped(10.0))
{
throw new RuntimeException('Der laufende Cache-Aufbau konnte noch nicht beendet werden. Bitte den Abbruch kurz abschließen lassen und erneut leeren.');
}
}
@unlink(bratonien_tools_main_cache_cancel_file());
$held_locks[] = bratonien_tools_atomic_cache_acquire_lock(
bratonien_tools_main_cache_lock_file(),
'Lokaler Piwigo-Cache-Worker',
10.0
);
// 2. WebDAV-Warmup regulär abbrechen und danach alle Verbindungs-Locks
// exklusiv halten. Der laufende Batch wird nicht hart beendet.
bratonien_tools_atomic_cache_prepare_webdav_guards($held_locks, $webdav_cancel_files);
$webdav_guards_ready = true;
// 3. Seit der Presentation-Refresh-Einführung kann auch dieser Worker
// Derivate erzeugen. Ohne seinen Lock konnte ein gerade geleerter Cache
// sofort wieder gefüllt werden und wie ein fehlgeschlagenes Löschen wirken.
bratonien_tools_atomic_cache_prepare_presentation_guard($held_locks);
$piwigo_root = realpath(PHPWG_ROOT_PATH);
if ($piwigo_root === false)
{
throw new RuntimeException('Piwigo-Root konnte für die Cache-Sicherheitsprüfung nicht aufgelöst werden.');
}
$cache_root = rtrim(PHPWG_ROOT_PATH, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.trim(PWG_DERIVATIVE_DIR, '/\\');
$real_cache_root = realpath($cache_root);
if ($real_cache_root === false || !is_dir($real_cache_root))
{
throw new RuntimeException('Bildcache-Verzeichnis wurde nicht gefunden: '.$cache_root);
}
$root_prefix = rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
if (strpos(rtrim($real_cache_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR, $root_prefix) !== 0)
{
throw new RuntimeException('Bildcache liegt außerhalb der Piwigo-Installation. Abbruch.');
}
if (rtrim($real_cache_root, DIRECTORY_SEPARATOR) === rtrim($piwigo_root, DIRECTORY_SEPARATOR))
{
throw new RuntimeException('Bildcache-Pfad entspricht dem Piwigo-Root. Sicherheitsabbruch.');
}
$before = bratonien_tools_scan_image_cache($real_cache_root);
$parent = dirname($real_cache_root);
if (!is_dir($parent) || !is_writable($parent))
{
throw new RuntimeException('Übergeordnetes Bildcache-Verzeichnis ist nicht beschreibbar: '.$parent);
}
$detached = $parent.'/.'.basename($real_cache_root).'.bratonien-clear-'.date('YmdHis').'-'.bin2hex(random_bytes(4));
if (file_exists($detached) || is_link($detached))
{
throw new RuntimeException('Temporäres Cache-Auslagerungsverzeichnis existiert bereits. Abbruch.');
}
if (!@rename($real_cache_root, $detached))
{
throw new RuntimeException('Bildcache konnte nicht atomar aus dem aktiven Pfad ausgelagert werden.');
}
$mode = isset($conf['chmod_value']) ? (int)$conf['chmod_value'] : 0755;
$umask = umask(0);
$created = @mkdir($real_cache_root, $mode, true);
umask($umask);
if (!$created && !is_dir($real_cache_root))
{
if (!file_exists($real_cache_root) && @rename($detached, $real_cache_root))
{
throw new RuntimeException('Neuer Bildcache konnte nicht angelegt werden; der bisherige Cache wurde vollständig wiederhergestellt.');
}
throw new RuntimeException('Neuer Bildcache konnte nicht angelegt und der bisherige Cache nicht automatisch wiederhergestellt werden. Manueller Eingriff erforderlich.');
}
@chmod($real_cache_root, $mode);
@file_put_contents($real_cache_root.'/index.htm', 'Not allowed!');
// Der Quellenindex selbst bleibt bestehen, weil sich die Quellen durch ein
// Cache-Leeren nicht ändern. Seine Fertigmarkierungen sind danach aber
// zwingend ungültig. Der Worker darf sonst einen leeren Piwigo-Cache als
// bereits verarbeitet ansehen, ohne den Cache selbst anzuschauen.
$webdav_invalidated = 0;
if (function_exists('bratonien_tools_invalidate_webdav_cache_completion'))
{
$webdav_invalidated = bratonien_tools_invalidate_webdav_cache_completion('Piwigo-Bildcache wurde atomar geleert.');
}
$failed = array();
bratonien_tools_atomic_cache_remove_tree($detached, $failed);
$active = bratonien_tools_scan_image_cache($real_cache_root);
bratonien_tools_write_main_cache_status(array(
'state'=>'idle',
'message'=>'Bildcache wurde atomar geleert. Kein Cache-Worker schreibt während des Löschvorgangs in den Derivatbaum.',
));
if ($failed)
{
throw new RuntimeException(sprintf(
'Der aktive Bildcache wurde erfolgreich geleert und neu angelegt, aber %d Datei(en)/Verzeichnis(se) des ausgelagerten Altbestands konnten nicht entfernt werden. Erste problematische Stelle: %s',
count($failed),
$failed[0]
));
}
$message = sprintf(
'Bildcache atomar geleert: %d alte Datei(en) (%s) entfernt, davon %d Custom-Derivate.',
$before['files'],
bratonien_tools_format_bytes($before['bytes']),
$before['custom']
);
if ($webdav_invalidated > 0)
{
$message .= sprintf(' Die Cache-Fertigmarkierungen von %d WebDAV-Worker-Index(en) wurden verworfen; die Quellenindizes selbst bleiben erhalten.', $webdav_invalidated);
}
if ($active['files'] > 0)
{
$message .= sprintf(
' Unmittelbar nach dem atomaren Umschalten wurden bereits %d neue Derivatdatei(en) durch normale Piwigo-Anfragen erzeugt; die eigenen Cache-Worker waren während des Löschens gesperrt und der alte Cachebaum wurde vollständig entfernt.',
$active['files']
);
}
else
{
$message .= ' Der neu aktive Cache ist zum Abschluss der Prüfung leer.';
}
return array('message'=>$message);
}
finally
{
// Nur wenn wirklich alle WebDAV-Prozess-Locks gesichert wurden, sind die
// Cancel-Dateien nicht mehr nötig. Bei einem Timeout bleiben sie bestehen,
// damit der noch laufende Worker nach seinem aktuellen Batch tatsächlich
// stoppt und das fehlgeschlagene Cache-Leeren nicht durch einen Folgebatch
// verschärft wird.
if ($webdav_guards_ready)
{
foreach ($webdav_cancel_files as $cancel)
{
@unlink($cancel);
}
}
bratonien_tools_atomic_cache_release_locks($held_locks);
}
}
-4
View File
@@ -4,8 +4,6 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_warmup_settings.inc.php');
function bratonien_tools_cache_worker_settings_file() function bratonien_tools_cache_worker_settings_file()
{ {
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-tools-cache-workers.json'; return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-tools-cache-workers.json';
@@ -138,8 +136,6 @@ function bratonien_tools_get_cache_worker_settings()
$settings['auto_workers'] = min($settings['max_workers'], $settings['cpu_count']); $settings['auto_workers'] = min($settings['max_workers'], $settings['cpu_count']);
$settings['manual_workers'] = min($settings['max_workers'], $settings['manual_workers']); $settings['manual_workers'] = min($settings['max_workers'], $settings['manual_workers']);
$settings['worker_count'] = $settings['auto'] ? $settings['auto_workers'] : $settings['manual_workers']; $settings['worker_count'] = $settings['auto'] ? $settings['auto_workers'] : $settings['manual_workers'];
$settings['webdav_warmup'] = bratonien_tools_get_webdav_warmup_settings();
$settings['webdav_warmup']['status'] = bratonien_tools_get_webdav_warmup_status();
return $settings; return $settings;
} }
-105
View File
@@ -1,105 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_customer_qr_register_frontend_menu($menu_ref_array)
{
if (defined('IN_ADMIN'))
{
return;
}
$settings = bratonien_tools_customer_qr_settings();
if (empty($settings['enabled']))
{
return;
}
if (!is_array($menu_ref_array) || empty($menu_ref_array[0]) || !is_object($menu_ref_array[0]))
{
return;
}
$menu = $menu_ref_array[0];
if (!method_exists($menu, 'get_id') || $menu->get_id() !== 'menubar' || !method_exists($menu, 'register_block'))
{
return;
}
if (!class_exists('RegisteredBlock'))
{
return;
}
$menu->register_block(new RegisteredBlock(
'mbBratonienCustomerQr',
'QR-Code hochladen',
BRATONIEN_TOOLS_ID
));
}
function bratonien_tools_customer_qr_prepare_frontend_menu($menu_ref_array)
{
if (defined('IN_ADMIN'))
{
return;
}
$settings = bratonien_tools_customer_qr_settings();
if (empty($settings['enabled']))
{
return;
}
if (!is_array($menu_ref_array) || empty($menu_ref_array[0]) || !is_object($menu_ref_array[0]))
{
return;
}
$menu = $menu_ref_array[0];
if (!method_exists($menu, 'get_id') || $menu->get_id() !== 'menubar')
{
return;
}
$block = $menu->get_block('mbBratonienCustomerQr');
if ($block === null)
{
return;
}
$url = htmlspecialchars(
get_root_url().'plugins/'.BRATONIEN_TOOLS_ID.'/customer-qr-upload.php',
ENT_QUOTES,
'UTF-8'
);
// Bootstrap Darkroom renders raw-content blocks directly in the root navbar.
// Keeping this as a separate Piwigo menu block avoids hiding the customer
// action inside mbMenu/"Entdecken" and still follows the normal menubar
// lifecycle instead of modifying the theme.
$block->raw_content = '<li class="nav-item bratonien-customer-qr-nav">'
.'<a class="nav-link" href="'.$url.'" title="QR-Code hochladen">'
.'<i class="fas fa-qrcode fa-fw" aria-hidden="true"></i> QR-Code hochladen'
.'</a></li>';
if (method_exists($menu, 'set_block_position'))
{
// Core defaults: Specials=200, Menu=250. Darkroom combines those two as
// "Entdecken"; 225 therefore places QR upload directly after it.
$menu->set_block_position('mbBratonienCustomerQr', 225);
}
}
add_event_handler(
'blockmanager_register_blocks',
'bratonien_tools_customer_qr_register_frontend_menu',
EVENT_HANDLER_PRIORITY_NEUTRAL + 10
);
add_event_handler(
'blockmanager_prepare_display',
'bratonien_tools_customer_qr_prepare_frontend_menu',
EVENT_HANDLER_PRIORITY_NEUTRAL + 10
);
-503
View File
@@ -1,503 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_customer_qr_table()
{
return $GLOBALS['prefixeTable'].'bratonien_tools_customer_qr_uploads';
}
function bratonien_tools_customer_qr_year_limits()
{
return array(
2023 => 100,
2024 => 50,
2025 => 30,
2026 => 30,
);
}
function bratonien_tools_customer_qr_settings()
{
$default = array(
'enabled' => false,
);
if (!function_exists('conf_get_param'))
{
return $default;
}
$raw = conf_get_param('bratonien_customer_qr_upload', null);
if ($raw === null || $raw === '')
{
return $default;
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? array_merge($default, $decoded) : $default;
}
function bratonien_tools_customer_qr_save_settings()
{
if (!function_exists('conf_update_param'))
{
throw new RuntimeException('Piwigo-Konfiguration ist nicht verfügbar.');
}
$settings = array(
'enabled' => !empty($_POST['customer_qr_enabled']),
);
conf_update_param('bratonien_customer_qr_upload', json_encode($settings));
return array(
'message' => $settings['enabled']
? 'QR-Upload aktiviert.'
: 'QR-Upload deaktiviert.',
);
}
function bratonien_tools_customer_qr_storage_root()
{
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-customer-qr';
}
function bratonien_tools_customer_qr_ensure_storage()
{
$table = bratonien_tools_customer_qr_table();
pwg_query("CREATE TABLE IF NOT EXISTS `$table` (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
upload_year smallint(5) unsigned NOT NULL,
code_number varchar(32) NOT NULL,
original_name varchar(255) NOT NULL,
stored_name varchar(255) NOT NULL,
mime_type varchar(100) NOT NULL,
file_size bigint(20) unsigned NOT NULL DEFAULT 0,
created datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY year_code (upload_year, code_number),
KEY upload_year (upload_year)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$root = bratonien_tools_customer_qr_storage_root();
if (!is_dir($root) && !@mkdir($root, 0770, true) && !is_dir($root))
{
throw new RuntimeException('QR-Upload-Verzeichnis konnte nicht angelegt werden.');
}
return $root;
}
function bratonien_tools_customer_qr_year($value)
{
$year = (int)$value;
$limits = bratonien_tools_customer_qr_year_limits();
if (!isset($limits[$year]))
{
throw new InvalidArgumentException('Erlaubt sind nur die Jahre 2023, 2024, 2025 und 2026.');
}
return $year;
}
function bratonien_tools_customer_qr_default_year()
{
$limits = bratonien_tools_customer_qr_year_limits();
$current = (int)date('Y');
if (isset($limits[$current]))
{
return $current;
}
$years = array_keys($limits);
sort($years, SORT_NUMERIC);
return (int)end($years);
}
function bratonien_tools_customer_qr_number($value)
{
$value = trim((string)$value);
if ($value === '' || !preg_match('/^[0-9]{1,32}$/', $value))
{
throw new InvalidArgumentException('Die QR-Code-Nummer darf nur aus Ziffern bestehen.');
}
$value = ltrim($value, '0');
if ($value === '' || $value === '0')
{
throw new InvalidArgumentException('Die QR-Code-Nummer muss mindestens 1 sein.');
}
return $value;
}
function bratonien_tools_customer_qr_number_for_year($year, $value)
{
$year = bratonien_tools_customer_qr_year($year);
$number = bratonien_tools_customer_qr_number($value);
$limits = bratonien_tools_customer_qr_year_limits();
$limit = (int)$limits[$year];
if (strlen($number) > strlen((string)$limit) || (int)$number > $limit)
{
throw new InvalidArgumentException('Für '.$year.' sind nur QR-Code-Nummern 1 bis '.$limit.' vorgesehen.');
}
return $number;
}
function bratonien_tools_customer_qr_exists($year, $number)
{
bratonien_tools_customer_qr_ensure_storage();
$year = bratonien_tools_customer_qr_year($year);
$number = bratonien_tools_customer_qr_number_for_year($year, $number);
$table = bratonien_tools_customer_qr_table();
$query = 'SELECT id FROM `'.$table.'` WHERE upload_year='.(int)$year
." AND code_number='".pwg_db_real_escape_string($number)."' LIMIT 1";
return pwg_db_num_rows(pwg_query($query)) > 0;
}
function bratonien_tools_customer_qr_files_array(array $files)
{
if (!isset($files['name']))
{
return array();
}
if (!is_array($files['name']))
{
return array($files);
}
$normalized = array();
$count = count($files['name']);
for ($i = 0; $i < $count; $i++)
{
$normalized[] = array(
'name' => isset($files['name'][$i]) ? $files['name'][$i] : '',
'type' => isset($files['type'][$i]) ? $files['type'][$i] : '',
'tmp_name' => isset($files['tmp_name'][$i]) ? $files['tmp_name'][$i] : '',
'error' => isset($files['error'][$i]) ? $files['error'][$i] : UPLOAD_ERR_NO_FILE,
'size' => isset($files['size'][$i]) ? $files['size'][$i] : 0,
);
}
return $normalized;
}
function bratonien_tools_customer_qr_validate_image(array $file)
{
if ((int)$file['error'] !== UPLOAD_ERR_OK)
{
throw new RuntimeException('Datei-Upload fehlgeschlagen (Code '.(int)$file['error'].').');
}
if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name']))
{
throw new RuntimeException('Die hochgeladene Datei konnte nicht verifiziert werden.');
}
$image = @getimagesize($file['tmp_name']);
$mime = is_array($image) && !empty($image['mime']) ? strtolower((string)$image['mime']) : '';
$extensions = array(
'image/png' => 'png',
'image/jpeg' => 'jpg',
'image/webp' => 'webp',
'image/gif' => 'gif',
);
if (!isset($extensions[$mime]))
{
throw new RuntimeException('Erlaubt sind PNG, JPG, WEBP und GIF.');
}
return array(
'mime' => $mime,
'extension' => $extensions[$mime],
);
}
function bratonien_tools_customer_qr_unique_name($number, $extension)
{
try
{
$suffix = bin2hex(random_bytes(6));
}
catch (Throwable $e)
{
$suffix = str_replace('.', '', uniqid('', true));
}
return 'qr-'.$number.'-'.$suffix.'.'.$extension;
}
function bratonien_tools_customer_qr_process_uploads($year, array $files, array $numbers)
{
$year = bratonien_tools_customer_qr_year($year);
$root = bratonien_tools_customer_qr_ensure_storage();
$year_dir = $root.DIRECTORY_SEPARATOR.$year;
if (!is_dir($year_dir) && !@mkdir($year_dir, 0770, true) && !is_dir($year_dir))
{
throw new RuntimeException('Jahresverzeichnis für den QR-Upload konnte nicht angelegt werden.');
}
$lock_dir = $root.DIRECTORY_SEPARATOR.'.locks';
if (!is_dir($lock_dir) && !@mkdir($lock_dir, 0770, true) && !is_dir($lock_dir))
{
throw new RuntimeException('QR-Lockverzeichnis konnte nicht angelegt werden.');
}
$items = bratonien_tools_customer_qr_files_array($files);
$results = array();
$seen = array();
foreach ($items as $index => $file)
{
$display_name = trim((string)($file['name'] ?? ''));
$number = '';
if ($display_name === '')
{
$display_name = 'Datei '.($index + 1);
}
try
{
$number = bratonien_tools_customer_qr_number_for_year($year, isset($numbers[$index]) ? $numbers[$index] : '');
if (isset($seen[$number]))
{
$results[] = array(
'status' => 'duplicate',
'file' => $display_name,
'year' => $year,
'number' => $number,
'message' => 'Nummer ist innerhalb dieses Batches doppelt.',
);
continue;
}
$seen[$number] = true;
$lock_path = $lock_dir.DIRECTORY_SEPARATOR.$year.'-'.$number.'.lock';
$lock = @fopen($lock_path, 'c+');
if ($lock === false || !@flock($lock, LOCK_EX))
{
if (is_resource($lock)) fclose($lock);
throw new RuntimeException('Die Nummer konnte nicht sicher reserviert werden.');
}
try
{
if (bratonien_tools_customer_qr_exists($year, $number))
{
$results[] = array(
'status' => 'duplicate',
'file' => $display_name,
'year' => $year,
'number' => $number,
'message' => 'Diese QR-Code-Nummer ist für das gewählte Jahr bereits vorhanden.',
);
continue;
}
$image = bratonien_tools_customer_qr_validate_image($file);
$stored_name = bratonien_tools_customer_qr_unique_name($number, $image['extension']);
$target = $year_dir.DIRECTORY_SEPARATOR.$stored_name;
if (!@move_uploaded_file($file['tmp_name'], $target))
{
throw new RuntimeException('Die Datei konnte nicht dauerhaft gespeichert werden.');
}
@chmod($target, 0660);
$table = bratonien_tools_customer_qr_table();
$query = "INSERT INTO `$table` (upload_year, code_number, original_name, stored_name, mime_type, file_size, created) VALUES ("
.(int)$year.", '".pwg_db_real_escape_string($number)."', '"
.pwg_db_real_escape_string($display_name)."', '"
.pwg_db_real_escape_string($stored_name)."', '"
.pwg_db_real_escape_string($image['mime'])."', "
.max(0, (int)($file['size'] ?? 0)).", NOW())";
if (pwg_query($query) === false)
{
@unlink($target);
throw new RuntimeException('Der Upload konnte nicht in der QR-Code-Liste gespeichert werden.');
}
$results[] = array(
'status' => 'ok',
'file' => $display_name,
'year' => $year,
'number' => $number,
'message' => 'QR-Code erfolgreich gespeichert.',
);
}
finally
{
@flock($lock, LOCK_UN);
fclose($lock);
}
}
catch (Throwable $e)
{
$results[] = array(
'status' => 'error',
'file' => $display_name,
'year' => $year,
'number' => $number,
'message' => $e->getMessage(),
);
}
}
return $results;
}
function bratonien_tools_customer_qr_delete_upload()
{
$id = isset($_POST['customer_qr_id']) ? (int)$_POST['customer_qr_id'] : 0;
if ($id < 1)
{
throw new InvalidArgumentException('Ungültiger QR-Upload.');
}
$root = bratonien_tools_customer_qr_ensure_storage();
$table = bratonien_tools_customer_qr_table();
$result = pwg_query('SELECT id, upload_year, code_number, stored_name FROM `'.$table.'` WHERE id='.$id.' LIMIT 1');
$row = pwg_db_fetch_assoc($result);
if (!$row)
{
throw new RuntimeException('Der QR-Upload wurde nicht gefunden.');
}
$year = bratonien_tools_customer_qr_year($row['upload_year']);
$number = bratonien_tools_customer_qr_number_for_year($year, $row['code_number']);
$stored_name = (string)$row['stored_name'];
if ($stored_name === '' || basename($stored_name) !== $stored_name)
{
throw new RuntimeException('Der gespeicherte Dateiname ist ungültig.');
}
$lock_dir = $root.DIRECTORY_SEPARATOR.'.locks';
if (!is_dir($lock_dir) && !@mkdir($lock_dir, 0770, true) && !is_dir($lock_dir))
{
throw new RuntimeException('QR-Lockverzeichnis konnte nicht angelegt werden.');
}
$lock_path = $lock_dir.DIRECTORY_SEPARATOR.$year.'-'.$number.'.lock';
$lock = @fopen($lock_path, 'c+');
if ($lock === false || !@flock($lock, LOCK_EX))
{
if (is_resource($lock)) fclose($lock);
throw new RuntimeException('Der QR-Upload konnte nicht sicher gesperrt werden.');
}
try
{
$file = $root.DIRECTORY_SEPARATOR.$year.DIRECTORY_SEPARATOR.$stored_name;
if (is_file($file) && !@unlink($file))
{
throw new RuntimeException('Die QR-Datei konnte nicht gelöscht werden. Der Datenbankeintrag bleibt erhalten.');
}
if (pwg_query('DELETE FROM `'.$table.'` WHERE id='.$id.' LIMIT 1') === false)
{
throw new RuntimeException('Der QR-Datenbankeintrag konnte nicht gelöscht werden.');
}
}
finally
{
@flock($lock, LOCK_UN);
fclose($lock);
}
return array(
'message' => 'QR-Code '.$number.' aus '.$year.' gelöscht. Die Nummer ist wieder frei.',
);
}
function bratonien_tools_customer_qr_size_label($bytes)
{
$bytes = max(0, (int)$bytes);
if ($bytes >= 1048576)
{
return number_format($bytes / 1048576, 1, ',', '.').' MB';
}
if ($bytes >= 1024)
{
return number_format($bytes / 1024, 0, ',', '.').' KB';
}
return $bytes.' B';
}
function bratonien_tools_customer_qr_admin_data()
{
bratonien_tools_customer_qr_ensure_storage();
$settings = bratonien_tools_customer_qr_settings();
$table = bratonien_tools_customer_qr_table();
$limits = bratonien_tools_customer_qr_year_limits();
$counts = array_fill_keys(array_keys($limits), 0);
$total = 0;
$current_year = bratonien_tools_customer_qr_default_year();
$result = pwg_query('SELECT upload_year, COUNT(*) AS cnt FROM `'.$table.'` GROUP BY upload_year');
while ($row = pwg_db_fetch_assoc($result))
{
$year = (int)$row['upload_year'];
$count = (int)$row['cnt'];
$total += $count;
if (isset($counts[$year]))
{
$counts[$year] = $count;
}
}
$years = array();
foreach ($limits as $year => $capacity)
{
$used = (int)$counts[$year];
$years[] = array(
'year' => (int)$year,
'capacity' => (int)$capacity,
'used' => $used,
'remaining' => max(0, (int)$capacity - $used),
);
}
$uploads = array();
$upload_result = pwg_query(
'SELECT id, upload_year, code_number, original_name, file_size, created '
.'FROM `'.$table.'` '
.'ORDER BY upload_year DESC, CAST(code_number AS UNSIGNED) ASC, id ASC'
);
while ($row = pwg_db_fetch_assoc($upload_result))
{
$id = (int)$row['id'];
$uploads[] = array(
'id' => $id,
'year' => (int)$row['upload_year'],
'number' => (string)$row['code_number'],
'original_name' => (string)$row['original_name'],
'file_size' => (int)$row['file_size'],
'file_size_label' => bratonien_tools_customer_qr_size_label($row['file_size']),
'created' => (string)$row['created'],
'preview_url' => get_absolute_root_url(true).'plugins/'.BRATONIEN_TOOLS_ID.'/customer-qr-admin-file.php?id='.$id,
);
}
$year_keys = array_keys($limits);
sort($year_keys, SORT_NUMERIC);
return array(
'enabled' => !empty($settings['enabled']),
'url' => get_absolute_root_url(true).'plugins/'.BRATONIEN_TOOLS_ID.'/customer-qr-upload.php',
'year_min' => (int)reset($year_keys),
'year_max' => (int)end($year_keys),
'default_year' => $current_year,
'years' => $years,
'uploads' => $uploads,
'total' => $total,
'current_year_total' => (int)$counts[$current_year],
'current_year_capacity' => (int)$limits[$current_year],
'max_files' => max(1, (int)ini_get('max_file_uploads')),
);
}
-16
View File
@@ -20,7 +20,6 @@ function bratonien_tools_create_tables()
$profiles = bratonien_tools_table('watermark_profiles'); $profiles = bratonien_tools_table('watermark_profiles');
$rules = bratonien_tools_table('watermark_rules'); $rules = bratonien_tools_table('watermark_rules');
$nc_connections = bratonien_tools_table('nc_connections'); $nc_connections = bratonien_tools_table('nc_connections');
$customer_qr = bratonien_tools_table('customer_qr_uploads');
pwg_query("CREATE TABLE IF NOT EXISTS `$profiles` ( pwg_query("CREATE TABLE IF NOT EXISTS `$profiles` (
id int(11) NOT NULL AUTO_INCREMENT, id int(11) NOT NULL AUTO_INCREMENT,
@@ -82,20 +81,6 @@ function bratonien_tools_create_tables()
UNIQUE KEY connection_key (connection_key) UNIQUE KEY connection_key (connection_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
pwg_query("CREATE TABLE IF NOT EXISTS `$customer_qr` (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
upload_year smallint(5) unsigned NOT NULL,
code_number varchar(32) NOT NULL,
original_name varchar(255) NOT NULL,
stored_name varchar(255) NOT NULL,
mime_type varchar(100) NOT NULL,
file_size bigint(20) unsigned NOT NULL DEFAULT 0,
created datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY year_code (upload_year, code_number),
KEY upload_year (upload_year)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
if (function_exists('bratonien_tools_create_album_shares_table')) if (function_exists('bratonien_tools_create_album_shares_table'))
{ {
bratonien_tools_create_album_shares_table(); bratonien_tools_create_album_shares_table();
@@ -107,7 +92,6 @@ function bratonien_tools_drop_tables()
pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('watermark_profiles').'`'); pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('watermark_profiles').'`');
pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('watermark_rules').'`'); pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('watermark_rules').'`');
pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('nc_connections').'`'); pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('nc_connections').'`');
pwg_query('DROP TABLE IF EXISTS `'.bratonien_tools_table('customer_qr_uploads').'`');
if (function_exists('bratonien_tools_drop_album_shares_table')) if (function_exists('bratonien_tools_drop_album_shares_table'))
{ {
-320
View File
@@ -1,320 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_friendship_code_table()
{
return $GLOBALS['prefixeTable'].'bratonien_tools_friendship_codes';
}
function bratonien_tools_friendship_code_storage_root()
{
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-friendship-codes';
}
function bratonien_tools_friendship_code_ensure_storage()
{
$table = bratonien_tools_friendship_code_table();
pwg_query("CREATE TABLE IF NOT EXISTS `$table` (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
person_name varchar(190) NOT NULL,
normalized_name varchar(190) NOT NULL,
original_name varchar(255) NOT NULL,
stored_name varchar(255) NOT NULL,
mime_type varchar(100) NOT NULL,
file_size bigint(20) unsigned NOT NULL DEFAULT 0,
file_sha256 char(64) NOT NULL,
created datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY normalized_name (normalized_name),
UNIQUE KEY file_sha256 (file_sha256)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$root = bratonien_tools_friendship_code_storage_root();
if (!is_dir($root) && !@mkdir($root, 0770, true) && !is_dir($root))
{
throw new RuntimeException('Verzeichnis für Freundschaftscodes konnte nicht angelegt werden.');
}
return $root;
}
function bratonien_tools_friendship_code_name($value)
{
$name = trim((string)$value);
$name = preg_replace('/\s+/u', ' ', $name);
if ($name === '')
{
throw new InvalidArgumentException('Bitte einen Namen eingeben.');
}
$length = function_exists('mb_strlen') ? mb_strlen($name, 'UTF-8') : strlen($name);
if ($length > 120)
{
throw new InvalidArgumentException('Der Name darf höchstens 120 Zeichen lang sein.');
}
if (preg_match('/[\x00-\x1F\x7F]/u', $name))
{
throw new InvalidArgumentException('Der Name enthält ungültige Steuerzeichen.');
}
return $name;
}
function bratonien_tools_friendship_code_normalized_name($value)
{
$name = bratonien_tools_friendship_code_name($value);
return function_exists('mb_strtolower')
? mb_strtolower($name, 'UTF-8')
: strtolower($name);
}
function bratonien_tools_friendship_code_exists_for_name($name)
{
bratonien_tools_friendship_code_ensure_storage();
$normalized = bratonien_tools_friendship_code_normalized_name($name);
$table = bratonien_tools_friendship_code_table();
$query = "SELECT id FROM `$table` WHERE normalized_name='"
.pwg_db_real_escape_string($normalized)."' LIMIT 1";
return pwg_db_num_rows(pwg_query($query)) > 0;
}
function bratonien_tools_friendship_code_validate_image(array $file)
{
if ((int)($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK)
{
throw new RuntimeException('Datei-Upload fehlgeschlagen (Code '.(int)($file['error'] ?? UPLOAD_ERR_NO_FILE).').');
}
if (empty($file['tmp_name']) || !is_uploaded_file($file['tmp_name']))
{
throw new RuntimeException('Die hochgeladene Datei konnte nicht verifiziert werden.');
}
$image = @getimagesize($file['tmp_name']);
$mime = is_array($image) && !empty($image['mime']) ? strtolower((string)$image['mime']) : '';
$extensions = array(
'image/png' => 'png',
'image/jpeg' => 'jpg',
'image/webp' => 'webp',
'image/gif' => 'gif',
);
if (!isset($extensions[$mime]))
{
throw new RuntimeException('Erlaubt sind PNG, JPG, WEBP und GIF.');
}
return array(
'mime' => $mime,
'extension' => $extensions[$mime],
);
}
function bratonien_tools_friendship_code_unique_name($extension)
{
try
{
$suffix = bin2hex(random_bytes(8));
}
catch (Throwable $e)
{
$suffix = str_replace('.', '', uniqid('', true));
}
return 'friendship-'.$suffix.'.'.$extension;
}
function bratonien_tools_friendship_code_process_upload($name, array $file)
{
$name = bratonien_tools_friendship_code_name($name);
$normalized = bratonien_tools_friendship_code_normalized_name($name);
$root = bratonien_tools_friendship_code_ensure_storage();
$table = bratonien_tools_friendship_code_table();
$lock_dir = $root.DIRECTORY_SEPARATOR.'.locks';
if (!is_dir($lock_dir) && !@mkdir($lock_dir, 0770, true) && !is_dir($lock_dir))
{
throw new RuntimeException('Lockverzeichnis für Freundschaftscodes konnte nicht angelegt werden.');
}
$lock_path = $lock_dir.DIRECTORY_SEPARATOR.sha1($normalized).'.lock';
$lock = @fopen($lock_path, 'c+');
if ($lock === false || !@flock($lock, LOCK_EX))
{
if (is_resource($lock)) fclose($lock);
throw new RuntimeException('Der Freundschaftscode konnte nicht sicher reserviert werden.');
}
try
{
if (bratonien_tools_friendship_code_exists_for_name($name))
{
return array(
'status' => 'duplicate',
'kind' => 'friendship',
'name' => $name,
'file' => trim((string)($file['name'] ?? '')),
'message' => 'Für diesen Namen ist bereits ein Freundschaftscode hinterlegt.',
);
}
$image = bratonien_tools_friendship_code_validate_image($file);
$sha256 = @hash_file('sha256', $file['tmp_name']);
if (!is_string($sha256) || !preg_match('/^[a-f0-9]{64}$/', $sha256))
{
throw new RuntimeException('Der Freundschaftscode konnte nicht eindeutig geprüft werden.');
}
$hash_query = "SELECT person_name FROM `$table` WHERE file_sha256='"
.pwg_db_real_escape_string($sha256)."' LIMIT 1";
$hash_result = pwg_query($hash_query);
if (pwg_db_num_rows($hash_result) > 0)
{
$existing = pwg_db_fetch_assoc($hash_result);
return array(
'status' => 'duplicate',
'kind' => 'friendship',
'name' => $name,
'file' => trim((string)($file['name'] ?? '')),
'message' => 'Dieser Freundschaftscode wurde bereits hinterlegt'.(!empty($existing['person_name']) ? ' ('.(string)$existing['person_name'].')' : '').'.',
);
}
$stored_name = bratonien_tools_friendship_code_unique_name($image['extension']);
$target = $root.DIRECTORY_SEPARATOR.$stored_name;
if (!@move_uploaded_file($file['tmp_name'], $target))
{
throw new RuntimeException('Die Datei konnte nicht dauerhaft gespeichert werden.');
}
@chmod($target, 0660);
$original_name = trim((string)($file['name'] ?? ''));
if ($original_name === '')
{
$original_name = 'Freundschaftscode.'.$image['extension'];
}
$query = "INSERT INTO `$table` (person_name, normalized_name, original_name, stored_name, mime_type, file_size, file_sha256, created) VALUES ('"
.pwg_db_real_escape_string($name)."', '"
.pwg_db_real_escape_string($normalized)."', '"
.pwg_db_real_escape_string($original_name)."', '"
.pwg_db_real_escape_string($stored_name)."', '"
.pwg_db_real_escape_string($image['mime'])."', "
.max(0, (int)($file['size'] ?? 0)).", '"
.pwg_db_real_escape_string($sha256)."', NOW())";
if (pwg_query($query) === false)
{
@unlink($target);
throw new RuntimeException('Der Freundschaftscode konnte nicht in der Liste gespeichert werden.');
}
return array(
'status' => 'ok',
'kind' => 'friendship',
'name' => $name,
'file' => $original_name,
'message' => 'Freundschaftscode erfolgreich gespeichert.',
);
}
finally
{
@flock($lock, LOCK_UN);
fclose($lock);
}
}
function bratonien_tools_friendship_code_delete_upload()
{
$id = isset($_POST['friendship_code_id']) ? (int)$_POST['friendship_code_id'] : 0;
if ($id < 1)
{
throw new InvalidArgumentException('Ungültiger Freundschaftscode.');
}
$root = bratonien_tools_friendship_code_ensure_storage();
$table = bratonien_tools_friendship_code_table();
$result = pwg_query('SELECT id, person_name, normalized_name, stored_name FROM `'.$table.'` WHERE id='.$id.' LIMIT 1');
$row = pwg_db_fetch_assoc($result);
if (!$row)
{
throw new RuntimeException('Der Freundschaftscode wurde nicht gefunden.');
}
$stored_name = (string)$row['stored_name'];
if ($stored_name === '' || basename($stored_name) !== $stored_name)
{
throw new RuntimeException('Der gespeicherte Dateiname ist ungültig.');
}
$normalized = (string)$row['normalized_name'];
$lock_dir = $root.DIRECTORY_SEPARATOR.'.locks';
if (!is_dir($lock_dir) && !@mkdir($lock_dir, 0770, true) && !is_dir($lock_dir))
{
throw new RuntimeException('Lockverzeichnis für Freundschaftscodes konnte nicht angelegt werden.');
}
$lock = @fopen($lock_dir.DIRECTORY_SEPARATOR.sha1($normalized).'.lock', 'c+');
if ($lock === false || !@flock($lock, LOCK_EX))
{
if (is_resource($lock)) fclose($lock);
throw new RuntimeException('Der Freundschaftscode konnte nicht sicher gesperrt werden.');
}
try
{
$file = $root.DIRECTORY_SEPARATOR.$stored_name;
if (is_file($file) && !@unlink($file))
{
throw new RuntimeException('Die Datei konnte nicht gelöscht werden. Der Datenbankeintrag bleibt erhalten.');
}
if (pwg_query('DELETE FROM `'.$table.'` WHERE id='.$id.' LIMIT 1') === false)
{
throw new RuntimeException('Der Datenbankeintrag konnte nicht gelöscht werden.');
}
}
finally
{
@flock($lock, LOCK_UN);
fclose($lock);
}
return array(
'message' => 'Freundschaftscode von '.(string)$row['person_name'].' gelöscht.',
);
}
function bratonien_tools_friendship_code_admin_data()
{
bratonien_tools_friendship_code_ensure_storage();
$table = bratonien_tools_friendship_code_table();
$uploads = array();
$result = pwg_query(
'SELECT id, person_name, original_name, file_size, created FROM `'.$table.'` ORDER BY person_name ASC, id ASC'
);
while ($row = pwg_db_fetch_assoc($result))
{
$id = (int)$row['id'];
$base_url = get_absolute_root_url(true).'plugins/'.BRATONIEN_TOOLS_ID.'/friendship-code-admin-file.php?id='.$id;
$uploads[] = array(
'id' => $id,
'name' => (string)$row['person_name'],
'original_name' => (string)$row['original_name'],
'file_size' => (int)$row['file_size'],
'file_size_label' => function_exists('bratonien_tools_customer_qr_size_label')
? bratonien_tools_customer_qr_size_label($row['file_size'])
: (string)((int)$row['file_size']).' B',
'created' => (string)$row['created'],
'preview_url' => $base_url,
'download_url' => $base_url.'&download=1',
);
}
return array(
'total' => count($uploads),
'uploads' => $uploads,
);
}
+9 -87
View File
@@ -4,15 +4,11 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
function bratonien_tools_nc_connector_command_run(array $command) function bratonien_tools_nc_connector_systemctl_value(array $args)
{ {
$result = array('exit'=>-1, 'stdout'=>'', 'stderr'=>''); if (!function_exists('proc_open')) return '';
if (!function_exists('proc_open'))
{
$result['stderr'] = 'proc_open ist in PHP nicht verfügbar.';
return $result;
}
$command = array_merge(array('/usr/bin/systemctl'), $args);
$spec = array( $spec = array(
0=>array('file','/dev/null','r'), 0=>array('file','/dev/null','r'),
1=>array('pipe','w'), 1=>array('pipe','w'),
@@ -20,78 +16,13 @@ function bratonien_tools_nc_connector_command_run(array $command)
); );
$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)) if (!is_resource($process)) return '';
{ $stdout = stream_get_contents($pipes[1]);
$result['stderr'] = 'Prozess konnte nicht gestartet werden.'; stream_get_contents($pipes[2]);
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]);
$result['exit'] = (int)proc_close($process); $exit = proc_close($process);
return $result; return $exit === 0 ? trim((string)$stdout) : '';
}
function bratonien_tools_nc_connector_systemctl_run(array $args)
{
return bratonien_tools_nc_connector_command_run(array_merge(array('/usr/bin/systemctl'), $args));
}
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()
{
$service = 'bratonien-nc-connector.service';
$load_state = bratonien_tools_nc_connector_systemctl_value(array('show',$service,'--property=LoadState','--value'));
if ($load_state !== 'loaded')
{
throw new RuntimeException('WebDAV-Abgleich konnte nicht gestartet werden: der privilegierte Connector-Dienst '.$service.' ist nicht geladen.');
}
// Der Connector verwaltet bewusst geschützte Runtime-Daten unter /etc und
// /var/lib. Er darf deshalb niemals direkt als Webserver-Benutzer laufen.
// Der manuelle Admin-Button startet denselben privilegierten systemd-Dienst,
// den auch der Timer verwendet.
$result = bratonien_tools_nc_connector_systemctl_run(array('start',$service));
// Auf Installationen ohne passende polkit-Regel darf ausschließlich die
// eng begrenzte, vom Betreiber eingerichtete sudo-Freigabe für genau diesen
// systemd-Dienst verwendet werden. Es gibt absichtlich keinen Fallback auf
// runtime/run-all.sh als www-data.
if ($result['exit'] !== 0 && is_file('/usr/bin/sudo') && is_executable('/usr/bin/sudo'))
{
$result = bratonien_tools_nc_connector_command_run(array(
'/usr/bin/sudo',
'-n',
'/usr/bin/systemctl',
'start',
$service,
));
}
if ($result['exit'] !== 0)
{
$detail = $result['stderr'] !== '' ? $result['stderr'] : $result['stdout'];
if ($detail === '') $detail = 'Exit-Code '.$result['exit'];
throw new RuntimeException(
'WebDAV-Abgleich konnte den privilegierten Connector-Dienst nicht starten. '
.'Der Connector wird aus Sicherheitsgründen nicht als Webserver-Benutzer direkt ausgeführt. '.$detail
);
}
$service_result = bratonien_tools_nc_connector_systemctl_value(array('show',$service,'--property=Result','--value'));
if ($service_result !== '' && $service_result !== 'success')
{
throw new RuntimeException('WebDAV-Abgleich wurde gestartet, der Connector-Dienst endete aber mit Ergebnis '.$service_result.'.');
}
return array('message'=>'WebDAV-Abgleich wurde über den privilegierten Connector-Dienst ausgeführt.');
} }
function bratonien_tools_nc_connector_parse_systemd_time($value) function bratonien_tools_nc_connector_parse_systemd_time($value)
@@ -181,15 +112,6 @@ 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';
@@ -221,7 +143,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'=>bratonien_tools_nc_connector_status_message($last), 'last_run_message'=>(string)$last['message'],
'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'],
-161
View File
@@ -1,161 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_presentation_refresh_queue_dir()
{
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-presentation-refresh';
}
function bratonien_tools_presentation_refresh_normalize_categories(array $category_ids)
{
$result = array();
foreach ($category_ids as $category_id)
{
$category_id = (int)$category_id;
if ($category_id > 0) $result[$category_id] = $category_id;
}
ksort($result, SORT_NUMERIC);
return array_values($result);
}
function bratonien_tools_presentation_refresh_start()
{
if (!function_exists('exec'))
{
error_log('Bratonien presentation refresh: PHP exec() ist deaktiviert; Auftrag bleibt in der Queue.');
return false;
}
$php = function_exists('bratonien_tools_webdav_warmup_php_cli')
? bratonien_tools_webdav_warmup_php_cli()
: null;
if (!$php)
{
foreach (array('/usr/bin/php', '/usr/bin/php8.4', '/usr/bin/php8.3', '/usr/local/bin/php') as $candidate)
{
if (is_file($candidate) && is_executable($candidate))
{
$php = $candidate;
break;
}
}
}
if (!$php)
{
error_log('Bratonien presentation refresh: PHP-CLI wurde nicht gefunden; Auftrag bleibt in der Queue.');
return false;
}
$worker = realpath(BRATONIEN_TOOLS_PATH.'runtime/piwigo-presentation-refresh.php');
if (!$worker || !is_file($worker))
{
error_log('Bratonien presentation refresh: Worker fehlt; Auftrag bleibt in der Queue.');
return false;
}
$log = PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-presentation-refresh.log';
$command = 'nohup '.escapeshellarg($php).' '.escapeshellarg($worker)
.' >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!';
$output = array();
$exit = 1;
@exec($command, $output, $exit);
return $exit === 0 && !empty($output[0]) && (int)$output[0] > 0;
}
function bratonien_tools_presentation_refresh_enqueue(array $category_ids, $reason='presentation-changed', $all=false)
{
$category_ids = bratonien_tools_presentation_refresh_normalize_categories($category_ids);
$all = (bool)$all;
if (!$all && !$category_ids) return array('queued'=>false, 'started'=>false);
$dir = bratonien_tools_presentation_refresh_queue_dir();
if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir))
{
throw new RuntimeException('Queue für die Vorschau-Aktualisierung konnte nicht angelegt werden.');
}
$payload = array(
'all'=>$all,
'categories'=>$category_ids,
'reason'=>(string)$reason,
'requested_at'=>time(),
);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if ($json === false) throw new RuntimeException('Vorschau-Auftrag konnte nicht serialisiert werden.');
$name = sprintf('%010d-%s.json', time(), bin2hex(random_bytes(6)));
$target = $dir.'/'.$name;
$tmp = $target.'.tmp';
if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false)
{
throw new RuntimeException('Vorschau-Auftrag konnte nicht geschrieben werden.');
}
@chmod($tmp, 0664);
if (!@rename($tmp, $target))
{
@unlink($tmp);
throw new RuntimeException('Vorschau-Auftrag konnte nicht atomar bereitgestellt werden.');
}
return array('queued'=>true, 'started'=>bratonien_tools_presentation_refresh_start());
}
function bratonien_tools_presentation_refresh_enqueue_all($reason='presentation-changed')
{
return bratonien_tools_presentation_refresh_enqueue(array(), $reason, true);
}
function bratonien_tools_presentation_refresh_category_snapshot()
{
$snapshot = array();
$result = pwg_query('SELECT id, status, visible FROM '.CATEGORIES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result))
{
$snapshot[(int)$row['id']] = array(
'status'=>(string)$row['status'],
'visible'=>(string)$row['visible'],
);
}
return $snapshot;
}
function bratonien_tools_presentation_refresh_watch_admin_categories()
{
if (!defined('IN_ADMIN') || ($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') return;
// Connector-Sync hat einen eigenen, priorisierten Bildcache-Pfad. Neue
// Kategorien werden dort bereits korrekt an den Worker übergeben.
if ((string)($_GET['page'] ?? '') === 'site_update' && (string)($_POST['bratonien_connector'] ?? '') === '1') return;
$before = bratonien_tools_presentation_refresh_category_snapshot();
register_shutdown_function(function () use ($before) {
try
{
$after = bratonien_tools_presentation_refresh_category_snapshot();
$changed = array();
foreach ($after as $category_id=>$state)
{
if (!isset($before[$category_id])) continue;
if (
(string)$before[$category_id]['status'] !== (string)$state['status']
|| (string)$before[$category_id]['visible'] !== (string)$state['visible']
)
{
$changed[] = (int)$category_id;
}
}
if ($changed)
{
bratonien_tools_presentation_refresh_enqueue($changed, 'album-status-or-visibility-changed');
}
}
catch (Throwable $e)
{
error_log('Bratonien presentation refresh watcher: '.$e->getMessage());
}
});
}
+5 -24
View File
@@ -5,20 +5,16 @@ if (!defined('PHPWG_ROOT_PATH'))
} }
require_once(BRATONIEN_TOOLS_PATH . 'tools/image_cache.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/image_cache.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/cache_clear_atomic.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_profiles.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_profiles.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_settings.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/watermark_settings.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/album_rules.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/album_rules.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'tools/asset_manager.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'tools/asset_manager.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_engine.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/watermark_engine.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/presentation_refresh.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/self_update.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/self_update.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_lock.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/customer_qr_upload.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/friendship_code_upload.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_piwigo_api.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard.inc.php');
@@ -27,20 +23,14 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_connection_scope.inc.p
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_warmup_settings.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_scan_diagnostic.inc.php');
function bratonien_tools_get_tools() function bratonien_tools_get_tools()
{ {
return array( return array(
'image_cache_clear' => array('handler' => 'bratonien_tools_clear_image_cache_atomic'), 'image_cache_clear' => array('handler' => 'bratonien_tools_clear_image_cache'),
'image_cache_build' => array('handler' => 'bratonien_tools_start_combined_image_cache_build'), 'image_cache_build' => array('handler' => 'bratonien_tools_start_main_cache_build'),
'image_cache_cancel' => array('handler' => 'bratonien_tools_cancel_combined_image_cache_build'), 'image_cache_cancel' => array('handler' => 'bratonien_tools_cancel_main_cache_build'),
'image_cache_worker_settings' => array('handler' => 'bratonien_tools_save_cache_worker_settings'), 'image_cache_worker_settings' => array('handler' => 'bratonien_tools_save_cache_worker_settings'),
'image_cache_webdav_warmup_settings' => array('handler' => 'bratonien_tools_save_webdav_warmup_settings'),
'image_cache_webdav_warmup_manual' => array('handler' => 'bratonien_tools_start_webdav_warmup_manual'),
'image_cache_webdav_warmup_audit' => array('handler' => 'bratonien_tools_run_webdav_warmup_audit'),
'image_cache_webdav_scan_diagnostic' => array('handler' => 'bratonien_tools_run_webdav_scan_diagnostic'),
'watermark_save' => array('handler' => 'bratonien_tools_save_watermark'), 'watermark_save' => array('handler' => 'bratonien_tools_save_watermark'),
'watermark_file_delete' => array('handler' => 'bratonien_tools_delete_watermark_file'), 'watermark_file_delete' => array('handler' => 'bratonien_tools_delete_watermark_file'),
'watermark_engine' => array('handler' => 'bratonien_tools_handle_watermark_engine'), 'watermark_engine' => array('handler' => 'bratonien_tools_handle_watermark_engine'),
@@ -50,9 +40,6 @@ function bratonien_tools_get_tools()
'watermark_defaults' => array('handler' => 'bratonien_tools_save_watermark_defaults'), 'watermark_defaults' => array('handler' => 'bratonien_tools_save_watermark_defaults'),
'watermark_rule' => array('handler' => 'bratonien_tools_save_album_rule'), 'watermark_rule' => array('handler' => 'bratonien_tools_save_album_rule'),
'public_selection_settings' => array('handler' => 'bratonien_tools_save_public_selection_settings'), 'public_selection_settings' => array('handler' => 'bratonien_tools_save_public_selection_settings'),
'customer_qr_settings' => array('handler' => 'bratonien_tools_customer_qr_save_settings'),
'customer_qr_delete' => array('handler' => 'bratonien_tools_customer_qr_delete_upload'),
'friendship_code_delete' => array('handler' => 'bratonien_tools_friendship_code_delete_upload'),
'asset_upload' => array('handler' => 'bratonien_tools_upload_asset'), 'asset_upload' => array('handler' => 'bratonien_tools_upload_asset'),
'asset_delete' => array('handler' => 'bratonien_tools_delete_asset'), 'asset_delete' => array('handler' => 'bratonien_tools_delete_asset'),
'asset_upload_limits' => array('handler' => 'bratonien_tools_save_upload_limits'), 'asset_upload_limits' => array('handler' => 'bratonien_tools_save_upload_limits'),
@@ -63,7 +50,6 @@ 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'),
@@ -87,14 +73,9 @@ function bratonien_tools_handle_watermark_engine()
$enabled = !empty($_POST['engine_enabled']); $enabled = !empty($_POST['engine_enabled']);
bratonien_tools_set_watermark_engine($enabled); bratonien_tools_set_watermark_engine($enabled);
if (function_exists('bratonien_tools_presentation_refresh_enqueue_all'))
{
bratonien_tools_presentation_refresh_enqueue_all('watermark-engine-changed');
}
return array( return array(
'message' => $enabled 'message' => $enabled
? 'Bratonien-Wasserzeichenverwaltung aktiviert. Piwigos bisherige Wasserzeichen-Nutzung wurde gesichert und die Vorschauen werden aktualisiert.' ? 'Bratonien-Wasserzeichenverwaltung aktiviert. Piwigos bisherige Wasserzeichen-Nutzung wurde gesichert und unterdrueckt.'
: 'Bratonien-Wasserzeichenverwaltung deaktiviert. Die zuvor gesicherte Piwigo-Wasserzeichen-Nutzung wurde wiederhergestellt und die Vorschauen werden aktualisiert.', : 'Bratonien-Wasserzeichenverwaltung deaktiviert. Die zuvor gesicherte Piwigo-Wasserzeichen-Nutzung wurde wiederhergestellt.',
); );
} }
+3 -25
View File
@@ -82,37 +82,16 @@ function bratonien_tools_runtime_b64url_encode($value)
return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
} }
function bratonien_tools_profile_watermark_relative(array $profile)
{
$relative = ltrim(trim((string)($profile['watermark_file'] ?? '')), '/');
if ($relative !== '')
{
return $relative;
}
// Ein Profil ohne eigene Datei bedeutet bewusst: Einstellungen dieses
// Profils verwenden, aber die zentrale Basis-Wasserzeichendatei erben.
// Dadurch kann das Standardprofil "Oeffentlich" ohne doppelte Dateipflege
// fuer jedes öffentliche Album als Schutzregel dienen.
if (function_exists('bratonien_tools_get_base_watermark_config'))
{
$base = bratonien_tools_get_base_watermark_config();
$relative = ltrim(trim((string)($base['file'] ?? '')), '/');
}
return $relative;
}
function bratonien_tools_profile_watermark_path(array $profile) function bratonien_tools_profile_watermark_path(array $profile)
{ {
static $paths = array(); static $paths = array();
$relative = bratonien_tools_profile_watermark_relative($profile); if (empty($profile['watermark_file']))
if ($relative === '')
{ {
return null; return null;
} }
$relative = ltrim((string)$profile['watermark_file'], '/');
if (array_key_exists($relative, $paths)) if (array_key_exists($relative, $paths))
{ {
return $paths[$relative]; return $paths[$relative];
@@ -140,10 +119,9 @@ function bratonien_tools_profile_watermark_path(array $profile)
function bratonien_tools_runtime_profile_version(array $profile) function bratonien_tools_runtime_profile_version(array $profile)
{ {
$watermark_path = bratonien_tools_profile_watermark_path($profile); $watermark_path = bratonien_tools_profile_watermark_path($profile);
$effective_file = bratonien_tools_profile_watermark_relative($profile);
$parts = array( $parts = array(
$profile['id'] ?? 0, $profile['id'] ?? 0,
$effective_file, $profile['watermark_file'] ?? '',
$profile['scale_percent'] ?? 100, $profile['scale_percent'] ?? 100,
$profile['xpos'] ?? 90, $profile['xpos'] ?? 90,
$profile['ypos'] ?? 90, $profile['ypos'] ?? 90,
-66
View File
@@ -1,66 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_webdav_derivative_cache_valid($target_path, $derivative, $params=null, &$reason=null)
{
$reason = '';
$target_path = (string)$target_path;
if ($target_path === '' || !is_file($target_path) || !is_readable($target_path))
{
$reason = 'missing';
return false;
}
$cache_root = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR;
if (strpos($target_path, $cache_root) !== 0)
{
$reason = 'outside-cache';
return false;
}
clearstatcache(true, $target_path);
$bytes = @filesize($target_path);
if ($bytes === false || $bytes < 1)
{
$reason = 'empty';
return false;
}
$actual = @getimagesize($target_path);
if (!is_array($actual) || (int)($actual[0] ?? 0) < 1 || (int)($actual[1] ?? 0) < 1)
{
$reason = 'not-image';
return false;
}
if (is_object($derivative) && method_exists($derivative, 'get_size'))
{
$expected = $derivative->get_size();
$expected_width = (int)($expected[0] ?? 0);
$expected_height = (int)($expected[1] ?? 0);
if ($expected_width > 0 && $expected_height > 0)
{
if ((int)$actual[0] !== $expected_width || (int)$actual[1] !== $expected_height)
{
$reason = 'dimension-mismatch:'.(int)$actual[0].'x'.(int)$actual[1].'!='.$expected_width.'x'.$expected_height;
return false;
}
}
}
if (is_object($params) && isset($params->last_mod_time))
{
$mtime = @filemtime($target_path);
if ($mtime === false || (int)$mtime < (int)$params->last_mod_time)
{
$reason = 'stale-params';
return false;
}
}
$reason = 'valid';
return true;
}
-297
View File
@@ -1,297 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_webdav_materialize_source_info($image_id)
{
static $cache = array();
$image_id = (int)$image_id;
if ($image_id < 1) return null;
$warmup_cli = PHP_SAPI === 'cli'
&& strpos((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 'Bratonien-WebDAV-Cache-Warmup/') === 0;
if (!$warmup_cli && 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,
'state_dir'=>$state_dir,
'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-guard.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;
}
}
-88
View File
@@ -1,88 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
function bratonien_tools_run_webdav_scan_diagnostic()
{
if (!function_exists('exec')) throw new RuntimeException('PHP exec() ist deaktiviert; der Scan-Test kann nicht gestartet werden.');
if (!function_exists('bratonien_tools_nc_connector_connections') || !function_exists('bratonien_tools_nc_connector_is_webdav'))
{
throw new RuntimeException('Connector-Verbindungen koennen nicht gelesen werden.');
}
$php = function_exists('bratonien_tools_webdav_warmup_php_cli') ? bratonien_tools_webdav_warmup_php_cli() : null;
if (!$php) throw new RuntimeException('PHP-CLI wurde fuer den Scan-Test nicht gefunden.');
$script = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/webdav-scan-diagnostic.php');
if (!$script || !is_file($script)) throw new RuntimeException('WebDAV-Scan-Diagnose wurde nicht gefunden.');
$reports = array();
$failed = false;
foreach (bratonien_tools_nc_connector_connections() as $connection)
{
if (empty($connection['enabled']) || !bratonien_tools_nc_connector_is_webdav($connection)) continue;
$connection_id = (int)($connection['id'] ?? 0);
if ($connection_id < 1) continue;
$output = array();
$exit = 1;
@exec(
escapeshellarg($php).' '.escapeshellarg($script).' --connection-id='.$connection_id.' 2>&1',
$output,
$exit
);
$text = trim(implode("\n", $output));
$decoded = $text !== '' ? json_decode($text, true) : null;
if (!is_array($decoded))
{
$decoded = array(
'ok'=>false,
'connection_id'=>$connection_id,
'fatal'=>'Diagnose lieferte kein gueltiges JSON.',
'raw'=>$text,
);
}
$decoded['exit_code'] = (int)$exit;
$reports[] = $decoded;
if ($exit !== 0 || empty($decoded['ok'])) $failed = true;
}
if (!$reports) throw new RuntimeException('Keine aktive WebDAV-Verbindung gefunden.');
$parts = array();
foreach ($reports as $report)
{
$id = (int)($report['connection_id'] ?? 0);
if (!empty($report['fatal']))
{
$parts[] = 'WebDAV #'.$id.': FEHLER: '.(string)$report['fatal'].' (Exit '.(int)$report['exit_code'].')';
continue;
}
$lock_state = (string)($report['worker_lock_state'] ?? 'unknown');
if ($lock_state === 'free') $lock_text = 'Worker-Lock frei';
elseif ($lock_state === 'busy') $lock_text = 'Worker-Lock BELEGT';
elseif ($lock_state === 'open_failed') $lock_text = 'Worker-Lock NICHT OEFFENBAR';
else $lock_text = 'Worker-Lock unbekannt';
$parts[] = sprintf(
'WebDAV #%d: Mapping %d Dateien, Shadow %d Links, %d passend, %d defekt, %d ohne Mapping, %d doppelt; %s%s (Exit %d).',
$id,
(int)($report['mapping_files'] ?? 0),
(int)($report['shadow_links'] ?? 0),
(int)($report['matched'] ?? 0),
(int)($report['broken'] ?? 0),
(int)($report['unmapped'] ?? 0),
(int)($report['duplicates'] ?? 0),
$lock_text,
!empty($report['worker_lock_detail']) ? ' - '.(string)$report['worker_lock_detail'] : '',
(int)$report['exit_code']
);
}
return array(
'message'=>($failed ? 'Scan-/Lock-Test auffaellig. ' : 'Scan-/Lock-Test erfolgreich. ').implode(' ', $parts),
);
}
-151
View File
@@ -1,151 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
if (!defined('BRATONIEN_WEBDAV_GENERATION_VERSION'))
{
define('BRATONIEN_WEBDAV_GENERATION_VERSION', 2);
}
function bratonien_tools_webdav_source_index_file($state_dir, $connection_id=0)
{
// Der Quellenindex gehoert dem Piwigo-Worker und muss deshalb in einem
// Verzeichnis liegen, das derselbe Benutzer wie der Admin-/CLI-Worker
// schreiben kann. Das Connector-State-Verzeichnis ist fuer den Worker nur
// lesende Eingabe (Mapping/Priority), nicht sein eigener persistenter State.
$connection_id = (int)$connection_id;
if ($connection_id < 1) throw new RuntimeException('Verbindungs-ID fehlt fuer den WebDAV-Quellenindex.');
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-cache-index.connection-'.$connection_id.'.json';
}
function bratonien_tools_webdav_source_index_empty($connection_id)
{
return array(
'schema_version'=>3,
'connection_id'=>(int)$connection_id,
'updated_at'=>0,
'last_periodic_at'=>0,
'sources'=>array(),
);
}
function bratonien_tools_webdav_source_index_load($state_dir, $connection_id)
{
$file = bratonien_tools_webdav_source_index_file($state_dir, $connection_id);
if (!is_file($file) || !is_readable($file)) return null;
$raw = @file_get_contents($file);
$index = $raw !== false ? json_decode($raw, true) : null;
if (!is_array($index)) return null;
$base = bratonien_tools_webdav_source_index_empty($connection_id);
$index = array_merge($base, $index);
if (!isset($index['sources']) || !is_array($index['sources'])) $index['sources'] = array();
$index['connection_id'] = (int)$connection_id;
return $index;
}
function bratonien_tools_webdav_source_index_save($state_dir, $connection_id, array $index)
{
$file = bratonien_tools_webdav_source_index_file($state_dir, $connection_id);
$directory = dirname($file);
if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory))
{
throw new RuntimeException('WebDAV-Quellenindex-Verzeichnis konnte nicht angelegt werden.');
}
$index['schema_version'] = 3;
$index['connection_id'] = (int)$connection_id;
$index['updated_at'] = time();
if (!isset($index['sources']) || !is_array($index['sources'])) $index['sources'] = array();
ksort($index['sources'], SORT_STRING);
$json = json_encode($index, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if ($json === false) throw new RuntimeException('WebDAV-Quellenindex konnte nicht serialisiert werden.');
$tmp = $file.'.tmp-'.bin2hex(random_bytes(4));
if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false)
{
throw new RuntimeException('WebDAV-Quellenindex konnte nicht geschrieben werden.');
}
@chmod($tmp, 0664);
if (!@rename($tmp, $file))
{
@unlink($tmp);
throw new RuntimeException('WebDAV-Quellenindex konnte nicht atomar gespeichert werden.');
}
}
function bratonien_tools_webdav_source_index_key(array $source)
{
$connection_id = (int)($source['connection_id'] ?? 0);
$fileid = (int)($source['fileid'] ?? 0);
if ($fileid > 0) return 'c'.$connection_id.':f'.$fileid;
$root_fileid = (int)($source['root_fileid'] ?? 0);
$path = trim((string)($source['webdav_path'] ?? ''), '/');
return 'c'.$connection_id.':r'.$root_fileid.':p'.sha1($path);
}
function bratonien_tools_webdav_source_index_signature(array $source)
{
return sha1(implode('|', array(
'generation-v'.(int)BRATONIEN_WEBDAV_GENERATION_VERSION,
(int)($source['connection_id'] ?? 0),
(int)($source['root_fileid'] ?? 0),
(int)($source['fileid'] ?? 0),
trim((string)($source['webdav_path'] ?? ''), '/'),
(string)($source['etag'] ?? ''),
(int)($source['size'] ?? 0),
strtolower((string)($source['content_type'] ?? $source['mime'] ?? '')),
(int)($source['width'] ?? 0),
(int)($source['height'] ?? 0),
)));
}
function bratonien_tools_webdav_source_index_metadata(array $source, $shadow_relative)
{
$path = trim((string)($source['webdav_path'] ?? ''), '/');
return array(
'fileid'=>(int)($source['fileid'] ?? 0),
'root_fileid'=>(int)($source['root_fileid'] ?? 0),
'webdav_path'=>$path,
'name'=>basename($path),
'etag'=>(string)($source['etag'] ?? ''),
'size'=>(int)($source['size'] ?? 0),
'content_type'=>(string)($source['content_type'] ?? $source['mime'] ?? ''),
'width'=>(int)($source['width'] ?? 0),
'height'=>(int)($source['height'] ?? 0),
'shadow_relative'=>ltrim((string)$shadow_relative, '/'),
'source_signature'=>bratonien_tools_webdav_source_index_signature($source),
'generation_version'=>(int)BRATONIEN_WEBDAV_GENERATION_VERSION,
'last_seen_at'=>time(),
);
}
function bratonien_tools_webdav_source_index_is_current(array $entry, $signature)
{
$signature = (string)$signature;
return isset($entry['source_signature'], $entry['stage1_signature'], $entry['stage2_signature'])
&& (int)($entry['generation_version'] ?? 0) === (int)BRATONIEN_WEBDAV_GENERATION_VERSION
&& hash_equals((string)$entry['source_signature'], $signature)
&& hash_equals((string)$entry['stage1_signature'], $signature)
&& hash_equals((string)$entry['stage2_signature'], $signature);
}
function bratonien_tools_webdav_source_index_prune(array &$index, array $current_keys)
{
$keep = array_fill_keys($current_keys, true);
$removed = 0;
foreach (array_keys((array)$index['sources']) as $key)
{
if (!isset($keep[$key]))
{
unset($index['sources'][$key]);
$removed++;
}
}
return $removed;
}
-394
View File
@@ -1,394 +0,0 @@
<?php
if (!defined('PHPWG_ROOT_PATH'))
{
die('Hacking attempt!');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_source_index.inc.php');
function bratonien_tools_webdav_warmup_settings_file()
{
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.settings.json';
}
function bratonien_tools_get_webdav_warmup_settings()
{
$settings = array(
'enabled'=>false,
'batch_size'=>10,
'periodic_hours'=>12,
);
$file = bratonien_tools_webdav_warmup_settings_file();
if (is_file($file) && is_readable($file))
{
$raw = @file_get_contents($file);
$saved = $raw !== false ? json_decode($raw, true) : null;
if (is_array($saved))
{
if (array_key_exists('enabled', $saved)) $settings['enabled'] = (bool)$saved['enabled'];
if (isset($saved['batch_size'])) $settings['batch_size'] = max(1, min(50, (int)$saved['batch_size']));
if (isset($saved['periodic_hours'])) $settings['periodic_hours'] = max(1, min(168, (int)$saved['periodic_hours']));
}
}
return $settings;
}
function bratonien_tools_webdav_warmup_connections()
{
$result = array();
if (!function_exists('bratonien_tools_nc_connector_connections') || !function_exists('bratonien_tools_nc_connector_is_webdav')) return $result;
foreach (bratonien_tools_nc_connector_connections() as $connection)
{
if (empty($connection['enabled']) || !bratonien_tools_nc_connector_is_webdav($connection)) continue;
$connection_id = (int)($connection['id'] ?? 0);
if ($connection_id < 1) continue;
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir === '') continue;
$result[$connection_id] = array('id'=>$connection_id, 'state_dir'=>$state_dir, 'connection'=>$connection);
}
return $result;
}
function bratonien_tools_webdav_warmup_status_file_for_connection($connection_id)
{
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.status-'.(int)$connection_id.'.json';
}
function bratonien_tools_webdav_warmup_cancel_file_for_connection($connection_id)
{
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.cancel-'.(int)$connection_id;
}
function bratonien_tools_webdav_warmup_write_status($connection_id, $state, $message, array $extra=array())
{
$file = bratonien_tools_webdav_warmup_status_file_for_connection($connection_id);
$payload = array_merge(array(
'state'=>(string)$state,
'message'=>(string)$message,
'connection_id'=>(int)$connection_id,
'updated_at'=>time(),
), $extra);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if ($json === false) throw new RuntimeException('WebDAV-Worker-Status konnte nicht serialisiert werden.');
$directory = dirname($file);
if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory)) throw new RuntimeException('WebDAV-Worker-Statusverzeichnis konnte nicht angelegt werden.');
$tmp = $file.'.tmp-'.bin2hex(random_bytes(4));
if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false) throw new RuntimeException('WebDAV-Worker-Status konnte nicht geschrieben werden.');
@chmod($tmp, 0664);
if (!@rename($tmp, $file))
{
@unlink($tmp);
throw new RuntimeException('WebDAV-Worker-Status konnte nicht atomar gespeichert werden.');
}
}
function bratonien_tools_invalidate_webdav_cache_completion($reason='Piwigo-Bildcache wurde geleert.')
{
$invalidated = 0;
foreach (bratonien_tools_webdav_warmup_connections() as $connection_id=>$runtime)
{
$index = bratonien_tools_webdav_source_index_load($runtime['state_dir'], $connection_id);
if (is_array($index))
{
foreach ($index['sources'] as &$entry)
{
if (!is_array($entry)) continue;
unset($entry['stage1_signature'], $entry['stage2_signature']);
}
unset($entry);
bratonien_tools_webdav_source_index_save($runtime['state_dir'], $connection_id, $index);
$invalidated++;
}
bratonien_tools_webdav_warmup_write_status(
$connection_id,
'idle',
$reason.' Der Quellenindex bleibt erhalten; seine Cache-Fertigmarkierungen wurden verworfen.',
array('cache_invalidated'=>true)
);
}
return $invalidated;
}
function bratonien_tools_save_webdav_warmup_settings()
{
$current = bratonien_tools_get_webdav_warmup_settings();
$enabled = !empty($_POST['webdav_warmup_enabled']);
$batch_size = isset($_POST['webdav_warmup_batch_size']) ? (int)$_POST['webdav_warmup_batch_size'] : (int)$current['batch_size'];
$periodic_hours = isset($_POST['webdav_warmup_periodic_hours']) ? (int)$_POST['webdav_warmup_periodic_hours'] : (int)$current['periodic_hours'];
$batch_size = max(1, min(50, $batch_size));
$periodic_hours = max(1, min(168, $periodic_hours));
$payload = array(
'enabled'=>$enabled,
'batch_size'=>$batch_size,
'periodic_hours'=>$periodic_hours,
);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if ($json === false) throw new RuntimeException('Warmup-Einstellungen konnten nicht serialisiert werden.');
$file = bratonien_tools_webdav_warmup_settings_file();
$directory = dirname($file);
if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory))
{
throw new RuntimeException('Warmup-Einstellungsverzeichnis konnte nicht angelegt werden.');
}
$tmp = $file.'.tmp-'.bin2hex(random_bytes(4));
if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false)
{
throw new RuntimeException('Warmup-Einstellungen konnten nicht gespeichert werden.');
}
@chmod($tmp, 0664);
if (!@rename($tmp, $file))
{
@unlink($tmp);
throw new RuntimeException('Warmup-Einstellungen konnten nicht atomar gespeichert werden.');
}
return array('message'=>sprintf(
'WebDAV-Worker gespeichert: %s, %d Bilder pro Batch, Shadow-Tree-Abgleich alle %d Stunden. Der Worker-Index wird bei Bedarf automatisch aus dem Shadow-Tree aufgebaut.',
$enabled ? 'automatisch aktiv' : 'Automatik deaktiviert',
$batch_size,
$periodic_hours
));
}
function bratonien_tools_webdav_warmup_php_cli()
{
if (function_exists('bratonien_tools_find_php_cli'))
{
$php = bratonien_tools_find_php_cli();
if ($php) return $php;
}
foreach (array('/usr/bin/php', '/usr/bin/php8.4', '/usr/bin/php8.3', '/usr/local/bin/php') as $candidate)
{
if (is_file($candidate) && is_executable($candidate)) return $candidate;
}
return null;
}
function bratonien_tools_start_webdav_warmup_mode($mode, $message)
{
if (!function_exists('exec')) throw new RuntimeException('PHP exec() ist deaktiviert; Worker kann nicht gestartet werden.');
$php = bratonien_tools_webdav_warmup_php_cli();
if (!$php) throw new RuntimeException('PHP-CLI wurde für den WebDAV-Worker nicht gefunden.');
$dispatcher = realpath(BRATONIEN_TOOLS_PATH.'runtime/webdav-warmup-dispatch.php');
if (!$dispatcher || !is_file($dispatcher)) throw new RuntimeException('WebDAV-Worker-Dispatcher wurde nicht gefunden.');
if (!in_array($mode, array('manual','rebuild'), true)) throw new RuntimeException('Ungültiger manueller Worker-Modus.');
$connections = bratonien_tools_webdav_warmup_connections();
if (!$connections) throw new RuntimeException('Keine aktive WebDAV-Verbindung mit vollständiger Runtime gefunden.');
$run_id = date('YmdHis').'-'.bin2hex(random_bytes(4));
foreach ($connections as $connection_id=>$runtime)
{
// Ein altes Abbruchsignal darf niemals den nächsten bewusst gestarteten
// Lauf stoppen. Es wird unmittelbar vor dem neuen Start entfernt.
@unlink(bratonien_tools_webdav_warmup_cancel_file_for_connection($connection_id));
bratonien_tools_webdav_warmup_write_status(
$connection_id,
'queued',
$mode === 'rebuild'
? 'Cache-Rebuild wurde angefordert. Der Worker wartet auf den Start und gleicht danach Shadow-Tree und eigenen Index ab.'
: 'Cache-Abgleich wurde angefordert. Bereits erfolgreich abgeschlossene Stufen bleiben erhalten; geladen werden nur Quellen mit fehlender Stufe 1 oder Stufe 2.',
array('mode'=>$mode, 'run_id'=>$run_id)
);
}
$log = PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup-dispatch.log';
$command = 'nohup '.escapeshellarg($php).' '.escapeshellarg($dispatcher).' --mode='.escapeshellarg($mode).' >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!';
$output = array();
$exit = 1;
@exec($command, $output, $exit);
$pid = isset($output[0]) ? (int)$output[0] : 0;
if ($exit !== 0 || $pid <= 0)
{
foreach ($connections as $connection_id=>$runtime)
{
bratonien_tools_webdav_warmup_write_status($connection_id, 'error', 'WebDAV-Worker konnte nicht gestartet werden.', array('mode'=>$mode, 'run_id'=>$run_id));
}
throw new RuntimeException('WebDAV-Worker konnte nicht gestartet werden.');
}
return array('message'=>$message, 'pid'=>$pid, 'run_id'=>$run_id);
}
function bratonien_tools_start_webdav_warmup_manual()
{
return bratonien_tools_start_webdav_warmup_mode(
'manual',
'WebDAV-Worker: Shadow-Tree und Worker-Index werden abgeglichen; vollständig erledigte Quellen werden nicht aus Nextcloud geladen.'
);
}
function bratonien_tools_start_webdav_cache_rebuild()
{
return bratonien_tools_start_webdav_warmup_mode(
'rebuild',
'WebDAV-Bildcache: der vollständige aktuelle Shadow-Bestand wird erneut an Piwigo übergeben.'
);
}
function bratonien_tools_request_webdav_cache_cancel()
{
$requested = 0;
foreach (bratonien_tools_webdav_warmup_connections() as $connection_id=>$runtime)
{
$status_file = bratonien_tools_webdav_warmup_status_file_for_connection($connection_id);
$status = null;
if (is_file($status_file) && is_readable($status_file))
{
$raw = @file_get_contents($status_file);
$status = $raw !== false ? json_decode($raw, true) : null;
}
if (!is_array($status)) $status = array();
$state = (string)($status['state'] ?? 'idle');
if (!in_array($state, array('queued','scan','running','cancelling'), true)) continue;
$cancel = bratonien_tools_webdav_warmup_cancel_file_for_connection($connection_id);
if (@file_put_contents($cancel, (string)time()."\n", LOCK_EX) === false)
{
throw new RuntimeException('WebDAV-Abbruchsignal für Verbindung #'.(int)$connection_id.' konnte nicht geschrieben werden.');
}
@chmod($cancel, 0664);
$extra = $status;
unset($extra['state'], $extra['message'], $extra['connection_id'], $extra['updated_at']);
$extra['cancel_requested'] = true;
bratonien_tools_webdav_warmup_write_status(
$connection_id,
'cancelling',
'Abbruch angefordert. Der aktuell laufende Batch wird noch vollständig beendet; danach werden keine weiteren Nextcloud-Originale geladen.',
$extra
);
$requested++;
}
return $requested;
}
function bratonien_tools_cancel_combined_image_cache_build()
{
$webdav = bratonien_tools_request_webdav_cache_cancel();
$local = false;
if (function_exists('bratonien_tools_main_cache_process_active') && function_exists('bratonien_tools_main_cache_is_running'))
{
$local = bratonien_tools_main_cache_process_active() || bratonien_tools_main_cache_is_running();
}
if ($local && function_exists('bratonien_tools_request_main_cache_cancel'))
{
bratonien_tools_request_main_cache_cancel();
}
if ($webdav > 0 && $local)
{
return array('message'=>'Cache-Abbruch angefordert. Der aktuelle WebDAV-Batch wird noch beendet; lokale Cache-Worker erhalten ebenfalls sofort das Abbruchsignal.');
}
if ($webdav > 0)
{
return array('message'=>'WebDAV-Cache-Abbruch angefordert. Der aktuell laufende Batch wird noch vollständig beendet; danach ist Schluss.');
}
if ($local)
{
return array('message'=>'Lokaler Piwigo-Cache-Abbruch wurde angefordert.');
}
return array('message'=>'Es läuft aktuell kein Cache-Aufbau.');
}
function bratonien_tools_has_local_cache_sources()
{
if (!function_exists('bratonien_tools_webdav_materialize_source_info')) return true;
$result = pwg_query('SELECT id FROM '.IMAGES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result))
{
$image_id = (int)($row['id'] ?? 0);
if ($image_id < 1) continue;
if (!bratonien_tools_webdav_materialize_source_info($image_id)) return true;
}
return false;
}
function bratonien_tools_start_combined_image_cache_build()
{
// Der normale "Piwigo-Bildcache aufbauen"-Lauf ist immer inkrementell.
// Der Worker liest zuerst ausschließlich Shadow-Tree/Mapping und seinen
// eigenen Index. Nur Quellen, denen laut Index Stufe 1 oder Stufe 2 fehlt,
// dürfen anschließend aus Nextcloud geladen werden. Ein kompletter
// Neuaufbau entsteht nur nach einer echten Cache-Invalidierung (z.B. Cache
// leeren), denn genau dort werden die Stage-Signaturen bewusst entfernt.
$webdav = bratonien_tools_start_webdav_warmup_manual();
$main = array('started'=>false, 'message'=>'');
if (bratonien_tools_has_local_cache_sources())
{
$main = bratonien_tools_start_main_cache_build();
}
else
{
bratonien_tools_write_main_cache_status(array(
'state'=>'complete',
'message'=>'Lokaler Piwigo-Teil nicht erforderlich. Der WebDAV-Worker arbeitet ausschließlich mit Shadow-Tree und eigenem Index.',
'total'=>0,
'completed'=>0,
'generated'=>0,
'cached'=>0,
'skipped'=>0,
'errors'=>0,
'current'=>'',
'worker_count'=>0,
));
$main['message'] = 'Lokaler Piwigo-Teil nicht gestartet, weil keine lokalen Bildquellen vorhanden sind.';
}
$parts = array();
if (!empty($main['message'])) $parts[] = $main['message'];
if (!empty($webdav['message'])) $parts[] = $webdav['message'];
return array(
'started'=>!empty($main['started']) || !empty($webdav['pid']),
'message'=>implode(' ', $parts),
);
}
function bratonien_tools_run_webdav_warmup_audit()
{
if (!function_exists('exec')) throw new RuntimeException('PHP exec() ist deaktiviert; Pfadaudit kann nicht gestartet werden.');
$php = bratonien_tools_webdav_warmup_php_cli();
if (!$php) throw new RuntimeException('PHP-CLI wurde für den Pfadaudit nicht gefunden.');
$audit = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/webdav-warmup-audit.php');
if (!$audit || !is_file($audit)) throw new RuntimeException('WebDAV-Pfadaudit wurde nicht gefunden.');
$output = array();
$exit = 1;
@exec(escapeshellarg($php).' '.escapeshellarg($audit).' 2>&1', $output, $exit);
$text = trim(implode(' | ', array_slice($output, -8)));
if ($exit !== 0) throw new RuntimeException('WebDAV-Pfadaudit meldet einen unsicheren Zustand'.($text !== '' ? ': '.$text : '.'));
return array('message'=>'WebDAV-Pfadaudit erfolgreich'.($text !== '' ? ': '.$text : '.'));
}
function bratonien_tools_get_webdav_warmup_status()
{
$status = array(
'state'=>'idle',
'message'=>'Noch kein Shadow-Tree-/Worker-Lauf protokolliert.',
'updated_at'=>0,
);
$files = glob(PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.status-*.json');
if (!$files) return $status;
$latest = null;
foreach ($files as $file)
{
if (!is_readable($file)) continue;
$raw = @file_get_contents($file);
$data = $raw !== false ? json_decode($raw, true) : null;
if (!is_array($data)) continue;
if ($latest === null || (int)($data['updated_at'] ?? 0) > (int)($latest['updated_at'] ?? 0)) $latest = $data;
}
return is_array($latest) ? array_merge($status, $latest) : $status;
}
+2 -41
View File
@@ -1,40 +1,6 @@
(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;
@@ -147,14 +113,9 @@
activate(initial, false); activate(initial, false);
} }
function init() {
initBratonienTabs();
initNcConnectorRunNow();
}
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', initBratonienTabs);
} else { } else {
init(); initBratonienTabs();
} }
})(); })();
-122
View File
@@ -216,105 +216,12 @@
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 {
@@ -334,35 +241,6 @@
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);
+4 -18
View File
@@ -23,7 +23,7 @@ $_SERVER['REQUEST_URI'] = '/';
$_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/main-cache-build.php'; $_SERVER['SCRIPT_NAME'] = '/plugins/bratonien_tools/main-cache-build.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME']; $_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = ''; $_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Piwigo-Cache-Builder/1.5'; $_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Piwigo-Cache-Builder/1.4';
$_SERVER['HTTPS'] = 'off'; $_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php'); require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
@@ -35,7 +35,6 @@ if (!defined('BRATONIEN_TOOLS_PATH'))
require_once(BRATONIEN_TOOLS_PATH.'tools/image_cache.inc.php'); require_once(BRATONIEN_TOOLS_PATH.'tools/image_cache.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/watermark_engine.inc.php'); require_once(BRATONIEN_TOOLS_PATH.'include/watermark_engine.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php'); require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
require_once(PHPWG_ROOT_PATH.'admin/include/image.class.php'); require_once(PHPWG_ROOT_PATH.'admin/include/image.class.php');
@@ -208,18 +207,7 @@ function bratonien_tools_cache_builder_worker($worker_index, $worker_count, $run
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' ORDER BY id'); $result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result)) while ($row = pwg_db_fetch_assoc($result))
{ {
$image_id = (int)$row['id']; if (((int)$row['id'] % $worker_count) === $worker_index)
// WebDAV-Bilder gehören vollständig in den materialisierenden Rebuild-/Warmup-Pfad.
// Sie dürfen im Legacy-Worker weder erzeugt noch als "übersprungen" gezählt werden,
// weil das die Fortschrittsanzeige nach einem Cache-Leeren fälschlich wie einen
// abgeschlossenen Lauf aussehen lässt.
if (bratonien_tools_webdav_materialize_source_info($image_id))
{
continue;
}
if (($image_id % $worker_count) === $worker_index)
{ {
$rows[] = $row; $rows[] = $row;
} }
@@ -260,7 +248,6 @@ function bratonien_tools_cache_builder_worker($worker_index, $worker_count, $run
{ {
$derivative = new DerivativeImage($requested_params, $src); $derivative = new DerivativeImage($requested_params, $src);
$target_path = $derivative->get_path(); $target_path = $derivative->get_path();
if (strpos($target_path, PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR) !== 0) if (strpos($target_path, PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR) !== 0)
{ {
$skipped++; $skipped++;
@@ -486,7 +473,7 @@ for ($i=0; $i<$worker_count; $i++)
} }
} }
$running_message = sprintf('Piwigo-Bildcache wird mit %d parallelen Worker(n) aufgebaut. WebDAV-Bilder laufen getrennt über den Piwigo-Rebuild.', $worker_count); $running_message = sprintf('Piwigo-Bildcache wird mit %d parallelen Worker(n) aufgebaut.', $worker_count);
bratonien_tools_write_main_cache_status(array( bratonien_tools_write_main_cache_status(array(
'state'=>'running', 'state'=>'running',
'message'=>$running_message, 'message'=>$running_message,
@@ -535,7 +522,7 @@ $state = bratonien_tools_cache_builder_aggregate(
$worker_count, $worker_count,
$was_cancelled $was_cancelled
? 'Piwigo-Bildcache-Aufbau wurde abgebrochen.' ? 'Piwigo-Bildcache-Aufbau wurde abgebrochen.'
: ($exit_error ? 'Piwigo-Bildcache beendet; mindestens ein Worker meldete einen Prozessfehler.' : 'Lokaler Piwigo-Bildcache wurde aufgebaut; WebDAV-Bilder laufen getrennt über den Piwigo-Rebuild.') : ($exit_error ? 'Piwigo-Bildcache beendet; mindestens ein Worker meldete einen Prozessfehler.' : 'Piwigo-Bildcache wurde aufgebaut.')
); );
for ($i=0; $i<$worker_count; $i++) for ($i=0; $i<$worker_count; $i++)
@@ -549,4 +536,3 @@ fclose($lock);
@unlink($lock_path); @unlink($lock_path);
exit(($state === 'error' || $exit_error) ? 1 : 0); exit(($state === 'error' || $exit_error) ? 1 : 0);
}
+19 -253
View File
@@ -25,266 +25,32 @@ if (!function_exists('is_admin') || !is_admin())
} }
require_once(BRATONIEN_TOOLS_PATH.'tools/image_cache.inc.php'); require_once(BRATONIEN_TOOLS_PATH.'tools/image_cache.inc.php');
$file = bratonien_tools_main_cache_status_file();
function bratonien_tools_status_read_json($file) if (!is_file($file) || !is_readable($file))
{ {
if (!is_file($file) || !is_readable($file)) return null; echo json_encode(array(
$raw = @file_get_contents($file); 'state'=>'idle','message'=>'Noch kein manueller Cache-Aufbau gestartet.',
$data = $raw !== false ? json_decode($raw, true) : null;
return is_array($data) ? $data : null;
}
function bratonien_tools_status_webdav_label(array $status)
{
$connection = (int)($status['connection_id'] ?? 0);
$state = (string)($status['state'] ?? 'idle');
$prefix = $connection > 0 ? 'WebDAV #'.$connection.': ' : 'WebDAV: ';
if ($state === 'queued' || $state === 'waiting') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Worker wartet auf den nächsten sicheren Startpunkt.');
if ($state === 'scan')
{
$sources = (int)($status['shadow_sources'] ?? 0);
$selected = (int)($status['selected'] ?? 0);
$removed = (int)($status['removed_from_index'] ?? 0);
$created = !empty($status['index_created']);
return $prefix.'Shadow-Tree wird mit dem eigenen Worker-Index verglichen · '.$sources.' Quellen im Shadow-Tree · '.$selected.' zu verarbeiten'.($removed > 0 ? ' · '.$removed.' gelöschte Quelle(n) aus dem Worker-Index entfernt' : '').($created ? ' · Worker-Index wurde neu angelegt' : '').'.';
}
if ($state === 'running')
{
$stage = (int)($status['stage'] ?? 0);
$batch = (int)($status['batch'] ?? 0);
$requested = (int)($status['batch_requested'] ?? 0);
$downloaded = (int)($status['batch_downloaded'] ?? 0);
$completed = (int)($status['stage_completed'] ?? 0);
$total = (int)($status['selected_total'] ?? 0);
$parts = array($prefix.'Worker stellt Originale temporär bereit; Piwigo erzeugt die Derivate');
if ($stage > 0) $parts[] = 'Stufe '.$stage;
if ($batch > 0) $parts[] = 'Batch '.$batch;
if ($requested > 0) $parts[] = $downloaded.' / '.$requested.' Quellen dieses Batches geladen';
if ($total > 0) $parts[] = $completed.' / '.$total.' Quellen dieser Stufe abgeschlossen';
return implode(' · ', $parts).'.';
}
if ($state === 'cancelling') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Abbruch angefordert; aktueller Batch wird noch beendet.');
if ($state === 'cancelled') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Cache-Aufbau wurde abgebrochen.');
if ($state === 'partial') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Einzelne Quellen sind noch offen und werden beim nächsten Lauf erneut versucht.');
if ($state === 'preempted') return $prefix.'Stufe 2 wurde nach einem vollständigen Batch unterbrochen, damit neue Connector-Inhalte zuerst verarbeitet werden können.';
if ($state === 'complete') return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'Verarbeitung abgeschlossen.');
if ($state === 'error' || $state === 'fatal') return $prefix.'FEHLER: '.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'WebDAV-Verarbeitung fehlgeschlagen.');
return $prefix.((string)($status['message'] ?? '') !== '' ? (string)$status['message'] : 'wartet.');
}
$local_file = bratonien_tools_main_cache_status_file();
$local = bratonien_tools_status_read_json($local_file);
if ($local === null)
{
$local = array(
'state'=>'idle','message'=>'Noch kein lokaler Piwigo-Cache-Aufbau gestartet.',
'total'=>0,'completed'=>0,'generated'=>0,'cached'=>0,'skipped'=>0,'errors'=>0,'current'=>'','updated_at'=>0, 'total'=>0,'completed'=>0,'generated'=>0,'cached'=>0,'skipped'=>0,'errors'=>0,'current'=>'','updated_at'=>0,
); ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} }
$webdav = array(); $raw = @file_get_contents($file);
foreach ((array)glob(PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.status-*.json') as $file) $data = $raw !== false ? json_decode($raw, true) : null;
if (!is_array($data))
{ {
$status = bratonien_tools_status_read_json($file); http_response_code(500);
if (!$status) continue; echo json_encode(array('state'=>'error','message'=>'Cache-Status ist nicht lesbar.'));
if (!isset($status['connection_id']) && preg_match('/status-(\d+)\.json$/', $file, $m)) $status['connection_id'] = (int)$m[1]; exit;
$webdav[] = $status;
}
usort($webdav, function($a, $b) {
return (int)($a['connection_id'] ?? 0) <=> (int)($b['connection_id'] ?? 0);
});
$webdav_active = false;
$webdav_waiting = false;
$webdav_partial = false;
$webdav_cancelling = false;
$webdav_cancelled = false;
$webdav_error = false;
$webdav_pending = false;
$webdav_latest = 0;
$webdav_lines = array();
$webdav_all_complete = !empty($webdav);
$webdav_progress_total = 0;
$webdav_progress_completed = 0;
$webdav_source_total = 0;
foreach ($webdav as &$status)
{
$state = (string)($status['state'] ?? 'idle');
$updated = (int)($status['updated_at'] ?? 0);
$webdav_latest = max($webdav_latest, $updated);
if ($state === 'queued' && $updated > 0 && (time() - $updated) > 45)
{
$state = 'error';
$status['state'] = 'error';
$status['message'] = 'Worker wurde angefordert, hat aber innerhalb von 45 Sekunden keinen Scan gestartet.';
}
// Ältere Dispatcher-Versionen konnten den Zustand "waiting" schreiben und
// anschließend ohne Fortsetzungsauftrag enden. Dieser reine Statuswert ist
// kein Beweis für einen laufenden Connector und darf die Cache-Seite nicht
// dauerhaft blockieren. Neue Wartezustände tragen resume_scheduled=true und
// besitzen einen echten Hintergrund-Warter.
if ($state === 'waiting' && empty($status['resume_scheduled']) && $updated > 0 && (time() - $updated) > 45)
{
$state = 'idle';
$status['state'] = 'idle';
$status['message'] = 'Veralteter Connector-Wartezustand wurde verworfen; aktuell ist kein fortsetzbarer Cache-Auftrag registriert.';
}
if ($state === 'scan')
{
$selected = max(0, (int)($status['selected'] ?? 0));
$webdav_source_total += $selected;
$webdav_progress_total += $selected * 2;
}
elseif ($state === 'running' || $state === 'cancelling')
{
$selected = max(0, (int)($status['selected_total'] ?? $status['selected'] ?? 0));
$stage = max(1, (int)($status['stage'] ?? 1));
$stage_completed = max(0, min($selected, (int)($status['stage_completed'] ?? 0)));
$webdav_source_total += $selected;
$webdav_progress_total += $selected * 2;
$webdav_progress_completed += $stage >= 2 ? $selected + $stage_completed : $stage_completed;
}
elseif (in_array($state, array('complete','error','fatal','preempted','cancelled','waiting','partial'), true))
{
$selected = max(0, (int)($status['selected'] ?? 0));
if ($selected > 0)
{
$stage1_completed = max(0, (int)($status['stage1_completed'] ?? 0));
$stage2_completed = max(0, (int)($status['stage2_completed'] ?? 0));
$stage1_failed = max(0, (int)($status['stage1_failed'] ?? 0));
$stage2_failed = max(0, (int)($status['stage2_failed'] ?? 0));
$stage1_deferred = max(0, (int)($status['stage1_deferred'] ?? 0));
$stage2_deferred = max(0, (int)($status['stage2_deferred'] ?? 0));
$webdav_source_total += $selected;
$webdav_progress_total += $selected * 2;
$webdav_progress_completed += min($selected * 2, $stage1_completed + $stage2_completed + $stage1_failed + $stage2_failed + $stage1_deferred + $stage2_deferred);
}
}
if (in_array($state, array('queued','scan','running'), true)) $webdav_active = true;
if ($state === 'waiting') $webdav_waiting = true;
if ($state === 'partial') $webdav_partial = true;
if ($state === 'cancelling') $webdav_cancelling = true;
if ($state === 'cancelled') $webdav_cancelled = true;
if ($state === 'preempted') $webdav_pending = true;
if (in_array($state, array('error','fatal'), true)) $webdav_error = true;
if ($state !== 'complete' && $state !== 'idle') $webdav_all_complete = false;
$webdav_lines[] = bratonien_tools_status_webdav_label($status);
}
unset($status);
$local_state = (string)($local['state'] ?? 'idle');
$local_updated = (int)($local['updated_at'] ?? 0);
$local_stale = in_array($local_state, array('running','queued'), true) && $local_updated > 0 && (time() - $local_updated) > 45;
if ($local_stale && !$webdav_active && !$webdav_cancelling && !$webdav_waiting)
{
$local['state'] = 'error';
$local['message'] = 'Der lokale Piwigo-Teil liefert seit mehr als 45 Sekunden keinen Fortschritt.';
$local['errors'] = max(1, (int)($local['errors'] ?? 0));
$local_state = 'error';
} }
if ($local_state === 'running' || $local_state === 'queued') $local_label = 'Lokaler Piwigo-Teil läuft: '.((string)($local['message'] ?? '') !== '' ? (string)$local['message'] : 'Cache-Varianten werden verarbeitet.'); $state = (string)($data['state'] ?? 'idle');
elseif ($local_state === 'complete') $local_label = 'Lokaler Piwigo-Teil fertig.'; $updated = (int)($data['updated_at'] ?? 0);
elseif ($local_state === 'cancelled') $local_label = 'Lokaler Piwigo-Teil wurde abgebrochen.'; if (($state === 'running' || $state === 'queued') && $updated > 0 && (time() - $updated) > 45)
elseif ($local_state === 'error') $local_label = 'Lokaler Piwigo-Teil mit Fehler: '.((string)($local['message'] ?? '') !== '' ? (string)$local['message'] : 'unbekannter Fehler');
else $local_label = 'Lokaler Piwigo-Teil: keine relevanten lokalen Bildquellen aktiv.';
$overall = $local;
$overall['local'] = $local;
$overall['webdav'] = $webdav;
$overall['updated_at'] = max($local_updated, $webdav_latest);
$overall['source_total'] = $webdav_source_total;
$local_total = max(0, (int)($local['total'] ?? 0));
$local_completed = max(0, min($local_total, (int)($local['completed'] ?? 0)));
$combined_total = $local_total + $webdav_progress_total;
$combined_completed = $local_completed + min($webdav_progress_total, $webdav_progress_completed);
if ($webdav_cancelling || $local_state === 'cancelling')
{ {
$overall['state'] = 'cancelling'; $data['state'] = 'error';
$overall['total'] = $combined_total; $data['message'] = 'Cache-Aufbau liefert seit mehr als 45 Sekunden keinen Fortschritt. Der Prozess ist vermutlich beendet oder festgefahren.';
$overall['completed'] = $combined_completed; $data['errors'] = max(1, (int)($data['errors'] ?? 0));
$overall['message'] = 'Cache-Aufbau wird nach dem aktuell laufenden Batch beendet'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'.';
$overall['current'] = implode(' ', $webdav_lines);
}
elseif ($webdav_active)
{
$overall['state'] = 'running';
$overall['total'] = $combined_total;
$overall['completed'] = $combined_completed;
$overall['generated'] = (int)($local['generated'] ?? 0);
$overall['cached'] = (int)($local['cached'] ?? 0);
$overall['skipped'] = (int)($local['skipped'] ?? 0);
$overall['message'] = 'Gesamtaufbau läuft'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'. '.$local_label;
$overall['current'] = implode(' ', $webdav_lines);
}
elseif ($webdav_waiting)
{
$overall['state'] = 'queued';
$overall['total'] = $combined_total;
$overall['completed'] = $combined_completed;
$overall['message'] = 'Cache-Aufbau pausiert für laufende Connector-Synchronisierung'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'. Der Fortsetzungsauftrag bleibt registriert.';
$overall['current'] = implode(' ', $webdav_lines);
}
elseif ($webdav_partial)
{
$overall['state'] = 'queued';
$overall['total'] = $combined_total;
$overall['completed'] = $combined_completed;
$overall['errors'] = 0;
$overall['message'] = 'Cache-Aufbau bleibt fortsetzbar: einzelne Quellen sind noch offen; der restliche Bestand wurde weiterverarbeitet.';
$overall['current'] = implode(' ', $webdav_lines);
}
elseif ($webdav_error || $local_state === 'error')
{
$overall['state'] = 'error';
if ($combined_total > 0)
{
$overall['total'] = $combined_total;
$overall['completed'] = $combined_completed;
}
$overall['message'] = 'Cache-Aufbau mit Fehlern'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'. '.$local_label;
$overall['current'] = implode(' ', $webdav_lines);
$overall['errors'] = max(1, (int)($overall['errors'] ?? 0));
}
elseif ($webdav_pending)
{
$overall['state'] = 'queued';
$overall['total'] = $combined_total;
$overall['completed'] = $combined_completed;
$overall['message'] = 'Cache-Aufbau wartet auf priorisierte Connector-Inhalte'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'. '.$local_label;
$overall['current'] = implode(' ', $webdav_lines);
}
elseif ($webdav_cancelled || $local_state === 'cancelled')
{
$overall['state'] = 'cancelled';
$overall['total'] = $combined_total;
$overall['completed'] = $combined_completed;
$overall['message'] = 'Cache-Aufbau wurde nach dem letzten vollständig abgeschlossenen Batch beendet.';
$overall['current'] = implode(' ', $webdav_lines);
}
elseif ($webdav_all_complete && $local_state === 'complete')
{
$overall['state'] = 'complete';
if ($combined_total > 0)
{
$overall['total'] = $combined_total;
$overall['completed'] = $combined_total;
}
$overall['message'] = 'Cache-Aufbau abgeschlossen'.($webdav_source_total > 0 ? ' · '.$webdav_source_total.' WebDAV-Quellen' : '').'. '.$local_label;
$overall['current'] = implode(' ', $webdav_lines);
}
else
{
$overall['state'] = $local_state === 'complete' ? 'idle' : $local_state;
$overall['message'] = $local_label;
if ($webdav_lines) $overall['current'] = implode(' ', $webdav_lines);
} }
echo json_encode($overall, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+15 -112
View File
@@ -1,7 +1,7 @@
<?php <?php
/* /*
Plugin Name: Bratonien Tools Plugin Name: Bratonien Tools
Version: 0.9.7.1.51 Version: 0.9.6.3
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,32 +17,23 @@ 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_cache_validation.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/webdav_materialize_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/presentation_refresh.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');
require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/album_shares.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_orphan_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_productive_ws.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/customer_qr_upload.inc.php');
require_once(BRATONIEN_TOOLS_PATH . 'include/customer_qr_frontend_menu.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_materialize_src_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 2); add_event_handler('get_src_image_url', 'bratonien_tools_filter_webdav_src_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 2);
add_event_handler('get_derivative_url', 'bratonien_tools_filter_webdav_materialize_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 4); add_event_handler('get_derivative_url', 'bratonien_tools_filter_webdav_derivative_url', EVENT_HANDLER_PRIORITY_NEUTRAL + 50, 4);
add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action'); add_event_handler('loc_end_element_set_global', 'bratonien_tools_batch_titles_register_action');
add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2); add_event_handler('element_set_global_action', 'bratonien_tools_batch_titles_apply', EVENT_HANDLER_PRIORITY_NEUTRAL, 2);
add_event_handler('init', 'bratonien_tools_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_presentation_refresh_watch_admin_categories', EVENT_HANDLER_PRIORITY_NEUTRAL - 8);
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('loc_end_page_tail', 'bratonien_tools_add_legal_footer_links');
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');
@@ -90,124 +81,36 @@ function bratonien_tools_prepare_private_album_permissions()
return; return;
} }
bratonien_tools_grant_private_album_access($category_id, (int)$user['id']); $result = pwg_query('SELECT status FROM '.CATEGORIES_TABLE.' WHERE id = '.$category_id.' LIMIT 1');
} 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;
} }
$site_id = (int)($_GET['site'] ?? 0); $category = pwg_db_fetch_assoc($result);
$user_id = (int)$user['id']; if ($category['status'] !== 'public')
if ($site_id < 1 || $user_id < 1)
{ {
return; return;
} }
register_shutdown_function(function () use ($site_id, $user_id) { $users = isset($_POST['users']) && is_array($_POST['users']) ? $_POST['users'] : array();
$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;'; $current_user_id = (int)$user['id'];
$result = pwg_query($query); $normalized_user_ids = array_map('intval', $users);
$changed = false;
while ($row = pwg_db_fetch_assoc($result))
{
$category_id = (int)$row['id'];
if ($category_id < 1)
{
continue;
}
$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 (!in_array($current_user_id, $normalized_user_ids, true))
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; $users[] = $current_user_id;
} }
$query = '\nSELECT 1\n FROM '.USER_ACCESS_TABLE.'\n WHERE user_id = '.$user_id.'\n AND cat_id = '.$category_id.'\n LIMIT 1\n;'; $_POST['users'] = $users;
$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_add_legal_footer_links()
{
global $template;
if (defined('IN_ADMIN'))
{
return;
}
$template->append(
'footer_elements',
' - <a href="https://links.bratonien.de/@bratonien_cosplay#bratonien-popup-856170002" rel="noopener">Impressum</a>'
.' - <a href="https://links.bratonien.de/@bratonien_cosplay#bratonien-popup-907192090" rel="noopener">Datenschutzerklärung</a>'
);
} }
function bratonien_tools_admin_menu($menu) function bratonien_tools_admin_menu($menu)
{ {
$menu[] = array( $menu[] = array(
'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;
} }
?>
-46
View File
@@ -1,46 +0,0 @@
<?php
if (PHP_SAPI !== 'cli')
{
http_response_code(404);
exit;
}
$query = '';
foreach ($argv as $arg)
{
if (strpos($arg, '--query=') === 0)
{
$query = substr($arg, strlen('--query='));
}
}
if ($query === '')
{
fwrite(STDERR, "Parameter --query fehlt.\n");
exit(2);
}
$piwigo_root = realpath(dirname(__DIR__, 4));
$plugin_root = realpath(dirname(__DIR__, 2));
if ($piwigo_root === false || $plugin_root === false || !is_file($plugin_root.'/watermark.php'))
{
fwrite(STDERR, "Piwigo-Root oder Bratonien-Wasserzeichenmodul wurde nicht gefunden.\n");
exit(3);
}
$plugin_id = basename($plugin_root);
parse_str($query, $_GET);
$_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'] = '/plugins/'.$plugin_id.'/watermark.php?'.$query;
$_SERVER['SCRIPT_NAME'] = '/plugins/'.$plugin_id.'/watermark.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = $query;
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Presentation-Refresh/0.9.7.1.41';
$_SERVER['HTTPS'] = 'off';
chdir($piwigo_root);
include $plugin_root.'/watermark.php';
+6 -138
View File
@@ -25,24 +25,12 @@ 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.
PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==") PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
class WebDavAuthenticationError(RuntimeError):
pass
class WebDavPathUnavailable(RuntimeError):
def __init__(self, relative: str, status: int) -> None:
self.relative = validate_relative(relative)
self.status = int(status)
super().__init__(f"Nextcloud path unavailable with HTTP {self.status}: {self.relative or '/'}")
def fail(message: str) -> None: def fail(message: str) -> None:
raise RuntimeError(message) raise RuntimeError(message)
@@ -66,49 +54,6 @@ 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("/")
@@ -132,11 +77,9 @@ 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" xmlns:nc="http://nextcloud.org/ns">' '<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.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:getcontentlength/><d:getetag/><oc:fileid/></d:prop></d:propfind>'
'<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)
@@ -147,10 +90,8 @@ class WebDavClient:
status = response.status status = response.status
payload = response.read() payload = response.read()
except urllib.error.HTTPError as error: except urllib.error.HTTPError as error:
if error.code == 401: if error.code in {401, 403}:
raise WebDavAuthenticationError("Nextcloud rejected the WebDAV credentials") from error fail("Nextcloud rejected the WebDAV credentials or directory access")
if error.code in {403, 404}:
raise WebDavPathUnavailable(relative, error.code) from error
fail(f"Nextcloud PROPFIND failed with HTTP {error.code}") fail(f"Nextcloud PROPFIND failed with HTTP {error.code}")
except urllib.error.URLError as error: except urllib.error.URLError as error:
fail(f"Nextcloud WebDAV is unreachable: {error.reason}") fail(f"Nextcloud WebDAV is unreachable: {error.reason}")
@@ -181,7 +122,6 @@ 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,
@@ -189,8 +129,6 @@ 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
@@ -202,25 +140,6 @@ class WebDavClient:
return current, children return current, children
def confirmed_removed_root(client: WebDavClient, remote_root: str, error: WebDavPathUnavailable) -> bool:
remote_root = validate_relative(remote_root)
if remote_root == "":
return False
if error.status == 404:
return True
if error.status != 403:
return False
parent_path = PurePosixPath(remote_root).parent
parent = "" if str(parent_path) == "." else str(parent_path).strip("/")
_, siblings = client.list_collection(parent)
wanted = PurePosixPath(remote_root).name.casefold()
for sibling in siblings:
if str(sibling.get("display_name", "")).casefold() == wanted:
return False
return True
def link_placeholder(seed: Path, target: Path) -> None: def link_placeholder(seed: Path, target: Path) -> None:
target.parent.mkdir(parents=True, exist_ok=True) target.parent.mkdir(parents=True, exist_ok=True)
try: try:
@@ -278,8 +197,6 @@ 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
@@ -333,34 +250,14 @@ def main() -> int:
staging.mkdir(parents=True) staging.mkdir(parents=True)
client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout)) client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout))
# A successful request against the user's WebDAV root proves that the
# credentials and the Nextcloud endpoint are healthy. Only after this
# guard may an individual configured root be interpreted as removed.
webdav_home, _ = client.list_collection("")
mapping: dict[str, dict[str, object]] = {} mapping: dict[str, dict[str, object]] = {}
manifest: list[str] = [] manifest: list[str] = []
total_files = total_folders = total_skipped = 0 total_files = total_folders = total_skipped = 0
used_names: set[str] = set() used_names: set[str] = set()
removed_roots: list[dict[str, object]] = []
for remote_root_raw in args.root: for remote_root_raw in args.root:
remote_root = validate_relative(remote_root_raw) remote_root = validate_relative(remote_root_raw)
try: current, _ = client.list_collection(remote_root)
current, _ = client.list_collection(remote_root)
except WebDavPathUnavailable as error:
if not confirmed_removed_root(client, remote_root, error):
raise RuntimeError(
f"Configured WebDAV root cannot be read safely; existing mirror is preserved: {remote_root or '/'} (HTTP {error.status})"
) from error
removed_roots.append({"path": remote_root, "status": error.status})
print(
f"webdav-placeholder: configured root no longer available and will be removed from the mirror: {remote_root or '/'} (HTTP {error.status})",
file=sys.stderr,
)
continue
fileid = int(current["fileid"]) fileid = int(current["fileid"])
display = str(current.get("display_name", "")).strip() or (PurePosixPath(remote_root).name if remote_root else args.user) display = str(current.get("display_name", "")).strip() or (PurePosixPath(remote_root).name if remote_root else args.user)
local_name = f"root-{fileid}" local_name = f"root-{fileid}"
@@ -372,20 +269,7 @@ def main() -> int:
total_files += files total_files += files
total_folders += folders total_folders += folders
total_skipped += skipped total_skipped += skipped
manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}")
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}")
if previous.exists(): if previous.exists():
shutil.rmtree(previous) shutil.rmtree(previous)
@@ -409,19 +293,6 @@ def main() -> int:
final_path = source_text + path[len(staging_text):] if path.startswith(staging_text) else path final_path = source_text + path[len(staging_text):] if path.startswith(staging_text) else path
final_mapping[final_path] = data final_mapping[final_path] = data
# Der Worker erwartet ein syntaktisch nicht-leeres Connector-Mapping,
# filtert aber selbst ausschließlich kind=file. Wenn alle ausgewählten
# Roots entzogen wurden, hält dieser echte WebDAV-Home-Verzeichniseintrag
# das Mapping gültig, während der Shadowtree bewusst null Bildquellen hat.
if not final_mapping:
final_mapping[source_text] = {
"kind": "folder",
"fileid": int(webdav_home["fileid"]),
"webdav_path": "",
"display_name": str(webdav_home.get("display_name", "")) or args.user,
"control_entry": True,
}
atomic_text(args.manifest, "\n".join(manifest) + "\n") atomic_text(args.manifest, "\n".join(manifest) + "\n")
atomic_json(args.mapping, { atomic_json(args.mapping, {
"version": 1, "version": 1,
@@ -431,9 +302,6 @@ def main() -> int:
}) })
print(json.dumps({ print(json.dumps({
"roots": len(args.root), "roots": len(args.root),
"roots_available": len(args.root) - len(removed_roots),
"roots_removed": len(removed_roots),
"removed_root_details": removed_roots,
"files": total_files, "files": total_files,
"folders": total_folders, "folders": total_folders,
"skipped": total_skipped, "skipped": total_skipped,
@@ -1,248 +0,0 @@
<?php
if (PHP_SAPI !== 'cli')
{
http_response_code(404);
exit;
}
$piwigo_root = realpath(dirname(__DIR__, 4));
if ($piwigo_root === false)
{
fwrite(STDERR, "Piwigo-Root wurde nicht gefunden.\n");
exit(1);
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
$_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/piwigo-cache-request-by-shadow.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Piwigo-Shadow-Cache-Bridge/0.9.7.1.41';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
require_once(PHPWG_ROOT_PATH.'admin/include/image.class.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
fwrite(STDERR, "Bratonien Tools ist nicht aktiv.\n");
exit(1);
}
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_cache_validation.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
function bratonien_tools_shadow_bridge_fail($message, $code=1)
{
fwrite(STDERR, $message."\n");
exit((int)$code);
}
function bratonien_tools_shadow_bridge_i_request($target)
{
$root = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR;
$target = str_replace('\\', '/', (string)$target);
$root = str_replace('\\', '/', (string)$root);
if (strpos($target, $root) !== 0) return null;
$relative = ltrim(substr($target, strlen($root)), '/');
if ($relative === '') return null;
return '/i.php?/'.implode('/', array_map('rawurlencode', explode('/', $relative)));
}
function bratonien_tools_shadow_bridge_call_i($request, &$detail=null)
{
$detail = '';
$runner = BRATONIEN_TOOLS_PATH.'runtime/lib/piwigo-derivative-call.php';
if (!is_file($runner)) { $detail = 'Piwigo-Derivataufruf fehlt.'; return false; }
if (!function_exists('exec')) { $detail = 'PHP exec() ist deaktiviert.'; return false; }
$output = array();
$exit = 1;
@exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($runner).' --request='.escapeshellarg($request).' 2>&1', $output, $exit);
if ($exit !== 0)
{
$detail = 'Piwigo-Aufruf Exit '.$exit;
if ($output) $detail .= ': '.implode(' ', array_slice($output, -3));
return false;
}
return true;
}
function bratonien_tools_shadow_bridge_sync_actual_metadata(array &$row, $source_path)
{
$size = @getimagesize($source_path);
if (!is_array($size) || (int)($size[0] ?? 0) < 1 || (int)($size[1] ?? 0) < 1)
{
bratonien_tools_shadow_bridge_fail('Das temporär eingesetzte Original besitzt keine lesbaren Bildabmessungen.');
}
$width = (int)$size[0];
$height = (int)$size[1];
$rotation_angle = pwg_image::get_rotation_angle($source_path);
$rotation_angle = $rotation_angle === null ? 0 : (int)$rotation_angle;
$rotation = pwg_image::get_rotation_code_from_angle($rotation_angle);
if ($rotation === null) $rotation = 0;
$rotation = (int)$rotation;
if (
(int)($row['width'] ?? 0) === $width
&& (int)($row['height'] ?? 0) === $height
&& isset($row['rotation'])
&& (int)$row['rotation'] === $rotation
)
{
return false;
}
pwg_query(
'UPDATE '.IMAGES_TABLE
.' SET width='.$width.', height='.$height.', rotation='.$rotation
.' WHERE id='.(int)$row['id'].' LIMIT 1'
);
$row['width'] = $width;
$row['height'] = $height;
$row['rotation'] = $rotation;
return true;
}
function bratonien_tools_shadow_bridge_variants(array $row, $stage)
{
$src = new SrcImage($row);
$variants = array();
foreach (ImageStdParams::get_defined_type_map() as $type=>$params)
{
$derivative = new DerivativeImage($params, $src);
if ($derivative->same_as_source()) continue;
$size = $derivative->get_size();
$max_edge = max((int)($size[0] ?? 0), (int)($size[1] ?? 0));
$priority = $max_edge > 0 && $max_edge <= 1920;
if (($stage === 1 && $priority) || ($stage === 2 && !$priority))
{
$variants[] = array('name'=>'standard:'.$type, 'derivative'=>$derivative, 'params'=>$params, 'target'=>$derivative->get_path());
}
}
foreach (ImageStdParams::$custom as $key=>$last_used)
{
$params = bratonien_tools_webdav_materialize_custom_params($key);
if (!$params) continue;
$derivative = new DerivativeImage($params, $src);
if ($derivative->same_as_source()) continue;
$size = $derivative->get_size();
$max_edge = max((int)($size[0] ?? 0), (int)($size[1] ?? 0));
$priority = $max_edge > 0 && $max_edge <= 1920;
if (($stage === 1 && $priority) || ($stage === 2 && !$priority))
{
$variants[] = array('name'=>'custom:'.$key, 'derivative'=>$derivative, 'params'=>$params, 'target'=>$derivative->get_path());
}
}
return $variants;
}
$connection_id = 0;
$shadow_relative = '';
$stage = 0;
foreach ($argv as $arg)
{
if (preg_match('/^--connection-id=(\d+)$/', $arg, $m)) $connection_id = (int)$m[1];
elseif (strpos($arg, '--shadow-relative=') === 0) $shadow_relative = ltrim(substr($arg, strlen('--shadow-relative=')), '/');
elseif (preg_match('/^--stage=([12])$/', $arg, $m)) $stage = (int)$m[1];
}
if ($connection_id < 1 || $shadow_relative === '' || !in_array($stage, array(1,2), true))
{
bratonien_tools_shadow_bridge_fail('Parameter --connection-id, --shadow-relative und --stage=1|2 werden benötigt.', 2);
}
if (strpos($shadow_relative, '..') !== false || strpos($shadow_relative, "\0") !== false)
{
bratonien_tools_shadow_bridge_fail('Unsicherer Shadow-Pfad.', 2);
}
$connection = bratonien_tools_nc_connector_connection($connection_id, false);
if (!$connection || empty($connection['enabled'])) bratonien_tools_shadow_bridge_fail('WebDAV-Verbindung wurde nicht gefunden.');
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$gallery_root = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
if ($gallery_root === '' || !is_dir($gallery_root)) bratonien_tools_shadow_bridge_fail('Shadow-Tree der Verbindung fehlt.');
$shadow_absolute = $gallery_root.'/'.$shadow_relative;
if (!is_link($shadow_absolute)) bratonien_tools_shadow_bridge_fail('Shadow-Datei ist kein Symlink: '.$shadow_relative);
$shadow_real = realpath($shadow_absolute);
if ($shadow_real === false || !is_file($shadow_real)) bratonien_tools_shadow_bridge_fail('Shadow-Ziel ist nicht lesbar: '.$shadow_relative);
$root = rtrim(str_replace('\\', '/', PHPWG_ROOT_PATH), '/');
$gallery = rtrim(str_replace('\\', '/', $gallery_root), '/');
if (strpos($gallery.'/', $root.'/') !== 0) bratonien_tools_shadow_bridge_fail('Shadow-Tree liegt außerhalb von Piwigo.');
$relative_gallery = ltrim(substr($gallery, strlen($root)), '/');
$db_path = './'.$relative_gallery.'/'.$shadow_relative;
$result = pwg_query("SELECT * FROM ".IMAGES_TABLE." WHERE path='".pwg_db_real_escape_string($db_path)."' LIMIT 2");
$rows = array();
while ($row = pwg_db_fetch_assoc($result)) $rows[] = $row;
if (count($rows) !== 1)
{
bratonien_tools_shadow_bridge_fail('Piwigo kann den Shadow-Pfad nicht eindeutig einem Bild zuordnen: '.$db_path);
}
$row = $rows[0];
$metadata_repaired = bratonien_tools_shadow_bridge_sync_actual_metadata($row, $shadow_real);
$variants = bratonien_tools_shadow_bridge_variants($row, $stage);
$generated = 0;
$already = 0;
$invalid_removed = 0;
foreach ($variants as $variant)
{
$reason = '';
if (bratonien_tools_webdav_derivative_cache_valid($variant['target'], $variant['derivative'], $variant['params'], $reason))
{
$already++;
continue;
}
// Ein vorhandenes, aber geometrisch falsches Derivat darf nicht an i.php
// vorbeikommen. Piwigo entscheidet sonst allein anhand von mtimes und kann
// eine alte Datei trotz korrigierter Breite/Höhe/EXIF-Rotation behalten.
if (is_file($variant['target']))
{
if (!@unlink($variant['target']))
{
bratonien_tools_shadow_bridge_fail($variant['name'].': ungültiges Alt-Derivat konnte nicht entfernt werden ('.$reason.').');
}
$invalid_removed++;
}
$request = bratonien_tools_shadow_bridge_i_request($variant['target']);
if ($request === null) bratonien_tools_shadow_bridge_fail($variant['name'].': ungültiger Piwigo-Derivatpfad.');
$detail = '';
if (!bratonien_tools_shadow_bridge_call_i($request, $detail))
{
bratonien_tools_shadow_bridge_fail($variant['name'].': '.$detail);
}
clearstatcache(true, $variant['target']);
$reason = '';
if (!bratonien_tools_webdav_derivative_cache_valid($variant['target'], $variant['derivative'], $variant['params'], $reason))
{
bratonien_tools_shadow_bridge_fail($variant['name'].': Piwigo hat kein gültiges Derivat erzeugt ('.$reason.').');
}
$generated++;
}
echo json_encode(array(
'ok'=>true,
'connection_id'=>$connection_id,
'shadow_relative'=>$shadow_relative,
'stage'=>$stage,
'piwigo_image_id'=>(int)$row['id'],
'metadata_repaired'=>$metadata_repaired,
'variants'=>count($variants),
'generated'=>$generated,
'already_valid'=>$already,
'invalid_removed'=>$invalid_removed,
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)."\n";
exit(0);
-46
View File
@@ -1,46 +0,0 @@
<?php
if (PHP_SAPI !== 'cli')
{
http_response_code(404);
exit;
}
$request = '';
foreach ($argv as $arg)
{
if (strpos($arg, '--request=') === 0)
{
$request = substr($arg, strlen('--request='));
}
}
if ($request === '' || strpos($request, '/i.php?/') !== 0)
{
fwrite(STDERR, "Ungültige Piwigo-Derivatanforderung.\n");
exit(2);
}
$piwigo_root = realpath(dirname(__DIR__, 4));
if ($piwigo_root === false || !is_file($piwigo_root.'/i.php'))
{
fwrite(STDERR, "Piwigo i.php wurde nicht gefunden.\n");
exit(3);
}
chdir($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'] = $request;
$_SERVER['SCRIPT_NAME'] = '/i.php';
$_SERVER['PHP_SELF'] = '/i.php';
$_SERVER['QUERY_STRING'] = ltrim((string)parse_url($request, PHP_URL_QUERY), '?');
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Warmup-Piwigo-Caller/0.9.7.1.8';
$_SERVER['HTTPS'] = 'off';
// Piwigo selbst erzeugt das Derivat. Der Warmup benötigt den ausgelieferten
// Bild-Body nicht und prüft danach ausschließlich Piwigos Cache-Datei.
ob_start(function ($buffer) { return ''; }, 1);
include $piwigo_root.'/i.php';
+1 -1
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.8.21', CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.6.1',
)); ));
$body = curl_exec($ch); $body = curl_exec($ch);
$errno = curl_errno($ch); $errno = curl_errno($ch);
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
STATE_DIR="${1:-}"
shift || true
[[ -n "$STATE_DIR" ]] || { echo "Connector-State-Verzeichnis fehlt." >&2; exit 2; }
[[ $# -gt 0 ]] || { echo "Warmup-Kommando fehlt." >&2; exit 2; }
SYNC_LOCK="$STATE_DIR/webdav-sync.lock"
WAITER_LOCK="$STATE_DIR/webdav-cache-warmup-after-sync.lock"
# Pro Verbindung darf genau ein wartender Cache-Start existieren. Dadurch
# erzeugen wiederholte Klicks im Adminbereich keine Warteschlange aus Workern.
exec 8>"$WAITER_LOCK"
flock -n 8 || exit 0
# Der Connector hält diesen Lock exklusiv. Wir warten blockierend, ohne dabei
# Nextcloud-Dateien oder den Worker-Index anzufassen. Sobald der Connector den
# Lock freigibt, geben wir unseren kurzen Leselock sofort wieder frei und
# starten den normalen Worker. Dessen eigene Guards bleiben damit maßgeblich.
exec 9<>"$SYNC_LOCK"
flock -s 9
flock -u 9
exec "$@"
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
STATE_DIR="${1:-}"
shift || true
[[ -n "$STATE_DIR" ]] || { echo "Connector-State-Verzeichnis fehlt." >&2; exit 2; }
[[ $# -gt 0 ]] || { echo "Warmup-Kommando fehlt." >&2; exit 2; }
PRIORITY_FILE="$STATE_DIR/webdav-cache-warmup-priority-sync"
ATTEMPTS=0
MAX_ATTEMPTS=3600
# Der Dispatcher setzt die Prioritätsmarke vor dem Start. Falls bereits ein
# Warmup läuft, beendet der zusätzliche Worker sich wegen seines Prozess-Locks
# sofort und lässt die Marke unangetastet. Wir probieren deshalb erst dann
# erneut, wenn die Marke noch vorhanden ist. Sobald ein Sync-Worker den Lock
# tatsächlich übernimmt, entfernt er die Marke selbst und arbeitet den neuen
# Bestand ab. Dadurch geht die Sofort-Priorität nicht verloren und es gibt nie
# zwei gleichzeitig arbeitende Warmup-Worker derselben Verbindung.
while [[ -f "$PRIORITY_FILE" ]]; do
ATTEMPTS=$((ATTEMPTS + 1))
if [[ "$ATTEMPTS" -gt "$MAX_ATTEMPTS" ]]; then
echo "Priorisierter WebDAV-Warmup wartet seit zu langer Zeit auf den aktiven Worker." >&2
exit 3
fi
set +e
"$@"
result=$?
set -e
if [[ "$result" -ne 0 ]]; then
exit "$result"
fi
[[ -f "$PRIORITY_FILE" ]] || exit 0
sleep 1
done
exit 0
+2 -8
View File
@@ -12,7 +12,6 @@ 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:
@@ -80,13 +79,8 @@ 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:
ensure_shadow_directory(target) target.mkdir(parents=True, exist_ok=True)
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():
@@ -122,7 +116,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()
ensure_shadow_directory(staging) staging.mkdir(parents=True)
try: try:
used_roots: set[str] = set() used_roots: set[str] = set()
-125
View File
@@ -1,125 +0,0 @@
#!/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();
$mapped_files = 0;
foreach ($mapping['files'] as $path => $entry)
{
if (!is_array($entry) || (string)($entry['kind'] ?? '') !== 'file') continue;
$mapped_files++;
$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 ($mapped_files === 0)
{
echo "WebDAV-Metadaten: keine aktuellen Bildquellen vorhanden.\n";
exit(0);
}
// Nextcloud liefert die Bildabmessungen ueber WebDAV nicht fuer jede Datei.
// Das ist kein Fehler des Connector-Syncs: die Dateien und ihr Shadow Tree
// sind trotzdem gueltig und Piwigo kann die Bildquelle spaeter bei Bedarf
// materialisieren. Vorhandene Abmessungen werden uebernommen, fehlende Werte
// bleiben unangetastet. Nur ein syntaktisch/technisch ungueltiges Mapping ist
// oben weiterhin fatal.
if (!$dimensions)
{
echo 'WebDAV-Metadaten: bilder='.$mapped_files.' aktualisiert=0 ohne_masse='.$mapped_files." (Nextcloud lieferte keine Bildabmessungen; Synchronisierung bleibt gueltig)\n";
exit(0);
}
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.7.1.47';
$_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;
if ($missing > 0)
{
echo ' (fehlende WebDAV-Abmessungen sind nicht fatal)';
}
echo "\n";
exit(0);
-844
View File
@@ -1,844 +0,0 @@
<?php
if (PHP_SAPI !== 'cli')
{
http_response_code(404);
exit;
}
$piwigo_root = realpath(dirname(__DIR__, 4));
if ($piwigo_root === false)
{
fwrite(STDERR, "Piwigo-Root wurde nicht gefunden.\n");
exit(1);
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
$_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/webdav-cache-warmup.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Cache-Worker/0.9.7.1.30';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
fwrite(STDERR, "Bratonien Tools ist nicht aktiv.\n");
exit(1);
}
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_source_index.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_warmup_settings.inc.php');
function bratonien_tools_cache_warmup_log($event, array $fields=array())
{
$parts = array('[BRAT-WORKER]', $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, JSON_UNESCAPED_SLASHES);
$parts[] = $key.'='.str_replace(array("\r", "\n"), array('\\r', '\\n'), (string)$value);
}
fwrite(STDOUT, implode(' ', $parts)."\n");
}
function bratonien_tools_cache_warmup_status_file($connection_id)
{
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.status-'.(int)$connection_id.'.json';
}
function bratonien_tools_cache_warmup_cancel_file($connection_id)
{
return PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.cancel-'.(int)$connection_id;
}
function bratonien_tools_cache_warmup_cancel_pending($connection_id)
{
return is_file(bratonien_tools_cache_warmup_cancel_file($connection_id));
}
function bratonien_tools_cache_warmup_priority_file($state_dir)
{
return rtrim((string)$state_dir, '/').'/webdav-cache-warmup-priority-sync';
}
function bratonien_tools_cache_warmup_priority_pending($priority_file)
{
return is_string($priority_file) && $priority_file !== '' && is_file($priority_file);
}
function bratonien_tools_cache_warmup_write_json($file, array $payload)
{
$directory = dirname($file);
if (!is_dir($directory) && !@mkdir($directory, 0775, true) && !is_dir($directory))
{
throw new RuntimeException('Worker-Statusverzeichnis konnte nicht angelegt werden.');
}
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if ($json === false) throw new RuntimeException('Worker-Status konnte nicht serialisiert werden.');
$tmp = $file.'.tmp-'.bin2hex(random_bytes(4));
if (@file_put_contents($tmp, $json."\n", LOCK_EX) === false) throw new RuntimeException('Worker-Status konnte nicht geschrieben werden.');
@chmod($tmp, 0664);
if (!@rename($tmp, $file))
{
@unlink($tmp);
throw new RuntimeException('Worker-Status konnte nicht atomar gespeichert werden.');
}
}
function bratonien_tools_cache_warmup_status($connection_id, $state, $message, array $extra=array())
{
bratonien_tools_cache_warmup_write_json(
bratonien_tools_cache_warmup_status_file($connection_id),
array_merge(array(
'state'=>$state,
'message'=>$message,
'connection_id'=>(int)$connection_id,
'updated_at'=>time(),
), $extra)
);
}
function bratonien_tools_cache_warmup_credentials($connection_id)
{
$connection = bratonien_tools_nc_connector_connection((int)$connection_id, true);
if (!$connection || empty($connection['enabled'])) return null;
$plain = bratonien_tools_nc_connector_decrypt_secret((string)($connection['secret_blob'] ?? ''));
$secret = json_decode($plain, true);
if (!is_array($secret)) return null;
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$base_url = rtrim((string)($config['nextcloud_url'] ?? ''), '/');
$user = trim((string)($secret['nextcloud_user'] ?? ''));
$password = (string)($secret['nextcloud_password'] ?? '');
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
$gallery_root = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
if ($base_url === '' || $user === '' || $password === '' || $state_dir === '' || $gallery_root === '') return null;
return array(
'connection'=>$connection,
'config'=>$config,
'base_url'=>$base_url,
'user'=>$user,
'password'=>$password,
'state_dir'=>$state_dir,
'gallery_root'=>$gallery_root,
);
}
function bratonien_tools_cache_warmup_load_json($file, $label)
{
if (!is_file($file) || !is_readable($file)) throw new RuntimeException($label.' fehlt oder ist nicht lesbar: '.$file);
$raw = @file_get_contents($file);
$data = $raw !== false ? json_decode($raw, true) : null;
if (!is_array($data)) throw new RuntimeException($label.' enthält kein gültiges JSON.');
return $data;
}
function bratonien_tools_cache_warmup_normalize_path($path)
{
return rtrim(str_replace('\\', '/', (string)$path), '/');
}
function bratonien_tools_cache_warmup_scan($connection_id, array $credentials)
{
$state_dir = $credentials['state_dir'];
$gallery_root = $credentials['gallery_root'];
$mapping = bratonien_tools_cache_warmup_load_json($state_dir.'/webdav-map.json', 'WebDAV-Mapping');
$files = isset($mapping['files']) && is_array($mapping['files']) ? $mapping['files'] : array();
if (!$files) throw new RuntimeException('WebDAV-Mapping enthält keine Quellen.');
if (!is_dir($gallery_root)) throw new RuntimeException('Shadow-Tree fehlt: '.$gallery_root);
$by_source = array();
foreach ($files as $path=>$meta)
{
if (!is_array($meta) || (string)($meta['kind'] ?? '') !== 'file') continue;
$normalized = bratonien_tools_cache_warmup_normalize_path($path);
$by_source[$normalized] = $meta;
}
$sources = array();
$seen_targets = array();
$gallery_prefix = bratonien_tools_cache_warmup_normalize_path(realpath($gallery_root) ?: $gallery_root).'/';
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($gallery_root, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $item)
{
$shadow_absolute = $item->getPathname();
if (!is_link($shadow_absolute)) continue;
$resolved = realpath($shadow_absolute);
if ($resolved === false || !is_file($resolved)) throw new RuntimeException('Shadow-Link zeigt auf keine lesbare Quelle: '.$shadow_absolute);
$resolved = bratonien_tools_cache_warmup_normalize_path($resolved);
if (!isset($by_source[$resolved])) throw new RuntimeException('Shadow-Link besitzt keinen Eintrag im Connector-Mapping: '.$shadow_absolute);
if (isset($seen_targets[$resolved])) throw new RuntimeException('Sicherheitsabbruch: dieselbe Connector-Quelle ist mehrfach im Shadow-Tree verknüpft: '.$resolved);
$seen_targets[$resolved] = true;
$shadow_normalized = bratonien_tools_cache_warmup_normalize_path($shadow_absolute);
if (strpos($shadow_normalized.'/', $gallery_prefix) !== 0) throw new RuntimeException('Shadow-Datei liegt außerhalb der erwarteten Galerie-Wurzel.');
$shadow_relative = ltrim(substr($shadow_normalized, strlen(rtrim($gallery_prefix, '/'))), '/');
if ($shadow_relative === '') throw new RuntimeException('Leerer relativer Shadow-Pfad erkannt.');
$meta = $by_source[$resolved];
$root_fileid = 0;
if (preg_match('#/root-([0-9]+)/#', $resolved.'/', $m)) $root_fileid = (int)$m[1];
if ($root_fileid < 1) throw new RuntimeException('Connector-Wurzel konnte aus der Placeholder-Quelle nicht bestimmt werden: '.$resolved);
$source = array(
'connection_id'=>(int)$connection_id,
'root_fileid'=>$root_fileid,
'fileid'=>(int)($meta['fileid'] ?? 0),
'webdav_path'=>trim((string)($meta['webdav_path'] ?? ''), '/'),
'content_type'=>(string)($meta['content_type'] ?? ''),
'size'=>(int)($meta['size'] ?? 0),
'etag'=>(string)($meta['etag'] ?? ''),
'width'=>(int)($meta['width'] ?? 0),
'height'=>(int)($meta['height'] ?? 0),
);
if ($source['webdav_path'] === '') throw new RuntimeException('Connector-Mapping enthält für '.$shadow_relative.' keinen WebDAV-Pfad.');
$key = bratonien_tools_webdav_source_index_key($source);
if (isset($sources[$key])) throw new RuntimeException('Doppelte Quellenidentität im Shadow-Tree: '.$key);
$sources[$key] = array(
'index_key'=>$key,
'signature'=>bratonien_tools_webdav_source_index_signature($source),
'source'=>$source,
'source_path'=>$resolved,
'shadow_absolute'=>$shadow_absolute,
'shadow_relative'=>$shadow_relative,
);
}
return array('sources'=>$sources);
}
function bratonien_tools_cache_warmup_select(array $scan, array $index, $mode)
{
if ($mode === 'rebuild') return $scan['sources'];
$selected = array();
foreach ($scan['sources'] as $key=>$item)
{
$entry = isset($index['sources'][$key]) && is_array($index['sources'][$key]) ? $index['sources'][$key] : null;
if ($entry === null || !bratonien_tools_webdav_source_index_is_current($entry, $item['signature'])) $selected[$key] = $item;
}
return $selected;
}
function bratonien_tools_cache_warmup_quote_path($path)
{
$parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen'));
return implode('/', array_map('rawurlencode', $parts));
}
function bratonien_tools_cache_warmup_download(array $source, array $credentials, $destination, &$detail=null)
{
$detail = '';
$webdav_path = trim((string)($source['webdav_path'] ?? ''), '/');
if ($webdav_path === '') { $detail = 'WebDAV-Pfad fehlt.'; return false; }
$fp = @fopen($destination, 'xb');
if (!$fp) { $detail = 'Temporäre Quelldatei konnte nicht angelegt werden.'; return false; }
$url = $credentials['base_url'].'/remote.php/dav/files/'.rawurlencode($credentials['user']).'/'.bratonien_tools_cache_warmup_quote_path($webdav_path);
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION=>false,
CURLOPT_CONNECTTIMEOUT=>10,
CURLOPT_TIMEOUT=>300,
CURLOPT_HTTPAUTH=>CURLAUTH_BASIC,
CURLOPT_USERPWD=>$credentials['user'].':'.$credentials['password'],
CURLOPT_RETURNTRANSFER=>false,
CURLOPT_FAILONERROR=>false,
CURLOPT_FILE=>$fp,
CURLOPT_USERAGENT=>'Bratonien-WebDAV-Cache-Worker/0.9.7.1.30',
));
$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 = 'Geladene Datei ist kein lesbares vollständiges Bild.';
return false;
}
return true;
}
function bratonien_tools_cache_warmup_sync_guard(array $credentials, &$handle=null, &$detail=null)
{
$handle = null;
$detail = '';
$handle = @fopen($credentials['state_dir'].'/webdav-sync.lock', 'c');
if (!$handle) { $detail = 'Connector-Sync-Lock konnte nicht geöffnet werden.'; return false; }
if (!@flock($handle, LOCK_SH | LOCK_NB))
{
fclose($handle);
$handle = null;
$detail = 'Connector-Synchronisierung läuft.';
return false;
}
return true;
}
function bratonien_tools_cache_warmup_source_lock($index_key, &$handle=null)
{
$dir = PHPWG_ROOT_PATH.'upload/bratonien-webdav-materialize';
if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) return false;
$handle = @fopen($dir.'/source-'.sha1((string)$index_key).'.lock', 'c');
if (!$handle) return false;
return @flock($handle, LOCK_EX);
}
function bratonien_tools_cache_warmup_unlock(&$handle)
{
if (is_resource($handle))
{
@flock($handle, LOCK_UN);
fclose($handle);
}
$handle = null;
}
function bratonien_tools_cache_warmup_fresh_source(array $item, array $credentials)
{
$mapping = bratonien_tools_cache_warmup_load_json($credentials['state_dir'].'/webdav-map.json', 'WebDAV-Mapping');
$files = isset($mapping['files']) && is_array($mapping['files']) ? $mapping['files'] : array();
$wanted = bratonien_tools_cache_warmup_normalize_path($item['source_path']);
foreach ($files as $path=>$meta)
{
if (bratonien_tools_cache_warmup_normalize_path($path) !== $wanted || !is_array($meta)) continue;
$source = array(
'connection_id'=>(int)$item['source']['connection_id'],
'root_fileid'=>(int)$item['source']['root_fileid'],
'fileid'=>(int)($meta['fileid'] ?? 0),
'webdav_path'=>trim((string)($meta['webdav_path'] ?? ''), '/'),
'content_type'=>(string)($meta['content_type'] ?? ''),
'size'=>(int)($meta['size'] ?? 0),
'etag'=>(string)($meta['etag'] ?? ''),
'width'=>(int)($meta['width'] ?? 0),
'height'=>(int)($meta['height'] ?? 0),
);
return $source;
}
return null;
}
function bratonien_tools_cache_warmup_restore($source_path, $backup, $staging, $original_size, $original_hash, &$detail=null)
{
$detail = '';
@unlink($staging);
if (is_file($source_path) || is_link($source_path))
{
if (!@unlink($source_path)) { $detail = 'Temporäre Quelle konnte nicht entfernt werden.'; return false; }
}
if (!is_file($backup)) { $detail = 'Placeholder-Backup fehlt.'; return false; }
if (!@rename($backup, $source_path)) { $detail = 'Placeholder konnte nicht restauriert werden.'; return false; }
clearstatcache(true, $source_path);
if (!is_file($source_path)) { $detail = 'Restaurierter Placeholder fehlt.'; return false; }
if ((int)filesize($source_path) !== (int)$original_size) { $detail = 'Restaurierter Placeholder hat eine unerwartete Größe.'; return false; }
$hash = @sha1_file($source_path);
if (!is_string($hash) || !hash_equals($original_hash, $hash)) { $detail = 'Restaurierter Placeholder stimmt nicht mit dem Originalzustand überein.'; return false; }
return true;
}
function bratonien_tools_cache_warmup_call_piwigo(array $item, $stage, &$detail=null)
{
$detail = '';
$bridge = BRATONIEN_TOOLS_PATH.'runtime/lib/piwigo-cache-request-by-shadow.php';
if (!is_file($bridge)) { $detail = 'Piwigo-Shadow-Bridge fehlt.'; return false; }
if (!function_exists('exec')) { $detail = 'PHP exec() ist deaktiviert.'; return false; }
$output = array();
$exit = 1;
$command = escapeshellarg(PHP_BINARY).' '.escapeshellarg($bridge)
.' --connection-id='.(int)$item['source']['connection_id']
.' --shadow-relative='.escapeshellarg($item['shadow_relative'])
.' --stage='.(int)$stage.' 2>&1';
@exec($command, $output, $exit);
if ($exit !== 0)
{
$detail = $output ? implode(' ', array_slice($output, -4)) : 'Piwigo-Shadow-Bridge Exit '.$exit;
return false;
}
$detail = $output ? (string)end($output) : '';
return true;
}
function bratonien_tools_cache_warmup_process(array $item, $temp_file, array $credentials, $stage)
{
$source_lock = null;
if (!bratonien_tools_cache_warmup_source_lock($item['index_key'], $source_lock))
{
return array('ok'=>false, 'defer'=>true, 'message'=>'Quellen-Lock konnte nicht gesetzt werden.');
}
$sync_lock = null;
$detail = '';
if (!bratonien_tools_cache_warmup_sync_guard($credentials, $sync_lock, $detail))
{
bratonien_tools_cache_warmup_unlock($source_lock);
return array('ok'=>false, 'defer'=>true, 'message'=>$detail);
}
$fresh = bratonien_tools_cache_warmup_fresh_source($item, $credentials);
if (!$fresh || bratonien_tools_webdav_source_index_signature($fresh) !== $item['signature'])
{
bratonien_tools_cache_warmup_unlock($sync_lock);
bratonien_tools_cache_warmup_unlock($source_lock);
return array('ok'=>false, 'defer'=>true, 'message'=>'Connector-Mapping hat sich seit dem Batch-Download geändert.');
}
if (!is_link($item['shadow_absolute']) || bratonien_tools_cache_warmup_normalize_path(realpath($item['shadow_absolute']) ?: '') !== bratonien_tools_cache_warmup_normalize_path($item['source_path']))
{
bratonien_tools_cache_warmup_unlock($sync_lock);
bratonien_tools_cache_warmup_unlock($source_lock);
return array('ok'=>false, 'defer'=>true, 'message'=>'Shadow-Tree hat sich seit dem Batch-Download geändert.');
}
$source_path = $item['source_path'];
if (!is_file($source_path) || !is_readable($source_path) || !is_file($temp_file) || @getimagesize($temp_file) === false)
{
bratonien_tools_cache_warmup_unlock($sync_lock);
bratonien_tools_cache_warmup_unlock($source_lock);
return array('ok'=>false, 'message'=>'Placeholder oder Batch-Quelle ist nicht vollständig lesbar.');
}
$placeholder_size = filesize($source_path);
$placeholder_hash = @sha1_file($source_path);
if (!is_string($placeholder_hash))
{
bratonien_tools_cache_warmup_unlock($sync_lock);
bratonien_tools_cache_warmup_unlock($source_lock);
return array('ok'=>false, 'fatal'=>true, 'message'=>'Placeholder konnte vor dem Swap nicht verifiziert werden.');
}
$request_id = substr(bin2hex(random_bytes(8)), 0, 8);
$backup = $source_path.'.bratonien-placeholder-'.$request_id;
$staging = $source_path.'.bratonien-source-'.$request_id;
if (file_exists($backup) || file_exists($staging))
{
bratonien_tools_cache_warmup_unlock($sync_lock);
bratonien_tools_cache_warmup_unlock($source_lock);
return array('ok'=>false, 'fatal'=>true, 'message'=>'Unerwartete Swap-Datei ist bereits vorhanden.');
}
if (!@copy($temp_file, $staging) || @getimagesize($staging) === false)
{
@unlink($staging);
bratonien_tools_cache_warmup_unlock($sync_lock);
bratonien_tools_cache_warmup_unlock($source_lock);
return array('ok'=>false, 'message'=>'Batch-Quelle konnte nicht sicher neben dem Placeholder bereitgestellt werden.');
}
$swapped = false;
try
{
if (!@rename($source_path, $backup)) throw new RuntimeException('Placeholder konnte nicht atomar gesichert werden.');
if (!@rename($staging, $source_path))
{
@rename($backup, $source_path);
throw new RuntimeException('Temporäre Quelle konnte nicht atomar eingesetzt werden.');
}
$swapped = true;
clearstatcache(true, $source_path);
if (@getimagesize($source_path) === false) throw new RuntimeException('Shadow-Quelle enthält nach dem Swap kein lesbares Bild.');
$bridge_detail = '';
if (!bratonien_tools_cache_warmup_call_piwigo($item, $stage, $bridge_detail))
{
throw new RuntimeException('Piwigo-Verarbeitung fehlgeschlagen: '.$bridge_detail);
}
}
catch (Throwable $e)
{
$restore_detail = '';
$restored = $swapped
? bratonien_tools_cache_warmup_restore($source_path, $backup, $staging, $placeholder_size, $placeholder_hash, $restore_detail)
: true;
if (!$swapped) @unlink($staging);
bratonien_tools_cache_warmup_unlock($sync_lock);
bratonien_tools_cache_warmup_unlock($source_lock);
if (!$restored) return array('ok'=>false, 'fatal'=>true, 'message'=>$e->getMessage().' RESTORE FEHLGESCHLAGEN: '.$restore_detail);
return array('ok'=>false, 'message'=>$e->getMessage());
}
$restore_detail = '';
$restored = bratonien_tools_cache_warmup_restore($source_path, $backup, $staging, $placeholder_size, $placeholder_hash, $restore_detail);
bratonien_tools_cache_warmup_unlock($sync_lock);
bratonien_tools_cache_warmup_unlock($source_lock);
if (!$restored) return array('ok'=>false, 'fatal'=>true, 'message'=>'RESTORE FEHLGESCHLAGEN: '.$restore_detail);
return array('ok'=>true, 'message'=>'Piwigo hat den Shadow-Pfad verarbeitet.');
}
function bratonien_tools_cache_warmup_stage_pending(array $selected, array $index, $stage)
{
$pending = array();
$field = $stage === 1 ? 'stage1_signature' : 'stage2_signature';
foreach ($selected as $key=>$item)
{
$entry = isset($index['sources'][$key]) && is_array($index['sources'][$key]) ? $index['sources'][$key] : array();
if (!isset($entry[$field]) || !hash_equals((string)$entry[$field], $item['signature'])) $pending[$key] = $item;
}
return $pending;
}
function bratonien_tools_cache_warmup_stage_completed_count(array $selected, array $index, $stage)
{
return count($selected) - count(bratonien_tools_cache_warmup_stage_pending($selected, $index, $stage));
}
function bratonien_tools_cache_warmup_run_stage($connection_id, $stage, array $selected, array &$index, array $credentials, $batch_size, $priority_file='')
{
$pending = bratonien_tools_cache_warmup_stage_pending($selected, $index, $stage);
if (!$pending) return array('failed'=>0, 'deferred'=>0, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>false, 'completed'=>0);
if (bratonien_tools_cache_warmup_cancel_pending($connection_id))
{
return array('failed'=>0, 'deferred'=>0, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>true, 'completed'=>0);
}
if ($stage === 2 && bratonien_tools_cache_warmup_priority_pending($priority_file))
{
return array('failed'=>0, 'deferred'=>0, 'waiting'=>false, 'preempted'=>true, 'cancelled'=>false, 'completed'=>0);
}
$temp_root = PHPWG_ROOT_PATH.'upload/bratonien-webdav-warmup';
if (!is_dir($temp_root) && !@mkdir($temp_root, 0775, true) && !is_dir($temp_root)) throw new RuntimeException('Worker-Temp-Verzeichnis konnte nicht angelegt werden.');
$failed = 0;
$deferred = 0;
$completed = 0;
$batch_number = 0;
foreach (array_chunk(array_keys($pending), max(1, (int)$batch_size)) as $keys)
{
// Harte Batch-Grenze: Sobald ein Abbruchsignal existiert, darf dieser
// Batch gar nicht mehr begonnen und damit auch kein weiteres Original
// aus Nextcloud geladen werden.
if (bratonien_tools_cache_warmup_cancel_pending($connection_id))
{
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>true, 'completed'=>$completed);
}
if ($stage === 2 && bratonien_tools_cache_warmup_priority_pending($priority_file))
{
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>true, 'cancelled'=>false, 'completed'=>$completed);
}
// Der Connector-Lock wird VOR jedem Download geprueft und fuer den
// kompletten Batch gemeinsam gehalten. Ist der Connector aktiv, werden
// exakt null Bilder dieses Batches geladen.
$batch_sync_lock = null;
$batch_sync_detail = '';
if (!bratonien_tools_cache_warmup_sync_guard($credentials, $batch_sync_lock, $batch_sync_detail))
{
bratonien_tools_cache_warmup_status($connection_id, 'waiting', 'Connector-Synchronisierung läuft. Der nächste Batch startet erst danach; es wurden für diesen Batch keine Nextcloud-Originale geladen.', array(
'stage'=>$stage,
'batch'=>$batch_number + 1,
'selected_total'=>count($selected),
'stage_completed'=>$completed,
'deferred_by_connector'=>true,
));
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>true, 'preempted'=>false, 'cancelled'=>false, 'completed'=>$completed);
}
$batch_number++;
$dir = $temp_root.'/connection-'.(int)$connection_id.'-stage'.$stage.'-'.date('YmdHis').'-'.bin2hex(random_bytes(4));
if (!@mkdir($dir, 0775, true) && !is_dir($dir))
{
bratonien_tools_cache_warmup_unlock($batch_sync_lock);
throw new RuntimeException('Worker-Batch-Verzeichnis konnte nicht angelegt werden.');
}
$downloads = array();
foreach ($keys as $key)
{
$file = $dir.'/'.sha1($key).'.img';
$detail = '';
if (bratonien_tools_cache_warmup_download($pending[$key]['source'], $credentials, $file, $detail)) $downloads[$key] = $file;
else
{
$failed++;
bratonien_tools_cache_warmup_log('download_failed', array('stage'=>$stage, 'source'=>$key, 'detail'=>$detail));
}
}
$cancel_requested = bratonien_tools_cache_warmup_cancel_pending($connection_id);
bratonien_tools_cache_warmup_status(
$connection_id,
$cancel_requested ? 'cancelling' : 'running',
$cancel_requested
? 'Abbruch angefordert. Der bereits geladene aktuelle Batch wird vollständig beendet; danach ist Schluss.'
: 'Piwigo verarbeitet WebDAV-Quellen aus dem Worker-Index.',
array(
'stage'=>$stage,
'batch'=>$batch_number,
'batch_requested'=>count($keys),
'batch_downloaded'=>count($downloads),
'selected_total'=>count($selected),
'stage_completed'=>$completed,
'cancel_requested'=>$cancel_requested,
)
);
foreach ($downloads as $key=>$file)
{
$result = bratonien_tools_cache_warmup_process($pending[$key], $file, $credentials, $stage);
bratonien_tools_cache_warmup_log('source', array('stage'=>$stage, 'source'=>$key, 'ok'=>!empty($result['ok']), 'defer'=>!empty($result['defer']), 'message'=>(string)($result['message'] ?? '')));
if (!empty($result['fatal']))
{
foreach ($downloads as $cleanup) @unlink($cleanup);
@rmdir($dir);
bratonien_tools_cache_warmup_unlock($batch_sync_lock);
throw new RuntimeException('Sicherheitsabbruch bei '.$key.': '.(string)$result['message']);
}
if (empty($result['ok']))
{
// Temporäre Sperren oder parallel belegte Quellen sind kein Fehler.
// Der Index bleibt offen und der nächste Lauf versucht nur diese
// Quelle erneut.
if (!empty($result['defer'])) $deferred++;
else $failed++;
continue;
}
$item = $pending[$key];
$existing = isset($index['sources'][$key]) && is_array($index['sources'][$key]) ? $index['sources'][$key] : array();
$metadata = bratonien_tools_webdav_source_index_metadata($item['source'], $item['shadow_relative']);
$index['sources'][$key] = array_merge($existing, $metadata);
$index['sources'][$key][$stage === 1 ? 'stage1_signature' : 'stage2_signature'] = $item['signature'];
bratonien_tools_webdav_source_index_save($credentials['state_dir'], $connection_id, $index);
$completed++;
}
foreach ($downloads as $file) @unlink($file);
@rmdir($dir);
bratonien_tools_cache_warmup_unlock($batch_sync_lock);
if (bratonien_tools_cache_warmup_cancel_pending($connection_id))
{
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>true, 'completed'=>$completed);
}
if ($stage === 2 && bratonien_tools_cache_warmup_priority_pending($priority_file))
{
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>true, 'cancelled'=>false, 'completed'=>$completed);
}
}
return array('failed'=>$failed, 'deferred'=>$deferred, 'waiting'=>false, 'preempted'=>false, 'cancelled'=>false, 'completed'=>$completed);
}
function bratonien_tools_cache_warmup_run($connection_id, $mode)
{
$settings = bratonien_tools_get_webdav_warmup_settings();
if (!in_array($mode, array('manual','rebuild'), true) && empty($settings['enabled'])) return 0;
$credentials = bratonien_tools_cache_warmup_credentials($connection_id);
if (!$credentials) throw new RuntimeException('WebDAV-Verbindung, Runtime-Pfade oder Zugangsdaten konnten nicht geladen werden.');
$state_dir = $credentials['state_dir'];
if (!is_dir($state_dir) && !@mkdir($state_dir, 0750, true) && !is_dir($state_dir)) throw new RuntimeException('Connector-State-Verzeichnis konnte nicht angelegt werden.');
$priority_file = bratonien_tools_cache_warmup_priority_file($state_dir);
$process_lock = @fopen($state_dir.'/webdav-cache-warmup.lock', 'c');
if (!$process_lock || !@flock($process_lock, LOCK_EX | LOCK_NB))
{
if (is_resource($process_lock)) fclose($process_lock);
bratonien_tools_cache_warmup_log('already_running', array('connection_id'=>$connection_id));
return 0;
}
try
{
if ($mode === 'sync' && is_file($priority_file)) @unlink($priority_file);
$index = bratonien_tools_webdav_source_index_load($state_dir, $connection_id);
$index_created = false;
if ($index === null)
{
$index = bratonien_tools_webdav_source_index_empty($connection_id);
$index_created = true;
}
if ($mode === 'periodic')
{
$last = (int)($index['last_periodic_at'] ?? 0);
$interval = max(1, (int)$settings['periodic_hours']) * 3600;
if ($last > 0 && time() - $last < $interval) return 0;
}
$scan = bratonien_tools_cache_warmup_scan($connection_id, $credentials);
$removed = bratonien_tools_webdav_source_index_prune($index, array_keys($scan['sources']));
$selected = bratonien_tools_cache_warmup_select($scan, $index, $mode);
bratonien_tools_cache_warmup_status($connection_id, 'scan', 'Worker-Index wurde ausschließlich mit dem aktuellen Shadow-Tree abgeglichen.', array(
'mode'=>$mode,
'shadow_sources'=>count($scan['sources']),
'selected'=>count($selected),
'removed_from_index'=>$removed,
'index_created'=>$index_created,
));
if (!$selected)
{
if ($mode === 'periodic' || $mode === 'manual') $index['last_periodic_at'] = time();
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
@unlink(bratonien_tools_cache_warmup_cancel_file($connection_id));
bratonien_tools_cache_warmup_status($connection_id, 'complete', 'Keine neuen oder geänderten Shadow-Quellen gefunden.', array('mode'=>$mode, 'shadow_sources'=>count($scan['sources'])));
return 0;
}
if ($mode === 'rebuild')
{
foreach ($selected as $key=>$item)
{
if (!isset($index['sources'][$key]) || !is_array($index['sources'][$key])) $index['sources'][$key] = array();
unset($index['sources'][$key]['stage1_signature'], $index['sources'][$key]['stage2_signature']);
}
}
$stage1 = bratonien_tools_cache_warmup_run_stage($connection_id, 1, $selected, $index, $credentials, $settings['batch_size'], $priority_file);
if (!empty($stage1['cancelled']))
{
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
@unlink(bratonien_tools_cache_warmup_cancel_file($connection_id));
bratonien_tools_cache_warmup_status($connection_id, 'cancelled', 'Cache-Aufbau nach dem vollständig abgeschlossenen aktuellen Batch beendet.', array(
'mode'=>$mode,
'shadow_sources'=>count($scan['sources']),
'selected'=>count($selected),
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
'stage1_failed'=>(int)$stage1['failed'],
'stage1_deferred'=>(int)$stage1['deferred'],
'stage2_failed'=>0,
'stage2_deferred'=>0,
));
return 0;
}
if (!empty($stage1['waiting']))
{
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
return 0;
}
$stage2_candidates = array();
foreach ($selected as $key=>$item)
{
$entry = isset($index['sources'][$key]) && is_array($index['sources'][$key]) ? $index['sources'][$key] : array();
if (isset($entry['stage1_signature']) && hash_equals((string)$entry['stage1_signature'], $item['signature'])) $stage2_candidates[$key] = $item;
}
$stage2 = bratonien_tools_cache_warmup_run_stage($connection_id, 2, $stage2_candidates, $index, $credentials, $settings['batch_size'], $priority_file);
if (!empty($stage2['cancelled']))
{
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
@unlink(bratonien_tools_cache_warmup_cancel_file($connection_id));
bratonien_tools_cache_warmup_status($connection_id, 'cancelled', 'Cache-Aufbau nach dem vollständig abgeschlossenen aktuellen Batch beendet.', array(
'mode'=>$mode,
'shadow_sources'=>count($scan['sources']),
'selected'=>count($selected),
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
'stage1_failed'=>(int)$stage1['failed'],
'stage1_deferred'=>(int)$stage1['deferred'],
'stage2_failed'=>(int)$stage2['failed'],
'stage2_deferred'=>(int)$stage2['deferred'],
));
return 0;
}
if (!empty($stage2['waiting']))
{
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
return 0;
}
if (!empty($stage2['preempted']))
{
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
bratonien_tools_cache_warmup_status($connection_id, 'preempted', 'Stufe 2 wurde nach einem vollständigen Batch für neue Connector-Inhalte freigegeben.', array(
'mode'=>$mode,
'selected'=>count($selected),
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
'stage1_failed'=>(int)$stage1['failed'],
'stage1_deferred'=>(int)$stage1['deferred'],
'stage2_failed'=>(int)$stage2['failed'],
'stage2_deferred'=>(int)$stage2['deferred'],
));
return 0;
}
if ($mode === 'periodic' || $mode === 'manual') $index['last_periodic_at'] = time();
bratonien_tools_webdav_source_index_save($state_dir, $connection_id, $index);
@unlink(bratonien_tools_cache_warmup_cancel_file($connection_id));
$failed = (int)$stage1['failed'] + (int)$stage2['failed'];
$deferred = (int)$stage1['deferred'] + (int)$stage2['deferred'];
if ($failed > 0 || $deferred > 0)
{
bratonien_tools_cache_warmup_status($connection_id, 'partial', 'Worker-Lauf beendet; einzelne Quellen bleiben offen und werden beim nächsten Lauf erneut versucht. Der übrige Bestand wurde weiterverarbeitet.', array(
'mode'=>$mode,
'shadow_sources'=>count($scan['sources']),
'selected'=>count($selected),
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
'stage1_failed'=>(int)$stage1['failed'],
'stage1_deferred'=>(int)$stage1['deferred'],
'stage2_failed'=>(int)$stage2['failed'],
'stage2_deferred'=>(int)$stage2['deferred'],
));
return 0;
}
bratonien_tools_cache_warmup_status($connection_id, 'complete', $mode === 'rebuild' ? 'Piwigo hat den vollständigen Shadow-Bestand für den Cache-Rebuild verarbeitet.' : 'Worker-Lauf vollständig beendet.', array(
'mode'=>$mode,
'shadow_sources'=>count($scan['sources']),
'selected'=>count($selected),
'stage1_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 1),
'stage2_completed'=>bratonien_tools_cache_warmup_stage_completed_count($selected, $index, 2),
'stage1_failed'=>0,
'stage1_deferred'=>0,
'stage2_failed'=>0,
'stage2_deferred'=>0,
));
return 0;
}
finally
{
@flock($process_lock, LOCK_UN);
fclose($process_lock);
}
}
$connection_id = 0;
$mode = 'periodic';
foreach ($argv as $arg)
{
if (preg_match('/^--connection-id=(\d+)$/', $arg, $m)) $connection_id = (int)$m[1];
elseif (preg_match('/^--mode=(sync|periodic|manual|rebuild)$/', $arg, $m)) $mode = $m[1];
}
if ($connection_id < 1)
{
fwrite(STDERR, "--connection-id fehlt oder ist ungültig.\n");
exit(1);
}
try
{
exit(bratonien_tools_cache_warmup_run($connection_id, $mode));
}
catch (Throwable $e)
{
try { bratonien_tools_cache_warmup_status($connection_id, 'fatal', $e->getMessage()); } catch (Throwable $ignored) {}
fwrite(STDERR, '[BRAT-WORKER] fatal '.$e->getMessage()."\n");
exit(1);
}
-180
View File
@@ -1,180 +0,0 @@
#!/usr/bin/env php
<?php
if (PHP_SAPI !== 'cli')
{
fwrite(STDERR, "CLI only\n");
exit(1);
}
$connection_id = 0;
foreach ($argv as $arg)
{
if (preg_match('/^--connection-id=(\d+)$/', $arg, $m)) $connection_id = (int)$m[1];
}
if ($connection_id < 1)
{
fwrite(STDERR, "--connection-id fehlt oder ist ungueltig.\n");
exit(2);
}
$piwigo_root = realpath(dirname(__DIR__, 4));
if ($piwigo_root === false)
{
fwrite(STDERR, "Piwigo-Root wurde nicht gefunden.\n");
exit(3);
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
$_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/webdav-scan-diagnostic.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Scan-Diagnostic/0.9.7.1.21';
$_SERVER['HTTPS'] = 'off';
try
{
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH')) throw new RuntimeException('Bratonien Tools ist nicht aktiv.');
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector.inc.php');
$connection = bratonien_tools_nc_connector_connection($connection_id, true);
if (!$connection || empty($connection['enabled'])) throw new RuntimeException('WebDAV-Verbindung wurde nicht gefunden oder ist deaktiviert.');
if (!bratonien_tools_nc_connector_is_webdav($connection)) throw new RuntimeException('Verbindung ist keine WebDAV-Placeholder-Verbindung.');
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
$gallery_root = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
if ($state_dir === '') throw new RuntimeException('Connector-State-Verzeichnis fehlt.');
if ($gallery_root === '') throw new RuntimeException('Shadow-Tree-Pfad fehlt in der Connector-Konfiguration.');
$lock_file = $state_dir.'/webdav-cache-warmup.lock';
$worker_lock_state = 'open_failed';
$worker_lock_detail = '';
$lock_handle = @fopen($lock_file, 'c');
if (is_resource($lock_handle))
{
if (@flock($lock_handle, LOCK_EX | LOCK_NB))
{
$worker_lock_state = 'free';
@flock($lock_handle, LOCK_UN);
}
else
{
$worker_lock_state = 'busy';
$worker_lock_detail = 'Ein anderer Prozess haelt den WebDAV-Worker-Lock.';
}
fclose($lock_handle);
}
else
{
$worker_lock_detail = 'Worker-Lock-Datei konnte nicht geoeffnet werden.';
}
$mapping_file = $state_dir.'/webdav-map.json';
if (!is_file($mapping_file) || !is_readable($mapping_file)) throw new RuntimeException('WebDAV-Mapping fehlt oder ist nicht lesbar: '.$mapping_file);
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (!is_array($mapping) || !isset($mapping['files']) || !is_array($mapping['files'])) throw new RuntimeException('WebDAV-Mapping ist ungueltig.');
if (!is_dir($gallery_root)) throw new RuntimeException('Shadow-Tree fehlt: '.$gallery_root);
$normalize = static function ($path) {
return rtrim(str_replace('\\', '/', (string)$path), '/');
};
$mapped_files = array();
foreach ($mapping['files'] as $path=>$meta)
{
if (!is_array($meta) || (string)($meta['kind'] ?? '') !== 'file') continue;
$mapped_files[$normalize($path)] = $meta;
}
$shadow_links = 0;
$matched = 0;
$broken = 0;
$unmapped = 0;
$duplicates = 0;
$seen = array();
$examples = array();
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($gallery_root, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $item)
{
$shadow = $item->getPathname();
if (!is_link($shadow)) continue;
$shadow_links++;
$resolved = realpath($shadow);
if ($resolved === false || !is_file($resolved))
{
$broken++;
if (count($examples) < 8) $examples[] = array('state'=>'broken','shadow'=>$shadow);
continue;
}
$resolved = $normalize($resolved);
if (!isset($mapped_files[$resolved]))
{
$unmapped++;
if (count($examples) < 8) $examples[] = array('state'=>'unmapped','shadow'=>$shadow,'target'=>$resolved);
continue;
}
if (isset($seen[$resolved]))
{
$duplicates++;
if (count($examples) < 8) $examples[] = array('state'=>'duplicate','shadow'=>$shadow,'target'=>$resolved);
continue;
}
$seen[$resolved] = true;
$matched++;
if (count($examples) < 8)
{
$meta = $mapped_files[$resolved];
$examples[] = array(
'state'=>'matched',
'shadow'=>$shadow,
'target'=>$resolved,
'fileid'=>(int)($meta['fileid'] ?? 0),
'webdav_path'=>(string)($meta['webdav_path'] ?? ''),
'etag'=>(string)($meta['etag'] ?? ''),
);
}
}
$scan_ok = ($shadow_links > 0 && $matched === $shadow_links && $broken === 0 && $unmapped === 0 && $duplicates === 0);
$payload = array(
'ok'=>$scan_ok && $worker_lock_state !== 'open_failed',
'scan_ok'=>$scan_ok,
'connection_id'=>$connection_id,
'state_dir'=>$state_dir,
'gallery_root'=>$gallery_root,
'mapping_file'=>$mapping_file,
'worker_lock_file'=>$lock_file,
'worker_lock_state'=>$worker_lock_state,
'worker_lock_detail'=>$worker_lock_detail,
'mapping_files'=>count($mapped_files),
'shadow_links'=>$shadow_links,
'matched'=>$matched,
'broken'=>$broken,
'unmapped'=>$unmapped,
'duplicates'=>$duplicates,
'examples'=>$examples,
);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)."\n";
exit($payload['ok'] ? 0 : 4);
}
catch (Throwable $e)
{
echo json_encode(array(
'ok'=>false,
'connection_id'=>$connection_id,
'fatal'=>$e->getMessage(),
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)."\n";
exit(1);
}
-112
View File
@@ -1,112 +0,0 @@
<?php
if (PHP_SAPI !== 'cli')
{
http_response_code(404);
exit;
}
$piwigo_root = realpath(dirname(__DIR__, 4));
if ($piwigo_root === false)
{
fwrite(STDERR, "Piwigo-Root wurde nicht gefunden.\n");
exit(1);
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
$_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/webdav-warmup-audit.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Warmup-Audit/0.9.7.1.8';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
fwrite(STDERR, "Bratonien Tools ist nicht aktiv.\n");
exit(1);
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
$total = 0;
$connector = 0;
$errors = array();
$warnings = array();
$resolved_to_ids = array();
$result = pwg_query('SELECT id, path FROM '.IMAGES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result))
{
$total++;
$image_id = (int)$row['id'];
$info = bratonien_tools_webdav_materialize_source_info($image_id);
if (!$info) continue;
$connector++;
$logical = (string)$row['path'];
if ($logical === '')
{
$errors[] = 'Bild #'.$image_id.': Piwigo-Pfad ist leer.';
continue;
}
if (strpos($logical, '/') !== 0)
{
$logical = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $logical), '/');
}
$resolved = realpath($logical);
if ($resolved === false || !is_file($resolved))
{
$errors[] = 'Bild #'.$image_id.': Piwigo-Pfad kann nicht auf eine Datei aufgelöst werden: '.$logical;
continue;
}
$normalized = str_replace('\\', '/', $resolved);
$expected = '/nc-webdav-source/connection-'.(int)$info['connection_id'].'/root-'.(int)$info['root_fileid'].'/';
if (strpos($normalized, $expected) === false)
{
$errors[] = 'Bild #'.$image_id.': realpath liegt nicht in der erwarteten Connector-Wurzel: '.$normalized;
}
$webdav_path = trim((string)($info['webdav_path'] ?? ''), '/');
if ($webdav_path === '')
{
$errors[] = 'Bild #'.$image_id.': WebDAV-Pfad fehlt.';
}
if ((int)($info['connection_id'] ?? 0) < 1 || (int)($info['root_fileid'] ?? 0) < 1)
{
$errors[] = 'Bild #'.$image_id.': Verbindung oder Root-fileid ist ungültig.';
}
if ((int)($info['fileid'] ?? 0) < 1)
{
$warnings[] = 'Bild #'.$image_id.': Nextcloud-fileid fehlt; Original-Fallback bleibt möglich.';
}
if (!isset($resolved_to_ids[$normalized])) $resolved_to_ids[$normalized] = array();
$resolved_to_ids[$normalized][] = $image_id;
}
foreach ($resolved_to_ids as $path => $ids)
{
if (count($ids) > 1)
{
$warnings[] = 'Mehrere Piwigo-Bild-IDs zeigen auf dieselbe physische Connector-Quelle: '.implode(',', $ids).' -> '.$path;
}
}
printf("WebDAV-Warmup-Pfadaudit\n");
printf("Piwigo-Bilder gesamt: %d\n", $total);
printf("Connector-Bilder: %d\n", $connector);
printf("Fehler: %d\n", count($errors));
printf("Warnungen: %d\n", count($warnings));
foreach ($errors as $error) fwrite(STDERR, "ERROR: ".$error."\n");
foreach ($warnings as $warning) fwrite(STDOUT, "WARN: ".$warning."\n");
exit($errors ? 2 : 0);
-324
View File
@@ -1,324 +0,0 @@
<?php
if (PHP_SAPI !== 'cli')
{
http_response_code(404);
exit;
}
$piwigo_root = realpath(dirname(__DIR__, 3));
if ($piwigo_root === false)
{
fwrite(STDERR, "Piwigo-Root wurde nicht gefunden.\n");
exit(1);
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
$_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/piwigo-presentation-refresh.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-Presentation-Refresh/0.9.7.1.41';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
fwrite(STDERR, "Bratonien Tools ist nicht aktiv.\n");
exit(1);
}
require_once(BRATONIEN_TOOLS_PATH.'include/presentation_refresh.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/watermark_runtime.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
function bratonien_tools_presentation_worker_log($event, array $data=array())
{
$parts = array('[BRAT-PRESENTATION]', $event);
foreach ($data as $key=>$value)
{
if (is_array($value) || is_object($value)) $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$parts[] = $key.'='.str_replace(array("\r", "\n"), array('\\r', '\\n'), (string)$value);
}
fwrite(STDOUT, implode(' ', $parts)."\n");
}
function bratonien_tools_presentation_worker_categories(array $requested, $all)
{
$requested = array_fill_keys(array_map('intval', $requested), true);
$categories = array();
$result = pwg_query('SELECT id, uppercats FROM '.CATEGORIES_TABLE.' ORDER BY id');
while ($row = pwg_db_fetch_assoc($result))
{
$id = (int)$row['id'];
if ($id < 1) continue;
if ($all)
{
$categories[$id] = $id;
continue;
}
$uppercats = array_filter(array_map('intval', explode(',', (string)$row['uppercats'])));
foreach ($uppercats as $ancestor)
{
if (isset($requested[$ancestor]))
{
$categories[$id] = $id;
break;
}
}
}
ksort($categories, SORT_NUMERIC);
return array_values($categories);
}
function bratonien_tools_presentation_worker_variants(SrcImage $src)
{
$variants = array();
foreach (ImageStdParams::get_defined_type_map() as $type=>$params)
{
$variants['standard:'.$type] = new DerivativeImage($params, $src);
}
foreach (ImageStdParams::$custom as $key=>$last_used)
{
$params = bratonien_tools_webdav_materialize_custom_params($key);
if ($params) $variants['custom:'.$key] = new DerivativeImage($params, $src);
}
return $variants;
}
function bratonien_tools_presentation_worker_call_watermark($url, &$detail=null)
{
$detail = '';
$url = html_entity_decode((string)$url, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$path = (string)parse_url($url, PHP_URL_PATH);
$query = (string)parse_url($url, PHP_URL_QUERY);
$endpoint = '/plugins/'.trim(BRATONIEN_TOOLS_ID, '/').'/watermark.php';
if ($path === '' || substr($path, -strlen($endpoint)) !== $endpoint)
{
if (strpos($path, '/bratonien-watermark/') !== false)
{
$detail = 'Bereits vorhanden.';
return true;
}
$detail = 'Kein Bratonien-Wasserzeichenaufruf erforderlich.';
return true;
}
if ($query === '')
{
$detail = 'Wasserzeichen-URL enthält keine Parameter.';
return false;
}
if (!function_exists('exec'))
{
$detail = 'PHP exec() ist deaktiviert.';
return false;
}
$runner = BRATONIEN_TOOLS_PATH.'runtime/lib/bratonien-watermark-call.php';
if (!is_file($runner))
{
$detail = 'Lokaler Wasserzeichen-Aufrufer fehlt.';
return false;
}
$command = escapeshellarg(PHP_BINARY).' '.escapeshellarg($runner)
.' --query='.escapeshellarg($query).' > /dev/null 2>&1';
$output = array();
$exit = 1;
@exec($command, $output, $exit);
if ($exit !== 0)
{
$detail = 'Wasserzeichen-Aufrufer Exit '.$exit.'.';
return false;
}
$detail = 'Wasserzeichen-Vorschau bereit.';
return true;
}
function bratonien_tools_presentation_worker_process(array $category_ids)
{
global $page;
if (!$category_ids) return array('images'=>0, 'profiles'=>0, 'variants'=>0, 'generated'=>0, 'missing_base'=>0, 'errors'=>0);
$category_lookup = array_fill_keys($category_ids, true);
$ids_sql = implode(',', array_map('intval', $category_ids));
$image_categories = array();
$result = pwg_query('SELECT image_id, category_id FROM '.IMAGE_CATEGORY_TABLE.' WHERE category_id IN ('.$ids_sql.') ORDER BY image_id, category_id');
while ($row = pwg_db_fetch_assoc($result))
{
$image_id = (int)$row['image_id'];
$category_id = (int)$row['category_id'];
if ($image_id > 0 && isset($category_lookup[$category_id])) $image_categories[$image_id][] = $category_id;
}
if (!$image_categories) return array('images'=>0, 'profiles'=>0, 'variants'=>0, 'generated'=>0, 'missing_base'=>0, 'errors'=>0);
$stats = array('images'=>0, 'profiles'=>0, 'variants'=>0, 'generated'=>0, 'missing_base'=>0, 'errors'=>0);
$image_ids = array_keys($image_categories);
foreach (array_chunk($image_ids, 500) as $chunk)
{
$rows = pwg_query('SELECT * FROM '.IMAGES_TABLE.' WHERE id IN ('.implode(',', array_map('intval', $chunk)).') ORDER BY id');
while ($row = pwg_db_fetch_assoc($rows))
{
$image_id = (int)$row['id'];
$stats['images']++;
$profile_contexts = array();
foreach ($image_categories[$image_id] ?? array() as $category_id)
{
$rule = bratonien_tools_runtime_effective_rule($category_id);
if ((string)($rule['mode'] ?? '') !== 'profile' || empty($rule['profile_id'])) continue;
$profile_id = (int)$rule['profile_id'];
if ($profile_id < 1 || isset($profile_contexts[$profile_id])) continue;
$profile = bratonien_tools_runtime_get_profile($profile_id);
if (!$profile || empty($profile['active']) || !bratonien_tools_profile_watermark_path($profile)) continue;
$profile_contexts[$profile_id] = $category_id;
}
if (!$profile_contexts) continue;
try
{
$src = new SrcImage($row);
$variants = bratonien_tools_presentation_worker_variants($src);
}
catch (Throwable $e)
{
$stats['errors']++;
bratonien_tools_presentation_worker_log('image_error', array('image_id'=>$image_id, 'message'=>$e->getMessage()));
continue;
}
foreach ($profile_contexts as $profile_id=>$category_id)
{
$stats['profiles']++;
$page['category'] = array('id'=>(int)$category_id);
foreach ($variants as $variant_name=>$derivative)
{
try
{
if ($derivative->same_as_source()) continue;
$stats['variants']++;
$target = $derivative->get_path();
if ($target === '' || !is_file($target) || !is_readable($target))
{
$stats['missing_base']++;
continue;
}
$detail = '';
if (!bratonien_tools_presentation_worker_call_watermark($derivative->get_url(), $detail))
{
$stats['errors']++;
bratonien_tools_presentation_worker_log('variant_error', array(
'image_id'=>$image_id,
'category_id'=>$category_id,
'profile_id'=>$profile_id,
'variant'=>$variant_name,
'message'=>$detail,
));
continue;
}
$stats['generated']++;
}
catch (Throwable $e)
{
$stats['errors']++;
bratonien_tools_presentation_worker_log('variant_error', array(
'image_id'=>$image_id,
'category_id'=>$category_id,
'profile_id'=>$profile_id,
'variant'=>$variant_name,
'message'=>$e->getMessage(),
));
}
}
}
}
}
unset($page['category']);
return $stats;
}
try
{
$queue_dir = bratonien_tools_presentation_refresh_queue_dir();
if (!is_dir($queue_dir)) exit(0);
$lock_path = $queue_dir.'/.worker.lock';
$lock = @fopen($lock_path, 'c');
if (!$lock) throw new RuntimeException('Presentation-Worker-Lock konnte nicht geöffnet werden.');
if (!@flock($lock, LOCK_EX)) throw new RuntimeException('Presentation-Worker-Lock konnte nicht gesetzt werden.');
try
{
while (true)
{
$request_files = glob($queue_dir.'/*.json') ?: array();
sort($request_files, SORT_STRING);
if (!$request_files) break;
$all = false;
$requested = array();
$valid_files = array();
$reasons = array();
foreach ($request_files as $file)
{
$data = json_decode((string)@file_get_contents($file), true);
if (!is_array($data))
{
bratonien_tools_presentation_worker_log('invalid_request', array('file'=>basename($file)));
@unlink($file);
continue;
}
$valid_files[] = $file;
if (!empty($data['all'])) $all = true;
foreach ((array)($data['categories'] ?? array()) as $category_id)
{
$category_id = (int)$category_id;
if ($category_id > 0) $requested[$category_id] = $category_id;
}
if (!empty($data['reason'])) $reasons[] = (string)$data['reason'];
}
if (!$valid_files) continue;
$categories = bratonien_tools_presentation_worker_categories(array_values($requested), $all);
$stats = bratonien_tools_presentation_worker_process($categories);
bratonien_tools_presentation_worker_log('complete', array(
'all'=>$all,
'requested'=>count($requested),
'categories'=>count($categories),
'reasons'=>array_values(array_unique($reasons)),
'images'=>$stats['images'],
'profiles'=>$stats['profiles'],
'variants'=>$stats['variants'],
'prepared'=>$stats['generated'],
'missing_base'=>$stats['missing_base'],
'errors'=>$stats['errors'],
));
foreach ($valid_files as $file) @unlink($file);
}
}
finally
{
@flock($lock, LOCK_UN);
fclose($lock);
}
exit(0);
}
catch (Throwable $e)
{
fwrite(STDERR, '[BRAT-PRESENTATION] fatal '.$e->getMessage()."\n");
exit(1);
}
+4 -14
View File
@@ -20,7 +20,6 @@ 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);
@@ -76,19 +75,11 @@ 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';
$stateBase = '/var/lib/bratonien-tools'; $stateRoot = '/var/lib/bratonien-tools/nc-connector';
$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';
@@ -120,8 +111,7 @@ 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);
if (!webdav_set_runtime_dir_permissions($stateBase)) fail_webdav_reconcile('Runtime-State-Basis konnte nicht fuer den Webserver freigegeben werden.'); @chmod($stateRoot, 0750);
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);
@@ -161,7 +151,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.');
if (!webdav_set_runtime_dir_permissions($stateDir)) fail_webdav_reconcile('State-Verzeichnis konnte nicht fuer den Webserver freigegeben werden.'); @chmod($stateDir, 0750);
$galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/'); $galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
$expectedGalleryRoot = $publicGalleryRoot.'/connection-'.$id; $expectedGalleryRoot = $publicGalleryRoot.'/connection-'.$id;
@@ -190,7 +180,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 ($display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.'); if ($path === '' || $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);
+23 -8
View File
@@ -25,8 +25,22 @@ $_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php'); require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'admin/include/functions.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 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-'; $relativePrefix = './_data/bratonien-tools/nc-webdav-gallery/connection-';
$absolutePrefix = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-webdav-gallery/connection-'; $absolutePrefix = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-webdav-gallery/connection-';
$relativeLength = strlen($relativePrefix); $relativeLength = strlen($relativePrefix);
@@ -37,7 +51,6 @@ try
" OR LEFT(path,".$absoluteLength.")='".pwg_db_real_escape_string($absolutePrefix)."'"; " OR LEFT(path,".$absoluteLength.")='".pwg_db_real_escape_string($absolutePrefix)."'";
$result = pwg_query($query); $result = pwg_query($query);
$checked = 0;
$staleIds = array(); $staleIds = array();
while ($row = pwg_db_fetch_assoc($result)) while ($row = pwg_db_fetch_assoc($result))
{ {
@@ -58,7 +71,6 @@ try
continue; continue;
} }
$checked++;
if (!is_file($filesystemPath)) if (!is_file($filesystemPath))
{ {
$staleIds[] = $id; $staleIds[] = $id;
@@ -68,19 +80,22 @@ try
$staleIds = array_values(array_unique(array_map('intval', $staleIds))); $staleIds = array_values(array_unique(array_map('intval', $staleIds)));
if ($staleIds) if ($staleIds)
{ {
// Die Shadow-Quelle existiert nicht mehr. Der Bilddatensatz ist damit
// unabhängig von eventuellen virtuellen Album-Verknüpfungen ungültig.
// delete_elements(..., false) entfernt Piwigos Datensatz und Derivate,
// fasst aber keine bereits verschwundene Quelldatei an.
delete_elements($staleIds, false); delete_elements($staleIds, false);
invalidate_user_cache(true); invalidate_user_cache(true);
} }
echo 'NC WebDAV laufende Bereinigung: geprueft='.$checked.' entfernte_verwaiste_bilder='.count($staleIds)."\n"; $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); exit(0);
} }
catch (Throwable $e) catch (Throwable $e)
{ {
fwrite(STDERR, 'NC WebDAV laufende Bereinigung: '.$e->getMessage()."\n"); fwrite(STDERR, 'NC WebDAV Altlasten-Reparatur: '.$e->getMessage()."\n");
exit(1); exit(1);
} }
+1 -22
View File
@@ -31,30 +31,9 @@ result=0
for config in "${webdav_configs[@]}"; do for config in "${webdav_configs[@]}"; do
name="$(basename "$config")" name="$(basename "$config")"
echo "NC Connector WebDAV: $name" echo "NC Connector WebDAV: $name"
state_dir="$(PIWIGO_CONFIG="$config" bash -c 'source "$PIWIGO_CONFIG"; printf "%s" "${STATE_DIR:-}"')" if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh"; then
change_file="${state_dir%/}/webdav-sync-content-changed"
if env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh"; then
# Nur ein tatsaechlich geaenderter Connector-Bestand darf einen
# laufenden Cache-Rebuild priorisiert unterbrechen. Ein erfolgreicher
# Sync ohne Aenderung ist kein neuer Inhalt.
if [[ -f "$change_file" && -f "$SCRIPT_DIR/webdav-warmup-dispatch.php" && "$name" =~ ^webdav-connection-([0-9]+)\.conf$ ]]; then
connection_id="${BASH_REMATCH[1]}"
php "$SCRIPT_DIR/webdav-warmup-dispatch.php" --mode=sync --connection-id="$connection_id" || result=1
rm -f -- "$change_file"
else
echo "NC Connector WebDAV: keine Inhaltsaenderung, kein priorisierter Warmup erforderlich."
fi
else
result=1 result=1
fi fi
done done
# Einzelne neue/geänderte Bilder in bereits bekannten Alben werden weiterhin
# über die periodische Eingangsprüfung behandelt. Der Dispatcher startet die
# eigentliche Prüfung erst, wenn das konfigurierte Intervall fällig ist.
if [[ "$result" -eq 0 && -f "$SCRIPT_DIR/webdav-warmup-dispatch.php" ]]; then
php "$SCRIPT_DIR/webdav-warmup-dispatch.php" --mode=periodic || result=1
fi
exit "$result" exit "$result"
+44 -102
View File
@@ -27,20 +27,7 @@ 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"
WORKER_LOCK_FILE="$STATE_DIR/webdav-cache-warmup.lock"
CHANGE_FILE="$STATE_DIR/webdav-sync-content-changed"
PRIORITY_FILE="$STATE_DIR/webdav-cache-warmup-priority-sync"
mkdir -p -- "$STATE_DIR" mkdir -p -- "$STATE_DIR"
chgrp www-data -- "$STATE_DIR"
chmod 0750 -- "$STATE_DIR"
# Das Connector-State-Verzeichnis bleibt fuer www-data bewusst nicht
# beschreibbar. Die beiden gemeinsam verwendeten Lock-Dateien werden deshalb
# vom privilegierten Connector explizit angelegt und gruppenschreibbar gemacht.
touch -- "$LOCK_FILE" "$WORKER_LOCK_FILE"
chgrp www-data -- "$LOCK_FILE" "$WORKER_LOCK_FILE"
chmod 0660 -- "$LOCK_FILE" "$WORKER_LOCK_FILE"
exec 9>"$LOCK_FILE" exec 9>"$LOCK_FILE"
flock -n 9 || exit 0 flock -n 9 || exit 0
@@ -79,76 +66,27 @@ 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-Synchronisierung fehlgeschlagen" "Exit-Code: $code; Zeile: $line; Befehl: $command" write_status error "WebDAV-Shadow-Tree 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= read -r line; do while IFS=$'\t' read -r path display_name; do
[[ -z "$line" || "$line" == \#* ]] && continue [[ -z "$path" || "$path" == \#* ]] && continue
[[ "$line" == *$'\t'* ]] || continue
path="${line%%$'\t'*}"
ROOT_ARGS+=(--root "$path") ROOT_ARGS+=(--root "$path")
done < "$WEBDAV_ROOTS_FILE" done < "$WEBDAV_ROOTS_FILE"
[[ ${#ROOT_ARGS[@]} -gt 0 ]] || { write_status error "Keine WebDAV-Wurzeln konfiguriert"; exit 1; } [[ ${#ROOT_ARGS[@]} -gt 0 ]] || { write_status error "Keine WebDAV-Wurzeln konfiguriert"; exit 1; }
OLD_MAPPING_HASH="" python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
if [[ -r "$WEBDAV_MAPPING_FILE" ]]; then
OLD_MAPPING_HASH="$(sha256sum -- "$WEBDAV_MAPPING_FILE" | awk '{print $1}')"
fi
# Der Scanner liefert die eigentliche Fehlerursache auf stderr. Diese Ausgabe
# darf nicht vom allgemeinen ERR-Trap verschluckt werden, weil im Admin sonst
# nur die aufgerufene Python-Befehlszeile statt der Ursache sichtbar ist.
PLACEHOLDER_OUTPUT=""
PLACEHOLDER_EXIT=0
trap - ERR
if PLACEHOLDER_OUTPUT="$(python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
--base-url "$WEBDAV_BASE_URL" \ --base-url "$WEBDAV_BASE_URL" \
--user "$WEBDAV_USER" \ --user "$WEBDAV_USER" \
--password-file "$WEBDAV_PASSWORD_FILE" \ --password-file "$WEBDAV_PASSWORD_FILE" \
"${ROOT_ARGS[@]}" \ "${ROOT_ARGS[@]}" \
--source-dir "$WEBDAV_SOURCE_DIR" \ --source-dir "$WEBDAV_SOURCE_DIR" \
--manifest "$MANIFEST" \ --manifest "$MANIFEST" \
--mapping "$WEBDAV_MAPPING_FILE" 2>&1)"; then --mapping "$WEBDAV_MAPPING_FILE"
PLACEHOLDER_EXIT=0
else
PLACEHOLDER_EXIT=$?
fi
trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR
[[ -z "$PLACEHOLDER_OUTPUT" ]] || printf '%s\n' "$PLACEHOLDER_OUTPUT"
if [[ "$PLACEHOLDER_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PLACEHOLDER_EXIT"
if [[ -n "$PLACEHOLDER_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PLACEHOLDER_OUTPUT" | compact_output)"
fi
write_status error "Nextcloud-WebDAV konnte nicht eingelesen werden" "$DETAIL"
exit "$PLACEHOLDER_EXIT"
fi
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"
NEW_MAPPING_HASH="$(sha256sum -- "$WEBDAV_MAPPING_FILE" | awk '{print $1}')"
if [[ -z "$OLD_MAPPING_HASH" || "$OLD_MAPPING_HASH" != "$NEW_MAPPING_HASH" ]]; then
printf '%s\n' "$NEW_MAPPING_HASH" > "$CHANGE_FILE"
chmod 0640 -- "$CHANGE_FILE"
else
rm -f -- "$CHANGE_FILE"
# Eine alte Prioritaetsmarke darf einen laufenden Rebuild nicht abbrechen,
# wenn der gerade abgeschlossene Connector-Sync keinerlei Aenderung
# festgestellt hat.
rm -f -- "$PRIORITY_FILE"
fi
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \ python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--manifest "$MANIFEST" \ --manifest "$MANIFEST" \
@@ -156,6 +94,28 @@ python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--state "$SHADOW_MAP_FILE" --state "$SHADOW_MAP_FILE"
trap - ERR trap - ERR
PREVIEW_CACHE="$PIWIGO_ROOT/_data/bratonien-tools/nc-webdav-preview/connection-$CONNECTION_ID"
PREVIEW_OUTPUT=""
PREVIEW_EXIT=0
if PREVIEW_OUTPUT="$(php "$SCRIPT_DIR/lib/precache-webdav-previews.php" \
--mapping="$WEBDAV_MAPPING_FILE" \
--base-url="$WEBDAV_BASE_URL" \
--user="$WEBDAV_USER" \
--password-file="$WEBDAV_PASSWORD_FILE" \
--cache-dir="$PREVIEW_CACHE" 2>&1)"; then
PREVIEW_EXIT=0
else
PREVIEW_EXIT=$?
fi
[[ -z "$PREVIEW_OUTPUT" ]] || printf '%s\n' "$PREVIEW_OUTPUT"
if [[ "$PREVIEW_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $PREVIEW_EXIT"
if [[ -n "$PREVIEW_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$PREVIEW_OUTPUT" | compact_output)"
fi
write_status error "WebDAV-Vorschaubilder konnten beim Einlesen nicht erzeugt werden" "$DETAIL"
exit "$PREVIEW_EXIT"
fi
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
PIWIGO_OUTPUT="" PIWIGO_OUTPUT=""
@@ -200,46 +160,28 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
exit "$PIWIGO_EXIT" exit "$PIWIGO_EXIT"
fi fi
CLEANUP_OUTPUT="" DERIVATIVE_OUTPUT=""
CLEANUP_EXIT=0 DERIVATIVE_EXIT=0
if CLEANUP_OUTPUT="$(php "$SCRIPT_DIR/repair-webdav-orphans.php" 2>&1)"; then if DERIVATIVE_OUTPUT="$(php "$SCRIPT_DIR/lib/build-webdav-derivatives.php" \
CLEANUP_EXIT=0
else
CLEANUP_EXIT=$?
fi
[[ -z "$CLEANUP_OUTPUT" ]] || printf '%s\n' "$CLEANUP_OUTPUT"
if [[ "$CLEANUP_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $CLEANUP_EXIT"
if [[ -n "$CLEANUP_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$CLEANUP_OUTPUT" | compact_output)"
fi
write_status error "Verwaiste Piwigo-Bilder entfernter WebDAV-Quellen konnten nicht bereinigt werden" "$DETAIL"
exit "$CLEANUP_EXIT"
fi
METADATA_OUTPUT=""
METADATA_EXIT=0
if METADATA_OUTPUT="$(php "$SCRIPT_DIR/lib/sync-webdav-metadata.php" \
--piwigo-root="$PIWIGO_ROOT" \ --piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID" \ --connection-id="$CONNECTION_ID" 2>&1)"; then
--mapping="$WEBDAV_MAPPING_FILE" 2>&1)"; then DERIVATIVE_EXIT=0
METADATA_EXIT=0
else else
METADATA_EXIT=$? DERIVATIVE_EXIT=$?
fi fi
[[ -z "$METADATA_OUTPUT" ]] || printf '%s\n' "$METADATA_OUTPUT" [[ -z "$DERIVATIVE_OUTPUT" ]] || printf '%s\n' "$DERIVATIVE_OUTPUT"
if [[ "$METADATA_EXIT" -ne 0 ]]; then if [[ "$DERIVATIVE_EXIT" -ne 0 ]]; then
DETAIL="Exit-Code: $METADATA_EXIT" DETAIL="Exit-Code: $DERIVATIVE_EXIT"
if [[ -n "$METADATA_OUTPUT" ]]; then if [[ -n "$DERIVATIVE_OUTPUT" ]]; then
DETAIL+="; Ausgabe: $(printf '%s\n' "$METADATA_OUTPUT" | compact_output)" DETAIL+="; Ausgabe: $(printf '%s\n' "$DERIVATIVE_OUTPUT" | compact_output)"
fi fi
write_status error "Piwigo-Bildabmessungen konnten nicht mit den WebDAV-Metadaten abgeglichen werden" "$DETAIL" write_status error "Piwigo-Derivate für WebDAV-Bilder konnten nicht erzeugt werden" "$DETAIL"
exit "$METADATA_EXIT" exit "$DERIVATIVE_EXIT"
fi fi
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
write_status ok \ write_status ok \
"WebDAV eingelesen und Piwigo synchronisiert" \ "WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt" \
"" \ "" \
"api" \ "api" \
"ok" \ "ok" \
@@ -248,7 +190,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 und Piwigo über Fallback synchronisiert" \ "WebDAV eingelesen, Piwigo über Fallback synchronisiert und Derivate erzeugt" \
"" \ "" \
"fallback" \ "fallback" \
"not_used" \ "not_used" \
@@ -256,8 +198,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 und Piwigo synchronisiert" write_status ok "WebDAV eingelesen, Piwigo synchronisiert und Derivate erzeugt"
fi fi
else else
write_status ok "WebDAV eingelesen; Registrierung erfolgt über den bestehenden produktiven Piwigo-Sync" write_status ok "WebDAV eingelesen und Vorschaubilder erzeugt; Registrierung erfolgt über den bestehenden produktiven Piwigo-Sync"
fi fi
-421
View File
@@ -1,421 +0,0 @@
<?php
if (PHP_SAPI !== 'cli')
{
http_response_code(404);
exit;
}
$piwigo_root = realpath(dirname(__DIR__, 3));
if ($piwigo_root === false)
{
fwrite(STDERR, "Piwigo-Root wurde nicht gefunden.\n");
exit(1);
}
define('PHPWG_ROOT_PATH', rtrim($piwigo_root, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR);
$_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/webdav-warmup-dispatch.php';
$_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'];
$_SERVER['QUERY_STRING'] = '';
$_SERVER['HTTP_USER_AGENT'] = 'Bratonien-WebDAV-Worker-Dispatcher/0.9.7.1.50';
$_SERVER['HTTPS'] = 'off';
require_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
fwrite(STDERR, "Bratonien Tools ist nicht aktiv.\n");
exit(1);
}
require_once(BRATONIEN_TOOLS_PATH.'include/nc_connector.inc.php');
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_warmup_settings.inc.php');
function bratonien_tools_webdav_dispatch_existing_status($connection_id)
{
if (!function_exists('bratonien_tools_webdav_warmup_status_file_for_connection')) return null;
$file = bratonien_tools_webdav_warmup_status_file_for_connection($connection_id);
if (!is_file($file) || !is_readable($file)) return null;
$raw = @file_get_contents($file);
$data = $raw !== false ? json_decode($raw, true) : null;
return is_array($data) ? $data : null;
}
function bratonien_tools_webdav_dispatch_failure_lines(array $output)
{
$failures = array();
foreach ($output as $line)
{
$line = trim((string)$line);
if ($line === '') continue;
if (
strpos($line, 'download_failed') !== false
|| strpos($line, ' ok=0 ') !== false
|| strpos($line, '[BRAT-WORKER] fatal ') !== false
|| stripos($line, 'RESTORE FEHLGESCHLAGEN') !== false
)
{
$failures[] = $line;
if (count($failures) >= 8) break;
}
}
return $failures;
}
function bratonien_tools_webdav_dispatch_only_connector_deferrals(array $output)
{
$seen = false;
foreach ($output as $line)
{
$line = trim((string)$line);
if ($line === '') continue;
if (strpos($line, '[BRAT-WORKER] fatal ') !== false || strpos($line, 'download_failed') !== false || stripos($line, 'RESTORE FEHLGESCHLAGEN') !== false)
{
return false;
}
if (strpos($line, ' ok=0 ') !== false)
{
$seen = true;
if (strpos($line, 'message=Connector-Synchronisierung läuft.') === false && strpos($line, 'message=Connector-Mapping hat sich seit dem Batch-Download geändert.') === false && strpos($line, 'message=Shadow-Tree hat sich seit dem Batch-Download geändert.') === false)
{
return false;
}
}
}
return $seen;
}
function bratonien_tools_webdav_dispatch_connector_service_active()
{
if (!function_exists('exec') || !is_executable('/usr/bin/systemctl')) return false;
$output = array();
$exit = 1;
@exec('/usr/bin/systemctl is-active --quiet bratonien-nc-connector.service 2>/dev/null', $output, $exit);
return $exit === 0;
}
function bratonien_tools_webdav_dispatch_reset_orphan_sync_lock($state_dir)
{
$path = rtrim((string)$state_dir, '/').'/webdav-sync.lock';
if (!is_file($path)) return true;
$lock = @fopen($path, 'c');
if (!$lock) return false;
if (@flock($lock, LOCK_SH | LOCK_NB))
{
@flock($lock, LOCK_UN);
fclose($lock);
return true;
}
fclose($lock);
// Der Produktions-Connector darf ausschließlich über den systemd-Dienst
// laufen. Ist dieser nicht aktiv, ist ein weiterhin exklusiv belegter Lock
// kein gültiger Connector-Zustand. Den alten Inode hängen wir aus und legen
// eine frische Lockdatei an, damit ein verwaister Altprozess den Cache nicht
// dauerhaft blockiert. Ein späterer echter Connector benutzt automatisch die
// neue Datei.
if (bratonien_tools_webdav_dispatch_connector_service_active()) return false;
$orphan = $path.'.orphan-'.date('YmdHis').'-'.getmypid();
if (!@rename($path, $orphan)) return false;
$fresh = @fopen($path, 'c');
if (!$fresh)
{
@rename($orphan, $path);
return false;
}
@chmod($path, 0660);
fclose($fresh);
@file_put_contents(PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.log', '[BRAT-WORKER] orphan_sync_lock_reset old='.basename($orphan)."\n", FILE_APPEND | LOCK_EX);
return true;
}
function bratonien_tools_webdav_dispatch_connector_busy($state_dir)
{
$path = rtrim((string)$state_dir, '/').'/webdav-sync.lock';
$lock = @fopen($path, 'c');
if (!$lock) return false;
if (@flock($lock, LOCK_SH | LOCK_NB))
{
@flock($lock, LOCK_UN);
fclose($lock);
return false;
}
fclose($lock);
if (bratonien_tools_webdav_dispatch_connector_service_active()) return true;
// Genau der hier beobachtete Fehlerfall: Lock belegt, aber kein Connector-
// Dienst aktiv. Nicht als "Connector läuft" darstellen, sondern den
// verwaisten Lockzustand reparieren und den Cache normal starten lassen.
if (bratonien_tools_webdav_dispatch_reset_orphan_sync_lock($state_dir)) return false;
// Konnte der ungültige Lockzustand nicht sicher repariert werden, wird nicht
// behauptet, dass ein Connector läuft. Der Worker erhält stattdessen einen
// echten Fehler und kann den Zustand sichtbar machen.
throw new RuntimeException('WebDAV-Sync-Lock ist belegt, obwohl der Connector-Dienst nicht aktiv ist, und konnte nicht sicher zurückgesetzt werden.');
}
function bratonien_tools_webdav_dispatch_schedule_after_sync($waiter, $state_dir, $worker_command, $log, &$pid=null)
{
$pid = 0;
$deferred = escapeshellarg('/bin/bash').' '.escapeshellarg($waiter).' '.escapeshellarg($state_dir).' '.$worker_command;
$command = 'nohup '.$deferred.' >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!';
$output = array();
$exit = 1;
@exec($command, $output, $exit);
$pid = isset($output[0]) ? (int)$output[0] : 0;
return $exit === 0 && $pid > 0;
}
$mode = 'periodic';
$wait = false;
$connection_filter = 0;
foreach ($argv as $arg)
{
if (preg_match('/^--mode=(sync|periodic|manual|rebuild)$/', $arg, $m)) $mode = $m[1];
elseif (preg_match('/^--connection-id=(\d+)$/', $arg, $m)) $connection_filter = (int)$m[1];
elseif ($arg === '--wait') $wait = true;
}
$settings = bratonien_tools_get_webdav_warmup_settings();
if (!in_array($mode, array('manual','rebuild'), true) && empty($settings['enabled']))
{
fwrite(STDOUT, "WebDAV-Worker ist deaktiviert.\n");
exit(0);
}
if (!function_exists('exec'))
{
fwrite(STDERR, "PHP exec() ist deaktiviert; Worker kann nicht gestartet werden.\n");
exit(1);
}
$worker = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/webdav-cache-warmup.php');
$priority_waiter = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/run-webdav-priority-wait.sh');
$after_sync_waiter = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/run-webdav-after-sync.sh');
if (!$worker || !is_file($worker))
{
fwrite(STDERR, "WebDAV-Worker wurde nicht gefunden.\n");
exit(1);
}
if (!$priority_waiter || !is_file($priority_waiter))
{
fwrite(STDERR, "WebDAV-Prioritäts-Warter wurde nicht gefunden.\n");
exit(1);
}
if (!$after_sync_waiter || !is_file($after_sync_waiter))
{
fwrite(STDERR, "WebDAV-Connector-Warter wurde nicht gefunden.\n");
exit(1);
}
$log = PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.log';
$result = 0;
$started = 0;
$matched = 0;
$run_inline = $wait || in_array($mode, array('manual','rebuild'), true);
foreach (bratonien_tools_nc_connector_connections() as $connection)
{
if (empty($connection['enabled']) || !bratonien_tools_nc_connector_is_webdav($connection)) continue;
$connection_id = (int)$connection['id'];
if ($connection_id < 1) continue;
if ($connection_filter > 0 && $connection_id !== $connection_filter) continue;
$matched++;
$config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array();
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir === '')
{
fwrite(STDERR, "Worker für Verbindung #{$connection_id} übersprungen: Connector-State-Verzeichnis fehlt.\n");
$result = 1;
continue;
}
$worker_command = escapeshellarg(PHP_BINARY).' '.escapeshellarg($worker)
.' --connection-id='.$connection_id
.' --mode='.escapeshellarg($mode);
try
{
$connector_busy = $mode !== 'sync' && bratonien_tools_webdav_dispatch_connector_busy($state_dir);
}
catch (Throwable $e)
{
if (function_exists('bratonien_tools_webdav_warmup_write_status'))
{
bratonien_tools_webdav_warmup_write_status($connection_id, 'error', $e->getMessage(), array('mode'=>$mode, 'deferred_by_connector'=>false));
}
fwrite(STDERR, $e->getMessage()."\n");
$result = 1;
continue;
}
if ($mode === 'sync')
{
if (!is_dir($state_dir) && !@mkdir($state_dir, 0750, true) && !is_dir($state_dir))
{
fwrite(STDERR, "Worker-Priorität für Verbindung #{$connection_id} konnte nicht signalisiert werden: State-Verzeichnis fehlt.\n");
$result = 1;
continue;
}
$priority_file = $state_dir.'/webdav-cache-warmup-priority-sync';
$priority_payload = json_encode(array(
'connection_id'=>$connection_id,
'requested_at'=>time(),
'reason'=>'connector-sync-complete',
), JSON_UNESCAPED_SLASHES);
if (!is_string($priority_payload) || @file_put_contents($priority_file, $priority_payload."\n", LOCK_EX) === false)
{
fwrite(STDERR, "Worker-Priorität für Verbindung #{$connection_id} konnte nicht geschrieben werden.\n");
$result = 1;
continue;
}
@chmod($priority_file, 0664);
}
elseif ($connector_busy)
{
$resume_pid = 0;
$scheduled = bratonien_tools_webdav_dispatch_schedule_after_sync($after_sync_waiter, $state_dir, $worker_command, $log, $resume_pid);
if (function_exists('bratonien_tools_webdav_warmup_write_status'))
{
bratonien_tools_webdav_warmup_write_status(
$connection_id,
$scheduled ? 'waiting' : 'error',
$scheduled
? 'Connector-Synchronisierung läuft. Der Cache-Worker ist vorgemerkt und startet automatisch unmittelbar danach; es werden vorher keine Nextcloud-Originale geladen.'
: 'Connector-Synchronisierung läuft, aber der automatische Cache-Fortsetzungsauftrag konnte nicht gestartet werden.',
array('mode'=>$mode, 'deferred_by_connector'=>true, 'resume_scheduled'=>$scheduled, 'resume_pid'=>$resume_pid)
);
}
if (!$scheduled)
{
fwrite(STDERR, "Wartender WebDAV-Worker für Verbindung #{$connection_id} konnte nicht gestartet werden.\n");
$result = 1;
continue;
}
$started++;
fwrite(STDOUT, "WebDAV-Worker für Verbindung #{$connection_id} wartet auf den Connector-Sync und startet danach automatisch (PID {$resume_pid}).\n");
continue;
}
$base = $mode === 'sync'
? escapeshellarg('/bin/bash').' '.escapeshellarg($priority_waiter).' '.escapeshellarg($state_dir).' '.$worker_command
: $worker_command;
if ($run_inline)
{
$output = array();
$exit = 1;
@exec($base.' 2>&1', $output, $exit);
foreach ($output as $line)
{
fwrite(STDOUT, $line."\n");
@file_put_contents($log, $line."\n", FILE_APPEND | LOCK_EX);
}
if ($exit !== 0)
{
$deferred = bratonien_tools_webdav_dispatch_only_connector_deferrals($output);
if ($deferred)
{
$resume_pid = 0;
$scheduled = bratonien_tools_webdav_dispatch_schedule_after_sync($after_sync_waiter, $state_dir, $worker_command, $log, $resume_pid);
if (function_exists('bratonien_tools_webdav_warmup_write_status'))
{
$existing = bratonien_tools_webdav_dispatch_existing_status($connection_id);
$extra = is_array($existing) ? $existing : array();
unset($extra['state'], $extra['message'], $extra['connection_id'], $extra['updated_at']);
$extra['worker_exit_code'] = (int)$exit;
$extra['deferred_by_connector'] = true;
$extra['resume_scheduled'] = $scheduled;
$extra['resume_pid'] = $resume_pid;
bratonien_tools_webdav_warmup_write_status(
$connection_id,
$scheduled ? 'waiting' : 'error',
$scheduled
? 'Connector-Synchronisierung hat während des Batches Vorrang erhalten. Der bisherige Indexstand bleibt erhalten; der Cache-Worker ist zur automatischen Fortsetzung vorgemerkt.'
: 'Connector-Synchronisierung hat während des Batches Vorrang erhalten, aber die automatische Fortsetzung konnte nicht vorgemerkt werden.',
$extra
);
}
if (!$scheduled) $result = 1;
else $started++;
continue;
}
$result = $exit;
if (function_exists('bratonien_tools_webdav_warmup_write_status'))
{
$existing = bratonien_tools_webdav_dispatch_existing_status($connection_id);
$failures = bratonien_tools_webdav_dispatch_failure_lines($output);
$terminal = is_array($existing) && in_array((string)($existing['state'] ?? ''), array('error','fatal','preempted','cancelled'), true);
if ($terminal)
{
$message = trim((string)($existing['message'] ?? 'WebDAV-Worker wurde mit Fehlern beendet.'));
if ($failures)
{
$message .= ' Fehlerbeispiele: '.implode(' | ', $failures);
}
$extra = $existing;
unset($extra['state'], $extra['message'], $extra['connection_id'], $extra['updated_at']);
$extra['worker_exit_code'] = (int)$exit;
$extra['dispatcher_preserved_worker_status'] = true;
if ($failures) $extra['failure_examples'] = $failures;
bratonien_tools_webdav_warmup_write_status(
$connection_id,
(string)$existing['state'],
$message,
$extra
);
}
else
{
$detail = $failures
? implode(' | ', $failures)
: ($output ? implode(' ', array_slice($output, -8)) : 'keine Worker-Ausgabe');
bratonien_tools_webdav_warmup_write_status(
$connection_id,
'error',
'WebDAV-Worker Exit '.(int)$exit.'. Ursache: '.$detail,
array('mode'=>$mode, 'worker_exit_code'=>(int)$exit, 'failure_examples'=>$failures)
);
}
}
}
else
{
$started++;
}
}
else
{
$command = 'nohup '.$base.' >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!';
$output = array();
$exit = 1;
@exec($command, $output, $exit);
$pid = isset($output[0]) ? (int)$output[0] : 0;
if ($exit !== 0 || $pid <= 0)
{
fwrite(STDERR, "Worker für Verbindung #{$connection_id} konnte nicht gestartet werden.\n");
$result = 1;
continue;
}
$started++;
fwrite(STDOUT, "WebDAV-Worker {$mode} für Verbindung #{$connection_id} gestartet (PID {$pid}).\n");
}
}
if ($connection_filter > 0 && $matched === 0)
{
fwrite(STDERR, "WebDAV-Verbindung #{$connection_filter} wurde nicht als aktive Connector-Verbindung gefunden.\n");
exit(1);
}
if (!$run_inline) fwrite(STDOUT, "Gestartete WebDAV-Worker: {$started}.\n");
exit($result);
-1
View File
@@ -20,7 +20,6 @@
{ id:'wasserzeichen', label:'Wasserzeichen', sections:['uebersicht','wasserzeichen','regeln'] }, { id:'wasserzeichen', label:'Wasserzeichen', sections:['uebersicht','wasserzeichen','regeln'] },
{ id:'auswahl-download', label:'Fotoauswahl & Downloads' }, { id:'auswahl-download', label:'Fotoauswahl & Downloads' },
{ id:'freigaben', label:'Geschützte Freigaben' }, { id:'freigaben', label:'Geschützte Freigaben' },
{ id:'qr-upload', label:'QR-Upload' },
{ id:'nc-connector', label:'NC Connector' }, { id:'nc-connector', label:'NC Connector' },
{ id:'bilddateien', label:'Bilddateien & Pfade' }, { id:'bilddateien', label:'Bilddateien & Pfade' },
{ id:'wartung', label:'Wartung / Cache' }, { id:'wartung', label:'Wartung / Cache' },
-198
View File
@@ -1,198 +0,0 @@
{literal}
<style>
.bratonien-qr-stock-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px}
.bratonien-qr-stock-head h4{margin:0}
.bratonien-qr-legend{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin:0 0 16px}
.bratonien-qr-legend-item{display:inline-flex;gap:7px;align-items:center}
.bratonien-qr-legend-box{width:18px;height:18px;border-radius:4px;border:1px solid}
.bratonien-qr-legend-box.is-present{background:rgba(62,170,94,.28);border-color:#58c878}
.bratonien-qr-legend-box.is-missing{background:rgba(205,72,72,.25);border-color:#db6d6d}
.bratonien-qr-year{padding:14px 0;border-top:1px solid rgba(255,255,255,.10)}
.bratonien-qr-year:first-of-type{border-top:0;padding-top:0}
.bratonien-qr-year-head{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:10px}
.bratonien-qr-year-head strong{font-size:1.05rem}
.bratonien-qr-year-count{opacity:.78}
.bratonien-qr-year-download{margin-left:auto}
.bratonien-qr-slots{display:grid;grid-template-columns:repeat(auto-fill,minmax(42px,1fr));gap:6px}
.bratonien-qr-slot{min-height:38px;display:flex;align-items:center;justify-content:center;border:1px solid;border-radius:5px;font-weight:700;text-decoration:none!important}
.bratonien-qr-slot.is-present{background:rgba(62,170,94,.28);border-color:#58c878;color:#dff7e6}
.bratonien-qr-slot.is-present:hover{background:rgba(62,170,94,.42);color:#fff}
.bratonien-qr-slot.is-missing{background:rgba(205,72,72,.25);border-color:#db6d6d;color:#ffdede}
@media(max-width:760px){.bratonien-qr-year-download{margin-left:0}.bratonien-qr-slots{grid-template-columns:repeat(auto-fill,minmax(38px,1fr))}}
</style>
{/literal}
<section class="bratonien-section" id="qr-upload">
<h3>QR-Upload</h3>
<p class="bratonien-section__intro">Aktivierbares Uploadformular für nummerierte QR-Codes und persönliche Freundschaftscodes.</p>
<div class="bratonien-grid">
<div class="bratonien-card">
<h4>Formular</h4>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="bratonien_tool" value="customer_qr_settings">
<label style="display:flex;gap:10px;align-items:center;margin:0 0 14px">
<input type="checkbox" name="customer_qr_enabled" value="1" {if $CUSTOMER_QR.enabled}checked{/if}>
<strong>QR-Upload aktivieren</strong>
</label>
<button class="buttonLike" type="submit">Einstellung speichern</button>
</form>
<p class="bratonien-base-note" style="margin-top:14px">Ist das Formular deaktiviert, verschwindet der Menüpunkt und die Uploadseite nimmt keine Dateien an.</p>
</div>
<div class="bratonien-card">
<h4>Formular-Adresse</h4>
<div class="bratonien-form-grid">
<span class="bratonien-label">Status</span><strong>{if $CUSTOMER_QR.enabled}Aktiv{else}Deaktiviert{/if}</strong>
<span class="bratonien-label">Jahre</span><strong>{$CUSTOMER_QR.year_min}–{$CUSTOMER_QR.year_max}</strong>
<span class="bratonien-label">Standardjahr</span><strong>{$CUSTOMER_QR.default_year}</strong>
<span class="bratonien-label">Server-Batchlimit</span><strong>{$CUSTOMER_QR.max_files} Dateien</strong>
<span class="bratonien-label">Freundschaftscodes</span><strong>{$CUSTOMER_QR.friendship_codes.total}</strong>
</div>
<p style="margin-top:14px;overflow-wrap:anywhere"><a href="{$CUSTOMER_QR.url|escape:html}" target="_blank" rel="noopener">{$CUSTOMER_QR.url|escape:html}</a></p>
</div>
<div class="bratonien-card">
<h4>Vorgesehene QR-Codes</h4>
<div class="bratonien-form-grid">
{foreach from=$CUSTOMER_QR.years item=qr_year}
<span class="bratonien-label">{$qr_year.year}</span>
<strong>{$qr_year.used} / {$qr_year.capacity} belegt · {$qr_year.remaining} frei</strong>
{/foreach}
</div>
<p class="bratonien-base-note">2023: Nummern 1–100 · 2024: 1–50 · 2025 und 2026: jeweils 1–30. Weitere Jahre werden erst mit dem jeweiligen Jahresupdate ergänzt.</p>
</div>
<div class="bratonien-card">
<h4>Belegte QR-Code-Nummern</h4>
<div class="bratonien-form-grid">
<span class="bratonien-label">Gesamt</span><strong>{$CUSTOMER_QR.total}</strong>
<span class="bratonien-label">Im Standardjahr {$CUSTOMER_QR.default_year}</span><strong>{$CUSTOMER_QR.current_year_total} / {$CUSTOMER_QR.current_year_capacity}</strong>
</div>
<p class="bratonien-base-note">Eine QR-Code-Nummer ist pro Jahr eindeutig. Wird ein Upload gelöscht, ist die Nummer für dieses Jahr wieder frei.</p>
</div>
</div>
<div class="bratonien-card" style="margin-top:18px">
<div class="bratonien-qr-stock-head">
<h4>QR-Code-Bestand</h4>
{if $CUSTOMER_QR.batch_download_available and $CUSTOMER_QR.total > 0}
<a class="buttonLike" href="{$CUSTOMER_QR.batch_download_url|escape:html}">Alle QR-Codes als ZIP herunterladen</a>
{/if}
</div>
<div class="bratonien-qr-legend">
<span class="bratonien-qr-legend-item"><span class="bratonien-qr-legend-box is-present"></span>Vorhanden</span>
<span class="bratonien-qr-legend-item"><span class="bratonien-qr-legend-box is-missing"></span>Fehlt</span>
</div>
{foreach from=$CUSTOMER_QR.years item=qr_year}
<div class="bratonien-qr-year">
<div class="bratonien-qr-year-head">
<strong>{$qr_year.year}</strong>
<span class="bratonien-qr-year-count">{$qr_year.used} / {$qr_year.capacity} vorhanden · {$qr_year.remaining} fehlen</span>
{if $CUSTOMER_QR.batch_download_available and $qr_year.used > 0}
<a class="buttonLike bratonien-qr-year-download" href="{$qr_year.batch_download_url|escape:html}">{$qr_year.year} als ZIP</a>
{/if}
</div>
<div class="bratonien-qr-slots" aria-label="QR-Code-Bestand {$qr_year.year}">
{foreach from=$qr_year.slots item=qr_slot}
{if $qr_slot.present}
<a class="bratonien-qr-slot is-present" href="{$qr_slot.preview_url|escape:html}" target="_blank" rel="noopener" title="QR-Code #{$qr_slot.number} vorhanden – Vorschau öffnen">{$qr_slot.number}</a>
{else}
<span class="bratonien-qr-slot is-missing" title="QR-Code #{$qr_slot.number} fehlt">{$qr_slot.number}</span>
{/if}
{/foreach}
</div>
</div>
{/foreach}
{if not $CUSTOMER_QR.batch_download_available}
<p class="bratonien-base-note" style="margin-top:14px">Batch-Download ist erst verfügbar, wenn die PHP-Erweiterung ZipArchive auf dem Server aktiv ist.</p>
{/if}
</div>
<div class="bratonien-card" style="margin-top:18px;overflow-x:auto">
<h4>QR-Codes verwalten</h4>
{if $CUSTOMER_QR.total > 0}
<table style="width:100%;border-collapse:collapse;min-width:820px">
<thead>
<tr>
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Jahr</th>
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">QR-Nr.</th>
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Datei</th>
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Größe</th>
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Upload</th>
<th style="text-align:right;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Aktionen</th>
</tr>
</thead>
<tbody>
{foreach from=$CUSTOMER_QR.uploads item=qr_upload}
<tr>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)"><strong>{$qr_upload.year}</strong></td>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)"><strong>#{$qr_upload.number|escape:html}</strong></td>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08);max-width:300px;overflow-wrap:anywhere">{$qr_upload.original_name|escape:html}</td>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)">{$qr_upload.file_size_label|escape:html}</td>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)">{$qr_upload.created|escape:html}</td>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08);text-align:right;white-space:nowrap">
<a class="buttonLike" href="{$qr_upload.preview_url|escape:html}" target="_blank" rel="noopener" style="display:inline-block;margin-right:6px">Vorschau</a>
<a class="buttonLike" href="{$qr_upload.preview_url|escape:html}&amp;download=1" style="display:inline-block;margin-right:6px">Herunterladen</a>
<form method="post" style="display:inline" onsubmit="return confirm('Diesen QR-Upload wirklich löschen? Die QR-Nummer wird danach wieder frei.');">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="bratonien_tool" value="customer_qr_delete">
<input type="hidden" name="customer_qr_id" value="{$qr_upload.id}">
<button class="buttonLike" type="submit">Löschen</button>
</form>
</td>
</tr>
{/foreach}
</tbody>
</table>
{else}
<p class="bratonien-base-note">Noch keine QR-Codes hochgeladen.</p>
{/if}
</div>
<div class="bratonien-card" style="margin-top:18px;overflow-x:auto">
<h4>Persönliche Freundschaftscodes</h4>
{if $CUSTOMER_QR.friendship_codes.total > 0}
<table style="width:100%;border-collapse:collapse;min-width:760px">
<thead>
<tr>
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Name</th>
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Datei</th>
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Größe</th>
<th style="text-align:left;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Upload</th>
<th style="text-align:right;padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.14)">Aktionen</th>
</tr>
</thead>
<tbody>
{foreach from=$CUSTOMER_QR.friendship_codes.uploads item=friendship}
<tr>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)"><strong>{$friendship.name|escape:html}</strong></td>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08);max-width:300px;overflow-wrap:anywhere">{$friendship.original_name|escape:html}</td>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)">{$friendship.file_size_label|escape:html}</td>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08)">{$friendship.created|escape:html}</td>
<td style="padding:9px 8px;border-bottom:1px solid rgba(255,255,255,.08);text-align:right;white-space:nowrap">
<a class="buttonLike" href="{$friendship.preview_url|escape:html}" target="_blank" rel="noopener" style="display:inline-block;margin-right:6px">Vorschau</a>
<a class="buttonLike" href="{$friendship.download_url|escape:html}" style="display:inline-block;margin-right:6px">Herunterladen</a>
<form method="post" style="display:inline" onsubmit="return confirm('Diesen Freundschaftscode wirklich löschen?');">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<input type="hidden" name="bratonien_tool" value="friendship_code_delete">
<input type="hidden" name="friendship_code_id" value="{$friendship.id}">
<button class="buttonLike" type="submit">Löschen</button>
</form>
</td>
</tr>
{/foreach}
</tbody>
</table>
{else}
<p class="bratonien-base-note">Noch keine persönlichen Freundschaftscodes hochgeladen.</p>
{/if}
</div>
</section>
-4
View File
@@ -14,10 +14,6 @@
<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}
-50
View File
@@ -1,50 +0,0 @@
<div id="bratonien-webdav-warmup-card" class="bratonien-card" style="margin-top:16px;">
<h4>WebDAV-Quellenindex &amp; Cache-Warmup</h4>
<p>Bratonien Tools vergleicht den aktuellen Shadow-Tree mit einem eigenen kompakten Quellenindex. Nur neue, geänderte oder noch nicht vollständig verarbeitete Quellen werden an Piwigo übergeben. Der Piwigo-Bildcache selbst entscheidet nicht, ob eine Quelle neu ist.</p>
<p class="bratonien-main-cache__warning"><strong>Diagnose:</strong> Der Scan-Test startet synchron eine eigene PHP-CLI-Prüfung und liest ausschließlich Connector-Konfiguration, <code>webdav-map.json</code> und den Shadow-Tree. Er schreibt keinen Index, lädt keine Originale und erzeugt keine Derivate. Damit sehen wir direkt, ob der Scan-Prozess überhaupt starten kann und wie viele Shadow-Links er tatsächlich findet.</p>
<form method="post" class="bratonien-worker-settings">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<label><input type="checkbox" name="webdav_warmup_enabled" value="1" {if $CACHE_WORKERS.webdav_warmup.enabled}checked{/if}> Automatischen WebDAV-Warmup aktivieren</label>
<label>Bilder pro Batch: <input type="number" name="webdav_warmup_batch_size" value="{$CACHE_WORKERS.webdav_warmup.batch_size}" min="1" max="50" step="1"></label>
<label>Index-Abgleich: <input type="number" name="webdav_warmup_periodic_hours" value="{$CACHE_WORKERS.webdav_warmup.periodic_hours}" min="1" max="168" step="1"> Stunden</label>
<button class="buttonLike" type="submit" name="bratonien_tool" value="image_cache_webdav_warmup_settings">Warmup-Einstellungen speichern</button>
</form>
<div class="bratonien-form-grid" style="margin-top:12px;">
<span class="bratonien-label">Änderungserkennung</span><span>Shadow-Tree ↔ eigener Quellenindex; Piwigo-Index und Cache-Dateien sind dafür nicht maßgeblich</span>
<span class="bratonien-label">Stufe 1</span><span>Von Piwigo definierte Derivate bis einschließlich 1920 px längster Kante</span>
<span class="bratonien-label">Stufe 2</span><span>Danach alle übrigen von Piwigo definierten Derivate</span>
<span class="bratonien-label">Letzter Status</span><span><strong>{$CACHE_WORKERS.webdav_warmup.status.state|default:'idle'|escape:html}</strong>{if $CACHE_WORKERS.webdav_warmup.status.message} · {$CACHE_WORKERS.webdav_warmup.status.message|escape:html}{/if}</span>
</div>
<div class="bratonien-actions">
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="image_cache_webdav_scan_diagnostic">Scan jetzt testen</button>
</form>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="image_cache_webdav_warmup_manual">Quellenindex jetzt abgleichen</button>
</form>
<form method="post">
<input type="hidden" name="pwg_token" value="{$PWG_TOKEN|escape:html}">
<button class="buttonLike" type="submit" name="bratonien_tool" value="image_cache_webdav_warmup_audit">Produktive Pfade prüfen</button>
</form>
</div>
<p class="bratonien-base-note">Der Scan-Test und der Pfadaudit sind schreibgeschützt. Warmup und On-Demand verwenden denselben Bild-Lock; während der produktiven Materialisierung wird zusätzlich der Connector-Sync-Lock geteilt gehalten, damit Source- und Shadow-Tree nicht ausgetauscht werden können.</p>
</div>
{literal}
<script>
(function(){
function moveWarmupCard(){
var card=document.getElementById('bratonien-webdav-warmup-card');
var section=document.getElementById('wartung');
if(!card||!section||card.parentNode===section)return;
section.appendChild(card);
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',moveWarmupCard);else moveWarmupCard();
})();
</script>
{/literal}
+1 -6
View File
@@ -61,12 +61,7 @@ function bratonien_tools_save_album_rule()
))); )));
} }
if (function_exists('bratonien_tools_presentation_refresh_enqueue')) return array('message'=>'Albumregel gespeichert.');
{
bratonien_tools_presentation_refresh_enqueue(array($category), 'album-watermark-rule-changed');
}
return array('message'=>'Albumregel gespeichert. Betroffene Vorschauen werden im Hintergrund aktualisiert.');
} }
function bratonien_tools_get_category_tree() function bratonien_tools_get_category_tree()
+2 -10
View File
@@ -108,10 +108,9 @@ function bratonien_tools_save_watermark_profile()
throw new RuntimeException('Profilname darf nicht leer sein.'); throw new RuntimeException('Profilname darf nicht leer sein.');
} }
$existing_profile = $id > 0 ? bratonien_tools_get_watermark_profile($id) : null;
if ($id > 0) if ($id > 0)
{ {
if (!$existing_profile) if (!bratonien_tools_get_watermark_profile($id))
{ {
throw new RuntimeException('Wasserzeichenprofil nicht gefunden.'); throw new RuntimeException('Wasserzeichenprofil nicht gefunden.');
} }
@@ -129,14 +128,7 @@ function bratonien_tools_save_watermark_profile()
mass_inserts($table, array_keys($data), array($data)); mass_inserts($table, array_keys($data), array($data));
} }
if ($id > 0 && function_exists('bratonien_tools_presentation_refresh_enqueue_all')) return array('message'=>'Wasserzeichenprofil gespeichert.');
{
bratonien_tools_presentation_refresh_enqueue_all('watermark-profile-changed');
}
return array('message'=>$id > 0
? 'Wasserzeichenprofil gespeichert. Betroffene Vorschauen werden im Hintergrund aktualisiert.'
: 'Wasserzeichenprofil gespeichert.');
} }
function bratonien_tools_profile_is_referenced($id) function bratonien_tools_profile_is_referenced($id)
+4 -47
View File
@@ -4,47 +4,15 @@ if (!defined('PHPWG_ROOT_PATH'))
die('Hacking attempt!'); die('Hacking attempt!');
} }
function bratonien_tools_default_public_watermark_profile_id()
{
if (!function_exists('bratonien_tools_create_default_watermark_profiles'))
{
return null;
}
bratonien_tools_create_default_watermark_profiles();
$table = bratonien_tools_table('watermark_profiles');
$row = pwg_db_fetch_assoc(pwg_query(
"SELECT id FROM ".$table.
" WHERE active=1 AND name='Oeffentlich' ORDER BY id ASC LIMIT 1"
));
return $row ? (int)$row['id'] : null;
}
function bratonien_tools_get_watermark_defaults() function bratonien_tools_get_watermark_defaults()
{ {
$defaults = conf_get_param('bratonien_watermark_defaults', null); $defaults = conf_get_param('bratonien_watermark_defaults', null);
$defaults = $defaults ? json_decode($defaults, true) : array(); $defaults = $defaults ? json_decode($defaults, true) : array();
$defaults = array_merge(array(
return array_merge(array(
'public_profile' => null, 'public_profile' => null,
'private_profile' => null, 'private_profile' => null,
), is_array($defaults) ? $defaults : array()); ), is_array($defaults) ? $defaults : array());
// Oeffentliche Alben sind standardmaessig geschuetzt. Nur eine explizite
// Albumregel (anderes Profil oder deaktiviert) darf dieses Verhalten
// ueberschreiben. Bestehende Installationen ohne gesetztes globales
// Oeffentlich-Profil werden automatisch auf das mitgelieferte Profil
// "Oeffentlich" migriert.
if (empty($defaults['public_profile']))
{
$public_profile = bratonien_tools_default_public_watermark_profile_id();
if ($public_profile)
{
$defaults['public_profile'] = $public_profile;
conf_update_param('bratonien_watermark_defaults', json_encode($defaults));
}
}
return $defaults;
} }
function bratonien_tools_validate_default_profile($value) function bratonien_tools_validate_default_profile($value)
@@ -66,23 +34,12 @@ function bratonien_tools_validate_default_profile($value)
function bratonien_tools_save_watermark_defaults() function bratonien_tools_save_watermark_defaults()
{ {
$public_profile = bratonien_tools_validate_default_profile($_POST['public_profile'] ?? null);
if ($public_profile === null)
{
$public_profile = bratonien_tools_default_public_watermark_profile_id();
}
$config = array( $config = array(
'public_profile' => $public_profile, 'public_profile' => bratonien_tools_validate_default_profile($_POST['public_profile'] ?? null),
'private_profile' => bratonien_tools_validate_default_profile($_POST['private_profile'] ?? null), 'private_profile' => bratonien_tools_validate_default_profile($_POST['private_profile'] ?? null),
); );
conf_update_param('bratonien_watermark_defaults', json_encode($config)); conf_update_param('bratonien_watermark_defaults', json_encode($config));
if (function_exists('bratonien_tools_presentation_refresh_enqueue_all')) return array('message'=>'Globale Wasserzeichenregeln gespeichert.');
{
bratonien_tools_presentation_refresh_enqueue_all('global-watermark-rules-changed');
}
return array('message'=>'Globale Wasserzeichenregeln gespeichert. Oeffentliche Alben verwenden ohne abweichende Albumregel immer das Standard-Wasserzeichenprofil. Vorschauen werden im Hintergrund an die neuen Regeln angepasst.');
} }
-183
View File
@@ -1,183 +0,0 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
define('BRATONIEN_TOOLS_ID', basename(__DIR__));
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php');
function bratonien_tools_webdav_debug_out($status, array $data)
{
http_response_code((int)$status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
global $user;
if (!isset($user['status']) || !in_array($user['status'], array('admin', 'webmaster'), true))
{
bratonien_tools_webdav_debug_out(403, array('ok'=>false, 'stage'=>'auth', 'error'=>'Nur fuer Administratoren/Webmaster.'));
}
$image_id = (int)($_GET['id'] ?? 0);
if ($image_id < 1)
{
bratonien_tools_webdav_debug_out(400, array('ok'=>false, 'stage'=>'input', 'error'=>'Bild-ID fehlt.'));
}
$debug = array(
'ok'=>false,
'image_id'=>$image_id,
'stage'=>'start',
);
$result = pwg_query('SELECT id, path, coi FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result))
{
$debug['stage'] = 'image-row';
$debug['error'] = 'Bild nicht gefunden.';
bratonien_tools_webdav_debug_out(404, $debug);
}
$row = pwg_db_fetch_assoc($result);
$image_path = (string)($row['path'] ?? '');
$debug['images_path'] = $image_path;
$absolute = $image_path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\./#', '', $absolute), '/');
}
$debug['absolute_path'] = $absolute;
$debug['absolute_exists'] = file_exists($absolute);
$debug['absolute_is_file'] = is_file($absolute);
$debug['absolute_is_link'] = is_link($absolute);
if (is_link($absolute))
{
$debug['link_target'] = readlink($absolute);
}
$resolved = realpath($absolute);
$debug['realpath'] = $resolved === false ? null : $resolved;
if ($resolved === false)
{
$debug['stage'] = 'realpath';
$debug['error'] = 'realpath() konnte images.path nicht aufloesen.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$normalized = str_replace('\\', '/', $resolved);
$debug['normalized_realpath'] = $normalized;
$match = array();
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{
$debug['stage'] = 'source-regex';
$debug['error'] = 'realpath passt nicht zum erwarteten nc-webdav-source-Schema.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$connection_id = (int)$match[1];
$root_fileid = (int)$match[2];
$relative_path = trim((string)$match[3], '/');
$debug['connection_id'] = $connection_id;
$debug['root_fileid'] = $root_fileid;
$debug['relative_path'] = $relative_path;
$table = defined('BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE')
? BRATONIEN_TOOLS_NC_CONNECTIONS_TABLE
: $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$debug['connections_table'] = $table;
$connection_result = pwg_query('SELECT id, enabled, adapter, config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($connection_result))
{
$debug['stage'] = 'connection-row';
$debug['error'] = 'Erkannte WebDAV-Verbindung existiert nicht in der Connector-Tabelle.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$connection_row = pwg_db_fetch_assoc($connection_result);
$debug['connection_enabled'] = (int)($connection_row['enabled'] ?? 0);
$debug['connection_adapter'] = (string)($connection_row['adapter'] ?? '');
$config = json_decode((string)($connection_row['config_json'] ?? ''), true);
if (!is_array($config))
{
$debug['stage'] = 'config-json';
$debug['error'] = 'config_json ist kein gueltiges JSON-Objekt.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$debug['source_mode'] = (string)($config['source_mode'] ?? '');
$debug['state_dir'] = (string)($config['state_dir'] ?? '');
$debug['parallel_gallery_root'] = (string)($config['parallel_gallery_root'] ?? '');
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
$debug['roots'] = array();
$root_path = '';
foreach ($roots as $root)
{
$entry = array(
'fileid'=>(int)($root['fileid'] ?? 0),
'webdav_path'=>(string)($root['webdav_path'] ?? ''),
'display_name'=>(string)($root['display_name'] ?? ''),
);
$debug['roots'][] = $entry;
if ($entry['fileid'] === $root_fileid)
{
$root_path = trim($entry['webdav_path'], '/');
}
}
$debug['matched_root_path'] = $root_path;
if ($debug['source_mode'] !== 'webdav-placeholder')
{
$debug['stage'] = 'source-mode';
$debug['error'] = 'source_mode ist nicht webdav-placeholder.';
bratonien_tools_webdav_debug_out(200, $debug);
}
if ($root_path === '')
{
$debug['stage'] = 'root-match';
$debug['error'] = 'root_fileid aus dem Dateipfad kommt in config.roots nicht vor.';
bratonien_tools_webdav_debug_out(200, $debug);
}
$mapping_file = rtrim((string)($config['state_dir'] ?? ''), '/').'/webdav-map.json';
$debug['mapping_file'] = $mapping_file;
$debug['mapping_readable'] = is_readable($mapping_file);
$debug['mapping_has_resolved_key'] = false;
$debug['mapping_has_normalized_key'] = false;
$debug['mapping_entry'] = null;
if (is_readable($mapping_file))
{
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{
$debug['mapping_has_resolved_key'] = array_key_exists($resolved, $mapping['files']);
$debug['mapping_has_normalized_key'] = array_key_exists($normalized, $mapping['files']);
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null;
if (is_array($entry))
{
$debug['mapping_entry'] = $entry;
}
}
else
{
$debug['mapping_error'] = 'Mapping-Datei ist ungueltig oder enthaelt kein files-Objekt.';
}
}
$debug['runtime_source_info'] = bratonien_tools_webdav_image_source_info($image_id);
$debug['stage'] = $debug['runtime_source_info'] ? 'ok' : 'runtime-source-info-null';
$debug['ok'] = (bool)$debug['runtime_source_info'];
if (!$debug['ok'])
{
$debug['error'] = 'Die produktive Zuordnungsfunktion liefert trotz der obigen Zwischenergebnisse null.';
}
bratonien_tools_webdav_debug_out(200, $debug);
-70
View File
@@ -1,70 +0,0 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
define('BRATONIEN_TOOLS_ID', basename(__DIR__));
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
$image_id = (int)($_GET['id'] ?? 0);
if ($image_id < 1)
{
http_response_code(400);
header('Content-Type: text/plain; charset=utf-8');
echo 'Ungueltige Derivatanforderung.';
exit;
}
$source = bratonien_tools_webdav_materialize_source_info($image_id);
if (!$source)
{
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo 'WebDAV-Bildquelle nicht gefunden.';
exit;
}
$state_dir = rtrim((string)($source['state_dir'] ?? ''), '/');
if ($state_dir === '')
{
http_response_code(503);
header('Content-Type: text/plain; charset=utf-8');
echo 'Connector-State-Verzeichnis fehlt.';
exit;
}
$lock = @fopen($state_dir.'/webdav-sync.lock', 'c');
if (!$lock)
{
http_response_code(503);
header('Content-Type: text/plain; charset=utf-8');
echo 'Connector-Sync-Lock konnte nicht geoeffnet werden.';
exit;
}
// On-Demand bleibt parallel zu anderen Bildabrufen moeglich, verhindert aber
// fuer die Dauer dieses einzelnen Derivataufrufs einen exklusiven Connector-
// Tree-Swap. Der bestehende Bild-Lock im eigentlichen Endpoint koordiniert
// weiterhin On-Demand und Warmup fuer dieselbe Bild-ID.
if (!@flock($lock, LOCK_SH))
{
fclose($lock);
http_response_code(503);
header('Content-Type: text/plain; charset=utf-8');
echo 'Connector-Sync-Schutz konnte nicht gesetzt werden.';
exit;
}
try
{
include BRATONIEN_TOOLS_PATH.'webdav-derivative.php';
}
finally
{
@flock($lock, LOCK_UN);
fclose($lock);
}
-494
View File
@@ -1,494 +0,0 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
function bratonien_tools_webdav_derivative_test_abort($status, $message)
{
http_response_code((int)$status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo $message;
exit;
}
function bratonien_tools_webdav_derivative_test_decrypt_secret($blob, $hex_key)
{
$hex_key = trim((string)$hex_key);
if (!preg_match('/^[a-f0-9]{64}$/', $hex_key)) return null;
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) return null;
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) return null;
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) return null;
$decoded = json_decode((string)$plain, true);
return is_array($decoded) ? $decoded : null;
}
function bratonien_tools_webdav_derivative_test_quote_path($path)
{
$parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen'));
return implode('/', array_map('rawurlencode', $parts));
}
function bratonien_tools_webdav_derivative_test_source_info($image_id, array $image_row)
{
$image_id = (int)$image_id;
$path = (string)($image_row['path'] ?? '');
if ($image_id < 1 || $path === '') return null;
$absolute = $path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\\./#', '', $absolute), '/');
}
$resolved = realpath($absolute);
if ($resolved === false) return null;
$normalized = str_replace('\\', '/', $resolved);
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{
return null;
}
$connection_id = (int)$match[1];
$root_fileid = (int)$match[2];
$relative_path = trim((string)$match[3], '/');
if ($connection_id < 1 || $root_fileid < 1 || $relative_path === '') return null;
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$connection_result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($connection_result)) return null;
$connection_row = pwg_db_fetch_assoc($connection_result);
$config = json_decode((string)$connection_row['config_json'], true);
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') return null;
$root_found = false;
$root_path = '';
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
foreach ($roots as $root)
{
if ((int)($root['fileid'] ?? 0) === $root_fileid)
{
$root_found = true;
$root_path = trim((string)($root['webdav_path'] ?? ''), '/');
break;
}
}
if (!$root_found) return null;
$webdav_path = trim($root_path === '' ? $relative_path : $root_path.'/'.$relative_path, '/');
if ($webdav_path === '') return null;
$content_type = '';
$size = 0;
$etag = '';
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '')
{
$mapping_file = $state_dir.'/webdav-map.json';
if (is_readable($mapping_file))
{
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null;
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file')
{
$webdav_path = trim((string)($entry['webdav_path'] ?? $webdav_path), '/');
$content_type = (string)($entry['content_type'] ?? '');
$size = (int)($entry['size'] ?? 0);
$etag = (string)($entry['etag'] ?? '');
}
}
}
}
return array(
'image_id'=>$image_id,
'connection_id'=>$connection_id,
'webdav_path'=>$webdav_path,
'content_type'=>$content_type,
'size'=>$size,
'etag'=>$etag,
'coi'=>$image_row['coi'] ?? null,
);
}
function bratonien_tools_webdav_derivative_test_download(array $source, $destination, &$detail=null)
{
$detail = '';
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$result = pwg_query('SELECT config_json, secret_blob FROM `'.$table.'` WHERE id='.(int)$source['connection_id'].' LIMIT 1');
if (!pwg_db_num_rows($result))
{
$detail = 'WebDAV-Verbindung nicht gefunden.';
return false;
}
$row = pwg_db_fetch_assoc($result);
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config))
{
$detail = 'WebDAV-Konfiguration ist ungueltig.';
return false;
}
$key_result = pwg_query("SELECT value FROM ".$GLOBALS['prefixeTable']."config WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!pwg_db_num_rows($key_result))
{
$detail = 'Connector-Schluessel fehlt.';
return false;
}
$key_row = pwg_db_fetch_assoc($key_result);
$credentials = bratonien_tools_webdav_derivative_test_decrypt_secret((string)$row['secret_blob'], (string)$key_row['value']);
if (!is_array($credentials))
{
$detail = 'WebDAV-Zugangsdaten konnten nicht gelesen werden.';
return false;
}
$base_url = rtrim((string)($config['nextcloud_url'] ?? ''), '/');
$user = trim((string)($credentials['nextcloud_user'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
$webdav_path = trim((string)($source['webdav_path'] ?? ''), '/');
if ($base_url === '' || $user === '' || $password === '' || $webdav_path === '')
{
$detail = 'WebDAV-Bildquelle ist unvollstaendig.';
return false;
}
$fp = @fopen($destination, 'xb');
if (!$fp)
{
$detail = 'Temporaere Quelldatei konnte nicht angelegt werden.';
return false;
}
$url = $base_url.'/remote.php/dav/files/'.rawurlencode($user).'/'.bratonien_tools_webdav_derivative_test_quote_path($webdav_path);
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 180,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $user.':'.$password,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FILE => $fp,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Derivative-Parallel-Test',
));
$ok = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
if ($ok === false || $errno !== 0 || $http < 200 || $http >= 300)
{
@unlink($destination);
$detail = 'Nextcloud-Download fehlgeschlagen (HTTP '.$http.', cURL '.$errno.($error !== '' ? ': '.$error : '').').';
return false;
}
if (!is_file($destination) || filesize($destination) < 1 || @getimagesize($destination) === false)
{
@unlink($destination);
$detail = 'Nextcloud-Datei ist kein lesbares Bild.';
return false;
}
return true;
}
function bratonien_tools_webdav_derivative_test_inner_url($derivative_path)
{
$derivative_root = PHPWG_ROOT_PATH.PWG_DERIVATIVE_DIR;
if (strpos($derivative_path, $derivative_root) !== 0) return null;
$location = ltrim(substr($derivative_path, strlen($derivative_root)), '/');
if ($location === '') return null;
$segments = array_map('rawurlencode', explode('/', $location));
return rtrim(get_absolute_root_url(), '/').'/i.php?/'.implode('/', $segments);
}
function bratonien_tools_webdav_derivative_test_call_i($url, &$response_body, &$response_type, &$response_status, &$detail=null)
{
$detail = '';
$response_body = '';
$response_type = 'application/octet-stream';
$response_status = 500;
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 180,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => false,
CURLOPT_USERAGENT => 'Bratonien-Tools-Derivative-Parallel-Gate',
));
$body = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$content_type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
$response_status = $status;
if ($content_type !== '') $response_type = $content_type;
if (is_string($body)) $response_body = $body;
if ($body === false || $errno !== 0)
{
$detail = 'Interner i.php-Aufruf fehlgeschlagen (cURL '.$errno.($error !== '' ? ': '.$error : '').').';
return false;
}
if ($status < 200 || $status >= 400)
{
$detail = 'i.php antwortete mit HTTP '.$status.'.';
return false;
}
return true;
}
global $user;
if (!isset($user['status']) || !in_array($user['status'], array('admin', 'webmaster'), true))
{
bratonien_tools_webdav_derivative_test_abort(403, 'Dieser Test-Endpunkt ist nur fuer Administratoren verfuegbar.');
}
$metrics_requested = isset($_GET['metrics']) && (string)$_GET['metrics'] === '1';
$metrics_started_at = microtime(true);
$memory_start = memory_get_usage(true);
if (function_exists('memory_reset_peak_usage'))
{
memory_reset_peak_usage();
}
$image_id = (int)($_GET['id'] ?? 0);
if ($image_id < 1)
{
bratonien_tools_webdav_derivative_test_abort(400, 'Bild-ID fehlt.');
}
$type = trim((string)($_GET['type'] ?? IMG_THUMB));
$defined = ImageStdParams::get_defined_type_map();
if (!isset($defined[$type]))
{
bratonien_tools_webdav_derivative_test_abort(400, 'Unbekannter oder deaktivierter Derivat-Typ: '.$type);
}
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result))
{
bratonien_tools_webdav_derivative_test_abort(404, 'Bild nicht gefunden.');
}
$image_row = pwg_db_fetch_assoc($result);
$source = bratonien_tools_webdav_derivative_test_source_info($image_id, $image_row);
if (!$source)
{
bratonien_tools_webdav_derivative_test_abort(404, 'Keine WebDAV-Quelle fuer dieses Bild gefunden.');
}
$upload_dir = rtrim((string)($GLOBALS['conf']['upload_dir'] ?? './upload'), '/');
$test_rel_dir = $upload_dir.'/bratonien-webdav-test';
$test_root = PHPWG_ROOT_PATH.$test_rel_dir;
if (!is_dir($test_root) && !@mkdir($test_root, 0755, true))
{
bratonien_tools_webdav_derivative_test_abort(500, 'Paralleler Testbereich konnte nicht angelegt werden: '.$test_root);
}
if (!is_writable($test_root))
{
bratonien_tools_webdav_derivative_test_abort(500, 'Paralleler Testbereich ist fuer PHP nicht beschreibbar: '.$test_root);
}
$lock_path = $test_root.'/image-'.$image_id.'.lock';
$lock = @fopen($lock_path, 'c');
if (!$lock || !flock($lock, LOCK_EX))
{
if ($lock) fclose($lock);
bratonien_tools_webdav_derivative_test_abort(503, 'Paralleler Test-Lock konnte nicht gesetzt werden.');
}
$extension = strtolower(pathinfo((string)($image_row['path'] ?? ''), PATHINFO_EXTENSION));
if (!in_array($extension, array('jpg', 'jpeg', 'png', 'gif', 'webp'), true))
{
$extension = 'jpg';
}
$token = getmypid().'-'.bin2hex(random_bytes(6));
$source_filename = 'source-'.$image_id.'-'.$token.'.'.$extension;
$source_path = $test_root.'/'.$source_filename;
$source_rel = $test_rel_dir.'/'.$source_filename;
$temp_image_id = 0;
$derivative_path = '';
$cleaned = false;
$cleanup = function() use (&$cleaned, &$temp_image_id, &$derivative_path, $source_path, $lock)
{
if ($cleaned) return;
$cleaned = true;
if ($temp_image_id > 0)
{
@pwg_query('DELETE FROM '.IMAGES_TABLE.' WHERE id='.(int)$temp_image_id.' LIMIT 1');
$temp_image_id = 0;
}
if ($derivative_path !== '' && is_file($derivative_path))
{
@unlink($derivative_path);
}
if (is_file($source_path))
{
@unlink($source_path);
}
@flock($lock, LOCK_UN);
@fclose($lock);
};
register_shutdown_function($cleanup);
try
{
$detail = '';
if (!bratonien_tools_webdav_derivative_test_download($source, $source_path, $detail))
{
bratonien_tools_webdav_derivative_test_abort(502, $detail);
}
$source_bytes = (int)filesize($source_path);
$dimensions = @getimagesize($source_path);
if (!is_array($dimensions) || empty($dimensions[0]) || empty($dimensions[1]))
{
bratonien_tools_webdav_derivative_test_abort(500, 'Paralleles Original besitzt keine gueltigen Bildabmessungen.');
}
$test_row = $image_row;
unset($test_row['id']);
$test_row['path'] = $source_rel;
$test_row['file'] = $source_filename;
if (array_key_exists('width', $test_row)) $test_row['width'] = (int)$dimensions[0];
if (array_key_exists('height', $test_row)) $test_row['height'] = (int)$dimensions[1];
single_insert(IMAGES_TABLE, $test_row);
$temp_image_id = (int)pwg_db_insert_id();
if ($temp_image_id < 1)
{
bratonien_tools_webdav_derivative_test_abort(500, 'Temporaerer Piwigo-Testeintrag konnte nicht angelegt werden.');
}
$test_row['id'] = $temp_image_id;
$test_src = new SrcImage($test_row);
$derivative = new DerivativeImage($defined[$type], $test_src);
if ($derivative->same_as_source())
{
bratonien_tools_webdav_derivative_test_abort(409, 'Dieser Typ ist fuer das parallele Testbild identisch mit der Quelle.');
}
$derivative_path = $derivative->get_path();
if ($derivative_path === '')
{
bratonien_tools_webdav_derivative_test_abort(500, 'Piwigo konnte keinen Derivat-Zielpfad fuer den parallelen Test bestimmen.');
}
if (is_file($derivative_path))
{
@unlink($derivative_path);
clearstatcache(true, $derivative_path);
}
$inner_url = bratonien_tools_webdav_derivative_test_inner_url($derivative_path);
if ($inner_url === null)
{
bratonien_tools_webdav_derivative_test_abort(500, 'Piwigo-i.php-URL konnte fuer den parallelen Test nicht bestimmt werden.');
}
$body = '';
$content_type = 'application/octet-stream';
$status = 500;
if (!bratonien_tools_webdav_derivative_test_call_i($inner_url, $body, $content_type, $status, $detail))
{
bratonien_tools_webdav_derivative_test_abort(502, $detail."\n".$body);
}
clearstatcache(true, $derivative_path);
if (!is_file($derivative_path) || !is_readable($derivative_path))
{
bratonien_tools_webdav_derivative_test_abort(
500,
"i.php antwortete, aber das parallele Piwigo-Derivat wurde nicht erzeugt.\n"
.'Testquelle: '.$source_rel."\n"
.'i.php: '.$inner_url
);
}
$derivative_size = @getimagesize($derivative_path);
if (!is_array($derivative_size) || empty($derivative_size[0]) || empty($derivative_size[1]))
{
bratonien_tools_webdav_derivative_test_abort(500, 'Das von i.php erzeugte parallele Derivat ist keine lesbare Bilddatei.');
}
$generated_width = (int)$derivative_size[0];
$generated_height = (int)$derivative_size[1];
$derivative_bytes = (int)filesize($derivative_path);
$cleanup();
$elapsed_ms = round((microtime(true) - $metrics_started_at) * 1000, 2);
$memory_peak = memory_get_peak_usage(true);
$memory_delta_peak = max(0, $memory_peak - $memory_start);
if ($metrics_requested)
{
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode(array(
'source_bytes'=>$source_bytes,
'derivative_bytes'=>$derivative_bytes,
'elapsed_ms'=>$elapsed_ms,
'memory_start_bytes'=>$memory_start,
'memory_peak_bytes'=>$memory_peak,
'memory_delta_peak_bytes'=>$memory_delta_peak,
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
}
header('X-Bratonien-WebDAV-Test: parallel-i.php-success');
header('X-Bratonien-Resource-Source-Bytes: '.$source_bytes);
header('X-Bratonien-Resource-Derivative-Bytes: '.$derivative_bytes);
header('X-Bratonien-Resource-Elapsed-Ms: '.$elapsed_ms);
header('X-Bratonien-Resource-Memory-Start-Bytes: '.$memory_start);
header('X-Bratonien-Resource-Memory-Peak-Bytes: '.$memory_peak);
header('X-Bratonien-Resource-Memory-Delta-Peak-Bytes: '.$memory_delta_peak);
header('X-Bratonien-WebDAV-Test-Size: '.$generated_width.'x'.$generated_height);
header('Content-Type: '.$content_type);
header('Content-Length: '.strlen($body));
header('Cache-Control: no-store');
http_response_code($status);
echo $body;
}
finally
{
$cleanup();
}
-560
View File
@@ -1,560 +0,0 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
require_once(PHPWG_ROOT_PATH.'include/derivative.inc.php');
if (!defined('BRATONIEN_TOOLS_PATH'))
{
define('BRATONIEN_TOOLS_ID', basename(__DIR__));
define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/');
}
require_once(BRATONIEN_TOOLS_PATH.'include/webdav_materialize_runtime.inc.php');
function bratonien_tools_webdav_derivative_debug($request_id, $event, array $fields=array())
{
$parts = array('[BRAT-WD '.$request_id.']', $event);
foreach ($fields as $key => $value)
{
if (is_bool($value)) $value = $value ? '1' : '0';
elseif ($value === null) $value = 'NULL';
elseif (is_array($value) || is_object($value)) $value = json_encode($value);
$parts[] = $key.'='.str_replace(array("\r", "\n"), array('\\r', '\\n'), (string)$value);
}
error_log(implode(' ', $parts));
}
function bratonien_tools_webdav_derivative_abort($status, $message)
{
http_response_code((int)$status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo $message;
exit;
}
function bratonien_tools_webdav_derivative_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
View File
@@ -1,296 +0,0 @@
<?php
define('PHPWG_ROOT_PATH', '../../');
include_once(PHPWG_ROOT_PATH.'include/common.inc.php');
function bratonien_tools_webdav_download_test_abort($status, $message)
{
http_response_code((int)$status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo $message;
exit;
}
function bratonien_tools_webdav_download_test_decrypt_secret($blob, $hex_key)
{
$hex_key = trim((string)$hex_key);
if (!preg_match('/^[a-f0-9]{64}$/', $hex_key)) return null;
$outer = base64_decode(trim((string)$blob), true);
$payload = is_string($outer) ? json_decode($outer, true) : null;
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) return null;
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
if (!is_string($iv) || !is_string($tag) || !is_string($cipher)) return null;
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hex_key), OPENSSL_RAW_DATA, $iv, $tag);
if ($plain === false) return null;
$decoded = json_decode((string)$plain, true);
return is_array($decoded) ? $decoded : null;
}
function bratonien_tools_webdav_download_test_quote_path($path)
{
$parts = array_values(array_filter(explode('/', trim((string)$path, '/')), 'strlen'));
return implode('/', array_map('rawurlencode', $parts));
}
function bratonien_tools_webdav_download_test_source_info($image_id, array $image_row)
{
$path = (string)($image_row['path'] ?? '');
if ($image_id < 1 || $path === '') return null;
$absolute = $path;
if (strpos($absolute, '/') !== 0)
{
$absolute = PHPWG_ROOT_PATH.ltrim(preg_replace('#^\\./#', '', $absolute), '/');
}
$resolved = realpath($absolute);
if ($resolved === false) return null;
$normalized = str_replace('\\', '/', $resolved);
if (!preg_match('#/nc-webdav-source/connection-([0-9]+)/root-([0-9]+)/(.*)$#', $normalized, $match))
{
return null;
}
$connection_id = (int)$match[1];
$root_fileid = (int)$match[2];
$relative_path = trim((string)$match[3], '/');
if ($connection_id < 1 || $root_fileid < 1 || $relative_path === '') return null;
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$result = pwg_query('SELECT config_json FROM `'.$table.'` WHERE id='.$connection_id.' LIMIT 1');
if (!pwg_db_num_rows($result)) return null;
$row = pwg_db_fetch_assoc($result);
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config) || (string)($config['source_mode'] ?? '') !== 'webdav-placeholder') return null;
$root_found = false;
$root_path = '';
foreach ((array)($config['roots'] ?? array()) as $root)
{
if ((int)($root['fileid'] ?? 0) === $root_fileid)
{
$root_found = true;
$root_path = trim((string)($root['webdav_path'] ?? ''), '/');
break;
}
}
if (!$root_found) return null;
$webdav_path = trim($root_path === '' ? $relative_path : $root_path.'/'.$relative_path, '/');
if ($webdav_path === '') return null;
$size = 0;
$state_dir = rtrim((string)($config['state_dir'] ?? ''), '/');
if ($state_dir !== '')
{
$mapping_file = $state_dir.'/webdav-map.json';
if (is_readable($mapping_file))
{
$mapping = json_decode((string)file_get_contents($mapping_file), true);
if (is_array($mapping) && isset($mapping['files']) && is_array($mapping['files']))
{
$entry = $mapping['files'][$resolved] ?? $mapping['files'][$normalized] ?? null;
if (is_array($entry) && (string)($entry['kind'] ?? '') === 'file')
{
$webdav_path = trim((string)($entry['webdav_path'] ?? $webdav_path), '/');
$size = (int)($entry['size'] ?? 0);
}
}
}
}
return array(
'connection_id'=>$connection_id,
'webdav_path'=>$webdav_path,
'size'=>$size,
);
}
function bratonien_tools_webdav_download_test_connection(array $source)
{
$table = $GLOBALS['prefixeTable'].'bratonien_tools_nc_connections';
$result = pwg_query('SELECT config_json, secret_blob FROM `'.$table.'` WHERE id='.(int)$source['connection_id'].' LIMIT 1');
if (!pwg_db_num_rows($result)) return null;
$row = pwg_db_fetch_assoc($result);
$config = json_decode((string)$row['config_json'], true);
if (!is_array($config)) return null;
$key_result = pwg_query("SELECT value FROM ".$GLOBALS['prefixeTable']."config WHERE param='bratonien_nc_connector_secret' LIMIT 1");
if (!pwg_db_num_rows($key_result)) return null;
$key_row = pwg_db_fetch_assoc($key_result);
$credentials = bratonien_tools_webdav_download_test_decrypt_secret((string)$row['secret_blob'], (string)$key_row['value']);
if (!is_array($credentials)) return null;
$base_url = rtrim((string)($config['nextcloud_url'] ?? ''), '/');
$user = trim((string)($credentials['nextcloud_user'] ?? ''));
$password = (string)($credentials['nextcloud_password'] ?? '');
if ($base_url === '' || $user === '' || $password === '') return null;
return array('base_url'=>$base_url, 'user'=>$user, 'password'=>$password);
}
global $user;
if (!isset($user['status']) || !in_array($user['status'], array('admin', 'webmaster'), true))
{
bratonien_tools_webdav_download_test_abort(403, 'Dieser Test-Endpunkt ist nur fuer Administratoren verfuegbar.');
}
$image_id = (int)($_GET['id'] ?? 0);
$parallel = (int)($_GET['parallel'] ?? 1);
if ($image_id < 1)
{
bratonien_tools_webdav_download_test_abort(400, 'Bild-ID fehlt.');
}
if ($parallel < 1 || $parallel > 20)
{
bratonien_tools_webdav_download_test_abort(400, 'parallel muss zwischen 1 und 20 liegen.');
}
$result = pwg_query('SELECT * FROM '.IMAGES_TABLE.' WHERE id='.$image_id.' LIMIT 1');
if (!pwg_db_num_rows($result))
{
bratonien_tools_webdav_download_test_abort(404, 'Bild nicht gefunden.');
}
$image_row = pwg_db_fetch_assoc($result);
$source = bratonien_tools_webdav_download_test_source_info($image_id, $image_row);
if (!$source)
{
bratonien_tools_webdav_download_test_abort(404, 'Keine WebDAV-Quelle fuer dieses Bild gefunden.');
}
$connection = bratonien_tools_webdav_download_test_connection($source);
if (!$connection)
{
bratonien_tools_webdav_download_test_abort(500, 'WebDAV-Verbindung konnte nicht geladen werden.');
}
$upload_dir = rtrim((string)($GLOBALS['conf']['upload_dir'] ?? './upload'), '/');
$test_rel_dir = $upload_dir.'/bratonien-webdav-download-test';
$test_root = PHPWG_ROOT_PATH.$test_rel_dir;
if (!is_dir($test_root) && !@mkdir($test_root, 0755, true))
{
bratonien_tools_webdav_download_test_abort(500, 'Download-Testbereich konnte nicht angelegt werden: '.$test_root);
}
if (!is_writable($test_root))
{
bratonien_tools_webdav_download_test_abort(500, 'Download-Testbereich ist fuer PHP nicht beschreibbar: '.$test_root);
}
$url = $connection['base_url'].'/remote.php/dav/files/'.rawurlencode($connection['user']).'/'.bratonien_tools_webdav_download_test_quote_path($source['webdav_path']);
$token = getmypid().'-'.bin2hex(random_bytes(6));
$multi = curl_multi_init();
$items = array();
for ($i = 0; $i < $parallel; $i++)
{
$path = $test_root.'/download-'.$image_id.'-'.$token.'-'.$i.'.tmp';
$fp = @fopen($path, 'xb');
if (!$fp)
{
foreach ($items as $item)
{
@fclose($item['fp']);
@unlink($item['path']);
curl_multi_remove_handle($multi, $item['ch']);
curl_close($item['ch']);
}
curl_multi_close($multi);
bratonien_tools_webdav_download_test_abort(500, 'Temporaere Download-Datei konnte nicht angelegt werden.');
}
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 180,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $connection['user'].':'.$connection['password'],
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FAILONERROR => false,
CURLOPT_FILE => $fp,
CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Download-Stress-Test',
));
curl_multi_add_handle($multi, $ch);
$items[] = array('ch'=>$ch, 'fp'=>$fp, 'path'=>$path);
}
$started_at = microtime(true);
do
{
$status = curl_multi_exec($multi, $running);
if ($status !== CURLM_OK) break;
if ($running > 0)
{
$selected = curl_multi_select($multi, 1.0);
if ($selected === -1) usleep(10000);
}
}
while ($running > 0);
$elapsed_seconds = microtime(true) - $started_at;
$downloads = array();
$total_bytes = 0;
$successful = 0;
$failed = 0;
foreach ($items as $index => $item)
{
@fflush($item['fp']);
@fclose($item['fp']);
$http = (int)curl_getinfo($item['ch'], CURLINFO_HTTP_CODE);
$time_ms = round((float)curl_getinfo($item['ch'], CURLINFO_TOTAL_TIME) * 1000, 2);
$errno = curl_errno($item['ch']);
$error = curl_error($item['ch']);
$bytes = is_file($item['path']) ? (int)filesize($item['path']) : 0;
$ok = ($errno === 0 && $http >= 200 && $http < 300 && $bytes > 0);
if ($ok) $successful++; else $failed++;
$total_bytes += $bytes;
$downloads[] = array(
'index'=>$index + 1,
'ok'=>$ok,
'http'=>$http,
'bytes'=>$bytes,
'elapsed_ms'=>$time_ms,
'mbps'=>$time_ms > 0 ? round(($bytes * 8) / ($time_ms / 1000) / 1000000, 2) : 0,
'curl_errno'=>$errno,
'curl_error'=>$error,
);
@unlink($item['path']);
curl_multi_remove_handle($multi, $item['ch']);
curl_close($item['ch']);
}
curl_multi_close($multi);
$elapsed_ms = round($elapsed_seconds * 1000, 2);
$aggregate_mbps = $elapsed_seconds > 0 ? round(($total_bytes * 8) / $elapsed_seconds / 1000000, 2) : 0;
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode(array(
'image_id'=>$image_id,
'parallel'=>$parallel,
'same_source_repeated'=>true,
'expected_source_bytes'=>(int)$source['size'],
'successful'=>$successful,
'failed'=>$failed,
'total_bytes'=>$total_bytes,
'peak_temp_disk_bytes'=>$total_bytes,
'elapsed_ms'=>$elapsed_ms,
'aggregate_mbps'=>$aggregate_mbps,
'downloads'=>$downloads,
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+2 -3
View File
@@ -8,7 +8,6 @@ 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)
{ {
@@ -49,7 +48,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_materialize_source_info($image_id); $source = bratonien_tools_webdav_image_source_info($image_id);
if (!$source) bratonien_tools_webdav_image_abort(404, 'Keine WebDAV-Quelle für dieses Bild gefunden.'); if (!$source) bratonien_tools_webdav_image_abort(404, 'Keine WebDAV-Quelle für dieses Bild gefunden.');
if (!empty($_GET['preview'])) if (!empty($_GET['preview']))
@@ -121,7 +120,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.8.6', CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.6.1',
CURLOPT_HEADERFUNCTION => function($ch, $line) CURLOPT_HEADERFUNCTION => function($ch, $line)
{ {
$length = strlen($line); $length = strlen($line);