feat: serve melody .bsm binaries over plain HTTP instead of Firebase Storage

ESP32 devices can't spare the 40KB+ RAM a TLS client needs, so Firebase
Storage's HTTPS-only download URLs were blocking melody downloads. Binaries
are now written to local disk (./data/melody_binaries) and served through a
new unauthenticated /api/melodies/download/{pid} route, exposed publicly on
a separate melodies.bellsystems.net vhost (plain HTTP, no TLS) so the main
console domain can stay HTTPS-only with no exceptions. Preview audio still
uses Firebase Storage since it's only ever fetched by the HTTPS admin UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 09:41:24 +03:00
parent 9df80dd4e1
commit 72f7e00990
6 changed files with 223 additions and 42 deletions

View File

@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, UploadFile, File, Query, HTTPException, Response
from fastapi.responses import FileResponse
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from auth.models import TokenPayload
@@ -13,6 +14,22 @@ from shared.audit import log_action
router = APIRouter(prefix="/api/melodies", tags=["melodies"])
@router.get("/download/{pid}")
async def download_binary_by_pid(pid: str):
"""Download a melody's .bsm binary by PID over plain HTTP.
No auth — ESP32 devices call this directly and can't afford a TLS client's
memory footprint, so this is served from a plain-HTTP-only host
(melodies.bellsystems.net) rather than the HTTPS console domain.
"""
path = await service.get_binary_path_by_pid(pid)
return FileResponse(
path=str(path),
media_type="application/octet-stream",
filename=f"{pid}.bsm",
)
@router.get("", response_model=MelodyListResponse)
async def list_melodies(
search: Optional[str] = Query(None),
@@ -134,15 +151,20 @@ async def upload_file(
if file_type == "binary":
content_type = "application/octet-stream"
url = service.upload_file_for_melody(
melody_id=melody_id,
melody_uid=melody.uid,
melody_pid=melody.pid,
file_bytes=contents,
filename=file.filename,
content_type=content_type,
)
url = service.save_binary_for_melody(
melody_uid=melody.uid,
pid=melody.pid,
file_bytes=contents,
)
else:
url = service.upload_file_for_melody(
melody_id=melody_id,
melody_uid=melody.uid,
melody_pid=melody.pid,
file_bytes=contents,
filename=file.filename,
content_type=content_type,
)
# Update the melody document with the file URL
if file_type == "preview":
@@ -179,7 +201,7 @@ async def get_files(
):
"""Get storage file URLs for a melody."""
melody = await service.get_melody(melody_id)
return service.get_storage_files(melody_id, melody.uid)
return service.get_storage_files(melody_id, melody.uid, melody.pid)
@router.patch("/{melody_id}/set-outdated", response_model=MelodyInDB)