dar-backup 0.6.16__py3-none-any.whl → 0.6.17__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 +1 @@
1
- __version__ = "0.6.16"
1
+ __version__ = "0.6.17"
dar_backup/cleanup.py CHANGED
@@ -29,14 +29,14 @@ from . import __about__ as about
29
29
  from dar_backup.config_settings import ConfigSettings
30
30
  from dar_backup.util import extract_error_lines
31
31
  from dar_backup.util import list_backups
32
- from dar_backup.util import run_command
33
32
  from dar_backup.util import setup_logging
33
+ from dar_backup.util import get_logger
34
34
 
35
- from dar_backup.util import CommandResult
36
-
37
-
35
+ from dar_backup.command_runner import CommandRunner
36
+ from dar_backup.command_runner import CommandResult
38
37
 
39
38
  logger = None
39
+ runner = None
40
40
 
41
41
  def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=None):
42
42
  """
@@ -134,7 +134,7 @@ def delete_catalog(catalog_name: str, args: NamedTuple) -> bool:
134
134
  command = [f"manager", "--remove-specific-archive", catalog_name, "--config-file", args.config_file, '--log-level', 'debug', '--log-stdout']
135
135
  logger.info(f"Deleting catalog '{catalog_name}' using config file: '{args.config_file}'")
136
136
  try:
137
- result:CommandResult = run_command(command)
137
+ result:CommandResult = runner.run(command)
138
138
  if result.returncode == 0:
139
139
  logger.info(f"Deleted catalog '{catalog_name}', using config file: '{args.config_file}'")
140
140
  logger.debug(f"Stdout: manager.py --remove-specific-archive output:\n{result.stdout}")
@@ -159,7 +159,7 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
159
159
 
160
160
 
161
161
  def main():
162
- global logger
162
+ global logger, runner
163
163
 
164
164
  parser = argparse.ArgumentParser(description="Cleanup old archives according to AGE configuration.")
165
165
  parser.add_argument('-d', '--backup-definition', help="Specific backup definition to cleanup.")
@@ -188,7 +188,8 @@ def main():
188
188
  # command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
189
189
  command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
190
190
  logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
191
-
191
+ command_logger = get_logger(command_output_logger = True)
192
+ runner = CommandRunner(logger=logger, command_logger=command_logger)
192
193
 
193
194
  logger.info(f"=====================================")
194
195
  logger.info(f"cleanup.py started, version: {about.__version__}")
@@ -283,15 +284,6 @@ def main():
283
284
  end_time=int(time())
284
285
  logger.info(f"END TIME: {end_time}")
285
286
 
286
- # error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
287
- # if len(error_lines) > 0:
288
- # args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
289
- # for line in error_lines:
290
- # args.verbose and print(line)
291
- # sys.exit(1)
292
- # else:
293
- # args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
294
- # sys.exit(0)
295
287
 
296
288
  if __name__ == "__main__":
297
289
  main()
@@ -0,0 +1,81 @@
1
+ import subprocess
2
+ import logging
3
+ import threading
4
+ import os
5
+ import sys
6
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
7
+ from typing import List, Optional
8
+
9
+ class CommandResult:
10
+ def __init__(self, returncode: int, stdout: str, stderr: str):
11
+ self.returncode = returncode
12
+ self.stdout = stdout
13
+ self.stderr = stderr
14
+
15
+ def __repr__(self):
16
+ return f"<CommandResult returncode={self.returncode}>"
17
+
18
+ class CommandRunner:
19
+ def __init__(
20
+ self,
21
+ logger: Optional[logging.Logger] = None,
22
+ command_logger: Optional[logging.Logger] = None,
23
+ default_timeout: int = 30
24
+ ):
25
+ self.logger = logger or logging.getLogger(__name__)
26
+ self.command_logger = command_logger or self.logger
27
+ self.default_timeout = default_timeout
28
+
29
+ def run(
30
+ self,
31
+ cmd: List[str],
32
+ *,
33
+ timeout: Optional[int] = None,
34
+ check: bool = False,
35
+ capture_output: bool = True,
36
+ text: bool = True
37
+ ) -> CommandResult:
38
+ timeout = timeout or self.default_timeout
39
+ self.logger.debug(f"Executing command: {' '.join(cmd)} (timeout={timeout}s)")
40
+
41
+ process = subprocess.Popen(
42
+ cmd,
43
+ stdout=subprocess.PIPE if capture_output else None,
44
+ stderr=subprocess.PIPE if capture_output else None,
45
+ text=text,
46
+ bufsize=1
47
+ )
48
+
49
+ stdout_lines = []
50
+ stderr_lines = []
51
+
52
+ def stream_output(stream, lines, level):
53
+ for line in iter(stream.readline, ''):
54
+ lines.append(line)
55
+ self.command_logger.log(level, line.strip())
56
+ stream.close()
57
+
58
+ threads = []
59
+ if capture_output and process.stdout:
60
+ t_out = threading.Thread(target=stream_output, args=(process.stdout, stdout_lines, logging.INFO))
61
+ t_out.start()
62
+ threads.append(t_out)
63
+ if capture_output and process.stderr:
64
+ t_err = threading.Thread(target=stream_output, args=(process.stderr, stderr_lines, logging.ERROR))
65
+ t_err.start()
66
+ threads.append(t_err)
67
+
68
+ try:
69
+ process.wait(timeout=timeout)
70
+ except subprocess.TimeoutExpired:
71
+ process.kill()
72
+ self.logger.error(f"Command timed out: {' '.join(cmd)}")
73
+ return CommandResult(-1, ''.join(stdout_lines), ''.join(stderr_lines))
74
+
75
+ for t in threads:
76
+ t.join()
77
+
78
+ if check and process.returncode != 0:
79
+ self.logger.error(f"Command failed with exit code {process.returncode}")
80
+
81
+ return CommandResult(process.returncode, ''.join(stdout_lines), ''.join(stderr_lines))
@@ -63,7 +63,13 @@ class ConfigSettings:
63
63
  self.diff_age = int(self.config['AGE']['DIFF_AGE'])
64
64
  self.incr_age = int(self.config['AGE']['INCR_AGE'])
65
65
  self.error_correction_percent = int(self.config['PAR2']['ERROR_CORRECTION_PERCENT'])
66
- self.par2_enabled = self.config['PAR2']['ENABLED'].lower() in ('true', '1', 'yes')
66
+ val = self.config['PAR2']['ENABLED'].strip().lower()
67
+ if val in ('true', '1', 'yes'):
68
+ self.par2_enabled = True
69
+ elif val in ('false', '0', 'no'):
70
+ self.par2_enabled = False
71
+ else:
72
+ raise ValueError(f"Invalid boolean value for 'ENABLED' in [PAR2]: '{val}'")
67
73
 
68
74
  # Ensure the directories exist
69
75
  Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
@@ -9,7 +9,7 @@ MIN_SIZE_VERIFICATION_MB = 1
9
9
  NO_FILES_VERIFICATION = 5
10
10
  # timeout in seconds for backup, test, restore and par2 operations
11
11
  # The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
12
- # If a timeout is not specified when using the util.run_command(), a default timeout of 30 secs is used.
12
+ # If a timeout is not specified when using the CommandRunner, a default timeout of 30 secs is used.
13
13
  COMMAND_TIMEOUT_SECS = 86400
14
14
 
15
15
  [DIRECTORIES]
dar_backup/dar_backup.py CHANGED
@@ -25,14 +25,17 @@ from typing import List
25
25
  from . import __about__ as about
26
26
  from dar_backup.config_settings import ConfigSettings
27
27
  from dar_backup.util import list_backups
28
- from dar_backup.util import run_command
29
28
  from dar_backup.util import setup_logging
30
29
  from dar_backup.util import get_logger
31
30
  from dar_backup.util import BackupError
32
31
  from dar_backup.util import RestoreError
33
32
 
33
+ from dar_backup.command_runner import CommandRunner
34
+ from dar_backup.command_runner import CommandResult
35
+
34
36
 
35
37
  logger = None
38
+ runner = None
36
39
 
37
40
  def generic_backup(type: str, command: List[str], backup_file: str, backup_definition: str, darrc: str, config_settings: ConfigSettings, args: argparse.Namespace) -> List[str]:
38
41
  """
@@ -63,9 +66,8 @@ def generic_backup(type: str, command: List[str], backup_file: str, backup_defin
63
66
  result: List[tuple] = []
64
67
 
65
68
  logger.info(f"===> Starting {type} backup for {backup_definition}")
66
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
67
69
  try:
68
- process = run_command(command, config_settings.command_timeout_secs)
70
+ process = runner.run(command, timeout = config_settings.command_timeout_secs)
69
71
  if process.returncode == 0:
70
72
  logger.info(f"{type} backup completed successfully.")
71
73
  elif process.returncode == 5:
@@ -75,7 +77,7 @@ def generic_backup(type: str, command: List[str], backup_file: str, backup_defin
75
77
 
76
78
  if process.returncode == 0 or process.returncode == 5:
77
79
  add_catalog_command = ['manager', '--add-specific-archive' ,backup_file, '--config-file', args.config_file]
78
- command_result = run_command(add_catalog_command, config_settings.command_timeout_secs)
80
+ command_result = runner.run(add_catalog_command, timeout = config_settings.command_timeout_secs)
79
81
  if command_result.returncode == 0:
80
82
  logger.info(f"Catalog for archive '{backup_file}' added successfully to its manager.")
81
83
  else:
@@ -192,8 +194,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
192
194
  """
193
195
  result = True
194
196
  command = ['dar', '-t', backup_file, '-Q']
195
- logger.debug(f"Running command: {' '.join(map(shlex.quote, command))}")
196
- process = run_command(command, config_settings.command_timeout_secs)
197
+ process = runner.run(command, timeout = config_settings.command_timeout_secs)
197
198
  if process.returncode == 0:
198
199
  logger.info("Archive integrity test passed.")
199
200
  else:
@@ -235,7 +236,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
235
236
  args.verbose and logger.info(f"Restoring file: '{restored_file_path}' from backup to: '{config_settings.test_restore_dir}' for file comparing")
236
237
  command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-Q', '-B', args.darrc, 'restore-options']
237
238
  args.verbose and logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
238
- process = run_command(command, config_settings.command_timeout_secs)
239
+ process = runner.run(command, timeout = config_settings.command_timeout_secs)
239
240
  if process.returncode != 0:
240
241
  raise Exception(str(process))
241
242
 
@@ -276,7 +277,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
276
277
  command.extend(selection_criteria)
277
278
  command.extend(['-B', darrc, 'restore-options']) # the .darrc `restore-options` section
278
279
  logger.info(f"Running restore command: {' '.join(map(shlex.quote, command))}")
279
- process = run_command(command, config_settings.command_timeout_secs)
280
+ process = runner.run(command, timeout = config_settings.command_timeout_secs)
280
281
  if process.returncode == 0:
281
282
  logger.info(f"Restore completed successfully to: '{restore_dir}'")
282
283
  else:
@@ -309,7 +310,7 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
309
310
  try:
310
311
  command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
311
312
  logger.debug(f"Running command: {' '.join(map(shlex.quote, command))}")
312
- command_result = run_command(command)
313
+ command_result = runner.run(command)
313
314
  # Parse the XML data
314
315
  file_paths = find_files_with_paths(command_result.stdout)
315
316
  return file_paths
@@ -339,8 +340,7 @@ def list_contents(backup_name, backup_dir, selection=None):
339
340
  if selection:
340
341
  selection_criteria = shlex.split(selection)
341
342
  command.extend(selection_criteria)
342
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
343
- process = run_command(command)
343
+ process = runner.run(command)
344
344
  stdout,stderr = process.stdout, process.stderr
345
345
  if process.returncode != 0:
346
346
  logger.error(f"Error listing contents of backup: '{backup_name}'")
@@ -512,7 +512,7 @@ def generate_par2_files(backup_file: str, config_settings: ConfigSettings, args)
512
512
 
513
513
  # Run the par2 command to generate redundancy files with error correction
514
514
  command = ['par2', 'create', f'-r{config_settings.error_correction_percent}', '-q', '-q', file_path]
515
- process = run_command(command, config_settings.command_timeout_secs)
515
+ process = runner.run(command, timeout = config_settings.command_timeout_secs)
516
516
 
517
517
  if process.returncode == 0:
518
518
  logger.info(f"{counter}/{number_of_slices}: Done")
@@ -629,7 +629,7 @@ def requirements(type: str, config_setting: ConfigSettings):
629
629
  config_settings (ConfigSettings): An instance of the ConfigSettings class.
630
630
 
631
631
  Raises:
632
- RuntimeError: If a subprocess returns anything but zero.
632
+ RuntimeError: If a subprocess returns anything but zero.
633
633
 
634
634
  subprocess.CalledProcessError: if CalledProcessError is raised in subprocess.run(), let it bobble up.
635
635
  """
@@ -641,7 +641,7 @@ def requirements(type: str, config_setting: ConfigSettings):
641
641
  raise RuntimeError(f"requirements: {type} not in: {allowed_types}")
642
642
 
643
643
 
644
- logger.info(f"Performing {type}")
644
+ logger.debug(f"Performing {type}")
645
645
  if type in config_setting.config:
646
646
  for key in sorted(config_setting.config[type].keys()):
647
647
  script = config_setting.config[type][key]
@@ -658,7 +658,7 @@ def requirements(type: str, config_setting: ConfigSettings):
658
658
 
659
659
 
660
660
  def main():
661
- global logger
661
+ global logger, runner
662
662
  results: List[(str,int)] = [] # a list op tuples (<msg>, <exit code>)
663
663
 
664
664
  MIN_PYTHON_VERSION = (3, 9)
@@ -713,6 +713,9 @@ def main():
713
713
  print(f"Error: logfile_location in {args.config_file} does not end at 'dar-backup.log', exiting", file=stderr)
714
714
 
715
715
  logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
716
+ command_logger = get_logger(command_output_logger = True)
717
+ runner = CommandRunner(logger=logger, command_logger=command_logger)
718
+
716
719
 
717
720
  try:
718
721
  if not args.darrc:
@@ -741,9 +744,9 @@ def main():
741
744
  file_dir = os.path.normpath(os.path.dirname(__file__))
742
745
  args.verbose and (print(f"Script directory: {file_dir}"))
743
746
  args.verbose and (print(f"Config file: {args.config_file}"))
744
- args.verbose and args.full_backup and (print(f"Type of backup: FULL"))
745
- args.verbose and args.differential_backup and (print(f"Type of backup: DIFF"))
746
- args.verbose and args.incremental_backup and (print(f"Type of backup: INCR"))
747
+ args.verbose and args.full_backup and (print(f"Type of backup: FULL"))
748
+ args.verbose and args.differential_backup and (print(f"Type of backup: DIFF"))
749
+ args.verbose and args.incremental_backup and (print(f"Type of backup: INCR"))
747
750
  args.verbose and args.backup_definition and (print(f"Backup definition: '{args.backup_definition}'"))
748
751
  if args.alternate_reference_archive:
749
752
  args.verbose and (print(f"Alternate ref archive: {args.alternate_reference_archive}"))
dar_backup/manager.py CHANGED
@@ -29,9 +29,12 @@ import sys
29
29
 
30
30
  from . import __about__ as about
31
31
  from dar_backup.config_settings import ConfigSettings
32
- from dar_backup.util import run_command
33
32
  from dar_backup.util import setup_logging
34
33
  from dar_backup.util import CommandResult
34
+ from dar_backup.util import get_logger
35
+
36
+ from dar_backup.command_runner import CommandRunner
37
+ from dar_backup.command_runner import CommandResult
35
38
 
36
39
  from datetime import datetime
37
40
  from time import time
@@ -44,6 +47,7 @@ SCRIPTDIRPATH = os.path.dirname(SCRIPTPATH)
44
47
  DB_SUFFIX = ".db"
45
48
 
46
49
  logger = None
50
+ runner = None
47
51
 
48
52
  def show_more_help():
49
53
  help_text = f"""
@@ -66,7 +70,7 @@ def create_db(backup_def: str, config_settings: ConfigSettings):
66
70
  else:
67
71
  logger.info(f'Create catalog database: "{database_path}"')
68
72
  command = ['dar_manager', '--create' , database_path]
69
- process = run_command(command)
73
+ process = runner.run(command)
70
74
  logger.debug(f"return code from 'db created': {process.returncode}")
71
75
  if process.returncode == 0:
72
76
  logger.info(f'Database created: "{database_path}"')
@@ -104,7 +108,7 @@ def list_catalogs(backup_def: str, config_settings: ConfigSettings) -> NamedTupl
104
108
  command=[])
105
109
  return commandResult
106
110
  command = ['dar_manager', '--base', database_path, '--list']
107
- process = run_command(command)
111
+ process = runner.run(command)
108
112
  stdout, stderr = process.stdout, process.stderr
109
113
  if process.returncode != 0:
110
114
  logger.error(f'Error listing catalogs for: "{database_path}"')
@@ -156,7 +160,7 @@ def list_archive_contents(archive: str, config_settings: ConfigSettings) -> int
156
160
  logger.error(f"archive: '{archive}' not found in database: '{database_path}'")
157
161
  return 1
158
162
  command = ['dar_manager', '--base', database_path, '-u', f"{cat_no}"]
159
- process = run_command(command)
163
+ process = runner.run(command)
160
164
  stdout, stderr = process.stdout, process.stderr
161
165
  if process.returncode != 0:
162
166
  logger.error(f'Error listing catalogs for: "{database_path}"')
@@ -178,7 +182,7 @@ def list_catalog_contents(catalog_number: int, backup_def: str, config_settings:
178
182
  logger.error(f'Catalog database not found: "{database_path}"')
179
183
  return 1
180
184
  command = ['dar_manager', '--base', database_path, '-u', f"{catalog_number}"]
181
- process = run_command(command)
185
+ process = runner.run(command)
182
186
  stdout, stderr = process.stdout, process.stderr
183
187
  if process.returncode != 0:
184
188
  logger.error(f'Error listing catalogs for: "{database_path}"')
@@ -199,7 +203,7 @@ def find_file(file, backup_def, config_settings):
199
203
  logger.error(f'Database not found: "{database_path}"')
200
204
  return 1
201
205
  command = ['dar_manager', '--base', database_path, '-f', f"{file}"]
202
- process = run_command(command)
206
+ process = runner.run(command)
203
207
  stdout, stderr = process.stdout, process.stderr
204
208
  if process.returncode != 0:
205
209
  logger.error(f'Error finding file: {file} in: "{database_path}"')
@@ -234,7 +238,7 @@ def add_specific_archive(archive: str, config_settings: ConfigSettings, director
234
238
  logger.info(f'Add "{archive_path}" to catalog: "{database}"')
235
239
 
236
240
  command = ['dar_manager', '--base', database_path, "--add", archive_path, "-Q"]
237
- process = run_command(command)
241
+ process = runner.run(command)
238
242
  stdout, stderr = process.stdout, process.stderr
239
243
 
240
244
  if process.returncode == 0:
@@ -338,7 +342,7 @@ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> in
338
342
  cat_no:int = cat_no_for_name(archive, config_settings)
339
343
  if cat_no >= 0:
340
344
  command = ['dar_manager', '--base', database_path, "--delete", str(cat_no)]
341
- process: CommandResult = run_command(command)
345
+ process: CommandResult = runner.run(command)
342
346
  logger.info(f"CommandResult: {process}")
343
347
  else:
344
348
  logger.warning(f"archive: '{archive}' not found in it's catalog database: {database_path}")
@@ -355,7 +359,7 @@ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> in
355
359
 
356
360
 
357
361
  def main():
358
- global logger
362
+ global logger, runner
359
363
 
360
364
  MIN_PYTHON_VERSION = (3, 9)
361
365
  if sys.version_info < MIN_PYTHON_VERSION:
@@ -404,6 +408,8 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
404
408
  # command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
405
409
  command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
406
410
  logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
411
+ command_logger = get_logger(command_output_logger = True)
412
+ runner = CommandRunner(logger=logger, command_logger=command_logger)
407
413
 
408
414
 
409
415
  start_time=int(time())
dar_backup/util.py CHANGED
@@ -97,109 +97,6 @@ def get_logger(command_output_logger: bool = False) -> logging.Logger:
97
97
  return secondary_logger if command_output_logger else logger
98
98
 
99
99
 
100
-
101
- def _stream_reader(pipe, log_funcs, output_accumulator: List[str]):
102
- """
103
- Reads lines from the subprocess pipe and logs them to multiple destinations.
104
- """
105
- with pipe:
106
- for line in iter(pipe.readline, ''):
107
- stripped_line = line.strip()
108
- output_accumulator.append(stripped_line)
109
- for log_func in log_funcs:
110
- log_func(stripped_line) # Log the output in real-time
111
-
112
-
113
-
114
- def run_command(command: List[str], timeout: int = 30, no_output_log: bool = False):
115
- """
116
- Executes a command and streams output only to the secondary log unless no_log is set to True.
117
-
118
-
119
- Returns:
120
- A CommandResult NamedTuple with the following properties:
121
- - process: subprocess.CompletedProcess
122
- - stdout: str: The full standard output of the command.
123
- - stderr: str: The full standard error of the command.
124
- - returncode: int: The return code of the command.
125
- - timeout: int: The timeout value in seconds used to run the command.
126
- - command: list[str]: The command executed.
127
-
128
- Logs:
129
- - Logs standard output (`stdout`) and standard error in real-time to the
130
- logger.secondary_log (that contains the command output).
131
-
132
- Raises:
133
- subprocess.TimeoutExpired: If the command execution times out (see `timeout` parameter).
134
- Exception: If other exceptions occur during command execution.
135
- FileNotFoundError: If the command is not found.
136
- """
137
- stdout_lines, stderr_lines = [], []
138
- process = None
139
- stdout_thread, stderr_thread = None, None
140
-
141
- try:
142
- logger = get_logger(command_output_logger=False)
143
- command_logger = get_logger(command_output_logger=True)
144
-
145
- if not shutil.which(command[0]):
146
- raise FileNotFoundError(f"Command not found: {command[0]}")
147
-
148
- logger.debug(f"Running command: {command}")
149
- command_logger.info(f"Running command: {command}")
150
-
151
- process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
152
-
153
- log_funcs = [command_logger.info] if not no_output_log else []
154
- err_log_funcs = [command_logger.error] if not no_output_log else []
155
-
156
- stdout_thread = threading.Thread(target=_stream_reader, args=(process.stdout, log_funcs, stdout_lines))
157
- stderr_thread = threading.Thread(target=_stream_reader, args=(process.stderr, err_log_funcs, stderr_lines))
158
-
159
- stdout_thread.start()
160
- stderr_thread.start()
161
-
162
- process.wait(timeout=timeout)
163
-
164
- except FileNotFoundError as e:
165
- logger.error(f"Command not found: {command[0]}")
166
- return CommandResult(
167
- process=None,
168
- stdout="",
169
- stderr=str(e),
170
- returncode=127,
171
- timeout=timeout,
172
- command=command
173
- )
174
- except subprocess.TimeoutExpired:
175
- if process:
176
- process.terminate()
177
- logger.error(f"Command: '{command}' timed out and was terminated.")
178
- raise
179
- except Exception as e:
180
- logger.error(f"Error running command: {command}", exc_info=True)
181
- raise
182
- finally:
183
- if stdout_thread and stdout_thread.is_alive():
184
- stdout_thread.join()
185
- if stderr_thread and stderr_thread.is_alive():
186
- stderr_thread.join()
187
- if process:
188
- if process.stdout:
189
- process.stdout.close()
190
- if process.stderr:
191
- process.stderr.close()
192
-
193
-
194
- # Combine captured stdout and stderr lines into single strings
195
- stdout = "\n".join(stdout_lines)
196
- stderr = "\n".join(stderr_lines)
197
-
198
- #Build the result object
199
- result = CommandResult(process=process, stdout=stdout, stderr=stderr, returncode=process.returncode, timeout=timeout, command=command)
200
- return result
201
-
202
-
203
100
  class BackupError(Exception):
204
101
  """Exception raised for errors in the backup process."""
205
102
  pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dar-backup
3
- Version: 0.6.16
3
+ Version: 0.6.17
4
4
  Summary: A script to do full, differential and incremental backups using dar. Some files are restored from the backups during verification, after which par2 redundancy files are created. The script also has a cleanup feature to remove old backups and par2 files.
5
5
  Project-URL: Homepage, https://github.com/per2jensen/dar-backup/tree/main/v2
6
6
  Project-URL: Changelog, https://github.com/per2jensen/dar-backup/blob/main/v2/Changelog.md
@@ -744,13 +744,15 @@ This is the `Python` based **version 2** of `dar-backup`.
744
744
  - [Performance tip due to par2](#performance-tip-due-to-par2)
745
745
  - [.darrc sets -vd -vf (since v0.6.4)](#darrc-sets--vd--vf-since-v064)
746
746
  - [Separate log file for command output](#separate-log-file-for-command-output)
747
+ - [Skipping cache directories](#skipping-cache-directories)
747
748
  - [Todo](#todo)
748
749
  - [Reference](#reference)
749
- - [dar-backup](#dar-backup)
750
- - [manager](#manager)
751
- - [cleanup](#cleanup)
752
- - [clean-log](#clean-log)
753
- - [installer](#installer)
750
+ - [Test coverage report](#test-coverage)
751
+ - [dar-backup](#dar-backup-options)
752
+ - [manager](#manager-options)
753
+ - [cleanup](#cleanup-options)
754
+ - [clean-log](#clean-log-options)
755
+ - [installer](#installer-options)
754
756
 
755
757
  ## My use case
756
758
 
@@ -807,15 +809,24 @@ On Ubuntu, install the requirements this way:
807
809
 
808
810
  ### dar-backup
809
811
 
810
- `dar-backup` is built in a way that emphasizes getting backups. It loops over the backup definitions, and in the event of a failure while backing up a backup definition, dar-backup shall log an error and start working on the next backup definition.
812
+ `dar-backup` is built in a way that emphasizes getting backups. It loops over the [backup definitions](#backup-definition-example), and in the event of a failure while backing up a backup definition, dar-backup shall log an error and start working on the next backup definition.
811
813
 
812
814
  There are 3 levels of backups, FULL, DIFF and INCR.
813
815
 
814
- - The author does a FULL yearly backup once a year. This includes all files in all directories as defined in the backup definition(s).
815
- - The author makes a DIFF once a month. The DIFF backs up new and changed files compared to the FULL backup.
816
- - The author takes an INCR backup every 3 days. An INCR backup includes new and changed files compared to the DIFF backup.
817
- -- So, a set of INCR's will contain duplicates (this might change as I become more used to use the catalog databases)
818
- -- No INCR backups will taken until a DIFF backup has been taken.
816
+ - The author does a FULL yearly backup once a year. This includes all files in all directories as defined in the backup definition(s) (assuming `-d` was not given).
817
+ - The author makes a DIFF once a month. The DIFF backs up new and changed files **compared** to the **FULL** backup.
818
+
819
+ - No DIFF backups are taken until a FULL backup has been taken for a particular backup definition.
820
+
821
+ - The author takes an INCR backup every 3 days. An INCR backup includes new and changed files **compared** to the **DIFF** backup.
822
+
823
+ - So, a set of INCR's will contain duplicates (this might change as I become more used to use the catalog databases)
824
+
825
+ - No INCR backups are taken until a DIFF backup has been taken for a particular backup definition.
826
+
827
+ After each backup of a backup definition, `dar-backup` tests the archive and then performs a few restore operations of random files from the archive (see [dar-backup.conf](#config-file)). The restored files are compared to the originals to check if the restore went well.
828
+
829
+ `dar-backup` skips doing a backup of a backup definition if an archive is already in place. So, if you for some reason need to take a new backup on the same date, the first archive must be deleted (I recommend using [cleanup](#cleanup-1)).
819
830
 
820
831
  ### cleanup
821
832
 
@@ -823,6 +834,12 @@ The `cleanup` application deletes DIFF and INCR if the archives are older than t
823
834
 
824
835
  `cleanup` will only remove FULL archives if the option `--cleanup-specific-archives` is used. It requires the user to confirm deletion of FULL archives.
825
836
 
837
+ ### manager
838
+
839
+ `dar`has the concept of catalogs which can be exported and optionally be added to a catalog database. That database makes it much easier to restore the correct version of a backed up file if for example a target date has been set.
840
+
841
+ `dar-backup` adds archive catalogs to their databases (using the `manager` script). Should the operation fail, `dar-backup` logs an error and continue with testing and restore validation tests.
842
+
826
843
  ## How to run
827
844
 
828
845
  ### 1 - installation
@@ -839,7 +856,6 @@ Note:
839
856
 
840
857
  The module `inputimeout` is installed into the venv and used for the confirmation input (with a 30 second timeout)
841
858
 
842
-
843
859
  To install, create a venv and run pip:
844
860
 
845
861
  ```` bash
@@ -902,7 +918,7 @@ manager --create-db
902
918
  ### 4 - do FULL backups
903
919
 
904
920
  Prereq:
905
- Backup definitions are in place in BACKUP.D_DIR (see config file)
921
+ [Backup definitions](#backup-definition-example) are in place in BACKUP.D_DIR (see [config file](#config-file)).
906
922
 
907
923
  You are ready to do backups of all your backup definitions.
908
924
 
@@ -975,7 +991,7 @@ SCRIPT_1 = df -h
975
991
 
976
992
  ### .darrc
977
993
 
978
- The package includes a default `.darrc` file which configures `dar`.
994
+ The package includes a default `darrc` file which configures `dar`.
979
995
 
980
996
  You can override the default `.darrc` using the `--darrc` option.
981
997
 
@@ -1106,7 +1122,7 @@ compress-exclusion:
1106
1122
 
1107
1123
  ### Backup definition example
1108
1124
 
1109
- This piece of configuration is a `backup definition`. It is placed in the BACKUP.D_DIR (see config file description).
1125
+ This piece of configuration is a [backup definition](#backup-definition-example). It is placed in the BACKUP.D_DIR (see config file description).
1110
1126
  The name of the file is the name of the backup definition.
1111
1127
 
1112
1128
  You can use as many backup definitions as you need.
@@ -1143,6 +1159,7 @@ You can use as many backup definitions as you need.
1143
1159
 
1144
1160
  # bypass directores marked as cache directories
1145
1161
  # http://dar.linux.free.fr/doc/Features.html
1162
+ # https://bford.info/cachedir/
1146
1163
  --cache-directory-tagging
1147
1164
  ````
1148
1165
 
@@ -1367,7 +1384,7 @@ deactivate
1367
1384
 
1368
1385
  "dar" in newer versions emits a question about file ownership, which is "answered" with a "no" via the "-Q" option. That in turn leads to an error code 4.
1369
1386
 
1370
- Thus the dar option "--comparison-field=ignore-owner" has been placed in the supplied .darrc file (located in the virtual environment where dar-backup is installed).
1387
+ Thus the dar option "--comparison-field=ignore-owner" has been placed in the supplied [.darrc](#darrc) file (located in the virtual environment where dar-backup is installed).
1371
1388
 
1372
1389
  This causes dar to restore without an error.
1373
1390
 
@@ -1382,7 +1399,7 @@ My home directory is on a btrfs filesystem, while /tmp (for the restore test) is
1382
1399
 
1383
1400
  The restore test can result in an exit code 5, due to the different filesystems used. In order to avoid the errors, the "option "--fsa-scope none" can be used. That will restult in FSA's not being restored.
1384
1401
 
1385
- If you need to use this option, un-comment it in the .darrc file (located in the virtual environment where dar-backup is installed)
1402
+ If you need to use this option, un-comment it in the [.darrc](#darrc) file (located in the virtual environment where dar-backup is installed)
1386
1403
 
1387
1404
  ## Par2
1388
1405
 
@@ -1453,7 +1470,7 @@ Slice size should be smaller than available RAM, apparently a large performance
1453
1470
 
1454
1471
  ### .darrc sets -vd -vf (since v0.6.4)
1455
1472
 
1456
- These .darrc settings make `dar` print the current directory being processed (-vd) and some stats after (-vf)
1473
+ These [.darrc](#darrc) settings make `dar` print the current directory being processed (-vd) and some stats after (-vf)
1457
1474
 
1458
1475
  This is very useful in very long running jobs to get an indication that the backup is proceeding normally.
1459
1476
 
@@ -1465,6 +1482,14 @@ In order to not clutter that log file with the output of commands being run, a n
1465
1482
 
1466
1483
  The secondary log file can get quite cluttered, if you want to remove the clutter, run the `clean-log`script with the `--file` option, or simply delete it.
1467
1484
 
1485
+ ### Skipping cache directories
1486
+
1487
+ The author uses the `--cache-directory-tagging` option in his [backup definitions](#backup-definition-example).
1488
+
1489
+ The effect is that directories with the [CACHEDIR.TAG](https://bford.info/cachedir/) file are not backed up. Those directories contain content fetched from the net, which is of an ephemeral nature and probably not what you want to back up.
1490
+
1491
+ If the option is not in the backup definition, the cache directories are backed up as any other.
1492
+
1468
1493
  ## Todo
1469
1494
 
1470
1495
  - `installer` to generate, but not deploy systemd units and timers for:
@@ -1475,80 +1500,109 @@ The secondary log file can get quite cluttered, if you want to remove the clutte
1475
1500
 
1476
1501
  ## Reference
1477
1502
 
1478
- ### dar-backup
1503
+ ### test coverage
1479
1504
 
1480
- This script is responsible for managing the backup creation and validation process. It supports the following options:
1505
+ Running
1506
+
1507
+ ```` bash
1508
+ pytest --cov=dar_backup tests/
1509
+ ````
1510
+
1511
+ gives for version 0.6.17:
1512
+
1513
+ ```` code
1514
+ ---------- coverage: platform linux, python 3.12.3-final-0 -----------
1515
+ Name Stmts Miss Cover
1516
+ -------------------------------------------------------------------------------------
1517
+ venv/lib/python3.12/site-packages/dar_backup/__about__.py 1 0 100%
1518
+ venv/lib/python3.12/site-packages/dar_backup/__init__.py 0 0 100%
1519
+ venv/lib/python3.12/site-packages/dar_backup/clean_log.py 68 14 79%
1520
+ venv/lib/python3.12/site-packages/dar_backup/cleanup.py 196 53 73%
1521
+ venv/lib/python3.12/site-packages/dar_backup/config_settings.py 66 8 88%
1522
+ venv/lib/python3.12/site-packages/dar_backup/dar_backup.py 464 99 79%
1523
+ venv/lib/python3.12/site-packages/dar_backup/installer.py 46 46 0%
1524
+ venv/lib/python3.12/site-packages/dar_backup/manager.py 316 72 77%
1525
+ venv/lib/python3.12/site-packages/dar_backup/util.py 162 34 79%
1526
+ -------------------------------------------------------------------------------------
1527
+ TOTAL 1319 326 75%
1528
+ ````
1529
+
1530
+ ### dar-backup options
1531
+
1532
+ This script does backups, validation and restoring. It has the following options:
1481
1533
 
1482
1534
  ``` code
1483
- --full-backup Perform a full backup.
1484
- --differential-backup Perform a differential backup.
1485
- --incremental-backup Perform an incremental backup.
1486
- --backup-definition <name> Specify the backup definition file.
1487
- --alternate-reference-archive <file> Use a different archive for DIFF/INCR backups.
1488
- --config-file <path> Specify the path to the configuration file.
1489
- --darrc <path> Specify an optional path to .darrc.
1490
- --examples Show examples of using dar-backup.py.
1491
- --list List available backups.
1492
- --list-contents <archive> List the contents of a specified archive.
1493
- --selection <params> Define file selection for listing/restoring.
1494
- --restore <archive> Restore a specified archive.
1495
- --restore-dir <path> Directory to restore files to.
1496
- --verbose Enable verbose output.
1497
- --suppress-dar-msg Filter out this from the darrc: "-vt", "-vs", "-vd", "-vf", "-va"
1498
- --log-level <level> `debug` or `trace`, default is `info`", default="info".
1499
- --log-stdout Also print log messages to stdout.
1500
- --do-not-compare Do not compare restores to file system.
1501
- --version Show version and license information.
1535
+ -F, --full-backup Perform a full backup.
1536
+ -D, --differential-backup Perform a differential backup.
1537
+ -I, --incremental-backup Perform an incremental backup.
1538
+ -d, --backup-definition <name> Specify the backup definition file.
1539
+ --alternate-reference-archive <file> Use a different archive for DIFF/INCR backups.
1540
+ -c, --config-file <path> Specify the path to the configuration file.
1541
+ --darrc <path> Specify an optional path to .darrc.
1542
+ --examples Show examples of using dar-backup.py.
1543
+ -l, --list List available backups.
1544
+ --list-contents <archive> List the contents of a specified archive.
1545
+ --selection <params> Define file selection for listing/restoring.
1546
+ --restore <archive> Restore a specified archive.
1547
+ -r, --restore-dir <path> Directory to restore files to.
1548
+ --verbose Enable verbose output.
1549
+ --suppress-dar-msg Filter out this from the darrc: "-vt", "-vs", "-vd", "-vf", "-va"
1550
+ --log-level <level> `debug` or `trace`, default is `info`.
1551
+ --log-stdout Also print log messages to stdout.
1552
+ --do-not-compare Do not compare restores to file system.
1553
+ -v --version Show version and license information.
1502
1554
  ```
1503
1555
 
1504
- ### manager
1556
+ ### manager options
1505
1557
 
1506
- This script manages `dar` databases and catalogs. Available options include:
1558
+ This script manages `dar` databases and catalogs. Available options:
1507
1559
 
1508
1560
  ``` code
1509
- --create-db Create missing databases for all backup definitions.
1510
- --alternate-archive-dir <path> Use this directory instead of BACKUP_DIR in the config file.
1511
- --add-dir <path> Add all archive catalogs in this directory to databases.
1512
- -d, --backup-def <name> Restrict to work only on this backup definition.
1513
- --add-specific-archive <archive> Add this archive to the catalog database.
1514
- --remove-specific-archive <archive> Remove this archive from the catalog database.
1515
- -l, --list-catalogs List catalogs in databases for all backup definitions.
1516
- --list-catalog-contents <num> List contents of a catalog by catalog number.
1517
- --list-archive-contents <archive> List contents of an archive’s catalog, given the archive name.
1518
- --find-file <file> Search catalogs for a specific file.
1519
- --verbose Enable verbose output.
1520
- --log-level <level> `debug` or `trace`, default is `info`", default="info".
1561
+ -c, --config-file Path to dar-backup.conf
1562
+ --create-db Create missing databases for all backup definitions.
1563
+ --alternate-archive-dir <path> Use this directory instead of BACKUP_DIR in the config file.
1564
+ --add-dir <path> Add all archive catalogs in this directory to databases.
1565
+ -d, --backup-def <name> Restrict to work only on this backup definition.
1566
+ --add-specific-archive <archive> Add this archive to the catalog database.
1567
+ --remove-specific-archive <archive> Remove this archive from the catalog database.
1568
+ -l, --list-catalogs List catalogs in databases for all backup definitions.
1569
+ --list-catalog-contents <num> List contents of a catalog by catalog number.
1570
+ --list-archive-contents <archive> List contents of an archive’s catalog, given the archive name.
1571
+ --find-file <file> Search catalogs for a specific file.
1572
+ --verbose Enable verbose output.
1573
+ --log-level <level> `debug` or `trace`, default is `info`", default="info".
1521
1574
  ```
1522
1575
 
1523
- ### cleanup
1576
+ ### cleanup options
1524
1577
 
1525
- This script cleans up old backups and manages storage. Supported options:
1578
+ This script cleans up old backups and par2 files. Supported options:
1526
1579
 
1527
1580
  ``` code
1528
- -d, --backup-definition Backup definition to cleanup.
1529
- -c, --config-file Path to 'dar-backup.conf', default='~/.config/dar-backup/dar-backup.conf.
1530
- -v, --version Show version & license information.
1531
- --alternate-archive-dir Clean up in this directory instead of the default one.
1532
- --cleanup-specific-archives <archive>, ... Comma separated list of archives to cleanup.
1533
- -l, --list List available archives.
1534
- --verbose Print various status messages to screen.
1535
- --log-level <level> `debug` or `trace`, default is `info`", default="info".
1536
- --log-stdout Print log messages to stdout.
1581
+ -d, --backup-definition Backup definition to cleanup.
1582
+ -c, --config-file Path to 'dar-backup.conf'
1583
+ -v, --version Show version & license information.
1584
+ --alternate-archive-dir Clean up in this directory instead of the default one.
1585
+ --cleanup-specific-archives "<archive>, <>, ..." Comma separated list of archives to cleanup.
1586
+ -l, --list List available archives (filter using the -d option).
1587
+ --verbose Print various status messages to screen.
1588
+ --log-level <level> `debug` or `trace`, default is `info`", default="info".
1589
+ --log-stdout Print log messages to stdout.
1590
+ --test-mode This is used when running pytest test cases
1537
1591
  ```
1538
1592
 
1539
- ### clean-log
1593
+ ### clean-log options
1540
1594
 
1541
1595
  This script removes excessive logging output from `dar` logs, improving readability and efficiency. Available options:
1542
1596
 
1543
1597
  ``` code
1544
1598
  -f, --file <path> Specify the log file(s) to be cleaned.
1545
- -c, --config-file <path> Specify the configuration file (default: ~/.config/dar-backup/dar-backup.conf).
1599
+ -c, --config-file <path> Path to dar-backup.conf.
1546
1600
  --dry-run Show which lines would be removed without modifying the file.
1547
1601
  -v, --version Display version and licensing information.
1548
1602
  -h, --help Displays usage info
1549
1603
  ```
1550
1604
 
1551
- ### installer
1605
+ ### installer options
1552
1606
 
1553
1607
  Sets up `dar-backup`for a user.
1554
1608
 
@@ -0,0 +1,17 @@
1
+ dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
2
+ dar_backup/__about__.py,sha256=vii4GL7MExpBC8tvQjQXAsEgfxDE9p438_97wKl4XCc,22
3
+ dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ dar_backup/clean_log.py,sha256=cGhtKYnQJ2ceNQfw5XcCln_WNBasbmlfhO3kRydjDNk,5196
5
+ dar_backup/cleanup.py,sha256=HA8SmwrqDGjfFg4L0CuUQ5N0YJFT6qYbCuvh5mQseF0,13148
6
+ dar_backup/command_runner.py,sha256=74Fsylz1NN-dn8lbdRhkL6LA1r527QJeojBlniGrPuo,2708
7
+ dar_backup/config_settings.py,sha256=Rh4T35-w_5tpRAViMfv3YP3GBpG4mQy7Do8cNBzYAR0,4912
8
+ dar_backup/dar-backup.conf,sha256=64O3bGlzqupneT2gVeaETJ1qS6-3Exet9Zto27jgwPQ,897
9
+ dar_backup/dar_backup.py,sha256=NHBm3zsOhCHnCVoPO0ysD3uMdIQMe62AIdr0yCQ_6BY,37952
10
+ dar_backup/installer.py,sha256=ehp4KSgTc8D9Edsyve5v3NY2MuDbuTFYQQPgou8woV8,4331
11
+ dar_backup/manager.py,sha256=4NeIVgrhIzOS8UePUCdvtswEG55ue0tXWAK7SjD3tpo,21897
12
+ dar_backup/util.py,sha256=6dJXFOjIIZqerbNVFxJZ6gQ4ZVAxyY-RxHcO--9bxwg,8462
13
+ dar_backup-0.6.17.dist-info/METADATA,sha256=-TmZ95gGD9VX1Uz0so4G8sWWX0RE6Aq8qkfKThkDFnY,79979
14
+ dar_backup-0.6.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ dar_backup-0.6.17.dist-info/entry_points.txt,sha256=Z7P5BUbhtJxo8_nB9qNIMay2eGDbsMKB3Fjwv3GMa4g,202
16
+ dar_backup-0.6.17.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
17
+ dar_backup-0.6.17.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
2
- dar_backup/__about__.py,sha256=iZmWvp4Ehnidf9YJv6Dpn5Sma5S84lPbQtLoYZ2OuiI,22
3
- dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- dar_backup/clean_log.py,sha256=cGhtKYnQJ2ceNQfw5XcCln_WNBasbmlfhO3kRydjDNk,5196
5
- dar_backup/cleanup.py,sha256=EoT_jTiJmy_HoWxxlWuRn21GoMnr5ioN8yM7qiNRPKE,13351
6
- dar_backup/config_settings.py,sha256=uicCq6FnpxPFzbv7xfYSXNnQf1tfLk1Z3VIO9M71fsE,4659
7
- dar_backup/dar-backup.conf,sha256=-wXqP4vj5TS7cCfMJN1nbk-1Sqkq00Tg22ySQXynUF4,902
8
- dar_backup/dar_backup.py,sha256=PI154FIXZiU36iAyLZCCxAciDeBSwoBXYQh0n5JmNEs,37895
9
- dar_backup/installer.py,sha256=ehp4KSgTc8D9Edsyve5v3NY2MuDbuTFYQQPgou8woV8,4331
10
- dar_backup/manager.py,sha256=sQl0xdWwBgui11S9Ekg0hOSC4gt89nz_Z8Bt8IPXCDw,21640
11
- dar_backup/util.py,sha256=F6U-e-WugxCxLPVoiWsM6_YO8VrDw1wdgGvtnGnig2I,12279
12
- dar_backup-0.6.16.dist-info/METADATA,sha256=xaYjZK6XAWOc_NHsnktylhdvqWLqzmo94GT1MoR6qLw,76678
13
- dar_backup-0.6.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- dar_backup-0.6.16.dist-info/entry_points.txt,sha256=Z7P5BUbhtJxo8_nB9qNIMay2eGDbsMKB3Fjwv3GMa4g,202
15
- dar_backup-0.6.16.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
16
- dar_backup-0.6.16.dist-info/RECORD,,