dar-backup 0.6.18__py3-none-any.whl → 0.6.20__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/Changelog.md +31 -0
- dar_backup/README.md +49 -14
- dar_backup/__about__.py +1 -1
- dar_backup/clean_log.py +14 -7
- dar_backup/cleanup.py +29 -24
- dar_backup/command_runner.py +59 -9
- dar_backup/config_settings.py +83 -53
- dar_backup/dar-backup.conf +4 -0
- dar_backup/dar_backup.py +56 -35
- dar_backup/demo.py +138 -0
- dar_backup/exceptions.py +3 -0
- dar_backup/installer.py +49 -129
- dar_backup/manager.py +214 -90
- dar_backup/util.py +345 -3
- {dar_backup-0.6.18.dist-info → dar_backup-0.6.20.dist-info}/METADATA +51 -15
- dar_backup-0.6.20.dist-info/RECORD +23 -0
- {dar_backup-0.6.18.dist-info → dar_backup-0.6.20.dist-info}/entry_points.txt +1 -1
- dar_backup-0.6.18.dist-info/RECORD +0 -21
- {dar_backup-0.6.18.dist-info → dar_backup-0.6.20.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.18.dist-info → dar_backup-0.6.20.dist-info}/licenses/LICENSE +0 -0
dar_backup/util.py
CHANGED
|
@@ -9,6 +9,7 @@ See section 15 and section 16 in the supplied "LICENSE" file
|
|
|
9
9
|
"""
|
|
10
10
|
import typing
|
|
11
11
|
import locale
|
|
12
|
+
import configparser
|
|
12
13
|
import logging
|
|
13
14
|
import os
|
|
14
15
|
import re
|
|
@@ -18,10 +19,17 @@ import shutil
|
|
|
18
19
|
import sys
|
|
19
20
|
import threading
|
|
20
21
|
import traceback
|
|
22
|
+
from argcomplete.completers import ChoicesCompleter
|
|
21
23
|
from datetime import datetime
|
|
22
24
|
from dar_backup.config_settings import ConfigSettings
|
|
25
|
+
import dar_backup.__about__ as about
|
|
26
|
+
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
from rich.text import Text
|
|
23
29
|
|
|
24
30
|
from typing import NamedTuple, List
|
|
31
|
+
from typing import Tuple
|
|
32
|
+
|
|
25
33
|
|
|
26
34
|
logger=None
|
|
27
35
|
secondary_logger=None
|
|
@@ -72,11 +80,9 @@ def setup_logging(log_file: str, command_output_log_file: str, log_level: str =
|
|
|
72
80
|
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
73
81
|
stdout_handler.setFormatter(formatter)
|
|
74
82
|
logger.addHandler(stdout_handler)
|
|
75
|
-
#secondary_logger.addHandler(stdout_handler)
|
|
76
83
|
|
|
77
84
|
return logger
|
|
78
85
|
except Exception as e:
|
|
79
|
-
print("Logging initialization failed.")
|
|
80
86
|
traceback.print_exc()
|
|
81
87
|
sys.exit(1)
|
|
82
88
|
|
|
@@ -96,6 +102,72 @@ def get_logger(command_output_logger: bool = False) -> logging.Logger:
|
|
|
96
102
|
|
|
97
103
|
return secondary_logger if command_output_logger else logger
|
|
98
104
|
|
|
105
|
+
def show_version():
|
|
106
|
+
script_name = os.path.basename(sys.argv[0])
|
|
107
|
+
print(f"{script_name} {about.__version__}")
|
|
108
|
+
print(f"{script_name} source code is here: https://github.com/per2jensen/dar-backup")
|
|
109
|
+
print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
110
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
111
|
+
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
112
|
+
|
|
113
|
+
def extract_version(output):
|
|
114
|
+
match = re.search(r'(\d+\.\d+(\.\d+)?)', output)
|
|
115
|
+
return match.group(1) if match else "unknown"
|
|
116
|
+
|
|
117
|
+
def get_binary_info(command):
|
|
118
|
+
"""
|
|
119
|
+
Return information about a binary command.
|
|
120
|
+
Args:
|
|
121
|
+
command (str): The command to check.
|
|
122
|
+
Returns:
|
|
123
|
+
dict: A dictionary containing the command, path, version, and full output.
|
|
124
|
+
Dict structure:
|
|
125
|
+
{
|
|
126
|
+
"command": str,
|
|
127
|
+
"path": str,
|
|
128
|
+
"version": str,
|
|
129
|
+
"full_output": str
|
|
130
|
+
}
|
|
131
|
+
Raises:
|
|
132
|
+
Exception: If there is an error running the command.
|
|
133
|
+
"""
|
|
134
|
+
path = shutil.which(command)
|
|
135
|
+
if path is None:
|
|
136
|
+
return {
|
|
137
|
+
"command": command,
|
|
138
|
+
"path": "Not found",
|
|
139
|
+
"version": "unknown",
|
|
140
|
+
"full_output": ""
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
result = subprocess.run(
|
|
145
|
+
[path, '--version'],
|
|
146
|
+
stdout=subprocess.PIPE,
|
|
147
|
+
stderr=subprocess.PIPE,
|
|
148
|
+
text=True
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Combine output regardless of return code
|
|
152
|
+
combined_output = (result.stdout + result.stderr).strip()
|
|
153
|
+
|
|
154
|
+
# Even if returncode != 0, the version info may still be valid
|
|
155
|
+
version = extract_version(combined_output)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"command": command,
|
|
159
|
+
"path": path,
|
|
160
|
+
"version": version if version else "unknown",
|
|
161
|
+
"full_output": combined_output
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
return {
|
|
166
|
+
"command": command,
|
|
167
|
+
"path": path,
|
|
168
|
+
"version": "error",
|
|
169
|
+
"full_output": str(e)
|
|
170
|
+
}
|
|
99
171
|
|
|
100
172
|
|
|
101
173
|
def requirements(type: str, config_setting: ConfigSettings):
|
|
@@ -155,7 +227,6 @@ class RestoreError(Exception):
|
|
|
155
227
|
pass
|
|
156
228
|
|
|
157
229
|
|
|
158
|
-
|
|
159
230
|
class CommandResult(NamedTuple):
|
|
160
231
|
"""
|
|
161
232
|
The reult of the run_command() function.
|
|
@@ -238,4 +309,275 @@ def list_backups(backup_dir, backup_definition=None):
|
|
|
238
309
|
print(f"{backup.ljust(max_name_length)} : {formatted_size.rjust(max_size_length)} MB")
|
|
239
310
|
|
|
240
311
|
|
|
312
|
+
def expand_path(path: str) -> str:
|
|
313
|
+
"""
|
|
314
|
+
Expand ~ and environment variables like $HOME in a path.
|
|
315
|
+
"""
|
|
316
|
+
return os.path.expanduser(os.path.expandvars(path))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def backup_definition_completer(prefix, parsed_args, **kwargs):
|
|
321
|
+
config_path = getattr(parsed_args, 'config_file', '~/.config/dar-backup/dar-backup.conf')
|
|
322
|
+
config_path = expand_path(config_path)
|
|
323
|
+
config_file = os.path.expanduser(config_path)
|
|
324
|
+
try:
|
|
325
|
+
config = ConfigSettings(config_file)
|
|
326
|
+
backup_d_dir = os.path.expanduser(config.backup_d_dir)
|
|
327
|
+
return [f for f in os.listdir(backup_d_dir) if f.startswith(prefix)]
|
|
328
|
+
except Exception:
|
|
329
|
+
return []
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def extract_backup_definition_fallback() -> str:
|
|
333
|
+
"""
|
|
334
|
+
Extracts --backup-definition or -d value directly from COMP_LINE.
|
|
335
|
+
This is needed because argcomplete doesn't always populate parsed_args fully.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
str: The value of the --backup-definition argument if found, else an empty string.
|
|
339
|
+
"""
|
|
340
|
+
comp_line = os.environ.get("COMP_LINE", "")
|
|
341
|
+
# Match both "--backup-definition VALUE" and "-d VALUE"
|
|
342
|
+
match = re.search(r"(--backup-definition|-d)\s+([^\s]+)", comp_line)
|
|
343
|
+
if match:
|
|
344
|
+
return match.group(2)
|
|
345
|
+
return ""
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def list_archive_completer(prefix, parsed_args, **kwargs):
|
|
350
|
+
import os
|
|
351
|
+
import configparser
|
|
352
|
+
from dar_backup.util import extract_backup_definition_fallback
|
|
353
|
+
|
|
354
|
+
backup_def = getattr(parsed_args, "backup_definition", None) or extract_backup_definition_fallback()
|
|
355
|
+
config_path = getattr(parsed_args, "config_file", None) or "~/.config/dar-backup/dar-backup.conf"
|
|
356
|
+
|
|
357
|
+
config_path = os.path.expanduser(os.path.expandvars(config_path))
|
|
358
|
+
if not os.path.exists(config_path):
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
config = configparser.ConfigParser()
|
|
362
|
+
config.read(config_path)
|
|
363
|
+
backup_dir = config.get("DIRECTORIES", "BACKUP_DIR", fallback="")
|
|
364
|
+
backup_dir = os.path.expanduser(os.path.expandvars(backup_dir))
|
|
365
|
+
|
|
366
|
+
if not os.path.isdir(backup_dir):
|
|
367
|
+
return []
|
|
368
|
+
|
|
369
|
+
files = os.listdir(backup_dir)
|
|
370
|
+
archive_re = re.compile(rf"^{re.escape(backup_def)}_.+_\d{{4}}-\d{{2}}-\d{{2}}\.1\.dar$") if backup_def else re.compile(r".+_\d{4}-\d{2}-\d{2}\.1\.dar$")
|
|
371
|
+
|
|
372
|
+
return [
|
|
373
|
+
f.rsplit(".1.dar", 1)[0]
|
|
374
|
+
for f in files
|
|
375
|
+
if archive_re.match(f)
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def archive_content_completer(prefix, parsed_args, **kwargs):
|
|
380
|
+
"""
|
|
381
|
+
Completes archive names from all available *.db files.
|
|
382
|
+
If --backup-def is given, only that one is used.
|
|
383
|
+
Only entries found in the catalog database (via `dar_manager --list`) are shown.
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
from dar_backup.config_settings import ConfigSettings
|
|
387
|
+
import subprocess
|
|
388
|
+
import re
|
|
389
|
+
import os
|
|
390
|
+
from datetime import datetime
|
|
391
|
+
|
|
392
|
+
# Expand config path
|
|
393
|
+
config_file = expand_path(getattr(parsed_args, "config_file", "~/.config/dar-backup/dar-backup.conf"))
|
|
394
|
+
config = ConfigSettings(config_file=config_file)
|
|
395
|
+
backup_dir = config.backup_dir
|
|
396
|
+
|
|
397
|
+
# Which db files to inspect?
|
|
398
|
+
backup_def = getattr(parsed_args, "backup_def", None)
|
|
399
|
+
db_files = (
|
|
400
|
+
[os.path.join(backup_dir, f"{backup_def}.db")]
|
|
401
|
+
if backup_def
|
|
402
|
+
else [os.path.join(backup_dir, f) for f in os.listdir(backup_dir) if f.endswith(".db")]
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
completions = []
|
|
406
|
+
|
|
407
|
+
for db_path in db_files:
|
|
408
|
+
if not os.path.exists(db_path):
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
result = subprocess.run(
|
|
413
|
+
["dar_manager", "--base", db_path, "--list"],
|
|
414
|
+
stdout=subprocess.PIPE,
|
|
415
|
+
stderr=subprocess.DEVNULL,
|
|
416
|
+
text=True,
|
|
417
|
+
check=True
|
|
418
|
+
)
|
|
419
|
+
except subprocess.CalledProcessError:
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
for line in result.stdout.splitlines():
|
|
423
|
+
parts = line.strip().split("\t")
|
|
424
|
+
if len(parts) >= 3:
|
|
425
|
+
archive = parts[2].strip()
|
|
426
|
+
if archive.startswith(prefix):
|
|
427
|
+
completions.append(archive)
|
|
428
|
+
|
|
429
|
+
# Sort: first by name (before first '_'), then by date (YYYY-MM-DD)
|
|
430
|
+
def sort_key(archive):
|
|
431
|
+
name_part = archive.split("_")[0]
|
|
432
|
+
date_match = re.search(r"\d{4}-\d{2}-\d{2}", archive)
|
|
433
|
+
date = datetime.strptime(date_match.group(0), "%Y-%m-%d") if date_match else datetime.min
|
|
434
|
+
return (name_part, date)
|
|
435
|
+
|
|
436
|
+
completions = sorted(set(completions), key=sort_key)
|
|
437
|
+
return completions or ["[no matching archives]"]
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def add_specific_archive_completer(prefix, parsed_args, **kwargs):
|
|
442
|
+
"""
|
|
443
|
+
Autocompletes archives that are present in the BACKUP_DIR
|
|
444
|
+
but not yet present in the <backup_def>.db catalog.
|
|
445
|
+
If --backup-def is provided, restrict suggestions to that.
|
|
446
|
+
"""
|
|
447
|
+
from dar_backup.config_settings import ConfigSettings
|
|
448
|
+
import subprocess
|
|
449
|
+
import re
|
|
450
|
+
import os
|
|
451
|
+
from datetime import datetime
|
|
452
|
+
|
|
453
|
+
config_file = expand_path(getattr(parsed_args, "config_file", "~/.config/dar-backup/dar-backup.conf"))
|
|
454
|
+
config = ConfigSettings(config_file=config_file)
|
|
455
|
+
backup_dir = config.backup_dir
|
|
456
|
+
backup_def = getattr(parsed_args, "backup_def", None)
|
|
457
|
+
|
|
458
|
+
# Match pattern for archive base names: e.g. test_FULL_2025-04-01
|
|
459
|
+
dar_pattern = re.compile(r"^(.*?_(FULL|DIFF|INCR)_(\d{4}-\d{2}-\d{2}))\.1\.dar$")
|
|
460
|
+
|
|
461
|
+
# Step 1: scan backup_dir for .1.dar files
|
|
462
|
+
all_archives = set()
|
|
463
|
+
for fname in os.listdir(backup_dir):
|
|
464
|
+
match = dar_pattern.match(fname)
|
|
465
|
+
if match:
|
|
466
|
+
base = match.group(1)
|
|
467
|
+
if base.startswith(prefix):
|
|
468
|
+
if not backup_def or base.startswith(f"{backup_def}_"):
|
|
469
|
+
all_archives.add(base)
|
|
470
|
+
|
|
471
|
+
# Step 2: exclude ones already present in the .db
|
|
472
|
+
db_path = os.path.join(backup_dir, f"{backup_def}.db") if backup_def else None
|
|
473
|
+
existing = set()
|
|
474
|
+
|
|
475
|
+
if db_path and os.path.exists(db_path):
|
|
476
|
+
try:
|
|
477
|
+
result = subprocess.run(
|
|
478
|
+
["dar_manager", "--base", db_path, "--list"],
|
|
479
|
+
stdout=subprocess.PIPE,
|
|
480
|
+
stderr=subprocess.DEVNULL,
|
|
481
|
+
text=True,
|
|
482
|
+
check=True
|
|
483
|
+
)
|
|
484
|
+
for line in result.stdout.splitlines():
|
|
485
|
+
parts = line.strip().split("\t")
|
|
486
|
+
if len(parts) >= 3:
|
|
487
|
+
existing.add(parts[2].strip())
|
|
488
|
+
except subprocess.CalledProcessError:
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
# Step 3: return filtered list
|
|
492
|
+
candidates = sorted(archive for archive in all_archives if archive not in existing)
|
|
493
|
+
return candidates or ["[no new archives]"]
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def patch_config_file(path: str, replacements: dict) -> None:
|
|
499
|
+
"""
|
|
500
|
+
Replace specific key values in a config file in-place.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
path: Path to the config file.
|
|
504
|
+
replacements: Dictionary of keys to new values, e.g., {"LOGFILE_LOCATION": "/tmp/logfile.log"}.
|
|
505
|
+
"""
|
|
506
|
+
with open(path, 'r') as f:
|
|
507
|
+
lines = f.readlines()
|
|
508
|
+
|
|
509
|
+
with open(path, 'w') as f:
|
|
510
|
+
for line in lines:
|
|
511
|
+
key = line.split('=')[0].strip()
|
|
512
|
+
if key in replacements:
|
|
513
|
+
f.write(f"{key} = {replacements[key]}\n")
|
|
514
|
+
else:
|
|
515
|
+
f.write(line)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
console = Console()
|
|
521
|
+
|
|
522
|
+
def print_aligned_settings(
|
|
523
|
+
settings: List[Tuple[str, str]],
|
|
524
|
+
log: bool = True,
|
|
525
|
+
header: str = "Startup Settings",
|
|
526
|
+
highlight_keywords: List[str] = None
|
|
527
|
+
) -> None:
|
|
528
|
+
"""
|
|
529
|
+
Print and optionally log settings nicely, using rich for color.
|
|
530
|
+
Highlights settings if dangerous keywords are found inside label or text,
|
|
531
|
+
but only if text is not None or empty.
|
|
532
|
+
"""
|
|
533
|
+
if not settings:
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
settings = [(str(label), "" if text is None else str(text)) for label, text in settings]
|
|
537
|
+
logger = get_logger()
|
|
538
|
+
|
|
539
|
+
max_label_length = max(len(label) for label, _ in settings)
|
|
540
|
+
|
|
541
|
+
header_line = f"========== {header} =========="
|
|
542
|
+
footer_line = "=" * len(header_line)
|
|
543
|
+
|
|
544
|
+
console.print(f"[bold cyan]{header_line}[/bold cyan]")
|
|
545
|
+
if log and logger:
|
|
546
|
+
logger.info(header_line)
|
|
547
|
+
|
|
548
|
+
for label, text in settings:
|
|
549
|
+
padded_label = f"{label:<{max_label_length}}"
|
|
550
|
+
|
|
551
|
+
label_clean = label.rstrip(":").lower()
|
|
552
|
+
text_clean = text.lower()
|
|
553
|
+
|
|
554
|
+
# Skip highlighting if text is empty
|
|
555
|
+
if not text_clean.strip():
|
|
556
|
+
danger = False
|
|
557
|
+
else:
|
|
558
|
+
danger = False
|
|
559
|
+
if highlight_keywords:
|
|
560
|
+
combined_text = f"{label_clean} {text_clean}"
|
|
561
|
+
danger = any(keyword.lower() in combined_text for keyword in highlight_keywords)
|
|
562
|
+
|
|
563
|
+
# Build the line
|
|
564
|
+
line_text = Text()
|
|
565
|
+
line_text.append(padded_label, style="bold")
|
|
566
|
+
line_text.append(" ", style="none")
|
|
567
|
+
|
|
568
|
+
if danger:
|
|
569
|
+
line_text.append("[!]", style="bold red")
|
|
570
|
+
line_text.append(" ", style="none")
|
|
571
|
+
|
|
572
|
+
line_text.append(text, style="white")
|
|
573
|
+
|
|
574
|
+
console.print(line_text)
|
|
575
|
+
|
|
576
|
+
# Always log clean text (no [!] in log)
|
|
577
|
+
final_line_for_log = f"{padded_label} {text}"
|
|
578
|
+
if log and logger:
|
|
579
|
+
logger.info(final_line_for_log)
|
|
241
580
|
|
|
581
|
+
console.print(f"[bold cyan]{footer_line}[/bold cyan]")
|
|
582
|
+
if log and logger:
|
|
583
|
+
logger.info(footer_line)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dar-backup
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.20
|
|
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: GPG Public Key, https://keys.openpgp.org/search?q=dar-backup@pm.me
|
|
6
6
|
Project-URL: Homepage, https://github.com/per2jensen/dar-backup/tree/main/v2
|
|
@@ -689,6 +689,7 @@ Classifier: Operating System :: POSIX :: Linux
|
|
|
689
689
|
Classifier: Programming Language :: Python :: 3.9
|
|
690
690
|
Classifier: Topic :: System :: Archiving :: Backup
|
|
691
691
|
Requires-Python: >=3.9
|
|
692
|
+
Requires-Dist: argcomplete>=3.6.2
|
|
692
693
|
Requires-Dist: inputimeout>=1.0.4
|
|
693
694
|
Requires-Dist: rich>=13.0.0
|
|
694
695
|
Description-Content-Type: text/markdown
|
|
@@ -697,12 +698,18 @@ Description-Content-Type: text/markdown
|
|
|
697
698
|
# Full, differential or incremental backups using 'dar'
|
|
698
699
|
|
|
699
700
|
[](https://codecov.io/gh/per2jensen/dar-backup)
|
|
701
|
+
[](https://pypi.org/project/dar-backup/)
|
|
702
|
+
[](https://pypi.org/project/dar-backup/)
|
|
700
703
|
|
|
701
704
|
The wonderful 'dar' [Disk Archiver](https://github.com/Edrusb/DAR) is used for
|
|
702
705
|
the heavy lifting, together with the par2 suite in these scripts.
|
|
703
706
|
|
|
704
707
|
This is the `Python` based **version 2** of `dar-backup`.
|
|
705
708
|
|
|
709
|
+
## TL;DR
|
|
710
|
+
|
|
711
|
+
`dar-backup` is a Python-powered CLI for creating and validating full, differential, and incremental backups using dar and par2. Designed for long-term restore integrity, even on user-space filesystems like FUSE.
|
|
712
|
+
|
|
706
713
|
## Table of Contents
|
|
707
714
|
|
|
708
715
|
- [Full, differential or incremental backups using 'dar'](#full-differential-or-incremental-backups-using-dar)
|
|
@@ -728,6 +735,7 @@ This is the `Python` based **version 2** of `dar-backup`.
|
|
|
728
735
|
- [Generate systemd files](#generate-systemd-files)
|
|
729
736
|
- [Service: dar-back --incremental-backup](#service-dar-backup---incremental-backup)
|
|
730
737
|
- [Timer: dar-back --incremental-backup](#timer-dar-backup---incremental-backup)
|
|
738
|
+
- [Systemd timer note](#systemd-timer-note)
|
|
731
739
|
- [List contents of an archive](#list-contents-of-an-archive)
|
|
732
740
|
- [dar file selection examples](#dar-file-selection-examples)
|
|
733
741
|
- [Select a directory](#select-a-directory)
|
|
@@ -753,7 +761,9 @@ This is the `Python` based **version 2** of `dar-backup`.
|
|
|
753
761
|
- [Skipping cache directories](#skipping-cache-directories)
|
|
754
762
|
- [Progress bar + current directory](#progress-bar-and-current-directory)
|
|
755
763
|
- [Todo](#todo)
|
|
764
|
+
- [Known Limitations / Edge Cases](#known-limitations--edge-cases)
|
|
756
765
|
- [Reference](#reference)
|
|
766
|
+
- [CLI Tools Overview](#cli-tools-overview)
|
|
757
767
|
- [Test coverage report](#test-coverage)
|
|
758
768
|
- [dar-backup](#dar-backup-options)
|
|
759
769
|
- [manager](#manager-options)
|
|
@@ -1346,6 +1356,10 @@ Persistent=true
|
|
|
1346
1356
|
WantedBy=timers.target
|
|
1347
1357
|
````
|
|
1348
1358
|
|
|
1359
|
+
## systemd timer note
|
|
1360
|
+
|
|
1361
|
+
📅 OnCalendar syntax is flexible — you can tweak backup schedules easily. Run systemd-analyze calendar to preview timers.
|
|
1362
|
+
|
|
1349
1363
|
## list contents of an archive
|
|
1350
1364
|
|
|
1351
1365
|
```` bash
|
|
@@ -1636,8 +1650,27 @@ The indicators are not shown if dar-backup is run from systemd or if it is used
|
|
|
1636
1650
|
- Look into a way to move the .par2 files away from the `dar` slices, to maximize chance of good redundancy.
|
|
1637
1651
|
- Add option to dar-backup to use the `dar` option `--fsa-scope none`
|
|
1638
1652
|
|
|
1653
|
+
## Known Limitations / Edge Cases
|
|
1654
|
+
|
|
1655
|
+
Does not currently encrypt data (by design — relies on encrypted storage)
|
|
1656
|
+
|
|
1657
|
+
One backup definition per file
|
|
1658
|
+
|
|
1659
|
+
.par2 files created for each slice (may be moved in future)
|
|
1660
|
+
|
|
1639
1661
|
## Reference
|
|
1640
1662
|
|
|
1663
|
+
### CLI Tools Overview
|
|
1664
|
+
|
|
1665
|
+
| Command | Description |
|
|
1666
|
+
|-----------------------|-------------------------------------------|
|
|
1667
|
+
| `dar-backup` | Perform full, differential, or incremental backups with verification and restore testing |
|
|
1668
|
+
| `manager` | Maintain and query catalog databases for archives |
|
|
1669
|
+
| `cleanup` | Remove outdated DIFF/INCR archives (and optionally FULLs) |
|
|
1670
|
+
| `clean-log` | Clean up excessive log output from dar command logs |
|
|
1671
|
+
| `installer` | Set up required directories and default config files |
|
|
1672
|
+
| `dar-backup-systemd` | Generate (and optionally install) systemd timers and services for automated backups |
|
|
1673
|
+
|
|
1641
1674
|
### test coverage
|
|
1642
1675
|
|
|
1643
1676
|
Running
|
|
@@ -1646,23 +1679,26 @@ Running
|
|
|
1646
1679
|
pytest --cov=dar_backup tests/
|
|
1647
1680
|
````
|
|
1648
1681
|
|
|
1649
|
-
results for version 0.6.
|
|
1682
|
+
results for a dev version 0.6.19 in this report:
|
|
1650
1683
|
|
|
1651
1684
|
```` code
|
|
1652
1685
|
---------- coverage: platform linux, python 3.12.3-final-0 -----------
|
|
1653
|
-
Name
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1686
|
+
Name Stmts Miss Cover
|
|
1687
|
+
----------------------------------------------------------
|
|
1688
|
+
src/dar_backup/__about__.py 1 0 100%
|
|
1689
|
+
src/dar_backup/__init__.py 0 0 100%
|
|
1690
|
+
src/dar_backup/clean_log.py 68 13 81%
|
|
1691
|
+
src/dar_backup/cleanup.py 193 17 91%
|
|
1692
|
+
src/dar_backup/command_runner.py 73 1 99%
|
|
1693
|
+
src/dar_backup/config_settings.py 66 8 88%
|
|
1694
|
+
src/dar_backup/dar_backup.py 535 56 90%
|
|
1695
|
+
src/dar_backup/dar_backup_systemd.py 56 7 88%
|
|
1696
|
+
src/dar_backup/installer.py 59 6 90%
|
|
1697
|
+
src/dar_backup/manager.py 351 56 84%
|
|
1698
|
+
src/dar_backup/rich_progress.py 70 7 90%
|
|
1699
|
+
src/dar_backup/util.py 130 15 88%
|
|
1700
|
+
----------------------------------------------------------
|
|
1701
|
+
TOTAL 1602 186 88%
|
|
1666
1702
|
````
|
|
1667
1703
|
|
|
1668
1704
|
### dar-backup options
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
+
dar_backup/Changelog.md,sha256=hFz5NoZyezxgKza9i4VBv57aPnCuzhIQM9yK_2cxAZ8,9107
|
|
3
|
+
dar_backup/README.md,sha256=PcZQRaBw9i_GbRoA786OFKI0PwY87E7D30UzovWqEQQ,44902
|
|
4
|
+
dar_backup/__about__.py,sha256=bhh1JgaMOqVsHTPoLk0PdjZxyDSXmYzOzEaRGXYuakc,22
|
|
5
|
+
dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
dar_backup/clean_log.py,sha256=uQ_9HomK7RRkskXrgMPbAC84RgNlCFJ8RHL9KHeAMvc,5447
|
|
7
|
+
dar_backup/cleanup.py,sha256=4IG736gdmvjurcQQ_DC-B5CoA3Ix9u5C3WfwmMmFIpc,12500
|
|
8
|
+
dar_backup/command_runner.py,sha256=PQA968EXssSGjSs_16psFkdxRZi9-YK4TrBKFz0ss3k,4455
|
|
9
|
+
dar_backup/config_settings.py,sha256=8HhIDVtFk7D3VqJlYcveOlAaSEJwIaO3MICz-rN3tVY,5260
|
|
10
|
+
dar_backup/dar-backup.conf,sha256=WWNrysjQ1ii2jpab8jxgWCw3IkNxLBYVOW8fxWbO_9g,1155
|
|
11
|
+
dar_backup/dar_backup.py,sha256=x0at94NeQzu8tv2ohg3YKAt6j76gQUS2387wh5Z4B78,41856
|
|
12
|
+
dar_backup/dar_backup_systemd.py,sha256=oehD_t9CFu0CsMgDWRH-Gt74Ynl1m29yqQEh5Kxv7aw,3807
|
|
13
|
+
dar_backup/demo.py,sha256=fl2LHHWxuXRT814M_zuZh_YqqLk6nuNg9BI9HpLzdUU,4841
|
|
14
|
+
dar_backup/exceptions.py,sha256=6fpHpnhkbtFcFZVDMqCsdESg-kQNCjF40EROGKeq8yU,113
|
|
15
|
+
dar_backup/installer.py,sha256=gISJ3inz1uTjM5235gNtwjDUG-Fq6b3OZzMQuJl5eRg,2252
|
|
16
|
+
dar_backup/manager.py,sha256=UyEcm9ydkok47slgSlpPEKP_mAdkvcwCghLCXdNlGpc,26622
|
|
17
|
+
dar_backup/rich_progress.py,sha256=jTwM-4VlqHHzKqIfyXjL1pWEriobSJwCdN3YXzXzRdo,3105
|
|
18
|
+
dar_backup/util.py,sha256=2Uwd1bf3N-AEzIvc69gvwEnGXNYNzZ-RqxUfbvmOUAc,20079
|
|
19
|
+
dar_backup-0.6.20.dist-info/METADATA,sha256=dO6-9MvI9M_munyvCVFGNrq33NBbDDxn6UAIRLwVb5U,86624
|
|
20
|
+
dar_backup-0.6.20.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
dar_backup-0.6.20.dist-info/entry_points.txt,sha256=jFMqGdvGO8NeHrmcB0pxY7__PCSQAWRCFzsschMXsec,248
|
|
22
|
+
dar_backup-0.6.20.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
23
|
+
dar_backup-0.6.20.dist-info/RECORD,,
|
|
@@ -3,5 +3,5 @@ clean-log = dar_backup.clean_log:main
|
|
|
3
3
|
cleanup = dar_backup.cleanup:main
|
|
4
4
|
dar-backup = dar_backup.dar_backup:main
|
|
5
5
|
dar-backup-systemd = dar_backup.dar_backup_systemd:main
|
|
6
|
-
|
|
6
|
+
demo = dar_backup.demo:main
|
|
7
7
|
manager = dar_backup.manager:main
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
-
dar_backup/Changelog.md,sha256=ZRK03WnSbNWkMQAVNvfflKs06TGmkB4wBOm0Lp4ft1c,7830
|
|
3
|
-
dar_backup/README.md,sha256=b4eoXh8fo7Li1mJqMKbrW-SrI2gDYkczCtL8G9-KoFE,43261
|
|
4
|
-
dar_backup/__about__.py,sha256=g-mDk6iCtdmWAx2-NGjdBzw-GF98McMHGx0wkhbKFM4,22
|
|
5
|
-
dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
dar_backup/clean_log.py,sha256=cGhtKYnQJ2ceNQfw5XcCln_WNBasbmlfhO3kRydjDNk,5196
|
|
7
|
-
dar_backup/cleanup.py,sha256=BAiztBL0Dpr3fs6WZU_9oR5jUWumHua4dS37JtSxi-Q,12499
|
|
8
|
-
dar_backup/command_runner.py,sha256=74Fsylz1NN-dn8lbdRhkL6LA1r527QJeojBlniGrPuo,2708
|
|
9
|
-
dar_backup/config_settings.py,sha256=Rh4T35-w_5tpRAViMfv3YP3GBpG4mQy7Do8cNBzYAR0,4912
|
|
10
|
-
dar_backup/dar-backup.conf,sha256=64O3bGlzqupneT2gVeaETJ1qS6-3Exet9Zto27jgwPQ,897
|
|
11
|
-
dar_backup/dar_backup.py,sha256=THZmKZgOWxtYo9HaysU38GZ1hnpTcziJ45sPQ4HvJoE,41003
|
|
12
|
-
dar_backup/dar_backup_systemd.py,sha256=oehD_t9CFu0CsMgDWRH-Gt74Ynl1m29yqQEh5Kxv7aw,3807
|
|
13
|
-
dar_backup/installer.py,sha256=fl2LHHWxuXRT814M_zuZh_YqqLk6nuNg9BI9HpLzdUU,4841
|
|
14
|
-
dar_backup/manager.py,sha256=HRuWeDB1sd14HwMEM6dk6OVqpj2KDTk6y55OKw7vUHE,22420
|
|
15
|
-
dar_backup/rich_progress.py,sha256=jTwM-4VlqHHzKqIfyXjL1pWEriobSJwCdN3YXzXzRdo,3105
|
|
16
|
-
dar_backup/util.py,sha256=2V4ONpoGh_W6wFv2RbJoUIEDxcY6_ot4frtzEBWsu7U,8798
|
|
17
|
-
dar_backup-0.6.18.dist-info/METADATA,sha256=My1Utkqgj1iYhfC4OfWXzzzrU2bPasbWl9usZqS_7bw,84949
|
|
18
|
-
dar_backup-0.6.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
19
|
-
dar_backup-0.6.18.dist-info/entry_points.txt,sha256=NSCYoG5Dvh1UhvKWOQPgcHdFv4--R4Sre3d9FwJra3E,258
|
|
20
|
-
dar_backup-0.6.18.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
21
|
-
dar_backup-0.6.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|