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

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.dao 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 + "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 } 

45 

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

47 """ 

48 Initialize a new Config instance. 

49 

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

56 

57 # Load config from database 

58 db_config = Config._load_config_from_database(self.app) 

59 self.app.config.from_mapping(db_config) 

60 

61 self._check_config() # Do all checks 

62 

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) 

66 

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

72 

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

77 

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 ) 

86 

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 

91 

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 

97 

98 return config 

99 

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. 

104 

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

109 

110 All other values are returned unchanged. 

111 

112 Args: 

113 config_key: Full Flask configuration key. 

114 value: Raw value loaded from the database. 

115 

116 Returns: 

117 Any: The converted configuration value. 

118 """ 

119 normalized_key = config_key.upper() 

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

121 

122 if normalized_key.endswith("_ENABLED"): 

123 return raw_value.lower() not in { 

124 "", 

125 "0", 

126 } 

127 

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 

133 

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 

138 

139 return value 

140 

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)