npbackup/npbackup/gui/config.py
Orsiris de Jong d9e6b689a4 WIP: GUI UX
2024-02-05 16:35:37 +01:00

1407 lines
56 KiB
Python

#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# This file is part of npbackup
__intname__ = "npbackup.gui.config"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2022-2024 NetInvent"
__license__ = "GPL-3.0-only"
__build__ = "2024020501"
from typing import List, Union
import os
import pathlib
from logging import getLogger
import PySimpleGUI as sg
import textwrap
from ruamel.yaml.comments import CommentedMap
import npbackup.configuration as configuration
from ofunctions.misc import get_key_from_value
from npbackup.core.i18n_helper import _t
from npbackup.path_helper import CURRENT_EXECUTABLE
from npbackup.customization import INHERITED_ICON, NON_INHERITED_ICON, FILE_ICON, FOLDER_ICON, INHERITED_FILE_ICON, INHERITED_FOLDER_ICON, TREE_ICON, INHERITED_TREE_ICON
if os.name == "nt":
from npbackup.windows.task import create_scheduled_task
logger = getLogger()
# Monkeypatching PySimpleGUI
def delete(self, key):
if key == '':
return False
try:
node = self.tree_dict[key]
key_list = [key, ]
parent_node = self.tree_dict[node.parent]
parent_node.children.remove(node)
while key_list != []:
temp = []
for item in key_list:
temp += self.tree_dict[item].children
del self.tree_dict[item]
key_list = temp
return True
except KeyError:
pass
sg.TreeData.delete = delete
def ask_manager_password(manager_password: str) -> bool:
if manager_password:
if sg.PopupGetText(
_t("config_gui.set_manager_password"), password_char="*"
) == str(manager_password):
return True
sg.PopupError(_t("config_gui.wrong_password"))
return False
return True
def config_gui(full_config: dict, config_file: str):
logger.info("Launching configuration GUI")
# Don't let PySimpleGUI handle key errros since we might have new keys in config file
sg.set_options(
suppress_raise_key_errors=True,
suppress_error_popups=True,
suppress_key_guessing=True,
)
combo_boxes = {
"compression": {
"auto": _t("config_gui.auto"),
"max": _t("config_gui.max"),
"off": _t("config_gui.off"),
},
"source_type": {
"folder_list": _t("config_gui.folder_list"),
"files_from": _t("config_gui.files_from"),
"files_from_verbatim": _t("config_gui.files_from_verbatim"),
"files_from_raw": _t("config_gui.files_from_raw"),
},
"priority": {
"low": _t("config_gui.low"),
"normal": _t("config_gui.normal"),
"high": _t("config_gui.high"),
},
"permissions": {
"backup": _t("config_gui.backup_perms"),
"restore": _t("config_gui.restore_perms"),
"full": _t("config_gui.full_perms"),
},
}
byte_units = ["B", "KB", "KiB", "MB", "MiB", "GB", "GiB", "TB", "TiB", "PB", "PiB"]
ENCRYPTED_DATA_PLACEHOLDER = "<{}>".format(_t("config_gui.encrypted_data"))
def get_objects() -> List[str]:
"""
Adds repos and groups in a list for combobox
"""
object_list = []
for repo in configuration.get_repo_list(full_config):
object_list.append(f"Repo: {repo}")
for group in configuration.get_group_list(full_config):
object_list.append(f"Group: {group}")
return object_list
def create_object(full_config: dict) -> dict:
layout = [
[
sg.Text(_t("generic.type")),
sg.Combo(["repo", "group"], default_value="repo", key="-OBJECT-TYPE-"),
sg.Text(_t("generic.name")),
sg.Input(key="-OBJECT-NAME-"),
],
[
sg.Push(),
sg.Button(_t("generic.cancel"), key="--CANCEL--"),
sg.Button(_t("generic.accept"), key="--ACCEPT--"),
],
]
window = sg.Window(
_t("config_gui.create_object"), layout=layout, keep_on_top=True
)
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, sg.WIN_X_EVENT, "--CANCEL--"):
break
if event == "--ACCEPT--":
object_type = values["-OBJECT-TYPE-"]
object_name = values["-OBJECT-NAME-"]
if object_type == "repo":
if full_config.g(f"repos.{object_name}"):
sg.PopupError(
_t("config_gui.repo_already_exists"), keep_on_top=True
)
continue
full_config.s(f"repos.{object_name}", CommentedMap())
elif object_type == "group":
if full_config.g(f"groups.{object_name}"):
sg.PopupError(
_t("config_gui.group_already_exists"), keep_on_top=True
)
continue
full_config.s(f"groups.{object_name}", CommentedMap())
else:
raise ValueError("Bogus object type given")
window.close()
update_object_gui(None, unencrypted=False)
return full_config
def delete_object(full_config: dict, object_name: str) -> dict:
object_type, object_name = get_object_from_combo(object_name)
result = sg.PopupYesNo(
_t("config_gui.are_you_sure_to_delete") + f" {object_type} {object_name} ?"
)
if result:
full_config.d(f"{object_type}s.{object_name}")
update_object_gui(None, unencrypted=False)
return full_config
def update_object_selector() -> None:
objects = get_objects()
window["-OBJECT-SELECT-"].Update(objects)
window["-OBJECT-SELECT-"].Update(value=objects[0])
def get_object_from_combo(combo_value: str) -> (str, str):
"""
Extracts selected object from combobox
Returns object type and name
"""
if combo_value.startswith("Repo: "):
object_type = "repo"
object_name = combo_value[len("Repo: ") :]
elif combo_value.startswith("Group: "):
object_type = "group"
object_name = combo_value[len("Group: ") :]
return object_type, object_name
def update_gui_values(key, value, inherited, object_type, unencrypted):
"""
Update gui values depending on their type
"""
nonlocal backup_paths_tree
nonlocal tags_tree
nonlocal exclude_files_tree
nonlocal exclude_patterns_tree
nonlocal pre_exec_commands_tree
nonlocal post_exec_commands_tree
nonlocal env_variables_tree
nonlocal encrypted_env_variables_tree
print("MY", key)
if key in ("repo_uri", "repo_group"):
if object_type == "group":
window[key].Disabled = True
else:
window[key].Disabled = False
try:
# Don't bother to update repo name
# Also permissions / manager_password are in a separate gui
if key in ("name", "permissions", "manager_password"):
return
# Don't show sensible info unless unencrypted requested
if not unencrypted:
# Use last part of key only
if key in configuration.ENCRYPTED_OPTIONS:
try:
if value is None or value == "":
return
if not str(value).startswith(configuration.ID_STRING):
value = ENCRYPTED_DATA_PLACEHOLDER
except (KeyError, TypeError):
pass
# Update tree objects
if key in (
"backup_opts.paths",
"backup_opts.tags",
"backup_opts.exclude_patterns",
"backup_opts.exclude_files",
"backup_opts.pre_exec_commands",
"backup_opts.post_exec_commands",
"env.env_variables",
"env.encrypted_env_variables"
):
if not isinstance(value, list):
value = [value]
if key == "backup_opts.paths":
for val in value:
if pathlib.Path(val).is_dir():
if inherited[val]:
icon = INHERITED_FOLDER_ICON
else:
icon = FOLDER_ICON
else:
if inherited[val]:
icon = INHERITED_FILE_ICON
else:
icon = FILE_ICON
backup_paths_tree.insert('', val, val, val, icon=icon)
window['backup_opts.paths'].update(values=backup_paths_tree)
elif key in (
"backup_opts.tags",
"backup_opts.pre_exec_commands",
"backup_opts.post_exec_commands",
"backup_opts.exclude_files",
"backup_opts.exclude_patterns",
"env.env_variables",
"env.encrypted_env_variables"
):
if key == "backup_opts.tags":
tree = tags_tree
if key == "backup_opts.pre_exec_commands":
tree = pre_exec_commands_tree
if key == "backup_opts.post_exec_commands":
tree = post_exec_commands_tree
if key == "backup_opts.exclude_files":
tree = exclude_files_tree
if key == "backup_opts.exclude_patterns":
tree = exclude_patterns_tree
if key == "env.env_variables":
tree = env_variables_tree
if key == "env.encrypted_env_variables":
tree = encrypted_env_variables_tree
for val in value:
if inherited[val]:
icon = INHERITED_TREE_ICON
else:
icon = TREE_ICON
tree.insert('', val, val, val, icon=icon)
window[key].Update(values=tree)
return
# Update units into separate value and unit combobox
if key in ["backup_opts.minimum_backup_size_error", "backup_opts.exclude_files_larger_than", "upload_speed", "download_speed"]:
value, unit = value.split(" ")
window[f"{key}_unit"].Update(unit)
if isinstance(value, list):
value = "\n".join(value)
if key in combo_boxes:
print("combo key")
print(combo_boxes[key][value])
window[key].Update(value=combo_boxes[key][value])
else:
print("nOKEy", key)
window[key].Update(value=value)
# Enable inheritance icon when needed
inheritance_key = f"inherited.{key}"
if inheritance_key in window.AllKeysDict:
if inherited:
window[inheritance_key].update(INHERITED_ICON)
else:
window[inheritance_key].update(NON_INHERITED_ICON)
except KeyError:
logger.error(f"No GUI equivalent for key {key}.")
logger.debug("Trace:", exc_info=True)
except TypeError as exc:
logger.error(f"Error: {exc} for key {key}.")
logger.debug("Trace:", exc_info=True)
def iter_over_config(
object_config: dict,
config_inheritance: dict = None,
object_type: str = None,
unencrypted: bool = False,
root_key: str = "",
):
"""
Iter over a dict while retaining the full key path to current object
"""
base_object = object_config
def _iter_over_config(object_config: dict, root_key=""):
# Special case where env is a dict but we should pass it directly as it to update_gui_values
if isinstance(object_config, dict):
for key in object_config.keys():
if root_key:
_iter_over_config(
object_config[key], root_key=f"{root_key}.{key}"
)
else:
_iter_over_config(object_config[key], root_key=f"{key}")
else:
if config_inheritance:
inherited = config_inheritance.g(root_key)
else:
inherited = False
update_gui_values(
root_key,
base_object.g(root_key),
inherited,
object_type,
unencrypted,
)
_iter_over_config(object_config, root_key)
def update_object_gui(object_name=None, unencrypted=False):
nonlocal backup_paths_tree
nonlocal tags_tree
nonlocal exclude_files_tree
nonlocal exclude_patterns_tree
nonlocal env_variables_tree
nonlocal encrypted_env_variables_tree
# Load fist available repo or group if none given
if not object_name:
object_name = get_objects()[0]
# First we need to clear the whole GUI to reload new values
for key in window.AllKeysDict:
# We only clear config keys, wihch have '.' separator
if "." in str(key) and not "inherited" in str(key):
if isinstance(window[key], sg.Tree):
window[key].Update(sg.TreeData())
else:
window[key]("")
# We also need to clear tree objects
backup_paths_tree = sg.TreeData()
object_type, object_name = get_object_from_combo(object_name)
if object_type == "repo":
object_config, config_inheritance = configuration.get_repo_config(
full_config, object_name, eval_variables=False
)
# Enable settings only valid for repos
window["repo_uri"].Update(visible=True)
window["--SET-PERMISSIONS--"].Update(visible=True)
if object_type == "group":
object_config = configuration.get_group_config(
full_config, object_name, eval_variables=False
)
config_inheritance = None
# Disable settings only valid for repos
window["repo_uri"].Update(visible=False)
window["--SET-PERMISSIONS--"].Update(visible=False)
# Now let's iter over the whole config object and update keys accordingly
iter_over_config(
object_config, config_inheritance, object_type, unencrypted, None
)
def update_global_gui(full_config, unencrypted=False):
global_config = CommentedMap()
# Only update global options gui with identified global keys
for key in full_config.keys():
if key in ("identity", "global_options"):
global_config.s(key, full_config.g(key))
iter_over_config(global_config, None, "group", unencrypted, None)
def update_config_dict(full_config, values):
"""
Update full_config with keys from GUI
keys should always have form section.name or section.subsection.name
"""
return
object_type, object_name = get_object_from_combo(values["-OBJECT-SELECT-"])
if object_type == "repo":
object_group = full_config.g(f"repos.{object_name}.repo_group")
else:
object_group = None
for key, value in values.items():
# Don't update placeholders ;)
if value == ENCRYPTED_DATA_PLACEHOLDER:
continue
if not isinstance(key, str) or (isinstance(key, str) and not "." in key):
# Don't bother with keys that don't contain with "." since they're not in the YAML config file
# but are most probably for GUI events
continue
# Handle combo boxes first to transform translation into key
if key in combo_boxes:
value = get_key_from_value(combo_boxes[key], value)
# check whether we need to split into list
elif not isinstance(value, bool) and not isinstance(value, list):
result = value.split("\n")
if len(result) > 1:
value = result
else:
# Try to convert ints and floats before committing
if "." in value:
try:
value = float(value)
except ValueError:
pass
else:
try:
value = int(value)
except ValueError:
pass
active_object_key = f"{object_type}s.{object_name}.{key}"
current_value = full_config.g(active_object_key)
# Don't bother with inheritance on global options
if not key.startswith("global_options."):
# Don't update items that have been inherited from groups
if object_group:
inheritance_key = f"groups.{object_group}.{key}"
# If object is a list, check which values are inherited from group and remove them
if isinstance(value, list): # WIP # TODO
for entry in full_config.g(inheritance_key):
if entry in value:
value.remove(entry)
# check if value is inherited from group
if full_config.g(inheritance_key) == value:
continue
if object_group:
inherited = full_config.g(inheritance_key)
else:
inherited = False
# WIP print(f"UPDATING {active_object_key} curr={current_value} inherited={inherited} new={value}")
# if not full_config.g(active_object_key):
# full_config.s(active_object_key, CommentedMap())
# Don't bother to update empty strings, empty lists and None
if not current_value and not value:
continue
# Don't bother to update values which haven't changed
if current_value == value:
continue
# full_config.s(active_object_key, value)
return full_config
# TODO: Do we actually save every modified object or just the last ?
def set_permissions(full_config: dict, object_name: str) -> dict:
"""
Sets repo wide repo_uri / password / permissions
"""
object_type, object_name = get_object_from_combo(object_name)
if object_type == "group":
sg.PopupError(_t("config_gui.permissions_only_for_repos"))
return full_config
repo_config, _ = configuration.get_repo_config(
full_config, object_name, eval_variables=False
)
permissions = list(combo_boxes["permissions"].values())
default_perm = repo_config.g("permissions")
if not default_perm:
default_perm = permissions[-1]
manager_password = repo_config.g("manager_password")
layout = [
[
sg.Text(_t("config_gui.permissions"), size=(40, 1)),
sg.Combo(permissions, default_value=default_perm, key="-PERMISSIONS-"),
],
[sg.HorizontalSeparator()],
[
sg.Text(_t("config_gui.set_manager_password"), size=(40, 1)),
sg.Input(
manager_password,
key="-MANAGER-PASSWORD-",
size=(50, 1),
password_char="*",
),
# sg.Button(_t("generic.change"), key="--CHANGE-MANAGER-PASSWORD--")
],
[
sg.Push(),
sg.Button(_t("generic.cancel"), key="--CANCEL--"),
sg.Button(_t("generic.accept"), key="--ACCEPT--"),
],
]
window = sg.Window(_t("config_gui.permissions"), layout, keep_on_top=True)
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, sg.WIN_X_EVENT, "--CANCEL--"):
break
if event == "--ACCEPT--":
if not values["-MANAGER-PASSWORD-"]:
sg.PopupError(
_t("config_gui.setting_permissions_requires_manager_password"),
keep_on_top=True,
)
continue
if len(values["-MANAGER-PASSWORD-"]) < 8:
sg.PopupError(
_t("config_gui.manager_password_too_short"), keep_on_top=True
)
continue
if not values["-PERMISSIONS-"] in permissions:
sg.PopupError(_t("generic.bogus_data_given"), keep_on_top=True)
continue
repo_config.s("permissions", values["-PERMISSIONS-"])
repo_config.s("manager_password", values["-MANAGER-PASSWORD-"])
break
window.close()
full_config.s(f"repos.{object_name}", repo_config)
return full_config
def is_inherited(key: str, values: Union[str, int, float, list]) -> bool:
"""
Checks if value(s) are inherited from group settings
"""
# TODO
return False
def object_layout() -> List[list]:
"""
Returns the GUI layout depending on the object type
"""
backup_col = [
[
sg.Text(
textwrap.fill(f"{_t('config_gui.backup_paths')}"),
size=(None, None), expand_x=True,
),
sg.Text(
textwrap.fill(f"{_t('config_gui.source_type')}"),
size=(None, None), expand_x=True, justification='R'
),
sg.Combo(
list(combo_boxes["source_type"].values()),
key="backup_opts.source_type",
size=(48, 1),
),
],
[
sg.Tree(sg.TreeData(), key="backup_opts.paths", headings=[],
col0_heading=_t("generic.paths"),
expand_x=True, expand_y=True)
],
[
sg.Input(visible=False, key="--ADD-PATHS-FILE--", enable_events=True),
sg.FilesBrowse(_t("generic.add_files"), target="--ADD-PATHS-FILE--"),
sg.Input(visible=False, key="--ADD-PATHS-FOLDER--", enable_events=True),
sg.FolderBrowse(_t("generic.add_folder"), target="--ADD-PATHS-FOLDER--"),
sg.Button(_t("generic.remove_selected"), key="--REMOVE-BACKUP-PATHS--")
],
[
sg.Column(
[
[
sg.Text(_t("config_gui.compression"), size=(20, None)),
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.compression", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Combo(
list(combo_boxes["compression"].values()),
key="backup_opts.compression",
size=(20, 1), pad=0),
],
[
sg.Text(_t("config_gui.backup_priority"), size=(20, 1)),
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.priority", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Combo(
list(combo_boxes["priority"].values()),
key="backup_opts.priority",
size=(20, 1), pad=0
)
],
[
sg.Column([
[
sg.Button("+", key="--ADD-TAG--", size=(3, 1))
],
[
sg.Button("-", key="--REMOVE-TAG--", size=(3, 1))
]
], pad=0, size=(40, 80)),
sg.Column([
[
sg.Tree(sg.TreeData(), key="backup_opts.tags", headings=[],
col0_heading="Tags", col0_width=30, num_rows=3, expand_x=True, expand_y=True)
]
], pad=0, size=(300, 80))
]
], pad=0
),
sg.Column(
[
[
sg.Text(_t("config_gui.minimum_backup_size_error"), size=(40, 2)),
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.minimum_backup_size_error", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Input(key="backup_opts.minimum_backup_size_error", size=(8, 1)),
sg.Combo(byte_units, default_value=byte_units[3], key="backup_opts.minimum_backup_size_error_unit")
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.fs_snapshot", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Checkbox(textwrap.fill(f'{_t("config_gui.use_fs_snapshot")}', width=34), key="backup_opts.use_fs_snapshot", size=(40, 1), pad=0),
]
]
)
],
[
sg.Text(
_t("config_gui.additional_backup_only_parameters"), size=(40, 1)
),
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.additional_backup_only_parameters", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Input(
key="backup_opts.additional_backup_only_parameters", size=(100, 1)
),
],
]
exclusions_col = [
[
sg.Column(
[
[
sg.Button("+", key="--ADD-EXCLUDE-PATTERN--", size=(3, 1))
],
[
sg.Button("-", key="--REMOVE-EXCLUDE-PATTERN--", size=(3, 1))
]
], pad=0,
),
sg.Column(
[
[
sg.Tree(sg.TreeData(), key="backup_opts.exclude_patterns", headings=[],
col0_heading=_t('config_gui.exclude_patterns'),
num_rows=4, expand_x=True, expand_y=True)
]
], pad=0, expand_x=True
)
],
[
sg.HSeparator()
],
[
sg.Column(
[
[
sg.Button("+", key="--ADD-EXCLUDE-FILES--", size=(3, 1))
],
[
sg.Button("-", key="--REMOVE-EXCLUDE-FILES--", size=(3, 1))
]
], pad=0,
),
sg.Column(
[
[
sg.Tree(sg.TreeData(), key="backup_opts.exclude_files", headings=[],
col0_heading=_t('config_gui.exclude_files'),
num_rows=4, expand_x=True, expand_y=True)
]
], pad=0, expand_x=True
)
],
[
sg.HSeparator()
],
[
sg.Text(
_t("config_gui.exclude_files_larger_than"),
size=(40, 1),
),
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.exclude_files_larger_than", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Input(key="backup_opts.exclude_files_larger_than", size=(8, 1)),
sg.Combo(byte_units, default_value=byte_units[3], key="backup_opts.exclude_files_larger_than_unit")
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.ignore_cloud_files", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Checkbox(f'{_t("config_gui.ignore_cloud_files")} ({_t("config_gui.windows_only")})', key="backup_opts.ignore_cloud_files", size=(None, 1)),
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.excludes_case_ignore", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Checkbox(f'{_t("config_gui.excludes_case_ignore")} ({_t("config_gui.windows_always")})', key="backup_opts.excludes_case_ignore", size=(None, 1)),
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.exclude_caches", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Checkbox(_t("config_gui.exclude_cache_dirs"), key="backup_opts.exclude_caches", size=(None, 1)),
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.backup_opts.one_file_system", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Checkbox(_t("config_gui.one_file_system"), key="backup_opts.one_file_system", size=(None, 1)),
],
]
pre_post_col = [
[
sg.Column(
[
[
sg.Button("+", key="--ADD-PRE-EXEC-COMMAND--", size=(3, 1))
],
[
sg.Button("-", key="--REMOVE-PRE-EXEC-COMMAND--", size=(3, 1))
]
], pad=0,
),
sg.Column(
[
[
sg.Tree(sg.TreeData(), key="backup_opts.pre_exec_commands", headings=[],
col0_heading=_t('config_gui.pre_exec_commands'),
num_rows=4, expand_x=True, expand_y=True)
]
], pad=0, expand_x=True
)
],
[
sg.Text(_t("config_gui.maximum_exec_time"), size=(40, 1)),
sg.Input(key="backup_opts.pre_exec_per_command_timeout", size=(8, 1)),
sg.Text(_t("generic.seconds"))
],
[
sg.Checkbox(
_t("config_gui.exec_failure_is_fatal"), key="backup_opts.pre_exec_failure_is_fatal", size=(41, 1)
),
],
[
sg.HorizontalSeparator()
],
[
sg.Column(
[
[
sg.Button("+", key="--ADD-POST-EXEC-COMMAND--", size=(3, 1))
],
[
sg.Button("-", key="--REMOVE-POST-EXEC-COMMAND--", size=(3, 1))
]
], pad=0,
),
sg.Column(
[
[
sg.Tree(sg.TreeData(), key="backup_opts.post_exec_commands", headings=[],
col0_heading=_t('config_gui.post_exec_commands'),
num_rows=4, expand_x=True, expand_y=True)
]
], pad=0, expand_x=True
)
],
[
sg.Text(_t("config_gui.maximum_exec_time"), size=(40, 1)),
sg.Input(key="backup_opts.post_exec_per_command_timeout", size=(8, 1)),
sg.Text(_t("generic.seconds"))
],
[
sg.Checkbox(
_t("config_gui.exec_failure_is_fatal"), key="backup_opts.post_exec_failure_is_fatal", size=(41, 1)
),
],
[
sg.Checkbox(
_t("config_gui.execute_even_on_backup_error"),
key="backup_opts.post_exec_execute_even_on_backup_error",
size=(41, 1),
),
],
]
repo_col = [
[
sg.Text(_t("config_gui.backup_repo_uri"), size=(40, 1)),
sg.Input(key="repo_uri", size=(50, 1)),
],
[
sg.Text(_t("config_gui.backup_repo_password"), size=(40, 1)),
sg.Input(key="repo_opts.repo_password", size=(50, 1)),
],
[
sg.Text(_t("config_gui.backup_repo_password_command"), size=(40, 1)),
sg.Input(key="repo_opts.repo_password_command", size=(50, 1)),
],
[sg.Button(_t("config_gui.set_permissions"), key="--SET-PERMISSIONS--")],
[
sg.Text(_t("config_gui.repo_group"), size=(40, 1)),
sg.Input(key="repo_group", size=(50, 1)),
],
[
sg.Text(
"{}\n({})".format(
_t("config_gui.minimum_backup_age"), _t("generic.minutes")
),
size=(40, 2),
),
sg.Input(key="repo_opts.minimum_backup_age", size=(50, 1)),
],
[
sg.Text(_t("config_gui.upload_speed"), size=(40, 1)),
sg.Input(key="repo_opts.upload_speed", size=(50, 1)),
],
[
sg.Text(_t("config_gui.download_speed"), size=(40, 1)),
sg.Input(key="repo_opts.download_speed", size=(50, 1)),
],
[
sg.Text(_t("config_gui.backend_connections"), size=(40, 1)),
sg.Input(key="repo_opts.backend_connections", size=(50, 1)),
],
[sg.HorizontalSeparator()],
[sg.Text(_t("config_gui.retention_policy"))],
[
sg.Text(_t("config_gui.optional_ntp_server_uri"), size=(40, 1)),
sg.Input(
key="repo_opts.retention_strategy.ntp_time_server", size=(50, 1)
),
],
[
sg.Column(
[
[
sg.Text(_t("config_gui.keep"), size=(30, 1)),
]
]
),
sg.Column(
[
[
sg.Image(NON_INHERITED_ICON, key="inherited.repo_opts.retention_strategy.hourly", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Input(key="repo_opts.retention_strategy.hourly", size=(3, 1)),
sg.Text(_t("config_gui.hourly"), size=(20, 1)),
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.repo_opts.retention_strategy.daily", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Input(key="repo_opts.retention_strategy.daily", size=(3, 1)),
sg.Text(_t("config_gui.daily"), size=(20, 1)),
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.repo_opts.retention_strategy.weekly", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Input(key="repo_opts.retention_strategy.weekly", size=(3, 1)),
sg.Text(_t("config_gui.weekly"), size=(20, 1)),
]
]
),
sg.Column(
[
[
sg.Image(NON_INHERITED_ICON, key="inherited.repo_opts.retention_strategy.monthly", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Input(key="repo_opts.retention_strategy.monthly", size=(3, 1)),
sg.Text(_t("config_gui.monthly"), size=(20, 1)),
],
[
sg.Image(NON_INHERITED_ICON, key="inherited.repo_opts.retention_strategy.yearly", tooltip=_t("config_gui.group_inherited"), pad=1),
sg.Input(key="repo_opts.retention_strategy.yearly", size=(3, 1)),
sg.Text(_t("config_gui.yearly"), size=(20, 1)),
]
]
)
],
]
prometheus_col = [
[sg.Text(_t("config_gui.available_variables"))],
[
sg.Checkbox(_t("config_gui.enable_prometheus"), key="prometheus.metrics", size=(41, 1)),
],
[
sg.Text(_t("config_gui.job_name"), size=(40, 1)),
sg.Input(key="prometheus.backup_job", size=(50, 1)),
],
[
sg.Text(_t("config_gui.metrics_destination"), size=(40, 1)),
sg.Input(key="prometheus.destination", size=(50, 1)),
],
[
sg.Text(_t("config_gui.no_cert_verify"), size=(40, 1)),
sg.Checkbox("", key="prometheus.no_cert_verify", size=(41, 1)),
],
[
sg.Text(_t("config_gui.metrics_username"), size=(40, 1)),
sg.Input(key="prometheus.http_username", size=(50, 1)),
],
[
sg.Text(_t("config_gui.metrics_password"), size=(40, 1)),
sg.Input(key="prometheus.http_password", size=(50, 1)),
],
[
sg.Text(_t("config_gui.instance"), size=(40, 1)),
sg.Input(key="prometheus.instance", size=(50, 1)),
],
[
sg.Text(_t("generic.group"), size=(40, 1)),
sg.Input(key="prometheus.group", size=(50, 1)),
],
[
sg.Text(
f"{_t('config_gui.additional_labels')}\n({_t('config_gui.one_per_line')}\n{_t('config_gui.format_equals')})",
size=(40, 3),
),
sg.Multiline(key="prometheus.additional_labels", size=(48, 3)),
],
]
env_col = [
[
sg.Column(
[
[
sg.Button("+", key="--ADD-ENV-VARIABLE--", size=(3, 1))
],
[
sg.Button("-", key="--REMOVE-ENV-VARIABLE--", size=(3, 1))
]
], pad=0,
),
sg.Column(
[
[
sg.Tree(sg.TreeData(), key="env.env_variables", headings=[_t("generic.value")],
col0_heading=_t('config_gui.env_variables'),
col0_width=1,
auto_size_columns=True,
justification="L",
num_rows=4, expand_x=True, expand_y=True)
]
], pad=0, expand_x=True
)
],
[
sg.Column(
[
[
sg.Button("+", key="--ADD-ENCRYPTED-ENV-VARIABLE--", size=(3, 1))
],
[
sg.Button("-", key="--REMOVE-ENCRYPTED-ENV-VARIABLE--", size=(3, 1))
]
], pad=0,
),
sg.Column(
[
[
sg.Tree(sg.TreeData(), key="env.encrypted_env_variables", headings=[_t("generic.value")],
col0_heading=_t('config_gui.encrypted_env_variables'),
col0_width=1,
auto_size_columns=True,
justification="L",
num_rows=4, expand_x=True, expand_y=True)
]
], pad=0, expand_x=True
)
],
[
sg.Text(_t("config_gui.additional_parameters"), size=(40, 1)),
sg.Input(key="backup_opts.additional_parameters", size=(50, 1)),
],
]
object_list = get_objects()
object_selector = [
[
sg.Text(_t("config_gui.select_object")),
sg.Combo(
object_list,
default_value=object_list[0] if object_list else None,
key="-OBJECT-SELECT-",
enable_events=True,
),
]
]
tab_group_layout = [
[
sg.Tab(
_t("config_gui.backup"),
backup_col,
font="helvetica 16",
key="--tab-backup--",
expand_x=True,
expand_y=True
)
],
[
sg.Tab(
_t("config_gui.backup_destination"),
repo_col,
font="helvetica 16",
key="--tab-repo--",
expand_x=True, expand_y=True,
)
],
[
sg.Tab(
_t("config_gui.exclusions"),
exclusions_col,
font="helvetica 16",
key="--tab-exclusions--",
expand_x=True,
expand_y=True
)
],
[
sg.Tab(
_t("config_gui.pre_post"),
pre_post_col,
font="helvetica 16",
key="--tab-hooks--",
expand_x=True,
expand_y=True
)
],
[
sg.Tab(
_t("config_gui.prometheus_config"),
prometheus_col,
font="helvetica 16",
key="--tab-prometheus--",
expand_x=True, expand_y=True,
)
],
[
sg.Tab(
_t("config_gui.env_variables"),
env_col,
font="helvetica 16",
key="--tab-env--",
expand_x=True, expand_y=True,
)
],
]
_layout = [
[sg.Column(object_selector,
)],
[
sg.TabGroup(
tab_group_layout, enable_events=True, key="--object-tabgroup--"
)
],
]
return _layout
def global_options_layout():
""" "
Returns layout for global options that can't be overrided by group / repo settings
"""
identity_col = [
[sg.Text(_t("config_gui.available_variables_id"))],
[
sg.Text(_t("config_gui.machine_id"), size=(40, 1)),
sg.Input(key="identity.machine_id", size=(50, 1)),
],
[
sg.Text(_t("config_gui.machine_group"), size=(40, 1)),
sg.Input(key="identity.machine_group", size=(50, 1)),
],
]
global_options_col = [
[sg.Text(_t("config_gui.available_variables"))],
[
sg.Text(_t("config_gui.auto_upgrade"), size=(40, 1)),
sg.Checkbox("", key="global_options.auto_upgrade", size=(41, 1)),
],
[
sg.Text(_t("config_gui.auto_upgrade_server_url"), size=(40, 1)),
sg.Input(key="global_options.auto_upgrade_server_url", size=(50, 1)),
],
[
sg.Text(_t("config_gui.auto_upgrade_server_username"), size=(40, 1)),
sg.Input(
key="global_options.auto_upgrade_server_username", size=(50, 1)
),
],
[
sg.Text(_t("config_gui.auto_upgrade_server_password"), size=(40, 1)),
sg.Input(
key="global_options.auto_upgrade_server_password", size=(50, 1)
),
],
[
sg.Text(_t("config_gui.auto_upgrade_interval"), size=(40, 1)),
sg.Input(key="global_options.auto_upgrade_interval", size=(50, 1)),
],
[
sg.Text(_t("generic.identity"), size=(40, 1)),
sg.Input(key="global_options.auto_upgrade_host_identity", size=(50, 1)),
],
[
sg.Text(_t("generic.group"), size=(40, 1)),
sg.Input(key="global_options.auto_upgrade_group", size=(50, 1)),
],
[sg.HorizontalSeparator()],
]
scheduled_task_col = [
[
sg.Text(_t("config_gui.create_scheduled_task_every")),
sg.Input(key="scheduled_task_interval", size=(4, 1)),
sg.Text(_t("generic.minutes")),
sg.Button(_t("generic.create"), key="create_task"),
],
[sg.Text(_t("config_gui.scheduled_task_explanation"))],
]
tab_group_layout = [
[
sg.Tab(
_t("config_gui.machine_identification"),
identity_col,
font="helvetica 16",
key="--tab-global-identification--",
)
],
[
sg.Tab(
_t("generic.options"),
global_options_col,
font="helvetica 16",
key="--tab-global-options--",
)
],
[
sg.Tab(
_t("generic.scheduled_task"),
scheduled_task_col,
font="helvetica 16",
key="--tab-global-scheduled_task--",
)
],
]
_layout = [
[
sg.TabGroup(
tab_group_layout, enable_events=True, key="--global-tabgroup--"
)
],
]
return _layout
def config_layout() -> List[list]:
buttons = [
[
sg.Push(),
sg.Button(
_t("config_gui.create_object"), key="-OBJECT-CREATE-", size=(28, 1)
),
sg.Button(
_t("config_gui.delete_object"), key="-OBJECT-DELETE-", size=(28, 1)
),
sg.Button(_t("generic.cancel"), key="--CANCEL--", size=(13, 1)),
sg.Button(_t("generic.accept"), key="--ACCEPT--", size=(13, 1)),
]
]
tab_group_layout = [
[
sg.Tab(
_t("config_gui.repo_group_config"),
object_layout(),
key="--repo-group-config--",
expand_x=True,
expand_y=True,
pad=0
)
],
[
sg.Tab(
_t("config_gui.global_config"),
global_options_layout(),
key="--global-config--",
expand_x=True,
expand_y=True,
pad=0
)
],
]
_global_layout = [
[
sg.TabGroup(
tab_group_layout, enable_events=True, key="--configtabgroup--", expand_x=True, expand_y=True, pad=0,
)
],
[sg.Push(), sg.Column(buttons,
)],
]
return _global_layout
right_click_menu = ["", [_t("config_gui.show_decrypted")]]
window = sg.Window(
"Configuration",
config_layout(),
#size=(800, 650),
auto_size_text=True,
auto_size_buttons=False,
no_titlebar=False,
grab_anywhere=True,
keep_on_top=False,
alpha_channel=1.0,
default_button_element_size=(16, 1),
right_click_menu=right_click_menu,
finalize=True,
)
backup_paths_tree = sg.TreeData()
tags_tree = sg.TreeData()
exclude_patterns_tree = sg.TreeData()
exclude_files_tree = sg.TreeData()
pre_exec_commands_tree = sg.TreeData()
post_exec_commands_tree = sg.TreeData()
env_variables_tree = sg.TreeData()
encrypted_env_variables_tree = sg.TreeData()
# Update gui with first default object (repo or group)
update_object_gui(get_objects()[0], unencrypted=False)
update_global_gui(full_config, unencrypted=False)
while True:
event, values = window.read()
# Get object type for various delete operations
object_type, _ = get_object_from_combo(values["-OBJECT-SELECT-"])
if event in (sg.WIN_CLOSED, sg.WIN_X_EVENT, "--CANCEL--"):
break
if event == "-OBJECT-SELECT-":
update_config_dict(full_config, values)
update_object_gui(values["-OBJECT-SELECT-"], unencrypted=False)
update_global_gui(full_config, unencrypted=False)
if event == "-OBJECT-DELETE-":
full_config = delete_object(full_config, values["-OBJECT-SELECT-"])
update_object_selector()
continue
if event == "-OBJECT-CREATE-":
full_config = create_object(full_config)
update_object_selector()
continue
if event == "--SET-PERMISSIONS--":
object_type, object_name = get_object_from_combo(values["-OBJECT-SELECT-"])
manager_password = configuration.get_manager_password(
full_config, object_name
)
if ask_manager_password(manager_password):
full_config = set_permissions(full_config, values["-OBJECT-SELECT-"])
continue
if event in ("--ADD-PATHS-FILE--", '--ADD-PATHS-FOLDER--'):
if event == "--ADD-PATHS-FILE--":
node = values["--ADD-PATHS-FILE--"]
if object_type == "group":
icon = INHERITED_FILE_ICON
else:
icon = FILE_ICON
elif event == '--ADD-PATHS-FOLDER--':
node = values['--ADD-PATHS-FOLDER--']
if object_type == "group":
icon = INHERITED_FOLDER_ICON
else:
icon = FOLDER_ICON
backup_paths_tree.insert('', node, node, node, icon=icon)
window['backup_opts.paths'].update(values=backup_paths_tree)
if event in (
"--ADD-TAG--",
"--ADD-EXCLUDE-PATTERN--",
"--ADD-PRE-EXEC-COMMAND--",
"--ADD-POST-EXEC-COMMAND--",
"--ADD-ENV-VARIABLE--",
"--ADD-ENCRYPTED-ENV-VARIABLE--",
"--REMOVE-BACKUP-PATHS",
"--REMOVE-TAG--",
"--REMOVE-EXCLUDE-PATTERN--",
"--REMOVE-EXCLUDE-FILE--",
"--REMOVE-PRE-EXEC-COMMAND--",
"--REMOVE-POST-EXEC-COMMAND--",
"--REMOVE-ENV-VARIABLE--",
"--REMOVE-ENCRYPTED-ENV-VARIABLE--"
):
if "PATHS" in event:
option_key = "backup_opts.paths"
tree = backup_paths_tree
elif "TAG" in event:
popup_text = _t("config_gui.enter_tag")
tree = tags_tree
option_key = "backup_opts.tags"
elif "EXCLUDE_PATTERN" in event:
popup_text = _t("config_gui.enter_pattern")
tree = exclude_patterns_tree
option_key = "backup_opts.exclude_patterns"
elif "EXCLUDE-FILE" in event:
tree = exclude_files_tree
option_key = "backup_opts.exclude_files"
elif "PRE-EXEC-COMMANDS" in event:
popup_text = _t("config_gui.enter_command")
tree = pre_exec_commands_tree
option_key = "backup_opts.pre_exec_commands"
elif "POST-EXEC-COMMANDS" in event:
popup_text = _t("config_gui.enter_command")
tree = post_exec_commands_tree
option_key = "backup_opts.post_exec_commands"
elif "ENCRYPTED-ENV-VARIABLE" in event:
tree = encrypted_env_variables_tree
option_key = "env.encrypted_env_variables"
elif "ENV-VARIABLE" in event:
tree = env_variables_tree
option_key = "env.env_variables"
if event.startswith("--ADD-"):
if object_type == "group":
icon = INHERITED_TREE_ICON
else:
icon = TREE_ICON
if "ENV-VARIABLE" in event:
var_name = sg.PopupGetText(_t("config_gui.enter_var_name"))
var_value = sg.PopupGetText(_t("config_gui.enter_var_value"))
if var_name and var_value:
tree.insert('', var_name, var_name, var_value, icon=icon)
else:
node = sg.PopupGetText(popup_text)
if node:
tree.insert('', node, node, node, icon=icon)
if event.startswith("--REMOVE-"):
for key in values[option_key]:
if object_type != "group" and tree.tree_dict[key].icon in (INHERITED_TREE_ICON, INHERITED_FILE_ICON, INHERITED_FOLDER_ICON):
sg.PopupError(_t("config_gui.cannot_remove_group_inherited_settings"))
continue
tree.delete(key)
window[option_key].Update(values=tree)
if event == "--ACCEPT--":
if (
not values["repo_opts.repo_password"]
and not values["repo_opts.repo_password_command"]
):
sg.PopupError(_t("config_gui.repo_password_cannot_be_empty"))
continue
full_config = update_config_dict(full_config, values)
result = configuration.save_config(config_file, full_config)
if result:
sg.Popup(_t("config_gui.configuration_saved"), keep_on_top=True)
logger.info("Configuration saved successfully.")
break
sg.PopupError(_t("config_gui.cannot_save_configuration"), keep_on_top=True)
logger.info("Could not save configuration")
if event == _t("config_gui.show_decrypted"):
object_type, object_name = get_object_from_combo(values["-OBJECT-SELECT-"])
manager_password = configuration.get_manager_password(
full_config, object_name
)
if ask_manager_password(manager_password):
update_object_gui(values["-OBJECT-SELECT-"], unencrypted=True)
update_global_gui(full_config, unencrypted=True)
if event == "create_task":
if os.name == "nt":
result = create_scheduled_task(
CURRENT_EXECUTABLE, values["scheduled_task_interval"]
)
if result:
sg.Popup(_t("config_gui.scheduled_task_creation_success"))
else:
sg.PopupError(_t("config_gui.scheduled_task_creation_failure"))
else:
sg.PopupError(_t("config_gui.scheduled_task_creation_failure"))
window.close()
return full_config