dar-backup 0.6.0__py3-none-any.whl → 0.6.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.
- dar_backup/__about__.py +1 -1
- dar_backup/dar_backup.py +57 -71
- dar_backup/util.py +11 -12
- {dar_backup-0.6.0.dist-info → dar_backup-0.6.1.dist-info}/METADATA +1 -1
- dar_backup-0.6.1.dist-info/RECORD +13 -0
- dar_backup-0.6.0.dist-info/RECORD +0 -13
- {dar_backup-0.6.0.dist-info → dar_backup-0.6.1.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.0.dist-info → dar_backup-0.6.1.dist-info}/entry_points.txt +0 -0
- {dar_backup-0.6.0.dist-info → dar_backup-0.6.1.dist-info}/licenses/LICENSE +0 -0
dar_backup/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.6.
|
|
1
|
+
__version__ = "0.6.1"
|
dar_backup/dar_backup.py
CHANGED
|
@@ -64,23 +64,18 @@ def backup(backup_file: str, backup_definition: str, darrc: str, config_setting
|
|
|
64
64
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
65
65
|
try:
|
|
66
66
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
67
|
-
|
|
67
|
+
logger.info("Back from run_command")
|
|
68
68
|
if process.returncode == 0:
|
|
69
69
|
logger.info("FULL backup completed successfully.")
|
|
70
70
|
elif process.returncode == 5:
|
|
71
71
|
logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
|
|
72
72
|
else:
|
|
73
|
-
|
|
74
|
-
logger.error("stdout: ", stdout)
|
|
75
|
-
logger.error("stderr: ", stderr)
|
|
76
|
-
raise Exception(stderr)
|
|
73
|
+
raise Exception(str(process))
|
|
77
74
|
except subprocess.CalledProcessError as e:
|
|
78
75
|
logger.error(f"Backup command failed: {e}")
|
|
79
76
|
raise BackupError(f"Backup command failed: {e}") from e
|
|
80
77
|
except Exception as e:
|
|
81
78
|
logger.exception(f"Unexpected error during backup")
|
|
82
|
-
logger.error("stderr:", stderr)
|
|
83
|
-
logger.error("stdout: ", stdout)
|
|
84
79
|
raise BackupError(f"Unexpected error during backup: {e}") from e
|
|
85
80
|
|
|
86
81
|
|
|
@@ -120,16 +115,12 @@ def differential_backup(backup_file: str, backup_definition: str, base_backup_fi
|
|
|
120
115
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
121
116
|
try:
|
|
122
117
|
process = run_command(command)
|
|
123
|
-
stdout, stderr = process.stdout, process.stderr
|
|
124
118
|
if process.returncode == 0:
|
|
125
119
|
logger.info("DIFF backup completed successfully.")
|
|
126
120
|
elif process.returncode == 5:
|
|
127
121
|
logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
|
|
128
122
|
else:
|
|
129
|
-
|
|
130
|
-
logger.error("stdout: ", stdout)
|
|
131
|
-
logger.error("stderr: ", stderr)
|
|
132
|
-
raise Exception(stderr)
|
|
123
|
+
raise Exception(str(process))
|
|
133
124
|
except subprocess.CalledProcessError as e:
|
|
134
125
|
logger.error(f"Differential backup command failed: {e}")
|
|
135
126
|
raise DifferentialBackupError(f"Differential backup command failed: {e}") from e
|
|
@@ -174,22 +165,17 @@ def incremental_backup(backup_file: str, backup_definition: str, last_backup_fil
|
|
|
174
165
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
175
166
|
try:
|
|
176
167
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
177
|
-
stdout, stderr = process.stdout, process.stderr
|
|
178
168
|
if process.returncode == 0:
|
|
179
169
|
logger.info("INCR backup completed successfully.")
|
|
180
170
|
elif process.returncode == 5:
|
|
181
171
|
logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
|
|
182
172
|
else:
|
|
183
|
-
|
|
184
|
-
logger.error("stdout: ", stdout)
|
|
185
|
-
logger.error("stderr: ", stderr)
|
|
186
|
-
raise Exception(stderr)
|
|
173
|
+
raise Exception(str(process))
|
|
187
174
|
except subprocess.CalledProcessError as e:
|
|
188
175
|
logger.error(f"Incremental backup command failed: {e}")
|
|
189
176
|
raise IncrementalBackupError(f"Incremental backup command failed: {e}") from e
|
|
190
177
|
except Exception as e:
|
|
191
178
|
logger.exception(f"Unexpected error during incremental backup")
|
|
192
|
-
logger.error("Exception details:", exc_info=True)
|
|
193
179
|
raise IncrementalBackupError(f"Unexpected error during incremental backup: {e}") from e
|
|
194
180
|
|
|
195
181
|
|
|
@@ -286,11 +272,10 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
286
272
|
command = ['dar', '-t', backup_file, '-Q']
|
|
287
273
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
288
274
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
289
|
-
stdout, stderr = process.stdout, process.stderr
|
|
290
275
|
if process.returncode == 0:
|
|
291
276
|
logger.info("Archive integrity test passed.")
|
|
292
277
|
else:
|
|
293
|
-
raise Exception(
|
|
278
|
+
raise Exception(str(process))
|
|
294
279
|
|
|
295
280
|
if args.do_not_compare:
|
|
296
281
|
return result
|
|
@@ -325,10 +310,8 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
325
310
|
command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-O', '-Q']
|
|
326
311
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
327
312
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
328
|
-
stdout, stderr = process.stdout, process.stderr
|
|
329
313
|
if process.returncode != 0:
|
|
330
|
-
|
|
331
|
-
raise Exception(stderr)
|
|
314
|
+
raise Exception(str(process))
|
|
332
315
|
|
|
333
316
|
if filecmp.cmp(os.path.join(config_settings.test_restore_dir, restored_file_path.lstrip("/")), os.path.join(root_path, restored_file_path.lstrip("/")), shallow=False):
|
|
334
317
|
logger.info(f"Success: file '{restored_file_path}' matches the original")
|
|
@@ -365,17 +348,13 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
|
|
|
365
348
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
366
349
|
try:
|
|
367
350
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
368
|
-
|
|
351
|
+
if process.returncode == 0:
|
|
352
|
+
logger.info(f"Restore completed successfully to: '{restore_dir}'")
|
|
353
|
+
else:
|
|
354
|
+
raise Exception(str(process))
|
|
369
355
|
except subprocess.CalledProcessError as e:
|
|
370
|
-
logger.error("Exception details:", exc_info=True)
|
|
371
|
-
logger.error(f"stdout: {stdout}")
|
|
372
|
-
logger.error(f"stderr: {stderr}")
|
|
373
356
|
raise RestoreError(f"Restore command failed: {e}") from e
|
|
374
357
|
except Exception as e:
|
|
375
|
-
logger.exception(f"Unexpected error during restore")
|
|
376
|
-
logger.error("Exception details:", exc_info=True)
|
|
377
|
-
logger.error(f"stdout: {stdout}")
|
|
378
|
-
logger.error(f"stderr: {stderr}")
|
|
379
358
|
raise RestoreError(f"Unexpected error during restore: {e}") from e
|
|
380
359
|
|
|
381
360
|
|
|
@@ -393,19 +372,24 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
|
|
|
393
372
|
"""
|
|
394
373
|
logger.debug(f"Getting backed up files from DAR archive in xml: '{backup_name}'")
|
|
395
374
|
backup_path = os.path.join(backup_dir, backup_name)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
375
|
+
try:
|
|
376
|
+
command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
|
|
377
|
+
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
378
|
+
process = run_command(command)
|
|
379
|
+
# Parse the XML data
|
|
380
|
+
root = ET.fromstring(process.stdout)
|
|
381
|
+
output = None # help gc
|
|
382
|
+
# Extract full paths and file size for all <File> elements
|
|
383
|
+
file_paths = find_files_with_paths(root)
|
|
384
|
+
root = None # help gc
|
|
385
|
+
logger.trace(str(process))
|
|
386
|
+
logger.trace(file_paths)
|
|
387
|
+
return file_paths
|
|
388
|
+
except subprocess.CalledProcessError as e:
|
|
389
|
+
logger.error(f"Error listing backed up files from DAR archive: '{backup_name}'")
|
|
390
|
+
raise BackupError(f"Error listing backed up files from DAR archive: '{backup_name}'") from e
|
|
391
|
+
except Exception as e:
|
|
392
|
+
raise RuntimeError(f"Unexpected error listing backed up files from DAR archive: '{backup_name}'") from e
|
|
409
393
|
|
|
410
394
|
|
|
411
395
|
def list_contents(backup_name, backup_dir, selection=None):
|
|
@@ -421,17 +405,26 @@ def list_contents(backup_name, backup_dir, selection=None):
|
|
|
421
405
|
None
|
|
422
406
|
"""
|
|
423
407
|
backup_path = os.path.join(backup_dir, backup_name)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
408
|
+
try:
|
|
409
|
+
command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
|
|
410
|
+
if selection:
|
|
411
|
+
selection_criteria = shlex.split(selection)
|
|
412
|
+
command.extend(selection_criteria)
|
|
413
|
+
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
414
|
+
process = run_command(command)
|
|
415
|
+
stdout,stderr = process.stdout, process.stderr
|
|
416
|
+
if process.returncode != 0:
|
|
417
|
+
logger.error(f"Error listing contents of backup: '{backup_name}'")
|
|
418
|
+
raise subprocess.CalledProcessError(str(process))
|
|
419
|
+
for line in stdout.splitlines():
|
|
420
|
+
if "[--- REMOVED ENTRY ----]" in line or "[Saved]" in line:
|
|
421
|
+
print(line)
|
|
422
|
+
except subprocess.CalledProcessError as e:
|
|
423
|
+
logger.error(f"Error listing contents of backup: '{backup_name}'")
|
|
424
|
+
raise BackupError(f"Error listing contents of backup: '{backup_name}'") from e
|
|
425
|
+
except Exception as e:
|
|
426
|
+
raise RuntimeError(f"Unexpected error listing contents of backup: '{backup_name}'") from e
|
|
431
427
|
|
|
432
|
-
for line in stdout.splitlines():
|
|
433
|
-
if "[--- REMOVED ENTRY ----]" in line or "[Saved]" in line:
|
|
434
|
-
print(line)
|
|
435
428
|
|
|
436
429
|
|
|
437
430
|
def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, backup_type: str):
|
|
@@ -634,7 +627,7 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
634
627
|
for key in sorted(config_setting.config[type].keys()):
|
|
635
628
|
script = config_setting.config[type][key]
|
|
636
629
|
try:
|
|
637
|
-
result = subprocess.run(script, shell=True, check=True)
|
|
630
|
+
result = subprocess.run(script, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=True)
|
|
638
631
|
logger.info(f"{type} {key}: '{script}' run, return code: {result.returncode}")
|
|
639
632
|
logger.info(f"{type} stdout:\n{result.stdout}")
|
|
640
633
|
if result.returncode != 0:
|
|
@@ -642,9 +635,7 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
642
635
|
raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {result.returncode}")
|
|
643
636
|
except subprocess.CalledProcessError as e:
|
|
644
637
|
logger.error(f"Error executing {key}: '{script}': {e}")
|
|
645
|
-
|
|
646
|
-
logger.error(f"{type} stderr:\n{result.stderr}")
|
|
647
|
-
raise e
|
|
638
|
+
raise e
|
|
648
639
|
|
|
649
640
|
|
|
650
641
|
def main():
|
|
@@ -754,23 +745,18 @@ def main():
|
|
|
754
745
|
|
|
755
746
|
requirements('POSTREQ', config_settings)
|
|
756
747
|
|
|
757
|
-
|
|
748
|
+
args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
|
|
749
|
+
sys.exit(0)
|
|
758
750
|
except Exception as e:
|
|
759
751
|
logger.exception("An error occurred")
|
|
760
752
|
logger.error("Exception details:", exc_info=True)
|
|
761
|
-
|
|
762
|
-
end_time=int(time())
|
|
763
|
-
logger.info(f"END TIME: {end_time}")
|
|
764
|
-
|
|
765
|
-
error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
|
|
766
|
-
if len(error_lines) > 0:
|
|
767
753
|
args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
754
|
+
sys.exit(1)
|
|
755
|
+
finally:
|
|
756
|
+
end_time=int(time())
|
|
757
|
+
logger.info(f"END TIME: {end_time}")
|
|
758
|
+
|
|
759
|
+
# error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
|
|
774
760
|
|
|
775
761
|
|
|
776
762
|
if __name__ == "__main__":
|
dar_backup/util.py
CHANGED
|
@@ -81,9 +81,6 @@ def setup_logging(log_file: str, log_level: str, log_to_stdout: bool=False, logg
|
|
|
81
81
|
stdout_handler.setFormatter(formatter)
|
|
82
82
|
logger.addHandler(stdout_handler)
|
|
83
83
|
|
|
84
|
-
# logging.basicConfig(filename=log_file, level=level_used,
|
|
85
|
-
# format='%(asctime)s - %(levelname)s - %(message)s')
|
|
86
|
-
|
|
87
84
|
except Exception as e:
|
|
88
85
|
print("logging not initialized, exiting.")
|
|
89
86
|
traceback.print_exc()
|
|
@@ -103,6 +100,9 @@ class CommandResult(NamedTuple):
|
|
|
103
100
|
timeout: int
|
|
104
101
|
command: list[str]
|
|
105
102
|
|
|
103
|
+
def __str__(self):
|
|
104
|
+
return f"CommandResult: [Return Code: '{self.returncode}', Stdout: '{self.stdout}', Stderr: '{self.stderr}', Timeout: '{self.timeout}', Command: '{self.command}']"
|
|
105
|
+
|
|
106
106
|
|
|
107
107
|
def run_command(command: list[str], timeout: int=30) -> typing.NamedTuple:
|
|
108
108
|
"""
|
|
@@ -125,23 +125,22 @@ def run_command(command: list[str], timeout: int=30) -> typing.NamedTuple:
|
|
|
125
125
|
subprocess.TimeoutExpired: if the command execution times out (see `timeout` parameter).
|
|
126
126
|
Exception: raise exceptions during command runs.
|
|
127
127
|
"""
|
|
128
|
-
|
|
128
|
+
stdout = None
|
|
129
|
+
stderr = None
|
|
129
130
|
try:
|
|
130
|
-
logger.
|
|
131
|
+
logger.debug(f"Running command: {command}")
|
|
131
132
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
132
|
-
stdout, stderr = process.communicate(timeout
|
|
133
|
+
stdout, stderr = process.communicate(timeout) # Wait with timeout
|
|
134
|
+
result = CommandResult(process=process, stdout=stdout, stderr=stderr, returncode=process.returncode, timeout=timeout, command=command)
|
|
135
|
+
logger.debug(f"Command result: {str(result)}")
|
|
133
136
|
except subprocess.TimeoutExpired:
|
|
134
137
|
process.terminate()
|
|
135
138
|
logger.error(f"Command: '{command}' timed out and was terminated.")
|
|
139
|
+
raise
|
|
136
140
|
except Exception as e:
|
|
137
141
|
logger.error(f"Error running command: {command}", exc_info=True)
|
|
138
142
|
raise
|
|
139
|
-
|
|
140
|
-
if not stdout:
|
|
141
|
-
stdout = None
|
|
142
|
-
if not stderr:
|
|
143
|
-
stderr = None
|
|
144
|
-
return CommandResult(process=process, stdout=stdout, stderr=stderr, returncode=process.returncode, timeout=timeout, command=command)
|
|
143
|
+
return result
|
|
145
144
|
|
|
146
145
|
|
|
147
146
|
def extract_error_lines(log_file_path: str, start_time: str, end_time: str):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dar-backup
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
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
|
|
6
6
|
Project-URL: Issues, https://github.com/per2jensen/dar-backup/issues
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
dar_backup/.darrc,sha256=ggex9N6eETOS6u003_QRRJMeWbveQfkT1lDBt0XpU-I,2112
|
|
2
|
+
dar_backup/__about__.py,sha256=XvHFZM0padtrqitt9-p2enlBUGqc6vGvWNLx2iJv09g,21
|
|
3
|
+
dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
dar_backup/cleanup.py,sha256=DgmxSUwKrLLIQuYSIY_yTRhIuOMgI6ivjlQuH4u3wX4,9057
|
|
5
|
+
dar_backup/config_settings.py,sha256=CBMUhLOOZ-x7CRdS3vBDk4TYaGqC4N1Ot8IMH-qPaI0,3617
|
|
6
|
+
dar_backup/dar_backup.py,sha256=xnDuqkpo8q_X1FLKID9I_s__FQozl5EIrwNHdIJAd_0,37525
|
|
7
|
+
dar_backup/manager.py,sha256=lkw1ZAIdxY7WedLPKZMnHpuq_QbjkUdcG61ooiwUYpo,10197
|
|
8
|
+
dar_backup/util.py,sha256=6lPCFHr3MDdaLWAW9EDMZ4jdL7pt8rki-5dOXcesmP8,8955
|
|
9
|
+
dar_backup-0.6.1.dist-info/METADATA,sha256=m3P_iVauaMno0fAhc4iJgO_DS9FobsqV5StQzUdU6wY,22496
|
|
10
|
+
dar_backup-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
dar_backup-0.6.1.dist-info/entry_points.txt,sha256=x9vnW-JEl8mpDJC69f_XBcn0mBSkV1U0cyvFV-NAP1g,126
|
|
12
|
+
dar_backup-0.6.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
13
|
+
dar_backup-0.6.1.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
dar_backup/.darrc,sha256=ggex9N6eETOS6u003_QRRJMeWbveQfkT1lDBt0XpU-I,2112
|
|
2
|
-
dar_backup/__about__.py,sha256=DS49q_bFynltwBtgneHYeVXTHLg5bjAOFWvTkv-jYmY,21
|
|
3
|
-
dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
dar_backup/cleanup.py,sha256=DgmxSUwKrLLIQuYSIY_yTRhIuOMgI6ivjlQuH4u3wX4,9057
|
|
5
|
-
dar_backup/config_settings.py,sha256=CBMUhLOOZ-x7CRdS3vBDk4TYaGqC4N1Ot8IMH-qPaI0,3617
|
|
6
|
-
dar_backup/dar_backup.py,sha256=Kz_Gwe9t6vHGk590D3pbqDe1ptyrhLNpQWCU8oDcqos,37870
|
|
7
|
-
dar_backup/manager.py,sha256=lkw1ZAIdxY7WedLPKZMnHpuq_QbjkUdcG61ooiwUYpo,10197
|
|
8
|
-
dar_backup/util.py,sha256=ZYalRptXvSI9laLmHOe67WOOHoHF0CS4osE9NP1MDWA,8892
|
|
9
|
-
dar_backup-0.6.0.dist-info/METADATA,sha256=-D3-rzNmkAMpN5SpMSeoveXuGQ1x8EncsdBxs0Lfx-s,22496
|
|
10
|
-
dar_backup-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
-
dar_backup-0.6.0.dist-info/entry_points.txt,sha256=x9vnW-JEl8mpDJC69f_XBcn0mBSkV1U0cyvFV-NAP1g,126
|
|
12
|
-
dar_backup-0.6.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
13
|
-
dar_backup-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|