Coverage for seedboxsync/core/config.py: 96%
54 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 17:14 +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_DOUGHNUT_LEGEND": "hidden",
55 CONFIG_NAMESPACE + "WEBUI_DOUGHNUT_LEGEND_LIMIT": "0",
56 }
58 def __init__(self, app: Flask, test_config: dict[str, str] | None = None) -> None:
59 """
60 Initialize a new Config instance.
62 Args:
63 app (Flask): The Flask application to configure.
64 test_config (dict[str, str] | None): Configuration for testing.
65 """
66 self.app = app
67 self.app.config.from_prefixed_env() # Set from env prefixed by 'FLASK_'
69 # Load config from database
70 db_config = Config._load_config_from_database(self.app)
71 self.app.config.from_mapping(db_config)
73 self._check_config() # Do all checks
75 self.app.config.setdefault("CACHE_TYPE", "SimpleCache") # Init Flask Cache
76 self.app.config.setdefault("SWAGGER_UI_DOC_EXPANSION", "list") # Expense swager namespaces
77 self.app.config.setdefault("PROPAGATE_EXCEPTIONS", False)
78 self.app.config.setdefault("LOGIN_DISABLED", self.app.config.get(Config.CONFIG_NAMESPACE + "LOGIN_DISABLED", False)) # Disable login
79 self.app.config.setdefault("WTF_CSRF_ENABLED", self.app.config.get(Config.CONFIG_NAMESPACE + "WTF_CSRF_ENABLED", True)) # Disable CSRF
81 def _check_config(self) -> None:
82 """Check all configurations needed."""
83 # SECRET_KEY warning
84 if self.app.config.get("SECRET_KEY") is None and self.app.config["TESTING"] is None:
85 self.app.logger.warning("Warning: SECRET_KEY is still not set. Set it in production to secure your sessions.")
87 @staticmethod
88 def _load_config_from_database(app: Flask) -> dict[str, str]:
89 """Load application configuration from the database."""
90 config = Config.DEFAULT_CONFIG.copy()
92 db_config = typed_peewee_dicts(
93 SeedboxSync.select(
94 SeedboxSync.key,
95 SeedboxSync.value,
96 )
97 .where(SeedboxSync.key.startswith(Config.DB_CONFIG_PREFIX))
98 .dicts()
99 )
101 for conf in db_config:
102 config_key = Config.CONFIG_NAMESPACE + conf["key"].removeprefix("config_").upper()
103 value = Config._convert_config_value(config_key, conf["value"])
104 display_value = "*****" if "PASSWORD" in config_key.upper() else value
106 app.logger.debug(f"Loading config from database: {config_key} = {display_value}")
107 if config_key not in Config.DEFAULT_CONFIG:
108 app.logger.warning("Ignoring unknown configuration key: %s", conf["key"])
109 continue
110 config[config_key] = value
112 return config
114 @staticmethod
115 def _convert_config_value(config_key: str, value: Any) -> Any:
116 """
117 Convert a raw database configuration value to its expected Python type.
119 Boolean options whose names end with ``_ENABLED`` are converted to
120 ``True`` or ``False``. ``SEEDBOX_TIMEOUT`` is converted to an integer
121 when defined, otherwise to ``False``. ``SEEDBOX_CHMOD`` is converted
122 from an octal string to an integer when defined, otherwise to ``False``.
124 All other values are returned unchanged.
126 Args:
127 config_key: Full Flask configuration key.
128 value: Raw value loaded from the database.
130 Returns:
131 Any: The converted configuration value.
132 """
133 normalized_key = config_key.upper()
134 raw_value = str(value or "").strip()
136 if normalized_key.endswith(("_ENABLED", "_DISABLED")):
137 return raw_value.lower() not in {
138 "",
139 "0",
140 }
142 if normalized_key.endswith("SEEDBOX_TIMEOUT"):
143 if not raw_value:
144 return False
145 timeout = int(raw_value)
146 return timeout if timeout > 0 else False
148 if normalized_key.endswith("SEEDBOX_CHMOD"):
149 if not raw_value:
150 return False
151 return raw_value if raw_value not in ["", "0"] else False
153 return value
155 @staticmethod
156 def reload_config(app: Flask) -> dict[str, str]:
157 """Reload application configuration from the database."""
158 return Config._load_config_from_database(app)