last set of changes

This commit is contained in:
Georges-Antoine Assi
2026-03-07 09:56:17 -05:00
parent 76bdfb4891
commit ee8b55e6ef
16 changed files with 78 additions and 69 deletions

View File

@@ -137,7 +137,10 @@ async def add_smart_collection(
try:
parsed_filter_criteria = json.loads(filter_criteria)
except json.JSONDecodeError as e:
raise ValueError("Invalid JSON for filter_criteria field") from e
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid JSON for filter_criteria field",
) from e
cleaned_data = {
"name": name,
@@ -404,7 +407,10 @@ async def update_collection(
try:
parsed_rom_ids = json.loads(rom_ids)
except json.JSONDecodeError as e:
raise ValueError("Invalid list for rom_ids field in update collection") from e
raise HTTPException(
status_code=422,
detail="Invalid list for rom_ids field in update collection",
) from e
cleaned_data = {
"name": name if name is not None else collection.name,
@@ -502,7 +508,10 @@ async def update_smart_collection(
try:
parsed_filter_criteria = json.loads(filter_criteria)
except json.JSONDecodeError as e:
raise ValueError("Invalid JSON for filter_criteria field") from e
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid JSON for filter_criteria field",
) from e
cleaned_data = {
"name": name if name is not None else smart_collection.name,

View File

@@ -1,5 +1,6 @@
import os
from anyio import Path as AnyioPath
from fastapi import HTTPException, Request, status
from config import (
@@ -227,8 +228,9 @@ async def get_setup_library_info(request: Request):
)
# Count files and folders in the roms directory
if os.path.exists(roms_path):
items = os.listdir(roms_path)
roms_dir = AnyioPath(roms_path)
if await roms_dir.exists():
items = [entry.name async for entry in roms_dir.iterdir()]
# Filter out hidden files and system files
rom_count = len(
[

View File

@@ -100,7 +100,7 @@ router = APIRouter(
tags=["saves"],
)
SAVE_FILE_UPLOAD = File(default=None, description="Save file to upload.")
SAVE_FILE_UPLOAD = File(..., description="Save file to upload.")
SAVE_SCREENSHOT_UPLOAD = File(
default=None,
description="Screenshot file associated with this save.",
@@ -119,7 +119,7 @@ async def add_save(
overwrite: bool = False,
autocleanup: bool = False,
autocleanup_limit: int = 10,
saveFile: UploadFile | None = SAVE_FILE_UPLOAD,
saveFile: UploadFile = SAVE_FILE_UPLOAD,
screenshotFile: UploadFile | None = SAVE_SCREENSHOT_UPLOAD,
) -> SaveSchema:
"""Upload a save file for a ROM."""
@@ -131,12 +131,6 @@ async def add_save(
if not rom:
raise RomNotFoundInDatabaseException(rom_id)
if not saveFile:
log.error("No save file provided")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="No save file provided"
)
if not saveFile.filename:
log.error("Save file has no filename")
raise HTTPException(
@@ -487,7 +481,9 @@ async def update_save(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error)
if saveFile:
await fs_asset_handler.write_file(file=saveFile, path=db_save.file_path)
await fs_asset_handler.write_file(
file=saveFile, path=db_save.file_path, filename=db_save.file_name
)
db_save = db_save_handler.update_save(
db_save.id, {"file_size_bytes": saveFile.size}
)

View File

@@ -18,14 +18,14 @@ router = APIRouter(
tags=["screenshots"],
)
SCREENSHOT_FILE_UPLOAD = File(default=None, description="Screenshot file to upload.")
SCREENSHOT_FILE_UPLOAD = File(..., description="Screenshot file to upload.")
@protected_route(router.post, "", [Scope.ASSETS_WRITE])
async def add_screenshot(
request: Request,
rom_id: int,
screenshotFile: UploadFile | None = SCREENSHOT_FILE_UPLOAD,
screenshotFile: UploadFile = SCREENSHOT_FILE_UPLOAD,
) -> ScreenshotSchema:
rom = db_rom_handler.get_rom(id=rom_id)
if not rom:
@@ -38,12 +38,6 @@ async def add_screenshot(
user=request.user, platform_fs_slug=rom.platform_slug, rom_id=rom.id
)
if not screenshotFile:
log.error("No screenshot file provided")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No screenshot file provided",
)
if not screenshotFile.filename:
log.error("Screenshot file has no filename")
raise HTTPException(
@@ -51,12 +45,6 @@ async def add_screenshot(
detail="Screenshot file has no filename",
)
if not screenshotFile.filename:
log.warning("Skipping empty screenshot")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Screenshot has no filename"
)
try:
sanitized_screenshot_filename = sanitize_filename(screenshotFile.filename)
except ValueError as exc:

View File

@@ -22,7 +22,7 @@ router = APIRouter(
tags=["states"],
)
STATE_FILE_UPLOAD = File(default=None, description="State file to upload.")
STATE_FILE_UPLOAD = File(..., description="State file to upload.")
STATE_SCREENSHOT_UPLOAD = File(
default=None,
description="Screenshot file associated with this state.",
@@ -36,7 +36,7 @@ async def add_state(
request: Request,
rom_id: int,
emulator: str | None = None,
stateFile: UploadFile | None = STATE_FILE_UPLOAD,
stateFile: UploadFile = STATE_FILE_UPLOAD,
screenshotFile: UploadFile | None = STATE_SCREENSHOT_UPLOAD,
) -> StateSchema:
rom = db_rom_handler.get_rom(rom_id)
@@ -52,12 +52,6 @@ async def add_state(
emulator=emulator,
)
if not stateFile:
log.error("No state file provided")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="No state file provided"
)
if not stateFile.filename:
log.error("State file has no filename")
raise HTTPException(
@@ -228,7 +222,9 @@ async def update_state(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error)
if stateFile:
await fs_asset_handler.write_file(file=stateFile, path=db_state.file_path)
await fs_asset_handler.write_file(
file=stateFile, path=db_state.file_path, filename=db_state.file_name
)
db_state = db_state_handler.update_state(
db_state.id, {"file_size_bytes": stateFile.size}
)

View File

@@ -10,6 +10,7 @@ from pathlib import Path
from tempfile import SpooledTemporaryFile
from typing import BinaryIO
from anyio import Path as AnyioPath
from anyio import open_file
from starlette.datastructures import UploadFile
@@ -458,11 +459,13 @@ class FSHandler:
# Async thread-safe file copy
async with source_lock, dest_lock:
if not source_full_path.is_file():
source_anyio_path = AnyioPath(str(source_full_path))
if not await source_anyio_path.is_file():
raise FileNotFoundError(f"Source file not found: {source_full_path}")
# Create destination directory if needed
dest_full_path.parent.mkdir(parents=True, exist_ok=True)
dest_parent_anyio_path = AnyioPath(str(dest_full_path.parent))
await dest_parent_anyio_path.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(source_full_path), str(dest_full_path))
async def move_file_or_folder(self, source_path: str, dest_path: str) -> None:

View File

@@ -1,5 +1,7 @@
import os
from anyio import Path as AnyioPath
from config import LIBRARY_BASE_PATH
from config.config_manager import config_manager as cm
from exceptions.fs_exceptions import (
@@ -105,12 +107,13 @@ class FSPlatformsHandler(FSHandler):
# For Structure B, only include directories that have a roms subfolder
structure = self.detect_library_structure()
if structure == LibraryStructure.B:
platforms = [
platform
for platform in platforms
if os.path.exists(
filtered_platforms: list[str] = []
for platform in platforms:
roms_path = AnyioPath(
os.path.join(LIBRARY_BASE_PATH, platform, cnfg.ROMS_FOLDER_NAME)
)
]
if await roms_path.exists():
filtered_platforms.append(platform)
platforms = filtered_platforms
return self._exclude_platforms(platforms)

View File

@@ -15,7 +15,7 @@ from models.collection import Collection
from models.rom import Rom
from tasks.scheduled.convert_images_to_webp import ImageConverter
from utils.context import ctx_httpx_client
from utils.validation import ValidationError, validate_url_for_http_request
from utils.validation import validate_url_for_http_request
from .base_handler import CoverSize, FSHandler

View File

@@ -14,6 +14,7 @@ from typing import IO, Any, Final, Literal, TypedDict, cast
import magic
import zipfile_inflate64 # trunk-ignore(ruff/F401): Patches zipfile to support Enhanced Deflate
from anyio import Path as AnyioPath
from config import LIBRARY_BASE_PATH
from config.config_manager import config_manager as cm
@@ -448,7 +449,7 @@ class FSRomsHandler(FSHandler):
rom_ra_h = ""
# Check if rom is a multi-part rom
if os.path.isdir(f"{abs_fs_path}/{rom.fs_name}"):
if await AnyioPath(f"{abs_fs_path}/{rom.fs_name}").is_dir():
# Calculate the RA hash if the platform has a slug that matches a known RA slug
if calculate_hashes:
ra_platform = meta_ra_handler.get_platform(rom.platform_slug)

View File

@@ -5,7 +5,7 @@ import re
import unicodedata
from functools import lru_cache
from pathlib import Path
from typing import Final, NotRequired, TypedDict
from typing import Final, Mapping, NotRequired, TypedDict
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
from strsimpy.jaro_winkler import JaroWinkler
@@ -275,7 +275,9 @@ class MetadataHandler(abc.ABC):
return search_term
def _mask_sensitive_values(self, values: dict[str, str]) -> dict[str, str]:
def _mask_sensitive_values(
self, values: Mapping[str, str | None]
) -> dict[str, str]:
"""
Mask sensitive values (headers or params), leaving only the first 2 and last 2 characters of the token.
"""

View File

@@ -6,6 +6,7 @@ from datetime import datetime
from typing import NotRequired, TypedDict
import pydash
from anyio import Path as AnyioPath
from adapters.services.retroachievements import RetroAchievementsService
from adapters.services.retroachievements_types import (
@@ -169,7 +170,8 @@ class RAHandler(MetadataHandler):
return REFRESH_RETROACHIEVEMENTS_CACHE_DAYS + 1
full_path = fs_resource_handler.validate_path(file_path)
return int((time.time() - os.path.getmtime(full_path)) / (24 * 3600))
file_stat = await AnyioPath(str(full_path)).stat()
return int((time.time() - file_stat.st_mtime) / (24 * 3600))
async def _search_rom(self, rom: Rom, ra_hash: str) -> RAGameListItem | None:
if not rom.platform.ra_id:

View File

@@ -2,6 +2,8 @@ import os
import shutil
from dataclasses import dataclass
from anyio import Path as AnyioPath
from config import RESOURCES_BASE_PATH
from handler.database import db_platform_handler, db_rom_handler
from logger.logger import log
@@ -57,7 +59,8 @@ class CleanupOrphanedResourcesTask(Task):
cleanup_stats = CleanupStats()
roms_resources_path = os.path.join(RESOURCES_BASE_PATH, "roms")
if not os.path.exists(roms_resources_path):
roms_resources_dir = AnyioPath(roms_resources_path)
if not await roms_resources_dir.exists():
cleanup_stats.update()
log.info("Resources path does not exist, skipping cleanup")
return cleanup_stats.to_dict()
@@ -82,18 +85,19 @@ class CleanupOrphanedResourcesTask(Task):
# Count total platforms and ROMs for progress tracking
platform_dirs: set[int] = {
int(d)
for d in os.listdir(roms_resources_path)
if os.path.isdir(os.path.join(roms_resources_path, d))
int(entry.name)
async for entry in roms_resources_dir.iterdir()
if await entry.is_dir()
}
rom_dirs_by_platform: dict[int, set[int]] = {}
for platform_dir in platform_dirs:
platform_path = os.path.join(roms_resources_path, str(platform_dir))
platform_dir_path = AnyioPath(platform_path)
rom_dirs_by_platform[platform_dir] = {
int(d)
for d in os.listdir(platform_path)
if os.path.isdir(os.path.join(platform_path, d))
int(entry.name)
async for entry in platform_dir_path.iterdir()
if await entry.is_dir()
}
cleanup_stats.update(

View File

@@ -6,7 +6,7 @@ export type Body_add_save_api_saves_post = {
/**
* Save file to upload.
*/
saveFile?: (Blob | null);
saveFile: Blob;
/**
* Screenshot file associated with this save.
*/

View File

@@ -6,6 +6,6 @@ export type Body_add_screenshot_api_screenshots_post = {
/**
* Screenshot file to upload.
*/
screenshotFile?: (Blob | null);
screenshotFile: Blob;
};

View File

@@ -6,7 +6,7 @@ export type Body_add_state_api_states_post = {
/**
* State file to upload.
*/
stateFile?: (Blob | null);
stateFile: Blob;
/**
* Screenshot file associated with this state.
*/

View File

@@ -383,20 +383,23 @@ async function updateRom({
removeCover?: boolean;
unmatch?: boolean;
}) {
const toFormIdValue = (value: number | string | null | undefined): string =>
value === null || value === undefined ? "" : String(value);
const fields: FormInputField<UpdateRomInput>[] = [
["name", rom.name],
["fs_name", rom.fs_name],
["summary", rom.summary],
["igdb_id", rom.igdb_id?.toString()],
["sgdb_id", rom.sgdb_id?.toString()],
["moby_id", rom.moby_id?.toString()],
["ss_id", rom.ss_id?.toString()],
["launchbox_id", rom.launchbox_id?.toString()],
["ra_id", rom.ra_id?.toString()],
["flashpoint_id", rom.flashpoint_id?.toString()],
["hasheous_id", rom.hasheous_id?.toString()],
["tgdb_id", rom.tgdb_id?.toString()],
["hltb_id", rom.hltb_id?.toString()],
["igdb_id", toFormIdValue(rom.igdb_id)],
["sgdb_id", toFormIdValue(rom.sgdb_id)],
["moby_id", toFormIdValue(rom.moby_id)],
["ss_id", toFormIdValue(rom.ss_id)],
["launchbox_id", toFormIdValue(rom.launchbox_id)],
["ra_id", toFormIdValue(rom.ra_id)],
["flashpoint_id", toFormIdValue(rom.flashpoint_id)],
["hasheous_id", toFormIdValue(rom.hasheous_id)],
["tgdb_id", toFormIdValue(rom.tgdb_id)],
["hltb_id", toFormIdValue(rom.hltb_id)],
];
if (rom.manual_metadata) {