11"""Configuration management for backup system.
22
3- This module handles loading, saving, and validation of backup configuration.
3+ Handles loading, saving, and validation of backup configuration using INI (.conf) files.
4+ Comments are supported in the config file.
45"""
56
7+ import configparser
68import datetime
7- import json
89import logging
910from dataclasses import dataclass , field
1011from pathlib import Path
11- from typing import List , Tuple
12+ from typing import Tuple
1213
1314
1415@dataclass
1516class BackupConfig :
16- """Configuration data for backup manager (paths, retention, targets)."""
17+ """Configuration data for backup manager (paths, retention, targets).
18+
19+ Uses INI (.conf) file for configuration, allowing comments.
20+ Default values provide practical examples for common backup and ignore
21+ paths.
22+ """
1723
1824 backup_folder : str = "~/Documents/backup-for-cloud/"
1925 config_dir : str = "~/.config/autotarcompress"
20- keep_backup : int = 1
26+ keep_backup : int = 0
2127 keep_enc_backup : int = 1
22- dirs_to_backup : list [str ] = field (default_factory = list )
23- ignore_list : list [str ] = field (default_factory = list )
28+ log_level : str = "INFO"
29+ # List of directories to include in the backup.
30+ # These are typically user data, browser profiles, documents, photos,
31+ # application configs, and dotfiles. Adjust this list to match the
32+ # important data you want to preserve. Each path is expanded to an
33+ # absolute path at runtime.
34+ dirs_to_backup : list [str ] = field (
35+ default_factory = lambda : [
36+ "~/.zen/qknutvmw.Default Profile/" ,
37+ "~/.config/BraveSoftware/Brave-Browser/Default" ,
38+ "~/Photos" ,
39+ "~/Pictures" ,
40+ "~/Documents" ,
41+ "~/.config/syncthing" ,
42+ "~/.config/my-unicorn/" ,
43+ "~/.config/FreeTube" ,
44+ "~/dotfiles" ,
45+ ]
46+ )
47+
48+ # List of directories or patterns to exclude from the backup.
49+ # These are typically large, redundant, or auto-generated folders
50+ # such as build artifacts, caches, version control folders, and
51+ # backup destinations themselves. Adjust this list to avoid backing
52+ # up unnecessary or volatile data.
53+ ignore_list : list [str ] = field (
54+ default_factory = lambda : [
55+ "~/Documents/global-repos" ,
56+ "~/Documents/backup-for-cloud" ,
57+ "~/Documents/.stversions" ,
58+ "node_modules" ,
59+ ".venv" ,
60+ "__pycache__" ,
61+ ".ruff_cache" ,
62+ "~/Documents/my-repos/rust-unicorn/target" ,
63+ "lock" ,
64+ "chrome" ,
65+ ".bin" ,
66+ ]
67+ )
2468
2569 def __post_init__ (self ) -> None :
2670 """Expand all configured paths after initialization."""
2771 self .backup_folder = str (Path (self .backup_folder ).expanduser ())
2872 self .ignore_list = [str (Path (p ).expanduser ()) for p in self .ignore_list ]
2973 self .dirs_to_backup = [str (Path (d ).expanduser ()) for d in self .dirs_to_backup ]
3074 self .config_dir = str (Path (self .config_dir ).expanduser ())
75+ # Validate and normalize log level
76+ self .log_level = self ._validate_log_level (self .log_level )
77+
78+ def _validate_log_level (self , level : str ) -> str :
79+ """Validate and normalize the log level string.
80+
81+ Args:
82+ level (str): The log level string to validate.
83+
84+ Returns:
85+ str: A valid log level string (uppercase).
86+
87+ """
88+ valid_levels = {"DEBUG" , "INFO" , "WARNING" , "ERROR" , "CRITICAL" }
89+ level_upper = level .upper ()
90+ if level_upper not in valid_levels :
91+ logging .warning ("Invalid log level '%s', defaulting to INFO" , level )
92+ return "INFO"
93+ return level_upper
94+
95+ def get_log_level (self ) -> int :
96+ """Convert the string log level to logging module constant.
97+
98+ Returns:
99+ int: The logging level constant.
100+
101+ """
102+ return getattr (logging , self .log_level , logging .INFO )
31103
32104 @property
33105 def current_date (self ) -> str :
@@ -36,50 +108,122 @@ def current_date(self) -> str:
36108
37109 @property
38110 def config_path (self ) -> Path :
39- """Return the full path to the config file."""
40- return Path (self .config_dir ) / "config.json "
111+ """Return the full path to the config file (INI format) ."""
112+ return Path (self .config_dir ) / "config.conf "
41113
42114 @property
43115 def backup_path (self ) -> Path :
44116 """Return the full path to the backup file."""
45117 return Path (self .backup_folder ) / f"{ self .current_date } .tar.xz"
46118
47119 def save (self ) -> None :
48- """Save current configuration to the config file."""
49- config_data = {
50- "backup_folder" : self .backup_folder ,
51- "config_dir" : self .config_dir ,
52- "keep_backup" : self .keep_backup ,
53- "keep_enc_backup" : self .keep_enc_backup ,
54- "dirs_to_backup" : self .dirs_to_backup ,
55- "ignore_list" : self .ignore_list ,
56- }
57-
58- # Ensure the config directory exists
120+ """Save current configuration to the config file in INI format.
121+
122+ Comments are supported in the config file.
123+ Lists are saved as INI multi-line values for user-friendly editing.
124+ Explanatory comments are written to the config file for user guidance.
125+ """
59126 Path (self .config_dir ).mkdir (parents = True , exist_ok = True )
127+
128+ # Write config with inline comments for each setting
60129 with open (self .config_path , "w" , encoding = "utf-8" ) as f :
61- json .dump (config_data , f , indent = 4 )
130+ f .write ("[DEFAULT]\n " )
131+
132+ # log_level
133+ f .write ("# Logging level for the application.\n " )
134+ f .write ("# Valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL\n " )
135+ f .write ("# DEBUG: Detailed information for debugging purposes\n " )
136+ f .write ("# INFO: General information about application flow\n " )
137+ f .write ("# WARNING: Warning messages for unusual conditions\n " )
138+ f .write ("# ERROR: Error messages for failure conditions\n " )
139+ f .write ("# CRITICAL: Critical error messages\n " )
140+ f .write (f"log_level = { self .log_level } \n \n " )
141+
142+ # backup_folder
143+ f .write ("# Directory where backup archives will be stored.\n " )
144+ f .write ("# Default: ~/Documents/backup-for-cloud/\n " )
145+ f .write (f"backup_folder = { self .backup_folder } \n \n " )
146+
147+ # config_dir
148+ f .write ("# Directory where this config file is saved.\n " )
149+ f .write ("# Default: ~/.config/autotarcompress\n " )
150+ f .write (f"config_dir = { self .config_dir } \n \n " )
151+
152+ # keep_backup
153+ f .write ("# Number of unencrypted backups to keep (0 = unlimited).\n " )
154+ f .write ("# Useful for retention policy and disk space management.\n " )
155+ f .write (f"keep_backup = { self .keep_backup } \n \n " )
156+
157+ # keep_enc_backup
158+ f .write ("# Number of encrypted backups to keep (0 = unlimited).\n " )
159+ f .write ("# Applies to encrypted backup retention.\n " )
160+ f .write (f"keep_enc_backup = { self .keep_enc_backup } \n \n " )
161+
162+ # dirs_to_backup
163+ f .write ("# List of directories to include in the backup.\n " )
164+ f .write ("# Typically user data, browser profiles, documents, photos,\n " )
165+ f .write ("# application configs, and dotfiles. Adjust this list to match\n " )
166+ f .write ("# the important data you want to preserve. Each path is expanded\n " )
167+ f .write ("# to an absolute path at runtime.\n " )
168+ f .write ("dirs_to_backup =\n " )
169+ for dir_path in self .dirs_to_backup :
170+ f .write (f"\t { dir_path } \n " )
171+ f .write ("\n " )
172+
173+ # ignore_list
174+ f .write ("# List of directories or patterns to exclude from the backup.\n " )
175+ f .write ("# Typically large, redundant, or auto-generated folders such as build\n " )
176+ f .write ("# artifacts, caches, version control folders, and backup destinations\n " )
177+ f .write ("# themselves. Adjust this list to avoid backing up unnecessary or\n " )
178+ f .write ("# volatile data.\n " )
179+ f .write ("ignore_list =\n " )
180+ for ignore_path in self .ignore_list :
181+ f .write (f"\t { ignore_path } \n " )
182+
62183 logging .info ("Configuration saved to %s" , self .config_path )
63184
64185 @classmethod
65186 def load (cls ) -> "BackupConfig" :
66- """Load configuration from file or create with defaults if not exists."""
187+ """Load config from INI file or create with defaults if missing.
188+
189+ Lists are loaded as INI multi-line values for user-friendly editing.
190+ """
67191 default_config = cls ()
68192 config_path = default_config .config_path
69193
70194 if config_path .exists ():
195+ config = configparser .ConfigParser ()
71196 try :
72- with open (config_path , encoding = "utf-8" ) as f :
73- config_data = json .load (f )
197+ config .read (config_path , encoding = "utf-8" )
198+ section = config ["DEFAULT" ]
199+ # Parse fields
200+ backup_folder = section .get ("backup_folder" , default_config .backup_folder )
201+ config_dir = section .get ("config_dir" , default_config .config_dir )
202+ keep_backup = int (section .get ("keep_backup" , default_config .keep_backup ))
203+ keep_enc_backup = int (
204+ section .get ("keep_enc_backup" , default_config .keep_enc_backup )
205+ )
206+ log_level = section .get ("log_level" , default_config .log_level ).upper ()
207+ # Parse multi-line lists
208+
209+ def parse_multiline_list (val : str ) -> list [str ]:
210+ if not val :
211+ return []
212+ return [line .strip () for line in val .strip ().splitlines () if line .strip ()]
74213
75- # Filter out unknown fields (like old last_backup field)
76- valid_fields = {field .name for field in cls .__dataclass_fields__ .values ()}
77- filtered_data = {
78- key : value for key , value in config_data .items () if key in valid_fields
79- }
214+ dirs_to_backup = parse_multiline_list (section .get ("dirs_to_backup" , "" ))
215+ ignore_list = parse_multiline_list (section .get ("ignore_list" , "" ))
80216
81- return cls (** filtered_data )
82- except json .JSONDecodeError as e :
217+ return cls (
218+ backup_folder = backup_folder ,
219+ config_dir = config_dir ,
220+ keep_backup = keep_backup ,
221+ keep_enc_backup = keep_enc_backup ,
222+ log_level = log_level ,
223+ dirs_to_backup = dirs_to_backup ,
224+ ignore_list = ignore_list ,
225+ )
226+ except Exception as e :
83227 logging .error ("Error reading config file: %s" , e )
84228 logging .warning ("Using default configuration" )
85229 return default_config
@@ -103,30 +247,25 @@ def verify_config(cls) -> Tuple[bool, str]:
103247 return False , f"Configuration file not found at { config_path } "
104248
105249 try :
106- # Try to load the configuration
107- with open (config_path , encoding = "utf-8" ) as f :
108- config_data = json .load (f )
109-
110- config = cls (** config_data )
250+ config = configparser .ConfigParser ()
251+ config .read (config_path , encoding = "utf-8" )
252+ section = config ["DEFAULT" ]
253+ dirs_to_backup = [
254+ d .strip () for d in section .get ("dirs_to_backup" , "" ).split ("," ) if d .strip ()
255+ ]
256+ backup_folder = section .get ("backup_folder" , default_config .backup_folder )
111257
112- # Validate essential configuration
113- if not config .dirs_to_backup :
258+ if not dirs_to_backup :
114259 return False , "No backup directories configured"
115260
116- # Check if backup folder exists or can be created
117- backup_folder = Path (config .backup_folder )
118- if not backup_folder .exists ():
261+ backup_folder_path = Path (backup_folder ).expanduser ()
262+ if not backup_folder_path .exists ():
119263 try :
120- backup_folder .mkdir (parents = True , exist_ok = True )
264+ backup_folder_path .mkdir (parents = True , exist_ok = True )
121265 except OSError :
122- return False , f"Cannot create backup folder at { backup_folder } "
266+ return ( False , f"Cannot create backup folder at { backup_folder_path } " )
123267
124- # All checks passed
125268 return True , "Configuration is valid"
126269
127- except json .JSONDecodeError :
128- return False , "Configuration file is corrupt or invalid JSON"
129- except KeyError as e :
130- return False , f"Missing required configuration key: { e } "
131270 except Exception as e :
132271 return False , f"Configuration validation error: { e !s} "
0 commit comments