src/Controller/TimelineKmlController.php line 26

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Service\KmlStorageService;
  4. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  5. use Symfony\Component\HttpFoundation\Request;
  6. use Symfony\Component\HttpFoundation\Response;
  7. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  8. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  9. use Symfony\Component\Routing\Annotation\Route;
  10. /**
  11. * Serves live KML from outside the web root (no directory listing / direct static URLs).
  12. */
  13. class TimelineKmlController extends BaseController
  14. {
  15. /**
  16. * @Route(
  17. * "/timelines/kml/{project}/{filepath}",
  18. * name="timelines_kml",
  19. * methods={"GET"},
  20. * requirements={"project"="[a-z0-9_-]+", "filepath"="[a-zA-Z0-9_./-]+\.kml"}
  21. * )
  22. */
  23. public function serve(string $project, string $filepath, Request $request, KmlStorageService $storage): Response
  24. {
  25. $project = $storage->assertValidProject($project);
  26. $relative = $storage->assertValidRelativeKmlPath($filepath);
  27. $path = $storage->resolveLiveKmlPath($project, $relative);
  28. if (!is_file($path)) {
  29. throw new NotFoundHttpException('KML not found.');
  30. }
  31. $downloadName = basename($path);
  32. $servePath = $path;
  33. $useGzip = false;
  34. $accept = (string) $request->headers->get('Accept-Encoding', '');
  35. if (stripos($accept, 'gzip') !== false) {
  36. $gzPath = $storage->ensureGzipCompanion($path);
  37. if ($gzPath !== null && is_file($gzPath)) {
  38. $servePath = $gzPath;
  39. $useGzip = true;
  40. }
  41. }
  42. $response = new BinaryFileResponse($servePath);
  43. $response->headers->set('Content-Type', 'application/vnd.google-earth.kml+xml; charset=UTF-8');
  44. $response->headers->set('X-Content-Type-Options', 'nosniff');
  45. // Inline for map fetch — do not advertise as downloadable attachment.
  46. $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $downloadName);
  47. if ($useGzip) {
  48. $response->headers->set('Content-Encoding', 'gzip');
  49. $response->headers->set('Vary', 'Accept-Encoding');
  50. }
  51. // Cheap validators (avoid hashing multi‑MB bodies on every request).
  52. $mtime = (int) filemtime($path);
  53. $response->setLastModified((new \DateTimeImmutable())->setTimestamp($mtime));
  54. $response->setEtag(sprintf('"%d-%d-%s"', $mtime, (int) filesize($servePath), $useGzip ? 'gz' : 'raw'));
  55. $response->setCache([
  56. 'public' => true,
  57. 'max_age' => 300,
  58. 's_maxage' => 300,
  59. 'must_revalidate' => true,
  60. ]);
  61. $response->isNotModified($request);
  62. return $response;
  63. }
  64. }