dar-backup 0.6.20.1__py3-none-any.whl → 0.7.1__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 +29 -0
- dar_backup/README.md +720 -260
- dar_backup/__about__.py +6 -1
- dar_backup/clean_log.py +2 -0
- dar_backup/cleanup.py +4 -1
- dar_backup/command_runner.py +2 -0
- dar_backup/config_settings.py +2 -0
- dar_backup/dar-backup.conf +4 -2
- dar_backup/dar-backup.conf.j2 +60 -0
- dar_backup/dar_backup.py +30 -11
- dar_backup/dar_backup_systemd.py +3 -0
- dar_backup/demo.py +154 -81
- dar_backup/demo_backup_def.j2 +62 -0
- dar_backup/exceptions.py +2 -0
- dar_backup/installer.py +149 -19
- dar_backup/manager.py +6 -3
- dar_backup/rich_progress.py +3 -0
- dar_backup/util.py +68 -8
- {dar_backup-0.6.20.1.dist-info → dar_backup-0.7.1.dist-info}/METADATA +722 -261
- dar_backup-0.7.1.dist-info/RECORD +25 -0
- {dar_backup-0.6.20.1.dist-info → dar_backup-0.7.1.dist-info}/entry_points.txt +1 -0
- dar_backup-0.6.20.1.dist-info/RECORD +0 -23
- {dar_backup-0.6.20.1.dist-info → dar_backup-0.7.1.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.20.1.dist-info → dar_backup-0.7.1.dist-info}/licenses/LICENSE +0 -0
dar_backup/installer.py
CHANGED
|
@@ -1,58 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
1
4
|
import argparse
|
|
2
5
|
import os
|
|
6
|
+
from . import __about__ as about
|
|
3
7
|
from pathlib import Path
|
|
4
8
|
from dar_backup.config_settings import ConfigSettings
|
|
5
9
|
from dar_backup.util import setup_logging, get_logger
|
|
6
10
|
from dar_backup.command_runner import CommandRunner
|
|
7
11
|
from dar_backup.manager import create_db
|
|
12
|
+
# Always expand manager DB dir correctly, using helper function
|
|
13
|
+
from dar_backup.manager import get_db_dir
|
|
14
|
+
from dar_backup.util import expand_path
|
|
15
|
+
|
|
16
|
+
def install_autocompletion():
|
|
17
|
+
"""Detect user shell, choose RC file, and idempotently append autocompletion."""
|
|
18
|
+
shell = Path(os.environ.get("SHELL", "")).name
|
|
19
|
+
home = Path.home()
|
|
20
|
+
|
|
21
|
+
# pick RC file based on shell
|
|
22
|
+
if shell == "zsh":
|
|
23
|
+
rc_file = home / ".zshrc"
|
|
24
|
+
elif shell == "bash":
|
|
25
|
+
# prefer ~/.bash_profile on macOS if present
|
|
26
|
+
rc_file = home / ".bash_profile" if (home / ".bash_profile").exists() else home / ".bashrc"
|
|
27
|
+
else:
|
|
28
|
+
rc_file = home / ".bashrc"
|
|
29
|
+
|
|
30
|
+
marker = "# >>> dar-backup autocompletion >>>"
|
|
31
|
+
end_marker = "# <<< dar-backup autocompletion <<<"
|
|
32
|
+
|
|
33
|
+
block = "\n".join([
|
|
34
|
+
marker,
|
|
35
|
+
'eval "$(register-python-argcomplete dar-backup)"',
|
|
36
|
+
'eval "$(register-python-argcomplete cleanup)"',
|
|
37
|
+
'eval "$(register-python-argcomplete manager)"',
|
|
38
|
+
"#complete -o nosort -C 'python -m argcomplete cleanup' cleanup",
|
|
39
|
+
"#complete -o nosort -C 'python -m argcomplete manager' manager",
|
|
40
|
+
end_marker,
|
|
41
|
+
]) + "\n"
|
|
42
|
+
|
|
43
|
+
# ensure RC file and parent directory exist
|
|
44
|
+
rc_file.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
if not rc_file.exists():
|
|
46
|
+
rc_file.touch()
|
|
47
|
+
|
|
48
|
+
content = rc_file.read_text()
|
|
49
|
+
if marker in content:
|
|
50
|
+
print(f"Autocompletion already installed in {rc_file}")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# append the autocompletion block
|
|
54
|
+
rc_file.open("a").write("\n" + block)
|
|
55
|
+
print(f"✔️ Appended autocompletion block to {rc_file}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def uninstall_autocompletion() -> str:
|
|
60
|
+
"""Remove previously installed autocompletion block from shell RC file."""
|
|
61
|
+
shell = Path(os.environ.get("SHELL", "")).name
|
|
62
|
+
home = Path.home()
|
|
63
|
+
|
|
64
|
+
# pick RC file based on shell
|
|
65
|
+
if shell == "zsh":
|
|
66
|
+
rc_file = home / ".zshrc"
|
|
67
|
+
elif shell == "bash":
|
|
68
|
+
rc_file = home / ".bash_profile" if (home / ".bash_profile").exists() else home / ".bashrc"
|
|
69
|
+
else:
|
|
70
|
+
rc_file = home / ".bashrc"
|
|
71
|
+
|
|
72
|
+
marker = "# >>> dar-backup autocompletion >>>"
|
|
73
|
+
end_marker = "# <<< dar-backup autocompletion <<<"
|
|
74
|
+
|
|
75
|
+
if not rc_file.exists():
|
|
76
|
+
print(f"❌ RC file not found: {rc_file}")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
content = rc_file.read_text()
|
|
80
|
+
if marker not in content:
|
|
81
|
+
print(f"No autocompletion block found in {rc_file}")
|
|
82
|
+
return f"No autocompletion block found in {rc_file}" # for unit test
|
|
8
83
|
|
|
84
|
+
lines = content.splitlines(keepends=True)
|
|
85
|
+
new_lines = []
|
|
86
|
+
skipping = False
|
|
87
|
+
for line in lines:
|
|
88
|
+
if marker in line:
|
|
89
|
+
skipping = True
|
|
90
|
+
continue
|
|
91
|
+
if end_marker in line and skipping:
|
|
92
|
+
skipping = False
|
|
93
|
+
continue
|
|
94
|
+
if not skipping:
|
|
95
|
+
new_lines.append(line)
|
|
9
96
|
|
|
10
|
-
|
|
97
|
+
rc_file.write_text(''.join(new_lines))
|
|
98
|
+
print(f"✔️ Removed autocompletion block from {rc_file}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_installer(config_file: str, create_db_flag: bool, install_ac_flag: bool):
|
|
103
|
+
"""
|
|
104
|
+
Run the installation process for dar-backup using the given config file.
|
|
105
|
+
|
|
106
|
+
This includes:
|
|
107
|
+
- Expanding and parsing the config file
|
|
108
|
+
- Setting up logging
|
|
109
|
+
- Creating required backup directories
|
|
110
|
+
- Optionally initializing catalog databases for all backup definitions
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
config_file (str): Path to the configuration file (may include ~ or env vars).
|
|
114
|
+
create_db_flag (bool): If True, databases are initialized for each backup definition.
|
|
115
|
+
"""
|
|
11
116
|
config_file = os.path.expanduser(os.path.expandvars(config_file))
|
|
12
117
|
config_settings = ConfigSettings(config_file)
|
|
13
118
|
|
|
14
|
-
# Set up logging
|
|
119
|
+
# Set up logging
|
|
15
120
|
command_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
16
121
|
logger = setup_logging(
|
|
17
122
|
config_settings.logfile_location,
|
|
18
123
|
command_log,
|
|
19
124
|
log_level="info",
|
|
20
|
-
|
|
125
|
+
log_to_stdout=True,
|
|
21
126
|
)
|
|
22
127
|
command_logger = get_logger(command_output_logger=True)
|
|
23
128
|
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
24
129
|
|
|
25
|
-
# Create directories listed in config
|
|
26
|
-
for attr in ["backup_dir", "test_restore_dir", "backup_d_dir", "manager_db_dir"]:
|
|
27
|
-
path = getattr(config_settings, attr, None)
|
|
28
|
-
if path:
|
|
29
|
-
dir_path = Path(path).expanduser()
|
|
30
|
-
if not dir_path.exists():
|
|
31
|
-
dir_path.mkdir(parents=True, exist_ok=True)
|
|
32
|
-
print(f"Created directory: {dir_path}")
|
|
33
|
-
else:
|
|
34
|
-
print(f"Directory already exists: {dir_path}")
|
|
35
130
|
|
|
36
|
-
#
|
|
131
|
+
# Create required directories
|
|
132
|
+
required_dirs = {
|
|
133
|
+
"backup_dir": config_settings.backup_dir,
|
|
134
|
+
"test_restore_dir": config_settings.test_restore_dir,
|
|
135
|
+
"backup_d_dir": config_settings.backup_d_dir,
|
|
136
|
+
"manager_db_dir": get_db_dir(config_settings),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for name, dir_path in required_dirs.items():
|
|
140
|
+
expanded = Path(expand_path(dir_path))
|
|
141
|
+
if not expanded.exists():
|
|
142
|
+
logger.info(f"Creating directory: {expanded} ({name})")
|
|
143
|
+
expanded.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
|
|
145
|
+
# Optionally create databases for all backup definitions
|
|
37
146
|
if create_db_flag:
|
|
38
147
|
for file in os.listdir(config_settings.backup_d_dir):
|
|
39
148
|
backup_def = os.path.basename(file)
|
|
40
149
|
print(f"Creating catalog for: {backup_def}")
|
|
41
|
-
result = create_db(backup_def, config_settings, logger)
|
|
150
|
+
result = create_db(backup_def, config_settings, logger, runner)
|
|
42
151
|
if result == 0:
|
|
43
152
|
print(f"✔️ Catalog created (or already existed): {backup_def}")
|
|
44
153
|
else:
|
|
45
154
|
print(f"❌ Failed to create catalog: {backup_def}")
|
|
46
155
|
|
|
47
156
|
|
|
48
|
-
|
|
157
|
+
|
|
158
|
+
def main():
|
|
49
159
|
parser = argparse.ArgumentParser(description="dar-backup installer")
|
|
50
|
-
parser.add_argument("--config", required=
|
|
160
|
+
parser.add_argument("--config", required=False, help="Path to config file")
|
|
51
161
|
parser.add_argument("--create-db", action="store_true", help="Create catalog databases")
|
|
162
|
+
group = parser.add_mutually_exclusive_group()
|
|
163
|
+
group.add_argument(
|
|
164
|
+
"--install-autocompletion", action="store_true",
|
|
165
|
+
help="Append shell-completion setup to your shell RC"
|
|
166
|
+
)
|
|
167
|
+
group.add_argument(
|
|
168
|
+
"--remove-autocompletion", action="store_true",
|
|
169
|
+
help="Remove shell-completion setup from your shell RC"
|
|
170
|
+
)
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
"-v", "--version", action="version",
|
|
173
|
+
version=f"%(prog)s version {about.__version__}, {about.__license__}"
|
|
174
|
+
)
|
|
175
|
+
|
|
52
176
|
args = parser.parse_args()
|
|
53
177
|
|
|
54
|
-
|
|
178
|
+
|
|
179
|
+
if args.config:
|
|
180
|
+
run_installer(args.config, args.create_db)
|
|
181
|
+
elif args.install_autocompletion:
|
|
182
|
+
install_autocompletion()
|
|
183
|
+
elif args.remove_autocompletion:
|
|
184
|
+
uninstall_autocompletion()
|
|
55
185
|
|
|
56
186
|
|
|
57
187
|
if __name__ == "__main__":
|
|
58
|
-
|
|
188
|
+
main()
|
dar_backup/manager.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
3
|
|
|
3
4
|
"""
|
|
4
5
|
Copyright (C) 2024 Per Jensen
|
|
@@ -37,6 +38,7 @@ from dar_backup.util import CommandResult
|
|
|
37
38
|
from dar_backup.util import get_logger
|
|
38
39
|
from dar_backup.util import get_binary_info
|
|
39
40
|
from dar_backup.util import show_version
|
|
41
|
+
from dar_backup.util import get_invocation_command_line
|
|
40
42
|
from dar_backup.util import print_aligned_settings
|
|
41
43
|
|
|
42
44
|
from dar_backup.command_runner import CommandRunner
|
|
@@ -503,7 +505,7 @@ def build_arg_parser():
|
|
|
503
505
|
parser.add_argument('--log-level', type=str, help="`debug` or `trace`, default is `info`", default="info")
|
|
504
506
|
parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
|
|
505
507
|
parser.add_argument('--more-help', action='store_true', help='Show extended help message')
|
|
506
|
-
parser.add_argument('--version', action='store_true', help='Show version & license')
|
|
508
|
+
parser.add_argument('-v', '--version', action='store_true', help='Show version & license')
|
|
507
509
|
|
|
508
510
|
return parser
|
|
509
511
|
|
|
@@ -551,18 +553,19 @@ def main():
|
|
|
551
553
|
start_time = int(time())
|
|
552
554
|
start_msgs.append((f"{SCRIPTNAME}:", about.__version__))
|
|
553
555
|
logger.info(f"START TIME: {start_time}")
|
|
556
|
+
logger.debug(f"Command line: {get_invocation_command_line()}")
|
|
554
557
|
logger.debug(f"`args`:\n{args}")
|
|
555
558
|
logger.debug(f"`config_settings`:\n{config_settings}")
|
|
556
559
|
start_msgs.append(("Config file:", args.config_file))
|
|
557
560
|
args.verbose and start_msgs.append(("Backup dir:", config_settings.backup_dir))
|
|
558
561
|
start_msgs.append(("Logfile:", config_settings.logfile_location))
|
|
559
562
|
args.verbose and start_msgs.append(("--alternate-archive-dir:", args.alternate_archive_dir))
|
|
560
|
-
args.verbose and start_msgs.append(("--
|
|
563
|
+
args.verbose and start_msgs.append(("--remove-specific-archive:", args.remove_specific_archive))
|
|
561
564
|
dar_manager_properties = get_binary_info(command='dar_manager')
|
|
562
565
|
start_msgs.append(("dar_manager:", dar_manager_properties['path']))
|
|
563
566
|
start_msgs.append(("dar_manager v.:", dar_manager_properties['version']))
|
|
564
567
|
|
|
565
|
-
print_aligned_settings(start_msgs)
|
|
568
|
+
print_aligned_settings(start_msgs, quiet=not args.verbose)
|
|
566
569
|
|
|
567
570
|
# --- Sanity checks ---
|
|
568
571
|
if args.add_dir and not args.add_dir.strip():
|
dar_backup/rich_progress.py
CHANGED
dar_backup/util.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
util.py source code is here: https://github.com/per2jensen/dar-backup
|
|
3
5
|
|
|
@@ -19,11 +21,14 @@ import shutil
|
|
|
19
21
|
import sys
|
|
20
22
|
import threading
|
|
21
23
|
import traceback
|
|
24
|
+
|
|
25
|
+
import dar_backup.__about__ as about
|
|
26
|
+
|
|
27
|
+
|
|
22
28
|
from argcomplete.completers import ChoicesCompleter
|
|
23
29
|
from datetime import datetime
|
|
24
30
|
from dar_backup.config_settings import ConfigSettings
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
from pathlib import Path
|
|
27
32
|
from rich.console import Console
|
|
28
33
|
from rich.text import Text
|
|
29
34
|
|
|
@@ -119,14 +124,33 @@ completer_logger = _setup_completer_logger()
|
|
|
119
124
|
completer_logger.debug("Completer logger initialized.")
|
|
120
125
|
|
|
121
126
|
|
|
127
|
+
def get_invocation_command_line() -> str:
|
|
128
|
+
"""
|
|
129
|
+
Safely retrieves the exact command line used to invoke the current Python process.
|
|
130
|
+
|
|
131
|
+
On Unix-like systems, this reads from /proc/[pid]/cmdline to reconstruct the
|
|
132
|
+
command with interpreter and arguments. If any error occurs (e.g., file not found,
|
|
133
|
+
permission denied, non-Unix platform), it returns a descriptive error message.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
str: The full command line string, or an error description if it cannot be retrieved.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
cmdline_path = f"/proc/{os.getpid()}/cmdline"
|
|
140
|
+
with open(cmdline_path, "rb") as f:
|
|
141
|
+
content = f.read()
|
|
142
|
+
if not content:
|
|
143
|
+
return "[error: /proc/cmdline is empty]"
|
|
144
|
+
return content.replace(b'\x00', b' ').decode().strip()
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return f"[error: could not read /proc/[pid]/cmdline: {e}]"
|
|
147
|
+
|
|
122
148
|
|
|
123
149
|
def show_version():
|
|
124
150
|
script_name = os.path.basename(sys.argv[0])
|
|
125
151
|
print(f"{script_name} {about.__version__}")
|
|
126
152
|
print(f"{script_name} source code is here: https://github.com/per2jensen/dar-backup")
|
|
127
|
-
print(
|
|
128
|
-
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
129
|
-
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
153
|
+
print(about.__license__)
|
|
130
154
|
|
|
131
155
|
def extract_version(output):
|
|
132
156
|
match = re.search(r'(\d+\.\d+(\.\d+)?)', output)
|
|
@@ -263,6 +287,28 @@ class CommandResult(NamedTuple):
|
|
|
263
287
|
|
|
264
288
|
def list_backups(backup_dir, backup_definition=None):
|
|
265
289
|
"""
|
|
290
|
+
Lists the available backup files in the specified directory along with their total sizes in megabytes.
|
|
291
|
+
The function filters and processes `.dar` files, grouping them by their base names and ensuring proper
|
|
292
|
+
alignment of the displayed sizes.
|
|
293
|
+
Args:
|
|
294
|
+
backup_dir (str): The directory containing the backup files.
|
|
295
|
+
backup_definition (str, optional): A prefix to filter backups by their base name. Only backups
|
|
296
|
+
starting with this prefix will be included. Defaults to None.
|
|
297
|
+
Raises:
|
|
298
|
+
locale.Error: If setting the locale fails and the fallback to the 'C' locale is unsuccessful.
|
|
299
|
+
Behavior:
|
|
300
|
+
- Attempts to set the locale based on the environment for proper formatting of numbers.
|
|
301
|
+
- Filters `.dar` files in the specified directory based on the following criteria:
|
|
302
|
+
- The file name must contain one of the substrings: "_FULL_", "_DIFF_", or "_INCR_".
|
|
303
|
+
- The file name must include a date in the format "_YYYY-MM-DD".
|
|
304
|
+
- Groups files by their base name (excluding slice numbers and extensions) and calculates
|
|
305
|
+
the total size for each group in megabytes.
|
|
306
|
+
- Sorts the backups by their base name and date (if included in the name).
|
|
307
|
+
- Prints the backup names and their sizes in a formatted and aligned manner.
|
|
308
|
+
Returns:
|
|
309
|
+
None: The function prints the results directly to the console. If no backups are found,
|
|
310
|
+
it prints "No backups available.".
|
|
311
|
+
|
|
266
312
|
List the available backups in the specified directory and their sizes in megabytes, with aligned sizes.
|
|
267
313
|
"""
|
|
268
314
|
# Attempt to set locale from the environment or fall back to the default locale
|
|
@@ -565,6 +611,7 @@ def print_aligned_settings(
|
|
|
565
611
|
settings: List[Tuple[str, str]],
|
|
566
612
|
log: bool = True,
|
|
567
613
|
header: str = "Startup Settings",
|
|
614
|
+
quiet: bool = True,
|
|
568
615
|
highlight_keywords: List[str] = None
|
|
569
616
|
) -> None:
|
|
570
617
|
"""
|
|
@@ -583,7 +630,7 @@ def print_aligned_settings(
|
|
|
583
630
|
header_line = f"========== {header} =========="
|
|
584
631
|
footer_line = "=" * len(header_line)
|
|
585
632
|
|
|
586
|
-
console.print(f"[bold cyan]{header_line}[/bold cyan]")
|
|
633
|
+
not quiet and console.print(f"[bold cyan]{header_line}[/bold cyan]")
|
|
587
634
|
if log and logger:
|
|
588
635
|
logger.info(header_line)
|
|
589
636
|
|
|
@@ -613,13 +660,26 @@ def print_aligned_settings(
|
|
|
613
660
|
|
|
614
661
|
line_text.append(text, style="white")
|
|
615
662
|
|
|
616
|
-
console.print(line_text)
|
|
663
|
+
not quiet and console.print(line_text)
|
|
617
664
|
|
|
618
665
|
# Always log clean text (no [!] in log)
|
|
619
666
|
final_line_for_log = f"{padded_label} {text}"
|
|
620
667
|
if log and logger:
|
|
621
668
|
logger.info(final_line_for_log)
|
|
622
669
|
|
|
623
|
-
console.print(f"[bold cyan]{footer_line}[/bold cyan]")
|
|
670
|
+
not quiet and console.print(f"[bold cyan]{footer_line}[/bold cyan]")
|
|
624
671
|
if log and logger:
|
|
625
672
|
logger.info(footer_line)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def normalize_dir(path: str) -> str:
|
|
678
|
+
"""
|
|
679
|
+
Strip any trailing slash/backslash but leave root (“/” or “C:\\”) intact.
|
|
680
|
+
"""
|
|
681
|
+
p = Path(path)
|
|
682
|
+
# Path(__str__) drops any trailing separators
|
|
683
|
+
normalized = str(p)
|
|
684
|
+
return normalized
|
|
685
|
+
|