Coverage for seedboxsync/core/config.py: 96%
52 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 17:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 17:46 +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.dao 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 + "SYNC_BLACKHOLE_ENABLED": False,
21 CONFIG_NAMESPACE + "SYNC_SEEDBOX_ENABLED": False,
22 CONFIG_NAMESPACE + "SEEDBOX_HOST": "my-seedbox.ltd",
23 CONFIG_NAMESPACE + "SEEDBOX_PORT": "22",
24 CONFIG_NAMESPACE + "SEEDBOX_LOGIN": "me",
25 CONFIG_NAMESPACE + "SEEDBOX_PASSWORD": "p4sw0rd",
26 CONFIG_NAMESPACE + "SEEDBOX_TIMEOUT": False,
27 CONFIG_NAMESPACE + "SEEDBOX_PROTOCOL": "sftp",
28 CONFIG_NAMESPACE + "SEEDBOX_MAX_CONCURRENT_PREFETCH_REQUESTS": "128",
29 CONFIG_NAMESPACE + "SEEDBOX_CHMOD": False,
30 CONFIG_NAMESPACE + "SEEDBOX_TMP_PATH": "./tmp",
31 CONFIG_NAMESPACE + "SEEDBOX_WATCH_PATH": "./watch",
32 CONFIG_NAMESPACE + "SEEDBOX_FINISHED_PATH": "./files",
33 CONFIG_NAMESPACE + "SEEDBOX_PREFIXED_PATH": "/files",
34 CONFIG_NAMESPACE + "SEEDBOX_PART_SUFFIX": ".part",
35 CONFIG_NAMESPACE + "SEEDBOX_EXCLUDE_SYNCING": "",
36 CONFIG_NAMESPACE + "LOCAL_WATCH_PATH": "~/watch",
37 CONFIG_NAMESPACE + "LOCAL_DOWNLOAD_PATH": "~/Download/",
38 CONFIG_NAMESPACE + "HEALTHCHECKS_SYNC_SEEDBOX_ENABLED": False,
39 CONFIG_NAMESPACE + "HEALTHCHECKS_SYNC_SEEDBOX_PING_URL": "",
40 CONFIG_NAMESPACE + "HEALTHCHECKS_SYNC_BLACKHOLE_ENABLED": False,
41 CONFIG_NAMESPACE + "HEALTHCHECKS_SYNC_BLACKHOLE_PING_URL": "",
42 CONFIG_NAMESPACE + "WEBUI_THEME": "auto",
43 CONFIG_NAMESPACE + "WEBUI_LANGUAGE": "auto",
44 }
46 def __init__(self, app: Flask, test_config: dict[str, str] | None = None) -> None:
47 """
48 Initialize a new Config instance.
50 Args:
51 app (Flask): The Flask application to configure.
52 test_config (dict[str, str] | None): Configuration for testing.
53 """
54 self.app = app
55 self.app.config.from_prefixed_env() # Set from env prefixed by 'FLASK_'
57 # Load config from database
58 db_config = Config._load_config_from_database(self.app)
59 self.app.config.from_mapping(db_config)
61 self._check_config() # Do all checks
63 self.app.config.setdefault("CACHE_TYPE", "SimpleCache") # Init Flask Cache
64 self.app.config.setdefault("SWAGGER_UI_DOC_EXPANSION", "list") # Expense swager namespaces
65 self.app.config.setdefault("PROPAGATE_EXCEPTIONS", False)
67 def _check_config(self) -> None:
68 """Check all configurations needed."""
69 # SECRET_KEY warning
70 if self.app.config.get("SECRET_KEY") is None and self.app.config["TESTING"] is None:
71 self.app.logger.warning("Warning: SECRET_KEY is still not set. Set it in production to secure your sessions.")
73 @staticmethod
74 def _load_config_from_database(app: Flask) -> dict[str, str]:
75 """Load application configuration from the database."""
76 config = Config.DEFAULT_CONFIG.copy()
78 db_config = typed_peewee_dicts(
79 SeedboxSync.select(
80 SeedboxSync.key,
81 SeedboxSync.value,
82 )
83 .where(SeedboxSync.key.startswith(Config.DB_CONFIG_PREFIX))
84 .dicts()
85 )
87 for conf in db_config:
88 config_key = Config.CONFIG_NAMESPACE + conf["key"].removeprefix("config_").upper()
89 value = Config._convert_config_value(config_key, conf["value"])
90 display_value = "*****" if "PASSWORD" in config_key.upper() else value
92 app.logger.debug(f"Loading config from database: {config_key} = {display_value}")
93 if config_key not in Config.DEFAULT_CONFIG:
94 app.logger.warning("Ignoring unknown configuration key: %s", conf["key"])
95 continue
96 config[config_key] = value
98 return config
100 @staticmethod
101 def _convert_config_value(config_key: str, value: Any) -> Any:
102 """
103 Convert a raw database configuration value to its expected Python type.
105 Boolean options whose names end with ``_ENABLED`` are converted to
106 ``True`` or ``False``. ``SEEDBOX_TIMEOUT`` is converted to an integer
107 when defined, otherwise to ``False``. ``SEEDBOX_CHMOD`` is converted
108 from an octal string to an integer when defined, otherwise to ``False``.
110 All other values are returned unchanged.
112 Args:
113 config_key: Full Flask configuration key.
114 value: Raw value loaded from the database.
116 Returns:
117 Any: The converted configuration value.
118 """
119 normalized_key = config_key.upper()
120 raw_value = str(value or "").strip()
122 if normalized_key.endswith("_ENABLED"):
123 return raw_value.lower() not in {
124 "",
125 "0",
126 }
128 if normalized_key.endswith("SEEDBOX_TIMEOUT"):
129 if not raw_value:
130 return False
131 timeout = int(raw_value)
132 return timeout if timeout > 0 else False
134 if normalized_key.endswith("SEEDBOX_CHMOD"):
135 if not raw_value:
136 return False
137 return raw_value if raw_value not in ["", "0"] else False
139 return value
141 @staticmethod
142 def reload_config(app: Flask) -> dict[str, str]:
143 """Reload application configuration from the database."""
144 return Config._load_config_from_database(app)