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

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.""" 

8 

9from typing import Any, ClassVar 

10from flask import Flask 

11from seedboxsync.core.database.models import SeedboxSync, typed_peewee_dicts 

12 

13 

14class Config: 

15 """Config.""" 

16 

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 } 

58 

59 def __init__(self, app: Flask, test_config: dict[str, str] | None = None) -> None: 

60 """ 

61 Initialize a new Config instance. 

62 

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_' 

69 

70 # Load config from database 

71 db_config = Config._load_config_from_database(self.app) 

72 self.app.config.from_mapping(db_config) 

73 

74 self._check_config() # Do all checks 

75 

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 

81 

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.") 

87 

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() 

92 

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 ) 

101 

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 

106 

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 

112 

113 return config 

114 

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. 

119 

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``. 

124 

125 All other values are returned unchanged. 

126 

127 Args: 

128 config_key: Full Flask configuration key. 

129 value: Raw value loaded from the database. 

130 

131 Returns: 

132 Any: The converted configuration value. 

133 """ 

134 normalized_key = config_key.upper() 

135 raw_value = str(value or "").strip() 

136 

137 if normalized_key.endswith(("_ENABLED", "_DISABLED")): 

138 return raw_value.lower() not in { 

139 "", 

140 "0", 

141 } 

142 

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 

148 

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 

153 

154 return value 

155 

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)