Skip to content

Commit 3e629ba

Browse files
authored
Merge pull request #3544 from rommapp/fix/crash-on-startup-no-library
fix: Crash on startup when no library
2 parents 00af288 + a0f5484 commit 3e629ba

4 files changed

Lines changed: 76 additions & 40 deletions

File tree

backend/handler/filesystem/platforms_handler.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44

55
from config import LIBRARY_BASE_PATH
66
from config.config_manager import config_manager as cm
7-
from exceptions.fs_exceptions import (
8-
FolderStructureNotMatchException,
9-
PlatformAlreadyExistsException,
10-
)
7+
from exceptions.fs_exceptions import PlatformAlreadyExistsException
8+
from logger.logger import log
119

1210
from .base_handler import FSHandler, LibraryStructure
1311

@@ -59,9 +57,9 @@ def get_platforms_directory(self) -> str:
5957
cnfg = cm.get_config()
6058

6159
# Fallback to config hint when detection is inconclusive: default to
62-
# Structure A (roms/{platform}) so a malformed library fails loudly
63-
# (FolderStructureNotMatchException) rather than treating the bare
64-
# library root as a flat list of platforms.
60+
# Structure A (roms/{platform}) so the bare library root is not treated
61+
# as a flat list of platforms. When the roms folder is missing entirely,
62+
# get_platforms() bootstraps it instead of failing.
6563
return "" if cnfg.has_structure_path_b else cnfg.ROMS_FOLDER_NAME
6664

6765
def get_platform_fs_structure(self, fs_slug: str) -> str:
@@ -90,15 +88,31 @@ async def add_platform(self, fs_slug: str) -> None:
9088
async def get_platforms(self) -> list[str]:
9189
"""Retrieves all platforms from the filesystem.
9290
91+
If no library structure exists yet (neither Structure A's top-level roms
92+
folder nor a Structure B {platform}/roms folder), defaults to Structure A
93+
by creating the roms folder and returns an empty list, so RomM starts
94+
cleanly with an empty library instead of failing.
95+
9396
Returns:
9497
List of platform slugs.
9598
"""
9699
cnfg = cm.get_config()
97100

98101
try:
99102
platforms = await self.list_directories(path=self.get_platforms_directory())
100-
except FileNotFoundError as e:
101-
raise FolderStructureNotMatchException() from e
103+
except FileNotFoundError:
104+
# The platforms directory does not exist, which means no library
105+
# structure has been set up yet. Bootstrap Structure A so the
106+
# filesystem is in a valid state and report an empty library.
107+
log.warning(
108+
"No library structure found; creating default Structure A "
109+
"(roms folder) and starting with an empty library."
110+
)
111+
try:
112+
self.create_library_structure()
113+
except OSError:
114+
log.error("Failed to create default library structure", exc_info=True)
115+
return []
102116

103117
# For Structure B, only include directories that have a roms subfolder
104118
structure = self.detect_library_structure()

backend/tests/handler/filesystem/test_platforms_handler.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,48 @@ async def test_get_platforms_calls_list_directories_with_empty_path(
229229
await handler.get_platforms()
230230
mock_list.assert_called_once_with(path="")
231231

232+
async def test_get_platforms_bootstraps_structure_a_when_none_detected(
233+
self, handler: FSPlatformsHandler, config
234+
):
235+
"""When no structure exists, get_platforms creates Structure A (roms folder)
236+
and returns an empty list instead of raising."""
237+
config.has_structure_path_a = False
238+
config.has_structure_path_b = False
239+
with patch(
240+
"handler.filesystem.platforms_handler.cm.get_config", return_value=config
241+
):
242+
with patch.object(
243+
handler, "list_directories", side_effect=FileNotFoundError
244+
):
245+
with patch.object(handler, "create_library_structure") as mock_create:
246+
result = await handler.get_platforms()
247+
248+
assert result == []
249+
mock_create.assert_called_once()
250+
251+
async def test_get_platforms_returns_empty_when_bootstrap_fails(
252+
self, handler: FSPlatformsHandler, config
253+
):
254+
"""If creating the default structure fails, get_platforms still returns an
255+
empty list rather than propagating the error (so the heartbeat stays healthy).
256+
"""
257+
config.has_structure_path_a = False
258+
config.has_structure_path_b = False
259+
with patch(
260+
"handler.filesystem.platforms_handler.cm.get_config", return_value=config
261+
):
262+
with patch.object(
263+
handler, "list_directories", side_effect=FileNotFoundError
264+
):
265+
with patch.object(
266+
handler,
267+
"create_library_structure",
268+
side_effect=PermissionError("read-only filesystem"),
269+
):
270+
result = await handler.get_platforms()
271+
272+
assert result == []
273+
232274
def test_integration_with_base_handler_methods(self, handler: FSPlatformsHandler):
233275
"""Test that FSPlatformsHandler properly inherits from FSHandler"""
234276
# Test that handler has base methods

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ DEP002 = [ # DEP002 rule: Project should not contain unused dependencies
136136
[tool.uv]
137137
package = false
138138
exclude-newer = "7 days"
139-
exclude-newer-package = { starlette = "2026-05-22" }
139+
# vcrpy < 8.2.0 references aiohttp.streams.AsyncStreamReaderMixin, removed in
140+
# aiohttp 3.14; allow the fixed release past the global 7-day window.
141+
exclude-newer-package = { starlette = "2026-05-22", vcrpy = "2026-06-17" }
140142

141143
[tool.ty.environment]
142144
root = ["./backend"]

uv.lock

Lines changed: 8 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)