Tile-Server: eigene Range-fähige /tiles-Route (StaticFiles liefert hinter BaseHTTPMiddleware kein 206)
Spike-Befund: app-weit kommen nur 200 ohne Accept-Ranges zurück, FileResponse-Range wird von der BaseHTTPMiddleware gebrochen. MapLibre/pmtiles braucht aber Byte-Ranges. Route gibt 206 als normales Response (Byte-Slice) zurück. Produktion: nginx/NPM direkt.
This commit is contained in:
parent
d9ecdb15fb
commit
bdadde8b98
1 changed files with 43 additions and 4 deletions
|
|
@ -372,11 +372,50 @@ app.mount("/icons", StaticFiles(directory=f"{STATIC_DIR}/icons"), name="icons")
|
|||
app.mount("/img", StaticFiles(directory=f"{STATIC_DIR}/img"), name="img")
|
||||
|
||||
# Selbst-gehostete Vektor-Tiles (.pmtiles) — liegen im data-Volume, NICHT im Image.
|
||||
# Starlette FileResponse beherrscht Range-Requests (206) → MapLibre/pmtiles-Protokoll.
|
||||
# Guard: Mount nur wenn das Verzeichnis existiert (sonst No-Op, z. B. lokal/Prod ohne Tiles).
|
||||
# WICHTIG: Starlettes StaticFiles/FileResponse liefert hinter unserer BaseHTTPMiddleware
|
||||
# KEINE Range-Requests (206) — app-weit kommt nur 200 ohne Accept-Ranges zurück.
|
||||
# MapLibre/pmtiles BRAUCHT aber Byte-Ranges (liest einzelne Tiles aus dem Single-File).
|
||||
# Daher eine eigene Route, die 206 als normales Response (Byte-Slice) zurückgibt — das
|
||||
# überlebt die Middleware. Für Produktion/Skalierung gehört das hinter nginx/NPM direkt
|
||||
# (Range nativ, keine App-CPU) — siehe docs/TILE_SERVER_HANDOVER.md, Entscheidung #2.
|
||||
_TILES_DIR = os.getenv("TILES_DIR", "/data/tiles")
|
||||
if os.path.isdir(_TILES_DIR):
|
||||
app.mount("/tiles", StaticFiles(directory=_TILES_DIR), name="tiles")
|
||||
|
||||
@app.get("/tiles/{filename}")
|
||||
async def serve_tile(filename: str, request: Request):
|
||||
# Kein Path-Traversal
|
||||
if "/" in filename or "\\" in filename or ".." in filename:
|
||||
return Response(status_code=404)
|
||||
path = os.path.join(_TILES_DIR, filename)
|
||||
if not os.path.isfile(path):
|
||||
return Response(status_code=404)
|
||||
file_size = os.path.getsize(path)
|
||||
base_headers = {"Accept-Ranges": "bytes", "Cache-Control": "public, max-age=86400"}
|
||||
range_header = request.headers.get("range")
|
||||
if range_header and range_header.startswith("bytes="):
|
||||
rng = range_header[6:].split(",")[0] # nur erster Range (pmtiles nutzt single-range)
|
||||
start_s, _, end_s = rng.partition("-")
|
||||
try:
|
||||
if start_s == "": # Suffix-Range "bytes=-N"
|
||||
length = int(end_s)
|
||||
start = max(0, file_size - length)
|
||||
end = file_size - 1
|
||||
else:
|
||||
start = int(start_s)
|
||||
end = int(end_s) if end_s else file_size - 1
|
||||
except ValueError:
|
||||
return Response(status_code=416, headers={**base_headers, "Content-Range": f"bytes */{file_size}"})
|
||||
end = min(end, file_size - 1)
|
||||
if start > end or start >= file_size:
|
||||
return Response(status_code=416, headers={**base_headers, "Content-Range": f"bytes */{file_size}"})
|
||||
with open(path, "rb") as f:
|
||||
f.seek(start)
|
||||
data = f.read(end - start + 1)
|
||||
return Response(
|
||||
data, status_code=206, media_type="application/octet-stream",
|
||||
headers={**base_headers, "Content-Range": f"bytes {start}-{end}/{file_size}"},
|
||||
)
|
||||
# Kein Range → ganze Datei streamen (pmtiles macht das normalerweise nicht).
|
||||
return FileResponse(path, media_type="application/octet-stream", headers=base_headers)
|
||||
|
||||
# User-generierte Medien (Fotos aus Tagebuch, Giftköder-Alarm, etc.)
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue