dar-backup 1.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,11 @@
1
1
  # SPDX-License-Identifier: GPL-3.0-or-later
2
2
 
3
3
  import configparser
4
+ import re
4
5
  from dataclasses import dataclass, field, fields
5
6
  from os.path import expandvars, expanduser
6
7
  from pathlib import Path
8
+ from typing import Optional, Pattern
7
9
 
8
10
  from dar_backup.exceptions import ConfigSettingsError
9
11
 
@@ -44,8 +46,19 @@ class ConfigSettings:
44
46
  incr_age: int = field(init=False)
45
47
  error_correction_percent: int = field(init=False)
46
48
  par2_enabled: bool = field(init=False)
49
+ par2_dir: Optional[str] = field(init=False, default=None)
50
+ par2_layout: Optional[str] = field(init=False, default=None)
51
+ par2_mode: Optional[str] = field(init=False, default=None)
52
+ par2_ratio_full: Optional[int] = field(init=False, default=None)
53
+ par2_ratio_diff: Optional[int] = field(init=False, default=None)
54
+ par2_ratio_incr: Optional[int] = field(init=False, default=None)
55
+ par2_run_verify: Optional[bool] = field(init=False, default=None)
47
56
  logfile_max_bytes: int = field(init=False)
48
57
  logfile_no_count: int = field(init=False)
58
+ dar_backup_discord_webhook_url: Optional[str] = field(init=False, default=None)
59
+ restoretest_exclude_prefixes: list[str] = field(init=False, default_factory=list)
60
+ restoretest_exclude_suffixes: list[str] = field(init=False, default_factory=list)
61
+ restoretest_exclude_regex: Optional[Pattern[str]] = field(init=False, default=None)
49
62
 
50
63
 
51
64
  OPTIONAL_CONFIG_FIELDS = [
@@ -70,6 +83,13 @@ class ConfigSettings:
70
83
  "type": int,
71
84
  "default": 5,
72
85
  },
86
+ {
87
+ "section": "MISC",
88
+ "key": "DAR_BACKUP_DISCORD_WEBHOOK_URL",
89
+ "attr": "dar_backup_discord_webhook_url",
90
+ "type": str,
91
+ "default": None,
92
+ },
73
93
  # Add more optional fields here
74
94
  ]
75
95
 
@@ -103,6 +123,29 @@ class ConfigSettings:
103
123
  else:
104
124
  raise ConfigSettingsError(f"Invalid boolean value for 'ENABLED' in [PAR2]: '{val}'")
105
125
 
126
+ self.par2_dir = self._get_optional_str("PAR2", "PAR2_DIR", default=None)
127
+ self.par2_layout = self._get_optional_str("PAR2", "PAR2_LAYOUT", default="by-backup")
128
+ self.par2_mode = self._get_optional_str("PAR2", "PAR2_MODE", default=None)
129
+ self.par2_ratio_full = self._get_optional_int("PAR2", "PAR2_RATIO_FULL", default=None)
130
+ self.par2_ratio_diff = self._get_optional_int("PAR2", "PAR2_RATIO_DIFF", default=None)
131
+ self.par2_ratio_incr = self._get_optional_int("PAR2", "PAR2_RATIO_INCR", default=None)
132
+ self.par2_run_verify = self._get_optional_bool("PAR2", "PAR2_RUN_VERIFY", default=None)
133
+ self.restoretest_exclude_prefixes = self._get_optional_csv_list(
134
+ "MISC",
135
+ "RESTORETEST_EXCLUDE_PREFIXES",
136
+ default=[]
137
+ )
138
+ self.restoretest_exclude_suffixes = self._get_optional_csv_list(
139
+ "MISC",
140
+ "RESTORETEST_EXCLUDE_SUFFIXES",
141
+ default=[]
142
+ )
143
+ self.restoretest_exclude_regex = self._get_optional_regex(
144
+ "MISC",
145
+ "RESTORETEST_EXCLUDE_REGEX",
146
+ default=None
147
+ )
148
+
106
149
  # Load optional fields
107
150
  for opt in self.OPTIONAL_CONFIG_FIELDS:
108
151
  if self.config.has_option(opt['section'], opt['key']):
@@ -144,4 +187,104 @@ class ConfigSettings:
144
187
  ]
145
188
  return f"<ConfigSettings({', '.join(safe_fields)})>"
146
189
 
190
+ def _get_optional_str(self, section: str, key: str, default: Optional[str] = None) -> Optional[str]:
191
+ if self.config.has_option(section, key):
192
+ return self.config.get(section, key).strip()
193
+ return default
194
+
195
+ def _get_optional_int(self, section: str, key: str, default: Optional[int] = None) -> Optional[int]:
196
+ if self.config.has_option(section, key):
197
+ raw = self.config.get(section, key).strip()
198
+ return int(raw)
199
+ return default
200
+
201
+ def _get_optional_bool(self, section: str, key: str, default: Optional[bool] = None) -> Optional[bool]:
202
+ if not self.config.has_option(section, key):
203
+ return default
204
+ val = self.config.get(section, key).strip().lower()
205
+ if val in ('true', '1', 'yes'):
206
+ return True
207
+ if val in ('false', '0', 'no'):
208
+ return False
209
+ raise ConfigSettingsError(f"Invalid boolean value for '{key}' in [{section}]: '{val}'")
210
+
211
+ def _get_optional_csv_list(self, section: str, key: str, default: Optional[list[str]] = None) -> list[str]:
212
+ if not self.config.has_option(section, key):
213
+ return default if default is not None else []
214
+ raw = self.config.get(section, key).strip()
215
+ if not raw:
216
+ return default if default is not None else []
217
+ return [item.strip() for item in raw.split(",") if item.strip()]
218
+
219
+ def _get_optional_regex(
220
+ self,
221
+ section: str,
222
+ key: str,
223
+ default: Optional[Pattern[str]] = None
224
+ ) -> Optional[Pattern[str]]:
225
+ if not self.config.has_option(section, key):
226
+ return default
227
+ raw = self.config.get(section, key).strip()
228
+ if not raw:
229
+ return default
230
+ try:
231
+ return re.compile(raw, re.IGNORECASE)
232
+ except re.error as exc:
233
+ raise ConfigSettingsError(
234
+ f"Invalid regex for '{key}' in [{section}]: {exc}"
235
+ ) from exc
236
+
237
+ def get_par2_config(self, backup_definition: Optional[str] = None) -> dict:
238
+ """
239
+ Return PAR2 settings, applying per-backup overrides when present.
240
+ """
241
+ par2_config = {
242
+ "par2_dir": self.par2_dir,
243
+ "par2_layout": self.par2_layout,
244
+ "par2_mode": self.par2_mode,
245
+ "par2_ratio_full": self.par2_ratio_full,
246
+ "par2_ratio_diff": self.par2_ratio_diff,
247
+ "par2_ratio_incr": self.par2_ratio_incr,
248
+ "par2_run_verify": self.par2_run_verify,
249
+ "par2_enabled": self.par2_enabled,
250
+ }
251
+
252
+ if not backup_definition or not self.config.has_section(backup_definition):
253
+ return par2_config
254
+
255
+ section = self.config[backup_definition]
256
+ for raw_key, raw_value in section.items():
257
+ key = raw_key.upper()
258
+ value = raw_value.strip()
259
+ if not key.startswith("PAR2_"):
260
+ continue
261
+ if key == "PAR2_DIR":
262
+ par2_config["par2_dir"] = value
263
+ elif key == "PAR2_LAYOUT":
264
+ par2_config["par2_layout"] = value
265
+ elif key == "PAR2_MODE":
266
+ par2_config["par2_mode"] = value
267
+ elif key == "PAR2_RATIO_FULL":
268
+ par2_config["par2_ratio_full"] = int(value)
269
+ elif key == "PAR2_RATIO_DIFF":
270
+ par2_config["par2_ratio_diff"] = int(value)
271
+ elif key == "PAR2_RATIO_INCR":
272
+ par2_config["par2_ratio_incr"] = int(value)
273
+ elif key == "PAR2_RUN_VERIFY":
274
+ val = value.lower()
275
+ if val in ('true', '1', 'yes'):
276
+ par2_config["par2_run_verify"] = True
277
+ elif val in ('false', '0', 'no'):
278
+ par2_config["par2_run_verify"] = False
279
+ else:
280
+ raise ConfigSettingsError(f"Invalid boolean value for 'PAR2_RUN_VERIFY' in [{backup_definition}]: '{value}'")
281
+ elif key == "PAR2_ENABLED":
282
+ val = value.lower()
283
+ if val in ('true', '1', 'yes'):
284
+ par2_config["par2_enabled"] = True
285
+ elif val in ('false', '0', 'no'):
286
+ par2_config["par2_enabled"] = False
287
+ else:
288
+ raise ConfigSettingsError(f"Invalid boolean value for 'PAR2_ENABLED' in [{backup_definition}]: '{value}'")
147
289
 
290
+ return par2_config
@@ -9,10 +9,15 @@ LOGFILE_LOCATION = ~/dar-backup/dar-backup.log
9
9
  MAX_SIZE_VERIFICATION_MB = 20
10
10
  MIN_SIZE_VERIFICATION_MB = 1
11
11
  NO_FILES_VERIFICATION = 5
12
+ # Optional restore test filters (case-insensitive)
13
+ # RESTORETEST_EXCLUDE_PREFIXES = .cache/, .local/share/Trash/, .mozilla/
14
+ # RESTORETEST_EXCLUDE_SUFFIXES = .sqlite-wal, .sqlite-shm, .log, .tmp, .lock, .journal
15
+ # RESTORETEST_EXCLUDE_REGEX = (^|/)(Cache|Logs)/ # optional extra noise
12
16
  # timeout in seconds for backup, test, restore and par2 operations
13
17
  # The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
14
18
  # If a timeout is not specified when using the CommandRunner, a default timeout of 30 secs is used.
15
19
  COMMAND_TIMEOUT_SECS = 86400
20
+ #DAR_BACKUP_DISCORD_WEBHOOK_URL = https://discord.com/api/webhooks/<id>/<token>
16
21
 
17
22
  [DIRECTORIES]
18
23
  BACKUP_DIR = @@BACKUP_DIR@@
@@ -31,6 +36,12 @@ INCR_AGE = 40
31
36
  [PAR2]
32
37
  ERROR_CORRECTION_PERCENT = 5
33
38
  ENABLED = True
39
+ # Optional PAR2 configuration
40
+ # PAR2_DIR = /path/to/par2-store
41
+ # PAR2_RATIO_FULL = 10
42
+ # PAR2_RATIO_DIFF = 5
43
+ # PAR2_RATIO_INCR = 5
44
+ # PAR2_RUN_VERIFY = false
34
45
 
35
46
  [PREREQ]
36
47
  #SCRIPT_1 = <pre-script 1>
@@ -29,10 +29,16 @@ LOGFILE_LOCATION = {{ vars_map.DAR_BACKUP_DIR -}}/dar-backup.log
29
29
  # optional parameters
30
30
  # LOGFILE_MAX_BYTES = 26214400 # 25 MB default, change as neeeded
31
31
  # LOGFILE_BACKUP_COUNT = 5 # default, change as needed
32
+ # DAR_BACKUP_DISCORD_WEBHOOK_URL **should really** be given as an environment variable for security reasons
33
+ # DAR_BACKUP_DISCORD_WEBHOOK_URL = https://discord.com/api/webhooks/<id>/<token>
32
34
 
33
35
  MAX_SIZE_VERIFICATION_MB = 2
34
36
  MIN_SIZE_VERIFICATION_MB = 0
35
37
  NO_FILES_VERIFICATION = 1
38
+ # Optional restore test filters (case-insensitive)
39
+ # RESTORETEST_EXCLUDE_PREFIXES = .cache/, .local/share/Trash/, .mozilla/
40
+ # RESTORETEST_EXCLUDE_SUFFIXES = .sqlite-wal, .sqlite-shm, .log, .tmp, .lock, .journal
41
+ # RESTORETEST_EXCLUDE_REGEX = (^|/)(Cache|Logs)/
36
42
  # timeout in seconds for backup, test, restore and par2 operations
37
43
  # The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
38
44
  # If a timeout is not specified when using the CommandRunner, a default timeout of 30 secs is used.
@@ -54,7 +60,16 @@ INCR_AGE = 40
54
60
 
55
61
  [PAR2]
56
62
  ERROR_CORRECTION_PERCENT = 5
63
+ # Enable or disable PAR2 generation globally
57
64
  ENABLED = True
65
+ # Optional PAR2 configuration
66
+ # PAR2_DIR = /path/to/par2-store
67
+ # PAR2_LAYOUT = by-backup
68
+ # PAR2_RATIOs are meuasured as percentages. Same function as ERROR_CORRECTION_PERCENT
69
+ # PAR2_RATIO_FULL = 10
70
+ # PAR2_RATIO_DIFF = 5
71
+ # PAR2_RATIO_INCR = 5
72
+ # PAR2_RUN_VERIFY = false
58
73
 
59
74
  [PREREQ]
60
75
  #SCRIPT_1 = <pre-script 1>
@@ -62,3 +77,30 @@ ENABLED = True
62
77
  [POSTREQ]
63
78
  #SCRIPT_1 = <post-script 1>
64
79
 
80
+ #######################################################################
81
+ ## Per-backup configuration example overrides
82
+ #######################################################################
83
+ #
84
+ ## --------------------------------------------------------------------
85
+ ## Per-backup overrides (section name must match backup.d filename stem)
86
+ ## Example: backup.d/home.conf -> [home]
87
+ ## --------------------------------------------------------------------
88
+ #
89
+ ##[home]
90
+ ## Disable PAR2 entirely for this backup definition
91
+ #PAR2_ENABLED = false
92
+ ##
93
+ ##[media]
94
+ ## Store PAR2 files in a separate location for this backup definition
95
+ ##PAR2_DIR = /samba/par2/media
96
+ ## Raise redundancy only for FULL
97
+ ##
98
+ #[documents]
99
+ ## Run verify par2 sets after creation
100
+ #PAR2_RUN_VERIFY = true
101
+ ##
102
+ ##[etc]
103
+ ## Keep global PAR2 settings but tweak ratios for this backup definition
104
+ ##PAR2_RATIO_FULL = 15
105
+ ##PAR2_RATIO_DIFF = 8
106
+ ##PAR2_RATIO_INCR = 8