dar-backup 1.0.1__py3-none-any.whl → 1.1.0__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.
dar_backup/__about__.py CHANGED
@@ -1,8 +1,7 @@
1
- __version__ = "1.0.1"
1
+ __version__ = "1.1.0"
2
2
 
3
3
  __author__ = "Per Jensen"
4
4
 
5
5
  __license__ = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
6
6
  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
7
7
  See section 15 and section 16 in the supplied "LICENSE" file.'''
8
-
dar_backup/clean_log.py CHANGED
@@ -23,13 +23,54 @@ import re
23
23
  import os
24
24
  import sys
25
25
 
26
+ from datetime import datetime
27
+
26
28
  from dar_backup import __about__ as about
27
29
  from dar_backup.config_settings import ConfigSettings
30
+ from dar_backup.util import send_discord_message, get_logger
28
31
 
29
32
  LICENSE = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
30
33
  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
31
34
  See section 15 and section 16 in the supplied "LICENSE" file.'''
32
35
 
36
+ TIMESTAMP_RE = re.compile(r"^\d{4}-\d{2}-\d{2}\b")
37
+ CLEAN_MESSAGE_PREFIXES = (
38
+ "Inspecting directory",
39
+ "Finished Inspecting",
40
+ "<File",
41
+ "</File",
42
+ "<Attributes",
43
+ "</Attributes",
44
+ "<Directory",
45
+ "</Directory",
46
+ "<Catalog",
47
+ "</Catalog",
48
+ "<Symlink",
49
+ "</Symlink",
50
+ )
51
+
52
+ def _split_level_and_message(line):
53
+ line = line.rstrip("\n")
54
+ if " - " not in line:
55
+ return None, None
56
+
57
+ parts = line.split(" - ")
58
+ if len(parts) >= 3 and TIMESTAMP_RE.match(parts[0].strip()):
59
+ level = parts[1]
60
+ message = " - ".join(parts[2:])
61
+ else:
62
+ level = parts[0]
63
+ message = " - ".join(parts[1:])
64
+
65
+ return level.strip(), message
66
+
67
+ def _should_remove_line(line):
68
+ level, message = _split_level_and_message(line)
69
+ if level != "INFO" or message is None:
70
+ return False
71
+ message = message.lstrip()
72
+ return any(message.startswith(prefix) for prefix in CLEAN_MESSAGE_PREFIXES)
73
+
33
74
  def clean_log_file(log_file_path, dry_run=False):
34
75
  """Removes specific log lines from the given file using a memory-efficient streaming approach."""
35
76
 
@@ -42,7 +83,7 @@ def clean_log_file(log_file_path, dry_run=False):
42
83
  print(f"No read permission for '{log_file_path}'")
43
84
  sys.exit(1)
44
85
 
45
- if not os.access(log_file_path, os.W_OK):
86
+ if not dry_run and not os.access(log_file_path, os.W_OK):
46
87
  print(f"Error: No write permission for '{log_file_path}'")
47
88
  sys.exit(1)
48
89
 
@@ -51,47 +92,20 @@ def clean_log_file(log_file_path, dry_run=False):
51
92
  print(f"Performing a dry run on: {log_file_path}")
52
93
 
53
94
  temp_file_path = log_file_path + ".tmp"
54
-
55
- patterns = [
56
- r"INFO\s*-\s*Inspecting\s*directory",
57
- r"INFO\s*-\s*Finished\s*Inspecting",
58
- r"INFO\s*-\s*<File",
59
- r"INFO\s*-\s*</File",
60
- r"INFO\s*-\s*<Attributes",
61
- r"INFO\s*-\s*</Attributes",
62
- r"INFO\s*-\s*</Directory",
63
- r"INFO\s*-\s*<Directory",
64
- r"INFO\s*-\s*<Catalog",
65
- r"INFO\s*-\s*</Catalog",
66
- r"INFO\s*-\s*<Symlink",
67
- r"INFO\s*-\s*</Symlink",
68
- ]
69
95
 
70
96
  try:
71
- with open(log_file_path, "r", errors="ignore") as infile, open(temp_file_path, "w") as outfile:
97
+ if dry_run:
98
+ with open(log_file_path, "r", errors="ignore") as infile:
99
+ for line in infile:
100
+ if _should_remove_line(line):
101
+ print(f"Would remove: {line.strip()}")
102
+ return
72
103
 
104
+ with open(log_file_path, "r", errors="ignore") as infile, open(temp_file_path, "w") as outfile:
73
105
  for line in infile:
74
- original_line = line # Store the original line before modifying it
75
- matched = False # Track if a pattern is matched
76
-
77
- for pattern in patterns:
78
- if re.search(pattern, line): # Check if the pattern matches
79
- if dry_run:
80
- print(f"Would remove: {original_line.strip()}") # Print full line for dry-run
81
- matched = True # Mark that a pattern matched
82
- break # No need to check other patterns if one matches
83
-
84
- if not dry_run and not matched: # In normal mode, only write non-empty lines
106
+ if not _should_remove_line(line):
85
107
  outfile.write(line.rstrip() + "\n")
86
108
 
87
- if dry_run and matched:
88
- continue # In dry-run mode, skip writing (since we’re just showing)
89
-
90
-
91
- # Ensure the temp file exists before renaming
92
- if not os.path.exists(temp_file_path):
93
- open(temp_file_path, "w").close() # Create an empty file if nothing was written
94
-
95
109
  os.replace(temp_file_path, log_file_path)
96
110
  print(f"Successfully cleaned log file: {log_file_path}")
97
111
 
@@ -131,43 +145,68 @@ def main():
131
145
 
132
146
  args = parser.parse_args()
133
147
 
134
- config_settings = ConfigSettings(os.path.expanduser(os.path.expandvars(args.config_file)))
148
+ try:
149
+ config_settings = ConfigSettings(os.path.expanduser(os.path.expandvars(args.config_file)))
150
+ except Exception as exc:
151
+ msg = f"Config error: {exc}"
152
+ print(msg, file=sys.stderr)
153
+ ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
154
+ send_discord_message(f"{ts} - clean-log: FAILURE - {msg}")
155
+ sys.exit(127)
135
156
 
136
- if not args.file:
137
- args.file = [config_settings.logfile_location]
157
+ try:
158
+ files_to_clean = args.file if args.file else [config_settings.logfile_location]
159
+ logfile_dir = os.path.dirname(os.path.realpath(config_settings.logfile_location))
160
+ validated_files = []
138
161
 
139
- for file_path in args.file:
162
+ for file_path in files_to_clean:
163
+ if not isinstance(file_path, (str, bytes, os.PathLike)):
164
+ print(f"Error: Invalid file path type: {file_path}")
165
+ sys.exit(1)
140
166
 
141
- if ".." in os.path.normpath(file_path).split(os.sep):
142
- print(f"Error: Path traversal is not allowed: '{file_path}'")
143
- sys.exit(1)
167
+ file_path = os.fspath(file_path)
168
+ if isinstance(file_path, bytes):
169
+ file_path = os.fsdecode(file_path)
144
170
 
145
- logfile_dir = os.path.dirname(os.path.realpath(config_settings.logfile_location))
146
- resolved_path = os.path.realpath(file_path)
171
+ if file_path.strip() == "":
172
+ print(f"Error: Invalid empty filename '{file_path}'.")
173
+ sys.exit(1)
147
174
 
148
- if not resolved_path.startswith(logfile_dir + os.sep):
149
- print(f"Error: File is outside allowed directory: '{file_path}'")
150
- sys.exit(1)
175
+ if ".." in os.path.normpath(file_path).split(os.sep):
176
+ print(f"Error: Path traversal is not allowed: '{file_path}'")
177
+ sys.exit(1)
151
178
 
152
- # Validate the file path type and existence
153
- if not isinstance(file_path, (str, bytes, os.PathLike)):
154
- print(f"Error: Invalid file path type: {file_path}")
155
- sys.exit(1)
179
+ resolved_path = os.path.realpath(file_path)
156
180
 
157
- if not os.path.exists(file_path):
158
- print(f"Error: Log file '{file_path}' does not exist.")
159
- sys.exit(1)
181
+ if not resolved_path.startswith(logfile_dir + os.sep):
182
+ print(f"Error: File is outside allowed directory: '{file_path}'")
183
+ sys.exit(1)
160
184
 
161
- if file_path.strip() == "":
162
- print(f"Error: Invalid empty filename '{file_path}'.")
163
- sys.exit(1)
185
+ if not os.path.exists(file_path):
186
+ print(f"Error: Log file '{file_path}' does not exist.")
187
+ sys.exit(1)
164
188
 
165
-
166
- # Run the log file cleaning function
167
- for log_file in args.file:
168
- clean_log_file(log_file, dry_run=args.dry_run)
169
- print(f"Log file '{args.file}' has been cleaned successfully.")
189
+ validated_files.append(file_path)
170
190
 
171
191
 
192
+ # Run the log file cleaning function
193
+ for log_file in validated_files:
194
+ clean_log_file(log_file, dry_run=args.dry_run)
195
+ file_list = ", ".join(validated_files)
196
+ if args.dry_run:
197
+ print(f"Dry run complete for: {file_list}")
198
+ else:
199
+ print(f"Log file '{file_list}' has been cleaned successfully.")
200
+ except Exception as e:
201
+ msg = f"Unexpected error during clean-log: {e}"
202
+ logger = get_logger()
203
+ if logger:
204
+ logger.error(msg, exc_info=True)
205
+ else:
206
+ print(msg, file=sys.stderr)
207
+
208
+ ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
209
+ send_discord_message(f"{ts} - clean-log: FAILURE - {msg}", config_settings=config_settings)
210
+ sys.exit(1)
172
211
  if __name__ == "__main__":
173
212
  main()
dar_backup/cleanup.py CHANGED
@@ -16,10 +16,8 @@ This script removes old DIFF and INCR archives + accompanying .par2 files accord
16
16
 
17
17
  import argcomplete
18
18
  import argparse
19
- import logging
20
19
  import os
21
20
  import re
22
- import subprocess
23
21
  import sys
24
22
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
25
23
 
@@ -29,7 +27,7 @@ from inputimeout import inputimeout, TimeoutOccurred
29
27
  from pathlib import Path
30
28
  from sys import stderr
31
29
  from time import time
32
- from typing import Dict, List, NamedTuple, Tuple
30
+ from typing import List, NamedTuple, Tuple
33
31
  import glob
34
32
 
35
33
 
@@ -48,6 +46,7 @@ from dar_backup.util import is_archive_name_allowed
48
46
  from dar_backup.util import is_safe_filename
49
47
  from dar_backup.util import safe_remove_file
50
48
  from dar_backup.util import show_scriptname
49
+ from dar_backup.util import send_discord_message
51
50
 
52
51
  from dar_backup.command_runner import CommandRunner
53
52
  from dar_backup.command_runner import CommandResult
@@ -67,7 +66,6 @@ def _delete_par2_files(
67
66
  else:
68
67
  par2_config = {
69
68
  "par2_dir": None,
70
- "par2_mode": None,
71
69
  }
72
70
 
73
71
  par2_dir = par2_config.get("par2_dir") or backup_dir
@@ -76,49 +74,33 @@ def _delete_par2_files(
76
74
  logger.warning(f"PAR2 directory not found, skipping cleanup: {par2_dir}")
77
75
  return
78
76
 
79
- par2_mode = (par2_config.get("par2_mode") or "per-slice").lower()
80
-
81
- if par2_mode == "per-archive":
82
- par2_glob = os.path.join(par2_dir, f"{archive_name}*.par2")
83
- targets = glob.glob(par2_glob)
84
- manifest_path = os.path.join(par2_dir, f"{archive_name}.par2.manifest.ini")
85
- if os.path.exists(manifest_path):
86
- targets.append(manifest_path)
87
- if not targets:
88
- logger.info("No par2 files matched the per-archive cleanup pattern.")
89
- return
90
- for file_path in sorted(set(targets)):
91
- try:
92
- if dry_run:
93
- logger.info(f"Dry run: would delete PAR2 file: {file_path}")
94
- else:
95
- safe_remove_file(file_path, base_dir=Path(par2_dir))
96
- logger.info(f"Deleted PAR2 file: {file_path}")
97
- except Exception as e:
98
- logger.error(f"Error deleting PAR2 file {file_path}: {e}")
99
- return
100
-
101
- if par2_mode != "per-slice":
102
- logger.error(f"Unsupported PAR2_MODE during cleanup: {par2_mode}")
103
- return
77
+ par2_glob = os.path.join(par2_dir, f"{archive_name}*.par2")
78
+ targets = set(glob.glob(par2_glob))
79
+ manifest_path = os.path.join(par2_dir, f"{archive_name}.par2.manifest.ini")
80
+ if os.path.exists(manifest_path):
81
+ targets.add(manifest_path)
104
82
 
105
83
  par2_regex = re.compile(rf"^{re.escape(archive_name)}\.[0-9]+\.dar.*\.par2$")
106
- files_deleted = False
107
- for filename in sorted(os.listdir(par2_dir)):
84
+ for entry in os.scandir(par2_dir):
85
+ if not entry.is_file():
86
+ continue
87
+ filename = entry.name
108
88
  if par2_regex.match(filename):
109
- file_path = os.path.join(par2_dir, filename)
110
- try:
111
- if dry_run:
112
- logger.info(f"Dry run: would delete PAR2 file: {file_path}")
113
- else:
114
- safe_remove_file(file_path, base_dir=Path(par2_dir))
115
- logger.info(f"Deleted PAR2 file: {file_path}")
116
- files_deleted = True
117
- except Exception as e:
118
- logger.error(f"Error deleting PAR2 file {file_path}: {e}")
89
+ targets.add(entry.path)
119
90
 
120
- if not files_deleted:
121
- logger.info("No .par2 matched the regex for deletion.")
91
+ if not targets:
92
+ logger.info("No par2 files matched the cleanup patterns.")
93
+ return
94
+
95
+ for file_path in sorted(targets):
96
+ try:
97
+ if dry_run:
98
+ logger.info(f"Dry run: would delete PAR2 file: {file_path}")
99
+ else:
100
+ safe_remove_file(file_path, base_dir=Path(par2_dir))
101
+ logger.info(f"Deleted PAR2 file: {file_path}")
102
+ except Exception as e:
103
+ logger.error(f"Error deleting PAR2 file {file_path}: {e}")
122
104
 
123
105
 
124
106
  def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=None, config_settings: ConfigSettings = None):
@@ -138,7 +120,10 @@ def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=Non
138
120
  archives_deleted = {}
139
121
 
140
122
  dry_run = getattr(args, "dry_run", False) is True
141
- for filename in sorted(os.listdir(backup_dir)):
123
+ for entry in os.scandir(backup_dir):
124
+ if not entry.is_file():
125
+ continue
126
+ filename = entry.name
142
127
  if not filename.endswith('.dar'):
143
128
  continue
144
129
  if backup_definition and not filename.startswith(backup_definition):
@@ -152,7 +137,7 @@ def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=Non
152
137
  raise
153
138
 
154
139
  if file_date < cutoff_date:
155
- file_path = os.path.join(backup_dir, filename)
140
+ file_path = entry.path
156
141
  try:
157
142
  if dry_run:
158
143
  logger.info(f"Dry run: would delete {backup_type} backup: {file_path}")
@@ -160,7 +145,7 @@ def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=Non
160
145
  safe_remove_file(file_path, base_dir=Path(backup_dir))
161
146
  logger.info(f"Deleted {backup_type} backup: {file_path}")
162
147
  archive_name = filename.split('.')[0]
163
- if not archive_name in archives_deleted:
148
+ if archive_name not in archives_deleted:
164
149
  logger.debug(f"Archive name: '{archive_name}' added to catalog deletion list")
165
150
  archives_deleted[archive_name] = True
166
151
  except Exception as e:
@@ -190,9 +175,12 @@ def delete_archive(backup_dir, archive_name, args, config_settings: ConfigSettin
190
175
  # Delete the specified .dar files according to the naming convention
191
176
  files_deleted = False
192
177
  dry_run = getattr(args, "dry_run", False) is True
193
- for filename in sorted(os.listdir(backup_dir)):
178
+ for entry in os.scandir(backup_dir):
179
+ if not entry.is_file():
180
+ continue
181
+ filename = entry.name
194
182
  if archive_regex.match(filename):
195
- file_path = os.path.join(backup_dir, filename)
183
+ file_path = entry.path
196
184
  try:
197
185
  if dry_run:
198
186
  logger.info(f"Dry run: would delete archive slice: {file_path}")
@@ -219,7 +207,7 @@ def delete_catalog(catalog_name: str, args: NamedTuple) -> bool:
219
207
  """
220
208
  Call `manager.py` to delete the specified catalog in it's database
221
209
  """
222
- command = [f"manager", "--remove-specific-archive", catalog_name, "--config-file", args.config_file, '--log-level', 'debug', '--log-stdout']
210
+ command = ["manager", "--remove-specific-archive", catalog_name, "--config-file", args.config_file, '--log-level', 'debug', '--log-stdout']
223
211
  logger.info(f"Deleting catalog '{catalog_name}' using config file: '{args.config_file}'")
224
212
  try:
225
213
  result:CommandResult = runner.run(command)
@@ -308,15 +296,35 @@ def main():
308
296
  raise SystemExit(127)
309
297
  args.config_file = config_settings_path
310
298
 
311
- config_settings = ConfigSettings(args.config_file)
299
+ try:
300
+ config_settings = ConfigSettings(args.config_file)
301
+ except Exception as exc:
302
+ msg = f"Config error: {exc}"
303
+ print(msg, file=stderr)
304
+ ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
305
+ send_discord_message(f"{ts} - cleanup: FAILURE - {msg}")
306
+ sys.exit(127)
312
307
 
313
308
  start_time=int(time())
314
309
 
315
310
  # command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
316
311
  command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
317
- logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout, logfile_max_bytes=config_settings.logfile_max_bytes, logfile_backup_count=config_settings.logfile_backup_count)
312
+ logger = setup_logging(
313
+ config_settings.logfile_location,
314
+ command_output_log,
315
+ args.log_level,
316
+ args.log_stdout,
317
+ logfile_max_bytes=config_settings.logfile_max_bytes,
318
+ logfile_backup_count=config_settings.logfile_backup_count,
319
+ trace_log_max_bytes=getattr(config_settings, "trace_log_max_bytes", 10485760),
320
+ trace_log_backup_count=getattr(config_settings, "trace_log_backup_count", 1)
321
+ )
318
322
  command_logger = get_logger(command_output_logger = True)
319
- runner = CommandRunner(logger=logger, command_logger=command_logger)
323
+ runner = CommandRunner(
324
+ logger=logger,
325
+ command_logger=command_logger,
326
+ default_capture_limit_bytes=getattr(config_settings, "command_capture_max_bytes", None)
327
+ )
320
328
 
321
329
  start_msgs: List[Tuple[str, str]] = []
322
330
 
@@ -341,65 +349,89 @@ def main():
341
349
  print_aligned_settings(start_msgs, highlight_keywords=dangerous_keywords, quiet=not args.verbose)
342
350
 
343
351
  # run PREREQ scripts
344
- requirements('PREREQ', config_settings)
345
-
346
- if args.alternate_archive_dir:
347
- if not os.path.exists(args.alternate_archive_dir):
348
- logger.error(f"Alternate archive directory does not exist: {args.alternate_archive_dir}, exiting")
349
- sys.exit(1)
350
- if not os.path.isdir(args.alternate_archive_dir):
351
- logger.error(f"Alternate archive directory is not a directory, exiting")
352
- sys.exit(1)
353
- config_settings.backup_dir = args.alternate_archive_dir
354
-
355
- if args.cleanup_specific_archives is None and args.test_mode:
356
- logger.info("No --cleanup-specific-archives provided; skipping specific archive deletion in test mode.")
357
-
358
- if args.cleanup_specific_archives or args.cleanup_specific_archives_list:
359
- combined = []
360
- if args.cleanup_specific_archives:
361
- combined.extend(args.cleanup_specific_archives.split(','))
362
- combined.extend(args.cleanup_specific_archives_list or [])
363
- archive_names = [name.strip() for name in combined if name.strip()]
364
- logger.info(f"Cleaning up specific archives: {', '.join(archive_names)}")
365
- for archive_name in archive_names:
366
- if "_FULL_" in archive_name:
367
- if not confirm_full_archive_deletion(archive_name, args.test_mode):
352
+ try:
353
+ requirements('PREREQ', config_settings)
354
+ except Exception as exc:
355
+ msg = f"PREREQ failed: {exc}"
356
+ logger.error(msg)
357
+ ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
358
+ send_discord_message(f"{ts} - cleanup: FAILURE - {msg}", config_settings=config_settings)
359
+ sys.exit(1)
360
+
361
+ try:
362
+ if args.alternate_archive_dir:
363
+ if not os.path.exists(args.alternate_archive_dir):
364
+ logger.error(f"Alternate archive directory does not exist: {args.alternate_archive_dir}, exiting")
365
+ sys.exit(1)
366
+ if not os.path.isdir(args.alternate_archive_dir):
367
+ logger.error("Alternate archive directory is not a directory, exiting")
368
+ sys.exit(1)
369
+ config_settings.backup_dir = args.alternate_archive_dir
370
+
371
+ if args.cleanup_specific_archives is None and args.test_mode:
372
+ logger.info("No --cleanup-specific-archives provided; skipping specific archive deletion in test mode.")
373
+
374
+ if args.cleanup_specific_archives or args.cleanup_specific_archives_list:
375
+ combined = []
376
+ if args.cleanup_specific_archives:
377
+ combined.extend(args.cleanup_specific_archives.split(','))
378
+ combined.extend(args.cleanup_specific_archives_list or [])
379
+ archive_names = [name.strip() for name in combined if name.strip()]
380
+ logger.info(f"Cleaning up specific archives: {', '.join(archive_names)}")
381
+ for archive_name in archive_names:
382
+ if not is_archive_name_allowed(archive_name):
383
+ logger.error(f"Refusing unsafe archive name: {archive_name}")
368
384
  continue
369
- archive_path = os.path.join(config_settings.backup_dir, archive_name.strip())
370
- logger.info(f"Deleting archive: {archive_path}")
371
- delete_archive(config_settings.backup_dir, archive_name.strip(), args, config_settings)
372
- elif args.list:
373
- list_backups(config_settings.backup_dir, args.backup_definition)
374
- else:
375
- backup_definitions = []
376
- if args.backup_definition:
377
- backup_definitions.append(args.backup_definition)
385
+ if "_FULL_" in archive_name:
386
+ if not confirm_full_archive_deletion(archive_name, args.test_mode):
387
+ continue
388
+ archive_path = os.path.join(config_settings.backup_dir, archive_name.strip())
389
+ logger.info(f"Deleting archive: {archive_path}")
390
+ delete_archive(config_settings.backup_dir, archive_name.strip(), args, config_settings)
391
+ elif args.list:
392
+ list_backups(config_settings.backup_dir, args.backup_definition)
378
393
  else:
379
- for root, _, files in os.walk(config_settings.backup_d_dir):
380
- for file in files:
381
- backup_definitions.append(file.split('.')[0])
382
-
383
- for definition in backup_definitions:
384
- delete_old_backups(
385
- config_settings.backup_dir,
386
- config_settings.diff_age,
387
- 'DIFF',
388
- args,
389
- backup_definition=definition,
390
- config_settings=config_settings
391
- )
392
- delete_old_backups(
393
- config_settings.backup_dir,
394
- config_settings.incr_age,
395
- 'INCR',
396
- args,
397
- backup_definition=definition,
398
- config_settings=config_settings
399
- )
394
+ backup_definitions = []
395
+ if args.backup_definition:
396
+ backup_definitions.append(args.backup_definition)
397
+ else:
398
+ for root, _, files in os.walk(config_settings.backup_d_dir):
399
+ for file in files:
400
+ backup_definitions.append(file.split('.')[0])
401
+
402
+ for definition in backup_definitions:
403
+ delete_old_backups(
404
+ config_settings.backup_dir,
405
+ config_settings.diff_age,
406
+ 'DIFF',
407
+ args,
408
+ backup_definition=definition,
409
+ config_settings=config_settings
410
+ )
411
+ delete_old_backups(
412
+ config_settings.backup_dir,
413
+ config_settings.incr_age,
414
+ 'INCR',
415
+ args,
416
+ backup_definition=definition,
417
+ config_settings=config_settings
418
+ )
419
+ except Exception as e:
420
+ msg = f"Unexpected error during cleanup: {e}"
421
+ logger.error(msg, exc_info=True)
422
+ ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
423
+ send_discord_message(f"{ts} - cleanup: FAILURE - {msg}", config_settings=config_settings)
424
+ sys.exit(1)
400
425
 
401
426
  # run POST scripts
402
- requirements('POSTREQ', config_settings)
427
+ try:
428
+ requirements('POSTREQ', config_settings)
429
+ except Exception as exc:
430
+ msg = f"POSTREQ failed: {exc}"
431
+ logger.error(msg)
432
+ ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
433
+ send_discord_message(f"{ts} - cleanup: FAILURE - {msg}", config_settings=config_settings)
434
+ sys.exit(1)
403
435
 
404
436
 
405
437
  end_time=int(time())