Use default config values when config.yml not mount + show warning

This commit is contained in:
Georges-Antoine Assi
2025-09-19 09:16:00 -04:00
parent f9510be82a
commit 19b5a83261
11 changed files with 114 additions and 66 deletions

View File

@@ -1,4 +1,5 @@
import json
import os
import sys
from typing import Final, NotRequired, TypedDict
@@ -18,10 +19,7 @@ from config import (
ROMM_BASE_PATH,
ROMM_DB_DRIVER,
)
from exceptions.config_exceptions import (
ConfigNotReadableException,
ConfigNotWritableException,
)
from exceptions.config_exceptions import ConfigNotWritableException
from logger.formatter import BLUE
from logger.formatter import highlight as hl
from logger.logger import log
@@ -47,6 +45,7 @@ EjsOption = dict[str, str] # option_name -> option_value
class Config:
CONFIG_FILE_MOUNTED: bool
EXCLUDED_PLATFORMS: list[str]
EXCLUDED_SINGLE_EXT: list[str]
EXCLUDED_SINGLE_FILES: list[str]
@@ -69,13 +68,16 @@ class Config:
class ConfigManager:
"""Parse and load the user configuration from the config.yml file
"""
Parse and load the user configuration from the config.yml file.
If config.yml is not found, uses default configuration values.
Raises:
FileNotFoundError: Raises an error if the config.yml is not found
The config file will be created automatically when configuration is updated.
"""
_self = None
_raw_config: dict = {}
_config_file_mounted: bool = False
def __new__(cls, *args, **kwargs):
if cls._self is None:
@@ -88,14 +90,22 @@ class ConfigManager:
self.config_file = config_file
try:
self.get_config()
except ConfigNotReadableException as e:
log.critical(e.message)
sys.exit(5)
with open(self.config_file, "r+") as cf:
self._raw_config = yaml.load(cf, Loader=SafeLoader) or {}
self._config_file_mounted = True
except (FileNotFoundError, PermissionError):
log.critical(
"Config file not found or not writable, any changes made to the configuration will not persist after the application restarts."
)
self._config_file_mounted = False
# Set the config to default values
self._parse_config()
self._validate_config()
@staticmethod
def get_db_engine() -> URL:
"""Builds the database connection string depending on the defined database in the config.yml file
"""Builds the database connection string using environment variables
Returns:
str: database connection string
@@ -139,6 +149,7 @@ class ConfigManager:
"""Parses each entry in the config.yml"""
self.config = Config(
CONFIG_FILE_MOUNTED=self._config_file_mounted,
EXCLUDED_PLATFORMS=pydash.get(self._raw_config, "exclude.platforms", []),
EXCLUDED_SINGLE_EXT=[
e.lower()
@@ -193,6 +204,22 @@ class ConfigManager:
return controls
def _format_ejs_controls_for_yaml(
self,
) -> dict[str, dict[int, dict[int, EjsControlsButton]]]:
"""Format EJS controls back to YAML structure for saving"""
yaml_controls = {}
for core, controls in self.config.EJS_CONTROLS.items():
yaml_controls[core] = {
0: controls["_0"],
1: controls["_1"],
2: controls["_2"],
3: controls["_3"],
}
return yaml_controls
def _validate_config(self):
"""Validates the config.yml file"""
if not isinstance(self.config.EXCLUDED_PLATFORMS, list):
@@ -322,15 +349,23 @@ class ConfigManager:
sys.exit(3)
def get_config(self) -> Config:
with open(self.config_file) as config_file:
self._raw_config = yaml.load(config_file, Loader=SafeLoader) or {}
try:
with open(self.config_file, "r+") as config_file:
self._raw_config = yaml.load(config_file, Loader=SafeLoader) or {}
except (FileNotFoundError, PermissionError):
log.debug("Config file not found or not writable")
pass
self._parse_config()
self._validate_config()
return self.config
def update_config_file(self) -> None:
def _update_config_file(self) -> None:
if not self._config_file_mounted:
log.warning("Config file not mounted, skipping config file update")
raise ConfigNotWritableException
self._raw_config = {
"exclude": {
"platforms": self.config.EXCLUDED_PLATFORMS,
@@ -356,15 +391,22 @@ class ConfigManager:
"platforms": self.config.PLATFORMS_BINDING,
"versions": self.config.PLATFORMS_VERSIONS,
},
"emulatorjs": {
"debug": self.config.EJS_DEBUG,
"cache_limit": self.config.EJS_CACHE_LIMIT,
"settings": self.config.EJS_SETTINGS,
"controls": self._format_ejs_controls_for_yaml(),
},
}
try:
with open(self.config_file, "w") as config_file:
# Ensure the config directory exists
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
with open(self.config_file, "w+") as config_file:
yaml.dump(self._raw_config, config_file)
except FileNotFoundError:
self._raw_config = {}
except PermissionError as exc:
self._raw_config = {}
log.critical("Config file not writable, skipping config file update")
raise ConfigNotWritableException from exc
def add_platform_binding(self, fs_slug: str, slug: str) -> None:
@@ -375,7 +417,7 @@ class ConfigManager:
platform_bindings[fs_slug] = slug
self.config.PLATFORMS_BINDING = platform_bindings
self.update_config_file()
self._update_config_file()
def remove_platform_binding(self, fs_slug: str) -> None:
platform_bindings = self.config.PLATFORMS_BINDING
@@ -386,7 +428,7 @@ class ConfigManager:
pass
self.config.PLATFORMS_BINDING = platform_bindings
self.update_config_file()
self._update_config_file()
def add_platform_version(self, fs_slug: str, slug: str) -> None:
platform_versions = self.config.PLATFORMS_VERSIONS
@@ -396,7 +438,7 @@ class ConfigManager:
platform_versions[fs_slug] = slug
self.config.PLATFORMS_VERSIONS = platform_versions
self.update_config_file()
self._update_config_file()
def remove_platform_version(self, fs_slug: str) -> None:
platform_versions = self.config.PLATFORMS_VERSIONS
@@ -407,7 +449,7 @@ class ConfigManager:
pass
self.config.PLATFORMS_VERSIONS = platform_versions
self.update_config_file()
self._update_config_file()
def add_exclusion(self, exclusion_type: str, exclusion_value: str):
config_item = self.config.__getattribute__(exclusion_type)
@@ -419,7 +461,7 @@ class ConfigManager:
config_item.append(exclusion_value)
self.config.__setattr__(exclusion_type, config_item)
self.update_config_file()
self._update_config_file()
def remove_exclusion(self, exclusion_type: str, exclusion_value: str):
config_item = self.config.__getattribute__(exclusion_type)
@@ -430,7 +472,7 @@ class ConfigManager:
pass
self.config.__setattr__(exclusion_type, config_item)
self.update_config_file()
self._update_config_file()
config_manager = ConfigManager()

View File

@@ -3,10 +3,7 @@ from fastapi import HTTPException, Request, status
from config.config_manager import config_manager as cm
from decorators.auth import protected_route
from endpoints.responses.config import ConfigResponse
from exceptions.config_exceptions import (
ConfigNotReadableException,
ConfigNotWritableException,
)
from exceptions.config_exceptions import ConfigNotWritableException
from handler.auth.constants import Scope
from logger.logger import log
from utils.router import APIRouter
@@ -25,27 +22,22 @@ def get_config() -> ConfigResponse:
ConfigResponse: RomM's configuration
"""
try:
cfg = cm.get_config()
return ConfigResponse(
EXCLUDED_PLATFORMS=cfg.EXCLUDED_PLATFORMS,
EXCLUDED_SINGLE_EXT=cfg.EXCLUDED_SINGLE_EXT,
EXCLUDED_SINGLE_FILES=cfg.EXCLUDED_SINGLE_FILES,
EXCLUDED_MULTI_FILES=cfg.EXCLUDED_MULTI_FILES,
EXCLUDED_MULTI_PARTS_EXT=cfg.EXCLUDED_MULTI_PARTS_EXT,
EXCLUDED_MULTI_PARTS_FILES=cfg.EXCLUDED_MULTI_PARTS_FILES,
PLATFORMS_BINDING=cfg.PLATFORMS_BINDING,
PLATFORMS_VERSIONS=cfg.PLATFORMS_VERSIONS,
EJS_DEBUG=cfg.EJS_DEBUG,
EJS_CACHE_LIMIT=cfg.EJS_CACHE_LIMIT,
EJS_CONTROLS=cfg.EJS_CONTROLS,
EJS_SETTINGS=cfg.EJS_SETTINGS,
)
except ConfigNotReadableException as exc:
log.critical(exc.message)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=exc.message
) from exc
cfg = cm.get_config()
return ConfigResponse(
CONFIG_FILE_MOUNTED=cfg.CONFIG_FILE_MOUNTED,
EXCLUDED_PLATFORMS=cfg.EXCLUDED_PLATFORMS,
EXCLUDED_SINGLE_EXT=cfg.EXCLUDED_SINGLE_EXT,
EXCLUDED_SINGLE_FILES=cfg.EXCLUDED_SINGLE_FILES,
EXCLUDED_MULTI_FILES=cfg.EXCLUDED_MULTI_FILES,
EXCLUDED_MULTI_PARTS_EXT=cfg.EXCLUDED_MULTI_PARTS_EXT,
EXCLUDED_MULTI_PARTS_FILES=cfg.EXCLUDED_MULTI_PARTS_FILES,
PLATFORMS_BINDING=cfg.PLATFORMS_BINDING,
PLATFORMS_VERSIONS=cfg.PLATFORMS_VERSIONS,
EJS_DEBUG=cfg.EJS_DEBUG,
EJS_CACHE_LIMIT=cfg.EJS_CACHE_LIMIT,
EJS_CONTROLS=cfg.EJS_CONTROLS,
EJS_SETTINGS=cfg.EJS_SETTINGS,
)
@protected_route(router.post, "/system/platforms", [Scope.PLATFORMS_WRITE])

View File

@@ -4,6 +4,7 @@ from config.config_manager import EjsControls
class ConfigResponse(TypedDict):
CONFIG_FILE_MOUNTED: bool
EXCLUDED_PLATFORMS: list[str]
EXCLUDED_SINGLE_EXT: list[str]
EXCLUDED_SINGLE_FILES: list[str]

View File

@@ -1,12 +1,3 @@
class ConfigNotReadableException(Exception):
def __init__(self):
self.message = "Config file can't be read. Check config.yml permissions"
super().__init__(self.message)
def __repr__(self) -> str:
return self.message
class ConfigNotWritableException(Exception):
def __init__(self):
self.message = "Config file is not writable. Check config.yml permissions"

View File

@@ -26,7 +26,7 @@ services:
- romm_redis_data:/redis-data # Cached data for background tasks
- /path/to/library:/romm/library # Your game library. Check https://docs.romm.app/latest/Getting-Started/Folder-Structure/ for more details.
- /path/to/assets:/romm/assets # Uploaded saves, states, etc.
- /path/to/config:/romm/config # Path where config.yml is stored
- /path/to/config:/romm/config # (Optional) Path where config.yml is stored
ports:
- 80:8080
depends_on:

View File

@@ -4,6 +4,7 @@
/* eslint-disable */
import type { EjsControls } from './EjsControls';
export type ConfigResponse = {
CONFIG_FILE_MOUNTED: boolean;
EXCLUDED_PLATFORMS: Array<string>;
EXCLUDED_SINGLE_EXT: Array<string>;
EXCLUDED_SINGLE_FILES: Array<string>;

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import CreateExclusionDialog from "@/components/Settings/LibraryManagement/Config/Dialog/CreateExclusion.vue";
@@ -9,40 +10,41 @@ import storeConfig from "@/stores/config";
const { t } = useI18n();
const configStore = storeConfig();
const { config } = storeToRefs(configStore);
const authStore = storeAuth();
const exclusions = [
{
set: configStore.config.EXCLUDED_PLATFORMS,
set: config.value.EXCLUDED_PLATFORMS,
title: t("common.platform"),
icon: "mdi-controller-off",
type: "EXCLUDED_PLATFORMS",
},
{
set: configStore.config.EXCLUDED_SINGLE_FILES,
set: config.value.EXCLUDED_SINGLE_FILES,
title: t("settings.excluded-single-rom-files"),
icon: "mdi-file-document-remove-outline",
type: "EXCLUDED_SINGLE_FILES",
},
{
set: configStore.config.EXCLUDED_SINGLE_EXT,
set: config.value.EXCLUDED_SINGLE_EXT,
title: t("settings.excluded-single-rom-extensions"),
icon: "mdi-file-document-remove-outline",
type: "EXCLUDED_SINGLE_EXT",
},
{
set: configStore.config.EXCLUDED_MULTI_FILES,
set: config.value.EXCLUDED_MULTI_FILES,
title: t("settings.excluded-multi-rom-files"),
icon: "mdi-file-document-remove-outline",
type: "EXCLUDED_MULTI_FILES",
},
{
set: configStore.config.EXCLUDED_MULTI_PARTS_FILES,
set: config.value.EXCLUDED_MULTI_PARTS_FILES,
title: t("settings.excluded-multi-rom-parts-files"),
icon: "mdi-file-document-remove-outline",
type: "EXCLUDED_MULTI_PARTS_FILES",
},
{
set: configStore.config.EXCLUDED_MULTI_PARTS_EXT,
set: config.value.EXCLUDED_MULTI_PARTS_EXT,
title: t("settings.excluded-multi-rom-parts-extensions"),
icon: "mdi-file-document-remove-outline",
type: "EXCLUDED_MULTI_PARTS_EXT",
@@ -61,6 +63,7 @@ const editable = ref(false);
variant="text"
icon="mdi-cog"
@click="editable = !editable"
:disabled="!config.CONFIG_FILE_MOUNTED"
/>
</template>
<template #content>

View File

@@ -48,6 +48,7 @@ const editable = ref(false);
variant="text"
icon="mdi-cog"
@click="editable = !editable"
:disabled="!config.CONFIG_FILE_MOUNTED"
/>
</template>
<template #content>

View File

@@ -52,6 +52,7 @@ const editable = ref(false);
variant="text"
icon="mdi-cog"
@click="editable = !editable"
:disabled="!config.CONFIG_FILE_MOUNTED"
/>
</template>
<template #content>

View File

@@ -12,6 +12,7 @@ type ExclusionTypes =
| "EXCLUDED_MULTI_PARTS_FILES";
const defaultConfig = {
CONFIG_FILE_MOUNTED: false,
EXCLUDED_PLATFORMS: [],
EXCLUDED_SINGLE_EXT: [],
EXCLUDED_SINGLE_FILES: [],

View File

@@ -13,6 +13,7 @@ import PlatformVersions from "@/components/Settings/LibraryManagement/Config/Pla
import GameTable from "@/components/common/Game/VirtualTable.vue";
import MissingFromFSIcon from "@/components/common/MissingFromFSIcon.vue";
import PlatformIcon from "@/components/common/Platform/PlatformIcon.vue";
import storeConfig from "@/stores/config";
import storeGalleryFilter from "@/stores/galleryFilter";
import storeGalleryView from "@/stores/galleryView";
import storePlatforms from "@/stores/platforms";
@@ -21,6 +22,8 @@ import type { Events } from "@/types/emitter";
const { t } = useI18n();
const tab = ref<"config" | "missing">("config");
const configStore = storeConfig();
const { config } = storeToRefs(configStore);
const romsStore = storeRoms();
const { allRoms, fetchingRoms, fetchTotalRoms, filteredRoms } =
storeToRefs(romsStore);
@@ -195,6 +198,18 @@ onUnmounted(() => {
</v-tabs>
</v-col>
<v-col>
<v-alert
v-if="!config.CONFIG_FILE_MOUNTED"
type="error"
variant="tonal"
class="my-2"
>
<template #title> Configuration file not mounted </template>
<template #text>
The config.yml file is not mounted or writable. Any changes made to
the configuration will not persist after the application restarts.
</template>
</v-alert>
<v-tabs-window v-model="tab">
<v-tabs-window-item value="config">
<PlatformBinding class="mt-2" />