diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 5d17d2e93..b6bc18dc0 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -39,6 +39,33 @@ archive_progress path Current path +progress_message + A message-based progress information with no concrete progress information, just a message + saying what is currently worked on. + + operation + integer ID of the operation + finished + boolean indicating whether the operation has finished, only the last object for an *operation* + can have this property set to *true*. + message + current progress message (may be empty/absent) + +progress_percent + Absolute progress display with defined end/total and current value. + + operation + integer ID of the operation + finished + boolean indicating whether the operation has finished, only the last object for an *operation* + can have this property set to *true*. + message + A formatted progress message, this will include the percentage and perhaps other information + current + Current value (always less-or-equal to *total*) + total + Total value + file_status This is only output by :ref:`borg_create` and :ref:`borg_recreate` if ``--list`` is specified. The usual rules for the file listing applies, including the ``--filter`` option. diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 8f1e9cbca..7f2a4e670 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1377,10 +1377,20 @@ def ellipsis_truncate(msg, space): class ProgressIndicatorBase: LOGGER = 'borg.output.progress' + JSON_TYPE = None + json = False + + operation_id_counter = 0 + + @classmethod + def operation_id(cls): + cls.operation_id_counter += 1 + return cls.operation_id_counter def __init__(self): self.handler = None self.logger = logging.getLogger(self.LOGGER) + self.id = self.operation_id() # If there are no handlers, set one up explicitly because the # terminator and propagation needs to be set. If there are, @@ -1394,6 +1404,7 @@ class ProgressIndicatorBase: try: formatter = logger.formatter terminator = '\n' if logger.json else '\r' + self.json = logger.json except AttributeError: terminator = '\r' else: @@ -1404,12 +1415,30 @@ class ProgressIndicatorBase: if self.logger.level == logging.NOTSET: self.logger.setLevel(logging.WARN) self.logger.propagate = False + self.emit = self.logger.getEffectiveLevel() == logging.INFO def __del__(self): if self.handler is not None: self.logger.removeHandler(self.handler) self.handler.close() + def output_json(self, *, finished=False, **kwargs): + assert self.json + if not self.emit: + return + print(json.dumps(dict( + operation=self.id, + type=self.JSON_TYPE, + finished=finished, + **kwargs, + )), file=sys.stderr) + + def finish(self): + if self.json: + self.output_json(finished=True) + else: + self.output('') + def justify_to_terminal_size(message): terminal_space = get_terminal_size(fallback=(-1, -1))[0] @@ -1420,14 +1449,18 @@ def justify_to_terminal_size(message): class ProgressIndicatorMessage(ProgressIndicatorBase): - def output(self, msg): - self.logger.info(justify_to_terminal_size(msg)) + JSON_TYPE = 'progress_message' - def finish(self): - self.output('') + def output(self, msg): + if self.json: + self.output_json(message=msg) + else: + self.logger.info(justify_to_terminal_size(msg)) class ProgressIndicatorPercent(ProgressIndicatorBase): + JSON_TYPE = 'progress_percent' + def __init__(self, total=0, step=5, start=0, msg="%3.0f%%"): """ Percentage-based progress indicator @@ -1466,22 +1499,23 @@ class ProgressIndicatorPercent(ProgressIndicatorBase): if pct is not None: # truncate the last argument, if no space is available if info is not None: - # no need to truncate if we're not outputing to a terminal - terminal_space = get_terminal_size(fallback=(-1, -1))[0] - if terminal_space != -1: - space = terminal_space - len(self.msg % tuple([pct] + info[:-1] + [''])) - info[-1] = ellipsis_truncate(info[-1], space) + if not self.json: + # no need to truncate if we're not outputing to a terminal + terminal_space = get_terminal_size(fallback=(-1, -1))[0] + if terminal_space != -1: + space = terminal_space - len(self.msg % tuple([pct] + info[:-1] + [''])) + info[-1] = ellipsis_truncate(info[-1], space) return self.output(self.msg % tuple([pct] + info), justify=False) return self.output(self.msg % pct) def output(self, message, justify=True): - if justify: - message = justify_to_terminal_size(message) - self.logger.info(message) - - def finish(self): - self.output('') + if self.json: + self.output_json(message=message, current=self.counter, total=self.total) + else: + if justify: + message = justify_to_terminal_size(message) + self.logger.info(message) class ProgressIndicatorEndless: