dar-backup 0.6.10__py3-none-any.whl → 0.6.12__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/clean_log.py +1 -1
- dar_backup/cleanup.py +1 -1
- dar_backup/config_settings.py +34 -11
- dar_backup/dar-backup.conf +33 -0
- dar_backup/dar_backup.py +65 -46
- dar_backup/installer.py +129 -0
- dar_backup/manager.py +1 -1
- dar_backup/util.py +118 -0
- {dar_backup-0.6.10.dist-info → dar_backup-0.6.12.dist-info}/METADATA +128 -116
- dar_backup-0.6.12.dist-info/RECORD +16 -0
- {dar_backup-0.6.10.dist-info → dar_backup-0.6.12.dist-info}/entry_points.txt +1 -0
- dar_backup-0.6.10.dist-info/RECORD +0 -14
- {dar_backup-0.6.10.dist-info → dar_backup-0.6.12.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.10.dist-info → dar_backup-0.6.12.dist-info}/licenses/LICENSE +0 -0
dar_backup/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.6.
|
|
1
|
+
__version__ = "0.6.12"
|
dar_backup/clean_log.py
CHANGED
|
@@ -122,7 +122,7 @@ def main():
|
|
|
122
122
|
|
|
123
123
|
args = parser.parse_args()
|
|
124
124
|
|
|
125
|
-
config_settings = ConfigSettings(os.path.expanduser(args.config_file))
|
|
125
|
+
config_settings = ConfigSettings(os.path.expanduser(os.path.expandvars(args.config_file)))
|
|
126
126
|
|
|
127
127
|
if not args.file:
|
|
128
128
|
args.file = [config_settings.logfile_location]
|
dar_backup/cleanup.py
CHANGED
|
@@ -171,7 +171,7 @@ def main():
|
|
|
171
171
|
parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
|
|
172
172
|
args = parser.parse_args()
|
|
173
173
|
|
|
174
|
-
args.config_file = os.path.expanduser(args.config_file)
|
|
174
|
+
args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
if args.version:
|
dar_backup/config_settings.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
import configparser
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
import sys
|
|
5
3
|
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field, fields
|
|
7
|
+
from os.path import expandvars, expanduser
|
|
8
|
+
from pathlib import Path
|
|
6
9
|
|
|
7
10
|
@dataclass
|
|
8
11
|
class ConfigSettings:
|
|
@@ -20,21 +23,35 @@ class ConfigSettings:
|
|
|
20
23
|
backup_d_dir (str): The directory for backup.d.
|
|
21
24
|
diff_age (int): The age for differential backups before deletion.
|
|
22
25
|
incr_age (int): The age for incremental backups before deletion.
|
|
26
|
+
error_correction_percent (int): The error correction percentage for PAR2.
|
|
27
|
+
par2_enabled (bool): Whether PAR2 is enabled.
|
|
23
28
|
"""
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
config_file: str
|
|
31
|
+
logfile_location: str = field(init=False)
|
|
32
|
+
max_size_verification_mb: int = field(init=False)
|
|
33
|
+
min_size_verification_mb: int = field(init=False)
|
|
34
|
+
no_files_verification: int = field(init=False)
|
|
35
|
+
command_timeout_secs: int = field(init=False)
|
|
36
|
+
backup_dir: str = field(init=False)
|
|
37
|
+
test_restore_dir: str = field(init=False)
|
|
38
|
+
backup_d_dir: str = field(init=False)
|
|
39
|
+
diff_age: int = field(init=False)
|
|
40
|
+
incr_age: int = field(init=False)
|
|
41
|
+
error_correction_percent: int = field(init=False)
|
|
42
|
+
par2_enabled: bool = field(init=False)
|
|
43
|
+
|
|
44
|
+
def __post_init__(self):
|
|
26
45
|
"""
|
|
27
|
-
Initializes the ConfigSettings instance by reading the specified configuration file
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
config_file (str): The path to the configuration file.
|
|
46
|
+
Initializes the ConfigSettings instance by reading the specified configuration file
|
|
47
|
+
and expands environment variables for all string fields.
|
|
31
48
|
"""
|
|
32
|
-
if config_file is None:
|
|
49
|
+
if self.config_file is None:
|
|
33
50
|
raise ValueError("`config_file` must be specified.")
|
|
34
51
|
|
|
35
52
|
self.config = configparser.ConfigParser()
|
|
36
53
|
try:
|
|
37
|
-
self.config.read(config_file)
|
|
54
|
+
self.config.read(self.config_file)
|
|
38
55
|
self.logfile_location = self.config['MISC']['LOGFILE_LOCATION']
|
|
39
56
|
self.max_size_verification_mb = int(self.config['MISC']['MAX_SIZE_VERIFICATION_MB'])
|
|
40
57
|
self.min_size_verification_mb = int(self.config['MISC']['MIN_SIZE_VERIFICATION_MB'])
|
|
@@ -46,12 +63,18 @@ class ConfigSettings:
|
|
|
46
63
|
self.diff_age = int(self.config['AGE']['DIFF_AGE'])
|
|
47
64
|
self.incr_age = int(self.config['AGE']['INCR_AGE'])
|
|
48
65
|
self.error_correction_percent = int(self.config['PAR2']['ERROR_CORRECTION_PERCENT'])
|
|
49
|
-
self.par2_enabled =
|
|
66
|
+
self.par2_enabled = self.config['PAR2']['ENABLED'].lower() in ('true', '1', 'yes')
|
|
67
|
+
|
|
50
68
|
# Ensure the directories exist
|
|
51
69
|
Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
|
|
52
70
|
Path(self.test_restore_dir).mkdir(parents=True, exist_ok=True)
|
|
53
71
|
Path(self.backup_d_dir).mkdir(parents=True, exist_ok=True)
|
|
54
72
|
|
|
73
|
+
# Expand environment variables for all string fields
|
|
74
|
+
for field in fields(self):
|
|
75
|
+
if isinstance(getattr(self, field.name), str):
|
|
76
|
+
setattr(self, field.name, expanduser(expandvars(getattr(self, field.name))))
|
|
77
|
+
|
|
55
78
|
except FileNotFoundError as e:
|
|
56
79
|
logging.error(f"Configuration file not found: {self.config_file}")
|
|
57
80
|
logging.error(f"Error details: {e}")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# This config file is intended to demo `dar-backup`.
|
|
2
|
+
#
|
|
3
|
+
# The `installer` puts it in ~/.config/dar-backup/dar-backup.conf
|
|
4
|
+
|
|
5
|
+
[MISC]
|
|
6
|
+
LOGFILE_LOCATION = ~/dar-backup/dar-backup.log
|
|
7
|
+
MAX_SIZE_VERIFICATION_MB = 20
|
|
8
|
+
MIN_SIZE_VERIFICATION_MB = 1
|
|
9
|
+
NO_FILES_VERIFICATION = 5
|
|
10
|
+
# timeout in seconds for backup, test, restore and par2 operations
|
|
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.
|
|
13
|
+
COMMAND_TIMEOUT_SECS = 86400
|
|
14
|
+
|
|
15
|
+
[DIRECTORIES]
|
|
16
|
+
BACKUP_DIR = ~/dar-backup/backups
|
|
17
|
+
BACKUP.D_DIR = ~/.config/dar-backup/backup.d/
|
|
18
|
+
TEST_RESTORE_DIR = ~/dar-backup/restore/
|
|
19
|
+
|
|
20
|
+
[AGE]
|
|
21
|
+
# age settings are in days
|
|
22
|
+
DIFF_AGE = 100
|
|
23
|
+
INCR_AGE = 40
|
|
24
|
+
|
|
25
|
+
[PAR2]
|
|
26
|
+
ERROR_CORRECTION_PERCENT = 5
|
|
27
|
+
ENABLED = True
|
|
28
|
+
|
|
29
|
+
[PREREQ]
|
|
30
|
+
#SCRIPT_1 = <pre-script 1>
|
|
31
|
+
|
|
32
|
+
[POSTREQ]
|
|
33
|
+
#SCRIPT_1 = <post-script 1>
|
dar_backup/dar_backup.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import datetime
|
|
5
4
|
import filecmp
|
|
6
5
|
|
|
7
6
|
import os
|
|
@@ -9,13 +8,16 @@ import random
|
|
|
9
8
|
import re
|
|
10
9
|
import shlex
|
|
11
10
|
import subprocess
|
|
12
|
-
import sys
|
|
13
11
|
import xml.etree.ElementTree as ET
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
from argparse import ArgumentParser
|
|
17
15
|
from datetime import datetime
|
|
18
16
|
from pathlib import Path
|
|
17
|
+
from sys import exit
|
|
18
|
+
from sys import stderr
|
|
19
|
+
from sys import argv
|
|
20
|
+
from sys import version_info
|
|
19
21
|
from time import time
|
|
20
22
|
from typing import List
|
|
21
23
|
|
|
@@ -81,30 +83,31 @@ def generic_backup(type: str, command: List[str], backup_file: str, backup_defin
|
|
|
81
83
|
raise BackupError(f"Unexpected error during backup: {e}") from e
|
|
82
84
|
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
def find_files_with_paths(
|
|
86
|
+
|
|
87
|
+
def find_files_with_paths(xml_root: ET.Element):
|
|
86
88
|
"""
|
|
87
|
-
|
|
89
|
+
Finds files within an XML element and returns a list of file paths with their sizes.
|
|
88
90
|
|
|
89
91
|
Args:
|
|
90
|
-
|
|
91
|
-
current_path (str, optional): The current path of the directory element. Defaults to "".
|
|
92
|
+
xml_root (Element): The root element of the XML.
|
|
92
93
|
|
|
93
94
|
Returns:
|
|
94
95
|
list: A list of tuples containing file paths and their sizes.
|
|
95
96
|
"""
|
|
96
|
-
logger.debug(
|
|
97
|
+
logger.debug("Generating list of tuples with file paths and sizes for File elements in dar xml output")
|
|
97
98
|
files = []
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for
|
|
101
|
-
if
|
|
102
|
-
|
|
99
|
+
current_path = []
|
|
100
|
+
|
|
101
|
+
for elem in xml_root.iter():
|
|
102
|
+
if elem.tag == "directory":
|
|
103
|
+
current_path.append(elem.get('name'))
|
|
104
|
+
elif elem.tag == "file":
|
|
105
|
+
file_path = ("/".join(current_path + [elem.get('name')]), elem.get('size'))
|
|
103
106
|
files.append(file_path)
|
|
104
|
-
elif
|
|
105
|
-
|
|
106
|
-
return files
|
|
107
|
+
elif elem.tag == "directory" and elem.get('name') in current_path:
|
|
108
|
+
current_path.pop()
|
|
107
109
|
|
|
110
|
+
return files
|
|
108
111
|
|
|
109
112
|
|
|
110
113
|
def find_files_between_min_and_max_size(backed_up_files: list[(str, str)], config_settings: ConfigSettings):
|
|
@@ -238,20 +241,20 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
|
|
|
238
241
|
restore_dir (str): The directory where the backup should be restored to.
|
|
239
242
|
selection (str, optional): A selection criteria to restore specific files or directories. Defaults to None.
|
|
240
243
|
"""
|
|
241
|
-
backup_file = os.path.join(config_settings.backup_dir, backup_name)
|
|
242
|
-
command = ['dar', '-x', backup_file, '-Q', '-D']
|
|
243
|
-
if restore_dir:
|
|
244
|
-
if not os.path.exists(restore_dir):
|
|
245
|
-
os.makedirs(restore_dir)
|
|
246
|
-
command.extend(['-R', restore_dir])
|
|
247
|
-
else:
|
|
248
|
-
raise RestoreError("Restore directory ('-R <dir>') not specified")
|
|
249
|
-
if selection:
|
|
250
|
-
selection_criteria = shlex.split(selection)
|
|
251
|
-
command.extend(selection_criteria)
|
|
252
|
-
command.extend(['-B', darrc, 'restore-options']) # the .darrc `restore-options` section
|
|
253
|
-
logger.info(f"Running restore command: {' '.join(map(shlex.quote, command))}")
|
|
254
244
|
try:
|
|
245
|
+
backup_file = os.path.join(config_settings.backup_dir, backup_name)
|
|
246
|
+
command = ['dar', '-x', backup_file, '-Q', '-D']
|
|
247
|
+
if restore_dir:
|
|
248
|
+
if not os.path.exists(restore_dir):
|
|
249
|
+
os.makedirs(restore_dir)
|
|
250
|
+
command.extend(['-R', restore_dir])
|
|
251
|
+
else:
|
|
252
|
+
raise RestoreError("Restore directory ('-R <dir>') not specified")
|
|
253
|
+
if selection:
|
|
254
|
+
selection_criteria = shlex.split(selection)
|
|
255
|
+
command.extend(selection_criteria)
|
|
256
|
+
command.extend(['-B', darrc, 'restore-options']) # the .darrc `restore-options` section
|
|
257
|
+
logger.info(f"Running restore command: {' '.join(map(shlex.quote, command))}")
|
|
255
258
|
process = run_command(command, config_settings.command_timeout_secs)
|
|
256
259
|
if process.returncode == 0:
|
|
257
260
|
logger.info(f"Restore completed successfully to: '{restore_dir}'")
|
|
@@ -260,6 +263,9 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
|
|
|
260
263
|
raise RestoreError(str(process))
|
|
261
264
|
except subprocess.CalledProcessError as e:
|
|
262
265
|
raise RestoreError(f"Restore command failed: {e}") from e
|
|
266
|
+
except OSError as e:
|
|
267
|
+
logger.error(f"Failed to create restore directory: {e}")
|
|
268
|
+
raise RestoreError("Could not create restore directory")
|
|
263
269
|
except Exception as e:
|
|
264
270
|
raise RestoreError(f"Unexpected error during restore: {e}") from e
|
|
265
271
|
|
|
@@ -285,7 +291,7 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
|
|
|
285
291
|
# Parse the XML data
|
|
286
292
|
root = ET.fromstring(process.stdout)
|
|
287
293
|
output = None # help gc
|
|
288
|
-
# Extract full paths and file size for all <
|
|
294
|
+
# Extract full paths and file size for all <file> elements
|
|
289
295
|
file_paths = find_files_with_paths(root)
|
|
290
296
|
root = None # help gc
|
|
291
297
|
logger.trace(str(process))
|
|
@@ -404,7 +410,7 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
404
410
|
logger.info(f"Using alternate reference archive: {latest_base_backup}")
|
|
405
411
|
if not os.path.exists(latest_base_backup + '.1.dar'):
|
|
406
412
|
logger.error(f"Alternate reference archive: \"{latest_base_backup}.1.dar\" does not exist, exiting.")
|
|
407
|
-
|
|
413
|
+
exit(1)
|
|
408
414
|
else:
|
|
409
415
|
base_backups = sorted(
|
|
410
416
|
[f for f in os.listdir(config_settings.backup_dir) if f.startswith(f"{backup_definition}_{base_backup_type}_") and f.endswith('.1.dar')],
|
|
@@ -489,7 +495,7 @@ def generate_par2_files(backup_file: str, config_settings: ConfigSettings, args)
|
|
|
489
495
|
|
|
490
496
|
|
|
491
497
|
def show_version():
|
|
492
|
-
script_name = os.path.basename(
|
|
498
|
+
script_name = os.path.basename(argv[0])
|
|
493
499
|
print(f"{script_name} {about.__version__}")
|
|
494
500
|
print(f"dar-backup.py source code is here: https://github.com/per2jensen/dar-backup")
|
|
495
501
|
print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
@@ -577,9 +583,9 @@ def main():
|
|
|
577
583
|
global logger
|
|
578
584
|
|
|
579
585
|
MIN_PYTHON_VERSION = (3, 9)
|
|
580
|
-
if
|
|
581
|
-
|
|
582
|
-
|
|
586
|
+
if version_info < MIN_PYTHON_VERSION:
|
|
587
|
+
stderr.write(f"Error: This script requires Python {'.'.join(map(str, MIN_PYTHON_VERSION))} or higher.\n")
|
|
588
|
+
exit(1)
|
|
583
589
|
|
|
584
590
|
parser = argparse.ArgumentParser(description="Backup and verify using dar backup definitions.")
|
|
585
591
|
parser.add_argument('-F', '--full-backup', action='store_true', help="Perform a full backup.")
|
|
@@ -603,15 +609,25 @@ def main():
|
|
|
603
609
|
parser.add_argument('-v', '--version', action='store_true', help="Show version and license information.")
|
|
604
610
|
args = parser.parse_args()
|
|
605
611
|
|
|
606
|
-
args.config_file = os.path.expanduser(args.config_file)
|
|
607
|
-
config_settings = ConfigSettings(args.config_file)
|
|
608
|
-
|
|
609
612
|
if args.version:
|
|
610
613
|
show_version()
|
|
611
|
-
|
|
614
|
+
exit(0)
|
|
612
615
|
elif args.examples:
|
|
613
616
|
show_examples()
|
|
614
|
-
|
|
617
|
+
exit(0)
|
|
618
|
+
|
|
619
|
+
if not args.config_file:
|
|
620
|
+
print(f"Config file not specified, exiting", file=stderr)
|
|
621
|
+
exit(1)
|
|
622
|
+
|
|
623
|
+
config_settings_path = os.path.expanduser(os.path.expandvars(args.config_file))
|
|
624
|
+
if not os.path.exists(config_settings_path):
|
|
625
|
+
print(f"Config file {args.config_file} does not exist.", file=stderr)
|
|
626
|
+
exit(127)
|
|
627
|
+
|
|
628
|
+
args.config_file = config_settings_path
|
|
629
|
+
config_settings = ConfigSettings(args.config_file)
|
|
630
|
+
|
|
615
631
|
|
|
616
632
|
logger = setup_logging(config_settings.logfile_location, args.log_level, args.log_stdout)
|
|
617
633
|
|
|
@@ -619,10 +635,13 @@ def main():
|
|
|
619
635
|
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
620
636
|
args.darrc = os.path.join(current_script_dir, ".darrc")
|
|
621
637
|
|
|
622
|
-
|
|
638
|
+
darrc_file = os.path.expanduser(os.path.expandvars(args.darrc))
|
|
639
|
+
if os.path.exists(darrc_file) and os.path.isfile(darrc_file):
|
|
623
640
|
logger.debug(f"Using .darrc: {args.darrc}")
|
|
624
641
|
else:
|
|
625
|
-
logger.error(f"Supplied .darrc: '{args.darrc}' does not exist or is not a file")
|
|
642
|
+
logger.error(f"Supplied .darrc: '{args.darrc}' does not exist or is not a file, exiting", file=stderr)
|
|
643
|
+
exit(127)
|
|
644
|
+
|
|
626
645
|
|
|
627
646
|
|
|
628
647
|
try:
|
|
@@ -657,10 +676,10 @@ def main():
|
|
|
657
676
|
# sanity check
|
|
658
677
|
if args.backup_definition and not os.path.exists(os.path.join(config_settings.backup_d_dir, args.backup_definition)):
|
|
659
678
|
logger.error(f"Backup definition: '{args.backup_definition}' does not exist, exiting")
|
|
660
|
-
|
|
679
|
+
exit(127)
|
|
661
680
|
if args.backup_definition and '_' in args.backup_definition:
|
|
662
681
|
logger.error(f"Backup definition: '{args.backup_definition}' contains '_', exiting")
|
|
663
|
-
|
|
682
|
+
exit(1)
|
|
664
683
|
|
|
665
684
|
|
|
666
685
|
requirements('PREREQ', config_settings)
|
|
@@ -684,12 +703,12 @@ def main():
|
|
|
684
703
|
requirements('POSTREQ', config_settings)
|
|
685
704
|
|
|
686
705
|
args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
|
|
687
|
-
|
|
706
|
+
exit(0)
|
|
688
707
|
except Exception as e:
|
|
689
708
|
logger.exception("An error occurred")
|
|
690
709
|
logger.error("Exception details:", exc_info=True)
|
|
691
710
|
args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
|
|
692
|
-
|
|
711
|
+
exit(1)
|
|
693
712
|
finally:
|
|
694
713
|
end_time=int(time())
|
|
695
714
|
logger.info(f"END TIME: {end_time}")
|
dar_backup/installer.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
installer.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/installer.py
|
|
4
|
+
This script is part of dar-backup, a backup solution for Linux using dar and systemd.
|
|
5
|
+
|
|
6
|
+
Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
7
|
+
|
|
8
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW,
|
|
9
|
+
not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
10
|
+
See section 15 and section 16 in the supplied "LICENSE" file
|
|
11
|
+
|
|
12
|
+
This script can be used to configure dar-backup on your system.
|
|
13
|
+
It is non-destructive and will not overwrite any existing files or directories.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from . import __about__ as about
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
LICENSE = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
25
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
26
|
+
See section 15 and section 16 in the supplied "LICENSE" file.'''
|
|
27
|
+
|
|
28
|
+
CONFIG_DIR = os.path.expanduser("~/.config/dar-backup")
|
|
29
|
+
DAR_BACKUP_DIR = os.path.expanduser("~/dar-backup/")
|
|
30
|
+
|
|
31
|
+
BACKUP_DEFINITION = '''
|
|
32
|
+
# Demo of a `dar-backup` definition file
|
|
33
|
+
# This back definition file configures a backup of ~/.config/dar-backup
|
|
34
|
+
# `dar-backup` puts the backups in ~/dar-backup/backups
|
|
35
|
+
# ------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
# Switch to ordered selection mode, which means that the following options
|
|
38
|
+
# will be considered top to bottom
|
|
39
|
+
-am
|
|
40
|
+
|
|
41
|
+
# Backup Root dir
|
|
42
|
+
-R @@HOME_DIR@@
|
|
43
|
+
|
|
44
|
+
# Directories to backup below the Root dir
|
|
45
|
+
-g .config/dar-backup
|
|
46
|
+
|
|
47
|
+
# Examples of directories to exclude below the Root dir
|
|
48
|
+
-P mnt
|
|
49
|
+
-P .private
|
|
50
|
+
-P .cache
|
|
51
|
+
|
|
52
|
+
# compression level
|
|
53
|
+
-z5
|
|
54
|
+
|
|
55
|
+
# no overwrite, if you rerun a backup, 'dar' halts and asks what to do
|
|
56
|
+
-n
|
|
57
|
+
|
|
58
|
+
# size of each slice in the archive
|
|
59
|
+
--slice 10G
|
|
60
|
+
|
|
61
|
+
# bypass directores marked as cache directories
|
|
62
|
+
# http://dar.linux.free.fr/doc/Features.html
|
|
63
|
+
--cache-directory-tagging
|
|
64
|
+
'''
|
|
65
|
+
|
|
66
|
+
def main():
|
|
67
|
+
parser = argparse.ArgumentParser(
|
|
68
|
+
description="Set up `dar-backup` on your system.",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"-i", "--install",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Deploy a simple config file, use ~/dar-backup/ for log file, archives and restore tests."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--dry-run",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Show which lines would be removed without modifying the file."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"-v", "--version",
|
|
85
|
+
action="version",
|
|
86
|
+
version=f"%(prog)s version {about.__version__}, {LICENSE}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
args = parser.parse_args()
|
|
90
|
+
|
|
91
|
+
if args.install:
|
|
92
|
+
errors = []
|
|
93
|
+
if os.path.exists(CONFIG_DIR):
|
|
94
|
+
errors.append(f"Config directory '{CONFIG_DIR}' already exists.")
|
|
95
|
+
|
|
96
|
+
if os.path.exists(DAR_BACKUP_DIR):
|
|
97
|
+
errors.append(f"Directory '{DAR_BACKUP_DIR}' already exists.")
|
|
98
|
+
|
|
99
|
+
if len(errors) > 0:
|
|
100
|
+
for error in errors:
|
|
101
|
+
print(f"Error: {error}")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
os.makedirs(DAR_BACKUP_DIR, exist_ok=False)
|
|
105
|
+
os.makedirs(os.path.join(DAR_BACKUP_DIR, "backups"), exist_ok=False)
|
|
106
|
+
os.makedirs(os.path.join(DAR_BACKUP_DIR, "restore"), exist_ok=False)
|
|
107
|
+
os.makedirs(CONFIG_DIR, exist_ok=False)
|
|
108
|
+
os.makedirs(os.path.join(CONFIG_DIR, "backup.d"), exist_ok=False)
|
|
109
|
+
print(f"Directories created: `{DAR_BACKUP_DIR}` and `{CONFIG_DIR}`")
|
|
110
|
+
|
|
111
|
+
script_dir = Path(__file__).parent
|
|
112
|
+
source_file = script_dir / "dar-backup.conf"
|
|
113
|
+
destination_file = Path(CONFIG_DIR) / "dar-backup.conf"
|
|
114
|
+
shutil.copy(source_file, destination_file)
|
|
115
|
+
print(f"Config file deployed to {destination_file}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
backup_definition = BACKUP_DEFINITION.replace("@@HOME_DIR@@", os.path.expanduser("~"))
|
|
119
|
+
with open(os.path.join(CONFIG_DIR, "backup.d", "default"), "w") as f:
|
|
120
|
+
f.write(backup_definition)
|
|
121
|
+
print(f"Default backup definition file deployed to {os.path.join(CONFIG_DIR, 'backup.d', 'default')}")
|
|
122
|
+
print("1. Now run `manager --create` to create the catalog database.")
|
|
123
|
+
print("2. Then you can run `dar-backup --full-backup` to create a backup.")
|
|
124
|
+
print("3. List backups with `dar-backup --list`")
|
|
125
|
+
print("4. List contents of a backup with `dar-backup --list-contents <backup-name>`")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|
dar_backup/manager.py
CHANGED
|
@@ -395,7 +395,7 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
395
395
|
sys.exit(0)
|
|
396
396
|
|
|
397
397
|
# setup logging
|
|
398
|
-
args.config_file = os.path.expanduser(args.config_file)
|
|
398
|
+
args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
|
|
399
399
|
config_settings = ConfigSettings(args.config_file)
|
|
400
400
|
if not os.path.dirname(config_settings.logfile_location):
|
|
401
401
|
print(f"Directory for log file '{config_settings.logfile_location}' does not exist, exiting")
|
dar_backup/util.py
CHANGED
|
@@ -14,6 +14,7 @@ import os
|
|
|
14
14
|
import re
|
|
15
15
|
import subprocess
|
|
16
16
|
import shlex
|
|
17
|
+
import shutil
|
|
17
18
|
import sys
|
|
18
19
|
import threading
|
|
19
20
|
import traceback
|
|
@@ -123,10 +124,113 @@ def _stream_reader(pipe, log_func, output_accumulator: List[str]):
|
|
|
123
124
|
log_func(stripped_line) # Log the output in real time
|
|
124
125
|
|
|
125
126
|
|
|
127
|
+
|
|
126
128
|
def run_command(command: List[str], timeout: int = 30) -> CommandResult:
|
|
127
129
|
"""
|
|
128
130
|
Executes a given command via subprocess, logs its output in real time, and returns the result.
|
|
129
131
|
|
|
132
|
+
Args:
|
|
133
|
+
command (list): The command to be executed, represented as a list of strings.
|
|
134
|
+
timeout (int): The maximum time in seconds to wait for the command to complete. Defaults to 30 seconds.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
A CommandResult NamedTuple with the following properties:
|
|
138
|
+
- process: subprocess.CompletedProcess
|
|
139
|
+
- stdout: str: The full standard output of the command.
|
|
140
|
+
- stderr: str: The full standard error of the command.
|
|
141
|
+
- returncode: int: The return code of the command.
|
|
142
|
+
- timeout: int: The timeout value in seconds used to run the command.
|
|
143
|
+
- command: list[str]: The command executed.
|
|
144
|
+
|
|
145
|
+
Logs:
|
|
146
|
+
- Logs standard output (`stdout`) in real-time at the INFO log level.
|
|
147
|
+
- Logs standard error (`stderr`) in real-time at the ERROR log level.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
subprocess.TimeoutExpired: If the command execution times out (see `timeout` parameter).
|
|
151
|
+
Exception: If other exceptions occur during command execution.
|
|
152
|
+
FileNotFoundError: If the command is not found.
|
|
153
|
+
|
|
154
|
+
Notes:
|
|
155
|
+
- While the command runs, its `stdout` and `stderr` streams are logged in real-time.
|
|
156
|
+
- The returned `stdout` and `stderr` capture the complete output, even though the output is also logged.
|
|
157
|
+
- The command is forcibly terminated if it exceeds the specified timeout.
|
|
158
|
+
"""
|
|
159
|
+
stdout_lines = [] # To accumulate stdout
|
|
160
|
+
stderr_lines = [] # To accumulate stderr
|
|
161
|
+
process = None # Track the process for cleanup
|
|
162
|
+
stdout_thread = None
|
|
163
|
+
stderr_thread = None
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Check if the command exists before executing
|
|
167
|
+
if not shutil.which(command[0]):
|
|
168
|
+
raise FileNotFoundError(f"Command not found: {command[0]}")
|
|
169
|
+
|
|
170
|
+
logger.debug(f"Running command: {command}")
|
|
171
|
+
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
172
|
+
|
|
173
|
+
# Start threads to read and log stdout and stderr
|
|
174
|
+
stdout_thread = threading.Thread(target=_stream_reader, args=(process.stdout, logger.info, stdout_lines))
|
|
175
|
+
stderr_thread = threading.Thread(target=_stream_reader, args=(process.stderr, logger.error, stderr_lines))
|
|
176
|
+
|
|
177
|
+
stdout_thread.start()
|
|
178
|
+
stderr_thread.start()
|
|
179
|
+
|
|
180
|
+
# Wait for process to complete or timeout
|
|
181
|
+
process.wait(timeout=timeout)
|
|
182
|
+
|
|
183
|
+
except FileNotFoundError as e:
|
|
184
|
+
logger.error(f"Command not found: {command[0]}")
|
|
185
|
+
return CommandResult(
|
|
186
|
+
process=None,
|
|
187
|
+
stdout="",
|
|
188
|
+
stderr=str(e),
|
|
189
|
+
returncode=127,
|
|
190
|
+
timeout=timeout,
|
|
191
|
+
command=command
|
|
192
|
+
)
|
|
193
|
+
except subprocess.TimeoutExpired:
|
|
194
|
+
if process:
|
|
195
|
+
process.terminate()
|
|
196
|
+
logger.error(f"Command: '{command}' timed out and was terminated.")
|
|
197
|
+
raise
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"Error running command: {command}", exc_info=True)
|
|
200
|
+
raise
|
|
201
|
+
finally:
|
|
202
|
+
# Ensure threads are joined to clean up (only if they were started)
|
|
203
|
+
if stdout_thread and stdout_thread.is_alive():
|
|
204
|
+
stdout_thread.join()
|
|
205
|
+
if stderr_thread and stderr_thread.is_alive():
|
|
206
|
+
stderr_thread.join()
|
|
207
|
+
|
|
208
|
+
# Ensure process streams are closed
|
|
209
|
+
if process and process.stdout:
|
|
210
|
+
process.stdout.close()
|
|
211
|
+
if process and process.stderr:
|
|
212
|
+
process.stderr.close()
|
|
213
|
+
|
|
214
|
+
# Combine captured stdout and stderr lines into single strings
|
|
215
|
+
stdout = "\n".join(stdout_lines)
|
|
216
|
+
stderr = "\n".join(stderr_lines)
|
|
217
|
+
|
|
218
|
+
# Build the result object
|
|
219
|
+
result = CommandResult(
|
|
220
|
+
process=process,
|
|
221
|
+
stdout=stdout,
|
|
222
|
+
stderr=stderr,
|
|
223
|
+
returncode=process.returncode,
|
|
224
|
+
timeout=timeout,
|
|
225
|
+
command=command
|
|
226
|
+
)
|
|
227
|
+
logger.debug(f"Command result: {result}")
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
def run_command2(command: List[str], timeout: int = 30) -> CommandResult:
|
|
231
|
+
"""
|
|
232
|
+
Executes a given command via subprocess, logs its output in real time, and returns the result.
|
|
233
|
+
|
|
130
234
|
Args:
|
|
131
235
|
command (list): The command to be executed, represented as a list of strings.
|
|
132
236
|
timeout (int): The maximum time in seconds to wait for the command to complete. Defaults to 30 seconds.
|
|
@@ -158,6 +262,10 @@ def run_command(command: List[str], timeout: int = 30) -> CommandResult:
|
|
|
158
262
|
process = None # Track the process for cleanup
|
|
159
263
|
|
|
160
264
|
try:
|
|
265
|
+
# Check if the command exists before executing
|
|
266
|
+
if not shutil.which(command[0]):
|
|
267
|
+
raise FileNotFoundError(f"Command not found: {command[0]}")
|
|
268
|
+
|
|
161
269
|
logger.debug(f"Running command: {command}")
|
|
162
270
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
163
271
|
|
|
@@ -171,6 +279,16 @@ def run_command(command: List[str], timeout: int = 30) -> CommandResult:
|
|
|
171
279
|
# Wait for process to complete or timeout
|
|
172
280
|
process.wait(timeout=timeout)
|
|
173
281
|
|
|
282
|
+
except FileNotFoundError as e:
|
|
283
|
+
logger.error(f"Command not found: {command[0]}")
|
|
284
|
+
return CommandResult(
|
|
285
|
+
process=None,
|
|
286
|
+
stdout="",
|
|
287
|
+
stderr=str(e),
|
|
288
|
+
returncode=127,
|
|
289
|
+
timeout=timeout,
|
|
290
|
+
command=command
|
|
291
|
+
)
|
|
174
292
|
except subprocess.TimeoutExpired:
|
|
175
293
|
if process:
|
|
176
294
|
process.terminate()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dar-backup
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.12
|
|
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
|
|
@@ -690,9 +690,9 @@ Classifier: Topic :: System :: Archiving :: Backup
|
|
|
690
690
|
Requires-Python: >=3.9
|
|
691
691
|
Description-Content-Type: text/markdown
|
|
692
692
|
|
|
693
|
-
# Full, differential or incremental backups using 'dar'
|
|
693
|
+
# Full, differential or incremental backups using 'dar'
|
|
694
694
|
|
|
695
|
-
The wonderful 'dar' [Disk Archiver]
|
|
695
|
+
The wonderful 'dar' [Disk Archiver](https://github.com/Edrusb/DAR) is used for
|
|
696
696
|
the heavy lifting, together with the par2 suite in these scripts.
|
|
697
697
|
|
|
698
698
|
## Table of Contents
|
|
@@ -706,12 +706,11 @@ Description-Content-Type: text/markdown
|
|
|
706
706
|
- [Requirements](#requirements)
|
|
707
707
|
- [Config file](#config-file)
|
|
708
708
|
- [How to run](#how-to-run)
|
|
709
|
-
- [1](#1)
|
|
710
|
-
- [2](#2)
|
|
711
|
-
- [3](#3)
|
|
712
|
-
- [4](#4)
|
|
713
|
-
- [5](#5)
|
|
714
|
-
- [6](#6)
|
|
709
|
+
- [1 - installation](#1---installation)
|
|
710
|
+
- [2 - configuration](#2---configuration)
|
|
711
|
+
- [3 - generate catalog databases](#3---generate-catalog-databases)
|
|
712
|
+
- [4 - do FULL backups](#4---do-full-backups)
|
|
713
|
+
- [5 - deactivate venv](#5---deactivate-venv)
|
|
715
714
|
- [.darrc](#darrc)
|
|
716
715
|
- [Systemctl examples](#systemctl-examples)
|
|
717
716
|
- [Service: dar-back --incremental-backup](#service-dar-back---incremental-backup)
|
|
@@ -723,12 +722,19 @@ Description-Content-Type: text/markdown
|
|
|
723
722
|
- [Exclude .xmp files from that date](#exclude-xmp-files-from-that-date)
|
|
724
723
|
- [Restoring](#restoring)
|
|
725
724
|
- [Default location for restores](#default-location-for-restores)
|
|
726
|
-
- [--restore-dir option](
|
|
725
|
+
- [--restore-dir option](#--restore-dir-option)
|
|
727
726
|
- [A single file](#a-single-file)
|
|
728
727
|
- [A directory](#a-directory)
|
|
729
728
|
- [.NEF from a specific date](#nef-from-a-specific-date)
|
|
729
|
+
- [Restore test fails with exit code 4](#restore-test-fails-with-exit-code-4)
|
|
730
|
+
- [Restore test fails with exit code 5](#restore-test-fails-with-exit-code-5)
|
|
731
|
+
- [Par2](#par2)
|
|
732
|
+
- [Par2 to verify/repair](#par2-to-verifyrepair)
|
|
733
|
+
- [Par2 create redundancy files](#par2-create-redundancy-files)
|
|
730
734
|
- [Points of interest](#points-of-interest)
|
|
735
|
+
- [Merge FULL with DIFF, creating new FULL](#merge-full-with-diff-creating-new-full)
|
|
731
736
|
- [dar manager databases](#dar-manager-databases)
|
|
737
|
+
- [Performance tip due to par2](#performance-tip-due-to-par2)
|
|
732
738
|
- [.darrc sets -vd -vf (since v0.6.4)](#darrc-sets--vd--vf-since-v064)
|
|
733
739
|
- [Reference](#reference)
|
|
734
740
|
- [dar-backup.py](#dar-backuppy)
|
|
@@ -791,105 +797,9 @@ On Ubuntu, install the requirements this way:
|
|
|
791
797
|
|
|
792
798
|
The default configuration is expected here: ~/.config/dar-backup/dar-backup.conf
|
|
793
799
|
|
|
794
|
-
## How to run
|
|
800
|
+
## How to run
|
|
795
801
|
|
|
796
|
-
### 1
|
|
797
|
-
|
|
798
|
-
Config file default location is $HOME/.config/dar-backup/dar-backup.conf
|
|
799
|
-
|
|
800
|
-
Example:
|
|
801
|
-
|
|
802
|
-
```` code
|
|
803
|
-
[MISC]
|
|
804
|
-
LOGFILE_LOCATION=/home/user/dar-backup.log
|
|
805
|
-
MAX_SIZE_VERIFICATION_MB = 20
|
|
806
|
-
MIN_SIZE_VERIFICATION_MB = 1
|
|
807
|
-
NO_FILES_VERIFICATION = 5
|
|
808
|
-
|
|
809
|
-
# timeout in seconds for backup, test, restore and par2 operations
|
|
810
|
-
# The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
|
|
811
|
-
# If a timeout is not specified when using the util.run_command(), a default timeout of 30 secs is used.
|
|
812
|
-
COMMAND_TIMEOUT_SECS = 86400
|
|
813
|
-
|
|
814
|
-
[DIRECTORIES]
|
|
815
|
-
BACKUP_DIR = /home/user/mnt/dir/
|
|
816
|
-
BACKUP.D_DIR = /home/user/.config/dar-backup/backup.d/
|
|
817
|
-
TEST_RESTORE_DIR = /tmp/dar-backup/restore/
|
|
818
|
-
|
|
819
|
-
[AGE]
|
|
820
|
-
# age settings are in days
|
|
821
|
-
DIFF_AGE = 100
|
|
822
|
-
INCR_AGE = 40
|
|
823
|
-
|
|
824
|
-
[PAR2]
|
|
825
|
-
ERROR_CORRECTION_PERCENT = 5
|
|
826
|
-
# False means "do not generate par2 redundancy files"
|
|
827
|
-
ENABLED = True
|
|
828
|
-
|
|
829
|
-
[PREREQ]
|
|
830
|
-
# SCRIPT_1 = /home/user/programmer/dar-backup/prereq/mount-server.sh
|
|
831
|
-
# SCRIPT_2 = <something>
|
|
832
|
-
# ...
|
|
833
|
-
|
|
834
|
-
[POSTREQ]
|
|
835
|
-
# SCRIPT_1 = /home/user/programmer/dar-backup/postreq/umount-server.sh
|
|
836
|
-
# SCRIPT_2 = <something>
|
|
837
|
-
# ...
|
|
838
|
-
|
|
839
|
-
````
|
|
840
|
-
|
|
841
|
-
### 2
|
|
842
|
-
|
|
843
|
-
Put your backup definitions in the directory $BACKUP.D_DIR (defined in the config file)
|
|
844
|
-
|
|
845
|
-
The name of the file is the `backup definition` name.
|
|
846
|
-
|
|
847
|
-
Make as many backup definitions as you need. Run them all in one go, or run one at a time using the `-d` option.
|
|
848
|
-
|
|
849
|
-
The `dar` [documentation](http://dar.linux.free.fr/doc/man/dar.html#COMMANDS%20AND%20OPTIONS) has good information on file selection.
|
|
850
|
-
|
|
851
|
-
Example of backup definition for a home directory
|
|
852
|
-
|
|
853
|
-
```` code
|
|
854
|
-
|
|
855
|
-
# Switch to ordered selection mode, which means that the following
|
|
856
|
-
# options will be considered top to bottom
|
|
857
|
-
-am
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
# Backup Root dir
|
|
861
|
-
-R /home/user
|
|
862
|
-
|
|
863
|
-
# Directories to backup below the Root dir
|
|
864
|
-
# if you want to take a backup of /home/user/Documents only, uncomment next line
|
|
865
|
-
# -g Documents
|
|
866
|
-
|
|
867
|
-
# Some directories to exclude below the Root dir (here Root directory is `/home/user` as set in the -R option)
|
|
868
|
-
-P mnt
|
|
869
|
-
-P tmp
|
|
870
|
-
-P .cache
|
|
871
|
-
-P .config/Code/CachedData
|
|
872
|
-
-P .config/Code/Cache
|
|
873
|
-
-P ".config/Code/Service Worker"
|
|
874
|
-
-P .config/Code/logs
|
|
875
|
-
-P snap/firefox/common/.cache
|
|
876
|
-
|
|
877
|
-
# compression level
|
|
878
|
-
-z5
|
|
879
|
-
|
|
880
|
-
# no overwrite, if you rerun a backup, 'dar' halts and asks what to do (and Quits due to the "-Q" given by dar-backup)
|
|
881
|
-
-n
|
|
882
|
-
|
|
883
|
-
# size of each slice in the archive
|
|
884
|
-
--slice 10G
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
# bypass directores marked as cache directories
|
|
888
|
-
# http://dar.linux.free.fr/doc/Features.html
|
|
889
|
-
--cache-directory-tagging
|
|
890
|
-
````
|
|
891
|
-
|
|
892
|
-
### 3
|
|
802
|
+
### 1 - installation
|
|
893
803
|
|
|
894
804
|
Installation is currently in a venv. These commands are installed in the venv:
|
|
895
805
|
|
|
@@ -897,6 +807,7 @@ Installation is currently in a venv. These commands are installed in the venv:
|
|
|
897
807
|
- cleanup
|
|
898
808
|
- manager
|
|
899
809
|
- clean-log
|
|
810
|
+
- installer
|
|
900
811
|
|
|
901
812
|
To install, create a venv and run pip:
|
|
902
813
|
|
|
@@ -918,25 +829,48 @@ Typing `db` at the command line gives this
|
|
|
918
829
|
|
|
919
830
|
```` bash
|
|
920
831
|
(venv) user@machine:~$ db
|
|
921
|
-
dar-backup 0.6.
|
|
832
|
+
dar-backup 0.6.12
|
|
922
833
|
dar-backup.py source code is here: https://github.com/per2jensen/dar-backup
|
|
923
834
|
Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
924
835
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
925
836
|
See section 15 and section 16 in the supplied "LICENSE" file.
|
|
926
837
|
````
|
|
927
838
|
|
|
928
|
-
###
|
|
839
|
+
### 2 - configuration
|
|
840
|
+
|
|
841
|
+
The dar-backup installer is non-destructive and stops if some of the default directories exist.
|
|
842
|
+
|
|
843
|
+
Run the installer
|
|
844
|
+
|
|
845
|
+
```` bash
|
|
846
|
+
installer --install
|
|
847
|
+
````
|
|
848
|
+
|
|
849
|
+
The output is
|
|
850
|
+
|
|
851
|
+
```` text
|
|
852
|
+
Directories created: `/home/user/dar-backup/` and `/home/user/.config/dar-backup`
|
|
853
|
+
Config file deployed to /home/user/.config/dar-backup/dar-backup.conf
|
|
854
|
+
Default backup definition deployed to /home/user/.config/dar-backup/backup.d/default
|
|
855
|
+
1. Now run `manager --create` to create the catalog database.
|
|
856
|
+
2. Then you can run `dar-backup --full-backup` to create a backup.
|
|
857
|
+
3. List backups with `dar-backup --list`
|
|
858
|
+
4. List contents of a backup with `dar-backup --list-contents <backup-name>`
|
|
859
|
+
````
|
|
860
|
+
|
|
861
|
+
### 3 - generate catalog databases
|
|
862
|
+
|
|
863
|
+
Generate the archive catalog database(s).
|
|
929
864
|
|
|
930
|
-
Generate the archive catalog database(s).
|
|
931
865
|
`dar-backup` expects the catalog databases to be in place, it does not automatically create them (by design)
|
|
932
866
|
|
|
933
867
|
```` bash
|
|
934
|
-
manager --create
|
|
868
|
+
manager --create
|
|
935
869
|
````
|
|
936
870
|
|
|
937
|
-
###
|
|
871
|
+
### 4 - do FULL backups
|
|
938
872
|
|
|
939
|
-
You are ready to do backups of all your backup definitions, if your backup definitions are
|
|
873
|
+
You are ready to do backups of all your backup definitions, if your backup definitions are
|
|
940
874
|
in place in BACKUP.D_DIR (see config file)
|
|
941
875
|
|
|
942
876
|
```` bash
|
|
@@ -953,9 +887,9 @@ If you want a backup of a single definition, use the `-d <backup definition>` op
|
|
|
953
887
|
dar-backup --full-backup -d <your backup definition>
|
|
954
888
|
````
|
|
955
889
|
|
|
956
|
-
###
|
|
890
|
+
### 5 - deactivate venv
|
|
957
891
|
|
|
958
|
-
Deactivate the virtual environment
|
|
892
|
+
Deactivate the virtual environment (venv)
|
|
959
893
|
|
|
960
894
|
```` bash
|
|
961
895
|
deactivate
|
|
@@ -1311,14 +1245,92 @@ dar-backup --restore <archive_name> --selection "-X '*.xmp' -I '*2024-06-16*' -
|
|
|
1311
1245
|
deactivate
|
|
1312
1246
|
```
|
|
1313
1247
|
|
|
1248
|
+
### restore test fails with exit code 4
|
|
1249
|
+
|
|
1250
|
+
"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.
|
|
1251
|
+
|
|
1252
|
+
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).
|
|
1253
|
+
|
|
1254
|
+
This causes dar to restore without an error.
|
|
1255
|
+
|
|
1256
|
+
It is a good option when using dar as a non-privileged user.
|
|
1257
|
+
|
|
1258
|
+
### restore test fails with exit code 5
|
|
1259
|
+
|
|
1260
|
+
If exit code 5 is emitted on the restore test, FSA (File System specific Attributes) could be the cause.
|
|
1261
|
+
|
|
1262
|
+
That (might) occur if you backup a file stored on one type of filesystem, and restore it on another type.
|
|
1263
|
+
My home directory is on a btrfs filesystem, while /tmp (for the restore test) is on zfs.
|
|
1264
|
+
|
|
1265
|
+
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.
|
|
1266
|
+
|
|
1267
|
+
If you need to use this option, un-comment it in the .darrc file (located in the virtual environment where dar-backup is installed)
|
|
1268
|
+
|
|
1269
|
+
## Par2
|
|
1270
|
+
|
|
1271
|
+
### Par2 to verify/repair
|
|
1272
|
+
|
|
1273
|
+
You can run a par2 verification on an archive like this:
|
|
1274
|
+
|
|
1275
|
+
```` bash
|
|
1276
|
+
for file in <archive>*.dar.par2; do
|
|
1277
|
+
par2 verify "$file"
|
|
1278
|
+
done
|
|
1279
|
+
````
|
|
1280
|
+
|
|
1281
|
+
if there are problems with a slice, try to repair it like this:
|
|
1282
|
+
|
|
1283
|
+
```` bash
|
|
1284
|
+
par2 repair <archive>.<slice number>.dar.par2
|
|
1285
|
+
````
|
|
1286
|
+
|
|
1287
|
+
### Par2 create redundancy files
|
|
1288
|
+
|
|
1289
|
+
If you have merged archives, you will need to create the .par2 redundency files manually.
|
|
1290
|
+
Here is an example
|
|
1291
|
+
|
|
1292
|
+
```` bash
|
|
1293
|
+
for file in <some-archive>_FULL_yyyy-mm-dd.*; do
|
|
1294
|
+
par2 c -r5 -n1 "$file"
|
|
1295
|
+
done
|
|
1296
|
+
````
|
|
1297
|
+
|
|
1298
|
+
where "c" is create, -r5 is 5% redundency and -n1 is 1 redundency file
|
|
1299
|
+
|
|
1314
1300
|
## Points of interest
|
|
1315
1301
|
|
|
1302
|
+
### Merge FULL with DIFF, creating new FULL
|
|
1303
|
+
|
|
1304
|
+
Over time, the DIFF archives become larger and larger. At some point one wishes to create a new FULL archive to do DIFF's on.
|
|
1305
|
+
One way to do that, is to let dar create a FULL archive from scratch, another is to merge a FULL archive with a DIFF, and from there do DIFF's until they once again gets too large for your taste.
|
|
1306
|
+
|
|
1307
|
+
I do backups of my homedir. Here it is shown how a FULL archive is merged with a DIFF, creating a new FULL archive.
|
|
1308
|
+
|
|
1309
|
+
```` bash
|
|
1310
|
+
dar --merge pj-homedir_FULL_2021-09-12 -A pj-homedir_FULL_2021-06-06 -@pj-homedir_DIFF_2021-08-29 -s 12G
|
|
1311
|
+
|
|
1312
|
+
# test the new FULL archive
|
|
1313
|
+
dar -t pj-homedir_FULL_2021-09-12
|
|
1314
|
+
|
|
1315
|
+
# create Par2 redundancy files
|
|
1316
|
+
for file in pj-homedir_FULL_yyyy-mm-dd.*.dar; do
|
|
1317
|
+
par2 c -r5 -n1 "$file"
|
|
1318
|
+
done
|
|
1319
|
+
|
|
1320
|
+
````
|
|
1321
|
+
|
|
1316
1322
|
### dar manager databases
|
|
1317
1323
|
|
|
1318
1324
|
`dar-backup` now saves archive catalogs in dar catalog databases.
|
|
1319
1325
|
|
|
1320
1326
|
This makes it easier to restore to a given date when having many FULL, DIFF and INCR archives.
|
|
1321
1327
|
|
|
1328
|
+
### Performance tip due to par2
|
|
1329
|
+
|
|
1330
|
+
This [dar benchmark page](https://dar.sourceforge.io/doc/benchmark.html) has an interesting note on the slice size.
|
|
1331
|
+
|
|
1332
|
+
Slice size should be smaller than available RAM, apparently a large performance hit can be avoided keeping the the par2 data in memory.
|
|
1333
|
+
|
|
1322
1334
|
### .darrc sets -vd -vf (since v0.6.4)
|
|
1323
1335
|
|
|
1324
1336
|
These .darrc settings make `dar` print the current directory being processed (-vd) and some stats after (-vf)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
+
dar_backup/__about__.py,sha256=AY38r3HUSyMqkCPP-vaHASQmjQF5-PRm11j_QYtx28w,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=NaZMrTdtYv4uJSw3jwDQWc5F5jMfIdfIQdrcPGAVcnM,11439
|
|
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=Cye8gS0E0mNKaUzcjqsdsuTyyeZYCspRMBIdcGbsEik,33171
|
|
9
|
+
dar_backup/installer.py,sha256=0TgC_O-T7Y3sLn_NIQ9lBYt8GJqLZzxPqkmbjElfgkM,4491
|
|
10
|
+
dar_backup/manager.py,sha256=MkrB0AL0MefE_cUAvdzQb_0bzvqsPmYi5s0M3EPw-z8,21379
|
|
11
|
+
dar_backup/util.py,sha256=E-sEBQZY1hmdeVx5xNE22zKQ0BXDee1eI9F1-w7Fq1Q,15756
|
|
12
|
+
dar_backup-0.6.12.dist-info/METADATA,sha256=YEbPNJr_ntP03BVKSACo2MU-Qko_9quyzmuqrhryBz0,69768
|
|
13
|
+
dar_backup-0.6.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
+
dar_backup-0.6.12.dist-info/entry_points.txt,sha256=Z7P5BUbhtJxo8_nB9qNIMay2eGDbsMKB3Fjwv3GMa4g,202
|
|
15
|
+
dar_backup-0.6.12.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
16
|
+
dar_backup-0.6.12.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
-
dar_backup/__about__.py,sha256=G-zfU8sHQRTZnZK0te1cvbxqeLhT1z86XeAYFNUON6Q,22
|
|
3
|
-
dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
dar_backup/clean_log.py,sha256=VXKA2BMyQmaC6R08Bq9a3wP3mczdFb_moy6HkL-mnF8,5176
|
|
5
|
-
dar_backup/cleanup.py,sha256=9yEdRR84XPtEvBGc2QfwGBQl2tdTPttjetHeiSc_TsM,11419
|
|
6
|
-
dar_backup/config_settings.py,sha256=CBMUhLOOZ-x7CRdS3vBDk4TYaGqC4N1Ot8IMH-qPaI0,3617
|
|
7
|
-
dar_backup/dar_backup.py,sha256=hDy7aXU-XiWOtW40Pxql441liNkSYKGU76eOwy8m7fU,32714
|
|
8
|
-
dar_backup/manager.py,sha256=HDa8eYF89QFhlBRR4EWRzzmswOW00S_w8ToZ5SARO_o,21359
|
|
9
|
-
dar_backup/util.py,sha256=SSSJYM9lQZfubhTUBlX1xDGWmCpYEF3ePARmlY544xM,11283
|
|
10
|
-
dar_backup-0.6.10.dist-info/METADATA,sha256=CEgbqp93sB_cPnEDWuLn7gphZjdrn4zP_5dShC2Buv8,67980
|
|
11
|
-
dar_backup-0.6.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
-
dar_backup-0.6.10.dist-info/entry_points.txt,sha256=p6c4uQLjlTIVP1Od2iorGefrVUH0IWZdFRMl63mNaRg,164
|
|
13
|
-
dar_backup-0.6.10.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
14
|
-
dar_backup-0.6.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|