dar-backup 0.6.0__py3-none-any.whl → 0.6.2__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 +155 -241
- dar_backup/util.py +11 -12
- {dar_backup-0.6.0.dist-info → dar_backup-0.6.2.dist-info}/METADATA +1 -1
- dar_backup-0.6.2.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.2.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.0.dist-info → dar_backup-0.6.2.dist-info}/entry_points.txt +0 -0
- {dar_backup-0.6.0.dist-info → dar_backup-0.6.2.dist-info}/licenses/LICENSE +0 -0
dar_backup/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.6.
|
|
1
|
+
__version__ = "0.6.2"
|
dar_backup/dar_backup.py
CHANGED
|
@@ -17,181 +17,60 @@ from argparse import ArgumentParser
|
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from time import time
|
|
20
|
+
from typing import List
|
|
20
21
|
|
|
21
22
|
from . import __about__ as about
|
|
22
23
|
from dar_backup.config_settings import ConfigSettings
|
|
23
24
|
from dar_backup.util import list_backups
|
|
24
25
|
from dar_backup.util import run_command
|
|
25
26
|
from dar_backup.util import setup_logging
|
|
26
|
-
from dar_backup.util import extract_error_lines
|
|
27
27
|
from dar_backup.util import BackupError
|
|
28
|
-
from dar_backup.util import DifferentialBackupError
|
|
29
|
-
from dar_backup.util import IncrementalBackupError
|
|
30
28
|
from dar_backup.util import RestoreError
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
logger = None
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
def backup(backup_file: str, backup_definition: str, darrc: str, config_settings: ConfigSettings):
|
|
33
|
+
def generic_backup(type: str, command: List[str], backup_file: str, backup_definition: str, darrc: str, config_settings: ConfigSettings):
|
|
37
34
|
"""
|
|
38
|
-
Performs a
|
|
35
|
+
Performs a backup using the 'dar' command.
|
|
39
36
|
|
|
40
37
|
This function initiates a full backup operation by constructing and executing a command
|
|
41
38
|
with the 'dar' utility. It checks if the backup file already exists to avoid overwriting
|
|
42
39
|
previous backups. If the backup file does not exist, it proceeds with the backup operation.
|
|
43
40
|
|
|
44
41
|
Args:
|
|
42
|
+
type (str): The type of backup (FULL, DIFF, INCR).
|
|
43
|
+
command (List[str]): The command to execute for the backup operation.
|
|
45
44
|
backup_file (str): The base name of the backup file. The actual backup will be saved
|
|
46
45
|
as '{backup_file}.1.dar'.
|
|
47
46
|
backup_definition (str): The path to the backup definition file. This file contains
|
|
48
47
|
specific instructions for the 'dar' utility, such as which
|
|
49
48
|
directories to include or exclude.
|
|
49
|
+
darrc (str): The path to the '.darrc' configuration file.
|
|
50
|
+
config_settings (ConfigSettings): An instance of the ConfigSettings class.
|
|
51
|
+
|
|
50
52
|
|
|
51
|
-
Note:
|
|
52
|
-
This function logs an error and returns early if the backup file already exists.
|
|
53
|
-
It logs the command being executed and reports upon successful completion of the backup.
|
|
54
53
|
|
|
55
54
|
Raises:
|
|
56
55
|
BackupError: If an error occurs during the backup process.
|
|
57
56
|
"""
|
|
58
|
-
|
|
59
|
-
logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
logger.info(f"===> Starting FULL backup for {backup_definition}")
|
|
63
|
-
command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-Q', "compress-exclusion", "verbose"]
|
|
57
|
+
logger.info(f"===> Starting {type} backup for {backup_definition}")
|
|
64
58
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
65
59
|
try:
|
|
66
60
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
67
|
-
stdout, stderr = process.stdout, process.stderr
|
|
68
61
|
if process.returncode == 0:
|
|
69
|
-
logger.info("
|
|
62
|
+
logger.info(f"{type} backup completed successfully.")
|
|
70
63
|
elif process.returncode == 5:
|
|
71
64
|
logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
|
|
72
65
|
else:
|
|
73
|
-
|
|
74
|
-
logger.error("stdout: ", stdout)
|
|
75
|
-
logger.error("stderr: ", stderr)
|
|
76
|
-
raise Exception(stderr)
|
|
66
|
+
raise Exception(str(process))
|
|
77
67
|
except subprocess.CalledProcessError as e:
|
|
78
68
|
logger.error(f"Backup command failed: {e}")
|
|
79
69
|
raise BackupError(f"Backup command failed: {e}") from e
|
|
80
70
|
except Exception as e:
|
|
81
71
|
logger.exception(f"Unexpected error during backup")
|
|
82
|
-
logger.error("stderr:", stderr)
|
|
83
|
-
logger.error("stdout: ", stdout)
|
|
84
72
|
raise BackupError(f"Unexpected error during backup: {e}") from e
|
|
85
73
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def differential_backup(backup_file: str, backup_definition: str, base_backup_file: str, darrc: str, config_settings: ConfigSettings):
|
|
91
|
-
"""
|
|
92
|
-
Creates a differential backup based on a specified base backup.
|
|
93
|
-
|
|
94
|
-
This function performs a differential backup by comparing the current state of the data
|
|
95
|
-
against a specified base backup file. It captures only the changes made since that base
|
|
96
|
-
backup, resulting in a smaller and faster backup process compared to a full backup.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
backup_file (str): The base name for the differential backup file. The actual backup
|
|
100
|
-
will be saved as '{backup_file}.1.dar'.
|
|
101
|
-
backup_definition (str): The path to the backup definition file. This file contains
|
|
102
|
-
specific instructions for the 'dar' utility, such as which
|
|
103
|
-
directories to include or exclude.
|
|
104
|
-
base_backup_file (str): The base name of the full backup file that serves as the
|
|
105
|
-
reference point for the differential backup.
|
|
106
|
-
|
|
107
|
-
Note:
|
|
108
|
-
This function logs an error and returns early if the differential backup file already exists.
|
|
109
|
-
It logs the command being executed and reports upon successful completion of the differential backup.
|
|
110
|
-
|
|
111
|
-
Raises:
|
|
112
|
-
DifferentialBackupError: If the differential backup command fails or encounters an unexpected error.
|
|
113
|
-
"""
|
|
114
|
-
if os.path.exists(backup_file + '.1.dar'):
|
|
115
|
-
logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
logger.info(f"===> Starting DIFF backup for {backup_definition}")
|
|
119
|
-
command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-A', base_backup_file, '-Q', "compress-exclusion", "verbose"]
|
|
120
|
-
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
121
|
-
try:
|
|
122
|
-
process = run_command(command)
|
|
123
|
-
stdout, stderr = process.stdout, process.stderr
|
|
124
|
-
if process.returncode == 0:
|
|
125
|
-
logger.info("DIFF backup completed successfully.")
|
|
126
|
-
elif process.returncode == 5:
|
|
127
|
-
logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
|
|
128
|
-
else:
|
|
129
|
-
logger.error("dar return code: ", process.returncode)
|
|
130
|
-
logger.error("stdout: ", stdout)
|
|
131
|
-
logger.error("stderr: ", stderr)
|
|
132
|
-
raise Exception(stderr)
|
|
133
|
-
except subprocess.CalledProcessError as e:
|
|
134
|
-
logger.error(f"Differential backup command failed: {e}")
|
|
135
|
-
raise DifferentialBackupError(f"Differential backup command failed: {e}") from e
|
|
136
|
-
except Exception as e:
|
|
137
|
-
logger.exception(f"Unexpected error during differential backup")
|
|
138
|
-
logger.error("Exception details:", exc_info=True)
|
|
139
|
-
raise DifferentialBackupError(f"Unexpected error during differential backup: {e}") from e
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def incremental_backup(backup_file: str, backup_definition: str, last_backup_file: str, darrc: str, config_settings: ConfigSettings):
|
|
143
|
-
"""
|
|
144
|
-
Creates an incremental backup based on the last backup file.
|
|
145
|
-
|
|
146
|
-
This function performs an incremental backup by comparing the current state of the data
|
|
147
|
-
against the last backup file, whether it's a full backup or the most recent incremental backup.
|
|
148
|
-
It captures only the changes made since that last backup, making it efficient for frequent
|
|
149
|
-
backups with minimal data changes.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
backup_file (str): The base name for the incremental backup file. The actual backup
|
|
153
|
-
will be saved with a unique identifier to distinguish it from other backups.
|
|
154
|
-
backup_definition (str): The path to the backup definition file. This file contains
|
|
155
|
-
specific instructions for the 'dar' utility, such as which
|
|
156
|
-
directories to include or exclude.
|
|
157
|
-
last_backup_file (str): The base name of the last backup file (full or incremental) that
|
|
158
|
-
serves as the reference point for the incremental backup.
|
|
159
|
-
|
|
160
|
-
Note:
|
|
161
|
-
This function checks if the incremental backup file already exists to prevent overwriting
|
|
162
|
-
previous backups. It logs the command being executed and reports upon successful completion
|
|
163
|
-
of the incremental backup.
|
|
164
|
-
|
|
165
|
-
Raises:
|
|
166
|
-
IncrementalBackupError: If the incremental backup command fails or an unexpected error occurs.
|
|
167
|
-
"""
|
|
168
|
-
if os.path.exists(backup_file + '.1.dar'):
|
|
169
|
-
logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
logger.info(f"===> Starting INCR backup for {backup_definition}")
|
|
173
|
-
command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-A', last_backup_file, '-Q', "compress-exclusion", "verbose"]
|
|
174
|
-
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
175
|
-
try:
|
|
176
|
-
process = run_command(command, config_settings.command_timeout_secs)
|
|
177
|
-
stdout, stderr = process.stdout, process.stderr
|
|
178
|
-
if process.returncode == 0:
|
|
179
|
-
logger.info("INCR backup completed successfully.")
|
|
180
|
-
elif process.returncode == 5:
|
|
181
|
-
logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
|
|
182
|
-
else:
|
|
183
|
-
logger.error("dar return code: ", process.returncode)
|
|
184
|
-
logger.error("stdout: ", stdout)
|
|
185
|
-
logger.error("stderr: ", stderr)
|
|
186
|
-
raise Exception(stderr)
|
|
187
|
-
except subprocess.CalledProcessError as e:
|
|
188
|
-
logger.error(f"Incremental backup command failed: {e}")
|
|
189
|
-
raise IncrementalBackupError(f"Incremental backup command failed: {e}") from e
|
|
190
|
-
except Exception as e:
|
|
191
|
-
logger.exception(f"Unexpected error during incremental backup")
|
|
192
|
-
logger.error("Exception details:", exc_info=True)
|
|
193
|
-
raise IncrementalBackupError(f"Unexpected error during incremental backup: {e}") from e
|
|
194
|
-
|
|
195
74
|
|
|
196
75
|
# Function to recursively find <File> tags and build their full paths
|
|
197
76
|
def find_files_with_paths(element: ET, current_path=""):
|
|
@@ -286,11 +165,10 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
286
165
|
command = ['dar', '-t', backup_file, '-Q']
|
|
287
166
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
288
167
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
289
|
-
stdout, stderr = process.stdout, process.stderr
|
|
290
168
|
if process.returncode == 0:
|
|
291
169
|
logger.info("Archive integrity test passed.")
|
|
292
170
|
else:
|
|
293
|
-
raise Exception(
|
|
171
|
+
raise Exception(str(process))
|
|
294
172
|
|
|
295
173
|
if args.do_not_compare:
|
|
296
174
|
return result
|
|
@@ -325,10 +203,8 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
325
203
|
command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-O', '-Q']
|
|
326
204
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
327
205
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
328
|
-
stdout, stderr = process.stdout, process.stderr
|
|
329
206
|
if process.returncode != 0:
|
|
330
|
-
|
|
331
|
-
raise Exception(stderr)
|
|
207
|
+
raise Exception(str(process))
|
|
332
208
|
|
|
333
209
|
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
210
|
logger.info(f"Success: file '{restored_file_path}' matches the original")
|
|
@@ -348,7 +224,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
|
|
|
348
224
|
Restores a backup file to a specified directory.
|
|
349
225
|
|
|
350
226
|
Args:
|
|
351
|
-
backup_name (str): The name of the backup file.
|
|
227
|
+
backup_name (str): The base name of the backup file, without the "slice number.dar"
|
|
352
228
|
backup_dir (str): The directory where the backup file is located.
|
|
353
229
|
restore_dir (str): The directory where the backup should be restored to.
|
|
354
230
|
selection (str, optional): A selection criteria to restore specific files or directories. Defaults to None.
|
|
@@ -359,23 +235,22 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
|
|
|
359
235
|
if not os.path.exists(restore_dir):
|
|
360
236
|
os.makedirs(restore_dir)
|
|
361
237
|
command.extend(['-R', restore_dir])
|
|
238
|
+
else:
|
|
239
|
+
raise RestoreError("Restore directory ('-R <dir>') not specified")
|
|
362
240
|
if selection:
|
|
363
241
|
selection_criteria = shlex.split(selection)
|
|
364
242
|
command.extend(selection_criteria)
|
|
365
|
-
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
243
|
+
logger.info(f"Running restore command: {' '.join(map(shlex.quote, command))}")
|
|
366
244
|
try:
|
|
367
245
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
368
|
-
|
|
246
|
+
if process.returncode == 0:
|
|
247
|
+
logger.info(f"Restore completed successfully to: '{restore_dir}'")
|
|
248
|
+
else:
|
|
249
|
+
logger.error(f"Restore command failed: \n ==> stdout: {process.stdout}, \n ==> stderr: {process.stderr}")
|
|
250
|
+
raise RestoreError(str(process))
|
|
369
251
|
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
252
|
raise RestoreError(f"Restore command failed: {e}") from e
|
|
374
253
|
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
254
|
raise RestoreError(f"Unexpected error during restore: {e}") from e
|
|
380
255
|
|
|
381
256
|
|
|
@@ -393,19 +268,24 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
|
|
|
393
268
|
"""
|
|
394
269
|
logger.debug(f"Getting backed up files from DAR archive in xml: '{backup_name}'")
|
|
395
270
|
backup_path = os.path.join(backup_dir, backup_name)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
271
|
+
try:
|
|
272
|
+
command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
|
|
273
|
+
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
274
|
+
process = run_command(command)
|
|
275
|
+
# Parse the XML data
|
|
276
|
+
root = ET.fromstring(process.stdout)
|
|
277
|
+
output = None # help gc
|
|
278
|
+
# Extract full paths and file size for all <File> elements
|
|
279
|
+
file_paths = find_files_with_paths(root)
|
|
280
|
+
root = None # help gc
|
|
281
|
+
logger.trace(str(process))
|
|
282
|
+
logger.trace(file_paths)
|
|
283
|
+
return file_paths
|
|
284
|
+
except subprocess.CalledProcessError as e:
|
|
285
|
+
logger.error(f"Error listing backed up files from DAR archive: '{backup_name}'")
|
|
286
|
+
raise BackupError(f"Error listing backed up files from DAR archive: '{backup_name}'") from e
|
|
287
|
+
except Exception as e:
|
|
288
|
+
raise RuntimeError(f"Unexpected error listing backed up files from DAR archive: '{backup_name}'") from e
|
|
409
289
|
|
|
410
290
|
|
|
411
291
|
def list_contents(backup_name, backup_dir, selection=None):
|
|
@@ -421,17 +301,53 @@ def list_contents(backup_name, backup_dir, selection=None):
|
|
|
421
301
|
None
|
|
422
302
|
"""
|
|
423
303
|
backup_path = os.path.join(backup_dir, backup_name)
|
|
424
|
-
command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
|
|
425
|
-
if selection:
|
|
426
|
-
selection_criteria = shlex.split(selection)
|
|
427
|
-
command.extend(selection_criteria)
|
|
428
|
-
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
429
|
-
process = run_command(command)
|
|
430
|
-
stdout,stderr = process.stdout, process.stderr
|
|
431
304
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
305
|
+
try:
|
|
306
|
+
command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
|
|
307
|
+
if selection:
|
|
308
|
+
selection_criteria = shlex.split(selection)
|
|
309
|
+
command.extend(selection_criteria)
|
|
310
|
+
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
311
|
+
process = run_command(command)
|
|
312
|
+
stdout,stderr = process.stdout, process.stderr
|
|
313
|
+
if process.returncode != 0:
|
|
314
|
+
logger.error(f"Error listing contents of backup: '{backup_name}'")
|
|
315
|
+
raise subprocess.CalledProcessError(str(process))
|
|
316
|
+
for line in stdout.splitlines():
|
|
317
|
+
if "[--- REMOVED ENTRY ----]" in line or "[Saved]" in line:
|
|
318
|
+
print(line)
|
|
319
|
+
except subprocess.CalledProcessError as e:
|
|
320
|
+
logger.error(f"Error listing contents of backup: '{backup_name}'")
|
|
321
|
+
raise BackupError(f"Error listing contents of backup: '{backup_name}'") from e
|
|
322
|
+
except Exception as e:
|
|
323
|
+
raise RuntimeError(f"Unexpected error listing contents of backup: '{backup_name}'") from e
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def create_backup_command(backup_type: str, backup_file: str, darrc: str, backup_definition_path: str, latest_base_backup: str = None) -> List[str]:
|
|
329
|
+
"""
|
|
330
|
+
Generate the backup command for the specified backup type.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
backup_type (str): The type of backup (FULL, DIFF, INCR).
|
|
334
|
+
backup_file (str): The backup file path. Example: /path/to/example_2021-01-01_FULL
|
|
335
|
+
darrc (str): Path to the .darrc configuration file.
|
|
336
|
+
backup_definition_path (str): Path to the backup definition file.
|
|
337
|
+
latest_base_backup (str, optional): Path to the latest base backup for DIFF or INCR types.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
List[str]: The constructed backup command.
|
|
341
|
+
"""
|
|
342
|
+
base_command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition_path, '-Q', "compress-exclusion", "verbose"]
|
|
343
|
+
|
|
344
|
+
if backup_type in ['DIFF', 'INCR']:
|
|
345
|
+
if not latest_base_backup:
|
|
346
|
+
raise ValueError(f"Base backup is required for {backup_type} backups.")
|
|
347
|
+
base_command.extend(['-A', latest_base_backup])
|
|
348
|
+
|
|
349
|
+
return base_command
|
|
350
|
+
|
|
435
351
|
|
|
436
352
|
|
|
437
353
|
def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, backup_type: str):
|
|
@@ -441,32 +357,12 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
441
357
|
Args:
|
|
442
358
|
args: Command-line arguments.
|
|
443
359
|
config_settings: An instance of the ConfigSettings class.
|
|
444
|
-
backup_d: Directory containing backup definitions.
|
|
445
|
-
backup_dir: Directory to store backup files.
|
|
446
|
-
test_restore_dir: Directory to test restore backup files.
|
|
447
360
|
backup_type: Type of backup (FULL, DIFF, INCR).
|
|
448
|
-
min_size_verification_mb: Minimum size for verification in MB.
|
|
449
|
-
max_size_verification_mb: Maximum size for verification in MB.
|
|
450
|
-
no_files_verification: Flag indicating whether to skip file verification.
|
|
451
|
-
|
|
452
|
-
Returns:
|
|
453
|
-
None
|
|
454
|
-
|
|
455
|
-
Raises:
|
|
456
|
-
FileNotFoundError: If `backup_d` does not exist or a specified backup definition file does not exist.
|
|
457
|
-
PermissionError: If there is insufficient permission to access directories or files specified.
|
|
458
|
-
OSError: For various system-related errors, such as exhaustion of file descriptors.
|
|
459
|
-
ValueError: If there is an issue with the format string in `datetime.now().strftime`.
|
|
460
|
-
subprocess.CalledProcessError: If a subprocess invoked during the backup process exits with a non-zero status.
|
|
461
|
-
Exception: Catches any unexpected exceptions that may occur during the backup process.
|
|
462
|
-
|
|
463
|
-
Note:
|
|
464
|
-
This function assumes that any exceptions raised by the `backup` function or related subprocesses are handled
|
|
465
|
-
within those functions or propagated up to be handled by the caller of `perform_backup`.
|
|
466
361
|
"""
|
|
467
362
|
logger.debug(f"perform_backup({backup_type}) started")
|
|
468
363
|
backup_definitions = []
|
|
469
364
|
|
|
365
|
+
# Gather backup definitions
|
|
470
366
|
if args.backup_definition:
|
|
471
367
|
if '_' in args.backup_definition:
|
|
472
368
|
logger.error(f"Skipping backup definition: '{args.backup_definition}' due to '_' in name")
|
|
@@ -489,16 +385,15 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
489
385
|
logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
|
|
490
386
|
continue
|
|
491
387
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
else:
|
|
388
|
+
latest_base_backup = None
|
|
389
|
+
if backup_type in ['DIFF', 'INCR']:
|
|
495
390
|
base_backup_type = 'FULL' if backup_type == 'DIFF' else 'DIFF'
|
|
496
|
-
|
|
391
|
+
|
|
497
392
|
if args.alternate_reference_archive:
|
|
498
|
-
latest_base_backup = os.path.join(config_settings.backup_dir, args.alternate_reference_archive)
|
|
393
|
+
latest_base_backup = os.path.join(config_settings.backup_dir, args.alternate_reference_archive)
|
|
499
394
|
logger.info(f"Using alternate reference archive: {latest_base_backup}")
|
|
500
395
|
if not os.path.exists(latest_base_backup + '.1.dar'):
|
|
501
|
-
logger.error(f"Alternate reference archive: \"{latest_base_backup}.1.dar\" does not exist,
|
|
396
|
+
logger.error(f"Alternate reference archive: \"{latest_base_backup}.1.dar\" does not exist, exiting.")
|
|
502
397
|
sys.exit(1)
|
|
503
398
|
else:
|
|
504
399
|
base_backups = sorted(
|
|
@@ -510,33 +405,36 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
510
405
|
continue
|
|
511
406
|
latest_base_backup = os.path.join(config_settings.backup_dir, base_backups[-1].rsplit('.', 2)[0])
|
|
512
407
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
408
|
+
# Generate the backup command
|
|
409
|
+
command = create_backup_command(backup_type, backup_file, args.darrc, backup_definition_path, latest_base_backup)
|
|
410
|
+
|
|
411
|
+
# Perform backup
|
|
412
|
+
generic_backup(backup_type, command, backup_file, backup_definition_path, args.darrc, config_settings)
|
|
517
413
|
|
|
518
414
|
logger.info("Starting verification...")
|
|
519
415
|
result = verify(args, backup_file, backup_definition_path, config_settings)
|
|
520
416
|
if result:
|
|
521
417
|
logger.info("Verification completed successfully.")
|
|
522
418
|
else:
|
|
523
|
-
logger.error("Verification failed.")
|
|
419
|
+
logger.error("Verification failed.")
|
|
420
|
+
|
|
524
421
|
if config_settings.par2_enabled:
|
|
525
|
-
logger.info("Generate par2 redundancy files")
|
|
526
|
-
generate_par2_files(backup_file, config_settings
|
|
422
|
+
logger.info("Generate par2 redundancy files.")
|
|
423
|
+
generate_par2_files(backup_file, config_settings, args)
|
|
527
424
|
logger.info("par2 files completed successfully.")
|
|
528
|
-
# we want to continue with other backup definitions, thus only logging an error
|
|
529
425
|
except Exception as e:
|
|
530
|
-
logger.exception(f"Error during {backup_type} backup process, continuing
|
|
531
|
-
|
|
426
|
+
logger.exception(f"Error during {backup_type} backup process, continuing to next backup definition.")
|
|
427
|
+
|
|
428
|
+
|
|
532
429
|
|
|
533
|
-
def generate_par2_files(backup_file: str, config_settings: ConfigSettings):
|
|
430
|
+
def generate_par2_files(backup_file: str, config_settings: ConfigSettings, args):
|
|
534
431
|
"""
|
|
535
432
|
Generate PAR2 files for a given backup file in the specified backup directory.
|
|
536
433
|
|
|
537
434
|
Args:
|
|
538
435
|
backup_file (str): The name of the backup file.
|
|
539
|
-
|
|
436
|
+
config_settings: The configuration settings object.
|
|
437
|
+
args: The command-line arguments object.
|
|
540
438
|
|
|
541
439
|
Raises:
|
|
542
440
|
subprocess.CalledProcessError: If the par2 command fails to execute.
|
|
@@ -544,19 +442,39 @@ def generate_par2_files(backup_file: str, config_settings: ConfigSettings):
|
|
|
544
442
|
Returns:
|
|
545
443
|
None
|
|
546
444
|
"""
|
|
445
|
+
# Regular expression to match DAR slice files
|
|
446
|
+
dar_slice_pattern = re.compile(rf"{re.escape(os.path.basename(backup_file))}\.([0-9]+)\.dar")
|
|
447
|
+
|
|
448
|
+
# List of DAR slice files to be processed
|
|
449
|
+
dar_slices: List[str] = []
|
|
450
|
+
|
|
547
451
|
for filename in os.listdir(config_settings.backup_dir):
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
raise subprocess.CalledProcessError(process.returncode, command)
|
|
557
|
-
logger.debug(f"par2 files generated for {file_path}")
|
|
452
|
+
match = dar_slice_pattern.match(filename)
|
|
453
|
+
if match:
|
|
454
|
+
dar_slices.append(filename)
|
|
455
|
+
|
|
456
|
+
# Sort the DAR slices based on the slice number
|
|
457
|
+
dar_slices.sort(key=lambda x: int(dar_slice_pattern.match(x).group(1)))
|
|
458
|
+
number_of_slices = len(dar_slices)
|
|
459
|
+
counter = 1
|
|
558
460
|
|
|
461
|
+
for slice_file in dar_slices:
|
|
462
|
+
file_path = os.path.join(config_settings.backup_dir, slice_file)
|
|
463
|
+
|
|
464
|
+
if args.verbose or args.log_level == "debug" or args.log_level == "trace":
|
|
465
|
+
logger.info(f"{counter}/{number_of_slices}: Now generating par2 files for {file_path}")
|
|
466
|
+
|
|
467
|
+
# Run the par2 command to generate redundancy files with error correction
|
|
468
|
+
command = ['par2', 'create', f'-r{config_settings.error_correction_percent}', '-q', '-q', file_path]
|
|
469
|
+
process = run_command(command, config_settings.command_timeout_secs)
|
|
559
470
|
|
|
471
|
+
if process.returncode == 0:
|
|
472
|
+
if args.verbose or args.log_level == "debug" or args.log_level == "trace":
|
|
473
|
+
logger.info(f"{counter}/{number_of_slices}: Done")
|
|
474
|
+
else:
|
|
475
|
+
logger.error(f"Error generating par2 files for {file_path}")
|
|
476
|
+
raise subprocess.CalledProcessError(process.returncode, command)
|
|
477
|
+
counter += 1
|
|
560
478
|
|
|
561
479
|
|
|
562
480
|
|
|
@@ -634,7 +552,7 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
634
552
|
for key in sorted(config_setting.config[type].keys()):
|
|
635
553
|
script = config_setting.config[type][key]
|
|
636
554
|
try:
|
|
637
|
-
result = subprocess.run(script, shell=True, check=True)
|
|
555
|
+
result = subprocess.run(script, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=True)
|
|
638
556
|
logger.info(f"{type} {key}: '{script}' run, return code: {result.returncode}")
|
|
639
557
|
logger.info(f"{type} stdout:\n{result.stdout}")
|
|
640
558
|
if result.returncode != 0:
|
|
@@ -642,9 +560,7 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
642
560
|
raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {result.returncode}")
|
|
643
561
|
except subprocess.CalledProcessError as e:
|
|
644
562
|
logger.error(f"Error executing {key}: '{script}': {e}")
|
|
645
|
-
|
|
646
|
-
logger.error(f"{type} stderr:\n{result.stderr}")
|
|
647
|
-
raise e
|
|
563
|
+
raise e
|
|
648
564
|
|
|
649
565
|
|
|
650
566
|
def main():
|
|
@@ -667,8 +583,9 @@ def main():
|
|
|
667
583
|
parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
|
|
668
584
|
parser.add_argument('--list-contents', help="List the contents of the specified archive.")
|
|
669
585
|
parser.add_argument('--selection', help="dar file selection for listing/restoring specific files/directories.")
|
|
670
|
-
parser.add_argument('-r', '--restore', nargs=1, type=str, help="Restore specified archive.")
|
|
671
|
-
parser.add_argument('
|
|
586
|
+
# parser.add_argument('-r', '--restore', nargs=1, type=str, help="Restore specified archive.")
|
|
587
|
+
parser.add_argument('-r', '--restore', type=str, help="Restore specified archive.")
|
|
588
|
+
parser.add_argument('--restore-dir', type=str, help="Directory to restore files to.")
|
|
672
589
|
parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
|
|
673
590
|
parser.add_argument('--log-level', type=str, help="`debug` or `trace`", default="info")
|
|
674
591
|
parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
|
|
@@ -717,7 +634,10 @@ def main():
|
|
|
717
634
|
args.verbose and (print(f"Alternate ref archive: {args.alternate_reference_archive}"))
|
|
718
635
|
args.verbose and (print(f"Backup.d dir: {config_settings.backup_d_dir}"))
|
|
719
636
|
args.verbose and (print(f"Backup dir: {config_settings.backup_dir}"))
|
|
720
|
-
|
|
637
|
+
|
|
638
|
+
restore_dir = args.restore_dir if args.restore_dir else config_settings.test_restore_dir
|
|
639
|
+
args.verbose and (print(f"Restore dir: {restore_dir}"))
|
|
640
|
+
|
|
721
641
|
args.verbose and (print(f"Logfile location: {config_settings.logfile_location}"))
|
|
722
642
|
args.verbose and (print(f".darrc location: {args.darrc}"))
|
|
723
643
|
args.verbose and (print(f"PAR2 enabled: {config_settings.par2_enabled}"))
|
|
@@ -744,33 +664,27 @@ def main():
|
|
|
744
664
|
elif args.incremental_backup and not args.full_backup and not args.differential_backup:
|
|
745
665
|
perform_backup(args, config_settings, "INCR")
|
|
746
666
|
elif args.list_contents:
|
|
747
|
-
print(f"Listing contents of {args.list_contents}")
|
|
748
667
|
list_contents(args.list_contents, config_settings.backup_dir, args.selection)
|
|
749
668
|
elif args.restore:
|
|
750
|
-
|
|
669
|
+
logger.debug(f"Restoring {args.restore} to {restore_dir}")
|
|
751
670
|
restore_backup(args.restore, config_settings, restore_dir, args.selection)
|
|
752
671
|
else:
|
|
753
672
|
parser.print_help()
|
|
754
673
|
|
|
755
674
|
requirements('POSTREQ', config_settings)
|
|
756
675
|
|
|
757
|
-
|
|
676
|
+
args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
|
|
677
|
+
sys.exit(0)
|
|
758
678
|
except Exception as e:
|
|
759
679
|
logger.exception("An error occurred")
|
|
760
680
|
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
681
|
args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
682
|
+
sys.exit(1)
|
|
683
|
+
finally:
|
|
684
|
+
end_time=int(time())
|
|
685
|
+
logger.info(f"END TIME: {end_time}")
|
|
686
|
+
|
|
687
|
+
# error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
|
|
774
688
|
|
|
775
689
|
|
|
776
690
|
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.2
|
|
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=d7NGuoje3vHyudKIFR_PmfKozIOKDFvAhGx0QXiyuMw,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=YZoVu9NJX3_WIkQIG8EMLSK3-VWdslI0c2XKrM2Un38,32214
|
|
7
|
+
dar_backup/manager.py,sha256=lkw1ZAIdxY7WedLPKZMnHpuq_QbjkUdcG61ooiwUYpo,10197
|
|
8
|
+
dar_backup/util.py,sha256=6lPCFHr3MDdaLWAW9EDMZ4jdL7pt8rki-5dOXcesmP8,8955
|
|
9
|
+
dar_backup-0.6.2.dist-info/METADATA,sha256=PMfP8NhPwzhHRbjJt6_7Unqxbqqj13PW19QdYac6pcc,22496
|
|
10
|
+
dar_backup-0.6.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
dar_backup-0.6.2.dist-info/entry_points.txt,sha256=x9vnW-JEl8mpDJC69f_XBcn0mBSkV1U0cyvFV-NAP1g,126
|
|
12
|
+
dar_backup-0.6.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
13
|
+
dar_backup-0.6.2.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
|