fix: prevent crash on startup by bootstrapping library structure A when none is detected

This commit is contained in:
zurdi
2026-06-17 14:20:58 +02:00
parent 00af28821c
commit 2a53669459
3 changed files with 66 additions and 10 deletions

View File

@@ -4,10 +4,8 @@ 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 (
FolderStructureNotMatchException,
PlatformAlreadyExistsException,
)
from exceptions.fs_exceptions import PlatformAlreadyExistsException
from logger.logger import log
from .base_handler import FSHandler, LibraryStructure
@@ -59,9 +57,9 @@ class FSPlatformsHandler(FSHandler):
cnfg = cm.get_config()
# Fallback to config hint when detection is inconclusive: default to
# Structure A (roms/{platform}) so a malformed library fails loudly
# (FolderStructureNotMatchException) rather than treating the bare
# library root as a flat list of platforms.
# Structure A (roms/{platform}) so the bare library root is not treated
# as a flat list of platforms. When the roms folder is missing entirely,
# get_platforms() bootstraps it instead of failing.
return "" if cnfg.has_structure_path_b else cnfg.ROMS_FOLDER_NAME
def get_platform_fs_structure(self, fs_slug: str) -> str:
@@ -90,6 +88,11 @@ class FSPlatformsHandler(FSHandler):
async def get_platforms(self) -> list[str]:
"""Retrieves all platforms from the filesystem.
If no library structure exists yet (neither Structure A's top-level roms
folder nor a Structure B {platform}/roms folder), defaults to Structure A
by creating the roms folder and returns an empty list, so RomM starts
cleanly with an empty library instead of failing.
Returns:
List of platform slugs.
"""
@@ -97,8 +100,19 @@ class FSPlatformsHandler(FSHandler):
try:
platforms = await self.list_directories(path=self.get_platforms_directory())
except FileNotFoundError as e:
raise FolderStructureNotMatchException() from e
except FileNotFoundError:
# The platforms directory does not exist, which means no library
# structure has been set up yet. Bootstrap Structure A so the
# filesystem is in a valid state and report an empty library.
log.warning(
"No library structure found; creating default Structure A "
"(roms folder) and starting with an empty library."
)
try:
self.create_library_structure()
except OSError:
log.error("Failed to create default library structure", exc_info=True)
return []
# For Structure B, only include directories that have a roms subfolder
structure = self.detect_library_structure()

View File

@@ -229,6 +229,48 @@ class TestFSPlatformsHandler:
await handler.get_platforms()
mock_list.assert_called_once_with(path="")
async def test_get_platforms_bootstraps_structure_a_when_none_detected(
self, handler: FSPlatformsHandler, config
):
"""When no structure exists, get_platforms creates Structure A (roms folder)
and returns an empty list instead of raising."""
config.has_structure_path_a = False
config.has_structure_path_b = False
with patch(
"handler.filesystem.platforms_handler.cm.get_config", return_value=config
):
with patch.object(
handler, "list_directories", side_effect=FileNotFoundError
):
with patch.object(handler, "create_library_structure") as mock_create:
result = await handler.get_platforms()
assert result == []
mock_create.assert_called_once()
async def test_get_platforms_returns_empty_when_bootstrap_fails(
self, handler: FSPlatformsHandler, config
):
"""If creating the default structure fails, get_platforms still returns an
empty list rather than propagating the error (so the heartbeat stays healthy).
"""
config.has_structure_path_a = False
config.has_structure_path_b = False
with patch(
"handler.filesystem.platforms_handler.cm.get_config", return_value=config
):
with patch.object(
handler, "list_directories", side_effect=FileNotFoundError
):
with patch.object(
handler,
"create_library_structure",
side_effect=PermissionError("read-only filesystem"),
):
result = await handler.get_platforms()
assert result == []
def test_integration_with_base_handler_methods(self, handler: FSPlatformsHandler):
"""Test that FSPlatformsHandler properly inherits from FSHandler"""
# Test that handler has base methods

2
uv.lock generated
View File

@@ -11,7 +11,7 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for
exclude-newer-span = "P7D"
[options.exclude-newer-package]
starlette = "2026-05-23T00:00:00Z"
starlette = "2026-05-22T22:00:00Z"
[[package]]
name = "aiohappyeyeballs"