2026-03-08 14:51:28 -04:00

144 lines
4.2 KiB
Python

from typing import Annotated
from fastapi import Header, HTTPException
from fastapi import Path as PathVar
from fastapi import Request, status
from fastapi.responses import Response
from starlette.requests import ClientDisconnect
from streaming_form_data import StreamingFormDataParser
from streaming_form_data.targets import FileTarget, NullTarget
from decorators.auth import protected_route
from exceptions.endpoint_exceptions import RomNotFoundInDatabaseException
from handler.auth.constants import Scope
from handler.database import db_rom_handler
from handler.filesystem import fs_resource_handler
from logger.formatter import BLUE
from logger.formatter import highlight as hl
from logger.logger import log
from utils.router import APIRouter
router = APIRouter()
@protected_route(
router.post,
"/{id}/manuals",
[Scope.ROMS_WRITE],
status_code=status.HTTP_201_CREATED,
responses={status.HTTP_404_NOT_FOUND: {}},
)
async def add_rom_manuals(
request: Request,
id: Annotated[int, PathVar(description="Rom internal id.", ge=1)],
filename: Annotated[
str,
Header(
description="The name of the file being uploaded.",
alias="x-upload-filename",
),
],
) -> Response:
"""Upload manuals for a rom."""
rom = db_rom_handler.get_rom(id)
if not rom:
raise RomNotFoundInDatabaseException(id)
manuals_path = f"{rom.fs_resources_path}/manual"
file_location = fs_resource_handler.validate_path(f"{manuals_path}/{rom.id}.pdf")
log.info(f"Uploading manual to {hl(str(file_location))}")
await fs_resource_handler.make_directory(manuals_path)
parser = StreamingFormDataParser(headers=request.headers)
parser.register("x-upload-platform", NullTarget())
parser.register(filename, FileTarget(str(file_location)))
def cleanup_partial_file():
if file_location.exists():
file_location.unlink()
try:
async for chunk in request.stream():
parser.data_received(chunk)
db_rom_handler.update_rom(
id,
{
"path_manual": f"{manuals_path}/{rom.id}.pdf",
},
)
except ClientDisconnect:
log.error("Client disconnected during upload")
cleanup_partial_file()
except Exception as exc:
log.error("Error uploading files", exc_info=exc)
cleanup_partial_file()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="There was an error uploading the manual",
) from exc
return Response()
@protected_route(
router.delete,
"/{id}/manuals",
[Scope.ROMS_WRITE],
responses={status.HTTP_404_NOT_FOUND: {}},
)
async def delete_rom_manuals(
request: Request,
id: Annotated[int, PathVar(description="Rom internal id.", ge=1)],
) -> Response:
"""Delete manuals for a rom."""
rom = db_rom_handler.get_rom(id)
if not rom:
raise RomNotFoundInDatabaseException(id)
if not fs_resource_handler.manual_exists(rom):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No manual found for this ROM",
)
try:
await fs_resource_handler.remove_manual(rom)
db_rom_handler.update_rom(
id,
{
"path_manual": "",
"url_manual": "",
},
)
log.info(
f"Deleted manual for {hl(rom.name or 'ROM', color=BLUE)} [{hl(rom.fs_name)}]"
)
except FileNotFoundError:
log.warning(
f"Manual file not found for {hl(rom.name or 'ROM', color=BLUE)} [{hl(rom.fs_name)}]"
)
# Still update the database even if file doesn't exist
db_rom_handler.update_rom(
id,
{
"path_manual": "",
"url_manual": "",
},
)
except Exception as exc:
log.error(
f"Error deleting manual for {hl(rom.name or 'ROM', color=BLUE)} [{hl(rom.fs_name)}]",
exc_info=exc,
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="There was an error deleting the manual",
) from exc
return Response()