diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index df6d50329..5ce72f8da 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -96,6 +96,8 @@ log_message :ref:`msgid ` Message ID, may be *none* or absent +See Prompts_ for the types used by prompts. + .. rubric:: Examples (reformatted, each object would be on exactly one line) :ref:`borg_extract` progress:: @@ -129,10 +131,78 @@ A debug log message:: {"message": "35 self tests completed in 0.08 seconds", "type": "log_message", "created": 1488278449.5575905, "levelname": "DEBUG", "name": "borg.archiver"} +Prompts +------- + +Prompts assume a JSON form as well when the ``--log-json`` option is specified. Responses +are still read verbatim from *stdin*, while prompts are JSON messages printed to *stderr*, +just like log messages. + +Prompts use the *question_prompt*, *question_prompt_retry*, *question_invalid_answer*, +*question_accepted_default*, *question_accepted_true*, *question_accepted_false* and +*question_env_answer* types. + +The *message* property contains the same string displayed regularly in the same situation, +while the *msgid* property may contain a msgid_, typically the name of the +environment variable that can be used to override the prompt. It is the same for all JSON +messages pertaining to the same prompt. + +The *is_prompt* boolean property distinguishes informational messages from prompts, it +is true for *question_prompt* and *question_prompt_retry* types, otherwise it is false. + +.. rubric:: Examples (reformatted, each object would be on exactly one line) + +Providing an invalid answer:: + + {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": true, + "message": "... Type 'YES' if you understand this and want to continue: "} + incorrect answer # input on stdin + {"type": "question_invalid_answer", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": false, + "message": "Invalid answer, aborting."} + +Providing a false (negative) answer:: + + {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": true, + "message": "... Type 'YES' if you understand this and want to continue: "} + NO # input on stdin + {"type": "question_accepted_false", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", + "message": "Aborting.", "is_prompt": false} + +Providing a true (affirmative) answer:: + + {"type": "question_prompt", "msgid": "BORG_CHECK_I_KNOW_WHAT_I_AM_DOING", "is_prompt": true, + "message": "... Type 'YES' if you understand this and want to continue: "} + YES # input on stdin + # no further output, just like the prompt without --log-json + +Passphrase prompts +------------------ + +Passphrase prompts should be handled differently. Use the environment variables *BORG_PASSPHRASE* +and *BORG_NEW_PASSPHRASE* (see :ref:`env_vars` for reference) to pass passphrases to Borg, don't +use the interactive passphrase prompts. + +When setting a new passphrase (:ref:`borg_init`, :ref:`borg_key_change-passphrase`) normally +Borg prompts whether it should display the passphrase. This can be suppressed by setting +the environment variable *BORG_DISPLAY_PASSPHRASE* to *no*. + +When "confronted" with an unknown repository, where the application does not know whether +the repository is encrypted, the following algorithm can be followed to detect encryption: + +1. Set *BORG_PASSPHRASE* to gibberish (for example a freshly generated UUID4, which cannot + possibly be the passphrase) +2. Invoke ``borg list repository ...`` +3. If this fails, due the repository being encrypted and the passphrase obviously being + wrong, you'll get an error with the *PassphraseWrong* msgid. + + The repository is encrypted, for further access the application will need the passphrase. + +4. If this does not fail, then the repository is not encrypted. + Standard output --------------- -*stdout* is different and more command-dependent. Commands like :ref:`borg_info`, :ref:`borg_create` +*stdout* is different and more command-dependent than logging. Commands like :ref:`borg_info`, :ref:`borg_create` and :ref:`borg_list` implement a ``--json`` option which turns their regular output into a single JSON object. Dates are formatted according to ISO-8601 with the strftime format string '%a, %Y-%m-%d %H:%M:%S', @@ -461,3 +531,15 @@ Operations *info* is one string element, the name of the path currently extracted. - extract.permissions - archive.delete + +Prompts + BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK + For "Warning: Attempting to access a previously unknown unencrypted repository" + BORG_RELOCATED_REPO_ACCESS_IS_OK + For "Warning: The repository at location ... was previously located at ..." + BORG_CHECK_I_KNOW_WHAT_I_AM_DOING + For "Warning: 'check --repair' is an experimental feature that might result in data loss." + BORG_DELETE_I_KNOW_WHAT_I_AM_DOING + For "You requested to completely DELETE the repository *including* all archives it contains:" + BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING + For "recreate is an experimental feature." diff --git a/docs/usage_general.rst.inc b/docs/usage_general.rst.inc index f718b45b7..160a46490 100644 --- a/docs/usage_general.rst.inc +++ b/docs/usage_general.rst.inc @@ -117,6 +117,7 @@ Borg can exit with the following return codes (rc): If you use ``--show-rc``, the return code is also logged at the indicated level as the last log entry. +.. _env_vars: Environment Variables ~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/borg/helpers.py b/src/borg/helpers.py index fe82b92f7..6a9242205 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1307,7 +1307,8 @@ DEFAULTISH = ('Default', 'DEFAULT', 'default', 'D', 'd', '', ) def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, retry_msg=None, invalid_msg=None, env_msg='{} (from {})', falsish=FALSISH, truish=TRUISH, defaultish=DEFAULTISH, - default=False, retry=True, env_var_override=None, ofile=None, input=input, prompt=True): + default=False, retry=True, env_var_override=None, ofile=None, input=input, prompt=True, + msgid=None): """Output (usually a question) and let user input an answer. Qualifies the answer according to falsish, truish and defaultish as True, False or . If it didn't qualify and retry is False (no retries wanted), return the default [which @@ -1337,6 +1338,23 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, :param input: input function [input from builtins] :return: boolean answer value, True or False """ + def output(msg, msg_type, is_prompt=False, **kwargs): + json_output = getattr(logging.getLogger('borg'), 'json', False) + if json_output: + kwargs.update(dict( + type='question_%s' % msg_type, + msgid=msgid, + message=msg, + is_prompt=is_prompt, + )) + print(json.dumps(kwargs), file=sys.stderr) + else: + if is_prompt: + print(msg, file=ofile, end='', flush=True) + else: + print(msg, file=ofile) + + msgid = msgid or env_var_override # note: we do not assign sys.stderr as default above, so it is # really evaluated NOW, not at function definition time. if ofile is None: @@ -1344,13 +1362,13 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, if default not in (True, False): raise ValueError("invalid default value, must be True or False") if msg: - print(msg, file=ofile, end='', flush=True) + output(msg, 'prompt', is_prompt=True) while True: answer = None if env_var_override: answer = os.environ.get(env_var_override) if answer is not None and env_msg: - print(env_msg.format(answer, env_var_override), file=ofile) + output(env_msg.format(answer, env_var_override), 'env_answer', env_var=env_var_override) if answer is None: if not prompt: return default @@ -1361,23 +1379,23 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, answer = truish[0] if default else falsish[0] if answer in defaultish: if default_msg: - print(default_msg, file=ofile) + output(default_msg, 'accepted_default') return default if answer in truish: if true_msg: - print(true_msg, file=ofile) + output(true_msg, 'accepted_true') return True if answer in falsish: if false_msg: - print(false_msg, file=ofile) + output(false_msg, 'accepted_false') return False # if we get here, the answer was invalid if invalid_msg: - print(invalid_msg, file=ofile) + output(invalid_msg, 'invalid_answer') if not retry: return default if retry_msg: - print(retry_msg, file=ofile, end='', flush=True) + output(retry_msg, 'prompt_retry', is_prompt=True) # in case we used an environment variable and it gave an invalid answer, do not use it again: env_var_override = None