mirror of
https://github.com/Terranom674/Piwigo_Bratonien_Tools.git
synced 2026-09-20 05:14:33 +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
|
## Plugin
|
||||||
|
|
||||||
- Aktuelle Plugin-Version: **0.9.4.4**
|
- Aktuelle Plugin-Version: **0.9.5.7**
|
||||||
- Aktueller Entwicklungsblock: **NC Connector – Endnutzer-Assistent und Konsolidierung**
|
- Aktueller Entwicklungsblock: **NC Connector – WebDAV-basierter Parallelweg bei vollständigem Erhalt des bestehenden produktiven Wegs**
|
||||||
- `0.9.3.43` markiert den abgeschlossenen vorherigen Meilenstein.
|
- GitHub ist das führende Repository.
|
||||||
- `0.9.4.x` ist der neue Entwicklungsblock.
|
- 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.
|
Bestehende Quellenmodi:
|
||||||
- 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.
|
|
||||||
|
|
||||||
## 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.
|
## Architektur des WebDAV-Parallelwegs
|
||||||
- Neue Wizard-Durchläufe werden von alten serverseitigen Zuständen getrennt.
|
|
||||||
- Abbrechen und Schließen verwerfen den laufenden Wizard-State.
|
|
||||||
|
|
||||||
## 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.
|
- liest WebDAV rekursiv;
|
||||||
2. Web-Zugang und OCS werden geprüft.
|
- lädt keine Originalbilder herunter;
|
||||||
3. Derselbe Benutzer und dasselbe Passwort werden automatisch für PostgreSQL verwendet.
|
- erzeugt eine lokale Platzhalterquelle;
|
||||||
4. Der Assistent prüft den üblichen Weg mit Nextcloud-Host, Port 5432 und Datenbank `nextcloud` bzw. bekannte passende Verbindungswerte.
|
- erzeugt Manifest und WebDAV-Mapping;
|
||||||
5. Nur wenn die Datenbank dort nicht erreichbar ist, werden Host, Port und Datenbankname abgefragt. Benutzer und Passwort bleiben unverändert aus Schritt 1.
|
- verwendet einen nur wenige Dutzend Byte großen Platzhalter.
|
||||||
6. Danach folgen gegebenenfalls Storage-Zuordnung, Showcase-Benutzer, Piwigo-API und Fallback.
|
|
||||||
|
|
||||||
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.
|
- `include/nc_connector_webdav.inc.php`
|
||||||
- Piwigo erhält einen Shadow Tree aus Verzeichnissen und Symlinks.
|
- Backend-Aktion `nc_connector_create_webdav_parallel`
|
||||||
- PostgreSQL-Reader und die Views `piwigo_showcase_sources` / `piwigo_showcase_activity` liefern Quellen und Aktivität.
|
- `source_mode=webdav-placeholder`
|
||||||
- Runtime-Konfigurationen: `/etc/bratonien-tools/nc-connector/connection-*.conf`
|
- `adapter=remote`
|
||||||
- State: `/var/lib/bratonien-tools/nc-connector/connection-ID`
|
- ausgewählte WebDAV-Wurzeln werden verbindungseigen gespeichert;
|
||||||
- Öffentlicher Admin-Status: Piwigo `_data/bratonien-tools/nc-connector-status/`
|
- Nextcloud-Basis-URL und Benutzer werden verbindungseigen gespeichert;
|
||||||
- Runner: `runtime/run-all.sh`
|
- Secret v3 enthält `nextcloud_user` und `nextcloud_password` rückwärtskompatibel zu älteren Secrets.
|
||||||
- Einzelverbindung: `runtime/sync.sh`
|
|
||||||
|
|
||||||
## 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;
|
Neu:
|
||||||
2. bei nicht nutzbarer API gespeicherter Benutzername/Passwort-Fallback;
|
|
||||||
3. ohne beide Wege Abbruch mit Fehlerstatus.
|
|
||||||
|
|
||||||
Produktive API-Methoden:
|
- `runtime/reconcile-webdav.php`
|
||||||
|
- `runtime/sync-webdav.sh`
|
||||||
|
- `runtime/run-all.sh` verarbeitet lokale und WebDAV-Verbindungen getrennt.
|
||||||
|
|
||||||
- `bratonien.nc.syncProductive`
|
Wesentliche Regeln:
|
||||||
- `bratonien.nc.syncOrphans`
|
|
||||||
|
|
||||||
`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;
|
Unverändert:
|
||||||
- Share-Fingerprint;
|
|
||||||
- Quiet-Time;
|
|
||||||
- Max-Wartezeit;
|
|
||||||
- periodischen Full-Sync;
|
|
||||||
- Reparaturbedarf bei beschädigter lokaler Struktur.
|
|
||||||
|
|
||||||
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.
|
Testbild: 16.091.204 Byte.
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Noch offen
|
Intern:
|
||||||
|
|
||||||
- vollständiger Remote-Nextcloud-Adapter ohne direkten PostgreSQL-/Storage-Zugriff;
|
- 1,829 s
|
||||||
- weitere Bereinigung alter Legacy-Migrationshelfer, sobald sie nicht mehr benötigt werden;
|
- 8.796.915 Byte/s
|
||||||
- weitere Endnutzer-Optimierung des Assistenten auf Basis realer Fehler- und Installationspfade.
|
|
||||||
|
|
||||||
## 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.
|
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
|
## Grundprinzip
|
||||||
|
|
||||||
@@ -12,62 +12,104 @@ Für die Administration gilt: Der normale Nutzer sieht Aufgaben, Entscheidungen
|
|||||||
|
|
||||||
## NC Connector
|
## 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
|
### Datenmodell
|
||||||
|
|
||||||
- Nextcloud bleibt die Quelle der Originaldateien.
|
- Nextcloud bleibt die Quelle der Originaldateien.
|
||||||
- Originalbilder werden nicht in eine zweite dauerhafte Bibliothek kopiert.
|
- Originalbilder werden nicht in eine zweite dauerhafte Bibliothek kopiert.
|
||||||
- Ein Shadow Tree erzeugt die für Piwigo benötigte Verzeichnisstruktur mit Symlinks.
|
- Ein Shadow Tree erzeugt die für Piwigo benötigte Verzeichnisstruktur.
|
||||||
- Ordnerfreigaben werden als physische Piwigo-Alben synchronisiert.
|
- Der bestehende produktive Weg verwendet lokale bereits vorhandene Speicherpfade und Symlinks auf die Originale.
|
||||||
- Einzeldateifreigaben werden im Galerie-Root verlinkt und als echte Piwigo-Orphans ohne Albumzuordnung registriert.
|
- Ordner werden als physische Piwigo-Alben synchronisiert.
|
||||||
- Entfernte Freigaben werden aus Shadow Tree und Piwigo entfernt, das Nextcloud-Original bleibt erhalten.
|
- 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.
|
- 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
|
### Verbindungsassistent
|
||||||
|
|
||||||
Neue Verbindungen werden bevorzugt mit einem Endnutzer-Assistenten angelegt.
|
Neue Verbindungen werden bevorzugt mit einem Endnutzer-Assistenten angelegt.
|
||||||
|
|
||||||
Ablauf:
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
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.
|
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;
|
- PostgreSQL-Reader;
|
||||||
- Source-View `piwigo_showcase_sources`;
|
- Source-/Activity-Views;
|
||||||
- Activity-View `piwigo_showcase_activity`;
|
|
||||||
- explizite Storage-Zuordnungen auf bereits vorhandene lokale Mounts;
|
- explizite Storage-Zuordnungen auf bereits vorhandene lokale Mounts;
|
||||||
- Plugin-eigene Runtime und getrennten State pro Verbindung.
|
- 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
|
### Activity Gate
|
||||||
|
|
||||||
Der regelmäßige Lauf berücksichtigt:
|
Der bestehende regelmäßige Lauf berücksichtigt:
|
||||||
|
|
||||||
- Nextcloud-Aktivität;
|
- Nextcloud-Aktivität;
|
||||||
- Signatur der sichtbaren Freigaben;
|
- Signatur der sichtbaren Quellen;
|
||||||
- Quiet-Time;
|
- Quiet-Time;
|
||||||
- maximale Wartezeit;
|
- maximale Wartezeit;
|
||||||
- periodischen Full-Sync;
|
- periodischen Full-Sync;
|
||||||
- notwendigen Reparaturlauf bei beschädigtem Shadow Tree oder fehlenden Piwigo-Alben.
|
- 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
|
### Piwigo-Synchronisierung
|
||||||
|
|
||||||
Der produktive Lauf ist API-first:
|
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
|
### 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.timer`
|
||||||
- `bratonien-nc-connector.service`
|
- `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 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
|
### 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.
|
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;
|
- technisch bearbeitet, solange sie nicht aktiv sind;
|
||||||
- deaktiviert und anschließend gelöscht werden.
|
- 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
|
## Bildcache
|
||||||
|
|
||||||
@@ -187,21 +245,21 @@ Wichtige Dateien und Verzeichnisse:
|
|||||||
- `include/nc_connector.inc.php` – Datenmodell und gemeinsame Connector-Funktionen
|
- `include/nc_connector.inc.php` – Datenmodell und gemeinsame Connector-Funktionen
|
||||||
- `include/nc_connector_wizard.inc.php` – Endnutzer-Assistent und Connection-Bearbeitung
|
- `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_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_piwigo_api.inc.php` – API-Zugang und Fallback-Verwaltung
|
||||||
- `include/nc_connector_system.inc.php` – Timer- und Laufzeitstatus
|
- `include/nc_connector_system.inc.php` – Timer- und Laufzeitstatus
|
||||||
- `include/nc_productive_ws.inc.php` – produktiver Piwigo-Dateisync
|
- `include/nc_productive_ws.inc.php` – produktiver Piwigo-Dateisync
|
||||||
- `include/nc_orphan_ws.inc.php` – Orphan-Synchronisierung
|
- `include/nc_orphan_ws.inc.php` – Orphan-Synchronisierung
|
||||||
- `runtime/lib/activity_gate.py` – Activity Gate
|
- `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/shadow_tree.py` – atomarer Shadow Tree mit Rollback
|
||||||
- `runtime/lib/piwigo-sync.php` – API-first-Piwigo-Sync mit Fallback
|
- `runtime/lib/piwigo-sync.php` – API-first-Piwigo-Sync mit Fallback
|
||||||
- `runtime/run-all.sh` – Multi-Connection-Runner
|
- `runtime/run-all.sh` – Multi-Connection-Runner und Bereinigung verwaister Runtime-Dateien
|
||||||
- `runtime/sync.sh` – Ablauf einer Verbindung
|
- `runtime/sync.sh` – Ablauf einer bestehenden lokalen 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
|
|
||||||
- `include/self_update.inc.php` – Self-Updater
|
- `include/self_update.inc.php` – Self-Updater
|
||||||
- `include/album_shares.inc.php` – geschützte Albumfreigaben
|
- `include/album_shares.inc.php` – geschützte Albumfreigaben
|
||||||
- `include/public_selection.inc.php` – Fotoauswahl
|
- `include/public_selection.inc.php` – Fotoauswahl
|
||||||
@@ -214,15 +272,20 @@ Wichtige Dateien und Verzeichnisse:
|
|||||||
|
|
||||||
- administrative Schreibaktionen verwenden Piwigos CSRF-Schutz;
|
- administrative Schreibaktionen verwenden Piwigos CSRF-Schutz;
|
||||||
- Connector-Zugangsdaten werden verschlüsselt gespeichert;
|
- Connector-Zugangsdaten werden verschlüsselt gespeichert;
|
||||||
|
- Secret v3 speichert Nextcloud-Zugangsdaten verbindungseigen und verschlüsselt;
|
||||||
- Wizard-Geheimnisse werden nicht in Browser-Web-Storage persistiert;
|
- 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;
|
- SQL-View-Namen werden vor Verwendung validiert;
|
||||||
- gefährliche technische Pfade sind nicht Teil des normalen Wizard-Happy-Paths;
|
|
||||||
- produktive Piwigo-API ist versionsgebunden;
|
- produktive Piwigo-API ist versionsgebunden;
|
||||||
- Originalbilder werden vom Connector nicht gelöscht;
|
- 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;
|
- 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
|
## 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_nextcloud_url']=(string)$state['base_url'];
|
||||||
$_POST['nc_showcase_user']=(string)$state['showcase_user'];
|
$_POST['nc_showcase_user']=(string)$state['showcase_user'];
|
||||||
$_POST['nc_access_user']=(string)$state['username'];
|
$_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_product']=(string)$state['product'];
|
||||||
$_POST['nc_version']=(string)$state['version'];
|
$_POST['nc_version']=(string)$state['version'];
|
||||||
$_POST['nc_api_validated']=$state['api_status']==='ok'?'1':'0';
|
$_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(
|
$payload = json_encode(array(
|
||||||
'v'=>2,
|
'v'=>3,
|
||||||
'db_password'=>$db_password,
|
'db_password'=>$db_password,
|
||||||
'piwigo_user'=>$fallback_user,
|
'piwigo_user'=>$fallback_user,
|
||||||
'piwigo_password'=>$fallback_password,
|
'piwigo_password'=>$fallback_password,
|
||||||
'api_key_id'=>$api_key_id,
|
'api_key_id'=>$api_key_id,
|
||||||
'api_key_secret'=>$api_key_secret,
|
'api_key_secret'=>$api_key_secret,
|
||||||
|
'nextcloud_user'=>'',
|
||||||
|
'nextcloud_password'=>'',
|
||||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
if (!is_string($payload)) throw new RuntimeException('Verbindungsauthentifizierung konnte nicht migriert werden.');
|
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))
|
if (is_array($decoded) && array_key_exists('db_password', $decoded))
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
'v'=>2,
|
'v'=>3,
|
||||||
'db_password'=>(string)($decoded['db_password'] ?? ''),
|
'db_password'=>(string)($decoded['db_password'] ?? ''),
|
||||||
'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
|
'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
|
||||||
'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
|
'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
|
||||||
'api_key_id'=>(string)($decoded['api_key_id'] ?? ''),
|
'api_key_id'=>(string)($decoded['api_key_id'] ?? ''),
|
||||||
'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''),
|
'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''),
|
||||||
|
'nextcloud_user'=>(string)($decoded['nextcloud_user'] ?? ''),
|
||||||
|
'nextcloud_password'=>(string)($decoded['nextcloud_password'] ?? ''),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'v'=>2,
|
'v'=>3,
|
||||||
'db_password'=>(string)$plain,
|
'db_password'=>(string)$plain,
|
||||||
'piwigo_user'=>'',
|
'piwigo_user'=>'',
|
||||||
'piwigo_password'=>'',
|
'piwigo_password'=>'',
|
||||||
'api_key_id'=>'',
|
'api_key_id'=>'',
|
||||||
'api_key_secret'=>'',
|
'api_key_secret'=>'',
|
||||||
|
'nextcloud_user'=>'',
|
||||||
|
'nextcloud_password'=>'',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bratonien_tools_nc_connector_store_scoped_secret($id, array $connection, array $credentials)
|
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);
|
$payload = json_encode($credentials, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
if (!is_string($payload)) throw new RuntimeException('Verbindungszugangsdaten konnten nicht serialisiert werden.');
|
if (!is_string($payload)) throw new RuntimeException('Verbindungszugangsdaten konnten nicht serialisiert werden.');
|
||||||
$blob = bratonien_tools_nc_connector_encrypt_secret($payload);
|
$blob = bratonien_tools_nc_connector_encrypt_secret($payload);
|
||||||
|
|
||||||
$config = is_array($connection['config'] ?? null) ? $connection['config'] : array();
|
|
||||||
if (array_key_exists('api_enabled', $config))
|
if (array_key_exists('api_enabled', $config))
|
||||||
{
|
{
|
||||||
$config['api_enabled'] = trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '';
|
$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.');
|
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);
|
$wizard_auth_present = array_key_exists('nc_api_validated', $_POST);
|
||||||
$validated_pending_api = (string)($_POST['nc_api_validated'] ?? '') === '1';
|
$validated_pending_api = (string)($_POST['nc_api_validated'] ?? '') === '1';
|
||||||
$api_key_id = trim((string)($_POST['nc_connection_api_key_id'] ?? ''));
|
$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']);
|
bratonien_tools_nc_connector_view_name($config['activity_view']);
|
||||||
|
|
||||||
$secret_payload = json_encode(array(
|
$secret_payload = json_encode(array(
|
||||||
'v'=>2,
|
'v'=>3,
|
||||||
'db_password'=>(string)$_POST['nc_db_password'],
|
'db_password'=>(string)$_POST['nc_db_password'],
|
||||||
'piwigo_user'=>$fallback_user,
|
'piwigo_user'=>$fallback_user,
|
||||||
'piwigo_password'=>$fallback_password,
|
'piwigo_password'=>$fallback_password,
|
||||||
'api_key_id'=>$api_key_id,
|
'api_key_id'=>$api_key_id,
|
||||||
'api_key_secret'=>$api_key_secret,
|
'api_key_secret'=>$api_key_secret,
|
||||||
|
'nextcloud_user'=>$nextcloud_user,
|
||||||
|
'nextcloud_password'=>$nextcloud_password,
|
||||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
if (!is_string($secret_payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.');
|
if (!is_string($secret_payload)) throw new RuntimeException('Connector-Zugangsdaten konnten nicht serialisiert werden.');
|
||||||
$secret_blob = bratonien_tools_nc_connector_encrypt_secret($secret_payload);
|
$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_piwigo_password']=$fallback_password;
|
||||||
$_POST['nc_nextcloud_url']=(string)$state['base_url'];
|
$_POST['nc_nextcloud_url']=(string)$state['base_url'];
|
||||||
$_POST['nc_access_user']=(string)$state['username'];
|
$_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_product']=(string)$state['product'];
|
||||||
$_POST['nc_version']=(string)$state['version'];
|
$_POST['nc_version']=(string)$state['version'];
|
||||||
$_POST['nc_api_validated']=($state['api_status'] ?? '')==='ok'?'1':'0';
|
$_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_wizard_flow.inc.php');
|
||||||
require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_connection_scope.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_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()
|
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_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'),
|
||||||
'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'),
|
'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'),
|
||||||
'nc_connector_create_local' => array('handler' => 'bratonien_tools_nc_connector_create_local_api_first'),
|
'nc_connector_create_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_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'),
|
||||||
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'),
|
'nc_connector_update_technical' => array('handler' => 'bratonien_tools_nc_connector_update_technical'),
|
||||||
'nc_connector_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_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'),
|
||||||
'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'),
|
'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'),
|
||||||
'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),
|
'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'),
|
||||||
'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
|
'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'),
|
||||||
'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_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_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_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_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_back' => array('handler' => 'bratonien_tools_nc_wizard_back'),
|
||||||
'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'),
|
'nc_connector_wizard_reset' => array('handler' => 'bratonien_tools_nc_wizard_reset'),
|
||||||
'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'),
|
'nc_connector_import_legacy' => array('handler' => 'bratonien_tools_nc_connector_import_legacy'),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
/*
|
/*
|
||||||
Plugin Name: Bratonien Tools
|
Plugin Name: Bratonien Tools
|
||||||
Version: 0.9.5.3
|
Version: 0.9.5.12
|
||||||
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
|
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
|
||||||
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
|
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
|
||||||
Author: Bratonien
|
Author: Bratonien
|
||||||
|
|||||||
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
|
shopt -s nullglob
|
||||||
|
|
||||||
if ! php "$SCRIPT_DIR/reconcile.php"; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
configs=("$CONFIG_DIR"/connection-*.conf)
|
configs=("$CONFIG_DIR"/connection-*.conf)
|
||||||
|
webdav_configs=("$CONFIG_DIR"/webdav-connection-*.conf)
|
||||||
|
|
||||||
if [[ ${#configs[@]} -eq 0 ]]; then
|
if [[ ${#configs[@]} -eq 0 && ${#webdav_configs[@]} -eq 0 ]]; then
|
||||||
echo "Keine aktiven NC-Connector-Verbindungen konfiguriert."
|
echo "Keine NC-Connector-Verbindungen konfiguriert."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
result=0
|
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
|
for config in "${configs[@]}"; do
|
||||||
name="$(basename "$config")"
|
name="$(basename "$config")"
|
||||||
connection_id=""
|
connection_id=""
|
||||||
@@ -44,7 +69,7 @@ for config in "${configs[@]}"; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "NC Connector: $name"
|
echo "NC Connector lokal: $name"
|
||||||
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
|
if ! env PIWIGO_CONFIG="$config" bash "$SCRIPT_DIR/sync.sh"; then
|
||||||
result=1
|
result=1
|
||||||
fi
|
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 token(){var input=section.querySelector('input[name="pwg_token"]');return input?input.value:'';}
|
||||||
function setOpen(value){try{if(value)sessionStorage.setItem(storageKey,'1');else sessionStorage.removeItem(storageKey);}catch(e){}}
|
function setOpen(value){try{if(value)sessionStorage.setItem(storageKey,'1');else sessionStorage.removeItem(storageKey);}catch(e){}}
|
||||||
|
function showWizard(){
|
||||||
|
setOpen(true);
|
||||||
|
if(!dialog)return;
|
||||||
|
if(typeof dialog.showModal==='function'&&!dialog.open)dialog.showModal();
|
||||||
|
else dialog.setAttribute('open','open');
|
||||||
|
}
|
||||||
function resetServer(){
|
function resetServer(){
|
||||||
var pwgToken=token();
|
var pwgToken=token();
|
||||||
if(!pwgToken)return Promise.reject(new Error('CSRF token missing'));
|
if(!pwgToken)return Promise.reject(new Error('CSRF token missing'));
|
||||||
@@ -65,8 +71,7 @@
|
|||||||
|
|
||||||
if(openButton){
|
if(openButton){
|
||||||
openButton.addEventListener('click',function(event){
|
openButton.addEventListener('click',function(event){
|
||||||
if(resetBusy)return;event.preventDefault();event.stopImmediatePropagation();resetBusy=true;setOpen(true);
|
event.preventDefault();event.stopImmediatePropagation();showWizard();
|
||||||
resetServer().then(function(){window.location.reload();}).catch(function(){setOpen(false);resetBusy=false;});
|
|
||||||
},true);
|
},true);
|
||||||
}
|
}
|
||||||
if(closeButton){
|
if(closeButton){
|
||||||
@@ -75,12 +80,14 @@
|
|||||||
if(dialog){
|
if(dialog){
|
||||||
dialog.addEventListener('cancel',function(event){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();},true);
|
dialog.addEventListener('cancel',function(event){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();},true);
|
||||||
dialog.addEventListener('click',function(event){if(event.target===dialog){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();}},true);
|
dialog.addEventListener('click',function(event){if(event.target===dialog){event.preventDefault();event.stopImmediatePropagation();closeAfterReset();}},true);
|
||||||
|
try{if(sessionStorage.getItem(storageKey)==='1')showWizard();}catch(e){}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initNCConnectorPolling(){
|
function initNCConnectorPolling(){
|
||||||
var section=document.getElementById('nc-connector');if(!section)return;
|
var section=document.getElementById('nc-connector');if(!section)return;
|
||||||
var endpoint='plugins/bratonien_tools/nc-connector-status.php';
|
var endpoint='plugins/bratonien_tools/nc-connector-status.php';
|
||||||
|
var pollTimer=null;
|
||||||
|
|
||||||
function valueNodeForLabel(labelText){
|
function valueNodeForLabel(labelText){
|
||||||
var labels=[].slice.call(section.querySelectorAll('.bratonien-label'));
|
var labels=[].slice.call(section.querySelectorAll('.bratonien-label'));
|
||||||
@@ -108,18 +115,25 @@
|
|||||||
var nextRunNode=valueNodeForLabel('Nächster Lauf');
|
var nextRunNode=valueNodeForLabel('Nächster Lauf');
|
||||||
var lastResultNode=lastResultValueNode();
|
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(){
|
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(response){if(!response.ok)throw new Error('HTTP '+response.status);return response.json();})
|
||||||
.then(function(data){
|
.then(render)
|
||||||
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;
|
|
||||||
})
|
|
||||||
.catch(function(){});
|
.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();}
|
function initAll(){initBratonienTabs();initNCWizardLifecycle();initNCConnectorPolling();}
|
||||||
|
|||||||
Reference in New Issue
Block a user