Zeige Fortschritt bei der WebP-Migration
Teilt die Bestandsmigration in einen sicheren Request pro Bild auf, zeigt den tatsächlichen Fortschritt im Adminbereich und erlaubt die Wiederaufnahme nach Gateway- oder Netzwerkabbrüchen.
This commit is contained in:
@@ -117,7 +117,7 @@ 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.
|
Erkennt das Admin-Interface vorhandene JPG-/PNG-Bestandsbilder, erscheint automatisch der Hinweis „WebP-Migration verfügbar“. Der dortige Button konvertiert die erkannten Dateien einzeln, zeigt nach jeder Datei den tatsächlichen Fortschritt an, aktualisiert ihre Verweise in `data/talks.json` und entfernt die Originale erst nach erfolgreicher Datenbankaktualisierung. Eine unterbrochene Migration kann über denselben Button sicher fortgesetzt werden.
|
||||||
|
|
||||||
> **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.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ require_once __DIR__ . '/functions.php';
|
|||||||
|
|
||||||
$loggedIn = isset($_SESSION['admin']) && $_SESSION['admin'] === true;
|
$loggedIn = isset($_SESSION['admin']) && $_SESSION['admin'] === true;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['migration_step']) && !$loggedIn) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-store');
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Die Admin-Sitzung ist abgelaufen. Bitte Seite neu laden und anmelden.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
if (isset($_POST['action']) && $_POST['action'] === 'login') {
|
if (isset($_POST['action']) && $_POST['action'] === 'login') {
|
||||||
if (($_POST['password'] ?? '') === ADMIN_PASSWORD_PLAIN) {
|
if (($_POST['password'] ?? '') === ADMIN_PASSWORD_PLAIN) {
|
||||||
@@ -30,28 +38,36 @@ if ($loggedIn) {
|
|||||||
$_SESSION['migration_csrf'] = bin2hex(random_bytes(32));
|
$_SESSION['migration_csrf'] = bin2hex(random_bytes(32));
|
||||||
}
|
}
|
||||||
$migrationCsrf = (string)$_SESSION['migration_csrf'];
|
$migrationCsrf = (string)$_SESSION['migration_csrf'];
|
||||||
$migrationNotice = $_SESSION['migration_notice'] ?? null;
|
|
||||||
$migrationError = null;
|
$migrationError = null;
|
||||||
unset($_SESSION['migration_notice']);
|
|
||||||
|
|
||||||
// BESTANDS-UPLOADS NACH WEBP MIGRIEREN
|
// Je Request genau ein Bestandsbild migrieren, damit das Frontend echten
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['migrate_uploads'])) {
|
// Fortschritt anzeigen kann und kein langer PHP-Request am Gateway scheitert.
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['migration_step'])) {
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
header('Cache-Control: no-store');
|
||||||
$submittedToken = (string)($_POST['migration_csrf'] ?? '');
|
$submittedToken = (string)($_POST['migration_csrf'] ?? '');
|
||||||
if (!hash_equals($migrationCsrf, $submittedToken)) {
|
if (!hash_equals($migrationCsrf, $submittedToken)) {
|
||||||
$migrationError = 'Die Migrationsanfrage ist abgelaufen. Bitte Seite neu laden und erneut versuchen.';
|
http_response_code(403);
|
||||||
} else {
|
echo json_encode(['ok' => false, 'error' => 'Die Migrationsanfrage ist abgelaufen. Bitte Seite neu laden.']);
|
||||||
try {
|
exit;
|
||||||
$_SESSION['migration_notice'] = migrate_uploads_to_webp();
|
|
||||||
header('Location: admin.php?migration=done');
|
|
||||||
exit;
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$migrationError = 'Migration fehlgeschlagen: ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session_write_close();
|
||||||
|
try {
|
||||||
|
$result = migrate_next_upload_to_webp();
|
||||||
|
echo json_encode(['ok' => true, 'result' => $result], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
http_response_code(422);
|
||||||
|
echo json_encode(
|
||||||
|
['ok' => false, 'error' => 'Migration fehlgeschlagen: ' . $e->getMessage()],
|
||||||
|
JSON_UNESCAPED_UNICODE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CREATE/UPDATE
|
// CREATE/UPDATE
|
||||||
elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_event'])) {
|
if ($_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'] ?? '');
|
||||||
@@ -226,36 +242,43 @@ if ($loggedIn) {
|
|||||||
<div class="alert alert-danger" role="alert"><?= h($migrationError) ?></div>
|
<div class="alert alert-danger" role="alert"><?= h($migrationError) ?></div>
|
||||||
<?php endif; ?>
|
<?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)): ?>
|
<?php if (!empty($nonWebpUploads)): ?>
|
||||||
<div class="alert alert-warning d-md-flex justify-content-between align-items-center gap-3" role="alert">
|
<div
|
||||||
<div class="mb-3 mb-md-0">
|
class="alert alert-warning"
|
||||||
<div class="fw-semibold">WebP-Migration verfügbar</div>
|
id="webp-migration-panel"
|
||||||
<div class="small">
|
data-total="<?= count($nonWebpUploads) ?>"
|
||||||
<?= count($nonWebpUploads) ?> vorhandene(s) Upload-Bild(er) sind noch nicht als WebP gespeichert.
|
data-csrf="<?= h($migrationCsrf) ?>"
|
||||||
Die Veranstaltungseinträge werden automatisch aktualisiert.
|
role="status"
|
||||||
|
>
|
||||||
|
<div class="d-md-flex justify-content-between align-items-center gap-3">
|
||||||
|
<div class="mb-3 mb-md-0">
|
||||||
|
<div class="fw-semibold">WebP-Migration verfügbar</div>
|
||||||
|
<div class="small" id="webp-migration-summary">
|
||||||
|
<?= count($nonWebpUploads) ?> vorhandene(s) Upload-Bild(er) sind noch nicht als WebP gespeichert.
|
||||||
|
Die Veranstaltungseinträge werden automatisch aktualisiert.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button class="btn btn-warning text-nowrap" type="button" id="webp-migration-start">
|
||||||
<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
|
In WebP konvertieren
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
|
<div class="mt-3 d-none" id="webp-migration-progress-wrap">
|
||||||
|
<div
|
||||||
|
class="progress"
|
||||||
|
role="progressbar"
|
||||||
|
aria-label="Fortschritt der WebP-Migration"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
aria-valuenow="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
id="webp-migration-progress"
|
||||||
|
style="width: 0%"
|
||||||
|
>0%</div>
|
||||||
|
</div>
|
||||||
|
<div class="small mt-2" id="webp-migration-status" aria-live="polite">Migration wird vorbereitet …</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
@@ -491,6 +514,106 @@ if ($loggedIn) {
|
|||||||
toggle.addEventListener('change', apply);
|
toggle.addEventListener('change', apply);
|
||||||
apply();
|
apply();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Bestandsmigration: ein Bild pro Request, damit der Fortschritt sichtbar
|
||||||
|
// bleibt und die Migration nach Abbruch gefahrlos fortgesetzt werden kann.
|
||||||
|
(function () {
|
||||||
|
const panel = document.getElementById('webp-migration-panel');
|
||||||
|
const button = document.getElementById('webp-migration-start');
|
||||||
|
const wrap = document.getElementById('webp-migration-progress-wrap');
|
||||||
|
const progress = document.getElementById('webp-migration-progress');
|
||||||
|
const progressBox = progress ? progress.parentElement : null;
|
||||||
|
const status = document.getElementById('webp-migration-status');
|
||||||
|
const summary = document.getElementById('webp-migration-summary');
|
||||||
|
if (!panel || !button || !wrap || !progress || !progressBox || !status || !summary) return;
|
||||||
|
|
||||||
|
let total = Number.parseInt(panel.dataset.total || '0', 10);
|
||||||
|
let completed = 0;
|
||||||
|
let running = false;
|
||||||
|
|
||||||
|
const setProgress = (percent) => {
|
||||||
|
const bounded = Math.max(0, Math.min(100, percent));
|
||||||
|
progress.style.width = `${bounded}%`;
|
||||||
|
progress.textContent = `${bounded}%`;
|
||||||
|
progressBox.setAttribute('aria-valuenow', String(bounded));
|
||||||
|
};
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
setProgress(100);
|
||||||
|
progress.classList.remove('progress-bar-animated');
|
||||||
|
panel.classList.remove('alert-warning', 'alert-danger');
|
||||||
|
panel.classList.add('alert-success');
|
||||||
|
summary.textContent = 'WebP-Migration abgeschlossen.';
|
||||||
|
status.textContent = 'Alle Bestandsbilder wurden konvertiert und ihre Datenbank-Verweise aktualisiert.';
|
||||||
|
button.classList.add('d-none');
|
||||||
|
running = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runMigration = async () => {
|
||||||
|
if (running) return;
|
||||||
|
if (completed === 0 && !window.confirm('Alle erkannten Bestandsbilder jetzt nach WebP konvertieren?')) return;
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Migration läuft …';
|
||||||
|
wrap.classList.remove('d-none');
|
||||||
|
panel.classList.remove('alert-danger');
|
||||||
|
panel.classList.add('alert-warning');
|
||||||
|
progress.classList.add('progress-bar-animated');
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
status.textContent = `Konvertiere Bild ${Math.min(completed + 1, total)} von ${total} …`;
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
migration_step: '1',
|
||||||
|
migration_csrf: panel.dataset.csrf || ''
|
||||||
|
});
|
||||||
|
const response = await fetch('admin.php', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'},
|
||||||
|
body: body.toString()
|
||||||
|
});
|
||||||
|
const responseText = await response.text();
|
||||||
|
let payload;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(responseText);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Ungültige Serverantwort (HTTP ${response.status}).`);
|
||||||
|
}
|
||||||
|
if (!response.ok || !payload.ok) {
|
||||||
|
throw new Error(payload.error || `Serverfehler (HTTP ${response.status}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = payload.result;
|
||||||
|
if (result.processed === 0 || result.remaining_count === 0) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
completed += result.processed;
|
||||||
|
total = Math.max(total, completed + result.remaining_count);
|
||||||
|
setProgress(Math.round((completed / total) * 100));
|
||||||
|
status.textContent = `${result.source} wurde konvertiert. Noch ${result.remaining_count} Bild(er).`;
|
||||||
|
|
||||||
|
if (result.deletion_failed) {
|
||||||
|
throw new Error(`Die Originaldatei ${result.source} konnte nicht entfernt werden.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
running = false;
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = 'Migration fortsetzen';
|
||||||
|
progress.classList.remove('progress-bar-animated');
|
||||||
|
panel.classList.remove('alert-warning');
|
||||||
|
panel.classList.add('alert-danger');
|
||||||
|
summary.textContent = 'Die Migration wurde unterbrochen.';
|
||||||
|
status.textContent = error instanceof Error ? error.message : 'Unbekannter Fehler.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
button.addEventListener('click', runMigration);
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+117
-66
@@ -189,6 +189,11 @@ function find_non_webp_uploads(): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$name = $file->getFilename();
|
$name = $file->getFilename();
|
||||||
|
// Angefangene Uploads und temporäre Konvertierungen niemals als
|
||||||
|
// eigenständige Bestandsbilder behandeln.
|
||||||
|
if (strncmp($name, '.', 1) === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (strtolower(pathinfo($name, PATHINFO_EXTENSION)) === 'webp') {
|
if (strtolower(pathinfo($name, PATHINFO_EXTENSION)) === 'webp') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -240,67 +245,84 @@ function load_events_for_migration(): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konvertiert Bestandsbilder nach WebP und aktualisiert anschließend ihre JSON-Verweise.
|
* Konvertiert genau ein Bestandsbild. Dadurch kann der Adminbereich nach jeder
|
||||||
* Originaldateien werden erst nach einer erfolgreichen Datenbankaktualisierung entfernt.
|
* Datei einen echten Fortschritt anzeigen und lange Gateway-Timeouts vermeiden.
|
||||||
*/
|
*/
|
||||||
function migrate_uploads_to_webp(): array {
|
function migrate_next_upload_to_webp(): array {
|
||||||
$uploads = find_non_webp_uploads();
|
ensure_storage();
|
||||||
if (!$uploads) {
|
$lockPath = DATA_FILE . '.webp-migration.lock';
|
||||||
return ['processed' => 0, 'references' => 0, 'removed' => 0, 'remaining' => []];
|
$lockHandle = fopen($lockPath, 'c');
|
||||||
|
if ($lockHandle === false) {
|
||||||
|
throw new RuntimeException('Die WebP-Migration konnte nicht gesperrt werden.');
|
||||||
|
}
|
||||||
|
if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
|
||||||
|
fclose($lockHandle);
|
||||||
|
throw new RuntimeException('Eine WebP-Migration läuft bereits. Bitte kurz warten.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$uploadDir = rtrim(UPLOAD_DIR, '/\\');
|
try {
|
||||||
$createdFiles = [];
|
return migrate_next_upload_to_webp_unlocked();
|
||||||
$temporaryFiles = [];
|
} finally {
|
||||||
$mapping = [];
|
flock($lockHandle, LOCK_UN);
|
||||||
|
fclose($lockHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrate_next_upload_to_webp_unlocked(): array {
|
||||||
|
$uploads = find_non_webp_uploads();
|
||||||
|
if (!$uploads) {
|
||||||
|
return [
|
||||||
|
'processed' => 0,
|
||||||
|
'references' => 0,
|
||||||
|
'removed' => 0,
|
||||||
|
'remaining_count' => 0,
|
||||||
|
'source' => null,
|
||||||
|
'target' => null,
|
||||||
|
'deletion_failed' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$upload = $uploads[0];
|
||||||
$events = load_events_for_migration();
|
$events = load_events_for_migration();
|
||||||
|
$uploadDir = rtrim(UPLOAD_DIR, '/\\');
|
||||||
|
$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;
|
||||||
|
$temporaryPath = null;
|
||||||
|
$targetCreated = false;
|
||||||
|
$preserveTargetOnFailure = false;
|
||||||
$databaseBackup = null;
|
$databaseBackup = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
foreach ($uploads as $upload) {
|
if (is_file($targetPath)) {
|
||||||
$contentHash = hash_file('sha256', $upload['path']);
|
$targetMime = mime_content_type($targetPath);
|
||||||
if ($contentHash === false) {
|
if ($targetMime !== 'image/webp' || @getimagesize($targetPath) === false) {
|
||||||
throw new RuntimeException('Prüfsumme fehlgeschlagen: ' . $upload['name']);
|
throw new RuntimeException('Vorhandene Zieldatei ist kein gültiges WebP: ' . $targetName);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$temporaryPath = $uploadDir . '/.' . $targetName . '.' . bin2hex(random_bytes(4)) . '.tmp';
|
||||||
|
convert_image_to_webp($upload['path'], $upload['mime'], $temporaryPath, WEBP_QUALITY);
|
||||||
|
|
||||||
$stem = pathinfo($upload['name'], PATHINFO_FILENAME);
|
if (mime_content_type($temporaryPath) !== 'image/webp' || @getimagesize($temporaryPath) === false) {
|
||||||
$stem = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $stem);
|
throw new RuntimeException('Konvertierung konnte nicht validiert werden: ' . $upload['name']);
|
||||||
$stem = trim((string)$stem, '-_');
|
|
||||||
if ($stem === '') {
|
|
||||||
$stem = 'legacy-image';
|
|
||||||
}
|
}
|
||||||
$stem = substr($stem, 0, 160);
|
if (!@rename($temporaryPath, $targetPath)) {
|
||||||
|
throw new RuntimeException('WebP-Datei konnte nicht gespeichert werden: ' . $targetName);
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
$temporaryPath = null;
|
||||||
$mapping[$upload['name']] = '/uploads/' . $targetName;
|
$targetCreated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$updatedReferences = 0;
|
$updatedReferences = 0;
|
||||||
@@ -312,8 +334,8 @@ function migrate_uploads_to_webp(): array {
|
|||||||
|
|
||||||
$path = parse_url($image, PHP_URL_PATH);
|
$path = parse_url($image, PHP_URL_PATH);
|
||||||
$sourceName = rawurldecode(basename(is_string($path) ? $path : $image));
|
$sourceName = rawurldecode(basename(is_string($path) ? $path : $image));
|
||||||
if (isset($mapping[$sourceName])) {
|
if ($sourceName === $upload['name']) {
|
||||||
$event['image'] = $mapping[$sourceName];
|
$event['image'] = '/uploads/' . $targetName;
|
||||||
$updatedReferences++;
|
$updatedReferences++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,6 +351,7 @@ function migrate_uploads_to_webp(): array {
|
|||||||
save_events($events);
|
save_events($events);
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
if (!@copy($databaseBackup, DATA_FILE)) {
|
if (!@copy($databaseBackup, DATA_FILE)) {
|
||||||
|
$preserveTargetOnFailure = true;
|
||||||
$backupToKeep = $databaseBackup;
|
$backupToKeep = $databaseBackup;
|
||||||
$databaseBackup = null;
|
$databaseBackup = null;
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
@@ -341,10 +364,11 @@ function migrate_uploads_to_webp(): array {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
foreach (array_merge($temporaryFiles, $createdFiles) as $path) {
|
if ($temporaryPath !== null && is_file($temporaryPath)) {
|
||||||
if (is_file($path)) {
|
@unlink($temporaryPath);
|
||||||
@unlink($path);
|
}
|
||||||
}
|
if ($targetCreated && !$preserveTargetOnFailure && is_file($targetPath)) {
|
||||||
|
@unlink($targetPath);
|
||||||
}
|
}
|
||||||
throw $exception;
|
throw $exception;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -353,20 +377,47 @@ function migrate_uploads_to_webp(): array {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$removed = 0;
|
$removed = @unlink($upload['path']) ? 1 : 0;
|
||||||
$remaining = [];
|
$remaining = find_non_webp_uploads();
|
||||||
foreach ($uploads as $upload) {
|
|
||||||
if (@unlink($upload['path'])) {
|
|
||||||
$removed++;
|
|
||||||
} else {
|
|
||||||
$remaining[] = $upload['name'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'processed' => count($uploads),
|
'processed' => 1,
|
||||||
'references' => $updatedReferences,
|
'references' => $updatedReferences,
|
||||||
'removed' => $removed,
|
'removed' => $removed,
|
||||||
|
'remaining_count' => count($remaining),
|
||||||
|
'source' => $upload['name'],
|
||||||
|
'target' => $targetName,
|
||||||
|
'deletion_failed' => $removed === 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kompatibler Komplettlauf für CLI-Aufrufe. Das Admin-Interface verwendet
|
||||||
|
* migrate_next_upload_to_webp(), damit jeder Schritt separat bestätigt wird.
|
||||||
|
*/
|
||||||
|
function migrate_uploads_to_webp(): array {
|
||||||
|
$processed = 0;
|
||||||
|
$references = 0;
|
||||||
|
$removed = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$step = migrate_next_upload_to_webp();
|
||||||
|
$processed += $step['processed'];
|
||||||
|
$references += $step['references'];
|
||||||
|
$removed += $step['removed'];
|
||||||
|
} while ($step['processed'] > 0 && !$step['deletion_failed'] && $step['remaining_count'] > 0);
|
||||||
|
|
||||||
|
$remaining = array_map(
|
||||||
|
static function (array $upload): string {
|
||||||
|
return $upload['name'];
|
||||||
|
},
|
||||||
|
find_non_webp_uploads()
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'processed' => $processed,
|
||||||
|
'references' => $references,
|
||||||
|
'removed' => $removed,
|
||||||
'remaining' => $remaining,
|
'remaining' => $remaining,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user