Skip to content

Commit 85b7883

Browse files
authored
Merge pull request #35 from Cyber-Syntax:feat/improve-config
feat(config)!: Migrate configuration to INI format and enhance logging features
2 parents 390dc02 + 0075cc0 commit 85b7883

13 files changed

Lines changed: 1014 additions & 297 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Changelog
22
All notable changes to this project will be documented in this file. Commits automatically generated by github actions.
33

4+
## v0.6.0-beta
5+
### BREAKING CHANGES
6+
- The configuration file format has been changed from JSON to INI. Now located at `~/.config/autotarcompress/config.conf`. Please migrate your existing configuration accordingly.
7+
8+
#### Migration Steps
9+
Script will create a new config file in the new format if it does not exist. Please manually transfer your settings from the old JSON file to the new INI file, following the comments provided in the new config file for guidance.
10+
411
## v0.5.0-beta
512
### Changes
613
This release adds the `info` command to display details about the latest backup, including directories backed up and backup file status.

autotarcompress/config.py

Lines changed: 187 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,105 @@
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
68
import datetime
7-
import json
89
import logging
910
from dataclasses import dataclass, field
1011
from pathlib import Path
11-
from typing import List, Tuple
12+
from typing import Tuple
1213

1314

1415
@dataclass
1516
class 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

Comments
 (0)