Direct download for rom files

This commit is contained in:
Georges-Antoine Assi
2025-03-18 09:48:27 -04:00
parent 00ac55453c
commit dfe3004f5d
3 changed files with 128 additions and 7 deletions

View File

@@ -17,7 +17,7 @@ from fastapi import Request
from handler.auth.constants import Scope
from handler.database import db_platform_handler, db_rom_handler
from handler.metadata import meta_igdb_handler
from handler.metadata.base_hander import SWITCH_TITLEDB_REGEX
from handler.metadata.base_hander import SWITCH_PRODUCT_ID_REGEX, SWITCH_TITLEDB_REGEX
from models.rom import Rom
from starlette.datastructures import URLPath
from utils.router import APIRouter
@@ -141,10 +141,32 @@ async def tinfoil_index_feed(
) -> dict[str, TinfoilFeedTitleDBSchema]:
titledb = {}
for rom in roms:
match = SWITCH_TITLEDB_REGEX.search(rom.fs_name)
if match:
tdb_match = SWITCH_TITLEDB_REGEX.search(rom.fs_name)
pid_match = SWITCH_PRODUCT_ID_REGEX.search(rom.fs_name)
if tdb_match:
_search_term, index_entry = (
await meta_igdb_handler._switch_titledb_format(match, rom.fs_name)
await meta_igdb_handler._switch_titledb_format(
tdb_match, rom.fs_name
)
)
if index_entry:
titledb[str(index_entry["nsuId"])] = TinfoilFeedTitleDBSchema(
id=str(index_entry["nsuId"]),
name=index_entry["name"],
description=index_entry["description"],
size=index_entry["size"],
version=index_entry["version"] or 0,
region=index_entry["region"] or "US",
releaseDate=index_entry["releaseDate"] or 19700101,
rating=index_entry["rating"] or 0,
publisher=index_entry["publisher"] or "",
rank=0,
)
elif pid_match:
_search_term, index_entry = (
await meta_igdb_handler._switch_productid_format(
pid_match, rom.fs_name
)
)
if index_entry:
titledb[str(index_entry["nsuId"])] = TinfoilFeedTitleDBSchema(
@@ -166,11 +188,17 @@ async def tinfoil_index_feed(
files=[
TinfoilFeedFileSchema(
url=str(
request.url_for("get_rom_content", id=rom.id, file_name=rom.fs_name)
request.url_for(
"get_romfile_content",
id=rom_file.id,
file_name=rom_file.file_name,
)
),
size=rom.fs_size_bytes,
size=rom_file.file_size_bytes,
)
for rom in roms
for rom_file in rom.files
if rom_file.file_extension in ["xci", "nsp", "nsz", "xcz", "nro"]
],
directories=[],
success="RomM Switch Library",

View File

@@ -21,6 +21,7 @@ from decorators.auth import protected_route
from endpoints.responses import MessageResponse
from endpoints.responses.rom import (
DetailedRomSchema,
RomFileSchema,
RomSchema,
RomUserSchema,
SimpleRomSchema,
@@ -288,9 +289,11 @@ async def get_rom_content(
file_name: Zip file output name
Returns:
FileResponse: Returns one file for single file roms
Response: Returns a response with headers
Yields:
FileResponse: Returns one file for single file roms
FileRedirectResponse: Redirects to the file download path
ZipResponse: Returns a response for nginx to serve a Zip file for multi-part roms
"""
@@ -785,3 +788,75 @@ async def update_rom_user(request: Request, id: int) -> RomUserSchema:
rom_user = db_rom_handler.update_rom_user(db_rom_user.id, cleaned_data)
return RomUserSchema.model_validate(rom_user)
@protected_route(
router.get,
"files/{id}",
[] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else [Scope.ROMS_READ],
)
async def get_romfile(
request: Request,
id: int,
) -> RomFileSchema:
file = db_rom_handler.get_rom_file_by_id(id)
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found",
)
return RomFileSchema.model_validate(file)
@protected_route(
router.get,
"files/{id}/content/{file_name}",
[] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else [Scope.ROMS_READ],
)
async def get_romfile_content(
request: Request,
id: int,
file_name: str,
):
"""Download rom file endpoint
Args:
request (Request): Fastapi Request object
id (int): Rom internal id
file_id (int): File internal id
Returns:
FileResponse: Returns the response with headers
"""
current_username = (
request.user.username if request.user.is_authenticated else "unknown"
)
file = db_rom_handler.get_rom_file_by_id(id)
if not file:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found",
)
log.info(f"User {current_username} is downloading {file_name}")
# Serve the file directly in development mode for emulatorjs
if DEV_MODE:
rom_path = f"{LIBRARY_BASE_PATH}/{file.full_path}"
return FileResponse(
path=rom_path,
filename=file_name,
headers={
"Content-Disposition": f'attachment; filename="{quote(file_name)}"',
"Content-Type": "application/octet-stream",
"Content-Length": str(file.file_size_bytes),
},
)
# Otherwise proxy through nginx
return FileRedirectResponse(
download_path=Path(f"/library/{file.full_path}"),
)

View File

@@ -62,6 +62,24 @@ class RomFile(BaseModel):
def full_path(self) -> str:
return f"{self.file_path}/{self.file_name}"
@cached_property
def file_name_no_tags(self) -> str:
from handler.filesystem import fs_rom_handler
return fs_rom_handler.get_file_name_with_no_tags(self.file_name)
@cached_property
def file_name_no_ext(self) -> str:
from handler.filesystem import fs_rom_handler
return fs_rom_handler.get_file_name_with_no_extension(self.file_name)
@cached_property
def file_extension(self) -> str:
from handler.filesystem import fs_rom_handler
return fs_rom_handler.parse_file_extension(self.file_name)
def file_name_for_download(self, rom: Rom, hidden_folder: bool = False) -> str:
# This needs a trailing slash in the path to work!
return self.full_path.replace(