dar-backup 0.6.21__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/clean_log.py CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
2
4
  """
3
5
  clean-log.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/clean-log.py
4
6
  This script is part of dar-backup, a backup solution for Linux using dar and systemd.
dar_backup/cleanup.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
  cleanup.py source code is here: https://github.com/per2jensen/dar-backup
@@ -236,7 +237,7 @@ def main():
236
237
  args.verbose and start_msgs.append(("--cleanup-specific-archives:", args.cleanup_specific_archives))
237
238
 
238
239
  dangerous_keywords = ["--cleanup", "_FULL_"] # TODO: add more dangerous keywords
239
- print_aligned_settings(start_msgs, highlight_keywords=dangerous_keywords)
240
+ print_aligned_settings(start_msgs, highlight_keywords=dangerous_keywords, quiet=not args.verbose)
240
241
 
241
242
  # run PREREQ scripts
242
243
  requirements('PREREQ', config_settings)
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+
1
3
  import subprocess
2
4
  import logging
3
5
  import threading
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+
1
3
  import configparser
2
4
  from dataclasses import dataclass, field, fields
3
5
  from os.path import expandvars, expanduser
@@ -1,6 +1,8 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+
1
3
  # This config file is intended to demo `dar-backup`.
2
4
  #
3
- # The `installer` puts it in ~/.config/dar-backup/dar-backup.conf
5
+ # The `demo` application puts this file in ~/.config/dar-backup/dar-backup.conf
4
6
 
5
7
  [MISC]
6
8
  LOGFILE_LOCATION = ~/dar-backup/dar-backup.log
@@ -13,7 +15,7 @@ NO_FILES_VERIFICATION = 5
13
15
  COMMAND_TIMEOUT_SECS = 86400
14
16
 
15
17
  [DIRECTORIES]
16
- BACKUP_DIR = ~/dar-backup/backups
18
+ BACKUP_DIR = @@BACKUP_DIR@@
17
19
  BACKUP.D_DIR = ~/.config/dar-backup/backup.d/
18
20
  TEST_RESTORE_DIR = ~/dar-backup/restore/
19
21
  # Optional parameter
@@ -0,0 +1,60 @@
1
+
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ # ------------------------------------------------------------------------
5
+ # Demo of a `dar-backup` configuration file
6
+ # This file was generated by dar-backup's `demo` program.
7
+ #
8
+ {%- if opts_dict | length > 0 %}
9
+ # Options given to the `demo` program:
10
+ {% endif %}
11
+ {%- if opts_dict.ROOT_DIR -%}
12
+ # --root-dir : {{ opts_dict.ROOT_DIR }}
13
+ {% endif %}
14
+ {%- if opts_dict.DIR_TO_BACKUP -%}
15
+ # --dir-to-backup : {{ opts_dict.DIR_TO_BACKUP }}
16
+ {% endif -%}
17
+ {%- if opts_dict.BACKUP_DIR -%}
18
+ # --backup-dir : {{ opts_dict.BACKUP_DIR }}
19
+ {% endif %}
20
+ #
21
+ # Variables used to generate this file:
22
+ # =====================================
23
+ {% for k,v in vars_map|dictsort %}# {{ k }} : {{ v }}
24
+ {% endfor -%}
25
+ # ------------------------------------------------------------------------
26
+
27
+ [MISC]
28
+ LOGFILE_LOCATION = {{ vars_map.DAR_BACKUP_DIR -}}/dar-backup.log
29
+ MAX_SIZE_VERIFICATION_MB = 2
30
+ MIN_SIZE_VERIFICATION_MB = 0
31
+ NO_FILES_VERIFICATION = 1
32
+ # timeout in seconds for backup, test, restore and par2 operations
33
+ # The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
34
+ # If a timeout is not specified when using the CommandRunner, a default timeout of 30 secs is used.
35
+ COMMAND_TIMEOUT_SECS = 86400
36
+
37
+ [DIRECTORIES]
38
+ BACKUP_DIR = {{ vars_map.BACKUP_DIR }}
39
+ BACKUP.D_DIR = {{ vars_map.BACKUP_D_DIR }}
40
+ TEST_RESTORE_DIR = {{ vars_map.TEST_RESTORE_DIR }}
41
+ # Optional parameter
42
+ # If you want to store the catalog database away from the BACKUP_DIR, use the MANAGER_DB_DIR variable.
43
+ #MANAGER_DB_DIR = /some/where/else/
44
+
45
+ [AGE]
46
+ # DIFF and INCR backups are kept for a configured number of days, then deleted by the `cleanuo`
47
+ # age settings are in days
48
+ DIFF_AGE = 100
49
+ INCR_AGE = 40
50
+
51
+ [PAR2]
52
+ ERROR_CORRECTION_PERCENT = 5
53
+ ENABLED = True
54
+
55
+ [PREREQ]
56
+ #SCRIPT_1 = <pre-script 1>
57
+
58
+ [POSTREQ]
59
+ #SCRIPT_1 = <post-script 1>
60
+
dar_backup/dar_backup.py CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
-
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
3
  """
4
4
  installer.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/installer.py
5
5
  This script is part of dar-backup, a backup solution for Linux using dar and systemd.
@@ -877,9 +877,9 @@ def main():
877
877
  start_msgs.append(('Config file:', os.path.abspath(args.config_file)))
878
878
  start_msgs.append((".darrc location:", args.darrc))
879
879
 
880
- args.verbose and args.full_backup and start_msgs.append(("Type of backup:", "FULL"))
881
- args.verbose and args.differential_backup and start_msgs.append(("Type of backup:", "DIFF"))
882
- args.verbose and args.incremental_backup and start_msgs.append(("Type of backup:", "INCR"))
880
+ args.full_backup and start_msgs.append(("Type of backup:", "FULL"))
881
+ args.differential_backup and start_msgs.append(("Type of backup:", "DIFF"))
882
+ args.incremental_backup and start_msgs.append(("Type of backup:", "INCR"))
883
883
  args.verbose and args.backup_definition and start_msgs.append(("Backup definition:", args.backup_definition))
884
884
  if args.alternate_reference_archive:
885
885
  args.verbose and start_msgs.append(("Alternate ref archive:", args.alternate_reference_archive))
@@ -893,8 +893,8 @@ def main():
893
893
  args.verbose and start_msgs.append(("PAR2 enabled:", config_settings.par2_enabled))
894
894
  args.verbose and start_msgs.append(("--do-not-compare:", args.do_not_compare))
895
895
 
896
- dangerous_keywords = ["--do-not", "alternate"] # TODO: add more dangerous keywords
897
- print_aligned_settings(start_msgs)
896
+ highlight_keywords = ["--do-not", "alternate"] # TODO: add more dangerous keywords
897
+ print_aligned_settings(start_msgs, quiet=not args.verbose, highlight_keywords=highlight_keywords)
898
898
 
899
899
  # sanity check
900
900
  if args.backup_definition and not os.path.exists(os.path.join(config_settings.backup_d_dir, args.backup_definition)):
@@ -1,3 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
1
4
  from pathlib import Path
2
5
  import subprocess
3
6
  import argparse
dar_backup/demo.py CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
2
4
  """
3
- installer.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/installer.py
5
+ demo.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/demo.py
4
6
  This script is part of dar-backup, a backup solution for Linux using dar and systemd.
5
7
 
6
8
  Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
@@ -10,7 +12,9 @@ not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
12
  See section 15 and section 16 in the supplied "LICENSE" file
11
13
 
12
14
  This script can be used to configure dar-backup on your system.
13
- It is non-destructive and will not overwrite any existing files or directories.
15
+ It is non-destructive and will not overwrite any existing files or directories under --override is used.
16
+
17
+ User can set ROOT_DIR, DIR_TO_BACKUP and BACKUP_DIR (destination for backups) via optins to override defaults.
14
18
  """
15
19
 
16
20
  import argparse
@@ -19,117 +23,189 @@ import shutil
19
23
  import sys
20
24
 
21
25
  from . import __about__ as about
26
+ from . import util
22
27
 
28
+ from jinja2 import Environment, FileSystemLoader
23
29
  from pathlib import Path
30
+ from typing import Dict, Tuple
31
+
32
+
33
+ CONFIG_DIR = util.normalize_dir(util.expand_path("~/.config/dar-backup"))
34
+ DAR_BACKUP_DIR = util.normalize_dir(util.expand_path("~/dar-backup"))
35
+
36
+
24
37
 
25
- CONFIG_DIR = os.path.expanduser("~/.config/dar-backup")
26
- DAR_BACKUP_DIR = os.path.expanduser("~/dar-backup/")
38
+ def check_directories(args, vars_map: Dict[str,str]) -> bool:
39
+ """
40
+ Check if the directories exist and create them if they don't.
27
41
 
28
- BACKUP_DEFINITION = '''
29
- # Demo of a `dar-backup` definition file
30
- # This back definition file configures a backup of ~/.config/dar-backup
31
- # `dar-backup` puts the backups in ~/dar-backup/backups
32
- # ------------------------------------------------------------------------
42
+ Returns:
43
+ bool: True if the directories were created successfully, False otherwise.
44
+ """
45
+ result = True
46
+ for key in ("DAR_BACKUP_DIR","BACKUP_DIR","TEST_RESTORE_DIR","CONFIG_DIR","BACKUP_D_DIR"):
47
+ path = Path(vars_map[key])
48
+ if path.exists() and not args.override:
49
+ print(f"Directory '{path}' already exists")
50
+ result = False
51
+ return result
33
52
 
34
- # Switch to ordered selection mode, which means that the following options
35
- # will be considered top to bottom
36
- -am
37
53
 
38
- # Backup Root dir
39
- -R @@HOME_DIR@@
54
+ def generate_file(args, template: str, file_path: Path, vars_map: Dict[str, str], opts_dict: Dict[str, str]) -> bool:
55
+ """
56
+ Generate a file using a Jinja2 template.
40
57
 
41
- # Directories to backup below the Root dir
42
- -g .config/dar-backup
58
+ Args:
59
+ args: Command line arguments.
60
+ template (str): The name of the template file.
61
+ file_path (Path): The path where the generated file will be saved.
62
+ vars_map (Dict[str, str]): A dictionary containing variables for the template.
63
+ opts_dict (Dict[str, str]): A dictionary containing options given by user.
43
64
 
44
- # Examples of directories to exclude below the Root dir
45
- -P mnt
46
- -P .private
47
- -P .cache
65
+ Returns:
66
+ bool: True if the file was generated successfully, False otherwise.
67
+ """
68
+ current_script_dir = os.path.dirname(os.path.abspath(__file__))
69
+ env = Environment(loader=FileSystemLoader(current_script_dir))
70
+ tpl = env.get_template(template)
71
+ rendered = tpl.render(vars_map = vars_map, opts_dict = opts_dict)
72
+ if rendered is None:
73
+ print(f"Error: Template '{template}' could not be rendered.")
74
+ return False
75
+ if os.path.exists(file_path) and not args.override:
76
+ print(f"Error: File '{file_path}' already exists. Use --override to overwrite.")
77
+ return False
78
+ file_path.parent.mkdir(parents=True, exist_ok=True)
79
+ file_path.write_text(rendered)
80
+ print(f"File generated at '{file_path}'")
48
81
 
49
- # compression level
50
- -z5
51
82
 
52
- # no overwrite, if you rerun a backup, 'dar' halts and asks what to do
53
- -n
54
-
55
- # size of each slice in the archive
56
- --slice 10G
57
83
 
58
- # bypass directores marked as cache directories
59
- # http://dar.linux.free.fr/doc/Features.html
60
- --cache-directory-tagging
61
- '''
84
+ def setup_dicts(args, vars_map: Dict[str, str]) -> Tuple[Dict[str, str], Dict[str, str]]:
85
+ """
86
+ Override various entries in the dictionaries for jinja templating with user input.
87
+
88
+ Returns:
89
+ Tuple[Dict[str, str], Dict[str, str]]: A tuple containing the vars_map and opts_dict dictionaries.
90
+ """
91
+ opts_dict = {}
92
+ if args.root_dir:
93
+ opts_dict["ROOT_DIR"] = args.root_dir
94
+ if args.dir_to_backup:
95
+ opts_dict["DIR_TO_BACKUP"] = args.dir_to_backup
96
+ if args.backup_dir:
97
+ opts_dict["BACKUP_DIR"] = args.backup_dir
98
+
99
+ for key, value in opts_dict.items():
100
+ vars_map[key] = value
101
+
102
+ return vars_map, opts_dict
62
103
 
63
104
 
64
105
  def main():
65
106
  parser = argparse.ArgumentParser(
66
- description="Set up `dar-backup` on your system.",
107
+ description="Set up demo configuration for `dar-backup` on your system.",
67
108
  )
68
109
  parser.add_argument(
69
110
  "-i", "--install",
70
111
  action="store_true",
71
- help="Deploy a simple config file, use ~/dar-backup/ for log file, archives and restore tests."
112
+ help="Deploy demo config files and directories. Will not overwrite existing files or directories unless --override is used."
113
+ )
114
+ req = parser.add_argument_group(
115
+ 'These options must be used together'
116
+ )
117
+ req.add_argument(
118
+ "--root-dir",
119
+ type=str,
120
+ help="Specify the root directory for the backup."
121
+ )
122
+ req.add_argument(
123
+ "--dir-to-backup",
124
+ type=str,
125
+ help="Directory to backup, relative to the root directory."
126
+ )
127
+ parser.add_argument(
128
+ "--backup-dir",
129
+ type=str,
130
+ help="Directory where backups and redundancy files are put"
131
+ )
132
+ parser.add_argument(
133
+ "--override",
134
+ action="store_true",
135
+ help="By default, the script will not overwrite existing files or directories. Use this option to override this behavior."
72
136
  )
73
137
  parser.add_argument(
74
138
  "-v", "--version",
75
139
  action="version",
76
140
  version=f"%(prog)s version {about.__version__}, {about.__license__}"
77
141
  )
142
+ parser.add_argument(
143
+ "-g", "--generate",
144
+ action="store_true",
145
+ help="Generate config files and put them in /tmp/."
146
+ )
78
147
 
79
148
  args = parser.parse_args()
80
149
 
81
- if args.install:
82
- errors = []
83
- if os.path.exists(CONFIG_DIR):
84
- errors.append(f"Config directory '{CONFIG_DIR}' already exists.")
85
- if os.path.exists(DAR_BACKUP_DIR):
86
- errors.append(f"Directory '{DAR_BACKUP_DIR}' already exists.")
87
-
88
- if errors:
89
- for error in errors:
90
- print(f"Error: {error}")
150
+ group = [args.root_dir, args.dir_to_backup]
151
+ if any(group) and not all(group):
152
+ parser.error(
153
+ "Options --root-dir, --dir-to-backup must all be specified together."
154
+ )
155
+ exit(1)
156
+
157
+ args.root_dir = util.normalize_dir(util.expand_path(args.root_dir)) if args.root_dir else None
158
+ args.backup_dir = util.normalize_dir(util.expand_path(args.backup_dir)) if args.backup_dir else None
159
+ args.dir_to_backup = util.normalize_dir(util.expand_path(args.dir_to_backup)) if args.dir_to_backup else None
160
+
161
+
162
+
163
+ vars_map = {
164
+ # dar-backup.conf variables
165
+ "CONFIG_DIR" : CONFIG_DIR,
166
+ "DAR_BACKUP_DIR" : DAR_BACKUP_DIR,
167
+ "BACKUP_DIR" : os.path.join(DAR_BACKUP_DIR, "backups"),
168
+ "BACKUP_D_DIR" : os.path.join(CONFIG_DIR, "backup.d"),
169
+ "TEST_RESTORE_DIR" : os.path.join(DAR_BACKUP_DIR, "restore"),
170
+ # backup definition variables
171
+ "ROOT_DIR" : util.normalize_dir(util.expand_path("$HOME")),
172
+ "DIR_TO_BACKUP" : ".config/dar-backup",
173
+ }
174
+
175
+
176
+ vars_map, opts_dict = setup_dicts(args, vars_map)
177
+
178
+ if args.generate:
179
+ print("Generating backup definition file...")
180
+ vars_map["DAR_BACKUP_DIR"] = "/tmp"
181
+ args.override = True
182
+ generate_file(args, "demo_backup_def.j2", Path("/tmp/dar-backup/backup.d/demo"), vars_map, opts_dict)
183
+ vars_map["CONFIG_DIR"] = "/tmp"
184
+ generate_file(args, "dar-backup.conf.j2", Path("/tmp/dar-backup.conf"), vars_map, opts_dict)
185
+ elif args.install:
186
+ if not check_directories(args, vars_map):
187
+ print("Error: One or more directories already exist.\nSpecify non-existent directories or use --override to overwrite.")
91
188
  sys.exit(1)
92
189
 
93
- try:
94
- os.makedirs(DAR_BACKUP_DIR, exist_ok=False)
95
- os.makedirs(os.path.join(DAR_BACKUP_DIR, "backups"), exist_ok=False)
96
- os.makedirs(os.path.join(DAR_BACKUP_DIR, "restore"), exist_ok=False)
97
- os.makedirs(CONFIG_DIR, exist_ok=False)
98
- os.makedirs(os.path.join(CONFIG_DIR, "backup.d"), exist_ok=False)
99
- print(f"Directories created: `{DAR_BACKUP_DIR}` and `{CONFIG_DIR}`")
100
-
101
- script_dir = Path(__file__).parent
102
- source_file = script_dir / "dar-backup.conf"
103
- destination_file = Path(CONFIG_DIR) / "dar-backup.conf"
104
-
105
- try:
106
- shutil.copy2(source_file, destination_file)
107
- print(f"Config file deployed to {destination_file}")
108
- except Exception as e:
109
- print(f"Error: Could not copy config file: {e}")
110
- sys.exit(1)
111
-
112
-
113
- backup_definition = BACKUP_DEFINITION.replace("@@HOME_DIR@@", os.path.expanduser("~"))
114
-
115
- try:
116
- with open(os.path.join(CONFIG_DIR, "backup.d", "default"), "w") as f:
117
- f.write(backup_definition)
118
- print(f"Default backup definition file deployed to {os.path.join(CONFIG_DIR, 'backup.d', 'default')}")
119
- except Exception as e:
120
- print(f"Error: Could not write default backup definition: {e}")
121
- sys.exit(1)
122
- except Exception as e:
123
- print(f"Installation failed: {e}")
124
- sys.exit(1)
190
+ Path(vars_map["DAR_BACKUP_DIR"]).mkdir(parents=True, exist_ok=True)
191
+ Path(vars_map["BACKUP_DIR"]).mkdir(parents=True, exist_ok=True)
192
+ Path(vars_map["TEST_RESTORE_DIR"]).mkdir(parents=True, exist_ok=True)
193
+ Path(vars_map["CONFIG_DIR"]).mkdir(parents=True, exist_ok=True)
194
+ Path(vars_map["BACKUP_D_DIR"]).mkdir(parents=True, exist_ok=True)
195
+ print(f"Directories created.")
196
+
197
+ generate_file(args, "demo_backup_def.j2", Path(vars_map["BACKUP_D_DIR"]).joinpath("demo"), vars_map, opts_dict)
198
+ generate_file(args, "dar-backup.conf.j2", Path(vars_map["CONFIG_DIR"]).joinpath("dar-backup.conf"), vars_map, opts_dict)
125
199
 
126
- print("1. Now run `manager --create` to create the catalog database.")
200
+ print("1. Now run `manager --create-db` to create the catalog database.")
127
201
  print("2. Then you can run `dar-backup --full-backup` to create a backup.")
128
202
  print("3. List backups with `dar-backup --list`")
129
203
  print("4. List contents of a backup with `dar-backup --list-contents <backup-name>`")
204
+ else:
205
+ parser.print_help()
206
+ sys.exit(1)
130
207
 
131
208
  sys.exit(0)
132
209
 
133
-
134
210
  if __name__ == "__main__":
135
211
  main()
@@ -0,0 +1,62 @@
1
+
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ # ------------------------------------------------------------------------
5
+ # Demo of a `dar-backup` definition file
6
+ # This file was generated by dar-backup's `demo` program.
7
+ #
8
+ {%- if opts_dict | length > 0 %}
9
+ # Options given to the `demo` program:
10
+ {% endif %}
11
+ {%- if opts_dict.ROOT_DIR -%}
12
+ # --root-dir : {{ opts_dict.ROOT_DIR }}
13
+ {% endif %}
14
+ {%- if opts_dict.DIR_TO_BACKUP -%}
15
+ # --dir-to-backup : {{ opts_dict.DIR_TO_BACKUP }}
16
+ {% endif -%}
17
+ {%- if opts_dict.BACKUP_DIR -%}
18
+ # --backup-dir : {{ opts_dict.BACKUP_DIR }}
19
+ {% endif %}
20
+ #
21
+ # Variables used to generate this file:
22
+ # =====================================
23
+ {% for k,v in vars_map|dictsort %}# {{ k }} : {{ v }}
24
+ {% endfor -%}
25
+ # ------------------------------------------------------------------------
26
+
27
+ # Switch to ordered selection mode, which means that the following options
28
+ # will be considered top to bottom
29
+ -am
30
+
31
+ # Backup Root dir
32
+ {%- if vars_map.ROOT_DIR %}
33
+ -R {{ vars_map.ROOT_DIR }}
34
+ {% endif -%}
35
+
36
+ {% if vars_map.DIR_TO_BACKUP %}
37
+ # Directories to backup below the Root dir
38
+ -g {{ vars_map.DIR_TO_BACKUP }}
39
+
40
+ # This is an example of exclusion of a `.private` directory inside the
41
+ # directory that is backed up
42
+ -P {{ vars_map.DIR_TO_BACKUP }}/.private
43
+ {%- else %}
44
+ # Examples of directories to exclude below the Root dir
45
+ -P mnt
46
+ -P .cache
47
+ {% endif %}
48
+
49
+ # compression level
50
+ -z5
51
+
52
+ # no overwrite, if you rerun a backup, 'dar' halts and asks what to do
53
+ # as `dar-backup` gives the `-Q` option to `dar`, the net effect of `-n` and `-Q` is
54
+ # that `dar` will quit and not overwrite the existing backup
55
+ -n
56
+
57
+ # size of each slice in the archive (10G is 10 Gigabytes)
58
+ --slice 10G
59
+
60
+ # bypass directores marked as cache directories
61
+ # http://dar.linux.free.fr/doc/Features.html
62
+ --cache-directory-tagging
dar_backup/exceptions.py CHANGED
@@ -1,3 +1,5 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+
1
3
  class ConfigSettingsError(Exception):
2
4
  """Raised when ConfigSettings encounters a critical error."""
3
5
  pass
dar_backup/installer.py CHANGED
@@ -1,3 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
1
4
  import argparse
2
5
  import os
3
6
  from . import __about__ as about
@@ -10,9 +13,93 @@ from dar_backup.manager import create_db
10
13
  from dar_backup.manager import get_db_dir
11
14
  from dar_backup.util import expand_path
12
15
 
13
-
14
-
15
- def run_installer(config_file: str, create_db_flag: bool):
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
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)
96
+
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):
16
103
  """
17
104
  Run the installation process for dar-backup using the given config file.
18
105
 
@@ -67,16 +154,34 @@ def run_installer(config_file: str, create_db_flag: bool):
67
154
  print(f"❌ Failed to create catalog: {backup_def}")
68
155
 
69
156
 
157
+
70
158
  def main():
71
159
  parser = argparse.ArgumentParser(description="dar-backup installer")
72
- parser.add_argument("--config", required=True, help="Path to config file")
160
+ parser.add_argument("--config", required=False, help="Path to config file")
73
161
  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__}"
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__}"
75
174
  )
76
175
 
77
176
  args = parser.parse_args()
78
177
 
79
- 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()
80
185
 
81
186
 
82
187
  if __name__ == "__main__":