dar-backup 0.6.20__py3-none-any.whl → 0.6.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dar_backup/__about__.py CHANGED
@@ -1 +1,6 @@
1
- __version__ = "0.6.20"
1
+ __version__ = "0.6.21"
2
+
3
+ __license__ = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
4
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5
+ See section 15 and section 16 in the supplied "LICENSE" file.'''
6
+
dar_backup/cleanup.py CHANGED
@@ -37,6 +37,7 @@ from dar_backup.util import setup_logging
37
37
  from dar_backup.util import get_logger
38
38
  from dar_backup.util import requirements
39
39
  from dar_backup.util import show_version
40
+ from dar_backup.util import get_invocation_command_line
40
41
  from dar_backup.util import print_aligned_settings
41
42
  from dar_backup.util import backup_definition_completer, list_archive_completer
42
43
 
@@ -222,6 +223,7 @@ def main():
222
223
  start_msgs.append(("cleanup.py:", about.__version__))
223
224
 
224
225
  logger.info(f"START TIME: {start_time}")
226
+ logger.debug(f"Command line: {get_invocation_command_line()}")
225
227
  logger.debug(f"`args`:\n{args}")
226
228
  logger.debug(f"`config_settings`:\n{config_settings}")
227
229
 
dar_backup/dar_backup.py CHANGED
@@ -51,6 +51,7 @@ from dar_backup.util import BackupError
51
51
  from dar_backup.util import RestoreError
52
52
  from dar_backup.util import requirements
53
53
  from dar_backup.util import show_version
54
+ from dar_backup.util import get_invocation_command_line
54
55
  from dar_backup.util import get_binary_info
55
56
  from dar_backup.util import print_aligned_settings
56
57
  from dar_backup.util import backup_definition_completer, list_archive_completer
@@ -683,12 +684,29 @@ INCR back of a single backup definition in backup.d
683
684
 
684
685
 
685
686
  --selection
686
- --selection takes dar selection parameters between a pair of `"`.
687
687
 
688
- Example: select file names with this date in file names "2024-07-01" in the
689
- directory "path/to/a/dir" where the path is relative to root of the backup.
688
+ --selection takes dar file selection options inside a quoted string.
689
+
690
+ 💡 Shell quoting matters! Always wrap the entire selection string in double quotes to avoid shell splitting.
691
+
692
+ ✅ Use: --selection="-I '*.NEF'"
693
+ ❌ Avoid: --selection "-I '*.NEF'" → may break due to how your shell parses it.
694
+
695
+ Examples:
696
+ 1)
697
+ select file names with "Z50_" in file names:
698
+ python3 dar-backup.py --restore <name of dar archive> --selection="-I '*Z50_*'"
699
+ 2)
700
+ Filter out *.xmp files:
701
+ python3 dar-backup.py --restore <name of dar archive> --selection="-X '*.xmp'"
702
+
703
+ 3)
704
+ Include all files in a directory:
705
+ python3 dar-backup.py --restore <name of dar archive> --selection="-g 'path/to/a/dir'"
690
706
 
691
- python3 dar-backup.py --restore <name of dar archive> --selection "-I '*2024-07-01*' -g path/to/a/dir"
707
+ 4)
708
+ Exclude a directory:
709
+ python3 dar-backup.py --restore <name of dar archive> --selection="-P 'path/to/a/dir'"
692
710
 
693
711
  See dar documentation on file selection: http://dar.linux.free.fr/doc/man/dar.html#COMMANDS%20AND%20OPTIONS
694
712
  """
@@ -764,7 +782,7 @@ def main():
764
782
  parser.add_argument('--darrc', type=str, help='Optional path to .darrc')
765
783
  parser.add_argument('-l', '--list', action='store_true', help="List available archives.").completer = list_archive_completer
766
784
  parser.add_argument('--list-contents', help="List the contents of the specified archive.").completer = list_archive_completer
767
- parser.add_argument('--selection', help="dar file selection for listing/restoring specific files/directories.")
785
+ parser.add_argument('--selection', type=str, help="Selection string to pass to 'dar', e.g. --selection=\"-I '*.NEF'\"")
768
786
  # parser.add_argument('-r', '--restore', nargs=1, type=str, help="Restore specified archive.")
769
787
  parser.add_argument('-r', '--restore', type=str, help="Restore specified archive.").completer = list_archive_completer
770
788
  parser.add_argument('--restore-dir', type=str, help="Directory to restore files to.")
@@ -847,6 +865,7 @@ def main():
847
865
  start_time=int(time())
848
866
  start_msgs.append(('dar-backup.py:', about.__version__))
849
867
  logger.info(f"START TIME: {start_time}")
868
+ logger.debug(f"Command line: {get_invocation_command_line()}")
850
869
  logger.debug(f"{'`Args`:\n'}{args}")
851
870
  logger.debug(f"{'`Config_settings`:\n'}{config_settings}")
852
871
  dar_properties = get_binary_info(command='dar')
dar_backup/demo.py CHANGED
@@ -19,11 +19,8 @@ import shutil
19
19
  import sys
20
20
 
21
21
  from . import __about__ as about
22
- from pathlib import Path
23
22
 
24
- LICENSE = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
25
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
26
- See section 15 and section 16 in the supplied "LICENSE" file.'''
23
+ from pathlib import Path
27
24
 
28
25
  CONFIG_DIR = os.path.expanduser("~/.config/dar-backup")
29
26
  DAR_BACKUP_DIR = os.path.expanduser("~/dar-backup/")
@@ -76,7 +73,7 @@ def main():
76
73
  parser.add_argument(
77
74
  "-v", "--version",
78
75
  action="version",
79
- version=f"%(prog)s version {about.__version__}, {LICENSE}"
76
+ version=f"%(prog)s version {about.__version__}, {about.__license__}"
80
77
  )
81
78
 
82
79
  args = parser.parse_args()
dar_backup/installer.py CHANGED
@@ -1,58 +1,83 @@
1
1
  import argparse
2
2
  import os
3
+ from . import __about__ as about
3
4
  from pathlib import Path
4
5
  from dar_backup.config_settings import ConfigSettings
5
6
  from dar_backup.util import setup_logging, get_logger
6
7
  from dar_backup.command_runner import CommandRunner
7
8
  from dar_backup.manager import create_db
9
+ # Always expand manager DB dir correctly, using helper function
10
+ from dar_backup.manager import get_db_dir
11
+ from dar_backup.util import expand_path
12
+
8
13
 
9
14
 
10
15
  def run_installer(config_file: str, create_db_flag: bool):
16
+ """
17
+ Run the installation process for dar-backup using the given config file.
18
+
19
+ This includes:
20
+ - Expanding and parsing the config file
21
+ - Setting up logging
22
+ - Creating required backup directories
23
+ - Optionally initializing catalog databases for all backup definitions
24
+
25
+ Args:
26
+ config_file (str): Path to the configuration file (may include ~ or env vars).
27
+ create_db_flag (bool): If True, databases are initialized for each backup definition.
28
+ """
11
29
  config_file = os.path.expanduser(os.path.expandvars(config_file))
12
30
  config_settings = ConfigSettings(config_file)
13
31
 
14
- # Set up logging based on the config's specified log file
32
+ # Set up logging
15
33
  command_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
16
34
  logger = setup_logging(
17
35
  config_settings.logfile_location,
18
36
  command_log,
19
37
  log_level="info",
20
- log_stdout=True,
38
+ log_to_stdout=True,
21
39
  )
22
40
  command_logger = get_logger(command_output_logger=True)
23
41
  runner = CommandRunner(logger=logger, command_logger=command_logger)
24
42
 
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
43
 
36
- # Optionally create databases
44
+ # Create required directories
45
+ required_dirs = {
46
+ "backup_dir": config_settings.backup_dir,
47
+ "test_restore_dir": config_settings.test_restore_dir,
48
+ "backup_d_dir": config_settings.backup_d_dir,
49
+ "manager_db_dir": get_db_dir(config_settings),
50
+ }
51
+
52
+ for name, dir_path in required_dirs.items():
53
+ expanded = Path(expand_path(dir_path))
54
+ if not expanded.exists():
55
+ logger.info(f"Creating directory: {expanded} ({name})")
56
+ expanded.mkdir(parents=True, exist_ok=True)
57
+
58
+ # Optionally create databases for all backup definitions
37
59
  if create_db_flag:
38
60
  for file in os.listdir(config_settings.backup_d_dir):
39
61
  backup_def = os.path.basename(file)
40
62
  print(f"Creating catalog for: {backup_def}")
41
- result = create_db(backup_def, config_settings, logger)
63
+ result = create_db(backup_def, config_settings, logger, runner)
42
64
  if result == 0:
43
65
  print(f"✔️ Catalog created (or already existed): {backup_def}")
44
66
  else:
45
67
  print(f"❌ Failed to create catalog: {backup_def}")
46
68
 
47
69
 
48
- def installer_main():
70
+ def main():
49
71
  parser = argparse.ArgumentParser(description="dar-backup installer")
50
72
  parser.add_argument("--config", required=True, help="Path to config file")
51
73
  parser.add_argument("--create-db", action="store_true", help="Create catalog databases")
74
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s version {about.__version__}, {about.__license__}"
75
+ )
76
+
52
77
  args = parser.parse_args()
53
78
 
54
79
  run_installer(args.config, args.create_db)
55
80
 
56
81
 
57
82
  if __name__ == "__main__":
58
- installer_main()
83
+ main()
dar_backup/manager.py CHANGED
@@ -37,6 +37,7 @@ from dar_backup.util import CommandResult
37
37
  from dar_backup.util import get_logger
38
38
  from dar_backup.util import get_binary_info
39
39
  from dar_backup.util import show_version
40
+ from dar_backup.util import get_invocation_command_line
40
41
  from dar_backup.util import print_aligned_settings
41
42
 
42
43
  from dar_backup.command_runner import CommandRunner
@@ -503,7 +504,7 @@ def build_arg_parser():
503
504
  parser.add_argument('--log-level', type=str, help="`debug` or `trace`, default is `info`", default="info")
504
505
  parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
505
506
  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')
507
+ parser.add_argument('-v', '--version', action='store_true', help='Show version & license')
507
508
 
508
509
  return parser
509
510
 
@@ -535,7 +536,6 @@ def main():
535
536
 
536
537
  args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
537
538
  config_settings = ConfigSettings(args.config_file)
538
- print(f"Config settings: {config_settings}")
539
539
 
540
540
  if not os.path.dirname(config_settings.logfile_location):
541
541
  print(f"Directory for log file '{config_settings.logfile_location}' does not exist, exiting")
@@ -552,6 +552,7 @@ def main():
552
552
  start_time = int(time())
553
553
  start_msgs.append((f"{SCRIPTNAME}:", about.__version__))
554
554
  logger.info(f"START TIME: {start_time}")
555
+ logger.debug(f"Command line: {get_invocation_command_line()}")
555
556
  logger.debug(f"`args`:\n{args}")
556
557
  logger.debug(f"`config_settings`:\n{config_settings}")
557
558
  start_msgs.append(("Config file:", args.config_file))
dar_backup/util.py CHANGED
@@ -102,13 +102,50 @@ def get_logger(command_output_logger: bool = False) -> logging.Logger:
102
102
 
103
103
  return secondary_logger if command_output_logger else logger
104
104
 
105
+
106
+ # Setup completer logger only once
107
+ def _setup_completer_logger(logfile="/tmp/dar_backup_completer.log"):
108
+ logger = logging.getLogger("completer")
109
+ if not logger.handlers:
110
+ handler = logging.FileHandler(logfile)
111
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
112
+ handler.setFormatter(formatter)
113
+ logger.addHandler(handler)
114
+ logger.setLevel(logging.DEBUG)
115
+ return logger
116
+
117
+ # Singleton logger for completer debugging
118
+ completer_logger = _setup_completer_logger()
119
+ completer_logger.debug("Completer logger initialized.")
120
+
121
+
122
+ def get_invocation_command_line() -> str:
123
+ """
124
+ Safely retrieves the exact command line used to invoke the current Python process.
125
+
126
+ On Unix-like systems, this reads from /proc/[pid]/cmdline to reconstruct the
127
+ command with interpreter and arguments. If any error occurs (e.g., file not found,
128
+ permission denied, non-Unix platform), it returns a descriptive error message.
129
+
130
+ Returns:
131
+ str: The full command line string, or an error description if it cannot be retrieved.
132
+ """
133
+ try:
134
+ cmdline_path = f"/proc/{os.getpid()}/cmdline"
135
+ with open(cmdline_path, "rb") as f:
136
+ content = f.read()
137
+ if not content:
138
+ return "[error: /proc/cmdline is empty]"
139
+ return content.replace(b'\x00', b' ').decode().strip()
140
+ except Exception as e:
141
+ return f"[error: could not read /proc/[pid]/cmdline: {e}]"
142
+
143
+
105
144
  def show_version():
106
145
  script_name = os.path.basename(sys.argv[0])
107
146
  print(f"{script_name} {about.__version__}")
108
147
  print(f"{script_name} source code is here: https://github.com/per2jensen/dar-backup")
109
- print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
110
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
111
- See section 15 and section 16 in the supplied "LICENSE" file.''')
148
+ print(about.__license__)
112
149
 
113
150
  def extract_version(output):
114
151
  match = re.search(r'(\d+\.\d+(\.\d+)?)', output)
@@ -245,6 +282,28 @@ class CommandResult(NamedTuple):
245
282
 
246
283
  def list_backups(backup_dir, backup_definition=None):
247
284
  """
285
+ Lists the available backup files in the specified directory along with their total sizes in megabytes.
286
+ The function filters and processes `.dar` files, grouping them by their base names and ensuring proper
287
+ alignment of the displayed sizes.
288
+ Args:
289
+ backup_dir (str): The directory containing the backup files.
290
+ backup_definition (str, optional): A prefix to filter backups by their base name. Only backups
291
+ starting with this prefix will be included. Defaults to None.
292
+ Raises:
293
+ locale.Error: If setting the locale fails and the fallback to the 'C' locale is unsuccessful.
294
+ Behavior:
295
+ - Attempts to set the locale based on the environment for proper formatting of numbers.
296
+ - Filters `.dar` files in the specified directory based on the following criteria:
297
+ - The file name must contain one of the substrings: "_FULL_", "_DIFF_", or "_INCR_".
298
+ - The file name must include a date in the format "_YYYY-MM-DD".
299
+ - Groups files by their base name (excluding slice numbers and extensions) and calculates
300
+ the total size for each group in megabytes.
301
+ - Sorts the backups by their base name and date (if included in the name).
302
+ - Prints the backup names and their sizes in a formatted and aligned manner.
303
+ Returns:
304
+ None: The function prints the results directly to the console. If no backups are found,
305
+ it prints "No backups available.".
306
+
248
307
  List the available backups in the specified directory and their sizes in megabytes, with aligned sizes.
249
308
  """
250
309
  # Attempt to set locale from the environment or fall back to the default locale
@@ -346,6 +405,7 @@ def extract_backup_definition_fallback() -> str:
346
405
 
347
406
 
348
407
 
408
+
349
409
  def list_archive_completer(prefix, parsed_args, **kwargs):
350
410
  import os
351
411
  import configparser
@@ -369,12 +429,39 @@ def list_archive_completer(prefix, parsed_args, **kwargs):
369
429
  files = os.listdir(backup_dir)
370
430
  archive_re = re.compile(rf"^{re.escape(backup_def)}_.+_\d{{4}}-\d{{2}}-\d{{2}}\.1\.dar$") if backup_def else re.compile(r".+_\d{4}-\d{2}-\d{2}\.1\.dar$")
371
431
 
372
- return [
432
+ completions = [
373
433
  f.rsplit(".1.dar", 1)[0]
374
434
  for f in files
375
435
  if archive_re.match(f)
376
436
  ]
377
437
 
438
+ completions = sorted(set(completions), key=sort_key)
439
+ return completions or ["[no matching archives]"]
440
+
441
+
442
+
443
+ def sort_key(archive_name: str):
444
+ """
445
+ Sort by backup definition and then by date extracted from the archive name.
446
+ Handles formats like: <def>_<TYPE>_<YYYY-MM-DD>.<N>.dar
447
+ """
448
+ try:
449
+ base = archive_name.split('.')[0] # remove .1.dar
450
+ parts = base.split('_')
451
+ if len(parts) < 3:
452
+ return (archive_name, datetime.min) # fallback for non-matching formats
453
+
454
+ # Correct assumption: last two parts are TYPE and DATE
455
+ def_name = '_'.join(parts[:-2]) # everything before _TYPE_DATE
456
+ date_str = parts[-1]
457
+ date = datetime.strptime(date_str, "%Y-%m-%d")
458
+ completer_logger.debug(f"Archive: {archive_name}, Def: {def_name}, Date: {date}")
459
+ return (def_name, date)
460
+ except Exception:
461
+ return (archive_name, datetime.min)
462
+
463
+
464
+
378
465
 
379
466
  def archive_content_completer(prefix, parsed_args, **kwargs):
380
467
  """
@@ -392,14 +479,15 @@ def archive_content_completer(prefix, parsed_args, **kwargs):
392
479
  # Expand config path
393
480
  config_file = expand_path(getattr(parsed_args, "config_file", "~/.config/dar-backup/dar-backup.conf"))
394
481
  config = ConfigSettings(config_file=config_file)
395
- backup_dir = config.backup_dir
482
+ #db_dir = expand_path((getattr(config, 'manager_db_dir', config.backup_dir))) # use manager_db_dir if set, else backup_dir
483
+ db_dir = expand_path(getattr(config, 'manager_db_dir', None) or config.backup_dir)
396
484
 
397
485
  # Which db files to inspect?
398
486
  backup_def = getattr(parsed_args, "backup_def", None)
399
487
  db_files = (
400
- [os.path.join(backup_dir, f"{backup_def}.db")]
488
+ [os.path.join( db_dir, f"{backup_def}.db")]
401
489
  if backup_def
402
- else [os.path.join(backup_dir, f) for f in os.listdir(backup_dir) if f.endswith(".db")]
490
+ else [os.path.join( db_dir, f) for f in os.listdir( db_dir) if f.endswith(".db")]
403
491
  )
404
492
 
405
493
  completions = []
@@ -426,13 +514,6 @@ def archive_content_completer(prefix, parsed_args, **kwargs):
426
514
  if archive.startswith(prefix):
427
515
  completions.append(archive)
428
516
 
429
- # Sort: first by name (before first '_'), then by date (YYYY-MM-DD)
430
- def sort_key(archive):
431
- name_part = archive.split("_")[0]
432
- date_match = re.search(r"\d{4}-\d{2}-\d{2}", archive)
433
- date = datetime.strptime(date_match.group(0), "%Y-%m-%d") if date_match else datetime.min
434
- return (name_part, date)
435
-
436
517
  completions = sorted(set(completions), key=sort_key)
437
518
  return completions or ["[no matching archives]"]
438
519
 
@@ -452,6 +533,8 @@ def add_specific_archive_completer(prefix, parsed_args, **kwargs):
452
533
 
453
534
  config_file = expand_path(getattr(parsed_args, "config_file", "~/.config/dar-backup/dar-backup.conf"))
454
535
  config = ConfigSettings(config_file=config_file)
536
+ #db_dir = expand_path((getattr(config, 'manager_db_dir', config.backup_dir))) # use manager_db_dir if set, else backup_dir
537
+ db_dir = expand_path(getattr(config, 'manager_db_dir') or config.backup_dir)
455
538
  backup_dir = config.backup_dir
456
539
  backup_def = getattr(parsed_args, "backup_def", None)
457
540
 
@@ -469,7 +552,7 @@ def add_specific_archive_completer(prefix, parsed_args, **kwargs):
469
552
  all_archives.add(base)
470
553
 
471
554
  # Step 2: exclude ones already present in the .db
472
- db_path = os.path.join(backup_dir, f"{backup_def}.db") if backup_def else None
555
+ db_path = os.path.join(db_dir, f"{backup_def}.db") if backup_def else None
473
556
  existing = set()
474
557
 
475
558
  if db_path and os.path.exists(db_path):