mirror of
https://github.com/Terranom674/Piwigo_Bratonien_Tools.git
synced 2026-09-19 19:54:31 +00:00
Compare commits
35 Commits
1bb2088d06
...
577d1ebf81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
577d1ebf81 | ||
|
|
3bd3158817 | ||
|
|
c916d742f5 | ||
|
|
6f0891f6a8 | ||
|
|
b8ea6b1fe2 | ||
|
|
aeba24c6fa | ||
|
|
0d81d83d12 | ||
|
|
dbedb83c14 | ||
|
|
04620f20a2 | ||
|
|
2127649209 | ||
|
|
fd5b55ac53 | ||
|
|
933db21d88 | ||
|
|
a854bda115 | ||
|
|
08040edf8f | ||
|
|
bb024000f2 | ||
|
|
539605b19a | ||
|
|
a550bbd01a | ||
|
|
e344455e0c | ||
|
|
88edd5aee9 | ||
|
|
aa7ddefa0b | ||
|
|
ef37e3b612 | ||
|
|
eadee3cc24 | ||
|
|
d05a6f4f46 | ||
|
|
04b32309df | ||
|
|
db28692cfb | ||
|
|
5866824398 | ||
|
|
ac419ac56f | ||
|
|
65910375d2 | ||
|
|
c547c9b089 | ||
|
|
21caddcec5 | ||
|
|
237500a8fa | ||
|
|
f5dae306db | ||
|
|
20bd5105f3 | ||
|
|
82019fb14d | ||
|
|
17e4f66126 |
@@ -4,119 +4,184 @@ Stand: 18.08.2026
|
||||
|
||||
## Plugin
|
||||
|
||||
- Aktuelle Plugin-Version: **0.9.4.4**
|
||||
- Aktueller Entwicklungsblock: **NC Connector – Endnutzer-Assistent und Konsolidierung**
|
||||
- `0.9.3.43` markiert den abgeschlossenen vorherigen Meilenstein.
|
||||
- `0.9.4.x` ist der neue Entwicklungsblock.
|
||||
- Aktuelle Plugin-Version: **0.9.5.7**
|
||||
- Aktueller Entwicklungsblock: **NC Connector – WebDAV-basierter Parallelweg bei vollständigem Erhalt des bestehenden produktiven Wegs**
|
||||
- GitHub ist das führende Repository.
|
||||
- Das private Gitea-System bleibt Mirror/Fallback.
|
||||
|
||||
## Produktiver Stand
|
||||
## Nicht verhandelbare Migrationsregel
|
||||
|
||||
Der lokale NC Connector ist im aktuellen Bratonien-Einsatz End-to-End funktionsfähig:
|
||||
Der bestehende lokale NC Connector bleibt vollständig erhalten, bis der neue WebDAV-Weg End-to-End funktioniert.
|
||||
|
||||
- Nextcloud-Freigaben `folder` und `file` werden verarbeitet.
|
||||
- Ordnerfreigaben werden als physische Piwigo-Alben synchronisiert.
|
||||
- Einzeldateifreigaben werden als echte Piwigo-Orphans ohne Albumzuordnung registriert.
|
||||
- Entfernte Freigaben werden aus Piwigo entfernt, Originaldateien bleiben erhalten.
|
||||
- Neue physische Connector-Alben werden privat angelegt.
|
||||
- API-first-Synchronisierung ist produktiv für Piwigo **16.4.0** freigegeben.
|
||||
- Piwigo-Album- und Fotoinformationen werden nach dem direkten Sync aktualisiert.
|
||||
- Der gemeinsame systemd-Timer verarbeitet aktive Verbindungen regelmäßig.
|
||||
Bestehende Quellenmodi:
|
||||
|
||||
## 0.9.4.1
|
||||
- `legacy-view`
|
||||
- `user-shares`
|
||||
- `selected-fileids`
|
||||
|
||||
Die Statusaktualisierung im Admin-UI wurde von Seiten-Reload auf gezielte DOM-Aktualisierung umgestellt. Der Poller aktualisiert nur noch **Letzter Lauf** und **Nächster Lauf**. Offene Dialoge, Details und der Assistent bleiben dadurch erhalten.
|
||||
Sie werden nicht automatisch migriert, deaktiviert oder auf den neuen Weg umgestellt.
|
||||
|
||||
## 0.9.4.2
|
||||
Der neue Weg entsteht zusätzlich als eigener Modus:
|
||||
|
||||
### Verbindungsassistent
|
||||
- `webdav-placeholder`
|
||||
|
||||
Der Assistent wurde aus Endnutzersicht konsolidiert. Technische Werte werden nur gezielt abgefragt, wenn eine automatische Prüfung sie nicht bestätigen kann.
|
||||
## Ziel des neuen Wegs
|
||||
|
||||
### Storage-Mappings
|
||||
Benötigt werden sollen nur:
|
||||
|
||||
Das Storage-Format erlaubt einen leeren `source_prefix`. Damit kann ein kompletter Storage direkt auf einen lokalen Mount zeigen.
|
||||
- normale Piwigo-Installation;
|
||||
- bereits vorhandene Linux-/PHP-Umgebung;
|
||||
- Nextcloud-Adresse;
|
||||
- Nextcloud-Benutzer bzw. App-Passwort;
|
||||
- WebDAV-Zugriff auf die Inhalte dieses Benutzers.
|
||||
|
||||
### Native Aktivierung
|
||||
Nicht vorausgesetzt werden dürfen:
|
||||
|
||||
`nc-connector-install.php` unterstützt API-only-Verbindungen ohne dauerhaft gespeicherten Login-Fallback. Ein Fallback ist nur nötig, wenn keine nutzbare API gespeichert ist.
|
||||
- PostgreSQL-Zugriff auf Nextcloud;
|
||||
- `occ`-Adminzugriff;
|
||||
- Rootzugriff des Betreibers;
|
||||
- Storage-IDs oder Backend-Pfade;
|
||||
- zusätzliche Host-Mounts;
|
||||
- FUSE;
|
||||
- davfs;
|
||||
- rclone;
|
||||
- zusätzliche Connector-Systempakete.
|
||||
|
||||
## 0.9.4.3
|
||||
Originalbilder werden nicht dauerhaft nach Piwigo kopiert.
|
||||
|
||||
- Syntaxfehler im Wizard-Abschluss korrigiert.
|
||||
- Neue Wizard-Durchläufe werden von alten serverseitigen Zuständen getrennt.
|
||||
- Abbrechen und Schließen verwerfen den laufenden Wizard-State.
|
||||
## Architektur des WebDAV-Parallelwegs
|
||||
|
||||
## 0.9.4.4
|
||||
1. Ausgewählte Nextcloud-Verzeichnisse werden über WebDAV/PROPFIND gelesen.
|
||||
2. Das Plugin erfasst Ordner, Dateinamen, Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad.
|
||||
3. Für jedes Bild wird nur ein winziger lokaler Platzhalter bereitgestellt.
|
||||
4. Der bestehende Shadow Tree bleibt die physische Piwigo-Quelle.
|
||||
5. Der Shadow Tree bildet die reale Ordner- und Dateinamensstruktur ab, seine Bildziele zeigen jedoch auf Platzhalter statt auf Originale.
|
||||
6. Ein separates Mapping verbindet Shadow-Tree-Pfad, Connection-ID, Nextcloud-Datei-ID und WebDAV-Pfad.
|
||||
7. Piwigo soll Album und Bild über diese Struktur registrieren.
|
||||
8. Wenn echte Bilddaten benötigt werden, wird das Original bei Bedarf über WebDAV gelesen.
|
||||
9. Piwigo-Derivate werden normal lokal unter `_data/i/` gecacht.
|
||||
10. Das Original bleibt ausschließlich in Nextcloud.
|
||||
|
||||
Der Connector-Benutzer aus Schritt 1 ist zugleich der lesende Datenbank-Benutzer. Der Assistent fragt deshalb Benutzername und Passwort nicht erneut ab.
|
||||
## Bereits vorhandene Bausteine
|
||||
|
||||
Ablauf:
|
||||
### `runtime/lib/build_webdav_placeholder_source.py`
|
||||
|
||||
1. Nextcloud-Adresse + Connector-Benutzer + Passwort.
|
||||
2. Web-Zugang und OCS werden geprüft.
|
||||
3. Derselbe Benutzer und dasselbe Passwort werden automatisch für PostgreSQL verwendet.
|
||||
4. Der Assistent prüft den üblichen Weg mit Nextcloud-Host, Port 5432 und Datenbank `nextcloud` bzw. bekannte passende Verbindungswerte.
|
||||
5. Nur wenn die Datenbank dort nicht erreichbar ist, werden Host, Port und Datenbankname abgefragt. Benutzer und Passwort bleiben unverändert aus Schritt 1.
|
||||
6. Danach folgen gegebenenfalls Storage-Zuordnung, Showcase-Benutzer, Piwigo-API und Fallback.
|
||||
- liest WebDAV rekursiv;
|
||||
- lädt keine Originalbilder herunter;
|
||||
- erzeugt eine lokale Platzhalterquelle;
|
||||
- erzeugt Manifest und WebDAV-Mapping;
|
||||
- verwendet einen nur wenige Dutzend Byte großen Platzhalter.
|
||||
|
||||
Die PHP-Datei `include/nc_connector_wizard.inc.php` wurde vor dem Versionsbump mit dem PHP-Parser geprüft.
|
||||
### 0.9.5.6 – Verbindungsschicht
|
||||
|
||||
## Architektur NC Connector
|
||||
Der Parallelweg wurde als eigener Connection-Typ im Plugin angelegt.
|
||||
|
||||
- Nextcloud ist Quelle der Originalbilder.
|
||||
- Piwigo erhält einen Shadow Tree aus Verzeichnissen und Symlinks.
|
||||
- PostgreSQL-Reader und die Views `piwigo_showcase_sources` / `piwigo_showcase_activity` liefern Quellen und Aktivität.
|
||||
- Runtime-Konfigurationen: `/etc/bratonien-tools/nc-connector/connection-*.conf`
|
||||
- State: `/var/lib/bratonien-tools/nc-connector/connection-ID`
|
||||
- Öffentlicher Admin-Status: Piwigo `_data/bratonien-tools/nc-connector-status/`
|
||||
- Runner: `runtime/run-all.sh`
|
||||
- Einzelverbindung: `runtime/sync.sh`
|
||||
- `include/nc_connector_webdav.inc.php`
|
||||
- Backend-Aktion `nc_connector_create_webdav_parallel`
|
||||
- `source_mode=webdav-placeholder`
|
||||
- `adapter=remote`
|
||||
- ausgewählte WebDAV-Wurzeln werden verbindungseigen gespeichert;
|
||||
- Nextcloud-Basis-URL und Benutzer werden verbindungseigen gespeichert;
|
||||
- Secret v3 enthält `nextcloud_user` und `nextcloud_password` rückwärtskompatibel zu älteren Secrets.
|
||||
|
||||
## Piwigo-Synchronisierung
|
||||
## 0.9.5.7 – parallele Runtime angelegt
|
||||
|
||||
Priorität:
|
||||
Der neue Weg besitzt jetzt eine eigene Runtime neben dem bestehenden produktiven Connector.
|
||||
|
||||
1. gespeicherter Piwigo-API-Key;
|
||||
2. bei nicht nutzbarer API gespeicherter Benutzername/Passwort-Fallback;
|
||||
3. ohne beide Wege Abbruch mit Fehlerstatus.
|
||||
Neu:
|
||||
|
||||
Produktive API-Methoden:
|
||||
- `runtime/reconcile-webdav.php`
|
||||
- `runtime/sync-webdav.sh`
|
||||
- `runtime/run-all.sh` verarbeitet lokale und WebDAV-Verbindungen getrennt.
|
||||
|
||||
- `bratonien.nc.syncProductive`
|
||||
- `bratonien.nc.syncOrphans`
|
||||
Wesentliche Regeln:
|
||||
|
||||
`bratonien.nc.syncProductive` ist auf Piwigo 16.4.0 versionsgebunden. Der direkte Sync verwendet Bratonien-eigene Logik auf Basis der Piwigo-Core-Funktionen, nicht das Rendern oder Fernsteuern einer Admin-Seite.
|
||||
- `runtime/reconcile.php` für die bestehenden lokalen Modi bleibt unverändert.
|
||||
- `runtime/sync.sh` für die bestehenden lokalen Modi bleibt unverändert.
|
||||
- WebDAV-Verbindungen erhalten eigene Runtime-Dateien `webdav-connection-ID.*`.
|
||||
- Es werden keine PostgreSQL-/View-/Storage-Mapping-Werte für WebDAV verlangt.
|
||||
- Nextcloud-Zugang wird aus dem verschlüsselten Connection-Secret in eine Runtime-Passwortdatei mit restriktiven Rechten geschrieben.
|
||||
- WebDAV-Wurzeln werden getrennt als Runtime-Konfiguration gespeichert.
|
||||
- Der Platzhalter-Builder erzeugt die lokale Platzhalterquelle, Manifest und WebDAV-Mapping.
|
||||
- Das Manifest wird anschließend durch den bestehenden `shadow_tree.py` verarbeitet.
|
||||
- Der parallele Shadow Tree liegt absichtlich unter `galleries/bratonien-webdav-ID` und kann deshalb den bestehenden Galeriebaum nicht ersetzen oder überschreiben.
|
||||
- Die Platzhalterquelle liegt unter Piwigo `_data/bratonien-tools/nc-webdav-source/connection-ID`, damit Piwigo den Symlink-Zielen später folgen kann.
|
||||
- **Piwigo-Synchronisierung ist in dieser Stufe hart auf `PIWIGO_SYNC_ENABLED=0` gesetzt.** Die Runtime darf aktuell nur den parallelen Shadow Tree bauen. Die Registrierung in Piwigo wird erst nach Sichtprüfung bewusst freigeschaltet.
|
||||
|
||||
## Activity Gate und Shadow Tree
|
||||
Damit existieren lokaler Produktivweg und WebDAV-Testweg jetzt gleichzeitig, ohne dass ein Umzug stattgefunden hat.
|
||||
|
||||
Das Activity Gate berücksichtigt:
|
||||
## Bestehender produktiver Weg
|
||||
|
||||
- Aktivitätsstand;
|
||||
- Share-Fingerprint;
|
||||
- Quiet-Time;
|
||||
- Max-Wartezeit;
|
||||
- periodischen Full-Sync;
|
||||
- Reparaturbedarf bei beschädigter lokaler Struktur.
|
||||
Unverändert:
|
||||
|
||||
Der Shadow-Tree-Wechsel besitzt einen Rollback auf den vorherigen Baum, falls das Umschalten fehlschlägt.
|
||||
- PostgreSQL-/View-/Storage-Mapping-Logik;
|
||||
- lokale Symlinks auf bereits vorhandene Originalpfade;
|
||||
- `runtime/reconcile.php` für bestehende lokale Adapter;
|
||||
- `runtime/sync.sh` mit `legacy-view`, `user-shares` und `selected-fileids`;
|
||||
- API-first-Piwigo-Sync;
|
||||
- gemeinsame Runtime für aktive bestehende Verbindungen.
|
||||
|
||||
## Sicherheit
|
||||
## Gemessene WebDAV-Performance
|
||||
|
||||
- Nextcloud-Originale werden nicht gelöscht.
|
||||
- Connector-Zugangsdaten werden verschlüsselt gespeichert.
|
||||
- Wizard-Secrets bleiben serverseitig.
|
||||
- Storage-Pfade werden validiert.
|
||||
- SQL-View-Namen werden validiert.
|
||||
- Piwigo-API ist versionsgebunden.
|
||||
- Mutierende Admin-Aktionen verwenden Post/Redirect/Get, sodass Browser-Reloads sie nicht erneut ausführen.
|
||||
- Self-Updates sind an einen konkreten Commit und SHA-256 gebunden.
|
||||
Testbild: 16.091.204 Byte.
|
||||
|
||||
## Noch offen
|
||||
Intern:
|
||||
|
||||
- vollständiger Remote-Nextcloud-Adapter ohne direkten PostgreSQL-/Storage-Zugriff;
|
||||
- weitere Bereinigung alter Legacy-Migrationshelfer, sobald sie nicht mehr benötigt werden;
|
||||
- weitere Endnutzer-Optimierung des Assistenten auf Basis realer Fehler- und Installationspfade.
|
||||
- 1,829 s
|
||||
- 8.796.915 Byte/s
|
||||
|
||||
## Repository-Fallback
|
||||
Extern:
|
||||
|
||||
GitHub bleibt das führende Repository. Das private Gitea-System dient weiterhin als Mirror/Fallback.
|
||||
- 1,865 s
|
||||
- 8.627.524 Byte/s
|
||||
|
||||
Die externe Verbindung war im Test nur ungefähr 2 % langsamer. Der Ansatz bleibt deshalb für bedarfsweisen Originalzugriff geeignet, solange Piwigo-Derivate lokal gecacht werden.
|
||||
|
||||
## Löschverhalten seit 0.9.5.5
|
||||
|
||||
Das Löschen einer Connector-Verbindung darf nicht an Dateirechten Root-eigener Runtime-Dateien scheitern.
|
||||
|
||||
- Datenbankeintrag wird entfernt;
|
||||
- verwaiste Runtime-Dateien werden vor einem späteren Sync bereinigt;
|
||||
- andere Verbindungen bleiben unangetastet;
|
||||
- Nextcloud-Originale und vorhandene Piwigo-Bilder werden nicht gelöscht.
|
||||
|
||||
## Nächster Testschritt
|
||||
|
||||
Die parallele Runtime ist jetzt vorbereitet. Der nächste Schritt ist bewusst klein und kontrolliert:
|
||||
|
||||
1. Plugin auf 0.9.5.7 aktualisieren.
|
||||
2. Eine kleine neue WebDAV-Testverbindung mit einem überschaubaren Verzeichnis anlegen.
|
||||
3. Gemeinsamen Runner einmal ausführen lassen.
|
||||
4. Prüfen, ob unter `galleries/bratonien-webdav-ID` ausschließlich die erwartete Ordnerstruktur und Platzhalter-Symlinks entstehen.
|
||||
5. WebDAV-Mapping und Manifest im Connection-State prüfen.
|
||||
6. Bestehende produktive Galerie und bestehende Verbindungen dabei auf Unverändertheit prüfen.
|
||||
|
||||
Erst wenn dieser Test sauber ist, wird `PIWIGO_SYNC_ENABLED` für WebDAV separat freigeschaltet und die Piwigo-Registrierung getestet.
|
||||
|
||||
## Danach – noch nicht umsetzen
|
||||
|
||||
Erst wenn die Registrierung mit Platzhaltern funktioniert:
|
||||
|
||||
- echte Bilddaten über WebDAV anfordern;
|
||||
- Piwigo-Derivate aus echten Originalen erzeugen;
|
||||
- normalen Derivat-Cache weiterverwenden;
|
||||
- ETag-basierte Änderungserkennung ergänzen;
|
||||
- Fehlerfälle und Parallelität testen.
|
||||
|
||||
## Voraussetzung vor irgendeinem Umzug
|
||||
|
||||
Erfolgreich getestet sein müssen mindestens:
|
||||
|
||||
- Verbindungsanlage;
|
||||
- WebDAV-Verzeichnisauswahl;
|
||||
- Shadow Tree;
|
||||
- Piwigo-Registrierung;
|
||||
- echte Bildausgabe;
|
||||
- Derivat-Cache;
|
||||
- Änderungserkennung;
|
||||
- Löschen/Deaktivieren;
|
||||
- Fehlerbehandlung;
|
||||
- Verhalten bei vielen noch ungecachten Bildern.
|
||||
|
||||
Erst danach kann eine freiwillige Migration bestehender Verbindungen überhaupt diskutiert werden.
|
||||
|
||||
147
README.md
147
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
Modulares Piwigo-Plugin für Administration, Bildverarbeitung, geschützte Freigaben, Fotoauswahl und die Anbindung von Nextcloud an Piwigo.
|
||||
|
||||
Aktuelle Plugin-Version: **0.9.4.4**
|
||||
Aktuelle Plugin-Version: **0.9.5.6**
|
||||
|
||||
## Grundprinzip
|
||||
|
||||
@@ -12,62 +12,104 @@ Für die Administration gilt: Der normale Nutzer sieht Aufgaben, Entscheidungen
|
||||
|
||||
## NC Connector
|
||||
|
||||
Der NC Connector synchronisiert ausdrücklich freigegebene Nextcloud-Inhalte mit Piwigo.
|
||||
Der NC Connector synchronisiert ausdrücklich ausgewählte Nextcloud-Inhalte mit Piwigo.
|
||||
|
||||
### Datenmodell
|
||||
|
||||
- Nextcloud bleibt die Quelle der Originaldateien.
|
||||
- Originalbilder werden nicht in eine zweite dauerhafte Bibliothek kopiert.
|
||||
- Ein Shadow Tree erzeugt die für Piwigo benötigte Verzeichnisstruktur mit Symlinks.
|
||||
- Ordnerfreigaben werden als physische Piwigo-Alben synchronisiert.
|
||||
- Einzeldateifreigaben werden im Galerie-Root verlinkt und als echte Piwigo-Orphans ohne Albumzuordnung registriert.
|
||||
- Entfernte Freigaben werden aus Shadow Tree und Piwigo entfernt, das Nextcloud-Original bleibt erhalten.
|
||||
- Ein Shadow Tree erzeugt die für Piwigo benötigte Verzeichnisstruktur.
|
||||
- Der bestehende produktive Weg verwendet lokale bereits vorhandene Speicherpfade und Symlinks auf die Originale.
|
||||
- Ordner werden als physische Piwigo-Alben synchronisiert.
|
||||
- Root-Dateien können als echte Piwigo-Orphans ohne Albumzuordnung registriert werden.
|
||||
- Entfernte Quellen werden aus Shadow Tree und Piwigo entfernt, das Nextcloud-Original bleibt erhalten.
|
||||
- Neu importierte physische Connector-Alben werden privat angelegt.
|
||||
|
||||
### Bestehende produktive Quellenmodi
|
||||
|
||||
Der bestehende Connector unterstützt weiterhin getrennte Quellenmodi:
|
||||
|
||||
- `legacy-view`
|
||||
- `user-shares`
|
||||
- `selected-fileids`
|
||||
|
||||
Diese Modi bleiben während der Entwicklung des neuen WebDAV-Wegs vollständig erhalten. Es gibt keine automatische Migration bestehender Verbindungen.
|
||||
|
||||
### Verbindungsassistent
|
||||
|
||||
Neue Verbindungen werden bevorzugt mit einem Endnutzer-Assistenten angelegt.
|
||||
|
||||
Ablauf:
|
||||
|
||||
1. Nextcloud-Adresse, Benutzer und Passwort eingeben.
|
||||
2. Der Assistent prüft HTTP/HTTPS, Nextcloud-Status und Anmeldung.
|
||||
3. Derselbe in Schritt 1 angegebene Connector-Benutzer und dasselbe Passwort werden auch für den lesenden PostgreSQL-Zugriff verwendet; es gibt im normalen Ablauf keine zweite Benutzer-/Passwortabfrage.
|
||||
4. Vorhandene passende Connector-Werte für Host, Port, Datenbank und Storage-Zuordnungen werden wiederverwendet, aber erneut mit dem aktuellen Zugang geprüft.
|
||||
5. Nur wenn der übliche PostgreSQL-Weg nicht erreichbar ist, werden abweichende Datenbank-Adresse, Port und Datenbankname gezielt abgefragt. Benutzer und Passwort bleiben dabei die Angaben aus Schritt 1.
|
||||
6. Nicht automatisch zuordenbare Storage-Mounts werden einzeln bestätigt.
|
||||
7. Danach wird der Nextcloud-Benutzer gewählt, dessen Freigaben verwendet werden sollen. Empfohlen wird ein eigener `showcase`-Benutzer.
|
||||
8. Piwigo-API-Zugang testen oder bewusst überspringen.
|
||||
9. Optionaler bzw. bei übersprungener API verpflichtender Login-Fallback.
|
||||
10. Erst der Abschluss legt die Verbindung dauerhaft an.
|
||||
Der aktuelle Assistent kann Nextcloud per HTTP/HTTPS und OCS prüfen und Verzeichnisse per WebDAV anzeigen. Für den bestehenden produktiven lokalen Weg existieren weiterhin PostgreSQL-/View-/Storage-Mapping-Schritte, sofern sie benötigt werden.
|
||||
|
||||
Wizard-Geheimnisse werden während der Einrichtung serverseitig in der Sitzung gehalten. Ein fehlgeschlagener Test leert die Eingaben nicht. Ein API-Test verändert die dauerhafte API-Konfiguration erst beim erfolgreichen Abschluss des Assistenten.
|
||||
|
||||
Die vollständige technische Maske bleibt weiterhin über **Ohne Assistent anlegen** sowie bei bestehenden Verbindungen unter **Technische Einstellungen** erreichbar.
|
||||
|
||||
### Lokaler Adapter
|
||||
### Lokaler produktiver Adapter
|
||||
|
||||
Der derzeit produktive Adapter verwendet:
|
||||
Der derzeit produktive Adapter verwendet je nach bestehender Verbindung:
|
||||
|
||||
- PostgreSQL-Reader mit minimalen Leserechten;
|
||||
- Source-View `piwigo_showcase_sources`;
|
||||
- Activity-View `piwigo_showcase_activity`;
|
||||
- PostgreSQL-Reader;
|
||||
- Source-/Activity-Views;
|
||||
- explizite Storage-Zuordnungen auf bereits vorhandene lokale Mounts;
|
||||
- Plugin-eigene Runtime und getrennten State pro Verbindung.
|
||||
|
||||
Ein Storage-Mapping besteht aus `storage_id`, einem optionalen `source_prefix` und dem lokalen Mount. Ein leerer Prefix ist zulässig und bedeutet, dass der Mount direkt dem Storage-Root entspricht.
|
||||
Dieser Weg bleibt erhalten, bis der neue WebDAV-Weg vollständig End-to-End funktioniert.
|
||||
|
||||
### Neuer WebDAV-Weg
|
||||
|
||||
Der neue Quellenmodus wird zusätzlich zu den bestehenden Modi entwickelt als `webdav-placeholder`.
|
||||
|
||||
Ziel:
|
||||
|
||||
- nur normale Nextcloud-Anmeldung bzw. App-Passwort und WebDAV benötigen;
|
||||
- keine Nextcloud-PostgreSQL-Rechte voraussetzen;
|
||||
- keine Storage-IDs oder Backend-Pfade voraussetzen;
|
||||
- keine zusätzlichen Host-Mounts voraussetzen;
|
||||
- kein FUSE, davfs oder rclone voraussetzen;
|
||||
- keine Originalbilder dauerhaft nach Piwigo kopieren.
|
||||
|
||||
Grundidee:
|
||||
|
||||
1. Nextcloud-Verzeichnisse werden über WebDAV/PROPFIND gelesen.
|
||||
2. Ordner, Dateinamen, Datei-ID, MIME-Typ, Größe, ETag und WebDAV-Pfad werden erfasst.
|
||||
3. Für Bilder wird nur eine winzige lokale Platzhalterquelle erzeugt.
|
||||
4. Der bestehende Shadow Tree bildet daraus die reale Ordner-/Dateinamensstruktur für Piwigo.
|
||||
5. Ein Mapping verbindet den Piwigo-/Shadow-Tree-Pfad mit der WebDAV-Quelle.
|
||||
6. Wenn Piwigo echte Bilddaten benötigt, wird das Original bei Bedarf über WebDAV gelesen.
|
||||
7. Piwigo-Derivate werden normal in `_data/i/` gecacht.
|
||||
8. Das Original bleibt ausschließlich in Nextcloud.
|
||||
|
||||
Vorhandene Bausteine:
|
||||
|
||||
- `runtime/lib/build_webdav_placeholder_source.py` – rekursiver WebDAV-Scan, Platzhalterquelle, Manifest und Mapping;
|
||||
- `include/nc_connector_webdav.inc.php` – paralleler, zunächst deaktivierter Connection-Typ `webdav-placeholder`;
|
||||
- Secret-Format v3 mit verbindungseigenem `nextcloud_user` und `nextcloud_password`.
|
||||
|
||||
Neue WebDAV-Testverbindungen werden als `adapter=remote` und deaktiviert gespeichert. Sie verändern keine bestehenden Verbindungen und werden noch nicht von der produktiven Runtime aktiviert.
|
||||
|
||||
### Gemessene WebDAV-Performance
|
||||
|
||||
Ein realer Test mit einem 16.091.204-Byte-Bild ergab vom Piwigo-System:
|
||||
|
||||
- interne Nextcloud-Adresse: 1,829 s bei 8.796.915 Byte/s;
|
||||
- externe Nextcloud-Adresse: 1,865 s bei 8.627.524 Byte/s.
|
||||
|
||||
Damit war die externe Verbindung in diesem Test nur ungefähr 2 % langsamer. Der WebDAV-Ansatz wird deshalb weiterverfolgt. Wichtig bleibt, dass Piwigo-Derivate lokal gecacht werden und WebDAV nicht bei jedem Thumbnail-Aufruf erneut verwendet wird.
|
||||
|
||||
### Activity Gate
|
||||
|
||||
Der regelmäßige Lauf berücksichtigt:
|
||||
Der bestehende regelmäßige Lauf berücksichtigt:
|
||||
|
||||
- Nextcloud-Aktivität;
|
||||
- Signatur der sichtbaren Freigaben;
|
||||
- Signatur der sichtbaren Quellen;
|
||||
- Quiet-Time;
|
||||
- maximale Wartezeit;
|
||||
- periodischen Full-Sync;
|
||||
- notwendigen Reparaturlauf bei beschädigtem Shadow Tree oder fehlenden Piwigo-Alben.
|
||||
|
||||
Der neue WebDAV-Modus erhält später eine eigene Änderungserkennung, insbesondere über WebDAV-Metadaten wie ETag.
|
||||
|
||||
### Piwigo-Synchronisierung
|
||||
|
||||
Der produktive Lauf ist API-first:
|
||||
@@ -82,7 +124,7 @@ Der direkte produktive Sync führt außerdem die für den normalen Piwigo-Import
|
||||
|
||||
### Runtime
|
||||
|
||||
Aktive Verbindungen werden über einen gemeinsamen systemd-Timer verarbeitet:
|
||||
Aktive bestehende Verbindungen werden über einen gemeinsamen systemd-Timer verarbeitet:
|
||||
|
||||
- `bratonien-nc-connector.timer`
|
||||
- `bratonien-nc-connector.service`
|
||||
@@ -93,6 +135,8 @@ Verbindungsspezifische Konfigurationen liegen unter `/etc/bratonien-tools/nc-con
|
||||
|
||||
Der Shadow-Tree-Austausch besitzt einen Rollback: Scheitert der Wechsel auf den neuen Baum, wird der vorherige Galeriebaum wiederhergestellt.
|
||||
|
||||
Der WebDAV-Parallelweg wird in der nächsten Bauphase mit einem eigenen Reconcile-/Sync-Zweig ergänzt. Bis dahin bleibt er deaktiviert und kann keine bestehende Runtime beeinflussen.
|
||||
|
||||
### Statusanzeige
|
||||
|
||||
Die Administration zeigt letzten und nächsten Lauf. Der Status wird regelmäßig im Hintergrund abgefragt. Dafür wird die Admin-Seite **nicht** neu geladen; offene Dialoge, Assistenten und aufgeklappte Bereiche bleiben erhalten.
|
||||
@@ -106,11 +150,25 @@ Bestehende Verbindungen können:
|
||||
- technisch bearbeitet, solange sie nicht aktiv sind;
|
||||
- deaktiviert und anschließend gelöscht werden.
|
||||
|
||||
Das Löschen einer Connector-Verbindung entfernt keine Nextcloud-Originale und keine Piwigo-Bilder. Erreichbare Connector-eigene Status-/State-Daten der Verbindung werden bereinigt.
|
||||
Seit 0.9.5.5 hängt das Löschen einer Verbindung nicht mehr davon ab, dass der Webserver in Root-eigene Runtime-Verzeichnisse schreiben kann. Die Datenbankverbindung wird entfernt; verwaiste Runtime-Dateien werden vor einem späteren Sync bereinigt.
|
||||
|
||||
### Legacy-Migration
|
||||
Das Löschen einer Connector-Verbindung entfernt keine Nextcloud-Originale und keine Piwigo-Bilder.
|
||||
|
||||
Für ältere Installationen mit dem früheren separaten `piwigo-sync` existieren weiterhin Migrations-/Cutover-Helfer. Neue Installationen sollen den nativen Connector-Weg verwenden.
|
||||
### Migrationsregel für WebDAV
|
||||
|
||||
Der bestehende lokale Weg bleibt so lange vollständig erhalten, bis `webdav-placeholder` End-to-End funktioniert.
|
||||
|
||||
Vor einer freiwilligen Migration bestehender Verbindungen müssen mindestens erfolgreich getestet sein:
|
||||
|
||||
- Verbindungsanlage;
|
||||
- Verzeichnisauswahl;
|
||||
- Shadow Tree;
|
||||
- Piwigo-Registrierung;
|
||||
- echte Bildausgabe;
|
||||
- Derivat-Cache;
|
||||
- Änderungserkennung;
|
||||
- Löschen/Deaktivieren;
|
||||
- Fehlerbehandlung.
|
||||
|
||||
## Bildcache
|
||||
|
||||
@@ -187,21 +245,21 @@ Wichtige Dateien und Verzeichnisse:
|
||||
- `include/nc_connector.inc.php` – Datenmodell und gemeinsame Connector-Funktionen
|
||||
- `include/nc_connector_wizard.inc.php` – Endnutzer-Assistent und Connection-Bearbeitung
|
||||
- `include/nc_connector_manage.inc.php` – Credential-Format, Storage-Mappings, Verifikation und Löschen
|
||||
- `include/nc_connector_create_api.inc.php` – API-first-Verbindungserstellung
|
||||
- `include/nc_connector_connection_scope.inc.php` – verbindungseigene Authentifizierung
|
||||
- `include/nc_connector_create_api.inc.php` – API-first-Verbindungserstellung und Secret v3
|
||||
- `include/nc_connector_webdav.inc.php` – paralleler WebDAV-Connection-Typ
|
||||
- `include/nc_connector_piwigo_api.inc.php` – API-Zugang und Fallback-Verwaltung
|
||||
- `include/nc_connector_system.inc.php` – Timer- und Laufzeitstatus
|
||||
- `include/nc_productive_ws.inc.php` – produktiver Piwigo-Dateisync
|
||||
- `include/nc_orphan_ws.inc.php` – Orphan-Synchronisierung
|
||||
- `runtime/lib/activity_gate.py` – Activity Gate
|
||||
- `runtime/lib/build_manifest.py` – Auflösung der Nextcloud-Freigaben auf Storage-Mounts
|
||||
- `runtime/lib/build_manifest.py` – bestehende Auflösung der Nextcloud-Quellen auf Storage-Mounts
|
||||
- `runtime/lib/build_selected_manifest.py` – ausgewählte Nextcloud-Datei-IDs auf bestehende Storage-Pfade auflösen
|
||||
- `runtime/lib/build_webdav_placeholder_source.py` – experimentelle WebDAV-Platzhalterquelle
|
||||
- `runtime/lib/shadow_tree.py` – atomarer Shadow Tree mit Rollback
|
||||
- `runtime/lib/piwigo-sync.php` – API-first-Piwigo-Sync mit Fallback
|
||||
- `runtime/run-all.sh` – Multi-Connection-Runner
|
||||
- `runtime/sync.sh` – Ablauf einer Verbindung
|
||||
- `nc-connector-install.php` – native Root-Aktivierung
|
||||
- `nc-connector-disable.php` – Deaktivierung
|
||||
- `nc-connector-normalize.php` – Normalisierung älterer aktiver Verbindungen
|
||||
- `nc-connector-*-cleanup/switch/cutover` – Legacy-Migrationshelfer
|
||||
- `runtime/run-all.sh` – Multi-Connection-Runner und Bereinigung verwaister Runtime-Dateien
|
||||
- `runtime/sync.sh` – Ablauf einer bestehenden lokalen Verbindung
|
||||
- `include/self_update.inc.php` – Self-Updater
|
||||
- `include/album_shares.inc.php` – geschützte Albumfreigaben
|
||||
- `include/public_selection.inc.php` – Fotoauswahl
|
||||
@@ -214,15 +272,20 @@ Wichtige Dateien und Verzeichnisse:
|
||||
|
||||
- administrative Schreibaktionen verwenden Piwigos CSRF-Schutz;
|
||||
- Connector-Zugangsdaten werden verschlüsselt gespeichert;
|
||||
- Secret v3 speichert Nextcloud-Zugangsdaten verbindungseigen und verschlüsselt;
|
||||
- Wizard-Geheimnisse werden nicht in Browser-Web-Storage persistiert;
|
||||
- Storage-Mounts werden explizit zugeordnet und validiert;
|
||||
- bestehende lokale Storage-Mounts werden explizit zugeordnet und validiert;
|
||||
- SQL-View-Namen werden vor Verwendung validiert;
|
||||
- gefährliche technische Pfade sind nicht Teil des normalen Wizard-Happy-Paths;
|
||||
- produktive Piwigo-API ist versionsgebunden;
|
||||
- Originalbilder werden vom Connector nicht gelöscht;
|
||||
- der neue WebDAV-Weg soll Originalbilder nicht dauerhaft lokal speichern;
|
||||
- Update-Pakete werden an Commit und Hash gebunden;
|
||||
- Root-Helfer prüfen CLI-/Root-Ausführung, bevor sie Systemdienste oder `/etc`-/`/var/lib`-Daten verändern.
|
||||
- bestehende Verbindungen bleiben während der WebDAV-Entwicklung unangetastet.
|
||||
|
||||
## Entwicklungsstand
|
||||
|
||||
`0.9.4.x` ist der aktuelle Entwicklungsblock. Der lokale NC Connector ist End-to-End produktiv im Einsatz. Der Verbindungsassistent und die Verwaltung werden weiter aus Endnutzersicht konsolidiert. Ein vollständig entfernter Nextcloud-Adapter ohne direkten PostgreSQL-/Storage-Zugriff ist noch nicht umgesetzt.
|
||||
`0.9.5.x` ist der aktuelle Entwicklungsblock. Der bestehende lokale NC Connector bleibt produktiv und wird nicht durch den experimentellen WebDAV-Weg ersetzt.
|
||||
|
||||
Mit 0.9.5.6 ist die parallele Verbindungsschicht angelegt: Secret v3 kann Nextcloud-Zugangsdaten speichern und `webdav-placeholder` existiert als eigener deaktivierter Connection-Typ. Der nächste Schritt ist ein strikt getrennter Reconcile-/Runtime-Zweig für diesen Modus und danach der erste Platzhalter-PoC durch den bestehenden Shadow Tree und Piwigo-Sync.
|
||||
|
||||
Den detaillierten aktuellen Plan enthält `CURRENT_STATUS.md`.
|
||||
|
||||
@@ -43,6 +43,8 @@ function bratonien_tools_nc_wizard_finish_connection_scoped()
|
||||
$_POST['nc_nextcloud_url']=(string)$state['base_url'];
|
||||
$_POST['nc_showcase_user']=(string)$state['showcase_user'];
|
||||
$_POST['nc_access_user']=(string)$state['username'];
|
||||
$_POST['nc_nextcloud_user']=(string)$state['username'];
|
||||
$_POST['nc_nextcloud_password']=(string)$state['_password'];
|
||||
$_POST['nc_product']=(string)$state['product'];
|
||||
$_POST['nc_version']=(string)$state['version'];
|
||||
$_POST['nc_api_validated']=$state['api_status']==='ok'?'1':'0';
|
||||
@@ -121,12 +123,14 @@ function bratonien_tools_nc_connector_verify_connection_scoped()
|
||||
}
|
||||
|
||||
$payload = json_encode(array(
|
||||
'v'=>2,
|
||||
'v'=>3,
|
||||
'db_password'=>$db_password,
|
||||
'piwigo_user'=>$fallback_user,
|
||||
'piwigo_password'=>$fallback_password,
|
||||
'api_key_id'=>$api_key_id,
|
||||
'api_key_secret'=>$api_key_secret,
|
||||
'nextcloud_user'=>'',
|
||||
'nextcloud_password'=>'',
|
||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($payload)) throw new RuntimeException('Verbindungsauthentifizierung konnte nicht migriert werden.');
|
||||
|
||||
@@ -148,33 +152,44 @@ function bratonien_tools_nc_connector_scoped_secret(array $connection)
|
||||
if (is_array($decoded) && array_key_exists('db_password', $decoded))
|
||||
{
|
||||
return array(
|
||||
'v'=>2,
|
||||
'v'=>3,
|
||||
'db_password'=>(string)($decoded['db_password'] ?? ''),
|
||||
'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
|
||||
'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
|
||||
'api_key_id'=>(string)($decoded['api_key_id'] ?? ''),
|
||||
'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''),
|
||||
'nextcloud_user'=>(string)($decoded['nextcloud_user'] ?? ''),
|
||||
'nextcloud_password'=>(string)($decoded['nextcloud_password'] ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'v'=>2,
|
||||
'v'=>3,
|
||||
'db_password'=>(string)$plain,
|
||||
'piwigo_user'=>'',
|
||||
'piwigo_password'=>'',
|
||||
'api_key_id'=>'',
|
||||
'api_key_secret'=>'',
|
||||
'nextcloud_user'=>'',
|
||||
'nextcloud_password'=>'',
|
||||
);
|
||||
}
|
||||
|
||||
function bratonien_tools_nc_connector_store_scoped_secret($id, array $connection, array $credentials)
|
||||
{
|
||||
if (trim((string)($credentials['db_password'] ?? '')) === '') throw new RuntimeException('Das Datenbankpasswort der Verbindung fehlt.');
|
||||
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
|
||||
$is_webdav = (string)($config['source_mode'] ?? '') === 'webdav-placeholder';
|
||||
if (!$is_webdav && trim((string)($credentials['db_password'] ?? '')) === '') throw new RuntimeException('Das Datenbankpasswort der Verbindung fehlt.');
|
||||
if ($is_webdav && (trim((string)($credentials['nextcloud_user'] ?? '')) === '' || (string)($credentials['nextcloud_password'] ?? '') === ''))
|
||||
{
|
||||
throw new RuntimeException('Die Nextcloud-Zugangsdaten der WebDAV-Verbindung fehlen.');
|
||||
}
|
||||
|
||||
$credentials['v'] = 3;
|
||||
$payload = json_encode($credentials, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($payload)) throw new RuntimeException('Verbindungszugangsdaten konnten nicht serialisiert werden.');
|
||||
$blob = bratonien_tools_nc_connector_encrypt_secret($payload);
|
||||
|
||||
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
|
||||
if (array_key_exists('api_enabled', $config))
|
||||
{
|
||||
$config['api_enabled'] = trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '';
|
||||
|
||||
@@ -28,6 +28,13 @@ function bratonien_tools_nc_connector_create_local_api_first()
|
||||
throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort muessen entweder beide angegeben oder beide leer gelassen werden.');
|
||||
}
|
||||
|
||||
$nextcloud_user = trim((string)($_POST['nc_nextcloud_user'] ?? $_POST['nc_access_user'] ?? ''));
|
||||
$nextcloud_password = (string)($_POST['nc_nextcloud_password'] ?? '');
|
||||
if (($nextcloud_user === '') !== ($nextcloud_password === ''))
|
||||
{
|
||||
throw new RuntimeException('Nextcloud-Benutzer und Nextcloud-Passwort muessen entweder beide vorhanden oder beide leer sein.');
|
||||
}
|
||||
|
||||
$wizard_auth_present = array_key_exists('nc_api_validated', $_POST);
|
||||
$validated_pending_api = (string)($_POST['nc_api_validated'] ?? '') === '1';
|
||||
$api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? ''));
|
||||
@@ -84,12 +91,14 @@ function bratonien_tools_nc_connector_create_local_api_first()
|
||||
bratonien_tools_nc_connector_view_name($config['activity_view']);
|
||||
|
||||
$secret_payload = json_encode(array(
|
||||
'v'=>2,
|
||||
'v'=>3,
|
||||
'db_password'=>(string)$_POST['nc_db_password'],
|
||||
'piwigo_user'=>$fallback_user,
|
||||
'piwigo_password'=>$fallback_password,
|
||||
'api_key_id'=>$api_key_id,
|
||||
'api_key_secret'=>$api_key_secret,
|
||||
'nextcloud_user'=>$nextcloud_user,
|
||||
'nextcloud_password'=>$nextcloud_password,
|
||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($secret_payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.');
|
||||
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
|
||||
|
||||
47
include/nc_connector_delete_safe.inc.php
Normal file
47
include/nc_connector_delete_safe.inc.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
if (!defined('PHPWG_ROOT_PATH'))
|
||||
{
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a connector connection from Piwigo without depending on the web
|
||||
* server being allowed to write into root-owned runtime directories.
|
||||
*
|
||||
* Runtime files are removed by runtime/cleanup-stale.php on the next runner
|
||||
* cycle before any connector sync is started.
|
||||
*/
|
||||
function bratonien_tools_nc_connector_delete_safe()
|
||||
{
|
||||
$id = (int)($_POST['connection_id'] ?? 0);
|
||||
$connection = bratonien_tools_nc_connector_connection($id, false);
|
||||
if (!$connection)
|
||||
{
|
||||
throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.');
|
||||
}
|
||||
|
||||
$table = bratonien_tools_nc_connector_table();
|
||||
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
|
||||
|
||||
// Public status data is only cache/runtime information. Remove it when PHP
|
||||
// has permission, but never make successful DB deletion depend on ownership
|
||||
// of files previously created by the root-run connector service.
|
||||
$status_dir = rtrim(PHPWG_ROOT_PATH, '/').'/_data/bratonien-tools/nc-connector-status';
|
||||
$public_status = $status_dir.'/connection-'.$id.'.json';
|
||||
if (is_file($public_status))
|
||||
{
|
||||
@unlink($public_status);
|
||||
}
|
||||
|
||||
// Keep the old tombstone mechanism as a best-effort fast path for systems
|
||||
// where the web server can write here. cleanup-stale.php is the authoritative
|
||||
// fallback and does not require this file.
|
||||
if (is_dir($status_dir) || @mkdir($status_dir, 0755, true))
|
||||
{
|
||||
@file_put_contents($status_dir.'/deleted-'.$id, date('c')."\n", LOCK_EX);
|
||||
}
|
||||
|
||||
return array(
|
||||
'message'=>'Connector-Verbindung wurde gelöscht. Verbliebene Laufzeitdateien werden vor dem nächsten Connector-Lauf automatisch entfernt. Nextcloud- und Piwigo-Bilder blieben unverändert.',
|
||||
);
|
||||
}
|
||||
@@ -54,6 +54,8 @@ function bratonien_tools_nc_wizard_finish_generic_scope()
|
||||
$_POST['nc_piwigo_password']=$fallback_password;
|
||||
$_POST['nc_nextcloud_url']=(string)$state['base_url'];
|
||||
$_POST['nc_access_user']=(string)$state['username'];
|
||||
$_POST['nc_nextcloud_user']=(string)$state['username'];
|
||||
$_POST['nc_nextcloud_password']=(string)$state['_password'];
|
||||
$_POST['nc_product']=(string)$state['product'];
|
||||
$_POST['nc_version']=(string)$state['version'];
|
||||
$_POST['nc_api_validated']=($state['api_status'] ?? '')==='ok'?'1':'0';
|
||||
|
||||
133
include/nc_connector_webdav.inc.php
Normal file
133
include/nc_connector_webdav.inc.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
if (!defined('PHPWG_ROOT_PATH'))
|
||||
{
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a disabled WebDAV-backed connector beside the existing local modes.
|
||||
*
|
||||
* This deliberately does not activate, migrate or modify any existing
|
||||
* connection. It only persists the data required for the parallel WebDAV path.
|
||||
*/
|
||||
function bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard()
|
||||
{
|
||||
$state = bratonien_tools_nc_wizard_state();
|
||||
|
||||
if (empty($state['scan_ok']) || empty($state['technical_complete']))
|
||||
{
|
||||
throw new RuntimeException('Die WebDAV-Verbindung wurde im Assistenten noch nicht vollständig vorbereitet.');
|
||||
}
|
||||
|
||||
$base_url = rtrim(trim((string)($state['base_url'] ?? '')), '/');
|
||||
$username = trim((string)($state['username'] ?? ''));
|
||||
$password = (string)($state['_password'] ?? '');
|
||||
if ($base_url === '' || $username === '' || $password === '')
|
||||
{
|
||||
throw new RuntimeException('Für den WebDAV-Testpfad fehlen Nextcloud-Adresse oder Zugangsdaten.');
|
||||
}
|
||||
|
||||
$selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
|
||||
? array_values(array_unique(array_map(function($path) { return trim((string)$path, '/'); }, $state['directory_selected'])))
|
||||
: array();
|
||||
$selected_ids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids'])
|
||||
? $state['directory_selected_fileids']
|
||||
: array();
|
||||
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
|
||||
if (!$selected) throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis für den WebDAV-Testpfad auswählen.');
|
||||
|
||||
$roots = array();
|
||||
foreach ($selected as $path)
|
||||
{
|
||||
$fileid = isset($selected_ids[$path]) ? (int)$selected_ids[$path] : 0;
|
||||
if ($fileid < 1) throw new RuntimeException('Für ein ausgewähltes Nextcloud-Verzeichnis fehlt die eindeutige Datei-ID.');
|
||||
$roots[] = array(
|
||||
'fileid'=>$fileid,
|
||||
'display_name'=>basename($path),
|
||||
'webdav_path'=>$path,
|
||||
);
|
||||
}
|
||||
|
||||
$name = trim((string)($state['connection_name'] ?? ''));
|
||||
if ($name === '') $name = 'Nextcloud WebDAV';
|
||||
$gallery_root = rtrim(trim((string)($state['gallery_root'] ?? '')), '/');
|
||||
if ($gallery_root === '') $gallery_root = rtrim(PHPWG_ROOT_PATH, '/').'/galleries';
|
||||
|
||||
$api_key_id = ($state['api_status'] ?? '') === 'ok' ? trim((string)($state['_api_key_id'] ?? '')) : '';
|
||||
$api_key_secret = ($state['api_status'] ?? '') === 'ok' ? trim((string)($state['_api_key_secret'] ?? '')) : '';
|
||||
$fallback_user = trim((string)($state['_fallback_user'] ?? ''));
|
||||
$fallback_password = (string)($state['_fallback_password'] ?? '');
|
||||
$api_enabled = $api_key_id !== '' && $api_key_secret !== '';
|
||||
|
||||
$config = array(
|
||||
'origin'=>'native',
|
||||
'source_mode'=>'webdav-placeholder',
|
||||
'transport'=>'webdav',
|
||||
'nextcloud_url'=>$base_url,
|
||||
'access_user'=>$username,
|
||||
'nextcloud_access_user'=>$username,
|
||||
'gallery_root'=>$gallery_root,
|
||||
'roots'=>$roots,
|
||||
'state_dir'=>'',
|
||||
'status_file'=>'',
|
||||
'quiet_seconds'=>120,
|
||||
'max_wait_seconds'=>900,
|
||||
'full_sync_seconds'=>86400,
|
||||
'parallel_test'=>true,
|
||||
'piwigo_auth'=>'connection-scoped',
|
||||
'api_enabled'=>$api_enabled,
|
||||
);
|
||||
|
||||
foreach (array('product'=>'nextcloud_product', 'version'=>'nextcloud_version') as $state_key=>$config_key)
|
||||
{
|
||||
$value = trim((string)($state[$state_key] ?? ''));
|
||||
if ($value !== '') $config[$config_key] = $value;
|
||||
}
|
||||
|
||||
$secret_payload = json_encode(array(
|
||||
'v'=>3,
|
||||
'db_password'=>'',
|
||||
'piwigo_user'=>$fallback_user,
|
||||
'piwigo_password'=>$fallback_password,
|
||||
'api_key_id'=>$api_key_id,
|
||||
'api_key_secret'=>$api_key_secret,
|
||||
'nextcloud_user'=>$username,
|
||||
'nextcloud_password'=>$password,
|
||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($secret_payload)) throw new RuntimeException('WebDAV-Zugangsdaten konnten nicht serialisiert werden.');
|
||||
|
||||
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($config_json)) throw new RuntimeException('WebDAV-Konfiguration konnte nicht serialisiert werden.');
|
||||
|
||||
bratonien_tools_nc_connector_ensure_table();
|
||||
$table = bratonien_tools_nc_connector_table();
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$connection_key = 'webdav-'.bin2hex(random_bytes(12));
|
||||
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
|
||||
|
||||
pwg_query("INSERT INTO `$table` (connection_key,name,adapter,enabled,takeover_state,config_json,secret_blob,created,updated) VALUES ('"
|
||||
.pwg_db_real_escape_string($connection_key)."','"
|
||||
.pwg_db_real_escape_string($name)."','remote',0,'disabled','"
|
||||
.pwg_db_real_escape_string($config_json)."','"
|
||||
.pwg_db_real_escape_string($secret_blob)."','"
|
||||
.pwg_db_real_escape_string($now)."','"
|
||||
.pwg_db_real_escape_string($now)."')");
|
||||
|
||||
$id = (int)pwg_db_insert_id();
|
||||
if ($id < 1) throw new RuntimeException('Die WebDAV-Testverbindung konnte nicht eindeutig angelegt werden.');
|
||||
|
||||
$config['state_dir'] = '/var/lib/bratonien-tools/nc-connector/connection-'.$id;
|
||||
$config['status_file'] = $config['state_dir'].'/connector-status.json';
|
||||
$config_json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($config_json))
|
||||
{
|
||||
pwg_query("DELETE FROM `$table` WHERE id=".$id." LIMIT 1");
|
||||
throw new RuntimeException('Die WebDAV-Testverbindung konnte nach dem Anlegen nicht serialisiert werden.');
|
||||
}
|
||||
pwg_query("UPDATE `$table` SET config_json='".pwg_db_real_escape_string($config_json)."' WHERE id=".$id." LIMIT 1");
|
||||
|
||||
return array(
|
||||
'connection_id'=>$id,
|
||||
'message'=>'Parallele WebDAV-Testverbindung wurde deaktiviert angelegt. Bestehende Verbindungen blieben unverändert.',
|
||||
);
|
||||
}
|
||||
195
include/nc_connector_wizard_webdav_flow.inc.php
Normal file
195
include/nc_connector_wizard_webdav_flow.inc.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
if (!defined('PHPWG_ROOT_PATH'))
|
||||
{
|
||||
die('Hacking attempt!');
|
||||
}
|
||||
|
||||
function bratonien_tools_nc_wizard_scan_webdav_first()
|
||||
{
|
||||
$state = bratonien_tools_nc_wizard_state();
|
||||
$host_input = trim((string)($_POST['nc_wizard_host'] ?? $state['host_input']));
|
||||
$username = trim((string)($_POST['nc_wizard_user'] ?? $state['username']));
|
||||
$password = array_key_exists('nc_wizard_password', $_POST) ? (string)$_POST['nc_wizard_password'] : (string)$state['_password'];
|
||||
|
||||
$state['step'] = 1;
|
||||
$state['host_input'] = $host_input;
|
||||
$state['username'] = $username;
|
||||
$state['_password'] = $password;
|
||||
bratonien_tools_nc_wizard_store($state);
|
||||
|
||||
if ($username === '' || $password === '')
|
||||
{
|
||||
throw new RuntimeException('Nextcloud-Benutzer und Passwort werden für den Scan benötigt.');
|
||||
}
|
||||
|
||||
$base_url = '';
|
||||
$status_data = null;
|
||||
foreach (bratonien_tools_nc_wizard_candidate_urls($host_input) as $candidate_url)
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php');
|
||||
if ($response['status'] < 200 || $response['status'] >= 300) continue;
|
||||
$candidate_status = json_decode($response['body'], true);
|
||||
if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue;
|
||||
$base_url = $candidate_url;
|
||||
$status_data = $candidate_status;
|
||||
break;
|
||||
}
|
||||
catch (Throwable $ignored) {}
|
||||
}
|
||||
|
||||
if ($base_url === '' || !is_array($status_data))
|
||||
{
|
||||
throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.');
|
||||
}
|
||||
|
||||
$user_response = bratonien_tools_nc_wizard_http(
|
||||
$base_url.'/ocs/v2.php/cloud/user?format=json',
|
||||
$username,
|
||||
$password,
|
||||
array('OCS-APIRequest: true')
|
||||
);
|
||||
if ($user_response['status'] === 401 || $user_response['status'] === 403)
|
||||
{
|
||||
throw new RuntimeException('Nextcloud hat Benutzername oder Passwort abgelehnt.');
|
||||
}
|
||||
if ($user_response['status'] < 200 || $user_response['status'] >= 300)
|
||||
{
|
||||
throw new RuntimeException('Nextcloud ist erreichbar, aber die Anmeldung konnte nicht geprüft werden.');
|
||||
}
|
||||
|
||||
$user_data = bratonien_tools_nc_wizard_ocs_data($user_response['body']);
|
||||
$resolved_username = trim((string)($user_data['id'] ?? $username));
|
||||
if ($resolved_username === '') $resolved_username = $username;
|
||||
$url_host = (string)parse_url($base_url, PHP_URL_HOST);
|
||||
|
||||
$state = array_merge($state, array(
|
||||
'step'=>2,
|
||||
'scan_ok'=>true,
|
||||
'base_url'=>$base_url,
|
||||
'host_input'=>$host_input,
|
||||
'username'=>$resolved_username,
|
||||
'display_name'=>(string)($user_data['display-name'] ?? $user_data['displayname'] ?? ''),
|
||||
'version'=>(string)($status_data['versionstring'] ?? $status_data['version'] ?? ''),
|
||||
'product'=>(string)($status_data['productname'] ?? 'Nextcloud'),
|
||||
'users'=>array(),
|
||||
'can_list_users'=>false,
|
||||
'showcase_user'=>$resolved_username,
|
||||
'connection_name'=>$url_host !== '' ? $url_host : 'Nextcloud WebDAV',
|
||||
'scan_message'=>'Nextcloud und WebDAV-Zugriff wurden bestätigt.',
|
||||
'_password'=>$password,
|
||||
'source_mode'=>'webdav-placeholder',
|
||||
'transport'=>'webdav',
|
||||
'gallery_root'=>rtrim(PHPWG_ROOT_PATH, '/').'/galleries',
|
||||
'storages'=>array(),
|
||||
'storage_candidates'=>array(),
|
||||
'roots'=>array(),
|
||||
'technical_stage'=>'mounts',
|
||||
'technical_source'=>'WebDAV',
|
||||
'technical_error'=>'',
|
||||
'technical_complete'=>false,
|
||||
'directory_selection_ready'=>true,
|
||||
'directory_path'=>'',
|
||||
'directory_parent'=>'',
|
||||
'directory_children'=>array(),
|
||||
'directory_current_fileid'=>0,
|
||||
'directory_fileids'=>array(),
|
||||
'directory_selected'=>array(),
|
||||
'directory_selected_fileids'=>array(),
|
||||
'database_prompted'=>false,
|
||||
'mount_prompted'=>false,
|
||||
'api_status'=>'pending',
|
||||
'api_username'=>'',
|
||||
'api_error'=>'',
|
||||
));
|
||||
|
||||
bratonien_tools_nc_wizard_refresh_directory_state($state, '');
|
||||
bratonien_tools_nc_wizard_store($state);
|
||||
|
||||
return array('message'=>'Nextcloud und WebDAV wurden bestätigt. Jetzt können die Verzeichnisse des angemeldeten Benutzers ausgewählt werden.');
|
||||
}
|
||||
|
||||
function bratonien_tools_nc_wizard_save_sources_dispatch()
|
||||
{
|
||||
$state = bratonien_tools_nc_wizard_state();
|
||||
if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder')
|
||||
{
|
||||
return bratonien_tools_nc_wizard_save_mounts_server_side();
|
||||
}
|
||||
|
||||
if ((int)($state['step'] ?? 0) !== 2 || empty($state['directory_selection_ready']))
|
||||
{
|
||||
throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.');
|
||||
}
|
||||
|
||||
$selected = isset($state['directory_selected']) && is_array($state['directory_selected'])
|
||||
? array_values(array_unique(array_map(function($path) { return trim((string)$path, '/'); }, $state['directory_selected'])))
|
||||
: array();
|
||||
$selected = array_values(array_filter($selected, function($path) { return $path !== ''; }));
|
||||
if (!$selected)
|
||||
{
|
||||
throw new RuntimeException('Bitte mindestens ein Nextcloud-Verzeichnis auswählen.');
|
||||
}
|
||||
|
||||
$selected_ids = isset($state['directory_selected_fileids']) && is_array($state['directory_selected_fileids'])
|
||||
? $state['directory_selected_fileids']
|
||||
: array();
|
||||
$roots = array();
|
||||
foreach ($selected as $path)
|
||||
{
|
||||
$fileid = (int)($selected_ids[$path] ?? 0);
|
||||
if ($fileid < 1)
|
||||
{
|
||||
throw new RuntimeException('Für eine Auswahl fehlt die eindeutige Nextcloud-Datei-ID. Bitte das Verzeichnis erneut auswählen.');
|
||||
}
|
||||
$roots[] = array(
|
||||
'fileid'=>$fileid,
|
||||
'display_name'=>basename($path),
|
||||
'webdav_path'=>$path,
|
||||
);
|
||||
}
|
||||
|
||||
$state['roots'] = $roots;
|
||||
$state['technical_complete'] = true;
|
||||
$state['technical_stage'] = 'ready';
|
||||
$state['technical_source'] = 'WebDAV-Verzeichnisse ausgewählt';
|
||||
$state['technical_error'] = '';
|
||||
$state['directory_selection_ready'] = false;
|
||||
bratonien_tools_nc_wizard_store($state);
|
||||
|
||||
return array('message'=>'WebDAV-Verzeichnisse wurden übernommen. Die Verbindung ist für den parallelen Testweg vorbereitet.');
|
||||
}
|
||||
|
||||
function bratonien_tools_nc_wizard_finish_dispatch()
|
||||
{
|
||||
$state = bratonien_tools_nc_wizard_state();
|
||||
if ((string)($state['source_mode'] ?? '') !== 'webdav-placeholder')
|
||||
{
|
||||
return bratonien_tools_nc_wizard_finish_generic_scope();
|
||||
}
|
||||
|
||||
if ((int)($state['step'] ?? 0) !== 4 || empty($state['technical_complete']))
|
||||
{
|
||||
throw new RuntimeException('Der Assistent ist noch nicht vollständig.');
|
||||
}
|
||||
|
||||
if (array_key_exists('nc_wizard_fallback_user', $_POST)) $state['_fallback_user'] = trim((string)$_POST['nc_wizard_fallback_user']);
|
||||
if (array_key_exists('nc_wizard_fallback_password', $_POST)) $state['_fallback_password'] = (string)$_POST['nc_wizard_fallback_password'];
|
||||
bratonien_tools_nc_wizard_store($state);
|
||||
|
||||
$fallback_user = trim((string)($state['_fallback_user'] ?? ''));
|
||||
$fallback_password = (string)($state['_fallback_password'] ?? '');
|
||||
if (($fallback_user === '') !== ($fallback_password === ''))
|
||||
{
|
||||
throw new RuntimeException('Fallback-Benutzer und Fallback-Passwort müssen entweder beide angegeben oder beide leer gelassen werden.');
|
||||
}
|
||||
if (($state['api_status'] ?? '') !== 'ok' && $fallback_user === '')
|
||||
{
|
||||
throw new RuntimeException('Da die Piwigo-API übersprungen wurde, ist für diese Verbindung ein Fallback-Zugang erforderlich.');
|
||||
}
|
||||
|
||||
$result = bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard();
|
||||
unset($_SESSION['bratonien_nc_wizard']);
|
||||
return $result;
|
||||
}
|
||||
@@ -27,6 +27,9 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_user_scope.inc.
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_flow.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_connection_scope.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_generic_scope.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php');
|
||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php');
|
||||
|
||||
function bratonien_tools_get_tools()
|
||||
{
|
||||
@@ -52,19 +55,20 @@ function bratonien_tools_get_tools()
|
||||
'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'),
|
||||
'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'),
|
||||
'nc_connector_create_local' => array('handler' => 'bratonien_tools_nc_connector_create_local_api_first'),
|
||||
'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_any'),
|
||||
'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_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
|
||||
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'),
|
||||
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_user_scoped'),
|
||||
'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'),
|
||||
'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'),
|
||||
'nc_connector_wizard_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_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
|
||||
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_mounts_server_side'),
|
||||
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_sources_dispatch'),
|
||||
'nc_connector_wizard_select_user' => array('handler' => 'bratonien_tools_nc_wizard_select_current_user'),
|
||||
'nc_connector_wizard_api_test' => array('handler' => 'bratonien_tools_nc_wizard_api_test'),
|
||||
'nc_connector_wizard_api_skip' => array('handler' => 'bratonien_tools_nc_wizard_api_skip'),
|
||||
'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_generic_scope'),
|
||||
'nc_connector_wizard_finish' => array('handler' => 'bratonien_tools_nc_wizard_finish_dispatch'),
|
||||
'nc_connector_wizard_back' => array('handler' => 'bratonien_tools_nc_wizard_back'),
|
||||
'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'),
|
||||
'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
/*
|
||||
Plugin Name: Bratonien Tools
|
||||
Version: 0.9.5.3
|
||||
Version: 0.9.5.12
|
||||
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
|
||||
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
|
||||
Author: Bratonien
|
||||
|
||||
86
runtime/cleanup-stale.php
Normal file
86
runtime/cleanup-stale.php
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
if (PHP_SAPI !== 'cli')
|
||||
{
|
||||
fwrite(STDERR, "CLI only\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$pluginRoot = dirname(__DIR__);
|
||||
$piwigoRoot = dirname($pluginRoot, 2);
|
||||
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
|
||||
$configDir = '/etc/bratonien-tools/nc-connector';
|
||||
$stateRoot = '/var/lib/bratonien-tools/nc-connector';
|
||||
|
||||
function cleanup_fail($message)
|
||||
{
|
||||
fwrite(STDERR, $message."\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function cleanup_tree($path)
|
||||
{
|
||||
$path = rtrim((string)$path, '/');
|
||||
if ($path === '' || $path === '/' || !file_exists($path)) return;
|
||||
if (is_link($path) || is_file($path))
|
||||
{
|
||||
@unlink($path);
|
||||
return;
|
||||
}
|
||||
$items = @scandir($path);
|
||||
if (!is_array($items)) return;
|
||||
foreach ($items as $item)
|
||||
{
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
cleanup_tree($path.'/'.$item);
|
||||
}
|
||||
@rmdir($path);
|
||||
}
|
||||
|
||||
if (!is_readable($dbConfig)) cleanup_fail('Piwigo-Datenbankkonfiguration ist nicht lesbar.');
|
||||
$conf = array();
|
||||
$prefixeTable = 'piwigo_';
|
||||
require $dbConfig;
|
||||
foreach (array('db_host','db_user','db_password','db_base') as $key)
|
||||
{
|
||||
if (!isset($conf[$key])) cleanup_fail('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
|
||||
}
|
||||
|
||||
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
|
||||
if ($db->connect_errno) cleanup_fail('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
|
||||
$db->set_charset('utf8mb4');
|
||||
|
||||
$table = $prefixeTable.'bratonien_tools_nc_connections';
|
||||
$escapedTable = $db->real_escape_string($table);
|
||||
$exists = $db->query("SHOW TABLES LIKE '{$escapedTable}'");
|
||||
if (!$exists) cleanup_fail('Connector-Tabelle konnte nicht geprüft werden: '.$db->error);
|
||||
if ($exists->num_rows === 0) exit(0);
|
||||
|
||||
$activeIds = array();
|
||||
$result = $db->query("SELECT id FROM `{$table}`");
|
||||
if (!$result) cleanup_fail('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
|
||||
while ($row = $result->fetch_assoc())
|
||||
{
|
||||
$activeIds[(int)$row['id']] = true;
|
||||
}
|
||||
|
||||
if (!is_dir($configDir)) exit(0);
|
||||
foreach (glob($configDir.'/connection-*.conf') ?: array() as $configPath)
|
||||
{
|
||||
if (!preg_match('/connection-([0-9]+)\.conf$/', $configPath, $matches)) continue;
|
||||
$id = (int)$matches[1];
|
||||
if (isset($activeIds[$id])) continue;
|
||||
|
||||
echo "NC Connector: entferne verwaiste Laufzeitdateien fuer Verbindung {$id}.\n";
|
||||
foreach (array('.conf','.db-password','.piwigo-password','.storages.tsv','.roots.tsv') as $suffix)
|
||||
{
|
||||
@unlink($configDir.'/connection-'.$id.$suffix);
|
||||
}
|
||||
cleanup_tree($stateRoot.'/connection-'.$id);
|
||||
|
||||
$statusDir = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-status';
|
||||
@unlink($statusDir.'/connection-'.$id.'.json');
|
||||
@unlink($statusDir.'/deleted-'.$id);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
319
runtime/lib/build_webdav_placeholder_source.py
Normal file
319
runtime/lib/build_webdav_placeholder_source.py
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a placeholder-backed local source tree from Nextcloud WebDAV.
|
||||
|
||||
This is intentionally additive: it does not replace the existing local-storage
|
||||
connector path. It creates only tiny placeholder files plus a metadata mapping;
|
||||
no Nextcloud original media is downloaded.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import getpass
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import ssl
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
DAV = "DAV:"
|
||||
OC = "http://owncloud.org/ns"
|
||||
SUPPORTED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||||
# 1x1 transparent GIF, 34 bytes. The filename keeps the remote extension;
|
||||
# the placeholder exists only so Piwigo can discover the logical image entry.
|
||||
PLACEHOLDER = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==")
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
def validate_relative(value: str) -> str:
|
||||
value = str(value).strip("/")
|
||||
parts = PurePosixPath(value).parts
|
||||
if ".." in parts:
|
||||
raise ValueError(f"unsafe WebDAV path: {value!r}")
|
||||
return value
|
||||
|
||||
|
||||
def quote_path(value: str) -> str:
|
||||
value = validate_relative(value)
|
||||
return "/".join(urllib.parse.quote(part, safe="") for part in PurePosixPath(value).parts)
|
||||
|
||||
|
||||
def safe_local_name(name: str) -> str:
|
||||
if not name or name in {".", ".."} or "/" in name or "\x00" in name:
|
||||
raise ValueError(f"unsafe remote name: {name!r}")
|
||||
return name
|
||||
|
||||
|
||||
class WebDavClient:
|
||||
def __init__(self, base_url: str, user: str, password: str, timeout: int = 30) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.timeout = timeout
|
||||
token = base64.b64encode(f"{user}:{password}".encode("utf-8")).decode("ascii")
|
||||
self.auth_header = f"Basic {token}"
|
||||
self.context = ssl.create_default_context()
|
||||
|
||||
def collection_url(self, relative: str) -> str:
|
||||
user = urllib.parse.quote(self.user, safe="")
|
||||
suffix = quote_path(relative)
|
||||
url = f"{self.base_url}/remote.php/dav/files/{user}/"
|
||||
if suffix:
|
||||
url += suffix + "/"
|
||||
return url
|
||||
|
||||
def list_collection(self, relative: str) -> tuple[dict[str, object], list[dict[str, object]]]:
|
||||
relative = validate_relative(relative)
|
||||
url = self.collection_url(relative)
|
||||
body = (
|
||||
'<?xml version="1.0"?>'
|
||||
'<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">'
|
||||
'<d:prop><d:displayname/><d:resourcetype/><d:getcontenttype/>'
|
||||
'<d:getcontentlength/><d:getetag/><oc:fileid/></d:prop></d:propfind>'
|
||||
).encode("utf-8")
|
||||
request = urllib.request.Request(url, data=body, method="PROPFIND")
|
||||
request.add_header("Authorization", self.auth_header)
|
||||
request.add_header("Depth", "1")
|
||||
request.add_header("Content-Type", "application/xml; charset=utf-8")
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response:
|
||||
status = response.status
|
||||
payload = response.read()
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code in {401, 403}:
|
||||
fail("Nextcloud rejected the WebDAV credentials or directory access")
|
||||
fail(f"Nextcloud PROPFIND failed with HTTP {error.code}")
|
||||
except urllib.error.URLError as error:
|
||||
fail(f"Nextcloud WebDAV is unreachable: {error.reason}")
|
||||
if status != 207:
|
||||
fail(f"Nextcloud PROPFIND returned HTTP {status}")
|
||||
|
||||
try:
|
||||
root = ET.fromstring(payload)
|
||||
except ET.ParseError as error:
|
||||
raise RuntimeError("Nextcloud returned invalid WebDAV XML") from error
|
||||
|
||||
base_path = urllib.parse.unquote(urllib.parse.urlparse(url).path).rstrip("/")
|
||||
current: dict[str, object] | None = None
|
||||
children: list[dict[str, object]] = []
|
||||
for response in root.findall(f"{{{DAV}}}response"):
|
||||
href = response.findtext(f"{{{DAV}}}href", default="")
|
||||
href_path = urllib.parse.unquote(urllib.parse.urlparse(href).path).rstrip("/")
|
||||
prop = None
|
||||
for propstat in response.findall(f"{{{DAV}}}propstat"):
|
||||
status_text = propstat.findtext(f"{{{DAV}}}status", default="")
|
||||
if " 200 " in status_text:
|
||||
prop = propstat.find(f"{{{DAV}}}prop")
|
||||
if prop is not None:
|
||||
break
|
||||
if prop is None:
|
||||
continue
|
||||
display = prop.findtext(f"{{{DAV}}}displayname", default="")
|
||||
fileid_text = prop.findtext(f"{{{OC}}}fileid", default="").strip()
|
||||
resource_type = prop.find(f"{{{DAV}}}resourcetype")
|
||||
is_dir = resource_type is not None and resource_type.find(f"{{{DAV}}}collection") is not None
|
||||
item = {
|
||||
"display_name": display,
|
||||
"fileid": int(fileid_text) if fileid_text.isdigit() else 0,
|
||||
"is_dir": is_dir,
|
||||
"content_type": prop.findtext(f"{{{DAV}}}getcontenttype", default=""),
|
||||
"size": int(prop.findtext(f"{{{DAV}}}getcontentlength", default="0") or 0),
|
||||
"etag": prop.findtext(f"{{{DAV}}}getetag", default="").strip('"'),
|
||||
}
|
||||
if href_path == base_path:
|
||||
current = item
|
||||
continue
|
||||
if display:
|
||||
children.append(item)
|
||||
if current is None or int(current.get("fileid", 0)) < 1:
|
||||
fail(f"Nextcloud returned no stable fileid for {relative or '/'}")
|
||||
return current, children
|
||||
|
||||
|
||||
def link_placeholder(seed: Path, target: Path) -> None:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
os.link(seed, target)
|
||||
except OSError:
|
||||
target.write_bytes(PLACEHOLDER)
|
||||
|
||||
|
||||
def build_root(
|
||||
client: WebDavClient,
|
||||
remote_root: str,
|
||||
local_root: Path,
|
||||
seed: Path,
|
||||
mapping: dict[str, dict[str, object]],
|
||||
) -> tuple[int, int, int]:
|
||||
files = 0
|
||||
folders = 0
|
||||
skipped = 0
|
||||
stack: list[tuple[str, Path]] = [(validate_relative(remote_root), local_root)]
|
||||
visited: set[str] = set()
|
||||
|
||||
while stack:
|
||||
remote_dir, local_dir = stack.pop()
|
||||
if remote_dir in visited:
|
||||
continue
|
||||
visited.add(remote_dir)
|
||||
current, children = client.list_collection(remote_dir)
|
||||
local_dir.mkdir(parents=True, exist_ok=True)
|
||||
folders += 1
|
||||
mapping[str(local_dir)] = {
|
||||
"kind": "folder",
|
||||
"fileid": int(current["fileid"]),
|
||||
"webdav_path": remote_dir,
|
||||
"display_name": str(current.get("display_name", "")),
|
||||
}
|
||||
|
||||
for child in children:
|
||||
name = safe_local_name(str(child["display_name"]))
|
||||
child_remote = name if remote_dir == "" else f"{remote_dir}/{name}"
|
||||
child_local = local_dir / name
|
||||
if bool(child["is_dir"]):
|
||||
stack.append((child_remote, child_local))
|
||||
continue
|
||||
extension = Path(name).suffix.lower()
|
||||
if extension not in SUPPORTED_IMAGE_EXTENSIONS:
|
||||
skipped += 1
|
||||
continue
|
||||
link_placeholder(seed, child_local)
|
||||
files += 1
|
||||
mapping[str(child_local)] = {
|
||||
"kind": "file",
|
||||
"fileid": int(child.get("fileid", 0)),
|
||||
"webdav_path": child_remote,
|
||||
"display_name": name,
|
||||
"content_type": str(child.get("content_type", "")),
|
||||
"size": int(child.get("size", 0)),
|
||||
"etag": str(child.get("etag", "")),
|
||||
}
|
||||
return files, folders, skipped
|
||||
|
||||
|
||||
def atomic_json(path: Path, payload: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
temporary = Path(handle.name)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def atomic_text(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
||||
handle.write(text)
|
||||
temporary = Path(handle.name)
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--base-url", required=True)
|
||||
parser.add_argument("--user", required=True)
|
||||
parser.add_argument("--password-file", type=Path)
|
||||
parser.add_argument("--root", action="append", required=True, help="WebDAV path relative to the authenticated user's files root")
|
||||
parser.add_argument("--source-dir", required=True, type=Path)
|
||||
parser.add_argument("--manifest", required=True, type=Path)
|
||||
parser.add_argument("--mapping", required=True, type=Path)
|
||||
parser.add_argument("--timeout", type=int, default=30)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.password_file:
|
||||
password = args.password_file.read_text(encoding="utf-8").rstrip("\r\n")
|
||||
else:
|
||||
password = getpass.getpass("Nextcloud password: ")
|
||||
if not password:
|
||||
fail("Nextcloud password is empty")
|
||||
|
||||
source_dir = args.source_dir.resolve()
|
||||
staging = source_dir.with_name(f".{source_dir.name}.next")
|
||||
previous = source_dir.with_name(f".{source_dir.name}.previous")
|
||||
seed = source_dir.parent / ".bratonien-webdav-placeholder.gif"
|
||||
source_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
seed.write_bytes(PLACEHOLDER)
|
||||
os.chmod(seed, 0o644)
|
||||
if staging.exists():
|
||||
shutil.rmtree(staging)
|
||||
staging.mkdir(parents=True)
|
||||
|
||||
client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout))
|
||||
mapping: dict[str, dict[str, object]] = {}
|
||||
manifest: list[str] = []
|
||||
total_files = total_folders = total_skipped = 0
|
||||
used_names: set[str] = set()
|
||||
|
||||
for remote_root_raw in args.root:
|
||||
remote_root = validate_relative(remote_root_raw)
|
||||
current, _ = client.list_collection(remote_root)
|
||||
fileid = int(current["fileid"])
|
||||
display = str(current.get("display_name", "")).strip() or (PurePosixPath(remote_root).name if remote_root else args.user)
|
||||
local_name = f"root-{fileid}"
|
||||
if local_name in used_names:
|
||||
fail(f"duplicate selected Nextcloud root fileid: {fileid}")
|
||||
used_names.add(local_name)
|
||||
local_root = staging / local_name
|
||||
files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping)
|
||||
total_files += files
|
||||
total_folders += folders
|
||||
total_skipped += skipped
|
||||
manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}")
|
||||
|
||||
if previous.exists():
|
||||
shutil.rmtree(previous)
|
||||
if source_dir.exists():
|
||||
source_dir.rename(previous)
|
||||
try:
|
||||
staging.rename(source_dir)
|
||||
except Exception:
|
||||
if source_dir.exists():
|
||||
shutil.rmtree(source_dir)
|
||||
if previous.exists():
|
||||
previous.rename(source_dir)
|
||||
raise
|
||||
if previous.exists():
|
||||
shutil.rmtree(previous)
|
||||
|
||||
final_mapping: dict[str, dict[str, object]] = {}
|
||||
staging_text = str(staging)
|
||||
source_text = str(source_dir)
|
||||
for path, data in mapping.items():
|
||||
final_path = source_text + path[len(staging_text):] if path.startswith(staging_text) else path
|
||||
final_mapping[final_path] = data
|
||||
|
||||
atomic_text(args.manifest, "\n".join(manifest) + "\n")
|
||||
atomic_json(args.mapping, {
|
||||
"version": 1,
|
||||
"base_url": args.base_url.rstrip("/"),
|
||||
"user": args.user,
|
||||
"files": final_mapping,
|
||||
})
|
||||
print(json.dumps({
|
||||
"roots": len(args.root),
|
||||
"files": total_files,
|
||||
"folders": total_folders,
|
||||
"skipped": total_skipped,
|
||||
"source_dir": str(source_dir),
|
||||
"manifest": str(args.manifest),
|
||||
"mapping": str(args.mapping),
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
except Exception as error:
|
||||
print(f"webdav-placeholder: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
191
runtime/reconcile-webdav.php
Normal file
191
runtime/reconcile-webdav.php
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
if (PHP_SAPI !== 'cli')
|
||||
{
|
||||
fwrite(STDERR, "CLI only\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function fail_webdav_reconcile($message)
|
||||
{
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
function decrypt_webdav_credentials($blob, $hexKey)
|
||||
{
|
||||
if (!preg_match('/^[a-f0-9]{64}$/', (string)$hexKey)) fail_webdav_reconcile('Connector-Schluessel ist ungueltig.');
|
||||
$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) fail_webdav_reconcile('Connector-Zugangsdaten haben ein unbekanntes Format.');
|
||||
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
|
||||
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
|
||||
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
|
||||
$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.');
|
||||
$decoded = json_decode($plain, true);
|
||||
if (!is_array($decoded)) fail_webdav_reconcile('WebDAV-Verbindung besitzt kein kompatibles Secret-Format.');
|
||||
return array(
|
||||
'nextcloud_user'=>(string)($decoded['nextcloud_user'] ?? ''),
|
||||
'nextcloud_password'=>(string)($decoded['nextcloud_password'] ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
function webdav_shell_value($value)
|
||||
{
|
||||
return escapeshellarg((string)$value);
|
||||
}
|
||||
|
||||
$pluginRoot = dirname(__DIR__);
|
||||
$piwigoRoot = dirname($pluginRoot, 2);
|
||||
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
|
||||
$configDir = '/etc/bratonien-tools/nc-connector';
|
||||
$stateRoot = '/var/lib/bratonien-tools/nc-connector';
|
||||
$publicSourceRoot = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-webdav-source';
|
||||
|
||||
try
|
||||
{
|
||||
if (!is_readable($dbConfig)) fail_webdav_reconcile('Piwigo-Datenbankkonfiguration ist nicht lesbar: '.$dbConfig);
|
||||
$conf = array();
|
||||
$prefixeTable = 'piwigo_';
|
||||
require $dbConfig;
|
||||
foreach (array('db_host','db_user','db_password','db_base') as $key)
|
||||
{
|
||||
if (!isset($conf[$key])) fail_webdav_reconcile('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
|
||||
}
|
||||
|
||||
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
|
||||
if ($db->connect_errno) fail_webdav_reconcile('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
|
||||
$db->set_charset('utf8mb4');
|
||||
|
||||
$keyResult = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
|
||||
if (!$keyResult || !$keyResult->num_rows) fail_webdav_reconcile('Connector-Schluessel wurde nicht gefunden.');
|
||||
$hexKey = trim((string)$keyResult->fetch_assoc()['value']);
|
||||
|
||||
$table = $prefixeTable.'bratonien_tools_nc_connections';
|
||||
$rows = $db->query("SELECT id,name,adapter,config_json,secret_blob FROM `{$table}` ORDER BY id");
|
||||
if (!$rows) fail_webdav_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
|
||||
|
||||
if (!is_dir($configDir) && !mkdir($configDir, 0700, true)) fail_webdav_reconcile('Runtime-Konfigurationsverzeichnis konnte nicht angelegt werden.');
|
||||
@chmod($configDir, 0700);
|
||||
if (!is_dir($publicSourceRoot) && !mkdir($publicSourceRoot, 0755, true)) fail_webdav_reconcile('WebDAV-Platzhalterbereich konnte nicht angelegt werden.');
|
||||
@chmod($publicSourceRoot, 0755);
|
||||
|
||||
$known = array();
|
||||
|
||||
while ($row = $rows->fetch_assoc())
|
||||
{
|
||||
$id = (int)$row['id'];
|
||||
$config = json_decode((string)$row['config_json'], true);
|
||||
if (!is_array($config)) $config = array();
|
||||
if ((string)$row['adapter'] !== 'remote') continue;
|
||||
if ((string)($config['source_mode'] ?? '') !== 'webdav-placeholder') continue;
|
||||
if (empty($config['parallel_test'])) continue;
|
||||
|
||||
$known[$id] = true;
|
||||
try
|
||||
{
|
||||
$baseUrl = rtrim(trim((string)($config['nextcloud_url'] ?? '')), '/');
|
||||
$roots = isset($config['roots']) && is_array($config['roots']) ? $config['roots'] : array();
|
||||
if ($baseUrl === '') fail_webdav_reconcile('Nextcloud-Adresse fehlt.');
|
||||
if (!$roots) fail_webdav_reconcile('Keine WebDAV-Wurzeln gespeichert.');
|
||||
|
||||
$credentials = decrypt_webdav_credentials((string)$row['secret_blob'], $hexKey);
|
||||
$user = trim($credentials['nextcloud_user']);
|
||||
$password = $credentials['nextcloud_password'];
|
||||
if ($user === '' || $password === '') fail_webdav_reconcile('Nextcloud-Zugangsdaten fehlen.');
|
||||
|
||||
$stateDir = rtrim((string)($config['state_dir'] ?? ''), '/');
|
||||
if ($stateDir === '') $stateDir = $stateRoot.'/connection-'.$id;
|
||||
if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_webdav_reconcile('State-Verzeichnis konnte nicht angelegt werden.');
|
||||
@chmod($stateDir, 0750);
|
||||
|
||||
$galleryRoot = rtrim((string)($config['parallel_gallery_root'] ?? ''), '/');
|
||||
if ($galleryRoot === '') $galleryRoot = rtrim($piwigoRoot, '/').'/galleries/bratonien-webdav-'.$id;
|
||||
$sourceDir = $publicSourceRoot.'/connection-'.$id;
|
||||
if (!is_dir($sourceDir) && !mkdir($sourceDir, 0755, true)) fail_webdav_reconcile('WebDAV-Platzhalterquelle konnte nicht angelegt werden.');
|
||||
@chmod($sourceDir, 0755);
|
||||
|
||||
$base = $configDir.'/webdav-connection-'.$id;
|
||||
$passwordPath = $base.'.nextcloud-password';
|
||||
$rootsPath = $base.'.roots.tsv';
|
||||
$configPath = $base.'.conf';
|
||||
$statusFile = $stateDir.'/connector-status.json';
|
||||
$mappingFile = $stateDir.'/webdav-map.json';
|
||||
$manifestFile = $stateDir.'/webdav-manifest.tsv';
|
||||
$shadowMapFile = $stateDir.'/webdav-shadow-map.json';
|
||||
|
||||
file_put_contents($passwordPath, $password."\n", LOCK_EX);
|
||||
@chmod($passwordPath, 0600);
|
||||
|
||||
$rootLines = array('# webdav_path<TAB>display_name');
|
||||
foreach ($roots as $root)
|
||||
{
|
||||
$path = trim((string)($root['webdav_path'] ?? ''), '/');
|
||||
$display = trim((string)($root['display_name'] ?? ''));
|
||||
if ($path === '' || $display === '' || preg_match('/[\t\r\n]/', $path.$display)) fail_webdav_reconcile('Eine gespeicherte WebDAV-Wurzel ist ungueltig.');
|
||||
$rootLines[] = $path."\t".$display;
|
||||
}
|
||||
file_put_contents($rootsPath, implode("\n", $rootLines)."\n", LOCK_EX);
|
||||
@chmod($rootsPath, 0600);
|
||||
|
||||
$lines = array(
|
||||
'PIWIGO_ROOT='.webdav_shell_value($piwigoRoot),
|
||||
'CONNECTION_ID='.$id,
|
||||
'SOURCE_MODE=webdav-placeholder',
|
||||
'WEBDAV_BASE_URL='.webdav_shell_value($baseUrl),
|
||||
'WEBDAV_USER='.webdav_shell_value($user),
|
||||
'WEBDAV_PASSWORD_FILE='.webdav_shell_value($passwordPath),
|
||||
'WEBDAV_ROOTS_FILE='.webdav_shell_value($rootsPath),
|
||||
'WEBDAV_SOURCE_DIR='.webdav_shell_value($sourceDir),
|
||||
'WEBDAV_MAPPING_FILE='.webdav_shell_value($mappingFile),
|
||||
'MANIFEST='.webdav_shell_value($manifestFile),
|
||||
'SHADOW_MAP_FILE='.webdav_shell_value($shadowMapFile),
|
||||
'GALLERY_ROOT='.webdav_shell_value($galleryRoot),
|
||||
'STATE_DIR='.webdav_shell_value($stateDir),
|
||||
'STATUS_FILE='.webdav_shell_value($statusFile),
|
||||
// Der WebDAV-Zweig registriert seinen fertigen Shadow Tree selbst.
|
||||
// Damit ist er nicht von der Aktivitaets-Gate einer bestehenden
|
||||
// lokalen Verbindung abhaengig. Bestehende Verbindungen bleiben
|
||||
// unveraendert und koennen parallel weiterlaufen.
|
||||
'PIWIGO_SYNC_ENABLED=1',
|
||||
);
|
||||
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
|
||||
@chmod($configPath, 0600);
|
||||
|
||||
$config['state_dir'] = $stateDir;
|
||||
$config['status_file'] = $statusFile;
|
||||
$config['parallel_gallery_root'] = $galleryRoot;
|
||||
$config['runtime'] = array(
|
||||
'mode'=>'parallel-webdav',
|
||||
'config'=>$configPath,
|
||||
'piwigo_sync_enabled'=>true,
|
||||
'reconciled_at'=>date('Y-m-d H:i:s'),
|
||||
);
|
||||
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (is_string($json))
|
||||
{
|
||||
$escaped = $db->real_escape_string($json);
|
||||
$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} LIMIT 1");
|
||||
}
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
fwrite(STDERR, "NC WebDAV #{$id}: ".$e->getMessage()."\n");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (glob($configDir.'/webdav-connection-*.conf') ?: array() as $path)
|
||||
{
|
||||
if (!preg_match('/webdav-connection-([0-9]+)\.conf$/', $path, $match)) continue;
|
||||
$id = (int)$match[1];
|
||||
if (isset($known[$id])) continue;
|
||||
foreach (glob($configDir.'/webdav-connection-'.$id.'.*') ?: array() as $stale) @unlink($stale);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
fwrite(STDERR, "NC WebDAV Reconcile: ".$e->getMessage()."\n");
|
||||
exit(1);
|
||||
}
|
||||
@@ -6,18 +6,43 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
shopt -s nullglob
|
||||
|
||||
if ! php "$SCRIPT_DIR/reconcile.php"; then
|
||||
echo "NC Connector: gespeicherte Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2
|
||||
echo "NC Connector: gespeicherte lokale Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! php "$SCRIPT_DIR/reconcile-webdav.php"; then
|
||||
echo "NC Connector: WebDAV-Testverbindungen konnten nicht mit der parallelen Runtime abgeglichen werden." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! php "$SCRIPT_DIR/cleanup-stale.php"; then
|
||||
echo "NC Connector: verwaiste Laufzeitdateien konnten nicht bereinigt werden." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
configs=("$CONFIG_DIR"/connection-*.conf)
|
||||
webdav_configs=("$CONFIG_DIR"/webdav-connection-*.conf)
|
||||
|
||||
if [[ ${#configs[@]} -eq 0 ]]; then
|
||||
echo "Keine aktiven NC-Connector-Verbindungen konfiguriert."
|
||||
if [[ ${#configs[@]} -eq 0 && ${#webdav_configs[@]} -eq 0 ]]; then
|
||||
echo "Keine NC-Connector-Verbindungen konfiguriert."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
result=0
|
||||
|
||||
# WebDAV zuerst: Der parallele Shadow Tree muss bereits vollständig stehen,
|
||||
# bevor der weiterhin produktive lokale Weg seine normale Piwigo-
|
||||
# Dateisynchronisierung ausführt. So wird der neue Baum im selben Minutenlauf
|
||||
# sichtbar, ohne dass der WebDAV-Zweig selbst eine zweite globale Piwigo-
|
||||
# Synchronisierung startet.
|
||||
for config in "${webdav_configs[@]}"; do
|
||||
name="$(basename "$config")"
|
||||
echo "NC Connector WebDAV parallel: $name"
|
||||
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync-webdav.sh"; then
|
||||
result=1
|
||||
fi
|
||||
done
|
||||
|
||||
for config in "${configs[@]}"; do
|
||||
name="$(basename "$config")"
|
||||
connection_id=""
|
||||
@@ -44,7 +69,7 @@ for config in "${configs[@]}"; do
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "NC Connector: $name"
|
||||
echo "NC Connector lokal: $name"
|
||||
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
|
||||
result=1
|
||||
fi
|
||||
|
||||
121
runtime/sync-webdav.sh
Normal file
121
runtime/sync-webdav.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
CONFIG_FILE="${PIWIGO_CONFIG:-}"
|
||||
[[ -n "$CONFIG_FILE" && -r "$CONFIG_FILE" ]] || { echo "WebDAV-Konfiguration fehlt: ${CONFIG_FILE:-<leer>}" >&2; exit 1; }
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG_FILE"
|
||||
|
||||
: "${PIWIGO_ROOT:?PIWIGO_ROOT fehlt}"
|
||||
: "${CONNECTION_ID:?CONNECTION_ID fehlt}"
|
||||
: "${WEBDAV_BASE_URL:?WEBDAV_BASE_URL fehlt}"
|
||||
: "${WEBDAV_USER:?WEBDAV_USER fehlt}"
|
||||
: "${WEBDAV_PASSWORD_FILE:?WEBDAV_PASSWORD_FILE fehlt}"
|
||||
: "${WEBDAV_ROOTS_FILE:?WEBDAV_ROOTS_FILE fehlt}"
|
||||
: "${WEBDAV_SOURCE_DIR:?WEBDAV_SOURCE_DIR fehlt}"
|
||||
: "${WEBDAV_MAPPING_FILE:?WEBDAV_MAPPING_FILE fehlt}"
|
||||
: "${MANIFEST:?MANIFEST fehlt}"
|
||||
: "${SHADOW_MAP_FILE:?SHADOW_MAP_FILE fehlt}"
|
||||
: "${GALLERY_ROOT:?GALLERY_ROOT fehlt}"
|
||||
: "${STATE_DIR:?STATE_DIR fehlt}"
|
||||
: "${STATUS_FILE:?STATUS_FILE fehlt}"
|
||||
|
||||
[[ "${SOURCE_MODE:-}" == "webdav-placeholder" ]] || { echo "Falscher SOURCE_MODE: ${SOURCE_MODE:-<leer>}" >&2; exit 1; }
|
||||
[[ -r "$WEBDAV_PASSWORD_FILE" ]] || { echo "Nextcloud-Passwortdatei fehlt." >&2; exit 1; }
|
||||
[[ -r "$WEBDAV_ROOTS_FILE" ]] || { echo "WebDAV-Wurzeln fehlen." >&2; exit 1; }
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOCK_FILE="$STATE_DIR/webdav-sync.lock"
|
||||
mkdir -p -- "$STATE_DIR"
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -n 9 || exit 0
|
||||
|
||||
write_status() {
|
||||
local state="$1" message="$2" detail="${3:-}"
|
||||
python3 - "$STATUS_FILE" "$PIWIGO_ROOT" "$CONNECTION_ID" "$state" "$message" "$detail" <<'PY'
|
||||
import json, os, sys, tempfile, time
|
||||
status_file, piwigo_root, connection_id, state, message, detail = sys.argv[1:]
|
||||
payload = {
|
||||
"state": state,
|
||||
"message": message,
|
||||
"timestamp": int(time.time()),
|
||||
"auth_mode": "webdav",
|
||||
"api": {"state": "not_run", "message": ""},
|
||||
"fallback": {"state": "not_run", "message": ""},
|
||||
"error_detail": detail if state == "error" else "",
|
||||
}
|
||||
public_file = os.path.join(piwigo_root.rstrip('/'), '_data', 'bratonien-tools', 'nc-connector-status', f'connection-{connection_id}.json')
|
||||
for target in (status_file, public_file):
|
||||
directory = os.path.dirname(target)
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
fd, temporary = tempfile.mkstemp(dir=directory)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as handle:
|
||||
json.dump(payload, handle, ensure_ascii=False)
|
||||
handle.write('\n')
|
||||
os.chmod(temporary, 0o644)
|
||||
os.replace(temporary, target)
|
||||
PY
|
||||
}
|
||||
|
||||
compact_output() {
|
||||
tail -n 12 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
|
||||
}
|
||||
|
||||
failure() {
|
||||
local code="$1" command="$2" line="$3"
|
||||
trap - ERR
|
||||
write_status error "WebDAV-Shadow-Tree fehlgeschlagen" "Exit-Code: $code; Zeile: $line; Befehl: $command"
|
||||
exit "$code"
|
||||
}
|
||||
trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR
|
||||
|
||||
ROOT_ARGS=()
|
||||
while IFS=$'\t' read -r path display_name; do
|
||||
[[ -z "$path" || "$path" == \#* ]] && continue
|
||||
ROOT_ARGS+=(--root "$path")
|
||||
done < "$WEBDAV_ROOTS_FILE"
|
||||
|
||||
[[ ${#ROOT_ARGS[@]} -gt 0 ]] || { write_status error "Keine WebDAV-Wurzeln konfiguriert"; exit 1; }
|
||||
|
||||
python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \
|
||||
--base-url "$WEBDAV_BASE_URL" \
|
||||
--user "$WEBDAV_USER" \
|
||||
--password-file "$WEBDAV_PASSWORD_FILE" \
|
||||
"${ROOT_ARGS[@]}" \
|
||||
--source-dir "$WEBDAV_SOURCE_DIR" \
|
||||
--manifest "$MANIFEST" \
|
||||
--mapping "$WEBDAV_MAPPING_FILE"
|
||||
|
||||
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
|
||||
--manifest "$MANIFEST" \
|
||||
--destination "$GALLERY_ROOT" \
|
||||
--state "$SHADOW_MAP_FILE"
|
||||
|
||||
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
|
||||
trap - ERR
|
||||
PIWIGO_OUTPUT=""
|
||||
PIWIGO_EXIT=0
|
||||
if PIWIGO_OUTPUT="$(php "$SCRIPT_DIR/lib/piwigo-sync.php" \
|
||||
--piwigo-root="$PIWIGO_ROOT" \
|
||||
--connection-id="$CONNECTION_ID" \
|
||||
--base-url="http://127.0.0.1" 2>&1)"; then
|
||||
PIWIGO_EXIT=0
|
||||
else
|
||||
PIWIGO_EXIT=$?
|
||||
fi
|
||||
[[ -z "$PIWIGO_OUTPUT" ]] || printf '%s\n' "$PIWIGO_OUTPUT"
|
||||
|
||||
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
|
||||
DETAIL="Exit-Code: $PIWIGO_EXIT"
|
||||
if [[ -n "$PIWIGO_OUTPUT" ]]; then
|
||||
DETAIL+="; Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | compact_output)"
|
||||
fi
|
||||
write_status error "Piwigo-Synchronisierung des WebDAV-Shadow-Trees fehlgeschlagen" "$DETAIL"
|
||||
exit "$PIWIGO_EXIT"
|
||||
fi
|
||||
|
||||
write_status ok "WebDAV-Shadow-Tree und Piwigo-Synchronisierung erfolgreich"
|
||||
else
|
||||
write_status ok "WebDAV-Shadow-Tree erfolgreich; Registrierung erfolgt im selben Minutenlauf über den bestehenden produktiven Piwigo-Sync"
|
||||
fi
|
||||
@@ -52,6 +52,12 @@
|
||||
|
||||
function token(){var input=section.querySelector('input[name="pwg_token"]');return input?input.value:'';}
|
||||
function setOpen(value){try{if(value)sessionStorage.setItem(storageKey,'1');else sessionStorage.removeItem(storageKey);}catch(e){}}
|
||||
function showWizard(){
|
||||
setOpen(true);
|
||||
if(!dialog)return;
|
||||
if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();
|
||||
else dialog.setAttribute('open','open');
|
||||
}
|
||||
function resetServer(){
|
||||
var pwgToken=token();
|
||||
if(!pwgToken)return Promise.reject(new Error('CSRF token missing'));
|
||||
@@ -65,8 +71,7 @@
|
||||
|
||||
if(openButton){
|
||||
openButton.addEventListener('click',function(event){
|
||||
if(resetBusy)return;event.preventDefault();event.stopImmediatePropagation();resetBusy=true;setOpen(true);
|
||||
resetServer().then(function(){window.location.reload();}).catch(function(){setOpen(false);resetBusy=false;});
|
||||
event.preventDefault();event.stopImmediatePropagation();showWizard();
|
||||
},true);
|
||||
}
|
||||
if(closeButton){
|
||||
@@ -75,12 +80,14 @@
|
||||
if(dialog){
|
||||
dialog.addEventListener('cancel',function(event){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();},true);
|
||||
dialog.addEventListener('click',function(event){if(event.target===dialog){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();}},true);
|
||||
try{if(sessionStorage.getItem(storageKey)==='1')showWizard();}catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function initNCConnectorPolling(){
|
||||
var section=document.getElementById('nc-connector');if(!section)return;
|
||||
var endpoint='plugins/bratonien_tools/nc-connector-status.php';
|
||||
var pollTimer=null;
|
||||
|
||||
function valueNodeForLabel(labelText){
|
||||
var labels=[].slice.call(section.querySelectorAll('.bratonien-label'));
|
||||
@@ -108,18 +115,25 @@
|
||||
var nextRunNode=valueNodeForLabel('Nächster Lauf');
|
||||
var lastResultNode=lastResultValueNode();
|
||||
|
||||
function render(data){
|
||||
if(lastRunNode&&typeof data.last_run_label==='string'&&data.last_run_label!=='')lastRunNode.textContent=data.last_run_label;
|
||||
if(nextRunNode&&typeof data.next_run_label==='string'&&data.next_run_label!=='')nextRunNode.textContent=data.next_run_label;
|
||||
if(lastResultNode&&typeof data.message==='string'&&data.message!=='')lastResultNode.textContent=data.message;
|
||||
}
|
||||
|
||||
function poll(){
|
||||
fetch(endpoint+'?_='+Date.now(),{credentials:'same-origin',cache:'no-store'})
|
||||
fetch(endpoint+'?_='+Date.now(),{credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}})
|
||||
.then(function(response){if(!response.ok)throw new Error('HTTP '+response.status);return response.json();})
|
||||
.then(function(data){
|
||||
if(lastRunNode&&data.last_run_label)lastRunNode.textContent=data.last_run_label;
|
||||
if(nextRunNode&&data.next_run_label)nextRunNode.textContent=data.next_run_label;
|
||||
if(lastResultNode&&typeof data.message==='string'&&data.message!=='')lastResultNode.textContent=data.message;
|
||||
})
|
||||
.then(render)
|
||||
.catch(function(){});
|
||||
}
|
||||
|
||||
poll();window.setInterval(poll,5000);
|
||||
function schedule(){
|
||||
if(pollTimer)window.clearInterval(pollTimer);
|
||||
pollTimer=window.setInterval(poll,5000);
|
||||
}
|
||||
|
||||
poll();schedule();
|
||||
}
|
||||
|
||||
function initAll(){initBratonienTabs();initNCWizardLifecycle();initNCConnectorPolling();}
|
||||
|
||||
Reference in New Issue
Block a user