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

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_DOUGHNUT_LEGEND": "hidden", 

55 CONFIG_NAMESPACE + "WEBUI_DOUGHNUT_LEGEND_LIMIT": "0", 

56 } 

57 

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

59 """ 

60 Initialize a new Config instance. 

61 

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

68 

69 # Load config from database 

70 db_config = Config._load_config_from_database(self.app) 

71 self.app.config.from_mapping(db_config) 

72 

73 self._check_config() # Do all checks 

74 

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 

80 

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

86 

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

91 

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 ) 

100 

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 

105 

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 

111 

112 return config 

113 

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. 

118 

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

123 

124 All other values are returned unchanged. 

125 

126 Args: 

127 config_key: Full Flask configuration key. 

128 value: Raw value loaded from the database. 

129 

130 Returns: 

131 Any: The converted configuration value. 

132 """ 

133 normalized_key = config_key.upper() 

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

135 

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

137 return raw_value.lower() not in { 

138 "", 

139 "0", 

140 } 

141 

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 

147 

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 

152 

153 return value 

154 

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)