diff --git a/bin/npbackup-viewer b/bin/npbackup-viewer new file mode 100644 index 0000000..c0d6d62 --- /dev/null +++ b/bin/npbackup-viewer @@ -0,0 +1,16 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# This file is part of npbackup, and is really just a binary shortcut to launch npbackup.gui.__main__ + +import os +import sys + +sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))) + +from npbackup.gui.__main__ import main_gui + +del sys.path[0] + +if __name__ == "__main__": + main_gui(viewer_mode=True) diff --git a/npbackup/gui/__main__.py b/npbackup/gui/__main__.py index 2089770..7d1290d 100644 --- a/npbackup/gui/__main__.py +++ b/npbackup/gui/__main__.py @@ -19,6 +19,7 @@ import ofunctions.logger_utils from datetime import datetime import dateutil from time import sleep +from ruamel.yaml.comments import CommentedMap import atexit from ofunctions.threading import threaded from ofunctions.misc import BytesConverter @@ -58,15 +59,14 @@ from npbackup.restic_wrapper import ResticRunner LOG_FILE = os.path.join(CURRENT_DIR, "{}.log".format(__intname__)) logger = ofunctions.logger_utils.logger_get_logger(LOG_FILE, debug=_DEBUG) - sg.theme(PYSIMPLEGUI_THEME) sg.SetOptions(icon=OEM_ICON) -def about_gui(version_string: str, full_config: dict) -> None: +def about_gui(version_string: str, full_config: dict = None) -> None: license_content = LICENSE_TEXT - if full_config.g("global_options.auto_upgrade_server_url"): + if full_config and full_config.g("global_options.auto_upgrade_server_url"): auto_upgrade_result = check_new_version(full_config) else: auto_upgrade_result = None @@ -122,7 +122,26 @@ def about_gui(version_string: str, full_config: dict) -> None: window.close() - +def viewer_repo_gui(viewer_repo_uri: str = None, viewer_repo_password: str = None) -> Tuple[str, str]: + """ + Ask for repo and password if not defined in env variables + """ + layout = [ + [sg.Text(_t("config_gui.backup_repo_uri"), size=(35, 1)), sg.Input(viewer_repo_uri, key="-REPO-URI-")], + [sg.Text(_t("config_gui.backup_repo_password"), size=(35, 1)), sg.Input(viewer_repo_password, key="-REPO-PASSWORD-", password_char='*')], + [sg.Push(), sg.Button(_t("generic.cancel"), key="--CANCEL--"), sg.Button(_t("generic.accept"), key="--ACCEPT--")] + ] + window = sg.Window("Viewer", layout, keep_on_top=True, grab_anywhere=True) + while True: + event, values = window.read() + if event in (sg.WIN_CLOSED, sg.WIN_X_EVENT, '--CANCEL--'): + break + if event == '--ACCEPT--': + if values['-REPO-URI-'] and values['-REPO-PASSWORD-']: + break + sg.Popup(_t("main_gui.repo_and_password_cannot_be_empty")) + window.close() + return values['-REPO-URI-'], values['-REPO-PASSWORD-'] def get_gui_data(repo_config: dict) -> Tuple[bool, List[str]]: gui_msg = _t("main_gui.loading_snapshot_list_from_repo") @@ -224,28 +243,28 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool: try: # Since ls returns an iter now, we need to use next - snapshot_id = next(snapshot_content) + snapshot = next(snapshot_content) # Exception that happens when restic cannot successfully get snapshot content except StopIteration: return None, None try: - snap_date = dateutil.parser.parse(snapshot_id["time"]) + snap_date = dateutil.parser.parse(snapshot["time"]) except (KeyError, IndexError): snap_date = "[inconnu]" try: - short_id = snapshot_id["short_id"] + short_id = snapshot["short_id"] except (KeyError, IndexError): short_id = "[inconnu]" try: - username = snapshot_id["username"] + username = snapshot["username"] except (KeyError, IndexError): username = "[inconnu]" try: - hostname = snapshot_id["hostname"] + hostname = snapshot["hostname"] except (KeyError, IndexError): hostname = "[inconnu]" - backup_id = f" {_t('main_gui.backup_content_from')} {snap_date} {_t('main_gui.run_as')} {username}@{hostname} {_t('main_gui.identified_by')} {short_id}" + backup_id = f"{_t('main_gui.backup_content_from')} {snap_date} {_t('main_gui.run_as')} {username}@{hostname} {_t('main_gui.identified_by')} {short_id}" if not backup_id or not snapshot_content: sg.PopupError(_t("main_gui.cannot_get_content"), keep_on_top=True) @@ -375,7 +394,8 @@ def forget_snapshot(repo_config: dict, snapshot_ids: List[str]) -> bool: return result -def _main_gui(): +def _main_gui(viewer_mode: bool): + def select_config_file(): """ Option to select a configuration file @@ -430,24 +450,38 @@ def _main_gui(): window["snapshot-list"].Update(snapshot_list) - config_file = Path(f"{CURRENT_DIR}/npbackup.conf") - if not config_file.exists(): - while True: - config_file = select_config_file() - if config_file: + if not viewer_mode: + config_file = Path(f"{CURRENT_DIR}/npbackup.conf") + if not config_file.exists(): + while True: config_file = select_config_file() - else: - break + if config_file: + config_file = select_config_file() + else: + break - logger.info(f"Using configuration file {config_file}") - full_config = npbackup.configuration.load_config(config_file) - repo_config, config_inheritance = npbackup.configuration.get_repo_config( - full_config - ) - repo_list = npbackup.configuration.get_repo_list(full_config) + logger.info(f"Using configuration file {config_file}") + full_config = npbackup.configuration.load_config(config_file) + repo_config, config_inheritance = npbackup.configuration.get_repo_config( + full_config + ) + repo_list = npbackup.configuration.get_repo_list(full_config) - backup_destination = _t("main_gui.local_folder") - backend_type, repo_uri = get_anon_repo_uri(repo_config.g("repo_uri")) + backup_destination = _t("main_gui.local_folder") + backend_type, repo_uri = get_anon_repo_uri(repo_config.g("repo_uri")) + else: + # Init empty REPO + repo_config = CommentedMap() + repo_config.s("name", "external") + viewer_repo_uri = os.environ.get("RESTIC_REPOSITORY", None) + viewer_repo_password = os.environ.get("RESTIC_PASSWORD", None) + if not viewer_repo_uri or not viewer_repo_password: + viewer_repo_uri, viewer_repo_password = viewer_repo_gui(viewer_repo_uri, viewer_repo_password) + repo_config.s("repo_uri", viewer_repo_uri) + repo_config.s("repo_opts", CommentedMap()) + repo_config.s("repo_opts.repo_password", viewer_repo_password) + # Let's set default backup age to 24h + repo_config.s("repo_opts.minimum_backup_age", 1440) right_click_menu = ["", [_t("generic.destination")]] headings = [ @@ -469,6 +503,7 @@ def _main_gui(): sg.Column( [ [sg.Text(OEM_STRING, font="Arial 14")], + [sg.Text(_t("main_gui.viewer_mode"))] if viewer_mode else [], [sg.Text("{}: ".format(_t("main_gui.backup_state")))], [ sg.Button( @@ -492,7 +527,7 @@ def _main_gui(): enable_events=True, ), sg.Text(f"Type {backend_type}", key="-backend_type-"), - ], + ] if not viewer_mode else [], [ sg.Table( values=[[]], @@ -505,12 +540,12 @@ def _main_gui(): ], [ sg.Button( - _t("main_gui.launch_backup"), key="--LAUNCH-BACKUP--" + _t("main_gui.launch_backup"), key="--LAUNCH-BACKUP--", disabled=viewer_mode ), sg.Button(_t("main_gui.see_content"), key="--SEE-CONTENT--"), - sg.Button(_t("generic.forget"), key="--FORGET--"), # TODO , visible=False if repo_config.g("permissions") != "full" else True), - sg.Button(_t("main_gui.operations"), key="--OPERATIONS--"), - sg.Button(_t("generic.configure"), key="--CONFIGURE--"), + sg.Button(_t("generic.forget"), key="--FORGET--", disabled=viewer_mode), # TODO , visible=False if repo_config.g("permissions") != "full" else True), + sg.Button(_t("main_gui.operations"), key="--OPERATIONS--", disabled=viewer_mode), + sg.Button(_t("generic.configure"), key="--CONFIGURE--", disabled=viewer_mode), sg.Button(_t("generic.about"), key="--ABOUT--"), sg.Button(_t("generic.quit"), key="--EXIT--"), ], @@ -604,7 +639,7 @@ def _main_gui(): except (TypeError, KeyError): sg.PopupNoFrame(_t("main_gui.unknown_repo")) if event == "--ABOUT--": - about_gui(version_string, full_config) + about_gui(version_string, full_config if not viewer_mode else None) if event == "--STATE-BUTTON--": current_state, backup_tz, snapshot_list = get_gui_data(repo_config) gui_update_state() @@ -612,13 +647,13 @@ def _main_gui(): sg.Popup(_t("main_gui.cannot_get_repo_status")) -def main_gui(): +def main_gui(viewer_mode=True): atexit.register( npbackup.common.execution_logs, datetime.utcnow(), ) try: - _main_gui() + _main_gui(viewer_mode=viewer_mode) sys.exit(logger.get_worst_logger_level()) except _tkinter.TclError as exc: logger.critical(f'Tkinter error: "{exc}". Is this a headless server ?') diff --git a/npbackup/translations/main_gui.en.yml b/npbackup/translations/main_gui.en.yml index 6cff404..81ad904 100644 --- a/npbackup/translations/main_gui.en.yml +++ b/npbackup/translations/main_gui.en.yml @@ -35,6 +35,8 @@ en: forget_failed: Failed to forget. Please check the logs operations: Operations select_config_file: Select config file + repo_and_password_cannot_be_empty: Repo and password cannot be empty + viewer_mode: Repository view-only mode # logs last_messages: Last messages diff --git a/npbackup/translations/main_gui.fr.yml b/npbackup/translations/main_gui.fr.yml index ff8f6ab..f69cf30 100644 --- a/npbackup/translations/main_gui.fr.yml +++ b/npbackup/translations/main_gui.fr.yml @@ -35,6 +35,8 @@ fr: forget_failed: Oubli impossible. Veuillez vérifier les journaux operations: Opérations select_config_file: Sélectionner fichier de configuration + repo_and_password_cannot_be_empty: Le dépot et le mot de passe ne peuvent être vides + viewer_mode: Mode visualisation de dépot uniquement # logs last_messages: Last messages