Files
skyview.astronomiemuseum.de/public/ajax/sonnenfinsternis_export.php
T
EskimueandClaude Sonnet 5 08bae7479c Einzelbild-Export (24 fps) für Sonnenfinsternis-Simulation hinzufügen
Ein neuer Button rendert die Zeitleiste der aktuell aktiven Ansicht
(Weltraum oder Standort) Bild für Bild bei 24 fps und lädt sie als ZIP
herunter. Jedes Frame wird einzeln an einen neuen PHP-Endpunkt
hochgeladen und in einem Temp-Verzeichnis abgelegt statt im Browser
gesammelt zu werden - das hält den Speicherbedarf sowohl client- als
auch serverseitig unabhängig von der Frame-Anzahl vorhersagbar
(wichtig bei mehreren tausend Frames). Das serverseitige Zippen kommt
ohne ZipArchive-Modul aus und liefert die Datei in festen Blöcken statt
sie komplett im Speicher zu puffern. Zusätzlich lässt sich der
exportierte Zeitbereich per Von/Bis-Uhrzeit einschränken, statt immer
die komplette Finsternis zu exportieren.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 08:43:30 +02:00

240 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
session_start();
const SF_EXPORT_MAX_FRAME_BYTES = 8 * 1024 * 1024;
const SF_EXPORT_MAX_INDEX = 99999;
const SF_EXPORT_TTL_SECONDS = 3 * 3600;
function sfExportJson(array $payload, int $status = 200): never
{
http_response_code($status);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function sfExportRequireLogin(): void
{
if (!isset($_SESSION['user_id'])) {
sfExportJson(['success' => false, 'message' => 'Bitte anmelden.'], 403);
}
}
function sfExportRequireToken(): void
{
$sessionToken = isset($_SESSION['sonnenfinsternis_export_token']) && is_string($_SESSION['sonnenfinsternis_export_token'])
? $_SESSION['sonnenfinsternis_export_token']
: '';
$submittedToken = isset($_REQUEST['token']) ? (string) $_REQUEST['token'] : '';
if ($sessionToken === '' || $submittedToken === '' || !hash_equals($sessionToken, $submittedToken)) {
sfExportJson(['success' => false, 'message' => 'Sicherheitsprüfung fehlgeschlagen. Bitte Seite neu laden.'], 403);
}
}
function sfExportDir(string $exportId): string
{
return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'sfexport_' . $exportId;
}
function sfExportRequireOwnedExportId(): string
{
$exportId = isset($_REQUEST['exportId']) ? (string) $_REQUEST['exportId'] : '';
if (!preg_match('/^[a-f0-9]{32}$/', $exportId) || !isset($_SESSION['sonnenfinsternis_export_ids'][$exportId])) {
sfExportJson(['success' => false, 'message' => 'Unbekannte oder abgelaufene Export-ID.'], 403);
}
return $exportId;
}
function sfExportDeleteDir(string $dir): void
{
if (!is_dir($dir)) {
return;
}
foreach (glob($dir . DIRECTORY_SEPARATOR . '*') ?: [] as $file) {
@unlink($file);
}
@rmdir($dir);
}
function sfExportSweepStale(): void
{
$base = sys_get_temp_dir();
foreach (glob($base . DIRECTORY_SEPARATOR . 'sfexport_*', GLOB_ONLYDIR) ?: [] as $dir) {
$mtime = @filemtime($dir);
if ($mtime !== false && (time() - $mtime) > SF_EXPORT_TTL_SECONDS) {
sfExportDeleteDir($dir);
}
}
}
// ─── Store-Format-ZIP-Writer (kein ZipArchive-Modul vorausgesetzt) ─────────
function sfZipLocalHeader(string $name, int $crc, int $size, int $dosTime, int $dosDate): string
{
return pack(
'VvvvvvVVVvv',
0x04034b50, 20, 0, 0, $dosTime, $dosDate, $crc, $size, $size, strlen($name), 0
) . $name;
}
function sfZipCentralHeader(string $name, int $crc, int $size, int $dosTime, int $dosDate, int $offset): string
{
return pack(
'VvvvvvvVVVvvvvvVV',
0x02014b50, 20, 20, 0, 0, $dosTime, $dosDate, $crc, $size, $size,
strlen($name), 0, 0, 0, 0, 0, $offset
) . $name;
}
function sfBuildZip(array $sourceFiles, string $zipPath): void
{
$fh = fopen($zipPath, 'wb');
if ($fh === false) {
throw new RuntimeException('ZIP-Datei konnte nicht angelegt werden.');
}
$dt = getdate();
$dosTime = ($dt['hours'] << 11) | ($dt['minutes'] << 5) | intdiv($dt['seconds'], 2);
$dosDate = (($dt['year'] - 1980) << 9) | ($dt['mon'] << 5) | $dt['mday'];
$centralBlob = '';
$offset = 0;
foreach ($sourceFiles as $name => $path) {
$data = file_get_contents($path);
if ($data === false) {
fclose($fh);
@unlink($zipPath);
throw new RuntimeException('Frame-Datei konnte nicht gelesen werden: ' . $path);
}
$crc = crc32($data);
$size = strlen($data);
fwrite($fh, sfZipLocalHeader($name, $crc, $size, $dosTime, $dosDate));
fwrite($fh, $data);
unset($data);
$centralBlob .= sfZipCentralHeader($name, $crc, $size, $dosTime, $dosDate, $offset);
$offset += 30 + strlen($name) + $size;
}
$centralStart = $offset;
fwrite($fh, $centralBlob);
fwrite($fh, pack(
'VvvvvVVv',
0x06054b50, 0, 0, count($sourceFiles), count($sourceFiles), strlen($centralBlob), $centralStart, 0
));
fclose($fh);
}
// ─── Routing ────────────────────────────────────────────────────────────────
sfExportRequireLogin();
sfExportRequireToken();
$action = isset($_REQUEST['action']) ? (string) $_REQUEST['action'] : '';
if ($action === 'start') {
sfExportSweepStale();
$exportId = bin2hex(random_bytes(16));
$dir = sfExportDir($exportId);
if (!mkdir($dir, 0700, true) && !is_dir($dir)) {
sfExportJson(['success' => false, 'message' => 'Temp-Verzeichnis konnte nicht angelegt werden.'], 500);
}
$_SESSION['sonnenfinsternis_export_ids'][$exportId] = time();
sfExportJson(['success' => true, 'exportId' => $exportId]);
}
if ($action === 'frame') {
$exportId = sfExportRequireOwnedExportId();
$index = isset($_REQUEST['index']) ? (string) $_REQUEST['index'] : '';
if (!ctype_digit($index) || (int) $index > SF_EXPORT_MAX_INDEX) {
sfExportJson(['success' => false, 'message' => 'Ungültiger Frame-Index.'], 400);
}
$raw = file_get_contents('php://input', false, null, 0, SF_EXPORT_MAX_FRAME_BYTES + 1);
if ($raw === false || $raw === '' || strlen($raw) > SF_EXPORT_MAX_FRAME_BYTES) {
sfExportJson(['success' => false, 'message' => 'Ungültige oder zu große Bilddaten.'], 400);
}
$filename = sfExportDir($exportId) . DIRECTORY_SEPARATOR . sprintf('frame_%05d.png', (int) $index);
if (file_put_contents($filename, $raw) === false) {
sfExportJson(['success' => false, 'message' => 'Frame konnte nicht gespeichert werden.'], 500);
}
sfExportJson(['success' => true]);
}
if ($action === 'cancel') {
$exportId = sfExportRequireOwnedExportId();
sfExportDeleteDir(sfExportDir($exportId));
unset($_SESSION['sonnenfinsternis_export_ids'][$exportId]);
sfExportJson(['success' => true]);
}
if ($action === 'finish') {
$exportId = sfExportRequireOwnedExportId();
$dir = sfExportDir($exportId);
$frameFiles = glob($dir . DIRECTORY_SEPARATOR . 'frame_*.png') ?: [];
sort($frameFiles);
if ($frameFiles === []) {
sfExportJson(['success' => false, 'message' => 'Keine Frames für diesen Export gefunden.'], 404);
}
$sourceFiles = [];
foreach ($frameFiles as $path) {
$sourceFiles[basename($path)] = $path;
}
// Das Zippen tausender Frames dauert laenger als PHPs Standard-Zeitlimit
// (30s) - hier ist es ein legitimer, einmaliger Vorgang fuer einen
// eingeloggten Nutzer, daher wird das Limit fuer diesen Request aufgehoben.
set_time_limit(0);
$zipPath = $dir . DIRECTORY_SEPARATOR . 'export.zip';
try {
sfBuildZip($sourceFiles, $zipPath);
} catch (Throwable $e) {
sfExportDeleteDir($dir);
unset($_SESSION['sonnenfinsternis_export_ids'][$exportId]);
sfExportJson(['success' => false, 'message' => 'ZIP konnte nicht erstellt werden: ' . $e->getMessage()], 500);
}
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="sonnenfinsternis_export_' . substr($exportId, 0, 8) . '.zip"');
header('Content-Length: ' . (string) filesize($zipPath));
// Aktive Output-Buffer wuerden die komplette (potenziell mehrere GB grosse)
// ZIP-Datei im Speicher aufsammeln statt sie durchzureichen - das war die
// Ursache des Speicherfehlers. Deshalb Buffer abschalten und die Datei in
// festen Bloecken direkt ausgeben, statt readfile() das ini-abhaengige
// Verhalten zu ueberlassen.
while (ob_get_level() > 0) {
ob_end_clean();
}
$out = fopen($zipPath, 'rb');
if ($out !== false) {
while (!feof($out)) {
echo fread($out, 1024 * 1024);
flush();
}
fclose($out);
}
sfExportDeleteDir($dir);
unset($_SESSION['sonnenfinsternis_export_ids'][$exportId]);
exit;
}
sfExportJson(['success' => false, 'message' => 'Unbekannte Aktion.'], 400);