Coverage for seedboxsync/core/config.py: 96%
54 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-11 07:37 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-11 07:37 +0000
1#
2# Copyright (C) 2025-2026 Guillaume Kulakowski <guillaume@kulakowski.fr>
3#
4# For the full copyright and license information, please view the LICENSE
5# file that was distributed with this source code.
6#
7"""A module to manage SeedboxSync configuration from Database or environment variables."""
9from typing import Any, ClassVar
10from flask import Flask
11from seedboxsync.core.database.models import SeedboxSync, typed_peewee_dicts
14class Config:
15 """Config."""
17 DB_CONFIG_PREFIX = "config_"
18 CONFIG_NAMESPACE = "SEEDBOXSYNC_"
19 DEFAULT_CONFIG: ClassVar[dict[str, Any]] = {
20 CONFIG_NAMESPACE + "LOGIN_DISABLED": False,
21 CONFIG_NAMESPACE + "WTF_CSRF_ENABLED": True,
22 CONFIG_NAMESPACE + "AUTH_GRAVATAR_ENABLED": False,
23 CONFIG_NAMESPACE + "OAUTH_ENABLED": False,
24 CONFIG_NAMESPACE + "OAUTH_AUTO_CREATE_USER_ENABLED": False,
25 CONFIG_NAMESPACE + "OAUTH_BUILTIN_AUTHENTICATION_DISABLED": False,
26 CONFIG_NAMESPACE + "OAUTH_NAME": "oidc",
27 CONFIG_NAMESPACE + "OAUTH_CLIENT_ID": "",
28 CONFIG_NAMESPACE + "OAUTH_CLIENT_SECRET": "",
29 CONFIG_NAMESPACE + "OAUTH_SERVER_METADATA_URL": "",
30 CONFIG_NAMESPACE + "SYNC_BLACKHOLE_ENABLED": False,
31 CONFIG_NAMESPACE + "SYNC_SEEDBOX_ENABLED": False,
32 CONFIG_NAMESPACE + "SEEDBOX_HOST": "my-seedbox.ltd",
33 CONFIG_NAMESPACE + "SEEDBOX_PORT": "22",
34 CONFIG_NAMESPACE + "SEEDBOX_LOGIN": "me",
35 CONFIG_NAMESPACE + "SEEDBOX_PASSWORD": "p4sw0rd",
36 CONFIG_NAMESPACE + "SEEDBOX_TIMEOUT": False,
37 CONFIG_NAMESPACE + "SEEDBOX_PROTOCOL": "sftp",
38 CONFIG_NAMESPACE + "SEEDBOX_MAX_CONCURRENT_PREFETCH_REQUESTS": "128",
39 CONFIG_NAMESPACE + "SEEDBOX_CHMOD": False,
40 CONFIG_NAMESPACE + "SEEDBOX_TMP_PATH": "./tmp",
41 CONFIG_NAMESPACE + "SEEDBOX_WATCH_PATH": "./watch",
42 CONFIG_NAMESPACE + "SEEDBOX_FINISHED_PATH": "./files",
43 CONFIG_NAMESPACE + "SEEDBOX_PREFIXED_PATH": "/files",
44 CONFIG_NAMESPACE + "SEEDBOX_PART_SUFFIX": ".part",
45 CONFIG_NAMESPACE + "SEEDBOX_EXCLUDE_SYNCING": "",
46 CONFIG_NAMESPACE + "LOCAL_WATCH_PATH": "~/watch",
47 CONFIG_NAMESPACE + "LOCAL_DOWNLOAD_PATH": "~/Download/",
48 CONFIG_NAMESPACE + "HEALTHCHECKS_SYNC_SEEDBOX_ENABLED": False,
49 CONFIG_NAMESPACE + "HEALTHCHECKS_SYNC_SEEDBOX_PING_URL": "",
50 CONFIG_NAMESPACE + "HEALTHCHECKS_SYNC_BLACKHOLE_ENABLED": False,
51 CONFIG_NAMESPACE + "HEALTHCHECKS_SYNC_BLACKHOLE_PING_URL": "",
52 CONFIG_NAMESPACE + "WEBUI_THEME": "auto",
53 CONFIG_NAMESPACE + "WEBUI_LANGUAGE": "auto",
54 CONFIG_NAMESPACE + "WEBUI_STATS_BAR_Y_LIMIT": "10",
55 CONFIG_NAMESPACE + "WEBUI_DASHBOARD_REFRESH": "60",
56 CONFIG_NAMESPACE + "WEBUI_DASHBOARD_NB_ELEMENTS": "5",
57 }
59 def __init__(self, app: Flask, test_config: dict[str, str] | None = None) -> None:
60 """
61 Initialize a new Config instance.
63 Args:
64 app (Flask): The Flask application to configure.
65 test_config (dict[str, str] | None): Configuration for testing.
66 """
67 self.app = app
68 self.app.config.from_prefixed_env() # Set from env prefixed by 'FLASK_'
70 # Load config from database
71 db_config = Config._load_config_from_database(self.app)
72 self.app.config.from_mapping(db_config)
74 self._check_config() # Do all checks
76 self.app.config.setdefault("CACHE_TYPE", "SimpleCache") # Init Flask Cache
77 self.app.config.setdefault("SWAGGER_UI_DOC_EXPANSION", "list") # Expense swager namespaces
78 self.app.config.setdefault("PROPAGATE_EXCEPTIONS", False)
79 self.app.config.setdefault("LOGIN_DISABLED", self.app.config.get(Config.CONFIG_NAMESPACE + "LOGIN_DISABLED", False)) # Disable login
80 self.app.config.setdefault("WTF_CSRF_ENABLED", self.app.config.get(Config.CONFIG_NAMESPACE + "WTF_CSRF_ENABLED", True)) # Disable CSRF
82 def _check_config(self) -> None:
83 """Check all configurations needed."""
84 # SECRET_KEY warning
85 if self.app.config.get("SECRET_KEY") is None and self.app.config["TESTING"] is None:
86 self.app.logger.warning("Warning: SECRET_KEY is still not set. Set it in production to secure your sessions.")
88 @staticmethod
89 def _load_config_from_database(app: Flask) -> dict[str, str]:
90 """Load application configuration from the database."""
91 config = Config.DEFAULT_CONFIG.copy()
93 db_config = typed_peewee_dicts(
94 SeedboxSync.select(
95 SeedboxSync.key,
96 SeedboxSync.value,
97 )
98 .where(SeedboxSync.key.startswith(Config.DB_CONFIG_PREFIX))
99 .dicts()
100 )
102 for conf in db_config:
103 config_key = Config.CONFIG_NAMESPACE + conf["key"].removeprefix("config_").upper()
104 value = Config._convert_config_value(config_key, conf["value"])
105 display_value = "*****" if "PASSWORD" in config_key.upper() else value
107 app.logger.debug(f"Loading config from database: {config_key} = {display_value}")
108 if config_key not in Config.DEFAULT_CONFIG:
109 app.logger.warning("Ignoring unknown configuration key: %s", conf["key"])
110 continue
111 config[config_key] = value
113 return config
115 @staticmethod
116 def _convert_config_value(config_key: str, value: Any) -> Any:
117 """
118 Convert a raw database configuration value to its expected Python type.
120 Boolean options whose names end with ``_ENABLED`` are converted to
121 ``True`` or ``False``. ``SEEDBOX_TIMEOUT`` is converted to an integer
122 when defined, otherwise to ``False``. ``SEEDBOX_CHMOD`` is converted
123 from an octal string to an integer when defined, otherwise to ``False``.
125 All other values are returned unchanged.
127 Args:
128 config_key: Full Flask configuration key.
129 value: Raw value loaded from the database.
131 Returns:
132 Any: The converted configuration value.
133 """
134 normalized_key = config_key.upper()
135 raw_value = str(value or "").strip()
137 if normalized_key.endswith(("_ENABLED", "_DISABLED")):
138 return raw_value.lower() not in {
139 "",
140 "0",
141 }
143 if normalized_key.endswith("SEEDBOX_TIMEOUT"):
144 if not raw_value:
145 return False
146 timeout = int(raw_value)
147 return timeout if timeout > 0 else False
149 if normalized_key.endswith("SEEDBOX_CHMOD"):
150 if not raw_value:
151 return False
152 return raw_value if raw_value not in ["", "0"] else False
154 return value
156 @staticmethod
157 def reload_config(app: Flask) -> dict[str, str]:
158 """Reload application configuration from the database."""
159 return Config._load_config_from_database(app)