Files
bellsystems-cp/backend/melodies/router.py

213 lines
7.8 KiB
Python

from fastapi import APIRouter, Depends, UploadFile, File, Query, HTTPException, Response
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from auth.models import TokenPayload
from auth.dependencies import require_permission
from melodies.models import (
MelodyCreate, MelodyUpdate, MelodyInDB, MelodyListResponse, MelodyInfo,
)
from melodies import service
from database.postgres import get_pg_session
from shared.audit import log_action
router = APIRouter(prefix="/api/melodies", tags=["melodies"])
@router.get("", response_model=MelodyListResponse)
async def list_melodies(
search: Optional[str] = Query(None),
type: Optional[str] = Query(None),
tone: Optional[str] = Query(None),
total_notes: Optional[int] = Query(None),
status: Optional[str] = Query(None),
_user: TokenPayload = Depends(require_permission("melodies", "view")),
):
melodies = await service.list_melodies(
search=search,
melody_type=type,
tone=tone,
total_notes=total_notes,
status=status,
)
return MelodyListResponse(melodies=melodies, total=len(melodies))
@router.get("/{melody_id}", response_model=MelodyInDB)
async def get_melody(
melody_id: str,
_user: TokenPayload = Depends(require_permission("melodies", "view")),
):
return await service.get_melody(melody_id)
@router.post("", response_model=MelodyInDB, status_code=201)
async def create_melody(
body: MelodyCreate,
publish: bool = Query(False),
_user: TokenPayload = Depends(require_permission("melodies", "add")),
db: AsyncSession = Depends(get_pg_session),
):
melody = await service.create_melody(body, publish=publish, actor_name=_user.name)
await log_action(db, _user.sub, _user.name or _user.email, "CREATE", "melody",
melody.id, melody.information.name if melody.information else melody.id)
return melody
@router.put("/{melody_id}", response_model=MelodyInDB)
async def update_melody(
melody_id: str,
body: MelodyUpdate,
_user: TokenPayload = Depends(require_permission("melodies", "edit")),
db: AsyncSession = Depends(get_pg_session),
):
old = await service.get_melody(melody_id)
melody = await service.update_melody(melody_id, body, actor_name=_user.name)
_SKIP = {"updated_at", "id", "metadata", "information", "noteAssignments"}
changes = {
k: {"old": getattr(old, k, None), "new": getattr(melody, k, None)}
for k in body.model_fields_set
if k not in _SKIP and getattr(old, k, None) != getattr(melody, k, None)
}
# Surface the name change from inside the information sub-object
old_name = old.information.name if old.information else None
new_name = melody.information.name if melody.information else None
if old_name != new_name:
changes["name"] = {"old": old_name, "new": new_name}
await log_action(db, _user.sub, _user.name or _user.email, "UPDATE", "melody",
melody_id, new_name or melody_id, changes=changes or None)
return melody
@router.delete("/{melody_id}", status_code=204)
async def delete_melody(
melody_id: str,
_user: TokenPayload = Depends(require_permission("melodies", "delete")),
db: AsyncSession = Depends(get_pg_session),
):
melody = await service.get_melody(melody_id)
label = melody.information.name if melody.information else melody_id
await service.delete_melody(melody_id)
await log_action(db, _user.sub, _user.name or _user.email, "DELETE", "melody",
melody_id, label)
@router.post("/{melody_id}/publish", response_model=MelodyInDB)
async def publish_melody(
melody_id: str,
_user: TokenPayload = Depends(require_permission("melodies", "edit")),
db: AsyncSession = Depends(get_pg_session),
):
melody = await service.publish_melody(melody_id)
await log_action(db, _user.sub, _user.name or _user.email, "PUBLISH", "melody",
melody_id, melody.information.name if melody.information else melody_id)
return melody
@router.post("/{melody_id}/unpublish", response_model=MelodyInDB)
async def unpublish_melody(
melody_id: str,
_user: TokenPayload = Depends(require_permission("melodies", "edit")),
db: AsyncSession = Depends(get_pg_session),
):
melody = await service.unpublish_melody(melody_id)
await log_action(db, _user.sub, _user.name or _user.email, "UNPUBLISH", "melody",
melody_id, melody.information.name if melody.information else melody_id)
return melody
@router.post("/{melody_id}/upload/{file_type}")
async def upload_file(
melody_id: str,
file_type: str,
file: UploadFile = File(...),
_user: TokenPayload = Depends(require_permission("melodies", "edit")),
):
"""Upload a binary or preview file. file_type must be 'binary' or 'preview'."""
if file_type not in ("binary", "preview"):
raise HTTPException(status_code=400, detail="file_type must be 'binary' or 'preview'")
# Verify melody exists
melody = await service.get_melody(melody_id)
contents = await file.read()
content_type = file.content_type or "application/octet-stream"
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,
)
# Update the melody document with the file URL
if file_type == "preview":
await service.update_melody(melody_id, MelodyUpdate(
information=MelodyInfo(
name=melody.information.name,
previewURL=url,
)
), actor_name=_user.name)
elif file_type == "binary":
await service.update_melody(melody_id, MelodyUpdate(url=url), actor_name=_user.name)
return {"url": url, "file_type": file_type}
@router.delete("/{melody_id}/files/{file_type}", status_code=204)
async def delete_file(
melody_id: str,
file_type: str,
_user: TokenPayload = Depends(require_permission("melodies", "edit")),
):
"""Delete a binary or preview file. file_type must be 'binary' or 'preview'."""
if file_type not in ("binary", "preview"):
raise HTTPException(status_code=400, detail="file_type must be 'binary' or 'preview'")
melody = await service.get_melody(melody_id)
service.delete_file(melody_id, file_type, melody.uid)
@router.get("/{melody_id}/files")
async def get_files(
melody_id: str,
_user: TokenPayload = Depends(require_permission("melodies", "view")),
):
"""Get storage file URLs for a melody."""
melody = await service.get_melody(melody_id)
return service.get_storage_files(melody_id, melody.uid)
@router.patch("/{melody_id}/set-outdated", response_model=MelodyInDB)
async def set_outdated(
melody_id: str,
outdated: bool = Query(...),
_user: TokenPayload = Depends(require_permission("melodies", "edit")),
):
"""Manually set or clear the outdated_archetype flag on a melody."""
melody = await service.get_melody(melody_id)
info = melody.information.model_dump()
info["outdated_archetype"] = outdated
return await service.update_melody(
melody_id,
MelodyUpdate(information=MelodyInfo(**info)),
actor_name=_user.name,
)
@router.get("/{melody_id}/download/binary")
async def download_binary_file(
melody_id: str,
_user: TokenPayload = Depends(require_permission("melodies", "view")),
):
"""Download current melody binary with a PID-based filename."""
melody = await service.get_melody(melody_id)
file_bytes, content_type = service.get_binary_file_bytes(melody_id, melody.uid)
filename = f"{(melody.pid or 'binary')}.bsm"
headers = {"Content-Disposition": f'attachment; filename="{filename}"'}
return Response(content=file_bytes, media_type=content_type, headers=headers)