diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 37dea145d..cca771065 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -1,4 +1,6 @@ import enum +import functools +import glob import json import os import sys @@ -137,6 +139,14 @@ class Config: self.STRUCTURE_PATH_A = f"{LIBRARY_BASE_PATH}/{self.ROMS_FOLDER_NAME}/*" self.STRUCTURE_PATH_B = f"{LIBRARY_BASE_PATH}/*/{self.ROMS_FOLDER_NAME}" + @functools.cached_property + def has_structure_b(self) -> bool: + """True when any `///` directory exists.""" + for match in glob.iglob(self.STRUCTURE_PATH_B): + if os.path.isdir(match): + return True + return False + class ConfigManager: """ diff --git a/backend/handler/filesystem/firmware_handler.py b/backend/handler/filesystem/firmware_handler.py index ef6d8e6da..c0ac7daf4 100644 --- a/backend/handler/filesystem/firmware_handler.py +++ b/backend/handler/filesystem/firmware_handler.py @@ -1,5 +1,4 @@ import binascii -import glob import hashlib import os @@ -19,7 +18,7 @@ class FSFirmwareHandler(FSHandler): cnfg = cm.get_config() return ( f"{fs_slug}/{cnfg.FIRMWARE_FOLDER_NAME}" - if glob.glob(cnfg.STRUCTURE_PATH_B) + if cnfg.has_structure_b else f"{cnfg.FIRMWARE_FOLDER_NAME}/{fs_slug}" ) diff --git a/backend/handler/filesystem/platforms_handler.py b/backend/handler/filesystem/platforms_handler.py index ea1019f67..c20c3632f 100644 --- a/backend/handler/filesystem/platforms_handler.py +++ b/backend/handler/filesystem/platforms_handler.py @@ -1,4 +1,3 @@ -import glob import os from anyio import Path as AnyioPath @@ -63,7 +62,7 @@ class FSPlatformsHandler(FSHandler): cnfg = cm.get_config() # Fallback to config hint when detection is inconclusive - return "" if glob.glob(cnfg.STRUCTURE_PATH_B) else cnfg.ROMS_FOLDER_NAME + return "" if cnfg.has_structure_b else cnfg.ROMS_FOLDER_NAME def get_platform_fs_structure(self, fs_slug: str) -> str: cnfg = cm.get_config() @@ -71,7 +70,7 @@ class FSPlatformsHandler(FSHandler): # Fallback to config hint when detection is inconclusive return ( f"{fs_slug}/{cnfg.ROMS_FOLDER_NAME}" - if glob.glob(cnfg.STRUCTURE_PATH_B) + if cnfg.has_structure_b else f"{cnfg.ROMS_FOLDER_NAME}/{fs_slug}" ) diff --git a/backend/handler/filesystem/roms_handler.py b/backend/handler/filesystem/roms_handler.py index ef6561462..af48b9a58 100644 --- a/backend/handler/filesystem/roms_handler.py +++ b/backend/handler/filesystem/roms_handler.py @@ -1,7 +1,6 @@ import binascii import bz2 import fnmatch -import glob import hashlib import os import re @@ -315,7 +314,7 @@ class FSRomsHandler(FSHandler): cnfg = cm.get_config() return ( f"{fs_slug}/{cnfg.ROMS_FOLDER_NAME}" - if glob.glob(cnfg.STRUCTURE_PATH_B) + if cnfg.has_structure_b else f"{cnfg.ROMS_FOLDER_NAME}/{fs_slug}" ) diff --git a/backend/tests/handler/filesystem/test_firmware_handler.py b/backend/tests/handler/filesystem/test_firmware_handler.py index 23143be1b..0e0f7523c 100644 --- a/backend/tests/handler/filesystem/test_firmware_handler.py +++ b/backend/tests/handler/filesystem/test_firmware_handler.py @@ -143,33 +143,26 @@ class TestFSFirmwareHandler: ): """Test that firmware paths are constructed correctly for Structure A""" platform_fs_slug = "n64" + config.has_structure_b = False with patch( "handler.filesystem.firmware_handler.cm.get_config", return_value=config ): - with patch( - "handler.filesystem.firmware_handler.glob.glob", return_value=[] - ): - path = handler.get_firmware_fs_structure(platform_fs_slug) - assert path == f"{config.FIRMWARE_FOLDER_NAME}/{platform_fs_slug}" + path = handler.get_firmware_fs_structure(platform_fs_slug) + assert path == f"{config.FIRMWARE_FOLDER_NAME}/{platform_fs_slug}" def test_firmware_path_construction_structure_b( self, handler: FSFirmwareHandler, config ): """Test that firmware paths are constructed correctly for Structure B""" platform_fs_slug = "n64" + config.has_structure_b = True with patch( "handler.filesystem.firmware_handler.cm.get_config", return_value=config ): - with patch( - "handler.filesystem.firmware_handler.glob.glob", - return_value=[ - f"{LIBRARY_BASE_PATH}/{platform_fs_slug}/{config.ROMS_FOLDER_NAME}" - ], - ): - path = handler.get_firmware_fs_structure(platform_fs_slug) - assert path == f"{platform_fs_slug}/{config.FIRMWARE_FOLDER_NAME}" + path = handler.get_firmware_fs_structure(platform_fs_slug) + assert path == f"{platform_fs_slug}/{config.FIRMWARE_FOLDER_NAME}" async def test_multiple_platform_handling(self, handler: FSFirmwareHandler, config): """Test handling of different platform slugs""" diff --git a/backend/tests/handler/filesystem/test_platforms_handler.py b/backend/tests/handler/filesystem/test_platforms_handler.py index a302cabc3..24c5302ce 100644 --- a/backend/tests/handler/filesystem/test_platforms_handler.py +++ b/backend/tests/handler/filesystem/test_platforms_handler.py @@ -90,80 +90,60 @@ class TestFSPlatformsHandler: self, handler: FSPlatformsHandler, config ): """Test get_platforms_directory with Structure A (roms/{platform})""" + config.has_structure_b = False with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): - with patch( - "handler.filesystem.platforms_handler.glob.glob", return_value=[] - ): - result = handler.get_platforms_directory() - assert result == config.ROMS_FOLDER_NAME + result = handler.get_platforms_directory() + assert result == config.ROMS_FOLDER_NAME def test_get_platforms_directory_structure_b( self, handler: FSPlatformsHandler, config ): """Test get_platforms_directory with Structure B ({platform}/roms)""" + config.has_structure_b = True with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): - with patch( - "handler.filesystem.platforms_handler.glob.glob", - return_value=[f"{LIBRARY_BASE_PATH}/n64/{config.ROMS_FOLDER_NAME}"], - ): - result = handler.get_platforms_directory() - assert result == "" + result = handler.get_platforms_directory() + assert result == "" def test_get_platform_fs_structure_structure_a( self, handler: FSPlatformsHandler, config ): """Test get_platform_fs_structure with Structure A (roms/{platform})""" fs_slug = "n64" - + config.has_structure_b = False with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): - with patch( - "handler.filesystem.platforms_handler.glob.glob", return_value=[] - ): - result = handler.get_platform_fs_structure(fs_slug) - assert result == f"{config.ROMS_FOLDER_NAME}/{fs_slug}" + result = handler.get_platform_fs_structure(fs_slug) + assert result == f"{config.ROMS_FOLDER_NAME}/{fs_slug}" def test_get_platform_fs_structure_structure_b( self, handler: FSPlatformsHandler, config ): """Test get_platform_fs_structure with Structure B ({platform}/roms)""" fs_slug = "n64" - + config.has_structure_b = True with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): - with patch( - "handler.filesystem.platforms_handler.glob.glob", - return_value=[ - f"{LIBRARY_BASE_PATH}/{fs_slug}/{config.ROMS_FOLDER_NAME}" - ], - ): - result = handler.get_platform_fs_structure(fs_slug) - assert result == f"{fs_slug}/{config.ROMS_FOLDER_NAME}" + result = handler.get_platform_fs_structure(fs_slug) + assert result == f"{fs_slug}/{config.ROMS_FOLDER_NAME}" def test_get_platform_fs_structure_custom_folder_name( self, handler: FSPlatformsHandler, config_custom_folder ): """Test get_platform_fs_structure with custom folder name (Structure B)""" fs_slug = "psx" - + config_custom_folder.has_structure_b = True with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config_custom_folder, ): - with patch( - "handler.filesystem.platforms_handler.glob.glob", - return_value=[ - f"{LIBRARY_BASE_PATH}/{fs_slug}/{config_custom_folder.ROMS_FOLDER_NAME}" - ], - ): - result = handler.get_platform_fs_structure(fs_slug) - assert result == f"{fs_slug}/{config_custom_folder.ROMS_FOLDER_NAME}" + result = handler.get_platform_fs_structure(fs_slug) + assert result == f"{fs_slug}/{config_custom_folder.ROMS_FOLDER_NAME}" async def test_add_platform_creates_directory( self, handler: FSPlatformsHandler, config @@ -171,16 +151,14 @@ class TestFSPlatformsHandler: """Test that add_platform creates the correct directory (Structure A)""" fs_slug = "gba" expected_path = f"{config.ROMS_FOLDER_NAME}/{fs_slug}" + config.has_structure_b = False with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): - with patch( - "handler.filesystem.platforms_handler.glob.glob", return_value=[] - ): - with patch.object(handler, "make_directory") as mock_make_directory: - await handler.add_platform(fs_slug) - mock_make_directory.assert_called_once_with(expected_path) + with patch.object(handler, "make_directory") as mock_make_directory: + await handler.add_platform(fs_slug) + mock_make_directory.assert_called_once_with(expected_path) async def test_add_platform_normal_structure( self, handler: FSPlatformsHandler, config @@ -188,19 +166,14 @@ class TestFSPlatformsHandler: """Test that add_platform creates directory with Structure B""" fs_slug = "gba" expected_path = f"{fs_slug}/{config.ROMS_FOLDER_NAME}" + config.has_structure_b = True with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): - with patch( - "handler.filesystem.platforms_handler.glob.glob", - return_value=[ - f"{LIBRARY_BASE_PATH}/{fs_slug}/{config.ROMS_FOLDER_NAME}" - ], - ): - with patch.object(handler, "make_directory") as mock_make_directory: - await handler.add_platform(fs_slug) - mock_make_directory.assert_called_once_with(expected_path) + with patch.object(handler, "make_directory") as mock_make_directory: + await handler.add_platform(fs_slug) + mock_make_directory.assert_called_once_with(expected_path) async def test_get_platforms_returns_existing_platforms( self, handler: FSPlatformsHandler, config @@ -230,17 +203,15 @@ class TestFSPlatformsHandler: self, handler: FSPlatformsHandler, config ): """Test that get_platforms calls list_directories with correct path""" + config.has_structure_b = False with patch( "handler.filesystem.platforms_handler.cm.get_config", return_value=config ): - with patch( - "handler.filesystem.platforms_handler.glob.glob", return_value=[] - ): - with patch.object( - handler, "list_directories", return_value=[] - ) as mock_list: - await handler.get_platforms() - mock_list.assert_called_once_with(path=config.ROMS_FOLDER_NAME) + with patch.object( + handler, "list_directories", return_value=[] + ) as mock_list: + await handler.get_platforms() + mock_list.assert_called_once_with(path=config.ROMS_FOLDER_NAME) async def test_get_platforms_calls_list_directories_with_empty_path( self, handler: FSPlatformsHandler, config diff --git a/backend/tests/handler/filesystem/test_roms_handler.py b/backend/tests/handler/filesystem/test_roms_handler.py index a882827a0..4153ac5b0 100644 --- a/backend/tests/handler/filesystem/test_roms_handler.py +++ b/backend/tests/handler/filesystem/test_roms_handler.py @@ -105,27 +105,22 @@ class TestFSRomsHandler: def test_get_roms_fs_structure_structure_b(self, handler: FSRomsHandler): """Test get_roms_fs_structure with Structure B ({platform}/roms)""" fs_slug = "n64" + cfg = Config( + EXCLUDED_PLATFORMS=[], + EXCLUDED_SINGLE_EXT=[], + EXCLUDED_SINGLE_FILES=[], + EXCLUDED_MULTI_FILES=[], + EXCLUDED_MULTI_PARTS_EXT=[], + EXCLUDED_MULTI_PARTS_FILES=[], + PLATFORMS_BINDING={}, + PLATFORMS_VERSIONS={}, + ROMS_FOLDER_NAME="roms", + FIRMWARE_FOLDER_NAME="bios", + ) + cfg.has_structure_b = True with pytest.MonkeyPatch.context() as m: - m.setattr( - "handler.filesystem.roms_handler.cm.get_config", - lambda: Config( - EXCLUDED_PLATFORMS=[], - EXCLUDED_SINGLE_EXT=[], - EXCLUDED_SINGLE_FILES=[], - EXCLUDED_MULTI_FILES=[], - EXCLUDED_MULTI_PARTS_EXT=[], - EXCLUDED_MULTI_PARTS_FILES=[], - PLATFORMS_BINDING={}, - PLATFORMS_VERSIONS={}, - ROMS_FOLDER_NAME="roms", - FIRMWARE_FOLDER_NAME="bios", - ), - ) - m.setattr( - "handler.filesystem.roms_handler.glob.glob", - lambda _: [f"{LIBRARY_BASE_PATH}/{fs_slug}/roms"], - ) + m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: cfg) result = handler.get_roms_fs_structure(fs_slug) assert result == f"{fs_slug}/roms" @@ -133,24 +128,22 @@ class TestFSRomsHandler: def test_get_roms_fs_structure_structure_a(self, handler: FSRomsHandler): """Test get_roms_fs_structure with Structure A (roms/{platform})""" fs_slug = "n64" + cfg = Config( + EXCLUDED_PLATFORMS=[], + EXCLUDED_SINGLE_EXT=[], + EXCLUDED_SINGLE_FILES=[], + EXCLUDED_MULTI_FILES=[], + EXCLUDED_MULTI_PARTS_EXT=[], + EXCLUDED_MULTI_PARTS_FILES=[], + PLATFORMS_BINDING={}, + PLATFORMS_VERSIONS={}, + ROMS_FOLDER_NAME="roms", + FIRMWARE_FOLDER_NAME="bios", + ) + cfg.has_structure_b = False with pytest.MonkeyPatch.context() as m: - m.setattr( - "handler.filesystem.roms_handler.cm.get_config", - lambda: Config( - EXCLUDED_PLATFORMS=[], - EXCLUDED_SINGLE_EXT=[], - EXCLUDED_SINGLE_FILES=[], - EXCLUDED_MULTI_FILES=[], - EXCLUDED_MULTI_PARTS_EXT=[], - EXCLUDED_MULTI_PARTS_FILES=[], - PLATFORMS_BINDING={}, - PLATFORMS_VERSIONS={}, - ROMS_FOLDER_NAME="roms", - FIRMWARE_FOLDER_NAME="bios", - ), - ) - m.setattr("handler.filesystem.roms_handler.glob.glob", lambda _: []) + m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: cfg) result = handler.get_roms_fs_structure(fs_slug) assert result == f"roms/{fs_slug}" @@ -509,7 +502,7 @@ class TestFSRomsHandler: assert parsed_tags.revision == "B" assert parsed_tags.version == "" - def test_platform_specific_behavior(self, handler: FSRomsHandler): + def test_platform_specific_behavior(self, handler: FSRomsHandler, config): """Test platform-specific behavior differences""" # Create mock platforms - one hashable, one non-hashable hashable_platform = Mock(spec=Platform) @@ -520,11 +513,10 @@ class TestFSRomsHandler: non_hashable_platform.fs_slug = "n64" non_hashable_platform.slug = "nintendo-64" + config.has_structure_b = True + with pytest.MonkeyPatch.context() as m: - m.setattr( - "handler.filesystem.roms_handler.glob.glob", - lambda _: [f"{LIBRARY_BASE_PATH}/n64/roms"], - ) # Structure B + m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: config) hashable_path = handler.get_roms_fs_structure(hashable_platform.fs_slug) non_hashable_path = handler.get_roms_fs_structure( @@ -551,23 +543,20 @@ class TestFSRomsHandler: assert "Super Mario 64 (J) (Rev A)" in filtered_dirs assert "Test Multi Rom [USA]" in filtered_dirs - def test_rom_fs_structure_consistency(self, handler: FSRomsHandler): + def test_rom_fs_structure_consistency(self, handler: FSRomsHandler, config): """Test that ROM filesystem structure is consistent across methods""" fs_slug = "gba" with pytest.MonkeyPatch.context() as m: - # Test with Structure B - m.setattr( - "handler.filesystem.roms_handler.glob.glob", - lambda _: [f"{LIBRARY_BASE_PATH}/{fs_slug}/roms"], - ) + m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: config) + # Test with Structure B + config.has_structure_b = True structure = handler.get_roms_fs_structure(fs_slug) assert structure == f"{fs_slug}/roms" # Test with Structure A - m.setattr("handler.filesystem.roms_handler.glob.glob", lambda _: []) - + config.has_structure_b = False structure = handler.get_roms_fs_structure(fs_slug) assert structure == f"roms/{fs_slug}" diff --git a/backend/watcher.py b/backend/watcher.py index 80675c20b..9234dea43 100644 --- a/backend/watcher.py +++ b/backend/watcher.py @@ -1,6 +1,5 @@ import enum import fnmatch -import glob import json import os from collections.abc import Sequence @@ -50,8 +49,8 @@ sentry_sdk.init( ) tracer = trace.get_tracer(__name__) -_cfg = cm.get_config() -structure_level = 1 if glob.glob(_cfg.STRUCTURE_PATH_B) else 2 +cfg = cm.get_config() +structure_level = 1 if cfg.has_structure_b else 2 @enum.unique diff --git a/env.template b/env.template index 7baa37177..fa9079aeb 100644 --- a/env.template +++ b/env.template @@ -1,7 +1,7 @@ # Core Application ROMM_BASE_PATH=/romm # Base folder path for library, resources and assets ROMM_TMP_PATH= # Custom temporary directory path -ROMM_BASE_URL=http://0.0.0.0 # Base URL used when rendering container log links +ROMM_BASE_URL=http://0.0.0.0 # Public URL of this instance (CORS origin, OIDC redirect base, links in container logs) ROMM_PORT=8080 # Port on which the application listens KIOSK_MODE=false # Read-only mode for public displays or kiosks