dar-backup 0.6.18__py3-none-any.whl → 0.6.19__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 +15 -0
- dar_backup/README.md +49 -14
- dar_backup/__about__.py +1 -1
- dar_backup/clean_log.py +14 -7
- dar_backup/cleanup.py +7 -2
- dar_backup/command_runner.py +59 -9
- dar_backup/dar_backup.py +21 -6
- dar_backup/manager.py +162 -65
- dar_backup/util.py +247 -2
- {dar_backup-0.6.18.dist-info → dar_backup-0.6.19.dist-info}/METADATA +51 -15
- dar_backup-0.6.19.dist-info/RECORD +21 -0
- dar_backup-0.6.18.dist-info/RECORD +0 -21
- {dar_backup-0.6.18.dist-info → dar_backup-0.6.19.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.18.dist-info → dar_backup-0.6.19.dist-info}/entry_points.txt +0 -0
- {dar_backup-0.6.18.dist-info → dar_backup-0.6.19.dist-info}/licenses/LICENSE +0 -0
dar_backup/Changelog.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
<!-- markdownlint-disable MD024 -->
|
|
2
2
|
# dar-backup Changelog
|
|
3
3
|
|
|
4
|
+
## v2-beta-0.6.19 - 2025-04-21
|
|
5
|
+
|
|
6
|
+
Github link: [v2-beta-0.6.19](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.6.19/v2)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- removed a BackupError in the verify() to reduce noise in logs and let the rest of "compares" run.
|
|
11
|
+
- Added bash and zsh auto completion for a nicer CLI experience.
|
|
12
|
+
|
|
13
|
+
-- See [README for details](https://github.com/per2jensen/dar-backup?tab=readme-ov-file#shell-autocompletion)
|
|
14
|
+
|
|
15
|
+
- Improvement to command_runner.run(), more robust decoding
|
|
16
|
+
|
|
17
|
+
- Manager --add-specific-archive now gives a prompt with a warning if user attempts to add a catalog that breaks chronology. The user is allowed to go forward and ignore the warning or can choose to abort. The program times out after a little while and discards the operation.
|
|
18
|
+
|
|
4
19
|
## v2-beta-0.6.18 - 2025-04-05
|
|
5
20
|
|
|
6
21
|
Github link: [v2-beta-0.6.18](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.6.18/v2)
|
dar_backup/README.md
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
# Full, differential or incremental backups using 'dar'
|
|
3
3
|
|
|
4
4
|
[](https://codecov.io/gh/per2jensen/dar-backup)
|
|
5
|
+
[](https://pypi.org/project/dar-backup/)
|
|
6
|
+
[](https://pypi.org/project/dar-backup/)
|
|
5
7
|
|
|
6
8
|
The wonderful 'dar' [Disk Archiver](https://github.com/Edrusb/DAR) is used for
|
|
7
9
|
the heavy lifting, together with the par2 suite in these scripts.
|
|
8
10
|
|
|
9
11
|
This is the `Python` based **version 2** of `dar-backup`.
|
|
10
12
|
|
|
13
|
+
## TL;DR
|
|
14
|
+
|
|
15
|
+
`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.
|
|
16
|
+
|
|
11
17
|
## Table of Contents
|
|
12
18
|
|
|
13
19
|
- [Full, differential or incremental backups using 'dar'](#full-differential-or-incremental-backups-using-dar)
|
|
@@ -33,6 +39,7 @@ This is the `Python` based **version 2** of `dar-backup`.
|
|
|
33
39
|
- [Generate systemd files](#generate-systemd-files)
|
|
34
40
|
- [Service: dar-back --incremental-backup](#service-dar-backup---incremental-backup)
|
|
35
41
|
- [Timer: dar-back --incremental-backup](#timer-dar-backup---incremental-backup)
|
|
42
|
+
- [Systemd timer note](#systemd-timer-note)
|
|
36
43
|
- [List contents of an archive](#list-contents-of-an-archive)
|
|
37
44
|
- [dar file selection examples](#dar-file-selection-examples)
|
|
38
45
|
- [Select a directory](#select-a-directory)
|
|
@@ -58,7 +65,9 @@ This is the `Python` based **version 2** of `dar-backup`.
|
|
|
58
65
|
- [Skipping cache directories](#skipping-cache-directories)
|
|
59
66
|
- [Progress bar + current directory](#progress-bar-and-current-directory)
|
|
60
67
|
- [Todo](#todo)
|
|
68
|
+
- [Known Limitations / Edge Cases](#known-limitations--edge-cases)
|
|
61
69
|
- [Reference](#reference)
|
|
70
|
+
- [CLI Tools Overview](#cli-tools-overview)
|
|
62
71
|
- [Test coverage report](#test-coverage)
|
|
63
72
|
- [dar-backup](#dar-backup-options)
|
|
64
73
|
- [manager](#manager-options)
|
|
@@ -651,6 +660,10 @@ Persistent=true
|
|
|
651
660
|
WantedBy=timers.target
|
|
652
661
|
````
|
|
653
662
|
|
|
663
|
+
## systemd timer note
|
|
664
|
+
|
|
665
|
+
📅 OnCalendar syntax is flexible — you can tweak backup schedules easily. Run systemd-analyze calendar to preview timers.
|
|
666
|
+
|
|
654
667
|
## list contents of an archive
|
|
655
668
|
|
|
656
669
|
```` bash
|
|
@@ -941,8 +954,27 @@ The indicators are not shown if dar-backup is run from systemd or if it is used
|
|
|
941
954
|
- Look into a way to move the .par2 files away from the `dar` slices, to maximize chance of good redundancy.
|
|
942
955
|
- Add option to dar-backup to use the `dar` option `--fsa-scope none`
|
|
943
956
|
|
|
957
|
+
## Known Limitations / Edge Cases
|
|
958
|
+
|
|
959
|
+
Does not currently encrypt data (by design — relies on encrypted storage)
|
|
960
|
+
|
|
961
|
+
One backup definition per file
|
|
962
|
+
|
|
963
|
+
.par2 files created for each slice (may be moved in future)
|
|
964
|
+
|
|
944
965
|
## Reference
|
|
945
966
|
|
|
967
|
+
### CLI Tools Overview
|
|
968
|
+
|
|
969
|
+
| Command | Description |
|
|
970
|
+
|-----------------------|-------------------------------------------|
|
|
971
|
+
| `dar-backup` | Perform full, differential, or incremental backups with verification and restore testing |
|
|
972
|
+
| `manager` | Maintain and query catalog databases for archives |
|
|
973
|
+
| `cleanup` | Remove outdated DIFF/INCR archives (and optionally FULLs) |
|
|
974
|
+
| `clean-log` | Clean up excessive log output from dar command logs |
|
|
975
|
+
| `installer` | Set up required directories and default config files |
|
|
976
|
+
| `dar-backup-systemd` | Generate (and optionally install) systemd timers and services for automated backups |
|
|
977
|
+
|
|
946
978
|
### test coverage
|
|
947
979
|
|
|
948
980
|
Running
|
|
@@ -951,23 +983,26 @@ Running
|
|
|
951
983
|
pytest --cov=dar_backup tests/
|
|
952
984
|
````
|
|
953
985
|
|
|
954
|
-
results for version 0.6.
|
|
986
|
+
results for a dev version 0.6.19 in this report:
|
|
955
987
|
|
|
956
988
|
```` code
|
|
957
989
|
---------- coverage: platform linux, python 3.12.3-final-0 -----------
|
|
958
|
-
Name
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
990
|
+
Name Stmts Miss Cover
|
|
991
|
+
----------------------------------------------------------
|
|
992
|
+
src/dar_backup/__about__.py 1 0 100%
|
|
993
|
+
src/dar_backup/__init__.py 0 0 100%
|
|
994
|
+
src/dar_backup/clean_log.py 68 13 81%
|
|
995
|
+
src/dar_backup/cleanup.py 193 17 91%
|
|
996
|
+
src/dar_backup/command_runner.py 73 1 99%
|
|
997
|
+
src/dar_backup/config_settings.py 66 8 88%
|
|
998
|
+
src/dar_backup/dar_backup.py 535 56 90%
|
|
999
|
+
src/dar_backup/dar_backup_systemd.py 56 7 88%
|
|
1000
|
+
src/dar_backup/installer.py 59 6 90%
|
|
1001
|
+
src/dar_backup/manager.py 351 56 84%
|
|
1002
|
+
src/dar_backup/rich_progress.py 70 7 90%
|
|
1003
|
+
src/dar_backup/util.py 130 15 88%
|
|
1004
|
+
----------------------------------------------------------
|
|
1005
|
+
TOTAL 1602 186 88%
|
|
971
1006
|
````
|
|
972
1007
|
|
|
973
1008
|
### dar-backup options
|
dar_backup/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.6.
|
|
1
|
+
__version__ = "0.6.19"
|
dar_backup/clean_log.py
CHANGED
|
@@ -11,6 +11,8 @@ See section 15 and section 16 in the supplied "LICENSE" file
|
|
|
11
11
|
|
|
12
12
|
This script can be used to remove (much of) the logged output from `dar`.
|
|
13
13
|
When `dar` verbose options are enabled, quite a lot of information is emitted.
|
|
14
|
+
|
|
15
|
+
If a rerex is matched, the entire line is removed (change in v2-beta-0.6.19).
|
|
14
16
|
"""
|
|
15
17
|
|
|
16
18
|
|
|
@@ -19,7 +21,7 @@ import re
|
|
|
19
21
|
import os
|
|
20
22
|
import sys
|
|
21
23
|
|
|
22
|
-
from
|
|
24
|
+
from dar_backup import __about__ as about
|
|
23
25
|
from dar_backup.config_settings import ConfigSettings
|
|
24
26
|
|
|
25
27
|
LICENSE = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
@@ -32,7 +34,7 @@ def clean_log_file(log_file_path, dry_run=False):
|
|
|
32
34
|
|
|
33
35
|
if not os.path.isfile(log_file_path):
|
|
34
36
|
print(f"File '{log_file_path}' not found!")
|
|
35
|
-
sys.exit(
|
|
37
|
+
sys.exit(127)
|
|
36
38
|
|
|
37
39
|
if not os.access(log_file_path, os.R_OK):
|
|
38
40
|
print(f"No read permission for '{log_file_path}'")
|
|
@@ -49,13 +51,18 @@ def clean_log_file(log_file_path, dry_run=False):
|
|
|
49
51
|
temp_file_path = log_file_path + ".tmp"
|
|
50
52
|
|
|
51
53
|
patterns = [
|
|
54
|
+
r"INFO\s*-\s*Inspecting\s*directory",
|
|
55
|
+
r"INFO\s*-\s*Finished\s*Inspecting",
|
|
52
56
|
r"INFO\s*-\s*<File",
|
|
57
|
+
r"INFO\s*-\s*</File",
|
|
53
58
|
r"INFO\s*-\s*<Attributes",
|
|
59
|
+
r"INFO\s*-\s*</Attributes",
|
|
54
60
|
r"INFO\s*-\s*</Directory",
|
|
55
61
|
r"INFO\s*-\s*<Directory",
|
|
56
|
-
r"INFO\s*-\s
|
|
57
|
-
r"INFO\s*-\s
|
|
58
|
-
r"INFO\s*-\s
|
|
62
|
+
r"INFO\s*-\s*<Catalog",
|
|
63
|
+
r"INFO\s*-\s*</Catalog",
|
|
64
|
+
r"INFO\s*-\s*<Symlink",
|
|
65
|
+
r"INFO\s*-\s*</Symlink",
|
|
59
66
|
]
|
|
60
67
|
|
|
61
68
|
try:
|
|
@@ -70,9 +77,9 @@ def clean_log_file(log_file_path, dry_run=False):
|
|
|
70
77
|
if dry_run:
|
|
71
78
|
print(f"Would remove: {original_line.strip()}") # Print full line for dry-run
|
|
72
79
|
matched = True # Mark that a pattern matched
|
|
73
|
-
|
|
80
|
+
break # No need to check other patterns if one matches
|
|
74
81
|
|
|
75
|
-
if not dry_run and
|
|
82
|
+
if not dry_run and not matched: # In normal mode, only write non-empty lines
|
|
76
83
|
outfile.write(line.rstrip() + "\n")
|
|
77
84
|
|
|
78
85
|
if dry_run and matched:
|
dar_backup/cleanup.py
CHANGED
|
@@ -13,6 +13,7 @@ This script removes old DIFF and INCR archives + accompanying .par2 files accord
|
|
|
13
13
|
[AGE] settings in the configuration file.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import argcomplete
|
|
16
17
|
import argparse
|
|
17
18
|
import logging
|
|
18
19
|
import os
|
|
@@ -31,6 +32,7 @@ from dar_backup.util import list_backups
|
|
|
31
32
|
from dar_backup.util import setup_logging
|
|
32
33
|
from dar_backup.util import get_logger
|
|
33
34
|
from dar_backup.util import requirements
|
|
35
|
+
from dar_backup.util import backup_definition_completer, list_archive_completer
|
|
34
36
|
|
|
35
37
|
from dar_backup.command_runner import CommandRunner
|
|
36
38
|
from dar_backup.command_runner import CommandResult
|
|
@@ -186,16 +188,19 @@ def main():
|
|
|
186
188
|
global logger, runner
|
|
187
189
|
|
|
188
190
|
parser = argparse.ArgumentParser(description="Cleanup old archives according to AGE configuration.")
|
|
189
|
-
parser.add_argument('-d', '--backup-definition', help="Specific backup definition to cleanup.")
|
|
191
|
+
parser.add_argument('-d', '--backup-definition', help="Specific backup definition to cleanup.").completer = backup_definition_completer
|
|
190
192
|
parser.add_argument('-c', '--config-file', '-c', type=str, help="Path to 'dar-backup.conf'", default='~/.config/dar-backup/dar-backup.conf')
|
|
191
193
|
parser.add_argument('-v', '--version', action='store_true', help="Show version information.")
|
|
192
194
|
parser.add_argument('--alternate-archive-dir', type=str, help="Cleanup in this directory instead of the default one.")
|
|
193
|
-
parser.add_argument('--cleanup-specific-archives', type=str, help="Comma separated list of archives to cleanup")
|
|
195
|
+
parser.add_argument('--cleanup-specific-archives', type=str, help="Comma separated list of archives to cleanup").completer = list_archive_completer
|
|
194
196
|
parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
|
|
195
197
|
parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
|
|
196
198
|
parser.add_argument('--log-level', type=str, help="`debug` or `trace`, default is `info`", default="info")
|
|
197
199
|
parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
|
|
198
200
|
parser.add_argument('--test-mode', action='store_true', help='Read envvars in order to run some pytest cases')
|
|
201
|
+
|
|
202
|
+
argcomplete.autocomplete(parser)
|
|
203
|
+
|
|
199
204
|
args = parser.parse_args()
|
|
200
205
|
|
|
201
206
|
args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
|
dar_backup/command_runner.py
CHANGED
|
@@ -3,8 +3,11 @@ import logging
|
|
|
3
3
|
import threading
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
|
+
import tempfile
|
|
6
7
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
|
|
7
8
|
from typing import List, Optional
|
|
9
|
+
from dar_backup.util import get_logger
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
class CommandResult:
|
|
10
13
|
def __init__(self, returncode: int, stdout: str, stderr: str):
|
|
@@ -15,6 +18,7 @@ class CommandResult:
|
|
|
15
18
|
def __repr__(self):
|
|
16
19
|
return f"<CommandResult returncode={self.returncode}>"
|
|
17
20
|
|
|
21
|
+
|
|
18
22
|
class CommandRunner:
|
|
19
23
|
def __init__(
|
|
20
24
|
self,
|
|
@@ -22,10 +26,41 @@ class CommandRunner:
|
|
|
22
26
|
command_logger: Optional[logging.Logger] = None,
|
|
23
27
|
default_timeout: int = 30
|
|
24
28
|
):
|
|
25
|
-
self.logger = logger or
|
|
26
|
-
self.command_logger = command_logger or
|
|
29
|
+
self.logger = logger or get_logger()
|
|
30
|
+
self.command_logger = command_logger or get_logger(command_output_logger=True)
|
|
27
31
|
self.default_timeout = default_timeout
|
|
28
32
|
|
|
33
|
+
if not self.logger or not self.command_logger:
|
|
34
|
+
self.logger_fallback()
|
|
35
|
+
|
|
36
|
+
def logger_fallback(self):
|
|
37
|
+
"""
|
|
38
|
+
Setup temporary log files
|
|
39
|
+
"""
|
|
40
|
+
main_log = tempfile.NamedTemporaryFile(delete=False)
|
|
41
|
+
command_log = tempfile.NamedTemporaryFile(delete=False)
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger("command_runner_fallback_main_logger")
|
|
44
|
+
command_logger = logging.getLogger("command_runner_fallback_command_logger")
|
|
45
|
+
logger.setLevel(logging.DEBUG)
|
|
46
|
+
command_logger.setLevel(logging.DEBUG)
|
|
47
|
+
|
|
48
|
+
main_handler = logging.FileHandler(main_log.name)
|
|
49
|
+
command_handler = logging.FileHandler(command_log.name)
|
|
50
|
+
|
|
51
|
+
logger.addHandler(main_handler)
|
|
52
|
+
command_logger.addHandler(command_handler)
|
|
53
|
+
|
|
54
|
+
self.logger = logger
|
|
55
|
+
self.command_logger = command_logger
|
|
56
|
+
self.default_timeout = 30
|
|
57
|
+
self.logger.info("CommandRunner initialized with fallback loggers")
|
|
58
|
+
self.command_logger.info("CommandRunner initialized with fallback loggers")
|
|
59
|
+
|
|
60
|
+
print(f"[WARN] Using fallback loggers:\n Main log: {main_log.name}\n Command log: {command_log.name}", file=sys.stderr)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
29
64
|
def run(
|
|
30
65
|
self,
|
|
31
66
|
cmd: List[str],
|
|
@@ -36,24 +71,39 @@ class CommandRunner:
|
|
|
36
71
|
text: bool = True
|
|
37
72
|
) -> CommandResult:
|
|
38
73
|
timeout = timeout or self.default_timeout
|
|
39
|
-
|
|
74
|
+
|
|
75
|
+
#log the command to be executed
|
|
76
|
+
command = f"Executing command: {' '.join(cmd)} (timeout={timeout}s)"
|
|
77
|
+
self.command_logger.info(command) # log to command logger
|
|
78
|
+
self.logger.debug(command) # log to main logger if "--log-level debug"
|
|
40
79
|
|
|
41
80
|
process = subprocess.Popen(
|
|
42
81
|
cmd,
|
|
43
82
|
stdout=subprocess.PIPE if capture_output else None,
|
|
44
83
|
stderr=subprocess.PIPE if capture_output else None,
|
|
45
|
-
text=
|
|
46
|
-
bufsize
|
|
84
|
+
text=False,
|
|
85
|
+
bufsize=-1
|
|
47
86
|
)
|
|
48
87
|
|
|
49
88
|
stdout_lines = []
|
|
50
89
|
stderr_lines = []
|
|
51
90
|
|
|
91
|
+
|
|
52
92
|
def stream_output(stream, lines, level):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
93
|
+
try:
|
|
94
|
+
while True:
|
|
95
|
+
chunk = stream.read(1024)
|
|
96
|
+
if not chunk:
|
|
97
|
+
break
|
|
98
|
+
decoded = chunk.decode('utf-8', errors='replace')
|
|
99
|
+
lines.append(decoded)
|
|
100
|
+
self.command_logger.log(level, decoded.strip())
|
|
101
|
+
except Exception as e:
|
|
102
|
+
self.logger.warning(f"stream_output decode error: {e}")
|
|
103
|
+
finally:
|
|
104
|
+
stream.close()
|
|
105
|
+
|
|
106
|
+
|
|
57
107
|
|
|
58
108
|
threads = []
|
|
59
109
|
if capture_output and process.stdout:
|
dar_backup/dar_backup.py
CHANGED
|
@@ -14,6 +14,8 @@ This script can be used to control `dar` to backup parts of or the whole system.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
|
|
18
|
+
import argcomplete
|
|
17
19
|
import argparse
|
|
18
20
|
import filecmp
|
|
19
21
|
|
|
@@ -46,12 +48,15 @@ from dar_backup.util import get_logger
|
|
|
46
48
|
from dar_backup.util import BackupError
|
|
47
49
|
from dar_backup.util import RestoreError
|
|
48
50
|
from dar_backup.util import requirements
|
|
51
|
+
from dar_backup.util import get_binary_info
|
|
52
|
+
from dar_backup.util import backup_definition_completer, list_archive_completer
|
|
49
53
|
|
|
50
54
|
from dar_backup.command_runner import CommandRunner
|
|
51
55
|
from dar_backup.command_runner import CommandResult
|
|
52
56
|
|
|
53
57
|
from dar_backup.rich_progress import show_log_driven_bar
|
|
54
58
|
|
|
59
|
+
from argcomplete.completers import FilesCompleter
|
|
55
60
|
|
|
56
61
|
logger = None
|
|
57
62
|
runner = None
|
|
@@ -312,7 +317,8 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
312
317
|
if filecmp.cmp(os.path.join(config_settings.test_restore_dir, restored_file_path.lstrip("/")), os.path.join(root_path, restored_file_path.lstrip("/")), shallow=False):
|
|
313
318
|
args.verbose and logger.info(f"Success: file '{restored_file_path}' matches the original")
|
|
314
319
|
else:
|
|
315
|
-
|
|
320
|
+
result = False
|
|
321
|
+
logger.error(f"Failure: file '{restored_file_path}' did not match the original")
|
|
316
322
|
except PermissionError:
|
|
317
323
|
result = False
|
|
318
324
|
logger.exception(f"Permission error while comparing files, continuing....")
|
|
@@ -523,6 +529,10 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
523
529
|
|
|
524
530
|
# Perform backup
|
|
525
531
|
backup_result = generic_backup(backup_type, command, backup_file, backup_definition_path, args.darrc, config_settings, args)
|
|
532
|
+
if not isinstance(backup_result, list) or not all(isinstance(i, tuple) and len(i) == 2 for i in backup_result):
|
|
533
|
+
logger.error("Unexpected return format from generic_backup")
|
|
534
|
+
backup_result = [("Unexpected return format from generic_backup", 1)]
|
|
535
|
+
|
|
526
536
|
results.extend(backup_result)
|
|
527
537
|
|
|
528
538
|
logger.info("Starting verification...")
|
|
@@ -752,15 +762,15 @@ def main():
|
|
|
752
762
|
parser.add_argument('-F', '--full-backup', action='store_true', help="Perform a full backup.")
|
|
753
763
|
parser.add_argument('-D', '--differential-backup', action='store_true', help="Perform differential backup.")
|
|
754
764
|
parser.add_argument('-I', '--incremental-backup', action='store_true', help="Perform incremental backup.")
|
|
755
|
-
parser.add_argument('-d', '--backup-definition', help="Specific 'recipe' to select directories and files.")
|
|
756
|
-
parser.add_argument('--alternate-reference-archive', help="DIFF or INCR compared to specified archive.")
|
|
765
|
+
parser.add_argument('-d', '--backup-definition', help="Specific 'recipe' to select directories and files.").completer = backup_definition_completer
|
|
766
|
+
parser.add_argument('--alternate-reference-archive', help="DIFF or INCR compared to specified archive.").completer = list_archive_completer
|
|
757
767
|
parser.add_argument('-c', '--config-file', type=str, help="Path to 'dar-backup.conf'", default='~/.config/dar-backup/dar-backup.conf')
|
|
758
768
|
parser.add_argument('--darrc', type=str, help='Optional path to .darrc')
|
|
759
|
-
parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
|
|
760
|
-
parser.add_argument('--list-contents', help="List the contents of the specified archive.")
|
|
769
|
+
parser.add_argument('-l', '--list', action='store_true', help="List available archives.").completer = list_archive_completer
|
|
770
|
+
parser.add_argument('--list-contents', help="List the contents of the specified archive.").completer = list_archive_completer
|
|
761
771
|
parser.add_argument('--selection', help="dar file selection for listing/restoring specific files/directories.")
|
|
762
772
|
# parser.add_argument('-r', '--restore', nargs=1, type=str, help="Restore specified archive.")
|
|
763
|
-
parser.add_argument('-r', '--restore', type=str, help="Restore specified archive.")
|
|
773
|
+
parser.add_argument('-r', '--restore', type=str, help="Restore specified archive.").completer = list_archive_completer
|
|
764
774
|
parser.add_argument('--restore-dir', type=str, help="Directory to restore files to.")
|
|
765
775
|
parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
|
|
766
776
|
parser.add_argument('--suppress-dar-msg', action='store_true', help="cancel dar options in .darrc: -vt, -vs, -vd, -vf and -va")
|
|
@@ -773,6 +783,8 @@ def main():
|
|
|
773
783
|
parser.add_argument("--changelog", action="store_true", help="Print Changelog.md to stdout and exit.")
|
|
774
784
|
parser.add_argument("--changelog-pretty", action="store_true", help="Print Changelog.md to stdout with Markdown styling and exit.")
|
|
775
785
|
parser.add_argument('-v', '--version', action='store_true', help="Show version and license information.")
|
|
786
|
+
|
|
787
|
+
argcomplete.autocomplete(parser)
|
|
776
788
|
args = parser.parse_args()
|
|
777
789
|
|
|
778
790
|
if args.version:
|
|
@@ -840,6 +852,9 @@ def main():
|
|
|
840
852
|
logger.info(f"START TIME: {start_time}")
|
|
841
853
|
logger.debug(f"`args`:\n{args}")
|
|
842
854
|
logger.debug(f"`config_settings`:\n{config_settings}")
|
|
855
|
+
dar_manager_properties = get_binary_info(command='dar')
|
|
856
|
+
logger.debug(f"dar path: {dar_manager_properties['path']}")
|
|
857
|
+
logger.debug(f"dar version: {dar_manager_properties['version']}")
|
|
843
858
|
|
|
844
859
|
file_dir = os.path.normpath(os.path.dirname(__file__))
|
|
845
860
|
args.verbose and (print(f"Script directory: {file_dir}"))
|
dar_backup/manager.py
CHANGED
|
@@ -20,11 +20,14 @@
|
|
|
20
20
|
This script creates and maintains `dar` databases with catalogs.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
import argcomplete
|
|
24
24
|
import argparse
|
|
25
25
|
import os
|
|
26
26
|
import re
|
|
27
27
|
import sys
|
|
28
|
+
import subprocess
|
|
29
|
+
|
|
30
|
+
from inputimeout import inputimeout, TimeoutOccurred
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
from . import __about__ as about
|
|
@@ -32,9 +35,11 @@ from dar_backup.config_settings import ConfigSettings
|
|
|
32
35
|
from dar_backup.util import setup_logging
|
|
33
36
|
from dar_backup.util import CommandResult
|
|
34
37
|
from dar_backup.util import get_logger
|
|
38
|
+
from dar_backup.util import get_binary_info
|
|
35
39
|
|
|
36
40
|
from dar_backup.command_runner import CommandRunner
|
|
37
41
|
from dar_backup.command_runner import CommandResult
|
|
42
|
+
from dar_backup.util import backup_definition_completer, list_archive_completer, archive_content_completer, add_specific_archive_completer
|
|
38
43
|
|
|
39
44
|
from datetime import datetime
|
|
40
45
|
from time import time
|
|
@@ -82,43 +87,58 @@ def create_db(backup_def: str, config_settings: ConfigSettings):
|
|
|
82
87
|
|
|
83
88
|
return process.returncode
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
def list_catalogs(backup_def: str, config_settings: ConfigSettings) -> NamedTuple:
|
|
90
|
+
def list_catalogs(backup_def: str, config_settings: ConfigSettings, suppress_output=False) -> CommandResult:
|
|
87
91
|
"""
|
|
92
|
+
List catalogs from the database for the given backup definition.
|
|
93
|
+
|
|
88
94
|
Returns:
|
|
89
|
-
|
|
90
|
-
- process: of type subprocess.CompletedProcess: The result of the command execution.
|
|
91
|
-
- stdout: of type str: The standard output of the command.
|
|
92
|
-
- stderr: of type str: The standard error of the command.
|
|
93
|
-
- returncode: of type int: The return code of the command.
|
|
94
|
-
- timeout: of type int: The timeout value in seconds used to run the command.
|
|
95
|
-
- command: of type list[str): The command executed.
|
|
95
|
+
A CommandResult containing the raw stdout/stderr and return code.
|
|
96
96
|
"""
|
|
97
97
|
database = f"{backup_def}{DB_SUFFIX}"
|
|
98
98
|
database_path = os.path.join(config_settings.backup_dir, database)
|
|
99
|
+
|
|
99
100
|
if not os.path.exists(database_path):
|
|
100
101
|
error_msg = f'Database not found: "{database_path}"'
|
|
101
102
|
logger.error(error_msg)
|
|
102
103
|
return CommandResult(1, '', error_msg)
|
|
103
104
|
|
|
104
|
-
# commandResult = CommandResult(
|
|
105
|
-
# process=None,
|
|
106
|
-
# stdout='',
|
|
107
|
-
# stderr=error_msg,
|
|
108
|
-
# returncode=1,
|
|
109
|
-
# timeout=1,
|
|
110
|
-
# command=[])
|
|
111
|
-
|
|
112
|
-
# return commandResult
|
|
113
105
|
command = ['dar_manager', '--base', database_path, '--list']
|
|
114
106
|
process = runner.run(command)
|
|
115
|
-
stdout, stderr = process.stdout, process.stderr
|
|
107
|
+
stdout, stderr = process.stdout, process.stderr
|
|
108
|
+
|
|
116
109
|
if process.returncode != 0:
|
|
117
110
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
118
|
-
logger.error(f"stderr: {stderr}")
|
|
111
|
+
logger.error(f"stderr: {stderr}")
|
|
119
112
|
logger.error(f"stdout: {stdout}")
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
return process
|
|
114
|
+
|
|
115
|
+
# Extract only archive basenames from stdout
|
|
116
|
+
archive_names = []
|
|
117
|
+
for line in stdout.splitlines():
|
|
118
|
+
line = line.strip()
|
|
119
|
+
if not line or "archive #" in line or "dar path" in line or "compression" in line:
|
|
120
|
+
continue
|
|
121
|
+
parts = line.split("\t")
|
|
122
|
+
if len(parts) >= 3:
|
|
123
|
+
archive_names.append(parts[2].strip())
|
|
124
|
+
|
|
125
|
+
# Sort by prefix and date
|
|
126
|
+
def extract_date(arch_name):
|
|
127
|
+
match = re.search(r"(\d{4}-\d{2}-\d{2})", arch_name)
|
|
128
|
+
if match:
|
|
129
|
+
return datetime.strptime(match.group(1), "%Y-%m-%d")
|
|
130
|
+
return datetime.min
|
|
131
|
+
|
|
132
|
+
def sort_key(name):
|
|
133
|
+
prefix = name.split("_", 1)[0]
|
|
134
|
+
return (prefix, extract_date(name))
|
|
135
|
+
|
|
136
|
+
archive_names = sorted(archive_names, key=sort_key)
|
|
137
|
+
|
|
138
|
+
if not suppress_output:
|
|
139
|
+
for name in archive_names:
|
|
140
|
+
print(name)
|
|
141
|
+
|
|
122
142
|
return process
|
|
123
143
|
|
|
124
144
|
|
|
@@ -130,47 +150,65 @@ def cat_no_for_name(archive: str, config_settings: ConfigSettings) -> int:
|
|
|
130
150
|
- the found number, if the archive catalog is present in the database
|
|
131
151
|
- "-1" if the archive is not found
|
|
132
152
|
"""
|
|
153
|
+
|
|
133
154
|
backup_def = backup_def_from_archive(archive)
|
|
134
|
-
process = list_catalogs(backup_def, config_settings)
|
|
155
|
+
process = list_catalogs(backup_def, config_settings, suppress_output=True)
|
|
135
156
|
if process.returncode != 0:
|
|
136
157
|
logger.error(f"Error listing catalogs for backup def: '{backup_def}'")
|
|
137
158
|
return -1
|
|
138
159
|
line_no = 1
|
|
139
160
|
for line in process.stdout.splitlines():
|
|
140
|
-
#print(f"{line_no}: '{line}'")
|
|
141
161
|
line_no += 1
|
|
142
162
|
search = re.search(rf".*?(\d+)\s+.*?({archive}).*", line)
|
|
143
163
|
if search:
|
|
144
|
-
#print(f"FOUND: archive: {search.group(2)}, catalog #: '{search.group(1)}'")
|
|
145
164
|
logger.info(f"Found archive: '{archive}', catalog #: '{search.group(1)}'")
|
|
146
165
|
return int(search.group(1))
|
|
147
166
|
return -1
|
|
148
167
|
|
|
149
168
|
|
|
150
|
-
|
|
151
|
-
def list_archive_contents(archive: str, config_settings: ConfigSettings) -> int :
|
|
169
|
+
def list_archive_contents(archive: str, config_settings: ConfigSettings) -> int:
|
|
152
170
|
"""
|
|
153
|
-
List the contents of a specific archive, given the archive name
|
|
171
|
+
List the contents of a specific archive, given the archive name.
|
|
172
|
+
Prints only actual file entries (lines beginning with '[ Saved ]').
|
|
173
|
+
If none are found, a notice is printed instead.
|
|
154
174
|
"""
|
|
155
175
|
backup_def = backup_def_from_archive(archive)
|
|
156
176
|
database = f"{backup_def}{DB_SUFFIX}"
|
|
157
177
|
database_path = os.path.join(config_settings.backup_dir, database)
|
|
178
|
+
|
|
158
179
|
if not os.path.exists(database_path):
|
|
159
180
|
logger.error(f'Database not found: "{database_path}"')
|
|
160
181
|
return 1
|
|
182
|
+
|
|
161
183
|
cat_no = cat_no_for_name(archive, config_settings)
|
|
162
184
|
if cat_no < 0:
|
|
163
185
|
logger.error(f"archive: '{archive}' not found in database: '{database_path}'")
|
|
164
186
|
return 1
|
|
187
|
+
|
|
188
|
+
|
|
165
189
|
command = ['dar_manager', '--base', database_path, '-u', f"{cat_no}"]
|
|
166
|
-
process = runner.run(command)
|
|
167
|
-
|
|
190
|
+
process = runner.run(command, timeout = 10)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
stdout = process.stdout or ""
|
|
194
|
+
stderr = process.stderr or ""
|
|
195
|
+
|
|
196
|
+
|
|
168
197
|
if process.returncode != 0:
|
|
169
198
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
170
|
-
logger.error(f"stderr: {stderr}")
|
|
199
|
+
logger.error(f"stderr: {stderr}")
|
|
171
200
|
logger.error(f"stdout: {stdout}")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
combined_lines = (stdout + "\n" + stderr).splitlines()
|
|
204
|
+
file_lines = [line for line in combined_lines if line.strip().startswith("[ Saved ]")]
|
|
205
|
+
|
|
206
|
+
if file_lines:
|
|
207
|
+
for line in file_lines:
|
|
208
|
+
print(line)
|
|
172
209
|
else:
|
|
173
|
-
print(
|
|
210
|
+
print(f"[info] Archive '{archive}' is empty.")
|
|
211
|
+
|
|
174
212
|
return process.returncode
|
|
175
213
|
|
|
176
214
|
|
|
@@ -217,42 +255,82 @@ def find_file(file, backup_def, config_settings):
|
|
|
217
255
|
return process.returncode
|
|
218
256
|
|
|
219
257
|
|
|
220
|
-
def add_specific_archive(archive: str, config_settings: ConfigSettings, directory: str =None) -> int:
|
|
221
|
-
|
|
258
|
+
def add_specific_archive(archive: str, config_settings: ConfigSettings, directory: str = None) -> int:
|
|
259
|
+
"""
|
|
260
|
+
Adds the specified archive to its catalog database. Prompts for confirmation if it's older than existing entries.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
0 on success
|
|
264
|
+
1 on failure
|
|
265
|
+
"""
|
|
266
|
+
# Determine archive path
|
|
222
267
|
if not directory:
|
|
223
268
|
directory = config_settings.backup_dir
|
|
224
|
-
archive = os.path.basename(archive) #
|
|
225
|
-
archive_path = os.path.join(directory,
|
|
269
|
+
archive = os.path.basename(archive) # strip path if present
|
|
270
|
+
archive_path = os.path.join(directory, archive)
|
|
271
|
+
archive_test_path = os.path.join(directory, f'{archive}.1.dar')
|
|
226
272
|
|
|
227
|
-
archive_test_path = os.path.join(directory, f'{archive}.1.dar')
|
|
228
273
|
if not os.path.exists(archive_test_path):
|
|
229
274
|
logger.error(f'dar backup: "{archive_test_path}" not found, exiting')
|
|
230
275
|
return 1
|
|
231
|
-
|
|
232
|
-
#
|
|
276
|
+
|
|
277
|
+
# Validate backup definition
|
|
233
278
|
backup_definition = archive.split('_')[0]
|
|
234
279
|
backup_def_path = os.path.join(config_settings.backup_d_dir, backup_definition)
|
|
235
280
|
if not os.path.exists(backup_def_path):
|
|
236
281
|
logger.error(f'backup definition "{backup_definition}" not found (--add-specific-archive option probably not correct), exiting')
|
|
237
282
|
return 1
|
|
238
|
-
|
|
283
|
+
|
|
284
|
+
# Determine catalog DB path
|
|
239
285
|
database = f"{backup_definition}{DB_SUFFIX}"
|
|
240
286
|
database_path = os.path.realpath(os.path.join(config_settings.backup_dir, database))
|
|
287
|
+
|
|
288
|
+
# Safety check: is archive older than latest in catalog?
|
|
289
|
+
try:
|
|
290
|
+
result = subprocess.run(
|
|
291
|
+
["dar_manager", "--base", database_path, "--list"],
|
|
292
|
+
stdout=subprocess.PIPE,
|
|
293
|
+
stderr=subprocess.DEVNULL,
|
|
294
|
+
text=True,
|
|
295
|
+
check=True
|
|
296
|
+
)
|
|
297
|
+
all_lines = result.stdout.splitlines()
|
|
298
|
+
date_pattern = re.compile(r"\d{4}-\d{2}-\d{2}")
|
|
299
|
+
|
|
300
|
+
catalog_dates = [
|
|
301
|
+
datetime.strptime(date_match.group(), "%Y-%m-%d")
|
|
302
|
+
for line in all_lines
|
|
303
|
+
if (date_match := date_pattern.search(line))
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
if catalog_dates:
|
|
307
|
+
latest_date = max(catalog_dates)
|
|
308
|
+
archive_date_match = date_pattern.search(archive)
|
|
309
|
+
if archive_date_match:
|
|
310
|
+
archive_date = datetime.strptime(archive_date_match.group(), "%Y-%m-%d")
|
|
311
|
+
if archive_date < latest_date:
|
|
312
|
+
if not confirm_add_old_archive(archive, latest_date.strftime("%Y-%m-%d")):
|
|
313
|
+
logger.info(f"Archive {archive} skipped due to user declining to add older archive.")
|
|
314
|
+
return 1
|
|
315
|
+
|
|
316
|
+
except subprocess.CalledProcessError:
|
|
317
|
+
logger.warning("Could not determine latest catalog date for chronological check.")
|
|
318
|
+
|
|
241
319
|
logger.info(f'Add "{archive_path}" to catalog: "{database}"')
|
|
242
|
-
|
|
243
|
-
command = ['dar_manager', '--base', database_path, "--add", archive_path, "-Q"]
|
|
320
|
+
|
|
321
|
+
command = ['dar_manager', '--base', database_path, "--add", archive_path, "-Q", "--alter=ignore-order"]
|
|
244
322
|
process = runner.run(command)
|
|
245
323
|
stdout, stderr = process.stdout, process.stderr
|
|
246
324
|
|
|
247
325
|
if process.returncode == 0:
|
|
248
|
-
logger.info(f'"{archive_path}" added to
|
|
326
|
+
logger.info(f'"{archive_path}" added to its catalog')
|
|
249
327
|
elif process.returncode == 5:
|
|
250
|
-
logger.warning(f'Something did not go completely right adding "{archive_path}" to
|
|
251
|
-
else:
|
|
252
|
-
logger.error(f'something went wrong adding "{archive_path}" to
|
|
328
|
+
logger.warning(f'Something did not go completely right adding "{archive_path}" to its catalog, dar_manager error: "{process.returncode}"')
|
|
329
|
+
else:
|
|
330
|
+
logger.error(f'something went wrong adding "{archive_path}" to its catalog, dar_manager error: "{process.returncode}"')
|
|
253
331
|
logger.error(f"stderr: {stderr}")
|
|
254
332
|
logger.error(f"stdout: {stdout}")
|
|
255
|
-
|
|
333
|
+
|
|
256
334
|
return process.returncode
|
|
257
335
|
|
|
258
336
|
|
|
@@ -330,6 +408,31 @@ def backup_def_from_archive(archive: str) -> str:
|
|
|
330
408
|
return None
|
|
331
409
|
|
|
332
410
|
|
|
411
|
+
def confirm_add_old_archive(archive_name: str, latest_known_date: str, timeout_secs: int = 20) -> bool:
|
|
412
|
+
"""
|
|
413
|
+
Confirm with the user if they want to proceed with adding an archive older than the most recent in the catalog.
|
|
414
|
+
Returns True if the user confirms with "yes", False otherwise.
|
|
415
|
+
"""
|
|
416
|
+
try:
|
|
417
|
+
prompt = (
|
|
418
|
+
f"⚠️ Archive '{archive_name}' is older than the latest in the catalog ({latest_known_date}).\n"
|
|
419
|
+
f"Adding older archives may lead to inconsistent restore chains.\n"
|
|
420
|
+
f"Are you sure you want to continue? (yes/no): "
|
|
421
|
+
)
|
|
422
|
+
confirmation = inputimeout(prompt=prompt, timeout=timeout_secs)
|
|
423
|
+
|
|
424
|
+
if confirmation is None:
|
|
425
|
+
logger.info(f"No confirmation received for old archive: {archive_name}. Skipping.")
|
|
426
|
+
return False
|
|
427
|
+
return confirmation.strip().lower() == "yes"
|
|
428
|
+
|
|
429
|
+
except TimeoutOccurred:
|
|
430
|
+
logger.info(f"Timeout waiting for confirmation for old archive: {archive_name}. Skipping.")
|
|
431
|
+
return False
|
|
432
|
+
except KeyboardInterrupt:
|
|
433
|
+
logger.info(f"User interrupted confirmation for old archive: {archive_name}. Skipping.")
|
|
434
|
+
return False
|
|
435
|
+
|
|
333
436
|
|
|
334
437
|
def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> int:
|
|
335
438
|
"""
|
|
@@ -366,12 +469,11 @@ def build_arg_parser():
|
|
|
366
469
|
parser.add_argument('--create-db', action='store_true', help='Create missing databases for all backup definitions')
|
|
367
470
|
parser.add_argument('--alternate-archive-dir', type=str, help='Use this directory instead of BACKUP_DIR in config file')
|
|
368
471
|
parser.add_argument('--add-dir', type=str, help='Add all archive catalogs in this directory to databases')
|
|
369
|
-
parser.add_argument('-d', '--backup-def', type=str, help='Restrict to work only on this backup definition')
|
|
370
|
-
parser.add_argument('--add-specific-archive', type=str, help='Add this archive to catalog database')
|
|
371
|
-
parser.add_argument('--remove-specific-archive', type=str, help='Remove this archive from catalog database')
|
|
472
|
+
parser.add_argument('-d', '--backup-def', type=str, help='Restrict to work only on this backup definition').completer = backup_definition_completer
|
|
473
|
+
parser.add_argument('--add-specific-archive', type=str, help='Add this archive to catalog database').completer = add_specific_archive_completer
|
|
474
|
+
parser.add_argument('--remove-specific-archive', type=str, help='Remove this archive from catalog database').completer = archive_content_completer
|
|
372
475
|
parser.add_argument('-l', '--list-catalogs', action='store_true', help='List catalogs in databases for all backup definitions')
|
|
373
|
-
parser.add_argument('--list-
|
|
374
|
-
parser.add_argument('--list-archive-contents', type=str, help="List contents of the archive's catalog.")
|
|
476
|
+
parser.add_argument('--list-archive-contents', type=str, help="List contents of the archive's catalog. Argument is the archive name.").completer = archive_content_completer
|
|
375
477
|
parser.add_argument('--find-file', type=str, help="List catalogs containing <path>/file. '-d <definition>' argument is also required")
|
|
376
478
|
parser.add_argument('--verbose', action='store_true', help='Be more verbose')
|
|
377
479
|
parser.add_argument('--log-level', type=str, help="`debug` or `trace`, default is `info`", default="info")
|
|
@@ -394,9 +496,10 @@ def main():
|
|
|
394
496
|
return
|
|
395
497
|
|
|
396
498
|
parser = argparse.ArgumentParser(description="Creates/maintains `dar` database catalogs")
|
|
397
|
-
# [parser.add_argument(...) as before...]
|
|
398
|
-
|
|
399
499
|
parser = build_arg_parser()
|
|
500
|
+
|
|
501
|
+
argcomplete.autocomplete(parser)
|
|
502
|
+
|
|
400
503
|
args = parser.parse_args()
|
|
401
504
|
|
|
402
505
|
if args.more_help:
|
|
@@ -426,11 +529,14 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
426
529
|
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
427
530
|
|
|
428
531
|
start_time = int(time())
|
|
429
|
-
logger.info(f"=====================================")
|
|
430
532
|
logger.info(f"{SCRIPTNAME} started, version: {about.__version__}")
|
|
431
533
|
logger.info(f"START TIME: {start_time}")
|
|
432
534
|
logger.debug(f"`args`:\n{args}")
|
|
433
535
|
logger.debug(f"`config_settings`:\n{config_settings}")
|
|
536
|
+
dar_manager_properties = get_binary_info(command='dar_manager')
|
|
537
|
+
logger.debug(f"dar_manager location: {dar_manager_properties['path']}")
|
|
538
|
+
logger.debug(f"dar_manager version: {dar_manager_properties['version']}")
|
|
539
|
+
|
|
434
540
|
|
|
435
541
|
# --- Sanity checks ---
|
|
436
542
|
if args.add_dir and not args.add_dir.strip():
|
|
@@ -475,11 +581,6 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
475
581
|
sys.exit(1)
|
|
476
582
|
return
|
|
477
583
|
|
|
478
|
-
if args.list_catalog_contents and not args.backup_def:
|
|
479
|
-
logger.error(f"--list-catalog-contents requires the --backup-def, exiting")
|
|
480
|
-
sys.exit(1)
|
|
481
|
-
return
|
|
482
|
-
|
|
483
584
|
if args.find_file and not args.backup_def:
|
|
484
585
|
logger.error(f"--find-file requires the --backup-def, exiting")
|
|
485
586
|
sys.exit(1)
|
|
@@ -538,10 +639,6 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
538
639
|
sys.exit(result)
|
|
539
640
|
return
|
|
540
641
|
|
|
541
|
-
if args.list_catalog_contents:
|
|
542
|
-
result = list_catalog_contents(args.list_catalog_contents, args.backup_def, config_settings)
|
|
543
|
-
sys.exit(result)
|
|
544
|
-
return
|
|
545
642
|
|
|
546
643
|
if args.find_file:
|
|
547
644
|
result = find_file(args.find_file, args.backup_def, config_settings)
|
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,11 +19,15 @@ 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
|
|
23
26
|
|
|
24
27
|
from typing import NamedTuple, List
|
|
25
28
|
|
|
29
|
+
|
|
30
|
+
|
|
26
31
|
logger=None
|
|
27
32
|
secondary_logger=None
|
|
28
33
|
|
|
@@ -72,11 +77,9 @@ def setup_logging(log_file: str, command_output_log_file: str, log_level: str =
|
|
|
72
77
|
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
73
78
|
stdout_handler.setFormatter(formatter)
|
|
74
79
|
logger.addHandler(stdout_handler)
|
|
75
|
-
#secondary_logger.addHandler(stdout_handler)
|
|
76
80
|
|
|
77
81
|
return logger
|
|
78
82
|
except Exception as e:
|
|
79
|
-
print("Logging initialization failed.")
|
|
80
83
|
traceback.print_exc()
|
|
81
84
|
sys.exit(1)
|
|
82
85
|
|
|
@@ -98,6 +101,66 @@ def get_logger(command_output_logger: bool = False) -> logging.Logger:
|
|
|
98
101
|
|
|
99
102
|
|
|
100
103
|
|
|
104
|
+
def extract_version(output):
|
|
105
|
+
match = re.search(r'(\d+\.\d+(\.\d+)?)', output)
|
|
106
|
+
return match.group(1) if match else "unknown"
|
|
107
|
+
|
|
108
|
+
def get_binary_info(command):
|
|
109
|
+
"""
|
|
110
|
+
Return information about a binary command.
|
|
111
|
+
Args:
|
|
112
|
+
command (str): The command to check.
|
|
113
|
+
Returns:
|
|
114
|
+
dict: A dictionary containing the command, path, version, and full output.
|
|
115
|
+
Dict structure:
|
|
116
|
+
{
|
|
117
|
+
"command": str,
|
|
118
|
+
"path": str,
|
|
119
|
+
"version": str,
|
|
120
|
+
"full_output": str
|
|
121
|
+
}
|
|
122
|
+
Raises:
|
|
123
|
+
Exception: If there is an error running the command.
|
|
124
|
+
"""
|
|
125
|
+
path = shutil.which(command)
|
|
126
|
+
if path is None:
|
|
127
|
+
return {
|
|
128
|
+
"command": command,
|
|
129
|
+
"path": "Not found",
|
|
130
|
+
"version": "unknown",
|
|
131
|
+
"full_output": ""
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
result = subprocess.run(
|
|
136
|
+
[path, '--version'],
|
|
137
|
+
stdout=subprocess.PIPE,
|
|
138
|
+
stderr=subprocess.PIPE,
|
|
139
|
+
text=True
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Combine output regardless of return code
|
|
143
|
+
combined_output = (result.stdout + result.stderr).strip()
|
|
144
|
+
|
|
145
|
+
# Even if returncode != 0, the version info may still be valid
|
|
146
|
+
version = extract_version(combined_output)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"command": command,
|
|
150
|
+
"path": path,
|
|
151
|
+
"version": version if version else "unknown",
|
|
152
|
+
"full_output": combined_output
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
return {
|
|
157
|
+
"command": command,
|
|
158
|
+
"path": path,
|
|
159
|
+
"version": "error",
|
|
160
|
+
"full_output": str(e)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
101
164
|
def requirements(type: str, config_setting: ConfigSettings):
|
|
102
165
|
"""
|
|
103
166
|
Perform PREREQ or POSTREQ requirements.
|
|
@@ -238,4 +301,186 @@ def list_backups(backup_dir, backup_definition=None):
|
|
|
238
301
|
print(f"{backup.ljust(max_name_length)} : {formatted_size.rjust(max_size_length)} MB")
|
|
239
302
|
|
|
240
303
|
|
|
304
|
+
def expand_path(path: str) -> str:
|
|
305
|
+
"""
|
|
306
|
+
Expand ~ and environment variables like $HOME in a path.
|
|
307
|
+
"""
|
|
308
|
+
return os.path.expanduser(os.path.expandvars(path))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def backup_definition_completer(prefix, parsed_args, **kwargs):
|
|
313
|
+
config_path = getattr(parsed_args, 'config_file', '~/.config/dar-backup/dar-backup.conf')
|
|
314
|
+
config_path = expand_path(config_path)
|
|
315
|
+
config_file = os.path.expanduser(config_path)
|
|
316
|
+
try:
|
|
317
|
+
config = ConfigSettings(config_file)
|
|
318
|
+
backup_d_dir = os.path.expanduser(config.backup_d_dir)
|
|
319
|
+
return [f for f in os.listdir(backup_d_dir) if f.startswith(prefix)]
|
|
320
|
+
except Exception:
|
|
321
|
+
return []
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def extract_backup_definition_fallback() -> str:
|
|
325
|
+
"""
|
|
326
|
+
Extracts --backup-definition or -d value directly from COMP_LINE.
|
|
327
|
+
This is needed because argcomplete doesn't always populate parsed_args fully.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
str: The value of the --backup-definition argument if found, else an empty string.
|
|
331
|
+
"""
|
|
332
|
+
comp_line = os.environ.get("COMP_LINE", "")
|
|
333
|
+
# Match both "--backup-definition VALUE" and "-d VALUE"
|
|
334
|
+
match = re.search(r"(--backup-definition|-d)\s+([^\s]+)", comp_line)
|
|
335
|
+
if match:
|
|
336
|
+
return match.group(2)
|
|
337
|
+
return ""
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def list_archive_completer(prefix, parsed_args, **kwargs):
|
|
342
|
+
import os
|
|
343
|
+
import configparser
|
|
344
|
+
from dar_backup.util import extract_backup_definition_fallback
|
|
345
|
+
|
|
346
|
+
backup_def = getattr(parsed_args, "backup_definition", None) or extract_backup_definition_fallback()
|
|
347
|
+
config_path = getattr(parsed_args, "config_file", None) or "~/.config/dar-backup/dar-backup.conf"
|
|
348
|
+
|
|
349
|
+
config_path = os.path.expanduser(os.path.expandvars(config_path))
|
|
350
|
+
if not os.path.exists(config_path):
|
|
351
|
+
return []
|
|
352
|
+
|
|
353
|
+
config = configparser.ConfigParser()
|
|
354
|
+
config.read(config_path)
|
|
355
|
+
backup_dir = config.get("DIRECTORIES", "BACKUP_DIR", fallback="")
|
|
356
|
+
backup_dir = os.path.expanduser(os.path.expandvars(backup_dir))
|
|
357
|
+
|
|
358
|
+
if not os.path.isdir(backup_dir):
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
files = os.listdir(backup_dir)
|
|
362
|
+
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$")
|
|
363
|
+
|
|
364
|
+
return [
|
|
365
|
+
f.rsplit(".1.dar", 1)[0]
|
|
366
|
+
for f in files
|
|
367
|
+
if archive_re.match(f)
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def archive_content_completer(prefix, parsed_args, **kwargs):
|
|
372
|
+
"""
|
|
373
|
+
Completes archive names from all available *.db files.
|
|
374
|
+
If --backup-def is given, only that one is used.
|
|
375
|
+
Only entries found in the catalog database (via `dar_manager --list`) are shown.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
from dar_backup.config_settings import ConfigSettings
|
|
379
|
+
import subprocess
|
|
380
|
+
import re
|
|
381
|
+
import os
|
|
382
|
+
from datetime import datetime
|
|
383
|
+
|
|
384
|
+
# Expand config path
|
|
385
|
+
config_file = expand_path(getattr(parsed_args, "config_file", "~/.config/dar-backup/dar-backup.conf"))
|
|
386
|
+
config = ConfigSettings(config_file=config_file)
|
|
387
|
+
backup_dir = config.backup_dir
|
|
388
|
+
|
|
389
|
+
# Which db files to inspect?
|
|
390
|
+
backup_def = getattr(parsed_args, "backup_def", None)
|
|
391
|
+
db_files = (
|
|
392
|
+
[os.path.join(backup_dir, f"{backup_def}.db")]
|
|
393
|
+
if backup_def
|
|
394
|
+
else [os.path.join(backup_dir, f) for f in os.listdir(backup_dir) if f.endswith(".db")]
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
completions = []
|
|
398
|
+
|
|
399
|
+
for db_path in db_files:
|
|
400
|
+
if not os.path.exists(db_path):
|
|
401
|
+
continue
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
result = subprocess.run(
|
|
405
|
+
["dar_manager", "--base", db_path, "--list"],
|
|
406
|
+
stdout=subprocess.PIPE,
|
|
407
|
+
stderr=subprocess.DEVNULL,
|
|
408
|
+
text=True,
|
|
409
|
+
check=True
|
|
410
|
+
)
|
|
411
|
+
except subprocess.CalledProcessError:
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
for line in result.stdout.splitlines():
|
|
415
|
+
parts = line.strip().split("\t")
|
|
416
|
+
if len(parts) >= 3:
|
|
417
|
+
archive = parts[2].strip()
|
|
418
|
+
if archive.startswith(prefix):
|
|
419
|
+
completions.append(archive)
|
|
420
|
+
|
|
421
|
+
# Sort: first by name (before first '_'), then by date (YYYY-MM-DD)
|
|
422
|
+
def sort_key(archive):
|
|
423
|
+
name_part = archive.split("_")[0]
|
|
424
|
+
date_match = re.search(r"\d{4}-\d{2}-\d{2}", archive)
|
|
425
|
+
date = datetime.strptime(date_match.group(0), "%Y-%m-%d") if date_match else datetime.min
|
|
426
|
+
return (name_part, date)
|
|
427
|
+
|
|
428
|
+
completions = sorted(set(completions), key=sort_key)
|
|
429
|
+
return completions or ["[no matching archives]"]
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def add_specific_archive_completer(prefix, parsed_args, **kwargs):
|
|
434
|
+
"""
|
|
435
|
+
Autocompletes archives that are present in the BACKUP_DIR
|
|
436
|
+
but not yet present in the <backup_def>.db catalog.
|
|
437
|
+
If --backup-def is provided, restrict suggestions to that.
|
|
438
|
+
"""
|
|
439
|
+
from dar_backup.config_settings import ConfigSettings
|
|
440
|
+
import subprocess
|
|
441
|
+
import re
|
|
442
|
+
import os
|
|
443
|
+
from datetime import datetime
|
|
444
|
+
|
|
445
|
+
config_file = expand_path(getattr(parsed_args, "config_file", "~/.config/dar-backup/dar-backup.conf"))
|
|
446
|
+
config = ConfigSettings(config_file=config_file)
|
|
447
|
+
backup_dir = config.backup_dir
|
|
448
|
+
backup_def = getattr(parsed_args, "backup_def", None)
|
|
449
|
+
|
|
450
|
+
# Match pattern for archive base names: e.g. test_FULL_2025-04-01
|
|
451
|
+
dar_pattern = re.compile(r"^(.*?_(FULL|DIFF|INCR)_(\d{4}-\d{2}-\d{2}))\.1\.dar$")
|
|
452
|
+
|
|
453
|
+
# Step 1: scan backup_dir for .1.dar files
|
|
454
|
+
all_archives = set()
|
|
455
|
+
for fname in os.listdir(backup_dir):
|
|
456
|
+
match = dar_pattern.match(fname)
|
|
457
|
+
if match:
|
|
458
|
+
base = match.group(1)
|
|
459
|
+
if base.startswith(prefix):
|
|
460
|
+
if not backup_def or base.startswith(f"{backup_def}_"):
|
|
461
|
+
all_archives.add(base)
|
|
462
|
+
|
|
463
|
+
# Step 2: exclude ones already present in the .db
|
|
464
|
+
db_path = os.path.join(backup_dir, f"{backup_def}.db") if backup_def else None
|
|
465
|
+
existing = set()
|
|
466
|
+
|
|
467
|
+
if db_path and os.path.exists(db_path):
|
|
468
|
+
try:
|
|
469
|
+
result = subprocess.run(
|
|
470
|
+
["dar_manager", "--base", db_path, "--list"],
|
|
471
|
+
stdout=subprocess.PIPE,
|
|
472
|
+
stderr=subprocess.DEVNULL,
|
|
473
|
+
text=True,
|
|
474
|
+
check=True
|
|
475
|
+
)
|
|
476
|
+
for line in result.stdout.splitlines():
|
|
477
|
+
parts = line.strip().split("\t")
|
|
478
|
+
if len(parts) >= 3:
|
|
479
|
+
existing.add(parts[2].strip())
|
|
480
|
+
except subprocess.CalledProcessError:
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
# Step 3: return filtered list
|
|
484
|
+
candidates = sorted(archive for archive in all_archives if archive not in existing)
|
|
485
|
+
return candidates or ["[no new archives]"]
|
|
241
486
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dar-backup
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.19
|
|
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,21 @@
|
|
|
1
|
+
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
+
dar_backup/Changelog.md,sha256=PbR2JupEHiWdRNP8ApuWEYIrRQEeJQUnlkVR7N4JJHg,8591
|
|
3
|
+
dar_backup/README.md,sha256=PcZQRaBw9i_GbRoA786OFKI0PwY87E7D30UzovWqEQQ,44902
|
|
4
|
+
dar_backup/__about__.py,sha256=1xdEgfhhUWpssnr_9DTiORat5DDv6NUXRtydGkm-Aog,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=mrpu9xAy6AKcEbjMetscEh-IzJp0xVFObdrwaXncFtA,12712
|
|
8
|
+
dar_backup/command_runner.py,sha256=PQA968EXssSGjSs_16psFkdxRZi9-YK4TrBKFz0ss3k,4455
|
|
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=Uy0VAFRbOAmZCyZyyEfWuIM4he2WivfQXhKzVGhWbHc,41939
|
|
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=t-CWxvxzhMHCOCU99px6heL3dRmil2JDOyw9PHL96xY,25712
|
|
15
|
+
dar_backup/rich_progress.py,sha256=jTwM-4VlqHHzKqIfyXjL1pWEriobSJwCdN3YXzXzRdo,3105
|
|
16
|
+
dar_backup/util.py,sha256=REZANvqLqczx3MvGszpVaesrpW03NuH_GPGjDllpT4Q,16877
|
|
17
|
+
dar_backup-0.6.19.dist-info/METADATA,sha256=jQwLE815tTEXjKh7l0rUd5SprMF_czZ3QPRXn8TNMnM,86624
|
|
18
|
+
dar_backup-0.6.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
19
|
+
dar_backup-0.6.19.dist-info/entry_points.txt,sha256=NSCYoG5Dvh1UhvKWOQPgcHdFv4--R4Sre3d9FwJra3E,258
|
|
20
|
+
dar_backup-0.6.19.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
21
|
+
dar_backup-0.6.19.dist-info/RECORD,,
|
|
@@ -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
|
|
File without changes
|