From 1d2c9e7d057e0392efd7ccb96534d11f3ab5eba2 Mon Sep 17 00:00:00 2001 From: Zurdi Date: Sun, 14 Jan 2024 01:12:07 +0100 Subject: [PATCH] standarized GET endpoints for roms and platforms --- backend/decorators/{oauth.py => auth.py} | 0 backend/decorators/database.py | 23 +++ backend/endpoints/assets.py | 12 +- backend/endpoints/config.py | 33 +++++ backend/endpoints/identity.py | 6 +- backend/endpoints/platform.py | 78 +++++----- backend/endpoints/rom.py | 136 +++++++++--------- backend/endpoints/search.py | 2 +- backend/endpoints/sockets/scan.py | 12 +- backend/endpoints/tasks.py | 2 +- backend/endpoints/webrcade.py | 2 +- backend/handler/__init__.py | 26 ++-- backend/handler/db_handler/__init__.py | 0 .../handler/{ => db_handler}/db_handler.py | 88 +----------- .../db_handler/db_platforms_handler.py | 69 +++++++++ backend/handler/db_handler/db_roms_handler.py | 76 ++++++++++ ...assets_handler.py => fs_assets_handler.py} | 2 +- ...rms_handler.py => fs_platforms_handler.py} | 2 +- ...ces_handler.py => fs_resources_handler.py} | 2 +- .../{roms_handler.py => fs_roms_handler.py} | 2 +- backend/handler/scan_handler.py | 28 ++-- backend/main.py | 3 +- backend/models/platform.py | 4 +- backend/models/rom.py | 6 +- backend/utils/tests/test_fs.py | 2 +- 25 files changed, 368 insertions(+), 248 deletions(-) rename backend/decorators/{oauth.py => auth.py} (100%) create mode 100644 backend/decorators/database.py create mode 100644 backend/endpoints/config.py create mode 100644 backend/handler/db_handler/__init__.py rename backend/handler/{ => db_handler}/db_handler.py (73%) create mode 100644 backend/handler/db_handler/db_platforms_handler.py create mode 100644 backend/handler/db_handler/db_roms_handler.py rename backend/handler/fs_handler/{assets_handler.py => fs_assets_handler.py} (99%) rename backend/handler/fs_handler/{platforms_handler.py => fs_platforms_handler.py} (96%) rename backend/handler/fs_handler/{resources_handler.py => fs_resources_handler.py} (99%) rename backend/handler/fs_handler/{roms_handler.py => fs_roms_handler.py} (99%) diff --git a/backend/decorators/oauth.py b/backend/decorators/auth.py similarity index 100% rename from backend/decorators/oauth.py rename to backend/decorators/auth.py diff --git a/backend/decorators/database.py b/backend/decorators/database.py new file mode 100644 index 000000000..141d7222d --- /dev/null +++ b/backend/decorators/database.py @@ -0,0 +1,23 @@ +import functools + +from fastapi import HTTPException, status +from logger.logger import log +from sqlalchemy.exc import ProgrammingError + + +def begin_session(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if hasattr(kwargs, "session"): + return func(*args, **kwargs) + + try: + with args[0].session.begin() as s: + return func(*args, **kwargs, session=s) + except ProgrammingError as e: + log.critical(str(e)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) + ) + + return wrapper diff --git a/backend/endpoints/assets.py b/backend/endpoints/assets.py index 98e2ccdfd..c050694e6 100644 --- a/backend/endpoints/assets.py +++ b/backend/endpoints/assets.py @@ -1,7 +1,7 @@ from pathlib import Path from config.config_manager import config_manager as cm -from decorators.oauth import protected_route +from decorators.auth import protected_route from endpoints.responses.assets import ( SaveSchema, StateSchema, @@ -9,7 +9,7 @@ from endpoints.responses.assets import ( UploadedStatesResponse, ) from fastapi import APIRouter, File, HTTPException, Request, UploadFile, status -from handler import dbh, romh +from handler import dbh, fsromh from handler.scan_handler import scan_save, scan_state from logger.logger import log @@ -42,7 +42,7 @@ def upload_saves( detail="No saves were uploaded", ) - saves_path = romh.build_upload_file_path( + saves_path = fsromh.build_upload_file_path( rom.platform.fs_slug, folder=cm.config.SAVES_FOLDER_NAME ) @@ -90,7 +90,7 @@ async def delete_saves(request: Request) -> list[SaveSchema]: log.info(f"Deleting {save.file_name} from filesystem") try: - romh.remove_file(file_name=save.file_name, file_path=save.file_path) + fsromh.remove_file(file_name=save.file_name, file_path=save.file_path) except FileNotFoundError: error = f"Save file {save.file_name} not found for platform {save.platform_slug}" log.error(error) @@ -113,7 +113,7 @@ def upload_states( detail="No states were uploaded", ) - states_path = romh.build_upload_file_path( + states_path = fsromh.build_upload_file_path( rom.platform.fs_slug, folder=cm.config.STATES_FOLDER_NAME ) @@ -160,7 +160,7 @@ async def delete_states(request: Request) -> list[StateSchema]: if delete_from_fs: log.info(f"Deleting {state.file_name} from filesystem") try: - romh.remove_file(file_name=state.file_name, file_path=state.file_path) + fsromh.remove_file(file_name=state.file_name, file_path=state.file_path) except FileNotFoundError: error = f"Save file {state.file_name} not found for platform {state.platform_slug}" log.error(error) diff --git a/backend/endpoints/config.py b/backend/endpoints/config.py new file mode 100644 index 000000000..643b5283a --- /dev/null +++ b/backend/endpoints/config.py @@ -0,0 +1,33 @@ +from config.config_manager import config_manager as cm +from decorators.auth import protected_route +from endpoints.responses import MessageResponse +from fastapi import APIRouter, Request + +router = APIRouter() + + +@protected_route(router.put, "/config/system/platforms", ["platforms.write"]) +async def add_platform_binding(request: Request) -> MessageResponse: + """Add platform binding to the configuration""" + + data = await request.form() + + fs_slug = data.get("fs_slug") + slug = data.get("slug") + + cm.add_binding(fs_slug, slug) + + return {"msg": f"{fs_slug} binded to: {slug} successfully!"} + + +@protected_route(router.patch, "/config/system/platforms", ["platforms.write"]) +async def delete_platform_binding(request: Request) -> MessageResponse: + """Delete platform binding from the configuration""" + + data = await request.form() + + fs_slug = data.get("fs_slug") + + cm.remove_binding(fs_slug) + + return {"msg": f"{fs_slug} bind removed successfully!"} diff --git a/backend/endpoints/identity.py b/backend/endpoints/identity.py index 9cfe8b17f..9306154e2 100644 --- a/backend/endpoints/identity.py +++ b/backend/endpoints/identity.py @@ -2,14 +2,14 @@ import secrets from typing import Annotated from config import ROMM_AUTH_ENABLED -from decorators.oauth import protected_route +from decorators.auth import protected_route from endpoints.forms.identity import UserForm from endpoints.responses import MessageResponse from endpoints.responses.identity import UserSchema from exceptions.auth_exceptions import AuthCredentialsException, DisabledException from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.security.http import HTTPBasic -from handler import authh, dbh, resourceh +from handler import authh, dbh, fsresourceh from handler.redis_handler import cache from models.user import Role, User @@ -207,7 +207,7 @@ def update_user( cleaned_data["enabled"] = form_data.enabled # type: ignore[assignment] if form_data.avatar is not None: - cleaned_data["avatar_path"], avatar_user_path = resourceh.build_avatar_path( + cleaned_data["avatar_path"], avatar_user_path = fsresourceh.build_avatar_path( form_data.avatar.filename, form_data.username ) file_location = f"{avatar_user_path}/{form_data.avatar.filename}" diff --git a/backend/endpoints/platform.py b/backend/endpoints/platform.py index d01d448f0..9db76e7e6 100644 --- a/backend/endpoints/platform.py +++ b/backend/endpoints/platform.py @@ -1,80 +1,82 @@ -from config.config_manager import config_manager as cm -from decorators.oauth import protected_route +from decorators.auth import protected_route from endpoints.responses import MessageResponse from endpoints.responses.platform import PlatformSchema from fastapi import APIRouter, HTTPException, Request, status -from handler import dbh +from handler import dbplatformh from logger.logger import log router = APIRouter() +@protected_route(router.post, "/platforms", ["platforms.write"]) +def add_platform(request: Request) -> MessageResponse: + """Create platform endpoint + + Args: + request (Request): Fastapi Request object + + Returns: + MessageResponse: Standard message response + """ + + pass + + @protected_route(router.get, "/platforms", ["platforms.read"]) def get_platforms(request: Request) -> list[PlatformSchema]: """Get platforms endpoint Args: request (Request): Fastapi Request object + id (int, optional): Platform id. Defaults to None. Returns: - list[PlatformSchema]: All platforms in the database + list[PlatformSchema]: List of platforms """ - return dbh.get_platform() + return dbplatformh.get_platforms() @protected_route(router.get, "/platforms/{id}", ["platforms.read"]) -def get_platforms(request: Request, id: int = None) -> PlatformSchema: - """Get platform endpoint +def get_platforms(request: Request, id: int) -> PlatformSchema: + """Get platforms endpoint + + Args: + request (Request): Fastapi Request object + id (int, optional): Platform id. Defaults to None. + + Returns: + PlatformSchema: Platform + """ + + return dbplatformh.get_platforms(id) + + +@protected_route(router.put, "/platforms/{id}", ["platforms.write"]) +def update_platform(request: Request, form) -> MessageResponse: + """Update platform endpoint Args: request (Request): Fastapi Request object Returns: - PlatformSchema: All platforms in the database + MessageResponse: Standard message response """ - return dbh.get_platform(id) + pass @protected_route(router.delete, "/platforms/{id}", ["platforms.write"]) def delete_platforms(request: Request, id: int) -> MessageResponse: """Detele platform from database [and filesystem]""" - platform = dbh.get_platform(id) + platform = dbplatformh.get_platforms(id) if not platform: error = f"Platform {platform.name} - [{platform.fs_slug}] not found" log.error(error) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error) log.info(f"Deleting {platform.name} [{platform.fs_slug}] from database") - dbh.delete_platform(platform.id) + dbplatformh.delete_platform(id) return {"msg": f"{platform.name} - [{platform.fs_slug}] deleted successfully!"} - - -@protected_route(router.put, "/config/system/platforms", ["platforms.write"]) -async def add_platform_binding(request: Request) -> MessageResponse: - """Add platform binding to the configuration""" - - data = await request.form() - - fs_slug = data.get("fs_slug") - slug = data.get("slug") - - cm.add_binding(fs_slug, slug) - - return {"msg": f"{fs_slug} binded to: {slug} successfully!"} - - -@protected_route(router.patch, "/config/system/platforms", ["platforms.write"]) -async def delete_platform_binding(request: Request) -> MessageResponse: - """Delete platform binding from the configuration""" - - data = await request.form() - - fs_slug = data.get("fs_slug") - - cm.remove_binding(fs_slug) - - return {"msg": f"{fs_slug} bind removed successfully!"} diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 5aae9e63c..09451eb52 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -4,7 +4,7 @@ from stat import S_IFREG from typing import Annotated, Optional from config import LIBRARY_BASE_PATH -from decorators.oauth import protected_route +from decorators.auth import protected_route from endpoints.responses import MessageResponse from endpoints.responses.rom import ( CustomStreamingResponse, @@ -17,7 +17,7 @@ from fastapi import APIRouter, File, HTTPException, Query, Request, UploadFile, from fastapi.responses import FileResponse from fastapi_pagination.cursor import CursorPage, CursorParams from fastapi_pagination.ext.sqlalchemy import paginate -from handler import asseth, dbh, romh +from handler import dbplatformh, dbromh, fsasseth, fsresourceh, fsromh from logger.logger import log from models import Rom from stream_zip import ZIP_64, stream_zip # type: ignore[import] @@ -25,8 +25,49 @@ from stream_zip import ZIP_64, stream_zip # type: ignore[import] router = APIRouter() +@protected_route(router.post, "/roms", ["roms.write"]) +def add_rom(request: Request) -> MessageResponse: + """Create rom endpoint + + Args + request (Request): Fastapi Request object + + Returns: + MessageResponse: Standard message response + + """ + + pass + + +@protected_route(router.get, "/roms", ["roms.read"]) +def get_roms( + request: Request, + platform_id: int = None, + size: int = 60, + cursor: str = "", + search_term: str = "", + order_by: str = "name", + order_dir: str = "asc", +) -> CursorPage[RomSchema]: + """Get roms endpoint + + Args: + request (Request): Fastapi Request object + id (int, optional): Rom internal id + + Returns: + EnhancedRomSchema: Rom stored in RomM's database + """ + + with dbromh.session.begin() as session: + cursor_params = CursorParams(size=size, cursor=cursor) + qq = dbromh.get_roms(platform_id, search_term, order_by, order_dir) + return paginate(session, qq, cursor_params) + + @protected_route(router.get, "/roms/{id}", ["roms.read"]) -def rom(request: Request, id: int) -> EnhancedRomSchema: +def get_rom(request: Request, id: int) -> EnhancedRomSchema: """Get rom endpoint Args: @@ -37,61 +78,12 @@ def rom(request: Request, id: int) -> EnhancedRomSchema: EnhancedRomSchema: Rom stored in RomM's database """ - return dbh.get_rom(id) - - -@protected_route(router.get, "/roms-recent", ["roms.read"]) -def recent_roms(request: Request) -> list[RomSchema]: - """Get recent roms endpoint - - Args: - request (Request): Fastapi Request object - - Returns: - list[RomSchema]: List of the last 15 stored roms in RomM's database - """ - - return dbh.get_recent_roms() - - -@protected_route(router.get, "/platforms/{platform_slug}/roms", ["roms.read"]) -def roms( - request: Request, - platform_slug: str, - size: int = 60, - cursor: str = "", - search_term: str = "", -) -> CursorPage[RomSchema]: - """Get all roms for a specific platform endpoint (paginated) - - Args: - request (Request): Fastapi Request object - platform_slug (str): Platform slug - size (int, optional): Size of each page. Defaults to 60. - cursor (str, optional): Cursor string. Defaults to "". - search_term (str, optional): Filter to search roms. Defaults to "". - - Returns: - CursorPage[RomSchema]: Paged list of roms - """ - - with dbh.session.begin() as session: - cursor_params = CursorParams(size=size, cursor=cursor) - qq = dbh.get_roms(platform_slug) - - if search_term: - return paginate( - session, - qq.filter(Rom.file_name.ilike(f"%{search_term}%")), - cursor_params, - ) - - return paginate(session, qq, cursor_params) + return dbromh.get_roms(id) @protected_route(router.put, "/roms/upload", ["roms.write"]) def upload_roms( - request: Request, platform_slug: str, roms: list[UploadFile] = File(...) + request: Request, platform_id: str, roms: list[UploadFile] = File(...) ) -> UploadRomResponse: """Upload roms endpoint (one or more at the same time) @@ -107,7 +99,7 @@ def upload_roms( UploadRomResponse: Standard message response """ - platform_fs_slug = dbh.get_platform(platform_slug).fs_slug + platform_fs_slug = dbromh.get_platforms(platform_id).fs_slug log.info(f"Uploading roms to {platform_fs_slug}") if roms is None: log.error("No roms were uploaded") @@ -116,13 +108,13 @@ def upload_roms( detail="No roms were uploaded", ) - roms_path = romh.build_upload_file_path(platform_fs_slug) + roms_path = fsromh.build_upload_file_path(platform_fs_slug) uploaded_roms = [] skipped_roms = [] for rom in roms: - if romh.file_exists(roms_path, rom.filename): + if fsromh.file_exists(roms_path, rom.filename): log.warning(f" - Skipping {rom.filename} since the file already exists") skipped_roms.append(rom.filename) continue @@ -163,7 +155,7 @@ def download_rom( CustomStreamingResponse: Streams a file for multi-part roms """ - rom = dbh.get_rom(id) + rom = dbromh.get_roms(id) rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}" if not rom.multi: @@ -219,8 +211,8 @@ async def update_rom( data = await request.form() - db_rom = dbh.get_rom(id) - platform_fs_slug = dbh.get_platform(db_rom.platform_slug).fs_slug + db_rom = dbromh.get_roms(id) + platform_fs_slug = dbplatformh.get_platforms(db_rom.platform_id).fs_slug cleaned_data = {} cleaned_data["igdb_id"] = data.get("igdb_id", db_rom.igdb_id) or None @@ -242,7 +234,7 @@ async def update_rom( try: if db_rom.file_name != fs_safe_file_name: - romh.rename_file( + fsromh.rename_file( old_name=db_rom.file_name, new_name=fs_safe_file_name, file_path=db_rom.file_path, @@ -254,11 +246,11 @@ async def update_rom( ) cleaned_data["file_name"] = fs_safe_file_name - cleaned_data["file_name_no_tags"] = romh.get_file_name_with_no_tags( + cleaned_data["file_name_no_tags"] = fsromh.get_file_name_with_no_tags( fs_safe_file_name ) cleaned_data.update( - asseth.get_rom_cover( + fsresourceh.get_rom_cover( overwrite=True, fs_slug=platform_fs_slug, rom_name=cleaned_data["name"], @@ -267,7 +259,7 @@ async def update_rom( ) cleaned_data.update( - asseth.get_rom_screenshots( + fsasseth.get_rom_screenshots( fs_slug=platform_fs_slug, rom_name=cleaned_data["name"], url_screenshots=cleaned_data.get("url_screenshots", []), @@ -276,7 +268,7 @@ async def update_rom( if artwork is not None: file_ext = artwork.filename.split(".")[-1] - path_cover_l, path_cover_s, artwork_path = asseth.build_artwork_path( + path_cover_l, path_cover_s, artwork_path = fsresourceh.build_artwork_path( cleaned_data["name"], platform_fs_slug, file_ext ) @@ -292,12 +284,12 @@ async def update_rom( with open(file_location_l, "wb+") as artwork_l: artwork_l.write(artwork_file) - dbh.update_rom(id, cleaned_data) + dbromh.update_rom(id, cleaned_data) - return dbh.get_rom(id) + return dbromh.get_roms(id) -def _delete_single_rom(rom_id: int, delete_from_fs: bool = False) -> Rom: +def _delete_single_rom(id: int, delete_from_fs: bool = False) -> Rom: """Auxiliar function to delete one single rom at once Args: @@ -312,19 +304,19 @@ def _delete_single_rom(rom_id: int, delete_from_fs: bool = False) -> Rom: Rom: Rom object """ - rom = dbh.get_rom(rom_id) + rom = dbromh.get_rosm(id) if not rom: - error = f"Rom with id {rom_id} not found" + error = f"Rom with id {id} not found" log.error(error) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error) log.info(f"Deleting {rom.file_name} from database") - dbh.delete_rom(rom_id) + dbromh.delete_rom(id) if delete_from_fs: log.info(f"Deleting {rom.file_name} from filesystem") try: - romh.remove_file(file_name=rom.file_name, file_path=rom.file_path) + fsromh.remove_file(file_name=rom.file_name, file_path=rom.file_path) except FileNotFoundError: error = ( f"Rom file {rom.file_name} not found for platform {rom.platform_slug}" diff --git a/backend/endpoints/search.py b/backend/endpoints/search.py index 9300e7f1f..bc58fbd43 100644 --- a/backend/endpoints/search.py +++ b/backend/endpoints/search.py @@ -1,5 +1,5 @@ import emoji -from decorators.oauth import protected_route +from decorators.auth import protected_route from endpoints.responses.search import RomSearchResponse from fastapi import APIRouter, Request from handler import dbh, igdbh diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index adfbdf267..f81a82992 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -7,7 +7,7 @@ from exceptions.fs_exceptions import ( FolderStructureNotMatchException, RomsNotFoundException, ) -from handler import asseth, dbh, platformh, resourceh, romh, socketh +from handler import fsasseth, dbh, fsplatformh, fsresourceh, fsromh, socketh from handler.redis_handler import high_prio_queue, redis_url from handler.scan_handler import ( scan_platform, @@ -43,7 +43,7 @@ async def scan_platforms( # Scanning file system try: - fs_platforms: list[str] = platformh.get_platforms() + fs_platforms: list[str] = fsplatformh.get_platforms() except FolderStructureNotMatchException as e: log.error(e) await sm.emit("scan:done_ko", e.message) @@ -71,7 +71,7 @@ async def scan_platforms( # Scanning roms try: - fs_roms = romh.get_roms(platform.fs_slug) + fs_roms = fsromh.get_roms(platform.fs_slug) except RomsNotFoundException as e: log.error(e) continue @@ -106,7 +106,7 @@ async def scan_platforms( }, ) - fs_assets = asseth.get_assets(platform.fs_slug) + fs_assets = fsasseth.get_assets(platform.fs_slug) # Scanning saves log.info(f"\t · {len(fs_assets['saves'])} saves found") @@ -190,7 +190,7 @@ async def scan_platforms( dbh.purge_roms(platform.id, [rom["file_name"] for rom in fs_roms]) # Scanning screenshots outside platform folders - fs_screenshots = asseth.get_screenshots() + fs_screenshots = fsasseth.get_screenshots() log.info("Screenshots") log.info(f" · {len(fs_screenshots)} screenshots found") for fs_platform, fs_screenshot_filename in fs_screenshots: @@ -231,7 +231,7 @@ async def scan_handler(_sid: str, options: dict): """ log.info(emoji.emojize(":magnifying_glass_tilted_right: Scanning ")) - resourceh.store_default_resources() + fsresourceh.store_default_resources() platform_slugs = options.get("platforms", []) complete_rescan = options.get("completeRescan", False) diff --git a/backend/endpoints/tasks.py b/backend/endpoints/tasks.py index d22cfc72f..a7d1d2f4c 100644 --- a/backend/endpoints/tasks.py +++ b/backend/endpoints/tasks.py @@ -1,4 +1,4 @@ -from decorators.oauth import protected_route +from decorators.auth import protected_route from endpoints.responses import MessageResponse from fastapi import APIRouter, Request from tasks.update_mame_xml import update_mame_xml_task diff --git a/backend/endpoints/webrcade.py b/backend/endpoints/webrcade.py index c0e2fad8f..3be518f80 100644 --- a/backend/endpoints/webrcade.py +++ b/backend/endpoints/webrcade.py @@ -1,5 +1,5 @@ from config import ROMM_HOST -from decorators.oauth import protected_route +from decorators.auth import protected_route from endpoints.responses.webrcade import ( WEBRCADE_SLUG_TO_TYPE_MAP, WEBRCADE_SUPPORTED_PLATFORM_SLUGS, diff --git a/backend/handler/__init__.py b/backend/handler/__init__.py index a835beee3..f83de85e1 100644 --- a/backend/handler/__init__.py +++ b/backend/handler/__init__.py @@ -1,9 +1,10 @@ from handler.auth_handler.auth_handler import AuthHandler, OAuthHandler -from handler.db_handler import DBHandler -from handler.fs_handler.assets_handler import AssetsHandler -from handler.fs_handler.platforms_handler import PlatformsHandler -from handler.fs_handler.resources_handler import ResourceHandler -from handler.fs_handler.roms_handler import RomsHandler +from handler.db_handler.db_platforms_handler import DBPlatformsHandler +from handler.db_handler.db_roms_handler import DBRomsHandler +from handler.fs_handler.fs_assets_handler import FSAssetsHandler +from handler.fs_handler.fs_platforms_handler import FSPlatformsHandler +from handler.fs_handler.fs_resources_handler import FSResourceHandler +from handler.fs_handler.fs_roms_handler import FSRomsHandler from handler.gh_handler import GHHandler from handler.igdb_handler import IGDBHandler from handler.sgdb_handler import SGDBHandler @@ -11,12 +12,17 @@ from handler.socket_handler import SocketHandler igdbh = IGDBHandler() sgdbh = SGDBHandler() -dbh = DBHandler() ghh = GHHandler() authh = AuthHandler() oauthh = OAuthHandler() socketh = SocketHandler() -platformh = PlatformsHandler() -romh = RomsHandler() -asseth = AssetsHandler() -resourceh = ResourceHandler() +dbplatformh = DBPlatformsHandler() +dbromh = DBRomsHandler() +fsplatformh = FSPlatformsHandler() +fsromh = FSRomsHandler() +fsasseth = FSAssetsHandler() +fsresourceh = FSResourceHandler() + + +from handler.db_handler.db_handler import DBHandler +dbh = DBHandler() diff --git a/backend/handler/db_handler/__init__.py b/backend/handler/db_handler/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/handler/db_handler.py b/backend/handler/db_handler/db_handler.py similarity index 73% rename from backend/handler/db_handler.py rename to backend/handler/db_handler/db_handler.py index a5e6366c6..32b8e40ca 100644 --- a/backend/handler/db_handler.py +++ b/backend/handler/db_handler/db_handler.py @@ -1,11 +1,7 @@ -import functools - from config.config_manager import ConfigManager -from fastapi import HTTPException, status -from logger.logger import log -from models import Platform, Role, Rom, Save, Screenshot, State, User -from sqlalchemy import and_, create_engine, delete, func, or_, select, update -from sqlalchemy.exc import ProgrammingError +from decorators.database import begin_session +from models import Role, Rom, Save, Screenshot, State, User +from sqlalchemy import and_, create_engine, delete, func, select, update from sqlalchemy.orm import Session, sessionmaker @@ -14,84 +10,6 @@ class DBHandler: self.engine = create_engine(ConfigManager.get_db_engine(), pool_pre_ping=True) self.session = sessionmaker(bind=self.engine, expire_on_commit=False) - @staticmethod - def begin_session(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - if hasattr(kwargs, "session"): - return func(*args, session=kwargs.get("session")) - - try: - with args[0].session.begin() as s: - return func(*args, session=s) - except ProgrammingError as e: - log.critical(str(e)) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) - ) - - return wrapper - - # ========= Platforms ========= - @begin_session - def add_platform(self, platform: Platform, session: Session = None): - return session.merge(platform) - - @begin_session - def get_platform(self, id: int = None, session: Session = None): - return ( - session.scalars(select(Platform).order_by(Platform.slug.asc())) - .unique() - .all() - if not id - else session.get(Platform, id) - ) - - @begin_session - def delete_platform(self, slug: str, session: Session = None): - # Remove all roms from that platforms first - session.execute( - delete(Rom) - .where(Rom.platform_slug == slug) - .execution_options(synchronize_session="evaluate") - ) - return session.execute( - delete(Platform) - .where(Platform.slug == slug) - .execution_options(synchronize_session="evaluate") - ) - - @begin_session - def get_rom_count(self, platform_id: int, session: Session = None): - return session.scalar( - select(func.count()).select_from(Rom).filter_by(platform_id=platform_id) - ) - - @begin_session - def purge_platforms(self, platforms: list[str], session: Session = None): - session.execute( - delete(Save) - .where(Save.platform_slug.not_in(platforms)) - .execution_options(synchronize_session="evaluate") - ) - session.execute( - delete(State) - .where(State.platform_slug.not_in(platforms)) - .execution_options(synchronize_session="evaluate") - ) - return session.execute( - delete(Platform) - .where(or_(Platform.fs_slug.not_in(platforms), Platform.slug.is_(None))) - .where( - select(func.count()) - .select_from(Rom) - .filter_by(platform_slug=Platform.slug) - .as_scalar() - == 0 - ) - .execution_options(synchronize_session="fetch") - ) - # ========= Roms ========= @begin_session def add_rom(self, rom: Rom, session: Session = None): diff --git a/backend/handler/db_handler/db_platforms_handler.py b/backend/handler/db_handler/db_platforms_handler.py new file mode 100644 index 000000000..32e2c9538 --- /dev/null +++ b/backend/handler/db_handler/db_platforms_handler.py @@ -0,0 +1,69 @@ +from models import Platform, Rom, Save, State +from sqlalchemy import delete, func, or_, select +from sqlalchemy.orm import Session + +from decorators.database import begin_session +from handler.db_handler.db_handler import DBHandler + + +class DBPlatformsHandler(DBHandler): + @begin_session + def add_platform(self, platform: Platform, session: Session = None): + return session.merge(platform) + + @begin_session + def get_platforms(self, id: int = None, session: Session = None): + return ( + session.get(Platform, id) + if id + else ( + session.scalars(select(Platform).order_by(Platform.name.asc())) + .unique() + .all() + ) + ) + + @begin_session + def delete_platform(self, slug: str, session: Session = None): + # Remove all roms from that platforms first + session.execute( + delete(Rom) + .where(Rom.platform_slug == slug) + .execution_options(synchronize_session="evaluate") + ) + return session.execute( + delete(Platform) + .where(Platform.slug == slug) + .execution_options(synchronize_session="evaluate") + ) + + @begin_session + def get_rom_count(self, platform_id: int, session: Session = None): + return session.scalar( + select(func.count()).select_from(Rom).filter_by(platform_id=platform_id) + ) + + @begin_session + def purge_platforms(self, platforms: list[str], session: Session = None): + session.execute( + delete(Save) + .where(Save.platform_slug.not_in(platforms)) + .execution_options(synchronize_session="evaluate") + ) + session.execute( + delete(State) + .where(State.platform_slug.not_in(platforms)) + .execution_options(synchronize_session="evaluate") + ) + return session.execute( + delete(Platform) + .where(or_(Platform.fs_slug.not_in(platforms), Platform.slug.is_(None))) + .where( + select(func.count()) + .select_from(Rom) + .filter_by(platform_slug=Platform.slug) + .as_scalar() + == 0 + ) + .execution_options(synchronize_session="fetch") + ) diff --git a/backend/handler/db_handler/db_roms_handler.py b/backend/handler/db_handler/db_roms_handler.py new file mode 100644 index 000000000..e3e1e6af7 --- /dev/null +++ b/backend/handler/db_handler/db_roms_handler.py @@ -0,0 +1,76 @@ +from decorators.database import begin_session +from handler.db_handler.db_handler import DBHandler +from models import Rom +from sqlalchemy import and_, delete, func, select, update +from sqlalchemy.orm import Session + + +class DBRomsHandler(DBHandler): + @staticmethod + def _filter(data, platform_id, search_term): + if platform_id: + data = data.filter_by(platform_id=platform_id) + if search_term: + data = data.filter(Rom.file_name.ilike(f"%{search_term}%")) + return data + + @staticmethod + def _order(data, order_by, order_dir): + if order_by == "name": + _column = func.lower(Rom.name) + elif order_by == "id": + _column = Rom.id + else: + _column = func.lower(Rom.name) + + if order_dir == "asc": + return data.order_by(_column.asc()) + elif order_dir == "desc": + return data.order_by(_column.desc()) + else: + return data.order_by(_column.asc()) + + @begin_session + def get_roms( + self, + id: int = None, + platform_id: int = None, + search_term: str = "", + order_by: str = "name", + order_dir: str = "asc", + session: Session = None, + ): + return ( + session.get(Rom, id) + if id + else self._order( + self._filter(select(Rom), platform_id, search_term), + order_by, + order_dir, + ) + ) + + @begin_session + def update_rom(self, id: int, data: dict, session: Session = None): + return session.execute( + update(Rom) + .where(Rom.id == id) + .values(**data) + .execution_options(synchronize_session="evaluate") + ) + + @begin_session + def delete_rom(self, id: int, session: Session = None): + return session.execute( + delete(Rom) + .where(Rom.id == id) + .execution_options(synchronize_session="evaluate") + ) + + @begin_session + def purge_roms(self, platform_id: int, roms: list[str], session: Session = None): + return session.execute( + delete(Rom) + .where(and_(Rom.platform_id == platform_id, Rom.file_name.not_in(roms))) + .execution_options(synchronize_session="evaluate") + ) diff --git a/backend/handler/fs_handler/assets_handler.py b/backend/handler/fs_handler/fs_assets_handler.py similarity index 99% rename from backend/handler/fs_handler/assets_handler.py rename to backend/handler/fs_handler/fs_assets_handler.py index fa7f09164..a844b340c 100644 --- a/backend/handler/fs_handler/assets_handler.py +++ b/backend/handler/fs_handler/fs_assets_handler.py @@ -10,7 +10,7 @@ from handler.fs_handler import RESOURCES_BASE_PATH from handler.fs_handler.fs_handler import FSHandler -class AssetsHandler(FSHandler): +class FSAssetsHandler(FSHandler): def __init__(self) -> None: pass diff --git a/backend/handler/fs_handler/platforms_handler.py b/backend/handler/fs_handler/fs_platforms_handler.py similarity index 96% rename from backend/handler/fs_handler/platforms_handler.py rename to backend/handler/fs_handler/fs_platforms_handler.py index e73263296..e73acc96f 100644 --- a/backend/handler/fs_handler/platforms_handler.py +++ b/backend/handler/fs_handler/fs_platforms_handler.py @@ -6,7 +6,7 @@ from exceptions.fs_exceptions import FolderStructureNotMatchException from handler.fs_handler.fs_handler import FSHandler -class PlatformsHandler(FSHandler): +class FSPlatformsHandler(FSHandler): def __init__(self) -> None: pass diff --git a/backend/handler/fs_handler/resources_handler.py b/backend/handler/fs_handler/fs_resources_handler.py similarity index 99% rename from backend/handler/fs_handler/resources_handler.py rename to backend/handler/fs_handler/fs_resources_handler.py index 5ef7a1004..028e44cfa 100644 --- a/backend/handler/fs_handler/resources_handler.py +++ b/backend/handler/fs_handler/fs_resources_handler.py @@ -23,7 +23,7 @@ from handler.fs_handler.fs_handler import FSHandler from PIL import Image -class ResourceHandler(FSHandler): +class FSResourceHandler(FSHandler): def __init__(self) -> None: pass diff --git a/backend/handler/fs_handler/roms_handler.py b/backend/handler/fs_handler/fs_roms_handler.py similarity index 99% rename from backend/handler/fs_handler/roms_handler.py rename to backend/handler/fs_handler/fs_roms_handler.py index 29954f0ee..aeaaf3d8f 100644 --- a/backend/handler/fs_handler/roms_handler.py +++ b/backend/handler/fs_handler/fs_roms_handler.py @@ -18,7 +18,7 @@ from handler.fs_handler import ( from handler.fs_handler.fs_handler import FSHandler -class RomsHandler(FSHandler): +class FSRomsHandler(FSHandler): def __init__(self) -> None: pass diff --git a/backend/handler/scan_handler.py b/backend/handler/scan_handler.py index 779e0951b..dab58b723 100644 --- a/backend/handler/scan_handler.py +++ b/backend/handler/scan_handler.py @@ -3,7 +3,7 @@ from typing import Any import emoji from config.config_manager import config_manager as cm -from handler import asseth, dbh, igdbh, resourceh, romh +from handler import fsasseth, dbh, igdbh, fsresourceh, fsromh from logger.logger import log from models import Platform, Rom, Save, Screenshot, State @@ -62,7 +62,7 @@ async def scan_rom( r_igbd_id_search: str = "", overwrite: bool = False, ) -> Rom: - roms_path = romh.get_fs_structure(platform.fs_slug) + roms_path = fsromh.get_fs_structure(platform.fs_slug) log.info(f"\t · {r_igbd_id_search or rom_attrs['file_name']}") @@ -71,21 +71,21 @@ async def scan_rom( log.info(f"\t\t · {file}") # Update properties that don't require IGDB - file_size, file_size_units = romh.get_rom_file_size( + file_size, file_size_units = fsromh.get_rom_file_size( multi=rom_attrs["multi"], file_name=rom_attrs["file_name"], multi_files=rom_attrs["files"], roms_path=roms_path, ) - regs, rev, langs, other_tags = romh.parse_tags(rom_attrs["file_name"]) + regs, rev, langs, other_tags = fsromh.parse_tags(rom_attrs["file_name"]) rom_attrs.update( { "file_path": roms_path, "file_name": rom_attrs["file_name"], - "file_name_no_tags": romh.get_file_name_with_no_tags( + "file_name_no_tags": fsromh.get_file_name_with_no_tags( rom_attrs["file_name"] ), - "file_extension": romh.parse_file_extension(rom_attrs["file_name"]), + "file_extension": fsromh.parse_file_extension(rom_attrs["file_name"]), "file_size": file_size, "file_size_units": file_size_units, "multi": rom_attrs["multi"], @@ -117,7 +117,7 @@ async def scan_rom( # Update properties from IGDB rom_attrs.update( - resourceh.get_rom_cover( + fsresourceh.get_rom_cover( overwrite=overwrite, platform_fs_slug=platform.slug, rom_name=rom_attrs["name"], @@ -125,7 +125,7 @@ async def scan_rom( ) ) rom_attrs.update( - asseth.get_rom_screenshots( + fsasseth.get_rom_screenshots( platform_fs_slug=platform.slug, rom_name=rom_attrs["name"], url_screenshots=rom_attrs["url_screenshots"], @@ -138,19 +138,19 @@ async def scan_rom( def _scan_asset(file_name: str, path: str): log.info(f"\t\t · {file_name}") - file_size = asseth.get_asset_size(file_name=file_name, asset_path=path) + file_size = fsasseth.get_asset_size(file_name=file_name, asset_path=path) return { "file_path": path, "file_name": file_name, - "file_name_no_tags": asseth.get_file_name_with_no_tags(file_name), - "file_extension": asseth.parse_file_extension(file_name), + "file_name_no_tags": fsasseth.get_file_name_with_no_tags(file_name), + "file_extension": fsasseth.parse_file_extension(file_name), "file_size_bytes": file_size, } def scan_save(platform: Platform, file_name: str, emulator: str = None) -> Save: - saves_path = asseth.get_fs_structure( + saves_path = fsasseth.get_fs_structure( platform.fs_slug, folder=cm.config.SAVES_FOLDER_NAME ) @@ -162,7 +162,7 @@ def scan_save(platform: Platform, file_name: str, emulator: str = None) -> Save: def scan_state(platform: Platform, file_name: str, emulator: str = None) -> State: - states_path = asseth.get_fs_structure( + states_path = fsasseth.get_fs_structure( platform.fs_slug, folder=cm.config.STATES_FOLDER_NAME ) @@ -174,7 +174,7 @@ def scan_state(platform: Platform, file_name: str, emulator: str = None) -> Stat def scan_screenshot(file_name: str, platform: Platform = None) -> Screenshot: - screenshots_path = asseth.get_fs_structure( + screenshots_path = fsasseth.get_fs_structure( platform.fs_slug, folder=cm.config.SCREENSHOTS_FOLDER_NAME ) diff --git a/backend/main.py b/backend/main.py index d1a5d0352..c20bec16b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -5,7 +5,7 @@ import alembic.config import uvicorn from config import DEV_HOST, DEV_PORT, ROMM_AUTH_ENABLED, ROMM_AUTH_SECRET_KEY from endpoints import (assets, heartbeat, identity, oauth, platform, rom, - search, tasks, webrcade) + search, tasks, webrcade, config) from endpoints.sockets import scan from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -57,6 +57,7 @@ app.include_router(search.router) app.include_router(assets.router) app.include_router(tasks.router) app.include_router(webrcade.router) +app.include_router(webrcade.router) add_pagination(app) app.mount("/ws", socketh.socket_app) diff --git a/backend/models/platform.py b/backend/models/platform.py index f6838f485..f6fac26b8 100644 --- a/backend/models/platform.py +++ b/backend/models/platform.py @@ -22,9 +22,9 @@ class Platform(BaseModel): @property def rom_count(self) -> int: - from handler import dbh + from handler import dbplatformh - return dbh.get_rom_count(self.id) + return dbplatformh.get_rom_count(self.id) def __repr__(self) -> str: return self.name diff --git a/backend/models/rom.py b/backend/models/rom.py index b4b91a72f..2581dbc88 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -130,14 +130,14 @@ class Rom(BaseModel): # This is an expensive operation so don't call it on a list of roms @cached_property def sibling_roms(self) -> list["Rom"]: - from handler import dbh + from handler import dbromh if not self.igdb_id: return [] - with dbh.session.begin() as session: + with dbromh.session.begin() as session: return session.scalars( - dbh.get_roms(self.platform_slug).filter( + dbromh.get_roms(platform_id=self.platform_id).filter( Rom.id != self.id, Rom.igdb_id == self.igdb_id, ) diff --git a/backend/utils/tests/test_fs.py b/backend/utils/tests/test_fs.py index 5ceefdef2..c50a13f4d 100644 --- a/backend/utils/tests/test_fs.py +++ b/backend/utils/tests/test_fs.py @@ -1,6 +1,6 @@ from unittest.mock import patch -from ...handler.fs_handler.roms_handler import ( +from ...handler.fs_handler.fs_roms_handler import ( get_rom_cover, get_platforms, get_fs_structure,