borgapi 0.6.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.
borgapi/borgapi.py CHANGED
@@ -1,16 +1,19 @@
1
- """Run Borg Backups"""
1
+ """Run Borg backups."""
2
2
 
3
+ import functools
3
4
  import logging
4
5
  import os
5
- import sys
6
- from io import BytesIO, StringIO, TextIOWrapper
6
+ from asyncio import wrap_future
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from io import StringIO
7
9
  from json import decoder, loads
8
- from typing import Callable, List, Optional, Tuple, Union
10
+ from typing import Callable, Optional, Union
9
11
 
10
12
  import borg.archiver
11
- from borg.logger import JsonFormatter
12
13
  from dotenv import dotenv_values, load_dotenv
13
14
 
15
+ from .capture import LOG_LVL, OutputCapture, OutputOptions
16
+ from .helpers import ENVIRONMENT_DEFAULTS, Options, Output
14
17
  from .options import (
15
18
  ArchiveInput,
16
19
  ArchiveOutput,
@@ -24,194 +27,116 @@ from .options import (
24
27
  OptionsBase,
25
28
  )
26
29
 
27
- __all__ = ["BorgAPI"]
30
+ __all__ = ["BorgAPI", "BorgAPIAsync"]
28
31
 
29
- Json = Union[list, dict]
30
- Output = Union[str, Json, None]
31
- Options = Union[bool, str, int]
32
32
 
33
- LOG_LVL = "warning"
33
+ class BorgAPIBase:
34
+ """Automate borg in code.
34
35
 
35
-
36
- class OutputCapture:
37
- """Capture stdout and stderr by redirecting to inmemory streams
38
-
39
- :param raw: Expecting raw bytes from stdout and stderr
40
- :type raw: bool
36
+ Base class for the wrapper. Contains all the non-Borg command calls.
37
+ Should not be called by itself. Only really here for readability purposes.
41
38
  """
42
39
 
43
- def __init__(
44
- self,
45
- raw: bool = False,
46
- log_json: bool = False,
47
- log_level: str = LOG_LVL,
48
- ):
49
- self.raw = raw
50
- fmt = "%(message)s"
51
- self.formatter = JsonFormatter(fmt) if log_json else logging.Formatter(fmt)
52
- self.level = log_level.upper()
53
- self._init_stdout(raw)
54
- self._init_stderr()
55
-
56
- self._init_list_capture()
57
- self._init_stats_capture()
58
- self._init_repo_capture()
59
-
60
- def _init_stdout(self, raw: bool):
61
- self.stdout = TextIOWrapper(BytesIO()) if raw else StringIO()
62
- self.stdout_original = sys.stdout
63
- sys.stdout = self.stdout
64
-
65
- def _init_stderr(self):
66
- self.stderr = StringIO()
67
- self.stderr_original = sys.stderr
68
- sys.stderr = self.stderr
69
-
70
- def _init_list_capture(self):
71
- self.list_handler = logging.StreamHandler(StringIO())
72
- self.list_handler.setFormatter(self.formatter)
73
- # self.list_handler.setLevel(self.level)
74
- self.list_handler.setLevel("INFO")
75
-
76
- self.list_logger = logging.getLogger("borg.output.list")
77
- self.list_logger.addHandler(self.list_handler)
78
-
79
- def _init_stats_capture(self):
80
- self.stats_handler = logging.StreamHandler(StringIO())
81
- self.stats_handler.setFormatter(self.formatter)
82
- # self.stats_handler.setLevel(self.level)
83
- self.stats_handler.setLevel("INFO")
84
-
85
- self.stats_logger = logging.getLogger("borg.output.stats")
86
- self.stats_logger.addHandler(self.stats_handler)
87
-
88
- def _init_repo_capture(self):
89
- self.repo_handler = logging.StreamHandler(StringIO())
90
- self.repo_handler.setFormatter(self.formatter)
91
- self.repo_handler.setLevel(self.level)
92
-
93
- self.repo_logger = logging.getLogger("borg.repository")
94
- self.repo_logger.addHandler(self.repo_handler)
95
-
96
- # pylint: disable=no-member
97
- def getvalues(self) -> Union[str, bytes]:
98
- """Get the captured values from the redirected stdout and stderr
99
-
100
- :return: Redirected values from stdout and stderr
101
- :rtype: Union[str, bytes]
102
- """
103
- stdout_value = stderr_value = None
104
- if self.raw:
105
- stdout_value = self.stdout.buffer.getvalue()
106
- else:
107
- stdout_value = self.stdout.getvalue().strip()
108
- stderr_value = self.stderr.getvalue().strip()
109
-
110
- list_value = self.list_handler.stream.getvalue()
111
- stats_value = self.stats_handler.stream.getvalue()
112
- repo_value = self.repo_handler.stream.getvalue()
113
-
114
- return {
115
- "stdout": stdout_value,
116
- "stderr": stderr_value,
117
- "list": list_value,
118
- "stats": stats_value,
119
- "repo": repo_value,
120
- }
121
-
122
- def close(self):
123
- """Close the underlying IO streams and reset stdout and stderr"""
124
- try:
125
- self.stdout.close()
126
- self.stderr.close()
127
- self.list_handler.stream.close()
128
- self.list_logger.removeHandler(self.list_handler)
129
- self.stats_handler.stream.close()
130
- self.stats_logger.removeHandler(self.stats_handler)
131
- finally:
132
- sys.stdout = self.stdout_original
133
- sys.stderr = self.stderr_original
134
-
135
-
136
- # pylint: disable=too-many-public-methods
137
- class BorgAPI:
138
- """Automate borg in code"""
139
-
140
- # pylint: disable=too-many-arguments,not-callable
141
40
  def __init__(
142
41
  self,
143
42
  defaults: dict = None,
144
43
  options: dict = None,
145
44
  log_level: str = LOG_LVL,
146
45
  log_json: bool = False,
46
+ environ: dict = None,
147
47
  ):
48
+ """Set the options to be used across the different command call.
49
+
50
+ :param defaults: options for specific commands to always use, defaults to None
51
+ :type defaults: dict, optional
52
+ :param options: common flags for all commands, defaults to None
53
+ :type options: dict, optional
54
+ :param log_level: level to record logged messages at, defaults to LOG_LVL
55
+ :type log_level: str, optional
56
+ :param log_json: if the output should be in json or string format, defaults to False
57
+ :type log_json: bool, optional
58
+ :param environ: envirnmental variables to set for borg to use (ie BORG_PASSCOMMAND),
59
+ defaults to None
60
+ :type environ: dict, optional
61
+ """
148
62
  self.options = options or {}
149
63
  self.optionals = CommandOptions(defaults)
150
64
  self.archiver = borg.archiver.Archiver()
151
65
  self._previous_dotenv = []
152
- self._setup_logging(log_level, log_json)
66
+ self._set_environ_defaults()
67
+ if environ is not None:
68
+ self.set_environ(**environ)
153
69
  self.log_level = log_level
70
+ if log_json:
71
+ self.options.set("log_json", log_json)
154
72
 
155
- def _setup_logging(self, log_level: str = LOG_LVL, log_json: bool = False):
156
- self.logger_setup = False
157
- self.archiver.log_json = log_json or self.options.get("log_json", False)
158
- borg.archiver.setup_logging(level=log_level, is_serve=False, json=log_json)
159
- self.original_stdout = sys.stdout
73
+ self.archiver.log_json = log_json
74
+ borg.archiver.setup_logging(level=self.log_level, is_serve=False, json=log_json)
160
75
  logging.getLogger("borgapi")
161
76
  self._logger = logging.getLogger(__name__)
162
77
 
78
+ self.output = OutputCapture()
79
+
163
80
  @staticmethod
164
- def _loads_json_lines(string: str) -> Union[dict, str, None]:
81
+ def _loads_json_lines(string: Union[str, list]) -> Union[dict, str, None]:
165
82
  result = None
166
83
  try:
167
- result = loads(string)
84
+ if type(string) is str:
85
+ result = loads(string)
86
+ elif type(string) is list:
87
+ result = loads(f"[{','.join(string)}]")
168
88
  except decoder.JSONDecodeError:
169
- clean = f'[{",".join(string.splitlines())}]'
89
+ if type(string) is str:
90
+ clean = f"[{','.join(string.splitlines())}]"
91
+ elif type(string) is str:
92
+ clean = str(string)
170
93
  try:
171
94
  result = loads(clean)
172
95
  except decoder.JSONDecodeError:
173
- result = string or None
96
+ try:
97
+ multiline = "[" + string.replace("}{", "},{") + "]"
98
+ result = loads(multiline)
99
+ except decoder.JSONDecodeError:
100
+ result = string or None
174
101
  return result
175
102
 
176
103
  @staticmethod
177
- def _build_result(*results: Tuple[str, Output]) -> Output:
104
+ def _build_result(*results: tuple[str, Output], log_json: bool = False) -> Output:
178
105
  if not results:
179
106
  return None
180
107
  if len(results) == 1:
181
- return results[0][1] or None
108
+ result = results[0][1]
109
+ if len(result) == 1:
110
+ return result[0]
111
+ return result
182
112
  result = {}
183
113
  for name, value in results:
184
- if value:
185
- result[name] = value
186
- return result or None
114
+ result[name] = value
115
+ return result
187
116
 
188
- # pylint: disable=no-member
189
117
  def _run(
190
118
  self,
191
- arg_list: List,
119
+ arg_list: list,
192
120
  func: Callable,
193
- raw_bytes: bool = False,
194
- log_level: str = LOG_LVL,
121
+ output_options: OutputOptions,
195
122
  ) -> dict:
196
123
  self._logger.debug("%s: %s", func.__name__, arg_list)
197
124
  arg_list.insert(0, "borgapi")
198
125
  arg_list = [str(arg) for arg in arg_list]
199
- args = self.archiver.get_args(arg_list, os.environ.get("SSH_ORIGINAL_COMMAND"))
126
+ args = self.archiver.get_args(arg_list, os.getenv("SSH_ORIGINAL_COMMAND", None))
200
127
 
201
128
  prev_json = self.archiver.log_json
202
129
  log_json = getattr(args, "log_json", prev_json)
203
130
  self.archiver.log_json = log_json
204
131
 
205
- capture = OutputCapture(raw_bytes, log_json, log_level)
206
- try:
207
- func(args)
208
- except Exception as e:
209
- self._logger.error(e)
210
- raise e
211
- else:
212
- capture_result = capture.getvalues()
213
- finally:
214
- capture.close()
132
+ with self.output(output_options):
133
+ try:
134
+ func(args)
135
+ except Exception as e:
136
+ self._logger.error(e)
137
+ raise e
138
+ else:
139
+ capture_result = self.output.getvalues()
215
140
 
216
141
  self.archiver.log_json = prev_json
217
142
 
@@ -221,7 +146,7 @@ class BorgAPI:
221
146
  args = {**self.options, **(value or {})}
222
147
  return options_class(**args)
223
148
 
224
- def _get_option_list(self, value: dict, options_class: OptionsBase) -> List:
149
+ def _get_option_list(self, value: dict, options_class: OptionsBase) -> list:
225
150
  option = self._get_option(value, options_class)
226
151
  return option.parse()
227
152
 
@@ -251,13 +176,65 @@ class BorgAPI:
251
176
 
252
177
  return lvl
253
178
 
179
+ def _get_basic_results(self, output: dict, opts: OutputOptions) -> dict:
180
+ result_list = []
181
+ if opts.stats_show:
182
+ if opts.stats_json:
183
+ result_list.append(("stats", self._loads_json_lines(output["stdout"])))
184
+ else:
185
+ result_list.append(("stats", output["stats"]))
186
+
187
+ if opts.list_show:
188
+ if opts.list_json:
189
+ result_list.append(("list", self._loads_json_lines(output["list"])))
190
+ else:
191
+ result_list.append(("list", output["list"]))
192
+
193
+ if opts.prog_show:
194
+ if opts.prog_json:
195
+ result_list.append(("prog", self._loads_json_lines(output["stderr"])))
196
+ else:
197
+ result_list.append(("prog", output["stderr"]))
198
+
199
+ return result_list
200
+
201
+ def _set_environ_defaults(self):
202
+ for key, value in ENVIRONMENT_DEFAULTS.items():
203
+ if os.getenv(key) is None:
204
+ os.environ[key] = value
205
+
206
+
207
+ class BorgAPI(BorgAPIBase):
208
+ """Automate borg in code."""
209
+
210
+ def __init__(
211
+ self,
212
+ defaults: dict = None,
213
+ options: dict = None,
214
+ log_level: str = LOG_LVL,
215
+ log_json: bool = False,
216
+ environ: dict = None,
217
+ ):
218
+ """Set the options to be used across the different command call.
219
+
220
+ :param defaults: options for specific commands to always use, defaults to None
221
+ :type defaults: dict, optional
222
+ :param options: common flags for all commands, defaults to None
223
+ :type options: dict, optional
224
+ :param log_level: level to record logged messages at, defaults to LOG_LVL
225
+ :type log_level: str, optional
226
+ :param log_json: if the output should be in json or string format, defaults to False
227
+ :type log_json: bool, optional
228
+ """
229
+ super().__init__(defaults, options, log_level, log_json, environ)
230
+
254
231
  def set_environ(
255
232
  self,
256
233
  filename: str = None,
257
234
  dictionary: dict = None,
258
235
  **kwargs: Options,
259
236
  ) -> None:
260
- """Load environment variables from file
237
+ """Load environment variables from file.
261
238
 
262
239
  If nothing is provided, load_dotenv's default value will be used.
263
240
 
@@ -281,15 +258,15 @@ class BorgAPI:
281
258
 
282
259
  self._previous_dotenv = variables.keys()
283
260
 
284
- config = StringIO()
285
- for key, value in variables.items():
286
- config.write(f"{key}={value}\n")
287
- config.seek(0)
288
- load_dotenv(stream=config, override=True)
289
- config.close()
261
+ with StringIO() as config:
262
+ for key, value in variables.items():
263
+ config.write(f"{key}={value}\n")
264
+ config.seek(0)
265
+ load_dotenv(stream=config, override=True)
266
+ config.close()
290
267
 
291
268
  def unset_environ(self, *variable: Optional[str]) -> None:
292
- """Remove variables from the environment
269
+ """Remove variables from the environment.
293
270
 
294
271
  If no variable is provided the values set from the previous call to `set_environ`
295
272
  will be removed.
@@ -309,8 +286,10 @@ class BorgAPI:
309
286
  encryption: str = "repokey",
310
287
  **options: Options,
311
288
  ) -> Output:
312
- """Initialize an empty repository. A repository is a filesystem directory
313
- containing the deduplicated data from zero or more archives.
289
+ """Initialize an empty repository.
290
+
291
+ A repository is a filesystem directory containing the deduplicated data
292
+ from zero or more archives.
314
293
 
315
294
  :param repository: repository to create
316
295
  :type repository: str
@@ -322,15 +301,26 @@ class BorgAPI:
322
301
  json dict if json flag used, str otherwise
323
302
  :rtype: Output
324
303
  """
304
+ common_options = self._get_option(options, CommonOptions)
305
+ init_options = self.optionals.get("init", options)
306
+
325
307
  arg_list = []
326
308
  arg_list.extend(self._get_option_list(options, CommonOptions))
327
309
  arg_list.append("init")
328
310
  arg_list.extend(["--encryption", encryption])
329
- arg_list.extend(self.optionals.to_list("init", options))
311
+ arg_list.extend(init_options.parse())
330
312
  arg_list.append(repository)
331
313
 
332
- lvl = self._get_log_level(options)
333
- return self._run(arg_list, self.archiver.do_init, log_level=lvl)
314
+ opts = OutputOptions(
315
+ log_lvl=self._get_log_level(options),
316
+ log_json=common_options.log_json,
317
+ prog_show=common_options.progress,
318
+ prog_json=common_options.log_json,
319
+ )
320
+ output = self._run(arg_list, self.archiver.do_init, output_options=opts)
321
+
322
+ result_list = self._get_basic_results(output, opts)
323
+ return self._build_result(*result_list, log_json=opts.log_json)
334
324
 
335
325
  def create(
336
326
  self,
@@ -338,8 +328,7 @@ class BorgAPI:
338
328
  *paths: str,
339
329
  **options: Options,
340
330
  ) -> Output:
341
- """Create a backup archive containing all files found while recursively
342
- traversing all paths specified.
331
+ """Create a backup archive of all files found while recursively traversing specified paths.
343
332
 
344
333
  :param archive: name of archive to create (must be also a valid directory name)
345
334
  :type archive: str
@@ -365,21 +354,24 @@ class BorgAPI:
365
354
  arg_list.append(archive)
366
355
  arg_list.extend(paths)
367
356
 
368
- lvl = self._get_log_level(options)
369
- output = self._run(arg_list, self.archiver.do_create, log_level=lvl)
370
-
371
- result_list = []
372
- if create_options.stats and not create_options.json:
373
- result_list.append(("stats", output["stats"]))
374
- elif create_options.json:
375
- result_list.append(("stats", self._loads_json_lines(output["stdout"])))
376
-
377
- if create_options.list and not common_options.log_json:
378
- result_list.append(("list", output["list"]))
379
- elif create_options.list and common_options.log_json:
380
- result_list.append(("list", self._loads_json_lines(output["stderr"])))
357
+ opts = OutputOptions(
358
+ log_lvl=self._get_log_level(options),
359
+ log_json=common_options.log_json,
360
+ stats_show=create_options.stats or create_options.json,
361
+ stats_json=create_options.json,
362
+ list_show=create_options.list,
363
+ list_json=common_options.log_json,
364
+ prog_show=common_options.progress,
365
+ prog_json=common_options.log_json,
366
+ )
367
+ output = self._run(arg_list, self.archiver.do_create, output_options=opts)
381
368
 
382
- return self._build_result(*result_list)
369
+ result_list = self._get_basic_results(output, opts)
370
+ if opts.list_show:
371
+ if opts.list_json:
372
+ result_list.remove(("list", []))
373
+ result_list.append(("list", self._loads_json_lines(output["stderr"])))
374
+ return self._build_result(*result_list, log_json=opts.log_json)
383
375
 
384
376
  def extract(
385
377
  self,
@@ -411,25 +403,26 @@ class BorgAPI:
411
403
  arg_list.append(archive)
412
404
  arg_list.extend(paths)
413
405
 
414
- raw_bytes = options.get("stdout", False)
415
- lvl = self._get_log_level(options)
406
+ opts = OutputOptions(
407
+ raw_bytes=extract_options.stdout,
408
+ log_lvl=self._get_log_level(options),
409
+ log_json=common_options.log_json,
410
+ list_show=extract_options.list,
411
+ list_json=common_options.log_json,
412
+ prog_show=common_options.progress,
413
+ prog_json=common_options.log_json,
414
+ )
416
415
  output = self._run(
417
416
  arg_list,
418
417
  self.archiver.do_extract,
419
- raw_bytes=raw_bytes,
420
- log_level=lvl,
418
+ output_options=opts,
421
419
  )
422
420
 
423
- result_list = []
424
- if extract_options.list and not common_options.log_json:
425
- result_list.append(("list", output["list"]))
426
- elif extract_options.list and common_options.log_json:
427
- result_list.append(("list", self._loads_json_lines(output["list"])))
428
-
429
- if extract_options.stdout:
421
+ result_list = self._get_basic_results(output, opts)
422
+ if opts.raw_bytes:
430
423
  result_list.append(("extract", output["stdout"]))
431
424
 
432
- return self._build_result(*result_list)
425
+ return self._build_result(*result_list, log_json=opts.log_json)
433
426
 
434
427
  def check(self, *repository_or_archive: str, **options: Options) -> Output:
435
428
  """Verify the consistency of a repository and the corresponding archives.
@@ -443,16 +436,26 @@ class BorgAPI:
443
436
  dict if json flag used, str otherwise
444
437
  :rtype: Output
445
438
  """
439
+ common_options = self._get_option(options, CommonOptions)
440
+ check_options = self.optionals.get("check", options)
441
+
446
442
  arg_list = []
447
- arg_list.extend(self._get_option_list(options, CommonOptions))
443
+ arg_list.extend(common_options.parse())
448
444
  arg_list.append("check")
449
- arg_list.extend(self.optionals.to_list("check", options))
445
+ arg_list.extend(check_options.parse())
450
446
  arg_list.extend(self._get_option_list(options, ArchiveOutput))
451
447
  arg_list.extend(repository_or_archive)
452
448
 
453
- lvl = self._get_log_level(options)
454
- self._run(arg_list, self.archiver.do_check, log_level=lvl)
455
- return self._build_result()
449
+ opts = OutputOptions(
450
+ log_lvl=self._get_log_level(options),
451
+ log_json=common_options.log_json,
452
+ prog_show=common_options.progress,
453
+ prog_json=common_options.log_json,
454
+ )
455
+ output = self._run(arg_list, self.archiver.do_check, output_options=opts)
456
+
457
+ result_list = self._get_basic_results(output, opts)
458
+ return self._build_result(*result_list, log_json=opts.log_json)
456
459
 
457
460
  def rename(
458
461
  self,
@@ -473,17 +476,25 @@ class BorgAPI:
473
476
  dict if json flag used, str otherwise
474
477
  :rtype: Output
475
478
  """
479
+ common_options = self._get_option(options, CommonOptions)
480
+
476
481
  arg_list = []
477
- arg_list.extend(self._get_option_list(options, CommonOptions))
482
+ arg_list.extend(common_options.parse())
478
483
  arg_list.append("rename")
479
484
  arg_list.append(archive)
480
485
  arg_list.append(newname)
481
486
 
482
- lvl = self._get_log_level(options)
483
- self._run(arg_list, self.archiver.do_rename, log_level=lvl)
484
- return self._build_result()
487
+ opts = OutputOptions(
488
+ log_lvl=self._get_log_level(options),
489
+ log_json=common_options.log_json,
490
+ prog_show=common_options.progress,
491
+ prog_json=common_options.log_json,
492
+ )
493
+ output = self._run(arg_list, self.archiver.do_rename, output_options=opts)
494
+
495
+ result_list = self._get_basic_results(output, opts)
496
+ return self._build_result(*result_list, log_json=opts.log_json)
485
497
 
486
- # pylint: disable=redefined-builtin
487
498
  def list(
488
499
  self,
489
500
  repository_or_archive: str,
@@ -515,16 +526,26 @@ class BorgAPI:
515
526
  arg_list.append(repository_or_archive)
516
527
  arg_list.extend(paths)
517
528
 
518
- lvl = self._get_log_level(options)
519
- output = self._run(arg_list, self.archiver.do_list, log_level=lvl)
529
+ opts = OutputOptions(
530
+ log_lvl=self._get_log_level(options),
531
+ log_json=common_options.log_json,
532
+ list_show=True,
533
+ list_json=list_options.json_lines or list_options.json,
534
+ prog_show=common_options.progress,
535
+ prog_json=common_options.log_json,
536
+ )
537
+ output = self._run(arg_list, self.archiver.do_list, output_options=opts)
520
538
 
521
- result_list = []
522
- if common_options.log_json or list_options.json or list_options.json_lines:
523
- result_list.append(("list", self._loads_json_lines(output["stdout"])))
524
- else:
525
- result_list.append(("list", output["stdout"]))
539
+ result_list = self._get_basic_results(output, opts)
540
+ if opts.list_show:
541
+ if opts.list_json:
542
+ result_list.remove(("list", []))
543
+ result_list.append(("list", self._loads_json_lines(output["stdout"])))
544
+ else:
545
+ result_list.remove(("list", ""))
546
+ result_list.append(("list", output["stdout"]))
526
547
 
527
- return self._build_result(*result_list)
548
+ return self._build_result(*result_list, log_json=opts.log_json)
528
549
 
529
550
  def diff(
530
551
  self,
@@ -560,16 +581,21 @@ class BorgAPI:
560
581
  arg_list.append(archive_2)
561
582
  arg_list.extend(paths)
562
583
 
563
- lvl = self._get_log_level(options)
564
- output = self._run(arg_list, self.archiver.do_diff, log_level=lvl)
584
+ opts = OutputOptions(
585
+ log_lvl=self._get_log_level(options),
586
+ log_json=diff_options.json_lines or common_options.log_json,
587
+ prog_show=common_options.progress,
588
+ prog_json=common_options.log_json,
589
+ )
590
+ output = self._run(arg_list, self.archiver.do_diff, output_options=opts)
565
591
 
566
- result_list = []
567
- if common_options.log_json or diff_options.json_lines:
592
+ result_list = self._get_basic_results(output, opts)
593
+ if opts.log_json:
568
594
  result_list.append(("diff", self._loads_json_lines(output["stdout"])))
569
595
  else:
570
596
  result_list.append(("diff", output["stdout"]))
571
597
 
572
- return self._build_result(*result_list)
598
+ return self._build_result(*result_list, log_json=opts.log_json)
573
599
 
574
600
  def delete(
575
601
  self,
@@ -577,7 +603,7 @@ class BorgAPI:
577
603
  *archives: Optional[str],
578
604
  **options: Options,
579
605
  ) -> Output:
580
- """Delete an archive from the repository or the complete repository
606
+ """Delete an archive from the repository or the complete repository.
581
607
 
582
608
  :param repository_or_archive: repository or archive to delete
583
609
  :type repository_or_archive: str
@@ -601,20 +627,24 @@ class BorgAPI:
601
627
  arg_list.append(repository_or_archive)
602
628
  arg_list.extend(archives)
603
629
 
604
- lvl = self._get_log_level(options)
605
- output = self._run(arg_list, self.archiver.do_delete, log_level=lvl)
606
-
607
- result_list = []
608
- if delete_options.stats and common_options.log_json:
609
- result_list.append(("stats", self._loads_json_lines(output["stats"])))
610
- elif delete_options.stats:
611
- result_list.append(("stats", output["stats"]))
630
+ opts = OutputOptions(
631
+ log_lvl=self._get_log_level(options),
632
+ log_json=common_options.log_json,
633
+ # no json option in this command
634
+ stats_show=delete_options.stats, # or delete_options.json,
635
+ # stats_json = delete_options.json,
636
+ list_show=delete_options.list,
637
+ list_json=common_options.log_json,
638
+ prog_show=common_options.progress,
639
+ prog_json=common_options.log_json,
640
+ )
641
+ output = self._run(arg_list, self.archiver.do_delete, output_options=opts)
612
642
 
613
- return self._build_result(*result_list)
643
+ result_list = self._get_basic_results(output, opts)
644
+ return self._build_result(*result_list, log_json=opts.log_json)
614
645
 
615
646
  def prune(self, repository: str, **options: Options) -> Output:
616
- """Prune a repository by deleting all archives not matching any of the specified
617
- retention options.
647
+ """Prune a repository by deleting all archives not matching the specified retention options.
618
648
 
619
649
  :param repository: repository to prune
620
650
  :type repository: str
@@ -635,20 +665,21 @@ class BorgAPI:
635
665
  arg_list.extend(self._get_option_list(options, ArchivePattern))
636
666
  arg_list.append(repository)
637
667
 
638
- lvl = self._get_log_level(options)
639
- output = self._run(arg_list, self.archiver.do_prune, log_level=lvl)
640
-
641
- result_list = []
642
- if prune_options.list and not common_options.log_json:
643
- result_list.append(("list", output["list"]))
644
- elif prune_options.list and common_options.log_json:
645
- result_list.append(("list", self._loads_json_lines(output["list"])))
646
- if prune_options.stats and not common_options.log_json:
647
- result_list.append(("stats", output["stats"]))
648
- elif prune_options.stats and common_options.log_json:
649
- result_list.append(("stats", self._loads_json_lines(output["stats"])))
668
+ opts = OutputOptions(
669
+ log_lvl=self._get_log_level(options),
670
+ log_json=common_options.log_json,
671
+ # no json option for stats
672
+ stats_show=prune_options.stats, # or prune_options.json,
673
+ # stats_json = prune_options.json,
674
+ list_show=prune_options.list,
675
+ list_json=common_options.log_json,
676
+ prog_show=common_options.progress,
677
+ prog_json=common_options.log_json,
678
+ )
679
+ output = self._run(arg_list, self.archiver.do_prune, output_options=opts)
650
680
 
651
- return self._build_result(*result_list)
681
+ result_list = self._get_basic_results(output, opts)
682
+ return self._build_result(*result_list, log_json=opts.log_json)
652
683
 
653
684
  def compact(self, repository: str, **options: Options) -> Output:
654
685
  """Compact frees repository space by compacting segments.
@@ -671,16 +702,23 @@ class BorgAPI:
671
702
  arg_list.extend(compact_options.parse())
672
703
  arg_list.append(repository)
673
704
 
674
- lvl = self._get_log_level(options)
675
- output = self._run(arg_list, self.archiver.do_compact, log_level=lvl)
676
-
677
- result_list = []
678
- if common_options.log_json:
679
- result_list.append(("compact", self._loads_json_lines(output["repo"])))
680
- else:
681
- result_list.append(("compact", output["repo"]))
705
+ opts = OutputOptions(
706
+ log_lvl=self._get_log_level(options),
707
+ log_json=common_options.log_json,
708
+ repo_show=common_options.verbose,
709
+ repo_json=common_options.log_json,
710
+ prog_show=common_options.progress,
711
+ prog_json=common_options.log_json,
712
+ )
713
+ output = self._run(arg_list, self.archiver.do_compact, output_options=opts)
682
714
 
683
- return self._build_result(*result_list)
715
+ result_list = self._get_basic_results(output, opts)
716
+ if opts.repo_show:
717
+ if opts.repo_json:
718
+ result_list.append(("compact", self._loads_json_lines(output["repo"])))
719
+ else:
720
+ result_list.append(("compact", output["repo"]))
721
+ return self._build_result(*result_list, log_json=opts.log_json)
684
722
 
685
723
  def info(self, repository_or_archive: str, **options: Options) -> Output:
686
724
  """Display detailed information about the specified archive or repository.
@@ -704,16 +742,21 @@ class BorgAPI:
704
742
  arg_list.extend(self._get_option_list(options, ArchiveOutput))
705
743
  arg_list.append(repository_or_archive)
706
744
 
707
- lvl = self._get_log_level(options)
708
- output = self._run(arg_list, self.archiver.do_info, log_level=lvl)
745
+ opts = OutputOptions(
746
+ log_lvl=self._get_log_level(options),
747
+ log_json=info_options.json or common_options.log_json,
748
+ prog_show=common_options.progress,
749
+ prog_json=common_options.log_json,
750
+ )
751
+ output = self._run(arg_list, self.archiver.do_info, output_options=opts)
709
752
 
710
- result_list = []
711
- if info_options.json:
753
+ result_list = self._get_basic_results(output, opts)
754
+ if opts.log_json:
712
755
  result_list.append(("info", self._loads_json_lines(output["stdout"])))
713
756
  else:
714
757
  result_list.append(("info", output["stdout"]))
715
758
 
716
- return self._build_result(*result_list)
759
+ return self._build_result(*result_list, log_json=opts.log_json)
717
760
 
718
761
  def mount(
719
762
  self,
@@ -737,23 +780,37 @@ class BorgAPI:
737
780
  dict if json flag used, str otherwise
738
781
  :rtype: Output
739
782
  """
783
+ common_options = self._get_option(options, CommonOptions)
784
+ mount_options = self.optionals.get("mount", options)
785
+
740
786
  arg_list = []
741
- arg_list.extend(self._get_option_list(options, CommonOptions))
787
+ arg_list.extend(common_options.parse())
742
788
  arg_list.append("mount")
743
- arg_list.extend(self.optionals.to_list("mount", options))
789
+ arg_list.extend(mount_options.parse())
744
790
  arg_list.extend(self._get_option_list(options, ArchiveOutput))
745
791
  arg_list.extend(self._get_option_list(options, ExclusionOutput))
746
792
  arg_list.append(repository_or_archive)
747
793
  arg_list.append(mountpoint)
748
794
  arg_list.extend(paths)
749
795
 
796
+ opts = OutputOptions(
797
+ log_lvl=self._get_log_level(options),
798
+ log_json=common_options.log_json,
799
+ prog_show=common_options.progress,
800
+ prog_json=common_options.log_json,
801
+ )
802
+
750
803
  pid = os.fork()
751
804
  # child process, this one does the actual mount (in the foreground)
752
805
  if pid == 0:
753
- lvl = self._get_log_level(options)
754
- self._run(arg_list, self.archiver.do_mount, log_level=lvl)
755
- return self._build_result()
756
- return self._build_result(("mount", {"pid": pid, "cid": os.getpid()}))
806
+ output = self._run(arg_list, self.archiver.do_mount, output_options=opts)
807
+
808
+ result_list = self._get_basic_results(output, opts)
809
+ return self._build_result(*result_list, log_json=opts.log_json)
810
+
811
+ result_list = self._get_basic_results({}, opts)
812
+ result_list.append(("mount", {"pid": pid, "cid": os.getpid()}))
813
+ return self._build_result(*result_list, log_json=opts.log_json)
757
814
 
758
815
  def umount(self, mountpoint: str, **options: Options) -> Output:
759
816
  """Un-mount a FUSE filesystem that was mounted with `mount`.
@@ -767,14 +824,23 @@ class BorgAPI:
767
824
  dict if json flag used, str otherwise
768
825
  :rtype: Output
769
826
  """
827
+ common_options = self._get_option(options, CommonOptions)
828
+
770
829
  arg_list = []
771
- arg_list.extend(self._get_option_list(options, CommonOptions))
830
+ arg_list.extend(common_options.parse())
772
831
  arg_list.append("umount")
773
832
  arg_list.append(mountpoint)
774
833
 
775
- lvl = self._get_log_level(options)
776
- self._run(arg_list, self.archiver.do_umount, log_level=lvl)
777
- return self._build_result()
834
+ opts = OutputOptions(
835
+ log_lvl=self._get_log_level(options),
836
+ log_json=common_options.log_json,
837
+ prog_show=common_options.progress,
838
+ prog_json=common_options.log_json,
839
+ )
840
+ output = self._run(arg_list, self.archiver.do_umount, output_options=opts)
841
+
842
+ result_list = self._get_basic_results(output, opts)
843
+ return self._build_result(*result_list, log_json=opts.log_json)
778
844
 
779
845
  def key_change_passphrase(self, repository: str, **options: Options) -> Output:
780
846
  """Change the passphrase protecting the repository encryption.
@@ -788,14 +854,23 @@ class BorgAPI:
788
854
  dict if json flag used, str otherwise
789
855
  :rtype: Output
790
856
  """
857
+ common_options = self._get_option(options, CommonOptions)
858
+
791
859
  arg_list = []
792
- arg_list.extend(self._get_option_list(options, CommonOptions))
860
+ arg_list.extend(common_options.parse())
793
861
  arg_list.extend(["key", "change-passphrase"])
794
862
  arg_list.append(repository)
795
863
 
796
- lvl = self._get_log_level(options)
797
- self._run(arg_list, self.archiver.do_change_passphrase, log_level=lvl)
798
- return self._build_result()
864
+ opts = OutputOptions(
865
+ log_lvl=self._get_log_level(options),
866
+ log_json=common_options.log_json,
867
+ prog_show=common_options.progress,
868
+ prog_json=common_options.log_json,
869
+ )
870
+ output = self._run(arg_list, self.archiver.do_change_passphrase, output_options=opts)
871
+
872
+ result_list = self._get_basic_results(output, opts)
873
+ return self._build_result(*result_list, log_json=opts.log_json)
799
874
 
800
875
  def key_export(
801
876
  self,
@@ -816,16 +891,26 @@ class BorgAPI:
816
891
  dict if json flag used, str otherwise
817
892
  :rtype: Output
818
893
  """
894
+ common_options = self._get_option(options, CommonOptions)
895
+ key_export_options = self.optionals.get("key_export", options)
896
+
819
897
  arg_list = []
820
- arg_list.extend(self._get_option_list(options, CommonOptions))
898
+ arg_list.extend(common_options.parse())
821
899
  arg_list.extend(["key", "export"])
822
- arg_list.extend(self.optionals.to_list("key_export", options))
900
+ arg_list.extend(key_export_options.parse())
823
901
  arg_list.append(repository)
824
902
  arg_list.append(path)
825
903
 
826
- lvl = self._get_log_level(options)
827
- self._run(arg_list, self.archiver.do_key_export, log_level=lvl)
828
- return self._build_result()
904
+ opts = OutputOptions(
905
+ log_lvl=self._get_log_level(options),
906
+ log_json=common_options.log_json,
907
+ prog_show=common_options.progress,
908
+ prog_json=common_options.log_json,
909
+ )
910
+ output = self._run(arg_list, self.archiver.do_key_export, output_options=opts)
911
+
912
+ result_list = self._get_basic_results(output, opts)
913
+ return self._build_result(*result_list, log_json=opts.log_json)
829
914
 
830
915
  def key_import(
831
916
  self,
@@ -846,16 +931,26 @@ class BorgAPI:
846
931
  dict if json flag used, str otherwise
847
932
  :rtype: Output
848
933
  """
934
+ common_options = self._get_option(options, CommonOptions)
935
+ key_import_options = self.optionals.get("key_import", options)
936
+
849
937
  arg_list = []
850
- arg_list.extend(self._get_option_list(options, CommonOptions))
938
+ arg_list.extend(common_options.parse())
851
939
  arg_list.extend(["key", "import"])
852
- arg_list.extend(self.optionals.to_list("key_import", options))
940
+ arg_list.extend(key_import_options.parse())
853
941
  arg_list.append(repository)
854
942
  arg_list.append(path)
855
943
 
856
- lvl = self._get_log_level(options)
857
- self._run(arg_list, self.archiver.do_key_import, log_level=lvl)
858
- return self._build_result()
944
+ opts = OutputOptions(
945
+ log_lvl=self._get_log_level(options),
946
+ log_json=common_options.log_json,
947
+ prog_show=common_options.progress,
948
+ prog_json=common_options.log_json,
949
+ )
950
+ output = self._run(arg_list, self.archiver.do_key_import, output_options=opts)
951
+
952
+ result_list = self._get_basic_results(output, opts)
953
+ return self._build_result(*result_list, log_json=opts.log_json)
859
954
 
860
955
  def upgrade(self, repository: str, **options: Options) -> Output:
861
956
  """Upgrade an existing, local Borg repository.
@@ -869,15 +964,115 @@ class BorgAPI:
869
964
  dict if json flag used, str otherwise
870
965
  :rtype: Output
871
966
  """
967
+ common_options = self._get_option(options, CommonOptions)
968
+ upgrade_options = self.optionals.to_list("upgrade", options)
969
+
872
970
  arg_list = []
873
- arg_list.extend(self._get_option_list(options, CommonOptions))
971
+ arg_list.extend(common_options.parse())
874
972
  arg_list.append("upgrade")
875
- arg_list.extend(self.optionals.to_list("upgrade", options))
973
+ arg_list.extend(upgrade_options.parse())
876
974
  arg_list.append(repository)
877
975
 
878
- lvl = self._get_log_level(options)
879
- self._run(arg_list, self.archiver.do_upgrade, log_level=lvl)
880
- return self._build_result()
976
+ opts = OutputOptions(
977
+ log_lvl=self._get_log_level(options),
978
+ log_json=common_options.log_json,
979
+ prog_show=common_options.progress,
980
+ prog_json=common_options.log_json,
981
+ )
982
+ output = self._run(arg_list, self.archiver.do_upgrade, output_options=opts)
983
+
984
+ result_list = self._get_basic_results(output, opts)
985
+ return self._build_result(*result_list, log_json=opts.log_json)
986
+
987
+ def recreate(
988
+ self,
989
+ repository_or_archive: str,
990
+ *paths: Optional[str],
991
+ **options: Options,
992
+ ):
993
+ """Recreate the contents of existing archives.
994
+
995
+ :param repository_or_archive: repository or archive to recreate
996
+ :type repository_or_archive: str
997
+ :param *paths: paths to recreate; patterns are supported
998
+ :type *paths: Optional[str]
999
+ :param **options: optional arguments specific to `recreate` as well as exclusion and
1000
+ common options; defaults to {}
1001
+ :type **options: Options
1002
+ :return: Output of command, None if no output created,
1003
+ dict if json flag used, str otherwise
1004
+ :rtype: Output
1005
+ """
1006
+ common_options = self._get_option(options, CommonOptions)
1007
+ recreate_options = self.optionals.get("recreate", options)
1008
+
1009
+ arg_list = []
1010
+ arg_list.extend(common_options.parse())
1011
+ arg_list.append("recreate")
1012
+ arg_list.extend(recreate_options.parse())
1013
+ arg_list.extend(self._get_option_list(options, ExclusionInput))
1014
+ arg_list.extend(self._get_option_list(options, ArchiveInput))
1015
+ arg_list.append(repository_or_archive)
1016
+ arg_list.extend(paths)
1017
+
1018
+ opts = OutputOptions(
1019
+ log_lvl=self._get_log_level(options),
1020
+ log_json=common_options.log_json,
1021
+ stats_show=recreate_options.stats,
1022
+ stats_json=False, # No json flag
1023
+ list_show=recreate_options.list,
1024
+ list_json=common_options.log_json,
1025
+ prog_show=common_options.progress,
1026
+ prog_json=common_options.log_json,
1027
+ )
1028
+ output = self._run(arg_list, self.archiver.do_recreate, output_options=opts)
1029
+
1030
+ result_list = self._get_basic_results(output, opts)
1031
+ return self._build_result(*result_list, log_json=opts.log_json)
1032
+
1033
+ def import_tar(
1034
+ self,
1035
+ archive: str,
1036
+ tarfile: str,
1037
+ **options: Options,
1038
+ ):
1039
+ """Create a backup archive from a tarball.
1040
+
1041
+ :param archive: name of archive to create (must be also a valid directory name)
1042
+ :type archive: str
1043
+ :param tarfile: input tar file. “-” to read from stdin instead.
1044
+ :type tarfile: str
1045
+ :param **options: optional arguments specific to `import_tar` as well as exclusion and
1046
+ common options; defaults to {}
1047
+ :type **options: Options
1048
+ :return: Output of command, None if no output created,
1049
+ dict if json flag used, str otherwise
1050
+ :rtype: Output
1051
+ """
1052
+ common_options = self._get_option(options, CommonOptions)
1053
+ import_tar_options = self.optionals.get("import_tar", options)
1054
+
1055
+ arg_list = []
1056
+ arg_list.extend(common_options.parse())
1057
+ arg_list.append("import-tar")
1058
+ arg_list.extend(import_tar_options.parse())
1059
+ arg_list.append(archive)
1060
+ arg_list.append(tarfile)
1061
+
1062
+ opts = OutputOptions(
1063
+ log_lvl=self._get_log_level(options),
1064
+ log_json=common_options.log_json,
1065
+ stats_show=import_tar_options.stats or import_tar_options.json,
1066
+ stats_json=import_tar_options.json,
1067
+ list_show=import_tar_options.list,
1068
+ list_json=common_options.log_json,
1069
+ prog_show=common_options.progress,
1070
+ prog_json=common_options.log_json,
1071
+ )
1072
+ output = self._run(arg_list, self.archiver.do_import_tar, output_options=opts)
1073
+
1074
+ result_list = self._get_basic_results(output, opts)
1075
+ return self._build_result(*result_list, log_json=opts.log_json)
881
1076
 
882
1077
  def export_tar(
883
1078
  self,
@@ -913,21 +1108,26 @@ class BorgAPI:
913
1108
  arg_list.append(file)
914
1109
  arg_list.extend(paths)
915
1110
 
916
- lvl = self._get_log_level(options)
1111
+ opts = OutputOptions(
1112
+ log_lvl=self._get_log_level(options),
1113
+ log_json=common_options.log_json,
1114
+ raw_bytes=(file == "-"),
1115
+ list_show=export_tar_options.list,
1116
+ list_json=common_options.log_json,
1117
+ prog_show=common_options.progress,
1118
+ prog_json=common_options.log_json,
1119
+ )
917
1120
  output = self._run(
918
1121
  arg_list,
919
1122
  self.archiver.do_export_tar,
920
- raw_bytes=(file == "-"),
921
- log_level=lvl,
1123
+ output_options=opts,
922
1124
  )
923
1125
 
924
- result_list = []
925
- if export_tar_options.list:
926
- result_list.append(("list", output["list"]))
927
- if file == "-":
1126
+ result_list = self._get_basic_results(output, opts)
1127
+ if opts.raw_bytes:
928
1128
  result_list.append(("tar", output["stdout"]))
929
1129
 
930
- return self._build_result(*result_list)
1130
+ return self._build_result(*result_list, log_json=opts.log_json)
931
1131
 
932
1132
  def serve(self, **options: Options) -> Output:
933
1133
  """Start a repository server process. This command is usually not used manually.
@@ -936,19 +1136,29 @@ class BorgAPI:
936
1136
  dict if json flag used, str otherwise
937
1137
  :rtype: Output
938
1138
  """
1139
+ common_options = self._get_option(options, CommonOptions)
1140
+ serve_options = self.optionals.to_list("serve", options)
1141
+
939
1142
  arg_list = []
940
- arg_list.extend(self._get_option_list(options, CommonOptions))
1143
+ arg_list.extend(common_options.parse())
941
1144
  arg_list.append("serve")
942
- arg_list.extend(self.optionals.to_list("serve", options))
1145
+ arg_list.extend(serve_options.parse())
1146
+
1147
+ opts = OutputOptions(
1148
+ log_lvl=self._get_log_level(options),
1149
+ log_json=common_options.log_json,
1150
+ prog_show=common_options.progress,
1151
+ prog_json=common_options.log_json,
1152
+ )
1153
+ output = self._run(arg_list, self.archiver.do_serve, output_options=opts)
943
1154
 
944
- lvl = self._get_log_level(options)
945
- self._run(arg_list, self.archiver.do_serve, log_level=lvl)
946
- return self._build_result()
1155
+ result_list = self._get_basic_results(output, opts)
1156
+ return self._build_result(*result_list, log_json=opts.log_json)
947
1157
 
948
1158
  def config(
949
1159
  self,
950
1160
  repository: str,
951
- *changes: Union[str, Tuple[str, str]],
1161
+ *changes: Union[str, tuple[str, str]],
952
1162
  **options: Options,
953
1163
  ) -> Output:
954
1164
  """Get and set options in a local repository or cache config file.
@@ -956,7 +1166,7 @@ class BorgAPI:
956
1166
  :param repository: repository to configure
957
1167
  :type repository: str
958
1168
  :param *changes: config key, new value
959
- :type *changes: Union[str, Tuple[str, str]]
1169
+ :type *changes: Union[str, tuple[str, str]]
960
1170
  :param **options: optional arguments specific to `config` as well as
961
1171
  common options; defaults to {}
962
1172
  :type **options: Options
@@ -974,25 +1184,36 @@ class BorgAPI:
974
1184
  arg_list.extend(self._get_option_list(options, ExclusionOutput))
975
1185
  arg_list.append(repository)
976
1186
 
977
- lvl = self._get_log_level(options)
1187
+ opts = OutputOptions(
1188
+ log_lvl=self._get_log_level(options),
1189
+ log_json=common_options.log_json,
1190
+ list_show=config_options.list,
1191
+ list_json=False,
1192
+ prog_show=common_options.progress,
1193
+ prog_json=common_options.log_json,
1194
+ )
1195
+
978
1196
  result_list = []
979
1197
  if not changes:
980
- output = self._run(arg_list, self.archiver.do_config, log_level=lvl)
981
- if config_options.list:
1198
+ output = self._run(arg_list, self.archiver.do_config, output_options=opts)
1199
+ result_list.extend(self._get_basic_results(output, opts))
1200
+ if opts.list_show:
1201
+ result_list.remove(("list", ""))
982
1202
  result_list.append(("list", output["stdout"]))
983
1203
 
984
1204
  change_result = []
985
1205
  for change in changes:
1206
+ new_args = arg_list
986
1207
  if isinstance(change, tuple):
987
- change = arg_list + [change[0], change[1]]
988
- self._run(change, self.archiver.do_config, log_level=lvl)
1208
+ new_args.extend([change[0], change[1]])
989
1209
  else:
990
- change = arg_list + [change]
991
- output = self._run(change, self.archiver.do_config, log_level=lvl)
992
- change_result.append(output["stdout"])
1210
+ new_args.extend([change])
1211
+ output = self._run(new_args, self.archiver.do_config, output_options=opts)
1212
+ change_result.append(output["stdout"].strip())
1213
+ if change_result:
993
1214
  result_list.append(("changes", change_result))
994
1215
 
995
- return self._build_result(*result_list)
1216
+ return self._build_result(*result_list, log_json=opts.log_json)
996
1217
 
997
1218
  def with_lock(
998
1219
  self,
@@ -1016,16 +1237,25 @@ class BorgAPI:
1016
1237
  dict if json flag used, str otherwise
1017
1238
  :rtype: Output
1018
1239
  """
1240
+ common_options = self._get_option(options, CommonOptions)
1241
+
1019
1242
  arg_list = []
1020
- arg_list.extend(self._get_option_list(options, CommonOptions))
1243
+ arg_list.extend(common_options.parse())
1021
1244
  arg_list.append("with-lock")
1022
1245
  arg_list.append(repository)
1023
1246
  arg_list.append(command)
1024
1247
  arg_list.extend(args)
1025
1248
 
1026
- lvl = self._get_log_level(options)
1027
- self._run(arg_list, self.archiver.do_with_lock, log_level=lvl)
1028
- return self._build_result()
1249
+ opts = OutputOptions(
1250
+ log_lvl=self._get_log_level(options),
1251
+ log_json=common_options.log_json,
1252
+ prog_show=common_options.progress,
1253
+ prog_json=common_options.log_json,
1254
+ )
1255
+ output = self._run(arg_list, self.archiver.do_with_lock, output_options=opts)
1256
+
1257
+ result_list = self._get_basic_results(output, opts)
1258
+ return self._build_result(*result_list, log_json=opts.log_json)
1029
1259
 
1030
1260
  def break_lock(self, repository: str, **options: Options) -> Output:
1031
1261
  """Break the repository and cache locks.
@@ -1039,14 +1269,23 @@ class BorgAPI:
1039
1269
  dict if json flag used, str otherwise
1040
1270
  :rtype: Output
1041
1271
  """
1272
+ common_options = self._get_option(options, CommonOptions)
1273
+
1042
1274
  arg_list = []
1043
- arg_list.extend(self._get_option_list(options, CommonOptions))
1275
+ arg_list.extend(common_options.parse())
1044
1276
  arg_list.append("break-lock")
1045
1277
  arg_list.append(repository)
1046
1278
 
1047
- lvl = self._get_log_level(options)
1048
- self._run(arg_list, self.archiver.do_break_lock, log_level=lvl)
1049
- return self._build_result()
1279
+ opts = OutputOptions(
1280
+ log_lvl=self._get_log_level(options),
1281
+ log_json=common_options.log_json,
1282
+ prog_show=common_options.progress,
1283
+ prog_json=common_options.log_json,
1284
+ )
1285
+ output = self._run(arg_list, self.archiver.do_break_lock, output_options=opts)
1286
+
1287
+ result_list = self._get_basic_results(output, opts)
1288
+ return self._build_result(*result_list, log_json=opts.log_json)
1050
1289
 
1051
1290
  def benchmark_crud(
1052
1291
  self,
@@ -1075,8 +1314,72 @@ class BorgAPI:
1075
1314
  arg_list.append(repository)
1076
1315
  arg_list.append(path)
1077
1316
 
1078
- lvl = self._get_log_level(options)
1079
- output = self._run(arg_list, self.archiver.do_benchmark_crud, log_level=lvl)
1317
+ opts = OutputOptions(
1318
+ log_lvl=self._get_log_level(options),
1319
+ log_json=common_options.log_json,
1320
+ prog_show=common_options.progress,
1321
+ prog_json=common_options.log_json,
1322
+ )
1323
+ output = self._run(arg_list, self.archiver.do_benchmark_crud, output_options=opts)
1324
+
1325
+ result_list = self._get_basic_results(output, opts)
1326
+ result_list.append(("benchmark", output["stdout"]))
1327
+ return self._build_result(*result_list, log_json=opts.log_json)
1328
+
1329
+
1330
+ class BorgAPIAsync(BorgAPI):
1331
+ """Async version of the :class:`BorgAPI`."""
1332
+
1333
+ CMDS = [
1334
+ "set_environ",
1335
+ "unset_environ",
1336
+ "init",
1337
+ "create",
1338
+ "extract",
1339
+ "check",
1340
+ "rename",
1341
+ "list",
1342
+ "diff",
1343
+ "delete",
1344
+ "prune",
1345
+ "compact",
1346
+ "info",
1347
+ "mount",
1348
+ "umount",
1349
+ "key_change_passphrase",
1350
+ "key_export",
1351
+ "key_import",
1352
+ "upgrade",
1353
+ "recreate",
1354
+ "import_tar",
1355
+ "export_tar",
1356
+ "serve",
1357
+ "config",
1358
+ "with_lock",
1359
+ "break_lock",
1360
+ "benchmark_crud",
1361
+ ]
1362
+
1363
+ def __init__(self, *args, **kwargs):
1364
+ """Turn the commands in `:class:`BorgAPI` into async methods.
1365
+
1366
+ View the `:class:`BorgAPI` for init arguments.
1367
+ """
1368
+ super().__init__(*args, **kwargs)
1369
+
1370
+ for cmd in self.CMDS:
1371
+ synced = getattr(super(), cmd)
1372
+ wrapped = self._force_async(synced)
1373
+ setattr(self, cmd, wrapped)
1374
+
1375
+ self.pool = ThreadPoolExecutor()
1376
+
1377
+ def _force_async(self, fn):
1378
+ """Turn a sync function to async function using threads."""
1379
+
1380
+ @functools.wraps(fn)
1381
+ def wrapper(*args, **kwargs):
1382
+ future = self.pool.submit(fn, *args, **kwargs)
1383
+ return wrap_future(future) # make it awaitable
1080
1384
 
1081
- result_list = [("benchmark", output["stdout"])]
1082
- return self._build_result(*result_list)
1385
+ return wrapper