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/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
- def run_installer(config_file: str, create_db_flag: bool):
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 based on the config's specified log file
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
- log_stdout=True,
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
- # Optionally create databases
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
- def installer_main():
157
+
158
+ def main():
49
159
  parser = argparse.ArgumentParser(description="dar-backup installer")
50
- parser.add_argument("--config", required=True, help="Path to config file")
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
- run_installer(args.config, args.create_db)
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
- installer_main()
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(("--cleanup-specific-archives:", args.cleanup_specific_archives))
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():
@@ -1,3 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
1
4
  import os
2
5
  import time
3
6
  from threading import Event
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
- import dar_backup.__about__ as about
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('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
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
+