Ergänze sichere WebP-Bestandsmigration
Zeigt bei älteren Upload-Bildern automatisch einen geschützten Migrationsbutton im Adminbereich an und aktualisiert die JSON-Verweise transaktionssicher. Ergänzt außerdem einen Server-Updater, der Datenbank, Uploads und Konfiguration beim Wechsel von älteren Versionen sichert und wiederherstellt.
This commit is contained in:
@@ -9,6 +9,7 @@ Ein kleines, dateibasiertes PHP-CMS für die Website der Walschleber KulTour. Ve
|
|||||||
- JSON-LD-Ausgabe für Veranstaltungssuchmaschinen
|
- JSON-LD-Ausgabe für Veranstaltungssuchmaschinen
|
||||||
- Dateibasierte Speicherung in `data/talks.json`
|
- Dateibasierte Speicherung in `data/talks.json`
|
||||||
- Automatische Konvertierung von JPG-, PNG- und WebP-Uploads nach WebP
|
- Automatische Konvertierung von JPG-, PNG- und WebP-Uploads nach WebP
|
||||||
|
- Admin-Migration für bereits vorhandene Bilder, die noch nicht als WebP gespeichert sind
|
||||||
- Keine Composer-, Node.js- oder Build-Abhängigkeiten
|
- Keine Composer-, Node.js- oder Build-Abhängigkeiten
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
@@ -116,6 +117,8 @@ Diese Pfade enthalten den individuellen Datenbestand einer Installation:
|
|||||||
|
|
||||||
Beide Bereiche werden von Git ignoriert. Ein normaler `git pull` überschreibt oder löscht sie daher nach der Umstellung nicht. `uploads/.htaccess` und die `.gitignore`-Dateien bleiben Bestandteil des Repositorys.
|
Beide Bereiche werden von Git ignoriert. Ein normaler `git pull` überschreibt oder löscht sie daher nach der Umstellung nicht. `uploads/.htaccess` und die `.gitignore`-Dateien bleiben Bestandteil des Repositorys.
|
||||||
|
|
||||||
|
Erkennt das Admin-Interface vorhandene JPG-/PNG-Bestandsbilder, erscheint automatisch der Hinweis „WebP-Migration verfügbar“. Der dortige Button konvertiert die erkannten Dateien, aktualisiert ihre Verweise in `data/talks.json` und entfernt die Originale erst nach erfolgreicher Datenbankaktualisierung.
|
||||||
|
|
||||||
> **Wichtig:** Auf einem Produktivserver niemals `git clean -fdx` ausführen. Der Parameter `-x` bezieht ignorierte Dateien ein und würde dadurch die JSON-Datenbank und Uploads löschen.
|
> **Wichtig:** Auf einem Produktivserver niemals `git clean -fdx` ausführen. Der Parameter `-x` bezieht ignorierte Dateien ein und würde dadurch die JSON-Datenbank und Uploads löschen.
|
||||||
|
|
||||||
## Datensicherung
|
## Datensicherung
|
||||||
@@ -136,15 +139,15 @@ Die Sicherung enthält damit Datenbank, Bilder und die installationsspezifische
|
|||||||
|
|
||||||
### Reguläres Update
|
### Reguläres Update
|
||||||
|
|
||||||
Zuerst den Arbeitsstand kontrollieren und eine Sicherung wie oben beschrieben erstellen:
|
Für Updates steht ein sicherer Updater bereit. Er sichert `data/`, `uploads/` und `config.php` außerhalb des Checkouts, aktualisiert ausschließlich per Fast-Forward und stellt die persistenten Dateien anschließend wieder her:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /var/www/walschleber-kultour
|
cd /var/www/walschleber-kultour
|
||||||
git status --short
|
bash scripts/update-server.sh .
|
||||||
git fetch origin
|
|
||||||
git pull --ff-only origin main
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Der ausgegebene Backup-Ordner wird absichtlich nicht automatisch gelöscht.
|
||||||
|
|
||||||
Anschließend Schreibrechte und PHP-Syntax prüfen:
|
Anschließend Schreibrechte und PHP-Syntax prüfen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -159,7 +162,7 @@ Lokale Änderungen an Anwendungsdateien können ein Update blockieren. Sie sollt
|
|||||||
|
|
||||||
### Lokale Änderungen an `config.php`
|
### Lokale Änderungen an `config.php`
|
||||||
|
|
||||||
`config.php` gehört zum Repository, enthält aber installationsspezifische Werte. Die Datei deshalb immer zusammen mit den Daten sichern. Falls `git pull` meldet, dass lokale Änderungen an `config.php` überschrieben würden:
|
`config.php` gehört zum Repository, enthält aber installationsspezifische Werte. `scripts/update-server.sh` sichert und erhält diese Datei automatisch. Bei einem manuellen Update muss sie vor dem Pull gesichert werden:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
config_backup="../kultour-config-$(date +%Y%m%d-%H%M%S).php"
|
config_backup="../kultour-config-$(date +%Y%m%d-%H%M%S).php"
|
||||||
@@ -174,30 +177,23 @@ Danach die Werte aus der gesicherten Datei manuell in die aktuelle `config.php`
|
|||||||
|
|
||||||
## Einmaliges Update von einer älteren Version
|
## Einmaliges Update von einer älteren Version
|
||||||
|
|
||||||
In älteren Versionen wurden `data/talks.json` und vorhandene Uploads noch von Git verfolgt. Beim **ersten Update auf die bereinigte Version** müssen diese Daten deshalb gesichert und nach dem Pull wiederhergestellt werden.
|
In älteren Versionen wurden `data/talks.json` und vorhandene Uploads noch von Git verfolgt. Ein normales `git pull` würde diese Dateien beim ersten Wechsel auf die bereinigte Version entfernen.
|
||||||
|
|
||||||
|
> **Vor diesem einmaligen Wechsel kein normales `git pull` ausführen.** Den aktuellen Updater zunächst außerhalb des alten Checkouts herunterladen und von dort starten:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
curl -fL \
|
||||||
|
https://git.mrblake.cc/MrBlake/Walschleber-Kultour-CMS/raw/branch/main/scripts/update-server.sh \
|
||||||
|
-o /tmp/kultour-update-server.sh
|
||||||
|
chmod 700 /tmp/kultour-update-server.sh
|
||||||
|
bash /tmp/kultour-update-server.sh /var/www/walschleber-kultour
|
||||||
|
|
||||||
cd /var/www/walschleber-kultour
|
cd /var/www/walschleber-kultour
|
||||||
|
|
||||||
install_backup="../kultour-migration-$(date +%Y%m%d-%H%M%S)"
|
|
||||||
mkdir -p "$install_backup"
|
|
||||||
cp -a data uploads config.php "$install_backup/"
|
|
||||||
|
|
||||||
# Nur die früher getrackten Persistenzpfade auf den alten Git-Stand setzen.
|
|
||||||
git restore --source=HEAD --staged --worktree -- data uploads
|
|
||||||
|
|
||||||
git pull --ff-only origin main
|
|
||||||
|
|
||||||
# Den gesicherten Datenbestand wiederherstellen.
|
|
||||||
mkdir -p data uploads
|
|
||||||
cp -a "$install_backup/data/." data/
|
|
||||||
cp -a "$install_backup/uploads/." uploads/
|
|
||||||
|
|
||||||
chown -R www-data:www-data data uploads
|
chown -R www-data:www-data data uploads
|
||||||
chmod 775 data uploads
|
chmod 775 data uploads
|
||||||
```
|
```
|
||||||
|
|
||||||
Falls das Update wegen einer lokal geänderten `config.php` stoppt, zusätzlich die Schritte aus „Lokale Änderungen an `config.php`“ durchführen. Nach dieser einmaligen Migration erscheinen Änderungen an der Datenbank und neue Uploads nicht mehr in `git status`.
|
Bei einem privaten Repository kann der Updater alternativ im angemeldeten Browser heruntergeladen und als Datei nach `/tmp/kultour-update-server.sh` übertragen werden. Nach dieser einmaligen Migration erscheinen Änderungen an der Datenbank und neue Uploads nicht mehr in `git status`.
|
||||||
|
|
||||||
## Wiederherstellung aus einem Backup
|
## Wiederherstellung aus einem Backup
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,32 @@ if (isset($_GET['logout'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($loggedIn) {
|
if ($loggedIn) {
|
||||||
|
if (empty($_SESSION['migration_csrf'])) {
|
||||||
|
$_SESSION['migration_csrf'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
$migrationCsrf = (string)$_SESSION['migration_csrf'];
|
||||||
|
$migrationNotice = $_SESSION['migration_notice'] ?? null;
|
||||||
|
$migrationError = null;
|
||||||
|
unset($_SESSION['migration_notice']);
|
||||||
|
|
||||||
|
// BESTANDS-UPLOADS NACH WEBP MIGRIEREN
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['migrate_uploads'])) {
|
||||||
|
$submittedToken = (string)($_POST['migration_csrf'] ?? '');
|
||||||
|
if (!hash_equals($migrationCsrf, $submittedToken)) {
|
||||||
|
$migrationError = 'Die Migrationsanfrage ist abgelaufen. Bitte Seite neu laden und erneut versuchen.';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$_SESSION['migration_notice'] = migrate_uploads_to_webp();
|
||||||
|
header('Location: admin.php?migration=done');
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$migrationError = 'Migration fehlgeschlagen: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CREATE/UPDATE
|
// CREATE/UPDATE
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_event'])) {
|
elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_event'])) {
|
||||||
$events = load_events();
|
$events = load_events();
|
||||||
$id = $_POST['id'] ?: 'evt_' . bin2hex(random_bytes(6));
|
$id = $_POST['id'] ?: 'evt_' . bin2hex(random_bytes(6));
|
||||||
$title = sanitize_text($_POST['title'] ?? '');
|
$title = sanitize_text($_POST['title'] ?? '');
|
||||||
@@ -121,6 +145,13 @@ if ($loggedIn) {
|
|||||||
if ($e['id'] === $id) { $editEvent = $e; break; }
|
if ($e['id'] === $id) { $editEvent = $e; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$nonWebpUploads = find_non_webp_uploads();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$nonWebpUploads = [];
|
||||||
|
$migrationError = 'Upload-Prüfung fehlgeschlagen: ' . $e->getMessage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -191,6 +222,43 @@ if ($loggedIn) {
|
|||||||
<?php if (isset($_GET['deleted'])): ?><span class="badge text-bg-warning">Gelöscht</span><?php endif; ?>
|
<?php if (isset($_GET['deleted'])): ?><span class="badge text-bg-warning">Gelöscht</span><?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($migrationError)): ?>
|
||||||
|
<div class="alert alert-danger" role="alert"><?= h($migrationError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (is_array($migrationNotice)): ?>
|
||||||
|
<?php if (!empty($migrationNotice['remaining'])): ?>
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<?= (int)$migrationNotice['processed'] ?> Bild(er) wurden konvertiert und
|
||||||
|
<?= (int)$migrationNotice['references'] ?> Datenbank-Verweis(e) aktualisiert.
|
||||||
|
<?= count($migrationNotice['remaining']) ?> Originaldatei(en) konnten nicht entfernt werden und werden erneut angeboten.
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="alert alert-success" role="alert">
|
||||||
|
Migration abgeschlossen: <?= (int)$migrationNotice['processed'] ?> Bild(er) konvertiert und
|
||||||
|
<?= (int)$migrationNotice['references'] ?> Datenbank-Verweis(e) aktualisiert.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($nonWebpUploads)): ?>
|
||||||
|
<div class="alert alert-warning d-md-flex justify-content-between align-items-center gap-3" role="alert">
|
||||||
|
<div class="mb-3 mb-md-0">
|
||||||
|
<div class="fw-semibold">WebP-Migration verfügbar</div>
|
||||||
|
<div class="small">
|
||||||
|
<?= count($nonWebpUploads) ?> vorhandene(s) Upload-Bild(er) sind noch nicht als WebP gespeichert.
|
||||||
|
Die Veranstaltungseinträge werden automatisch aktualisiert.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post" onsubmit="return confirm('Alle erkannten Bestandsbilder jetzt nach WebP konvertieren?');">
|
||||||
|
<input type="hidden" name="migration_csrf" value="<?= h($migrationCsrf) ?>">
|
||||||
|
<button class="btn btn-warning text-nowrap" type="submit" name="migrate_uploads" value="1">
|
||||||
|
In WebP konvertieren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-lg-5">
|
<div class="col-lg-5">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
|
|||||||
+235
-4
@@ -3,6 +3,11 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
require_once __DIR__ . '/config.php';
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
// Erlaubt ein sicheres Update mit einer älteren, serverspezifischen config.php.
|
||||||
|
if (!defined('WEBP_QUALITY')) {
|
||||||
|
define('WEBP_QUALITY', 85);
|
||||||
|
}
|
||||||
|
|
||||||
function ensure_storage(): void {
|
function ensure_storage(): void {
|
||||||
if (!is_dir(dirname(DATA_FILE))) {
|
if (!is_dir(dirname(DATA_FILE))) {
|
||||||
@mkdir(dirname(DATA_FILE), 0775, true);
|
@mkdir(dirname(DATA_FILE), 0775, true);
|
||||||
@@ -24,17 +29,43 @@ function load_events(): array {
|
|||||||
|
|
||||||
function save_events(array $events): void {
|
function save_events(array $events): void {
|
||||||
ensure_storage();
|
ensure_storage();
|
||||||
|
$json = json_encode(
|
||||||
|
['events' => array_values($events)],
|
||||||
|
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
$fp = fopen(DATA_FILE, 'c+');
|
$fp = fopen(DATA_FILE, 'c+');
|
||||||
if ($fp === false) {
|
if ($fp === false) {
|
||||||
throw new RuntimeException('Kann Datei nicht öffnen: ' . DATA_FILE);
|
throw new RuntimeException('Kann Datei nicht öffnen: ' . DATA_FILE);
|
||||||
}
|
}
|
||||||
flock($fp, LOCK_EX);
|
|
||||||
ftruncate($fp, 0);
|
if (!flock($fp, LOCK_EX)) {
|
||||||
fwrite($fp, json_encode(['events' => array_values($events)], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
fclose($fp);
|
||||||
fflush($fp);
|
throw new RuntimeException('Kann Datenbank nicht sperren: ' . DATA_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!ftruncate($fp, 0) || !rewind($fp)) {
|
||||||
|
throw new RuntimeException('Kann Datenbank nicht leeren: ' . DATA_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = strlen($json);
|
||||||
|
$offset = 0;
|
||||||
|
while ($offset < $length) {
|
||||||
|
$written = fwrite($fp, substr($json, $offset));
|
||||||
|
if ($written === false || $written === 0) {
|
||||||
|
throw new RuntimeException('Kann Datenbank nicht schreiben: ' . DATA_FILE);
|
||||||
|
}
|
||||||
|
$offset += $written;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fflush($fp)) {
|
||||||
|
throw new RuntimeException('Kann Datenbank nicht speichern: ' . DATA_FILE);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
flock($fp, LOCK_UN);
|
flock($fp, LOCK_UN);
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function by_datetime_asc(array $a, array $b): int {
|
function by_datetime_asc(array $a, array $b): int {
|
||||||
return strtotime($a['date'].' '.$a['time']) <=> strtotime($b['date'].' '.$b['time']);
|
return strtotime($a['date'].' '.$a['time']) <=> strtotime($b['date'].' '.$b['time']);
|
||||||
@@ -140,6 +171,206 @@ function handle_image_upload(?array $file): ?string {
|
|||||||
return '/uploads/'.$basename; // Pfad relativ zur Webroot
|
return '/uploads/'.$basename; // Pfad relativ zur Webroot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Findet vorhandene Bilddateien im Upload-Ordner, deren Name nicht auf .webp endet.
|
||||||
|
*/
|
||||||
|
function find_non_webp_uploads(): array {
|
||||||
|
ensure_storage();
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
if ($finfo === false) {
|
||||||
|
throw new RuntimeException('Die vorhandenen Uploads konnten nicht geprüft werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploads = [];
|
||||||
|
try {
|
||||||
|
foreach (new DirectoryIterator(UPLOAD_DIR) as $file) {
|
||||||
|
if (!$file->isFile() || $file->isDot()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $file->getFilename();
|
||||||
|
if (strtolower(pathinfo($name, PATHINFO_EXTENSION)) === 'webp') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mime = finfo_file($finfo, $file->getPathname());
|
||||||
|
if (!is_string($mime) || !in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (@getimagesize($file->getPathname()) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploads[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'path' => $file->getPathname(),
|
||||||
|
'mime' => $mime,
|
||||||
|
'size' => $file->getSize(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
finfo_close($finfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($uploads, static function (array $left, array $right): int {
|
||||||
|
return strcasecmp($left['name'], $right['name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $uploads;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_events_for_migration(): array {
|
||||||
|
ensure_storage();
|
||||||
|
$raw = file_get_contents(DATA_FILE);
|
||||||
|
if ($raw === false) {
|
||||||
|
throw new RuntimeException('Die Datenbank konnte nicht gelesen werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
throw new RuntimeException('Die Datenbank enthält ungültiges JSON. Migration wurde abgebrochen.', 0, $exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($data) || !isset($data['events']) || !is_array($data['events'])) {
|
||||||
|
throw new RuntimeException('Die Datenbank hat ein ungültiges Format. Migration wurde abgebrochen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($data['events']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konvertiert Bestandsbilder nach WebP und aktualisiert anschließend ihre JSON-Verweise.
|
||||||
|
* Originaldateien werden erst nach einer erfolgreichen Datenbankaktualisierung entfernt.
|
||||||
|
*/
|
||||||
|
function migrate_uploads_to_webp(): array {
|
||||||
|
$uploads = find_non_webp_uploads();
|
||||||
|
if (!$uploads) {
|
||||||
|
return ['processed' => 0, 'references' => 0, 'removed' => 0, 'remaining' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadDir = rtrim(UPLOAD_DIR, '/\\');
|
||||||
|
$createdFiles = [];
|
||||||
|
$temporaryFiles = [];
|
||||||
|
$mapping = [];
|
||||||
|
$events = load_events_for_migration();
|
||||||
|
$databaseBackup = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($uploads as $upload) {
|
||||||
|
$contentHash = hash_file('sha256', $upload['path']);
|
||||||
|
if ($contentHash === false) {
|
||||||
|
throw new RuntimeException('Prüfsumme fehlgeschlagen: ' . $upload['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stem = pathinfo($upload['name'], PATHINFO_FILENAME);
|
||||||
|
$stem = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $stem);
|
||||||
|
$stem = trim((string)$stem, '-_');
|
||||||
|
if ($stem === '') {
|
||||||
|
$stem = 'legacy-image';
|
||||||
|
}
|
||||||
|
$stem = substr($stem, 0, 160);
|
||||||
|
|
||||||
|
$targetName = $stem . '-converted-' . substr($contentHash, 0, 10) . '.webp';
|
||||||
|
$targetPath = $uploadDir . '/' . $targetName;
|
||||||
|
|
||||||
|
if (is_file($targetPath)) {
|
||||||
|
$targetMime = mime_content_type($targetPath);
|
||||||
|
if ($targetMime !== 'image/webp' || @getimagesize($targetPath) === false) {
|
||||||
|
throw new RuntimeException('Vorhandene Zieldatei ist kein gültiges WebP: ' . $targetName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$temporaryPath = $uploadDir . '/.' . $targetName . '.' . bin2hex(random_bytes(4)) . '.tmp';
|
||||||
|
$temporaryFiles[] = $temporaryPath;
|
||||||
|
convert_image_to_webp($upload['path'], $upload['mime'], $temporaryPath, WEBP_QUALITY);
|
||||||
|
|
||||||
|
if (mime_content_type($temporaryPath) !== 'image/webp' || @getimagesize($temporaryPath) === false) {
|
||||||
|
throw new RuntimeException('Konvertierung konnte nicht validiert werden: ' . $upload['name']);
|
||||||
|
}
|
||||||
|
if (!@rename($temporaryPath, $targetPath)) {
|
||||||
|
throw new RuntimeException('WebP-Datei konnte nicht gespeichert werden: ' . $targetName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$temporaryFiles = array_values(array_filter(
|
||||||
|
$temporaryFiles,
|
||||||
|
static function (string $path) use ($temporaryPath): bool {
|
||||||
|
return $path !== $temporaryPath;
|
||||||
|
}
|
||||||
|
));
|
||||||
|
$createdFiles[] = $targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapping[$upload['name']] = '/uploads/' . $targetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedReferences = 0;
|
||||||
|
foreach ($events as &$event) {
|
||||||
|
$image = (string)($event['image'] ?? '');
|
||||||
|
if ($image === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = parse_url($image, PHP_URL_PATH);
|
||||||
|
$sourceName = rawurldecode(basename(is_string($path) ? $path : $image));
|
||||||
|
if (isset($mapping[$sourceName])) {
|
||||||
|
$event['image'] = $mapping[$sourceName];
|
||||||
|
$updatedReferences++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($event);
|
||||||
|
|
||||||
|
if ($updatedReferences > 0) {
|
||||||
|
$databaseBackup = DATA_FILE . '.migration-' . bin2hex(random_bytes(6)) . '.bak';
|
||||||
|
if (!@copy(DATA_FILE, $databaseBackup)) {
|
||||||
|
throw new RuntimeException('Die Datenbank konnte vor der Migration nicht gesichert werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
save_events($events);
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
if (!@copy($databaseBackup, DATA_FILE)) {
|
||||||
|
$backupToKeep = $databaseBackup;
|
||||||
|
$databaseBackup = null;
|
||||||
|
throw new RuntimeException(
|
||||||
|
'Datenbankaktualisierung und Wiederherstellung sind fehlgeschlagen. Backup: ' . $backupToKeep,
|
||||||
|
0,
|
||||||
|
$exception
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
foreach (array_merge($temporaryFiles, $createdFiles) as $path) {
|
||||||
|
if (is_file($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw $exception;
|
||||||
|
} finally {
|
||||||
|
if ($databaseBackup !== null && is_file($databaseBackup)) {
|
||||||
|
@unlink($databaseBackup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$removed = 0;
|
||||||
|
$remaining = [];
|
||||||
|
foreach ($uploads as $upload) {
|
||||||
|
if (@unlink($upload['path'])) {
|
||||||
|
$removed++;
|
||||||
|
} else {
|
||||||
|
$remaining[] = $upload['name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'processed' => count($uploads),
|
||||||
|
'references' => $updatedReferences,
|
||||||
|
'removed' => $removed,
|
||||||
|
'remaining' => $remaining,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konvertiert ein geprüftes JPG-, PNG- oder WebP-Bild in eine WebP-Datei.
|
* Konvertiert ein geprüftes JPG-, PNG- oder WebP-Bild in eine WebP-Datei.
|
||||||
* Unterstützt GD mit WebP-Support sowie Imagick als Alternative.
|
* Unterstützt GD mit WebP-Support sowie Imagick als Alternative.
|
||||||
|
|||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Das Skript außerhalb des Checkouts weiterführen, damit ein Pull seine laufende
|
||||||
|
# Version nicht verändern kann.
|
||||||
|
if [[ "${KULTOUR_UPDATE_REEXEC:-0}" != "1" ]]; then
|
||||||
|
stable_script="$(mktemp "${TMPDIR:-/tmp}/kultour-update.XXXXXX")"
|
||||||
|
cp -- "$0" "$stable_script"
|
||||||
|
chmod 700 "$stable_script"
|
||||||
|
export KULTOUR_UPDATE_REEXEC=1
|
||||||
|
export KULTOUR_UPDATE_TEMP_SCRIPT="$stable_script"
|
||||||
|
exec bash "$stable_script" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
repository_input="${1:-.}"
|
||||||
|
cd "$repository_input"
|
||||||
|
repository_root="$(git rev-parse --show-toplevel)"
|
||||||
|
cd "$repository_root"
|
||||||
|
|
||||||
|
repository_parent="$(dirname "$repository_root")"
|
||||||
|
update_backup="$(mktemp -d "${repository_parent}/kultour-update-backup.XXXXXX")"
|
||||||
|
restore_required=0
|
||||||
|
|
||||||
|
cleanup_temp_script() {
|
||||||
|
if [[ -n "${KULTOUR_UPDATE_TEMP_SCRIPT:-}" && -f "$KULTOUR_UPDATE_TEMP_SCRIPT" ]]; then
|
||||||
|
rm -f -- "$KULTOUR_UPDATE_TEMP_SCRIPT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_persistence() {
|
||||||
|
mkdir -p "$repository_root/data" "$repository_root/uploads"
|
||||||
|
cp -a -- "$update_backup/data/." "$repository_root/data/"
|
||||||
|
cp -a -- "$update_backup/uploads/." "$repository_root/uploads/"
|
||||||
|
cp -a -- "$update_backup/config.php" "$repository_root/config.php"
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_failure() {
|
||||||
|
status=$?
|
||||||
|
trap - ERR INT TERM
|
||||||
|
if [[ "$restore_required" == "1" ]]; then
|
||||||
|
echo "Update fehlgeschlagen – stelle Persistenz aus dem Backup wieder her ..." >&2
|
||||||
|
restore_persistence || true
|
||||||
|
fi
|
||||||
|
echo "Backup bleibt erhalten: $update_backup" >&2
|
||||||
|
cleanup_temp_script
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap handle_failure ERR INT TERM
|
||||||
|
|
||||||
|
for required_path in data uploads config.php; do
|
||||||
|
if [[ ! -e "$repository_root/$required_path" ]]; then
|
||||||
|
echo "Erforderlicher Pfad fehlt: $repository_root/$required_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
cp -a -- "$repository_root/data" "$update_backup/data"
|
||||||
|
cp -a -- "$repository_root/uploads" "$update_backup/uploads"
|
||||||
|
cp -a -- "$repository_root/config.php" "$update_backup/config.php"
|
||||||
|
restore_required=1
|
||||||
|
|
||||||
|
echo "Persistenz gesichert: $update_backup"
|
||||||
|
echo "Aktueller Arbeitsstand:"
|
||||||
|
git status --short
|
||||||
|
|
||||||
|
# In älteren Versionen waren Datenbank und Bilder noch getrackt. Nur diese
|
||||||
|
# Pfade sowie die lokale Konfiguration werden für den Pull auf HEAD gesetzt.
|
||||||
|
git restore --source=HEAD --staged --worktree -- data uploads config.php
|
||||||
|
git fetch origin main
|
||||||
|
git merge --ff-only origin/main
|
||||||
|
|
||||||
|
restore_persistence
|
||||||
|
restore_required=0
|
||||||
|
trap - ERR INT TERM
|
||||||
|
cleanup_temp_script
|
||||||
|
|
||||||
|
echo "Update abgeschlossen. Datenbank, Uploads und config.php wurden wiederhergestellt."
|
||||||
|
echo "Sicherheits-Backup bleibt erhalten: $update_backup"
|
||||||
|
git status --short --branch
|
||||||
Reference in New Issue
Block a user