dar-backup 0.6.15__py3-none-any.whl → 0.6.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dar_backup/__about__.py +1 -1
- dar_backup/cleanup.py +39 -17
- dar_backup/command_runner.py +81 -0
- dar_backup/config_settings.py +7 -1
- dar_backup/dar-backup.conf +1 -1
- dar_backup/dar_backup.py +25 -22
- dar_backup/manager.py +15 -9
- dar_backup/util.py +0 -103
- {dar_backup-0.6.15.dist-info → dar_backup-0.6.17.dist-info}/METADATA +252 -82
- dar_backup-0.6.17.dist-info/RECORD +17 -0
- dar_backup-0.6.15.dist-info/RECORD +0 -16
- {dar_backup-0.6.15.dist-info → dar_backup-0.6.17.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.15.dist-info → dar_backup-0.6.17.dist-info}/entry_points.txt +0 -0
- {dar_backup-0.6.15.dist-info → dar_backup-0.6.17.dist-info}/licenses/LICENSE +0 -0
dar_backup/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.6.
|
|
1
|
+
__version__ = "0.6.17"
|
dar_backup/cleanup.py
CHANGED
|
@@ -21,6 +21,7 @@ import subprocess
|
|
|
21
21
|
import sys
|
|
22
22
|
|
|
23
23
|
from datetime import datetime, timedelta
|
|
24
|
+
from inputimeout import inputimeout, TimeoutOccurred
|
|
24
25
|
from time import time
|
|
25
26
|
from typing import Dict, List, NamedTuple
|
|
26
27
|
|
|
@@ -28,14 +29,14 @@ from . import __about__ as about
|
|
|
28
29
|
from dar_backup.config_settings import ConfigSettings
|
|
29
30
|
from dar_backup.util import extract_error_lines
|
|
30
31
|
from dar_backup.util import list_backups
|
|
31
|
-
from dar_backup.util import run_command
|
|
32
32
|
from dar_backup.util import setup_logging
|
|
33
|
+
from dar_backup.util import get_logger
|
|
33
34
|
|
|
34
|
-
from dar_backup.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
from dar_backup.command_runner import CommandRunner
|
|
36
|
+
from dar_backup.command_runner import CommandResult
|
|
37
37
|
|
|
38
38
|
logger = None
|
|
39
|
+
runner = None
|
|
39
40
|
|
|
40
41
|
def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=None):
|
|
41
42
|
"""
|
|
@@ -133,7 +134,7 @@ def delete_catalog(catalog_name: str, args: NamedTuple) -> bool:
|
|
|
133
134
|
command = [f"manager", "--remove-specific-archive", catalog_name, "--config-file", args.config_file, '--log-level', 'debug', '--log-stdout']
|
|
134
135
|
logger.info(f"Deleting catalog '{catalog_name}' using config file: '{args.config_file}'")
|
|
135
136
|
try:
|
|
136
|
-
result:CommandResult =
|
|
137
|
+
result:CommandResult = runner.run(command)
|
|
137
138
|
if result.returncode == 0:
|
|
138
139
|
logger.info(f"Deleted catalog '{catalog_name}', using config file: '{args.config_file}'")
|
|
139
140
|
logger.debug(f"Stdout: manager.py --remove-specific-archive output:\n{result.stdout}")
|
|
@@ -156,19 +157,21 @@ def show_version():
|
|
|
156
157
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
157
158
|
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
158
159
|
|
|
160
|
+
|
|
159
161
|
def main():
|
|
160
|
-
global logger
|
|
162
|
+
global logger, runner
|
|
161
163
|
|
|
162
164
|
parser = argparse.ArgumentParser(description="Cleanup old archives according to AGE configuration.")
|
|
163
165
|
parser.add_argument('-d', '--backup-definition', help="Specific backup definition to cleanup.")
|
|
164
166
|
parser.add_argument('-c', '--config-file', '-c', type=str, help="Path to 'dar-backup.conf'", default='~/.config/dar-backup/dar-backup.conf')
|
|
165
167
|
parser.add_argument('-v', '--version', action='store_true', help="Show version information.")
|
|
166
168
|
parser.add_argument('--alternate-archive-dir', type=str, help="Cleanup in this directory instead of the default one.")
|
|
167
|
-
parser.add_argument('--cleanup-specific-archives', type=str, help="
|
|
169
|
+
parser.add_argument('--cleanup-specific-archives', type=str, help="Comma separated list of archives to cleanup")
|
|
168
170
|
parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
|
|
169
171
|
parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
|
|
170
172
|
parser.add_argument('--log-level', type=str, help="`debug` or `trace`, default is `info`", default="info")
|
|
171
173
|
parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
|
|
174
|
+
parser.add_argument('--test-mode', action='store_true', help='Read envvars in order to run some pytest cases')
|
|
172
175
|
args = parser.parse_args()
|
|
173
176
|
|
|
174
177
|
args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
|
|
@@ -185,7 +188,8 @@ def main():
|
|
|
185
188
|
# command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
|
|
186
189
|
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
187
190
|
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
188
|
-
|
|
191
|
+
command_logger = get_logger(command_output_logger = True)
|
|
192
|
+
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
189
193
|
|
|
190
194
|
logger.info(f"=====================================")
|
|
191
195
|
logger.info(f"cleanup.py started, version: {about.__version__}")
|
|
@@ -232,6 +236,33 @@ def main():
|
|
|
232
236
|
logger.info(f"Cleaning up specific archives: {args.cleanup_specific_archives}")
|
|
233
237
|
archive_names = args.cleanup_specific_archives.split(',')
|
|
234
238
|
for archive_name in archive_names:
|
|
239
|
+
if "_FULL_" in archive_name:
|
|
240
|
+
try:
|
|
241
|
+
try:
|
|
242
|
+
# used for pytest cases
|
|
243
|
+
if args.test_mode:
|
|
244
|
+
confirmation = os.environ.get("CLEANUP_TEST_DELETE_FULL")
|
|
245
|
+
if confirmation == None:
|
|
246
|
+
raise RuntimeError("envvar 'CLEANUP_TEST_DELETE_FULL' not set")
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
confirmation = inputimeout(
|
|
250
|
+
prompt=f"Are you sure you want to delete the FULL archive '{archive_name}'? (yes/no): ",
|
|
251
|
+
timeout=30)
|
|
252
|
+
if confirmation == None:
|
|
253
|
+
continue
|
|
254
|
+
else:
|
|
255
|
+
confirmation = confirmation.strip().lower()
|
|
256
|
+
except TimeoutOccurred:
|
|
257
|
+
logger.info(f"Timeout waiting for confirmation for FULL archive: {archive_name}. Skipping deletion.")
|
|
258
|
+
continue
|
|
259
|
+
except KeyboardInterrupt:
|
|
260
|
+
logger.info(f"User interrupted confirmation for FULL archive: {archive_name}. Skipping deletion.")
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
if confirmation != 'yes':
|
|
264
|
+
logger.info(f"User did not answer 'yes' to confirm deletion of FULL archive: {archive_name}. Skipping deletion.")
|
|
265
|
+
continue
|
|
235
266
|
logger.info(f"Deleting archive: {archive_name}")
|
|
236
267
|
delete_archive(config_settings.backup_dir, archive_name.strip(), args)
|
|
237
268
|
elif args.list:
|
|
@@ -253,15 +284,6 @@ def main():
|
|
|
253
284
|
end_time=int(time())
|
|
254
285
|
logger.info(f"END TIME: {end_time}")
|
|
255
286
|
|
|
256
|
-
# error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
|
|
257
|
-
# if len(error_lines) > 0:
|
|
258
|
-
# args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
|
|
259
|
-
# for line in error_lines:
|
|
260
|
-
# args.verbose and print(line)
|
|
261
|
-
# sys.exit(1)
|
|
262
|
-
# else:
|
|
263
|
-
# args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
|
|
264
|
-
# sys.exit(0)
|
|
265
287
|
|
|
266
288
|
if __name__ == "__main__":
|
|
267
289
|
main()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
class CommandResult:
|
|
10
|
+
def __init__(self, returncode: int, stdout: str, stderr: str):
|
|
11
|
+
self.returncode = returncode
|
|
12
|
+
self.stdout = stdout
|
|
13
|
+
self.stderr = stderr
|
|
14
|
+
|
|
15
|
+
def __repr__(self):
|
|
16
|
+
return f"<CommandResult returncode={self.returncode}>"
|
|
17
|
+
|
|
18
|
+
class CommandRunner:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
logger: Optional[logging.Logger] = None,
|
|
22
|
+
command_logger: Optional[logging.Logger] = None,
|
|
23
|
+
default_timeout: int = 30
|
|
24
|
+
):
|
|
25
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
26
|
+
self.command_logger = command_logger or self.logger
|
|
27
|
+
self.default_timeout = default_timeout
|
|
28
|
+
|
|
29
|
+
def run(
|
|
30
|
+
self,
|
|
31
|
+
cmd: List[str],
|
|
32
|
+
*,
|
|
33
|
+
timeout: Optional[int] = None,
|
|
34
|
+
check: bool = False,
|
|
35
|
+
capture_output: bool = True,
|
|
36
|
+
text: bool = True
|
|
37
|
+
) -> CommandResult:
|
|
38
|
+
timeout = timeout or self.default_timeout
|
|
39
|
+
self.logger.debug(f"Executing command: {' '.join(cmd)} (timeout={timeout}s)")
|
|
40
|
+
|
|
41
|
+
process = subprocess.Popen(
|
|
42
|
+
cmd,
|
|
43
|
+
stdout=subprocess.PIPE if capture_output else None,
|
|
44
|
+
stderr=subprocess.PIPE if capture_output else None,
|
|
45
|
+
text=text,
|
|
46
|
+
bufsize=1
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
stdout_lines = []
|
|
50
|
+
stderr_lines = []
|
|
51
|
+
|
|
52
|
+
def stream_output(stream, lines, level):
|
|
53
|
+
for line in iter(stream.readline, ''):
|
|
54
|
+
lines.append(line)
|
|
55
|
+
self.command_logger.log(level, line.strip())
|
|
56
|
+
stream.close()
|
|
57
|
+
|
|
58
|
+
threads = []
|
|
59
|
+
if capture_output and process.stdout:
|
|
60
|
+
t_out = threading.Thread(target=stream_output, args=(process.stdout, stdout_lines, logging.INFO))
|
|
61
|
+
t_out.start()
|
|
62
|
+
threads.append(t_out)
|
|
63
|
+
if capture_output and process.stderr:
|
|
64
|
+
t_err = threading.Thread(target=stream_output, args=(process.stderr, stderr_lines, logging.ERROR))
|
|
65
|
+
t_err.start()
|
|
66
|
+
threads.append(t_err)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
process.wait(timeout=timeout)
|
|
70
|
+
except subprocess.TimeoutExpired:
|
|
71
|
+
process.kill()
|
|
72
|
+
self.logger.error(f"Command timed out: {' '.join(cmd)}")
|
|
73
|
+
return CommandResult(-1, ''.join(stdout_lines), ''.join(stderr_lines))
|
|
74
|
+
|
|
75
|
+
for t in threads:
|
|
76
|
+
t.join()
|
|
77
|
+
|
|
78
|
+
if check and process.returncode != 0:
|
|
79
|
+
self.logger.error(f"Command failed with exit code {process.returncode}")
|
|
80
|
+
|
|
81
|
+
return CommandResult(process.returncode, ''.join(stdout_lines), ''.join(stderr_lines))
|
dar_backup/config_settings.py
CHANGED
|
@@ -63,7 +63,13 @@ class ConfigSettings:
|
|
|
63
63
|
self.diff_age = int(self.config['AGE']['DIFF_AGE'])
|
|
64
64
|
self.incr_age = int(self.config['AGE']['INCR_AGE'])
|
|
65
65
|
self.error_correction_percent = int(self.config['PAR2']['ERROR_CORRECTION_PERCENT'])
|
|
66
|
-
|
|
66
|
+
val = self.config['PAR2']['ENABLED'].strip().lower()
|
|
67
|
+
if val in ('true', '1', 'yes'):
|
|
68
|
+
self.par2_enabled = True
|
|
69
|
+
elif val in ('false', '0', 'no'):
|
|
70
|
+
self.par2_enabled = False
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(f"Invalid boolean value for 'ENABLED' in [PAR2]: '{val}'")
|
|
67
73
|
|
|
68
74
|
# Ensure the directories exist
|
|
69
75
|
Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
|
dar_backup/dar-backup.conf
CHANGED
|
@@ -9,7 +9,7 @@ MIN_SIZE_VERIFICATION_MB = 1
|
|
|
9
9
|
NO_FILES_VERIFICATION = 5
|
|
10
10
|
# timeout in seconds for backup, test, restore and par2 operations
|
|
11
11
|
# The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
|
|
12
|
-
# If a timeout is not specified when using the
|
|
12
|
+
# If a timeout is not specified when using the CommandRunner, a default timeout of 30 secs is used.
|
|
13
13
|
COMMAND_TIMEOUT_SECS = 86400
|
|
14
14
|
|
|
15
15
|
[DIRECTORIES]
|
dar_backup/dar_backup.py
CHANGED
|
@@ -25,14 +25,17 @@ from typing import List
|
|
|
25
25
|
from . import __about__ as about
|
|
26
26
|
from dar_backup.config_settings import ConfigSettings
|
|
27
27
|
from dar_backup.util import list_backups
|
|
28
|
-
from dar_backup.util import run_command
|
|
29
28
|
from dar_backup.util import setup_logging
|
|
30
29
|
from dar_backup.util import get_logger
|
|
31
30
|
from dar_backup.util import BackupError
|
|
32
31
|
from dar_backup.util import RestoreError
|
|
33
32
|
|
|
33
|
+
from dar_backup.command_runner import CommandRunner
|
|
34
|
+
from dar_backup.command_runner import CommandResult
|
|
35
|
+
|
|
34
36
|
|
|
35
37
|
logger = None
|
|
38
|
+
runner = None
|
|
36
39
|
|
|
37
40
|
def generic_backup(type: str, command: List[str], backup_file: str, backup_definition: str, darrc: str, config_settings: ConfigSettings, args: argparse.Namespace) -> List[str]:
|
|
38
41
|
"""
|
|
@@ -63,9 +66,8 @@ def generic_backup(type: str, command: List[str], backup_file: str, backup_defin
|
|
|
63
66
|
result: List[tuple] = []
|
|
64
67
|
|
|
65
68
|
logger.info(f"===> Starting {type} backup for {backup_definition}")
|
|
66
|
-
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
67
69
|
try:
|
|
68
|
-
process =
|
|
70
|
+
process = runner.run(command, timeout = config_settings.command_timeout_secs)
|
|
69
71
|
if process.returncode == 0:
|
|
70
72
|
logger.info(f"{type} backup completed successfully.")
|
|
71
73
|
elif process.returncode == 5:
|
|
@@ -75,7 +77,7 @@ def generic_backup(type: str, command: List[str], backup_file: str, backup_defin
|
|
|
75
77
|
|
|
76
78
|
if process.returncode == 0 or process.returncode == 5:
|
|
77
79
|
add_catalog_command = ['manager', '--add-specific-archive' ,backup_file, '--config-file', args.config_file]
|
|
78
|
-
command_result =
|
|
80
|
+
command_result = runner.run(add_catalog_command, timeout = config_settings.command_timeout_secs)
|
|
79
81
|
if command_result.returncode == 0:
|
|
80
82
|
logger.info(f"Catalog for archive '{backup_file}' added successfully to its manager.")
|
|
81
83
|
else:
|
|
@@ -192,8 +194,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
192
194
|
"""
|
|
193
195
|
result = True
|
|
194
196
|
command = ['dar', '-t', backup_file, '-Q']
|
|
195
|
-
|
|
196
|
-
process = run_command(command, config_settings.command_timeout_secs)
|
|
197
|
+
process = runner.run(command, timeout = config_settings.command_timeout_secs)
|
|
197
198
|
if process.returncode == 0:
|
|
198
199
|
logger.info("Archive integrity test passed.")
|
|
199
200
|
else:
|
|
@@ -235,7 +236,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
235
236
|
args.verbose and logger.info(f"Restoring file: '{restored_file_path}' from backup to: '{config_settings.test_restore_dir}' for file comparing")
|
|
236
237
|
command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-Q', '-B', args.darrc, 'restore-options']
|
|
237
238
|
args.verbose and logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
238
|
-
process =
|
|
239
|
+
process = runner.run(command, timeout = config_settings.command_timeout_secs)
|
|
239
240
|
if process.returncode != 0:
|
|
240
241
|
raise Exception(str(process))
|
|
241
242
|
|
|
@@ -276,7 +277,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
|
|
|
276
277
|
command.extend(selection_criteria)
|
|
277
278
|
command.extend(['-B', darrc, 'restore-options']) # the .darrc `restore-options` section
|
|
278
279
|
logger.info(f"Running restore command: {' '.join(map(shlex.quote, command))}")
|
|
279
|
-
process =
|
|
280
|
+
process = runner.run(command, timeout = config_settings.command_timeout_secs)
|
|
280
281
|
if process.returncode == 0:
|
|
281
282
|
logger.info(f"Restore completed successfully to: '{restore_dir}'")
|
|
282
283
|
else:
|
|
@@ -309,7 +310,7 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
|
|
|
309
310
|
try:
|
|
310
311
|
command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
|
|
311
312
|
logger.debug(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
312
|
-
command_result =
|
|
313
|
+
command_result = runner.run(command)
|
|
313
314
|
# Parse the XML data
|
|
314
315
|
file_paths = find_files_with_paths(command_result.stdout)
|
|
315
316
|
return file_paths
|
|
@@ -339,8 +340,7 @@ def list_contents(backup_name, backup_dir, selection=None):
|
|
|
339
340
|
if selection:
|
|
340
341
|
selection_criteria = shlex.split(selection)
|
|
341
342
|
command.extend(selection_criteria)
|
|
342
|
-
|
|
343
|
-
process = run_command(command)
|
|
343
|
+
process = runner.run(command)
|
|
344
344
|
stdout,stderr = process.stdout, process.stderr
|
|
345
345
|
if process.returncode != 0:
|
|
346
346
|
logger.error(f"Error listing contents of backup: '{backup_name}'")
|
|
@@ -512,7 +512,7 @@ def generate_par2_files(backup_file: str, config_settings: ConfigSettings, args)
|
|
|
512
512
|
|
|
513
513
|
# Run the par2 command to generate redundancy files with error correction
|
|
514
514
|
command = ['par2', 'create', f'-r{config_settings.error_correction_percent}', '-q', '-q', file_path]
|
|
515
|
-
process =
|
|
515
|
+
process = runner.run(command, timeout = config_settings.command_timeout_secs)
|
|
516
516
|
|
|
517
517
|
if process.returncode == 0:
|
|
518
518
|
logger.info(f"{counter}/{number_of_slices}: Done")
|
|
@@ -629,7 +629,7 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
629
629
|
config_settings (ConfigSettings): An instance of the ConfigSettings class.
|
|
630
630
|
|
|
631
631
|
Raises:
|
|
632
|
-
RuntimeError: If a subprocess
|
|
632
|
+
RuntimeError: If a subprocess returns anything but zero.
|
|
633
633
|
|
|
634
634
|
subprocess.CalledProcessError: if CalledProcessError is raised in subprocess.run(), let it bobble up.
|
|
635
635
|
"""
|
|
@@ -641,7 +641,7 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
641
641
|
raise RuntimeError(f"requirements: {type} not in: {allowed_types}")
|
|
642
642
|
|
|
643
643
|
|
|
644
|
-
logger.
|
|
644
|
+
logger.debug(f"Performing {type}")
|
|
645
645
|
if type in config_setting.config:
|
|
646
646
|
for key in sorted(config_setting.config[type].keys()):
|
|
647
647
|
script = config_setting.config[type][key]
|
|
@@ -658,7 +658,7 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
658
658
|
|
|
659
659
|
|
|
660
660
|
def main():
|
|
661
|
-
global logger
|
|
661
|
+
global logger, runner
|
|
662
662
|
results: List[(str,int)] = [] # a list op tuples (<msg>, <exit code>)
|
|
663
663
|
|
|
664
664
|
MIN_PYTHON_VERSION = (3, 9)
|
|
@@ -713,6 +713,9 @@ def main():
|
|
|
713
713
|
print(f"Error: logfile_location in {args.config_file} does not end at 'dar-backup.log', exiting", file=stderr)
|
|
714
714
|
|
|
715
715
|
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
716
|
+
command_logger = get_logger(command_output_logger = True)
|
|
717
|
+
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
718
|
+
|
|
716
719
|
|
|
717
720
|
try:
|
|
718
721
|
if not args.darrc:
|
|
@@ -741,9 +744,9 @@ def main():
|
|
|
741
744
|
file_dir = os.path.normpath(os.path.dirname(__file__))
|
|
742
745
|
args.verbose and (print(f"Script directory: {file_dir}"))
|
|
743
746
|
args.verbose and (print(f"Config file: {args.config_file}"))
|
|
744
|
-
args.verbose and args.full_backup and (print(f"Type of backup:
|
|
745
|
-
args.verbose and args.differential_backup and (print(f"Type of backup:
|
|
746
|
-
args.verbose and args.incremental_backup and (print(f"Type of backup:
|
|
747
|
+
args.verbose and args.full_backup and (print(f"Type of backup: FULL"))
|
|
748
|
+
args.verbose and args.differential_backup and (print(f"Type of backup: DIFF"))
|
|
749
|
+
args.verbose and args.incremental_backup and (print(f"Type of backup: INCR"))
|
|
747
750
|
args.verbose and args.backup_definition and (print(f"Backup definition: '{args.backup_definition}'"))
|
|
748
751
|
if args.alternate_reference_archive:
|
|
749
752
|
args.verbose and (print(f"Alternate ref archive: {args.alternate_reference_archive}"))
|
|
@@ -797,10 +800,10 @@ def main():
|
|
|
797
800
|
end_time=int(time())
|
|
798
801
|
logger.info(f"END TIME: {end_time}")
|
|
799
802
|
# Clean up
|
|
800
|
-
if os.path.exists(args.darrc) and os.path.dirname(args.darrc) == os.path.expanduser("~"):
|
|
801
|
-
if args.darrc.startswith("filtered_darrc_"):
|
|
802
|
-
os.remove(args.darrc)
|
|
803
|
-
|
|
803
|
+
if os.path.exists(args.darrc) and (os.path.dirname(args.darrc) == os.path.expanduser("~")):
|
|
804
|
+
if os.path.basename(args.darrc).startswith("filtered_darrc_"):
|
|
805
|
+
if os.remove(args.darrc):
|
|
806
|
+
logger.debug(f"Removed filtered .darrc: {args.darrc}")
|
|
804
807
|
|
|
805
808
|
|
|
806
809
|
# Determine exit code
|
dar_backup/manager.py
CHANGED
|
@@ -29,9 +29,12 @@ import sys
|
|
|
29
29
|
|
|
30
30
|
from . import __about__ as about
|
|
31
31
|
from dar_backup.config_settings import ConfigSettings
|
|
32
|
-
from dar_backup.util import run_command
|
|
33
32
|
from dar_backup.util import setup_logging
|
|
34
33
|
from dar_backup.util import CommandResult
|
|
34
|
+
from dar_backup.util import get_logger
|
|
35
|
+
|
|
36
|
+
from dar_backup.command_runner import CommandRunner
|
|
37
|
+
from dar_backup.command_runner import CommandResult
|
|
35
38
|
|
|
36
39
|
from datetime import datetime
|
|
37
40
|
from time import time
|
|
@@ -44,6 +47,7 @@ SCRIPTDIRPATH = os.path.dirname(SCRIPTPATH)
|
|
|
44
47
|
DB_SUFFIX = ".db"
|
|
45
48
|
|
|
46
49
|
logger = None
|
|
50
|
+
runner = None
|
|
47
51
|
|
|
48
52
|
def show_more_help():
|
|
49
53
|
help_text = f"""
|
|
@@ -66,7 +70,7 @@ def create_db(backup_def: str, config_settings: ConfigSettings):
|
|
|
66
70
|
else:
|
|
67
71
|
logger.info(f'Create catalog database: "{database_path}"')
|
|
68
72
|
command = ['dar_manager', '--create' , database_path]
|
|
69
|
-
process =
|
|
73
|
+
process = runner.run(command)
|
|
70
74
|
logger.debug(f"return code from 'db created': {process.returncode}")
|
|
71
75
|
if process.returncode == 0:
|
|
72
76
|
logger.info(f'Database created: "{database_path}"')
|
|
@@ -104,7 +108,7 @@ def list_catalogs(backup_def: str, config_settings: ConfigSettings) -> NamedTupl
|
|
|
104
108
|
command=[])
|
|
105
109
|
return commandResult
|
|
106
110
|
command = ['dar_manager', '--base', database_path, '--list']
|
|
107
|
-
process =
|
|
111
|
+
process = runner.run(command)
|
|
108
112
|
stdout, stderr = process.stdout, process.stderr
|
|
109
113
|
if process.returncode != 0:
|
|
110
114
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
@@ -156,7 +160,7 @@ def list_archive_contents(archive: str, config_settings: ConfigSettings) -> int
|
|
|
156
160
|
logger.error(f"archive: '{archive}' not found in database: '{database_path}'")
|
|
157
161
|
return 1
|
|
158
162
|
command = ['dar_manager', '--base', database_path, '-u', f"{cat_no}"]
|
|
159
|
-
process =
|
|
163
|
+
process = runner.run(command)
|
|
160
164
|
stdout, stderr = process.stdout, process.stderr
|
|
161
165
|
if process.returncode != 0:
|
|
162
166
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
@@ -178,7 +182,7 @@ def list_catalog_contents(catalog_number: int, backup_def: str, config_settings:
|
|
|
178
182
|
logger.error(f'Catalog database not found: "{database_path}"')
|
|
179
183
|
return 1
|
|
180
184
|
command = ['dar_manager', '--base', database_path, '-u', f"{catalog_number}"]
|
|
181
|
-
process =
|
|
185
|
+
process = runner.run(command)
|
|
182
186
|
stdout, stderr = process.stdout, process.stderr
|
|
183
187
|
if process.returncode != 0:
|
|
184
188
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
@@ -199,7 +203,7 @@ def find_file(file, backup_def, config_settings):
|
|
|
199
203
|
logger.error(f'Database not found: "{database_path}"')
|
|
200
204
|
return 1
|
|
201
205
|
command = ['dar_manager', '--base', database_path, '-f', f"{file}"]
|
|
202
|
-
process =
|
|
206
|
+
process = runner.run(command)
|
|
203
207
|
stdout, stderr = process.stdout, process.stderr
|
|
204
208
|
if process.returncode != 0:
|
|
205
209
|
logger.error(f'Error finding file: {file} in: "{database_path}"')
|
|
@@ -234,7 +238,7 @@ def add_specific_archive(archive: str, config_settings: ConfigSettings, director
|
|
|
234
238
|
logger.info(f'Add "{archive_path}" to catalog: "{database}"')
|
|
235
239
|
|
|
236
240
|
command = ['dar_manager', '--base', database_path, "--add", archive_path, "-Q"]
|
|
237
|
-
process =
|
|
241
|
+
process = runner.run(command)
|
|
238
242
|
stdout, stderr = process.stdout, process.stderr
|
|
239
243
|
|
|
240
244
|
if process.returncode == 0:
|
|
@@ -338,7 +342,7 @@ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> in
|
|
|
338
342
|
cat_no:int = cat_no_for_name(archive, config_settings)
|
|
339
343
|
if cat_no >= 0:
|
|
340
344
|
command = ['dar_manager', '--base', database_path, "--delete", str(cat_no)]
|
|
341
|
-
process: CommandResult =
|
|
345
|
+
process: CommandResult = runner.run(command)
|
|
342
346
|
logger.info(f"CommandResult: {process}")
|
|
343
347
|
else:
|
|
344
348
|
logger.warning(f"archive: '{archive}' not found in it's catalog database: {database_path}")
|
|
@@ -355,7 +359,7 @@ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> in
|
|
|
355
359
|
|
|
356
360
|
|
|
357
361
|
def main():
|
|
358
|
-
global logger
|
|
362
|
+
global logger, runner
|
|
359
363
|
|
|
360
364
|
MIN_PYTHON_VERSION = (3, 9)
|
|
361
365
|
if sys.version_info < MIN_PYTHON_VERSION:
|
|
@@ -404,6 +408,8 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
404
408
|
# command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
|
|
405
409
|
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
406
410
|
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
411
|
+
command_logger = get_logger(command_output_logger = True)
|
|
412
|
+
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
407
413
|
|
|
408
414
|
|
|
409
415
|
start_time=int(time())
|
dar_backup/util.py
CHANGED
|
@@ -97,109 +97,6 @@ def get_logger(command_output_logger: bool = False) -> logging.Logger:
|
|
|
97
97
|
return secondary_logger if command_output_logger else logger
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
def _stream_reader(pipe, log_funcs, output_accumulator: List[str]):
|
|
102
|
-
"""
|
|
103
|
-
Reads lines from the subprocess pipe and logs them to multiple destinations.
|
|
104
|
-
"""
|
|
105
|
-
with pipe:
|
|
106
|
-
for line in iter(pipe.readline, ''):
|
|
107
|
-
stripped_line = line.strip()
|
|
108
|
-
output_accumulator.append(stripped_line)
|
|
109
|
-
for log_func in log_funcs:
|
|
110
|
-
log_func(stripped_line) # Log the output in real-time
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def run_command(command: List[str], timeout: int = 30, no_output_log: bool = False):
|
|
115
|
-
"""
|
|
116
|
-
Executes a command and streams output only to the secondary log unless no_log is set to True.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
A CommandResult NamedTuple with the following properties:
|
|
121
|
-
- process: subprocess.CompletedProcess
|
|
122
|
-
- stdout: str: The full standard output of the command.
|
|
123
|
-
- stderr: str: The full standard error of the command.
|
|
124
|
-
- returncode: int: The return code of the command.
|
|
125
|
-
- timeout: int: The timeout value in seconds used to run the command.
|
|
126
|
-
- command: list[str]: The command executed.
|
|
127
|
-
|
|
128
|
-
Logs:
|
|
129
|
-
- Logs standard output (`stdout`) and standard error in real-time to the
|
|
130
|
-
logger.secondary_log (that contains the command output).
|
|
131
|
-
|
|
132
|
-
Raises:
|
|
133
|
-
subprocess.TimeoutExpired: If the command execution times out (see `timeout` parameter).
|
|
134
|
-
Exception: If other exceptions occur during command execution.
|
|
135
|
-
FileNotFoundError: If the command is not found.
|
|
136
|
-
"""
|
|
137
|
-
stdout_lines, stderr_lines = [], []
|
|
138
|
-
process = None
|
|
139
|
-
stdout_thread, stderr_thread = None, None
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
logger = get_logger(command_output_logger=False)
|
|
143
|
-
command_logger = get_logger(command_output_logger=True)
|
|
144
|
-
|
|
145
|
-
if not shutil.which(command[0]):
|
|
146
|
-
raise FileNotFoundError(f"Command not found: {command[0]}")
|
|
147
|
-
|
|
148
|
-
logger.debug(f"Running command: {command}")
|
|
149
|
-
command_logger.info(f"Running command: {command}")
|
|
150
|
-
|
|
151
|
-
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
152
|
-
|
|
153
|
-
log_funcs = [command_logger.info] if not no_output_log else []
|
|
154
|
-
err_log_funcs = [command_logger.error] if not no_output_log else []
|
|
155
|
-
|
|
156
|
-
stdout_thread = threading.Thread(target=_stream_reader, args=(process.stdout, log_funcs, stdout_lines))
|
|
157
|
-
stderr_thread = threading.Thread(target=_stream_reader, args=(process.stderr, err_log_funcs, stderr_lines))
|
|
158
|
-
|
|
159
|
-
stdout_thread.start()
|
|
160
|
-
stderr_thread.start()
|
|
161
|
-
|
|
162
|
-
process.wait(timeout=timeout)
|
|
163
|
-
|
|
164
|
-
except FileNotFoundError as e:
|
|
165
|
-
logger.error(f"Command not found: {command[0]}")
|
|
166
|
-
return CommandResult(
|
|
167
|
-
process=None,
|
|
168
|
-
stdout="",
|
|
169
|
-
stderr=str(e),
|
|
170
|
-
returncode=127,
|
|
171
|
-
timeout=timeout,
|
|
172
|
-
command=command
|
|
173
|
-
)
|
|
174
|
-
except subprocess.TimeoutExpired:
|
|
175
|
-
if process:
|
|
176
|
-
process.terminate()
|
|
177
|
-
logger.error(f"Command: '{command}' timed out and was terminated.")
|
|
178
|
-
raise
|
|
179
|
-
except Exception as e:
|
|
180
|
-
logger.error(f"Error running command: {command}", exc_info=True)
|
|
181
|
-
raise
|
|
182
|
-
finally:
|
|
183
|
-
if stdout_thread and stdout_thread.is_alive():
|
|
184
|
-
stdout_thread.join()
|
|
185
|
-
if stderr_thread and stderr_thread.is_alive():
|
|
186
|
-
stderr_thread.join()
|
|
187
|
-
if process:
|
|
188
|
-
if process.stdout:
|
|
189
|
-
process.stdout.close()
|
|
190
|
-
if process.stderr:
|
|
191
|
-
process.stderr.close()
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# Combine captured stdout and stderr lines into single strings
|
|
195
|
-
stdout = "\n".join(stdout_lines)
|
|
196
|
-
stderr = "\n".join(stderr_lines)
|
|
197
|
-
|
|
198
|
-
#Build the result object
|
|
199
|
-
result = CommandResult(process=process, stdout=stdout, stderr=stderr, returncode=process.returncode, timeout=timeout, command=command)
|
|
200
|
-
return result
|
|
201
|
-
|
|
202
|
-
|
|
203
100
|
class BackupError(Exception):
|
|
204
101
|
"""Exception raised for errors in the backup process."""
|
|
205
102
|
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dar-backup
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.17
|
|
4
4
|
Summary: A script to do full, differential and incremental backups using dar. Some files are restored from the backups during verification, after which par2 redundancy files are created. The script also has a cleanup feature to remove old backups and par2 files.
|
|
5
5
|
Project-URL: Homepage, https://github.com/per2jensen/dar-backup/tree/main/v2
|
|
6
6
|
Project-URL: Changelog, https://github.com/per2jensen/dar-backup/blob/main/v2/Changelog.md
|
|
@@ -688,8 +688,10 @@ Classifier: Operating System :: POSIX :: Linux
|
|
|
688
688
|
Classifier: Programming Language :: Python :: 3.9
|
|
689
689
|
Classifier: Topic :: System :: Archiving :: Backup
|
|
690
690
|
Requires-Python: >=3.9
|
|
691
|
+
Requires-Dist: inputimeout>=1.0.4
|
|
691
692
|
Description-Content-Type: text/markdown
|
|
692
693
|
|
|
694
|
+
<!-- markdownlint-disable MD024 -->
|
|
693
695
|
# Full, differential or incremental backups using 'dar'
|
|
694
696
|
|
|
695
697
|
The wonderful 'dar' [Disk Archiver](https://github.com/Edrusb/DAR) is used for
|
|
@@ -706,17 +708,20 @@ This is the `Python` based **version 2** of `dar-backup`.
|
|
|
706
708
|
- [Breaking change in version 0.6.0](#breaking-change-in-version-060)
|
|
707
709
|
- [Homepage - Github](#homepage---github)
|
|
708
710
|
- [Requirements](#requirements)
|
|
709
|
-
- [
|
|
711
|
+
- [Principles](#dar-backup-principles)
|
|
710
712
|
- [How to run](#how-to-run)
|
|
711
713
|
- [1 - installation](#1---installation)
|
|
712
714
|
- [2 - configuration](#2---configuration)
|
|
713
715
|
- [3 - generate catalog databases](#3---generate-catalog-databases)
|
|
714
716
|
- [4 - do FULL backups](#4---do-full-backups)
|
|
715
717
|
- [5 - deactivate venv](#5---deactivate-venv)
|
|
716
|
-
- [
|
|
717
|
-
- [
|
|
718
|
-
- [
|
|
719
|
-
- [
|
|
718
|
+
- [Config](#config)
|
|
719
|
+
- [Config file](#config-file)
|
|
720
|
+
- [.darrc](#darrc)
|
|
721
|
+
- [Backup definition](#backup-definition-example)
|
|
722
|
+
- [Systemd examples](#systemctl-examples)
|
|
723
|
+
- [Service: dar-back --incremental-backup](#service-dar-backup---incremental-backup)
|
|
724
|
+
- [Timer: dar-back --incremental-backup](#timer-dar-backup---incremental-backup)
|
|
720
725
|
- [List contents of an archive](#list-contents-of-an-archive)
|
|
721
726
|
- [dar file selection examples](#dar-file-selection-examples)
|
|
722
727
|
- [Select a directory](#select-a-directory)
|
|
@@ -739,13 +744,15 @@ This is the `Python` based **version 2** of `dar-backup`.
|
|
|
739
744
|
- [Performance tip due to par2](#performance-tip-due-to-par2)
|
|
740
745
|
- [.darrc sets -vd -vf (since v0.6.4)](#darrc-sets--vd--vf-since-v064)
|
|
741
746
|
- [Separate log file for command output](#separate-log-file-for-command-output)
|
|
747
|
+
- [Skipping cache directories](#skipping-cache-directories)
|
|
742
748
|
- [Todo](#todo)
|
|
743
749
|
- [Reference](#reference)
|
|
744
|
-
- [
|
|
745
|
-
- [
|
|
746
|
-
- [
|
|
747
|
-
- [
|
|
748
|
-
- [
|
|
750
|
+
- [Test coverage report](#test-coverage)
|
|
751
|
+
- [dar-backup](#dar-backup-options)
|
|
752
|
+
- [manager](#manager-options)
|
|
753
|
+
- [cleanup](#cleanup-options)
|
|
754
|
+
- [clean-log](#clean-log-options)
|
|
755
|
+
- [installer](#installer-options)
|
|
749
756
|
|
|
750
757
|
## My use case
|
|
751
758
|
|
|
@@ -782,9 +789,9 @@ Version 0.6.0 and forwards requires the config variable *COMMAND_TIMEOUT_SECS* i
|
|
|
782
789
|
|
|
783
790
|
## Homepage - Github
|
|
784
791
|
|
|
785
|
-
|
|
792
|
+
'dar-backup' package lives here: [Github - dar-backup](https://github.com/per2jensen/dar-backup/tree/main/v2)
|
|
786
793
|
|
|
787
|
-
This python version is v2 of dar-backup,
|
|
794
|
+
This python version is v2 of dar-backup, v1 is made in bash.
|
|
788
795
|
|
|
789
796
|
## Requirements
|
|
790
797
|
|
|
@@ -798,9 +805,40 @@ On Ubuntu, install the requirements this way:
|
|
|
798
805
|
sudo apt install dar par2 python3
|
|
799
806
|
````
|
|
800
807
|
|
|
801
|
-
##
|
|
808
|
+
## dar-backup principles
|
|
802
809
|
|
|
803
|
-
|
|
810
|
+
### dar-backup
|
|
811
|
+
|
|
812
|
+
`dar-backup` is built in a way that emphasizes getting backups. It loops over the [backup definitions](#backup-definition-example), and in the event of a failure while backing up a backup definition, dar-backup shall log an error and start working on the next backup definition.
|
|
813
|
+
|
|
814
|
+
There are 3 levels of backups, FULL, DIFF and INCR.
|
|
815
|
+
|
|
816
|
+
- The author does a FULL yearly backup once a year. This includes all files in all directories as defined in the backup definition(s) (assuming `-d` was not given).
|
|
817
|
+
- The author makes a DIFF once a month. The DIFF backs up new and changed files **compared** to the **FULL** backup.
|
|
818
|
+
|
|
819
|
+
- No DIFF backups are taken until a FULL backup has been taken for a particular backup definition.
|
|
820
|
+
|
|
821
|
+
- The author takes an INCR backup every 3 days. An INCR backup includes new and changed files **compared** to the **DIFF** backup.
|
|
822
|
+
|
|
823
|
+
- So, a set of INCR's will contain duplicates (this might change as I become more used to use the catalog databases)
|
|
824
|
+
|
|
825
|
+
- No INCR backups are taken until a DIFF backup has been taken for a particular backup definition.
|
|
826
|
+
|
|
827
|
+
After each backup of a backup definition, `dar-backup` tests the archive and then performs a few restore operations of random files from the archive (see [dar-backup.conf](#config-file)). The restored files are compared to the originals to check if the restore went well.
|
|
828
|
+
|
|
829
|
+
`dar-backup` skips doing a backup of a backup definition if an archive is already in place. So, if you for some reason need to take a new backup on the same date, the first archive must be deleted (I recommend using [cleanup](#cleanup-1)).
|
|
830
|
+
|
|
831
|
+
### cleanup
|
|
832
|
+
|
|
833
|
+
The `cleanup` application deletes DIFF and INCR if the archives are older than the thresholds set up in the configuration file.
|
|
834
|
+
|
|
835
|
+
`cleanup` will only remove FULL archives if the option `--cleanup-specific-archives` is used. It requires the user to confirm deletion of FULL archives.
|
|
836
|
+
|
|
837
|
+
### manager
|
|
838
|
+
|
|
839
|
+
`dar`has the concept of catalogs which can be exported and optionally be added to a catalog database. That database makes it much easier to restore the correct version of a backed up file if for example a target date has been set.
|
|
840
|
+
|
|
841
|
+
`dar-backup` adds archive catalogs to their databases (using the `manager` script). Should the operation fail, `dar-backup` logs an error and continue with testing and restore validation tests.
|
|
804
842
|
|
|
805
843
|
## How to run
|
|
806
844
|
|
|
@@ -814,6 +852,10 @@ Installation is currently in a venv. These commands are installed in the venv:
|
|
|
814
852
|
- clean-log
|
|
815
853
|
- installer
|
|
816
854
|
|
|
855
|
+
Note:
|
|
856
|
+
|
|
857
|
+
The module `inputimeout` is installed into the venv and used for the confirmation input (with a 30 second timeout)
|
|
858
|
+
|
|
817
859
|
To install, create a venv and run pip:
|
|
818
860
|
|
|
819
861
|
```` bash
|
|
@@ -875,16 +917,18 @@ manager --create-db
|
|
|
875
917
|
|
|
876
918
|
### 4 - do FULL backups
|
|
877
919
|
|
|
878
|
-
|
|
879
|
-
in place in BACKUP.D_DIR (see config file)
|
|
920
|
+
Prereq:
|
|
921
|
+
[Backup definitions](#backup-definition-example) are in place in BACKUP.D_DIR (see [config file](#config-file)).
|
|
922
|
+
|
|
923
|
+
You are ready to do backups of all your backup definitions.
|
|
880
924
|
|
|
881
925
|
```` bash
|
|
882
926
|
dar-backup --full-backup
|
|
883
927
|
````
|
|
884
928
|
|
|
885
|
-
If you want to see dar-backup's log entries in the terminal, use the `--log-stdout` option. This
|
|
929
|
+
If you want to see dar-backup's log entries in the terminal, use the `--log-stdout` option. This can be useful if dar-backup is started by systemd.
|
|
886
930
|
|
|
887
|
-
If you want more log messages, use the `--log-level debug`
|
|
931
|
+
If you want more log messages, use the `--verbose` or `--log-level debug` for even more.
|
|
888
932
|
|
|
889
933
|
If you want a backup of a single definition, use the `-d <backup definition>` option. The definition's name is the filename of the definition in the `backup.d` config directory.
|
|
890
934
|
|
|
@@ -900,9 +944,54 @@ Deactivate the virtual environment (venv)
|
|
|
900
944
|
deactivate
|
|
901
945
|
````
|
|
902
946
|
|
|
903
|
-
##
|
|
947
|
+
## Config
|
|
948
|
+
|
|
949
|
+
### Config file
|
|
950
|
+
|
|
951
|
+
The configuration file's default location is: ~/.config/dar-backup/dar-backup.conf
|
|
952
|
+
|
|
953
|
+
If you have your config file somewhere else, use the `--config` option to point to it.
|
|
954
|
+
|
|
955
|
+
Tilde `~` and environment variables can be used in the paths for various file locations.
|
|
956
|
+
|
|
957
|
+
```` code
|
|
958
|
+
[MISC]
|
|
959
|
+
LOGFILE_LOCATION=~/.dar-backup.log
|
|
960
|
+
MAX_SIZE_VERIFICATION_MB = 20
|
|
961
|
+
MIN_SIZE_VERIFICATION_MB = 1
|
|
962
|
+
NO_FILES_VERIFICATION = 5
|
|
963
|
+
# timeout in seconds for backup, test, restore and par2 operations
|
|
964
|
+
# The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
|
|
965
|
+
# If a timeout is not specified when using the util.run_command(), a default timeout of 30 secs is used.
|
|
966
|
+
COMMAND_TIMEOUT_SECS = 86400
|
|
967
|
+
|
|
968
|
+
[DIRECTORIES]
|
|
969
|
+
BACKUP_DIR = /some/where/dar-backup/backups/
|
|
970
|
+
BACKUP.D_DIR = /some/where/dar-backup/backup.d
|
|
971
|
+
TEST_RESTORE_DIR = /tmp/dar-backup/restore/
|
|
972
|
+
|
|
973
|
+
[AGE]
|
|
974
|
+
# age settings are in days
|
|
975
|
+
DIFF_AGE = 100
|
|
976
|
+
INCR_AGE = 40
|
|
977
|
+
|
|
978
|
+
[PAR2]
|
|
979
|
+
ERROR_CORRECTION_PERCENT = 5
|
|
980
|
+
ENABLED = True
|
|
981
|
+
|
|
982
|
+
# scripts to run before the backup to setup the environment
|
|
983
|
+
[PREREQ]
|
|
984
|
+
SCRIPT_1 = ls -l /tmp
|
|
985
|
+
#SCRIPT_2 = another_script.sh
|
|
986
|
+
|
|
987
|
+
[POSTREQ]
|
|
988
|
+
SCRIPT_1 = df -h
|
|
989
|
+
#SCRIPT_2 = another_script.sh
|
|
990
|
+
````
|
|
991
|
+
|
|
992
|
+
### .darrc
|
|
904
993
|
|
|
905
|
-
The package includes a default
|
|
994
|
+
The package includes a default `darrc` file which configures `dar`.
|
|
906
995
|
|
|
907
996
|
You can override the default `.darrc` using the `--darrc` option.
|
|
908
997
|
|
|
@@ -1031,6 +1120,49 @@ compress-exclusion:
|
|
|
1031
1120
|
-acase
|
|
1032
1121
|
````
|
|
1033
1122
|
|
|
1123
|
+
### Backup definition example
|
|
1124
|
+
|
|
1125
|
+
This piece of configuration is a [backup definition](#backup-definition-example). It is placed in the BACKUP.D_DIR (see config file description).
|
|
1126
|
+
The name of the file is the name of the backup definition.
|
|
1127
|
+
|
|
1128
|
+
You can use as many backup definitions as you need.
|
|
1129
|
+
|
|
1130
|
+
```` code
|
|
1131
|
+
# Switch to ordered selection mode, which means that the following
|
|
1132
|
+
# options will be considered top to bottom
|
|
1133
|
+
-am
|
|
1134
|
+
|
|
1135
|
+
# Backup Root Dir
|
|
1136
|
+
# This is the top directory, where the backups start.
|
|
1137
|
+
#Directories mentioned below, are relative to the Root Dir.
|
|
1138
|
+
-R /home/user/
|
|
1139
|
+
|
|
1140
|
+
# Directories to backup below the Root dir
|
|
1141
|
+
# uncomment the next line to backup only the Documents directory
|
|
1142
|
+
# -g Documents
|
|
1143
|
+
|
|
1144
|
+
# Directories to exclude below the Root dir
|
|
1145
|
+
-P mnt
|
|
1146
|
+
-P tmp
|
|
1147
|
+
-P .cache
|
|
1148
|
+
-P .config/Code/CachedData
|
|
1149
|
+
|
|
1150
|
+
# compression level
|
|
1151
|
+
-z5
|
|
1152
|
+
|
|
1153
|
+
# no overwrite, if you rerun a backup, 'dar' halts and asks what to do
|
|
1154
|
+
# due to the -Q option given to `dar`, the program will terminate and give en error.
|
|
1155
|
+
-n
|
|
1156
|
+
|
|
1157
|
+
# size of each slice in the archive
|
|
1158
|
+
--slice 7G
|
|
1159
|
+
|
|
1160
|
+
# bypass directores marked as cache directories
|
|
1161
|
+
# http://dar.linux.free.fr/doc/Features.html
|
|
1162
|
+
# https://bford.info/cachedir/
|
|
1163
|
+
--cache-directory-tagging
|
|
1164
|
+
````
|
|
1165
|
+
|
|
1034
1166
|
## Systemctl examples
|
|
1035
1167
|
|
|
1036
1168
|
I have dar-backup scheduled to run via systemd --user settings.
|
|
@@ -1051,7 +1183,7 @@ Verify your timers are set up as you want:
|
|
|
1051
1183
|
systemctl --user list-timers
|
|
1052
1184
|
````
|
|
1053
1185
|
|
|
1054
|
-
## Service: dar-
|
|
1186
|
+
## Service: dar-backup --incremental-backup
|
|
1055
1187
|
|
|
1056
1188
|
File: dar-inc-backup.service
|
|
1057
1189
|
|
|
@@ -1067,7 +1199,7 @@ RemainAfterExit=no
|
|
|
1067
1199
|
ExecStart=/bin/bash -c '. /home/user/programmer/dar-backup.py/venv/bin/activate && dar-backup --incremental-backup --verbose'
|
|
1068
1200
|
````
|
|
1069
1201
|
|
|
1070
|
-
## Timer: dar-
|
|
1202
|
+
## Timer: dar-backup --incremental-backup
|
|
1071
1203
|
|
|
1072
1204
|
File: dar-inc-backup.timer
|
|
1073
1205
|
|
|
@@ -1252,7 +1384,7 @@ deactivate
|
|
|
1252
1384
|
|
|
1253
1385
|
"dar" in newer versions emits a question about file ownership, which is "answered" with a "no" via the "-Q" option. That in turn leads to an error code 4.
|
|
1254
1386
|
|
|
1255
|
-
Thus the dar option "--comparison-field=ignore-owner" has been placed in the supplied .darrc file (located in the virtual environment where dar-backup is installed).
|
|
1387
|
+
Thus the dar option "--comparison-field=ignore-owner" has been placed in the supplied [.darrc](#darrc) file (located in the virtual environment where dar-backup is installed).
|
|
1256
1388
|
|
|
1257
1389
|
This causes dar to restore without an error.
|
|
1258
1390
|
|
|
@@ -1267,7 +1399,7 @@ My home directory is on a btrfs filesystem, while /tmp (for the restore test) is
|
|
|
1267
1399
|
|
|
1268
1400
|
The restore test can result in an exit code 5, due to the different filesystems used. In order to avoid the errors, the "option "--fsa-scope none" can be used. That will restult in FSA's not being restored.
|
|
1269
1401
|
|
|
1270
|
-
If you need to use this option, un-comment it in the .darrc file (located in the virtual environment where dar-backup is installed)
|
|
1402
|
+
If you need to use this option, un-comment it in the [.darrc](#darrc) file (located in the virtual environment where dar-backup is installed)
|
|
1271
1403
|
|
|
1272
1404
|
## Par2
|
|
1273
1405
|
|
|
@@ -1328,26 +1460,35 @@ done
|
|
|
1328
1460
|
|
|
1329
1461
|
This makes it easier to restore to a given date when having many FULL, DIFF and INCR archives.
|
|
1330
1462
|
|
|
1463
|
+
If the manager does not add an archive to it's catalog database, `dar-backup` will log an error and continue. The important part is verify the archive is usable and continue to other backup definitions.
|
|
1464
|
+
|
|
1331
1465
|
### Performance tip due to par2
|
|
1332
1466
|
|
|
1333
1467
|
This [dar benchmark page](https://dar.sourceforge.io/doc/benchmark.html) has an interesting note on the slice size.
|
|
1334
1468
|
|
|
1335
|
-
Slice size should be smaller than available RAM, apparently a large performance hit can be avoided keeping the
|
|
1469
|
+
Slice size should be smaller than available RAM, apparently a large performance hit can be avoided keeping the par2 data in memory.
|
|
1336
1470
|
|
|
1337
1471
|
### .darrc sets -vd -vf (since v0.6.4)
|
|
1338
1472
|
|
|
1339
|
-
These .darrc settings make `dar` print the current directory being processed (-vd) and some stats after (-vf)
|
|
1340
|
-
This is very useful in very long running jobs to get an indication that the backup is proceeding normally.
|
|
1341
|
-
|
|
1342
|
-
if --log-stdout is used the information would be picked up by systemd and logged by journald.½
|
|
1473
|
+
These [.darrc](#darrc) settings make `dar` print the current directory being processed (-vd) and some stats after (-vf)
|
|
1343
1474
|
|
|
1344
|
-
|
|
1475
|
+
This is very useful in very long running jobs to get an indication that the backup is proceeding normally.
|
|
1345
1476
|
|
|
1346
1477
|
### Separate log file for command output
|
|
1347
1478
|
|
|
1348
1479
|
Dar-backup's log file is called `dar-backup.log`.
|
|
1349
1480
|
|
|
1350
|
-
In order to not clutter that log file with the output of commands being run, a new log file has been introduced `dar-backup-commands.log`.
|
|
1481
|
+
In order to not clutter that log file with the output of commands being run, a new secondary log file has been introduced `dar-backup-commands.log`.
|
|
1482
|
+
|
|
1483
|
+
The secondary log file can get quite cluttered, if you want to remove the clutter, run the `clean-log`script with the `--file` option, or simply delete it.
|
|
1484
|
+
|
|
1485
|
+
### Skipping cache directories
|
|
1486
|
+
|
|
1487
|
+
The author uses the `--cache-directory-tagging` option in his [backup definitions](#backup-definition-example).
|
|
1488
|
+
|
|
1489
|
+
The effect is that directories with the [CACHEDIR.TAG](https://bford.info/cachedir/) file are not backed up. Those directories contain content fetched from the net, which is of an ephemeral nature and probably not what you want to back up.
|
|
1490
|
+
|
|
1491
|
+
If the option is not in the backup definition, the cache directories are backed up as any other.
|
|
1351
1492
|
|
|
1352
1493
|
## Todo
|
|
1353
1494
|
|
|
@@ -1355,84 +1496,113 @@ In order to not clutter that log file with the output of commands being run, a n
|
|
|
1355
1496
|
- FULL, DIFF and INCR backups.
|
|
1356
1497
|
- cleanup.
|
|
1357
1498
|
|
|
1358
|
-
-
|
|
1499
|
+
- Add option to dar-backup to use the `dar` option `--fsa-scope none`
|
|
1359
1500
|
|
|
1360
1501
|
## Reference
|
|
1361
1502
|
|
|
1362
|
-
###
|
|
1503
|
+
### test coverage
|
|
1504
|
+
|
|
1505
|
+
Running
|
|
1506
|
+
|
|
1507
|
+
```` bash
|
|
1508
|
+
pytest --cov=dar_backup tests/
|
|
1509
|
+
````
|
|
1363
1510
|
|
|
1364
|
-
|
|
1511
|
+
gives for version 0.6.17:
|
|
1512
|
+
|
|
1513
|
+
```` code
|
|
1514
|
+
---------- coverage: platform linux, python 3.12.3-final-0 -----------
|
|
1515
|
+
Name Stmts Miss Cover
|
|
1516
|
+
-------------------------------------------------------------------------------------
|
|
1517
|
+
venv/lib/python3.12/site-packages/dar_backup/__about__.py 1 0 100%
|
|
1518
|
+
venv/lib/python3.12/site-packages/dar_backup/__init__.py 0 0 100%
|
|
1519
|
+
venv/lib/python3.12/site-packages/dar_backup/clean_log.py 68 14 79%
|
|
1520
|
+
venv/lib/python3.12/site-packages/dar_backup/cleanup.py 196 53 73%
|
|
1521
|
+
venv/lib/python3.12/site-packages/dar_backup/config_settings.py 66 8 88%
|
|
1522
|
+
venv/lib/python3.12/site-packages/dar_backup/dar_backup.py 464 99 79%
|
|
1523
|
+
venv/lib/python3.12/site-packages/dar_backup/installer.py 46 46 0%
|
|
1524
|
+
venv/lib/python3.12/site-packages/dar_backup/manager.py 316 72 77%
|
|
1525
|
+
venv/lib/python3.12/site-packages/dar_backup/util.py 162 34 79%
|
|
1526
|
+
-------------------------------------------------------------------------------------
|
|
1527
|
+
TOTAL 1319 326 75%
|
|
1528
|
+
````
|
|
1529
|
+
|
|
1530
|
+
### dar-backup options
|
|
1531
|
+
|
|
1532
|
+
This script does backups, validation and restoring. It has the following options:
|
|
1365
1533
|
|
|
1366
1534
|
``` code
|
|
1367
|
-
--full-backup
|
|
1368
|
-
--differential-backup
|
|
1369
|
-
--incremental-backup
|
|
1370
|
-
--backup-definition <name>
|
|
1371
|
-
--alternate-reference-archive <file>
|
|
1372
|
-
--config-file <path>
|
|
1373
|
-
--darrc <path>
|
|
1374
|
-
--examples
|
|
1375
|
-
--list
|
|
1376
|
-
--list-contents <archive>
|
|
1377
|
-
--selection <params>
|
|
1378
|
-
--restore <archive>
|
|
1379
|
-
--restore-dir <path>
|
|
1380
|
-
--verbose
|
|
1381
|
-
--suppress-dar-msg
|
|
1382
|
-
--log-level <level>
|
|
1383
|
-
--log-stdout
|
|
1384
|
-
--do-not-compare
|
|
1385
|
-
--version
|
|
1535
|
+
-F, --full-backup Perform a full backup.
|
|
1536
|
+
-D, --differential-backup Perform a differential backup.
|
|
1537
|
+
-I, --incremental-backup Perform an incremental backup.
|
|
1538
|
+
-d, --backup-definition <name> Specify the backup definition file.
|
|
1539
|
+
--alternate-reference-archive <file> Use a different archive for DIFF/INCR backups.
|
|
1540
|
+
-c, --config-file <path> Specify the path to the configuration file.
|
|
1541
|
+
--darrc <path> Specify an optional path to .darrc.
|
|
1542
|
+
--examples Show examples of using dar-backup.py.
|
|
1543
|
+
-l, --list List available backups.
|
|
1544
|
+
--list-contents <archive> List the contents of a specified archive.
|
|
1545
|
+
--selection <params> Define file selection for listing/restoring.
|
|
1546
|
+
--restore <archive> Restore a specified archive.
|
|
1547
|
+
-r, --restore-dir <path> Directory to restore files to.
|
|
1548
|
+
--verbose Enable verbose output.
|
|
1549
|
+
--suppress-dar-msg Filter out this from the darrc: "-vt", "-vs", "-vd", "-vf", "-va"
|
|
1550
|
+
--log-level <level> `debug` or `trace`, default is `info`.
|
|
1551
|
+
--log-stdout Also print log messages to stdout.
|
|
1552
|
+
--do-not-compare Do not compare restores to file system.
|
|
1553
|
+
-v --version Show version and license information.
|
|
1386
1554
|
```
|
|
1387
1555
|
|
|
1388
|
-
### manager
|
|
1556
|
+
### manager options
|
|
1389
1557
|
|
|
1390
|
-
This script manages `dar` databases and catalogs. Available options
|
|
1558
|
+
This script manages `dar` databases and catalogs. Available options:
|
|
1391
1559
|
|
|
1392
1560
|
``` code
|
|
1393
|
-
--
|
|
1394
|
-
--
|
|
1395
|
-
--
|
|
1396
|
-
|
|
1397
|
-
--
|
|
1398
|
-
--
|
|
1399
|
-
|
|
1400
|
-
--list-
|
|
1401
|
-
--list-
|
|
1402
|
-
--
|
|
1403
|
-
--
|
|
1404
|
-
--
|
|
1561
|
+
-c, --config-file Path to dar-backup.conf
|
|
1562
|
+
--create-db Create missing databases for all backup definitions.
|
|
1563
|
+
--alternate-archive-dir <path> Use this directory instead of BACKUP_DIR in the config file.
|
|
1564
|
+
--add-dir <path> Add all archive catalogs in this directory to databases.
|
|
1565
|
+
-d, --backup-def <name> Restrict to work only on this backup definition.
|
|
1566
|
+
--add-specific-archive <archive> Add this archive to the catalog database.
|
|
1567
|
+
--remove-specific-archive <archive> Remove this archive from the catalog database.
|
|
1568
|
+
-l, --list-catalogs List catalogs in databases for all backup definitions.
|
|
1569
|
+
--list-catalog-contents <num> List contents of a catalog by catalog number.
|
|
1570
|
+
--list-archive-contents <archive> List contents of an archive’s catalog, given the archive name.
|
|
1571
|
+
--find-file <file> Search catalogs for a specific file.
|
|
1572
|
+
--verbose Enable verbose output.
|
|
1573
|
+
--log-level <level> `debug` or `trace`, default is `info`", default="info".
|
|
1405
1574
|
```
|
|
1406
1575
|
|
|
1407
|
-
### cleanup
|
|
1576
|
+
### cleanup options
|
|
1408
1577
|
|
|
1409
|
-
This script cleans up old backups and
|
|
1578
|
+
This script cleans up old backups and par2 files. Supported options:
|
|
1410
1579
|
|
|
1411
1580
|
``` code
|
|
1412
|
-
-d, --backup-definition
|
|
1413
|
-
-c, --config-file
|
|
1414
|
-
-v, --version
|
|
1415
|
-
--alternate-archive-dir
|
|
1416
|
-
--cleanup-specific-archives <archive>, ...
|
|
1417
|
-
-l,
|
|
1418
|
-
--verbose
|
|
1419
|
-
--log-level <level>
|
|
1420
|
-
--log-stdout
|
|
1581
|
+
-d, --backup-definition Backup definition to cleanup.
|
|
1582
|
+
-c, --config-file Path to 'dar-backup.conf'
|
|
1583
|
+
-v, --version Show version & license information.
|
|
1584
|
+
--alternate-archive-dir Clean up in this directory instead of the default one.
|
|
1585
|
+
--cleanup-specific-archives "<archive>, <>, ..." Comma separated list of archives to cleanup.
|
|
1586
|
+
-l, --list List available archives (filter using the -d option).
|
|
1587
|
+
--verbose Print various status messages to screen.
|
|
1588
|
+
--log-level <level> `debug` or `trace`, default is `info`", default="info".
|
|
1589
|
+
--log-stdout Print log messages to stdout.
|
|
1590
|
+
--test-mode This is used when running pytest test cases
|
|
1421
1591
|
```
|
|
1422
1592
|
|
|
1423
|
-
### clean-log
|
|
1593
|
+
### clean-log options
|
|
1424
1594
|
|
|
1425
1595
|
This script removes excessive logging output from `dar` logs, improving readability and efficiency. Available options:
|
|
1426
1596
|
|
|
1427
1597
|
``` code
|
|
1428
1598
|
-f, --file <path> Specify the log file(s) to be cleaned.
|
|
1429
|
-
-c, --config-file <path>
|
|
1599
|
+
-c, --config-file <path> Path to dar-backup.conf.
|
|
1430
1600
|
--dry-run Show which lines would be removed without modifying the file.
|
|
1431
1601
|
-v, --version Display version and licensing information.
|
|
1432
1602
|
-h, --help Displays usage info
|
|
1433
1603
|
```
|
|
1434
1604
|
|
|
1435
|
-
### installer
|
|
1605
|
+
### installer options
|
|
1436
1606
|
|
|
1437
1607
|
Sets up `dar-backup`for a user.
|
|
1438
1608
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
+
dar_backup/__about__.py,sha256=vii4GL7MExpBC8tvQjQXAsEgfxDE9p438_97wKl4XCc,22
|
|
3
|
+
dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
dar_backup/clean_log.py,sha256=cGhtKYnQJ2ceNQfw5XcCln_WNBasbmlfhO3kRydjDNk,5196
|
|
5
|
+
dar_backup/cleanup.py,sha256=HA8SmwrqDGjfFg4L0CuUQ5N0YJFT6qYbCuvh5mQseF0,13148
|
|
6
|
+
dar_backup/command_runner.py,sha256=74Fsylz1NN-dn8lbdRhkL6LA1r527QJeojBlniGrPuo,2708
|
|
7
|
+
dar_backup/config_settings.py,sha256=Rh4T35-w_5tpRAViMfv3YP3GBpG4mQy7Do8cNBzYAR0,4912
|
|
8
|
+
dar_backup/dar-backup.conf,sha256=64O3bGlzqupneT2gVeaETJ1qS6-3Exet9Zto27jgwPQ,897
|
|
9
|
+
dar_backup/dar_backup.py,sha256=NHBm3zsOhCHnCVoPO0ysD3uMdIQMe62AIdr0yCQ_6BY,37952
|
|
10
|
+
dar_backup/installer.py,sha256=ehp4KSgTc8D9Edsyve5v3NY2MuDbuTFYQQPgou8woV8,4331
|
|
11
|
+
dar_backup/manager.py,sha256=4NeIVgrhIzOS8UePUCdvtswEG55ue0tXWAK7SjD3tpo,21897
|
|
12
|
+
dar_backup/util.py,sha256=6dJXFOjIIZqerbNVFxJZ6gQ4ZVAxyY-RxHcO--9bxwg,8462
|
|
13
|
+
dar_backup-0.6.17.dist-info/METADATA,sha256=-TmZ95gGD9VX1Uz0so4G8sWWX0RE6Aq8qkfKThkDFnY,79979
|
|
14
|
+
dar_backup-0.6.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
dar_backup-0.6.17.dist-info/entry_points.txt,sha256=Z7P5BUbhtJxo8_nB9qNIMay2eGDbsMKB3Fjwv3GMa4g,202
|
|
16
|
+
dar_backup-0.6.17.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
17
|
+
dar_backup-0.6.17.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
-
dar_backup/__about__.py,sha256=9pegTLVQP2o3JePGrLB6muzXif64umv9ihLnS7LsH8E,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=1g2si9-jPEL8T4OKaiGSKFGsF6rWh-Ke1-zQHE7HaPc,11703
|
|
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=TrYMYNbQ9jWabWGc7GA5p2ezprLqMUu9O0zt6CQ1QSA,37867
|
|
9
|
-
dar_backup/installer.py,sha256=ehp4KSgTc8D9Edsyve5v3NY2MuDbuTFYQQPgou8woV8,4331
|
|
10
|
-
dar_backup/manager.py,sha256=sQl0xdWwBgui11S9Ekg0hOSC4gt89nz_Z8Bt8IPXCDw,21640
|
|
11
|
-
dar_backup/util.py,sha256=F6U-e-WugxCxLPVoiWsM6_YO8VrDw1wdgGvtnGnig2I,12279
|
|
12
|
-
dar_backup-0.6.15.dist-info/METADATA,sha256=JwTNDXCElF0elUKBhwjrDUASEkpLIjgdtfEML5NvXUY,72750
|
|
13
|
-
dar_backup-0.6.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
-
dar_backup-0.6.15.dist-info/entry_points.txt,sha256=Z7P5BUbhtJxo8_nB9qNIMay2eGDbsMKB3Fjwv3GMa4g,202
|
|
15
|
-
dar_backup-0.6.15.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
16
|
-
dar_backup-0.6.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|