mirror of
https://github.com/netinvent/npbackup.git
synced 2025-10-25 04:46:58 +08:00
784 lines
No EOL
29 KiB
Python
784 lines
No EOL
29 KiB
Python
#! /usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# This file is part of npbackup
|
|
|
|
__intname__ = "npbackup.gui.main"
|
|
__author__ = "Orsiris de Jong"
|
|
__copyright__ = "Copyright (C) 2022-2023 NetInvent"
|
|
__license__ = "GPL-3.0-only"
|
|
__build__ = "2023121001"
|
|
|
|
|
|
from typing import List, Optional, Tuple
|
|
import sys
|
|
import os
|
|
from pathlib import Path
|
|
from logging import getLogger
|
|
from datetime import datetime
|
|
import dateutil
|
|
import queue
|
|
from time import sleep
|
|
from ofunctions.threading import threaded, Future
|
|
from threading import Thread
|
|
from ofunctions.misc import BytesConverter
|
|
import PySimpleGUI as sg
|
|
import _tkinter
|
|
import npbackup.configuration
|
|
from npbackup.customization import (
|
|
OEM_STRING,
|
|
OEM_LOGO,
|
|
GUI_LOADER_COLOR,
|
|
GUI_LOADER_TEXT_COLOR,
|
|
GUI_STATE_OK_BUTTON,
|
|
GUI_STATE_OLD_BUTTON,
|
|
GUI_STATE_UNKNOWN_BUTTON,
|
|
LOADER_ANIMATION,
|
|
FOLDER_ICON,
|
|
FILE_ICON,
|
|
LICENSE_TEXT,
|
|
LICENSE_FILE,
|
|
)
|
|
from npbackup.gui.config import config_gui
|
|
from npbackup.gui.operations import operations_gui
|
|
from npbackup.gui.helpers import get_anon_repo_uri
|
|
from npbackup.core.runner import NPBackupRunner
|
|
from npbackup.core.i18n_helper import _t
|
|
from npbackup.core.upgrade_runner import run_upgrade, check_new_version
|
|
from npbackup.path_helper import CURRENT_DIR
|
|
from npbackup.interface_entrypoint import entrypoint
|
|
from npbackup.__version__ import version_string
|
|
from npbackup.gui.config import config_gui
|
|
from npbackup.gui.operations import operations_gui
|
|
from npbackup.customization import (
|
|
PYSIMPLEGUI_THEME,
|
|
OEM_ICON,
|
|
)
|
|
|
|
sg.theme(PYSIMPLEGUI_THEME)
|
|
sg.SetOptions(icon=OEM_ICON)
|
|
|
|
logger = getLogger()
|
|
|
|
# Let's use mutable to get a cheap way of transfering data from thread to main program
|
|
# There are no possible race conditions since we don't modifiy the data from anywhere outside the thread
|
|
THREAD_SHARED_DICT = {}
|
|
|
|
|
|
def _about_gui(version_string: str, repo_config: dict) -> None:
|
|
license_content = LICENSE_TEXT
|
|
|
|
result = check_new_version(repo_config)
|
|
if result:
|
|
new_version = [
|
|
sg.Button(
|
|
_t("config_gui.auto_upgrade_launch"),
|
|
key="autoupgrade",
|
|
size=(12, 2),
|
|
)
|
|
]
|
|
elif result is False:
|
|
new_version = [sg.Text(_t("generic.is_uptodate"))]
|
|
elif result is None:
|
|
new_version = [sg.Text(_t("config_gui.auto_upgrade_disabled"))]
|
|
try:
|
|
with open(LICENSE_FILE, "r", encoding="utf-8") as file_handle:
|
|
license_content = file_handle.read()
|
|
except OSError:
|
|
logger.info("Could not read license file.")
|
|
|
|
layout = [
|
|
[sg.Text(version_string)],
|
|
new_version,
|
|
[sg.Text("License: GNU GPLv3")],
|
|
[sg.Multiline(license_content, size=(65, 20), disabled=True)],
|
|
[sg.Button(_t("generic.accept"), key="exit")],
|
|
]
|
|
|
|
window = sg.Window(
|
|
_t("generic.about"),
|
|
layout,
|
|
keep_on_top=True,
|
|
element_justification="C",
|
|
finalize=True,
|
|
)
|
|
|
|
while True:
|
|
event, _ = window.read()
|
|
if event in [sg.WIN_CLOSED, "exit"]:
|
|
break
|
|
elif event == "autoupgrade":
|
|
result = sg.PopupOKCancel(
|
|
_t("config_gui.auto_ugprade_will_quit"), keep_on_top=True
|
|
)
|
|
if result == "OK":
|
|
logger.info("Running GUI initiated upgrade")
|
|
sub_result = run_upgrade(repo_config)
|
|
if sub_result:
|
|
sys.exit(0)
|
|
else:
|
|
sg.Popup(_t("config_gui.auto_upgrade_failed"), keep_on_top=True)
|
|
window.close()
|
|
|
|
|
|
@threaded
|
|
def _get_gui_data(repo_config: dict) -> Future:
|
|
runner = NPBackupRunner(repo_config=repo_config)
|
|
snapshots = runner.list()
|
|
current_state, backup_tz = runner.check_recent_backups()
|
|
snapshot_list = []
|
|
if snapshots:
|
|
snapshots.reverse() # Let's show newer snapshots first
|
|
for snapshot in snapshots:
|
|
snapshot_date = dateutil.parser.parse(snapshot["time"]).strftime(
|
|
"%Y-%m-%d %H:%M:%S"
|
|
)
|
|
snapshot_username = snapshot["username"]
|
|
snapshot_hostname = snapshot["hostname"]
|
|
snapshot_id = snapshot["short_id"]
|
|
try:
|
|
snapshot_tags = " [TAGS: {}]".format(snapshot["tags"])
|
|
except KeyError:
|
|
snapshot_tags = ""
|
|
snapshot_list.append(
|
|
[
|
|
snapshot_id,
|
|
snapshot_date,
|
|
snapshot_hostname,
|
|
snapshot_username,
|
|
snapshot_tags,
|
|
]
|
|
)
|
|
|
|
return current_state, backup_tz, snapshot_list
|
|
|
|
|
|
def get_gui_data(repo_config: dict) -> Tuple[bool, List[str]]:
|
|
try:
|
|
if (
|
|
not repo_config.g("repo_uri")
|
|
and (not repo_config.g("repo_opts.repo_password")
|
|
and not repo_config.g("repo_opts.repo_password_command"))
|
|
):
|
|
sg.Popup(_t("main_gui.repository_not_configured"))
|
|
return None, None
|
|
except KeyError:
|
|
sg.Popup(_t("main_gui.repository_not_configured"))
|
|
return None, None
|
|
try:
|
|
runner = NPBackupRunner(repo_config=repo_config)
|
|
except ValueError:
|
|
sg.Popup(_t("config_gui.no_runner"))
|
|
return None, None
|
|
if not runner.is_ready:
|
|
sg.Popup(_t("config_gui.runner_not_configured"))
|
|
return None, None
|
|
if not runner.has_binary:
|
|
sg.Popup(_t("config_gui.no_binary"))
|
|
return None, None
|
|
# We get a thread result, hence pylint will complain the thread isn't a tuple
|
|
# pylint: disable=E1101 (no-member)
|
|
thread = _get_gui_data(repo_config)
|
|
while not thread.done() and not thread.cancelled():
|
|
sg.PopupAnimated(
|
|
LOADER_ANIMATION,
|
|
message=_t("main_gui.loading_data_from_repo"),
|
|
time_between_frames=50,
|
|
background_color=GUI_LOADER_COLOR,
|
|
text_color=GUI_LOADER_TEXT_COLOR,
|
|
)
|
|
sg.PopupAnimated(None)
|
|
return thread.result()
|
|
|
|
|
|
def _gui_update_state(
|
|
window, current_state: bool, backup_tz: Optional[datetime], snapshot_list: List[str]
|
|
) -> None:
|
|
if current_state:
|
|
window["state-button"].Update(
|
|
"{}: {}".format(_t("generic.up_to_date"), backup_tz),
|
|
button_color=GUI_STATE_OK_BUTTON,
|
|
)
|
|
elif current_state is False and backup_tz == datetime(1, 1, 1, 0, 0):
|
|
window["state-button"].Update(
|
|
_t("generic.no_snapshots"), button_color=GUI_STATE_OLD_BUTTON
|
|
)
|
|
elif current_state is False:
|
|
window["state-button"].Update(
|
|
"{}: {}".format(_t("generic.too_old"), backup_tz),
|
|
button_color=GUI_STATE_OLD_BUTTON,
|
|
)
|
|
elif current_state is None:
|
|
window["state-button"].Update(
|
|
_t("generic.not_connected_yet"), button_color=GUI_STATE_UNKNOWN_BUTTON
|
|
)
|
|
|
|
window["snapshot-list"].Update(snapshot_list)
|
|
|
|
|
|
@threaded
|
|
def _make_treedata_from_json(ls_result: List[dict]) -> sg.TreeData:
|
|
"""
|
|
Treelist data construction from json input that looks like
|
|
|
|
[
|
|
{"time": "2023-01-03T00:16:13.6256884+01:00", "parent": "40e16692030951e0224844ea160642a57786a765152eae10940293888ee1744a", "tree": "3f14a67b4d7cfe3974a2161a24beedfbf62ad289387207eda1bbb575533dbd33", "paths": ["C:\\GIT\\npbackup"], "hostname": "UNIMATRIX0", "username": "UNIMATRIX0\\Orsiris", "id": "a2103ca811e8b081565b162cca69ab5ac8974e43e690025236e759bf0d85afec", "short_id": "a2103ca8", "struct_type": "snapshot"}
|
|
|
|
{"name": "Lib", "type": "dir", "path": "/C/GIT/npbackup/.venv/Lib", "uid": 0, "gid": 0, "mode": 2147484159, "permissions": "drwxrwxrwx", "mtime": "2022-12-28T19:58:51.85719+01:00", "atime": "2022-12-28T19:58:51.85719+01:00", "ctime": "2022-12-28T19:58:51.85719+01:00", "struct_type": "node"}
|
|
{'name': 'xpTheme.tcl', 'type': 'file', 'path': '/C/GIT/npbackup/npbackup.dist/tk/ttk/xpTheme.tcl', 'uid': 0, 'gid': 0, 'size': 2103, 'mode': 438, 'permissions': '-rw-rw-rw-', 'mtime': '2022-09-05T14:18:52+02:00', 'atime': '2022-09-05T14:18:52+02:00', 'ctime': '2022-09-05T14:18:52+02:00', 'struct_type': 'node'}
|
|
{'name': 'unsupported.tcl', 'type': 'file', 'path': '/C/GIT/npbackup/npbackup.dist/tk/unsupported.tcl', 'uid': 0, 'gid': 0, 'size': 10521, 'mode': 438, 'permissions': '-rw-rw-rw-', 'mtime': '2022-09-05T14:18:52+02:00', 'atime': '2022-09-05T14:18:52+02:00', 'ctime': '2022-09-05T14:18:52+02:00', 'struct_type': 'node'}
|
|
{'name': 'xmfbox.tcl', 'type': 'file', 'path': '/C/GIT/npbackup/npbackup.dist/tk/xmfbox.tcl', 'uid': 0, 'gid': 0, 'size': 27064, 'mode': 438, 'permissions': '-rw-rw-rw-', 'mtime': '2022-09-05T14:18:52+02:00', 'atime': '2022-09-05T14:18:52+02:00', 'ctime': '2022-09-05T14:18:52+02:00', 'struct_type': 'node'}
|
|
]
|
|
"""
|
|
treedata = sg.TreeData()
|
|
count = 0
|
|
# First entry of list of list should be the snapshot description and can be discarded
|
|
# Since we use an iter now, first result was discarded by ls_window function already
|
|
# ls_result.pop(0)
|
|
for entry in ls_result:
|
|
# Make sure we drop the prefix '/' so sg.TreeData does not get an empty root
|
|
entry["path"] = entry["path"].lstrip("/")
|
|
if os.name == "nt":
|
|
# On windows, we need to make sure tree keys don't get duplicate because of lower/uppercase
|
|
# Shown filenames aren't affected by this
|
|
entry["path"] = entry["path"].lower()
|
|
parent = os.path.dirname(entry["path"])
|
|
|
|
# Make sure we normalize mtime, and remove microseconds
|
|
# dateutil.parser.parse is *really* cpu hungry, let's replace it with a dumb alternative
|
|
# mtime = dateutil.parser.parse(entry["mtime"]).strftime("%Y-%m-%d %H:%M:%S")
|
|
mtime = str(entry["mtime"])[0:19]
|
|
if entry["type"] == "dir" and entry["path"] not in treedata.tree_dict:
|
|
treedata.Insert(
|
|
parent=parent,
|
|
key=entry["path"],
|
|
text=entry["name"],
|
|
values=["", mtime],
|
|
icon=FOLDER_ICON,
|
|
)
|
|
elif entry["type"] == "file":
|
|
size = BytesConverter(entry["size"]).human
|
|
treedata.Insert(
|
|
parent=parent,
|
|
key=entry["path"],
|
|
text=entry["name"],
|
|
values=[size, mtime],
|
|
icon=FILE_ICON,
|
|
)
|
|
# Since the thread is heavily CPU bound, let's add a minimal
|
|
# arbitrary sleep time to let GUI update
|
|
# In a 130k entry scenario, this added less than a second on a 25 second run
|
|
count += 1
|
|
if not count % 1000:
|
|
sleep(0.0001)
|
|
return treedata
|
|
|
|
|
|
@threaded
|
|
def _forget_snapshot(repo_config: dict, snapshot_id: str) -> Future:
|
|
runner = NPBackupRunner(repo_config=repo_config)
|
|
result = runner.forget(snapshot=snapshot_id)
|
|
return result
|
|
|
|
|
|
@threaded
|
|
def _ls_window(repo_config: dict, snapshot_id: str) -> Future:
|
|
runner = NPBackupRunner(repo_config=repo_config)
|
|
result = runner.ls(snapshot=snapshot_id)
|
|
if not result:
|
|
return result, None
|
|
|
|
try:
|
|
# Since ls returns an iter now, we need to use next
|
|
snapshot_id = next(result)
|
|
# Exception that happens when restic cannot successfully get snapshot content
|
|
except StopIteration:
|
|
return None, None
|
|
try:
|
|
snap_date = dateutil.parser.parse(snapshot_id["time"])
|
|
except (KeyError, IndexError):
|
|
snap_date = "[inconnu]"
|
|
try:
|
|
short_id = snapshot_id["short_id"]
|
|
except (KeyError, IndexError):
|
|
short_id = "[inconnu]"
|
|
try:
|
|
username = snapshot_id["username"]
|
|
except (KeyError, IndexError):
|
|
username = "[inconnu]"
|
|
try:
|
|
hostname = snapshot_id["hostname"]
|
|
except (KeyError, IndexError):
|
|
hostname = "[inconnu]"
|
|
|
|
backup_content = " {} {} {} {}@{} {} {}".format(
|
|
_t("main_gui.backup_content_from"),
|
|
snap_date,
|
|
_t("main_gui.run_as"),
|
|
username,
|
|
hostname,
|
|
_t("main_gui.identified_by"),
|
|
short_id,
|
|
)
|
|
return backup_content, result
|
|
|
|
|
|
def forget_snapshot(config: dict, snapshot_ids: List[str]) -> bool:
|
|
batch_result = True
|
|
for snapshot_id in snapshot_ids:
|
|
# We get a thread result, hence pylint will complain the thread isn't a tuple
|
|
# pylint: disable=E1101 (no-member)
|
|
thread = _forget_snapshot(config, snapshot_id)
|
|
|
|
while not thread.done() and not thread.cancelled():
|
|
sg.PopupAnimated(
|
|
LOADER_ANIMATION,
|
|
message="{} {}. {}".format(
|
|
_t("generic.forgetting"),
|
|
snapshot_id,
|
|
_t("main_gui.this_will_take_a_while"),
|
|
),
|
|
time_between_frames=50,
|
|
background_color=GUI_LOADER_COLOR,
|
|
text_color=GUI_LOADER_TEXT_COLOR,
|
|
)
|
|
sg.PopupAnimated(None)
|
|
result = thread.result()
|
|
if not result:
|
|
batch_result = result
|
|
if not batch_result:
|
|
sg.PopupError(_t("main_gui.forget_failed"), keep_on_top=True)
|
|
return False
|
|
else:
|
|
sg.Popup(
|
|
"{} {} {}".format(
|
|
snapshot_ids, _t("generic.forgotten"), _t("generic.successfully")
|
|
)
|
|
)
|
|
|
|
|
|
def ls_window(config: dict, snapshot_id: str) -> bool:
|
|
# We get a thread result, hence pylint will complain the thread isn't a tuple
|
|
# pylint: disable=E1101 (no-member)
|
|
thread = _ls_window(config, snapshot_id)
|
|
|
|
while not thread.done() and not thread.cancelled():
|
|
sg.PopupAnimated(
|
|
LOADER_ANIMATION,
|
|
message="{}. {}".format(
|
|
_t("main_gui.loading_data_from_repo"),
|
|
_t("main_gui.this_will_take_a_while"),
|
|
),
|
|
time_between_frames=50,
|
|
background_color=GUI_LOADER_COLOR,
|
|
text_color=GUI_LOADER_TEXT_COLOR,
|
|
)
|
|
sg.PopupAnimated(None)
|
|
backup_content, ls_result = thread.result()
|
|
if not backup_content or not ls_result:
|
|
sg.PopupError(_t("main_gui.cannot_get_content"), keep_on_top=True)
|
|
return False
|
|
|
|
# The following thread is cpu intensive, so the GUI will update sluggerish
|
|
# In the thread, we added a sleep argument every 1000 iters so we get to update
|
|
# the GUI. Earlier fix was to preload animation
|
|
|
|
# We get a thread result, hence pylint will complain the thread isn't a tuple
|
|
# pylint: disable=E1101 (no-member)
|
|
thread = _make_treedata_from_json(ls_result)
|
|
|
|
while not thread.done() and not thread.cancelled():
|
|
sg.PopupAnimated(
|
|
LOADER_ANIMATION,
|
|
message="{}...".format(_t("main_gui.creating_tree")),
|
|
time_between_frames=50,
|
|
background_color=GUI_LOADER_COLOR,
|
|
text_color=GUI_LOADER_TEXT_COLOR,
|
|
)
|
|
sg.PopupAnimated(None)
|
|
treedata = thread.result()
|
|
|
|
left_col = [
|
|
[sg.Text(backup_content)],
|
|
[
|
|
sg.Tree(
|
|
data=treedata,
|
|
headings=[_t("generic.size"), _t("generic.modification_date")],
|
|
auto_size_columns=True,
|
|
select_mode=sg.TABLE_SELECT_MODE_EXTENDED,
|
|
num_rows=40,
|
|
col0_heading=_t("generic.path"),
|
|
col0_width=80,
|
|
key="-TREE-",
|
|
show_expanded=False,
|
|
enable_events=False,
|
|
expand_x=True,
|
|
expand_y=True,
|
|
vertical_scroll_only=False,
|
|
),
|
|
],
|
|
[
|
|
sg.Button(_t("main_gui.restore_to"), key="restore_to"),
|
|
sg.Button(_t("generic.quit"), key="quit"),
|
|
],
|
|
]
|
|
layout = [[sg.Column(left_col, element_justification="C")]]
|
|
window = sg.Window(
|
|
_t("generic.content"), layout=layout, grab_anywhere=True, keep_on_top=False
|
|
)
|
|
while True:
|
|
event, values = window.read()
|
|
if event in (sg.WIN_CLOSED, sg.WIN_CLOSE_ATTEMPTED_EVENT, "quit"):
|
|
break
|
|
if event == "restore_to":
|
|
if not values["-TREE-"]:
|
|
sg.PopupError(_t("main_gui.select_folder"))
|
|
continue
|
|
restore_window(config, snapshot_id, values["-TREE-"])
|
|
|
|
# Closing a big sg.TreeData is really slow
|
|
# This is a little trichery lesson
|
|
# Still we should open a case at PySimpleGUI to know why closing a sg.TreeData window is painfully slow # TODO
|
|
window.hide()
|
|
Thread(target=window.close, args=())
|
|
|
|
return True
|
|
|
|
|
|
@threaded
|
|
def _restore_window(
|
|
repo_config: dict, snapshot: str, target: str, restore_includes: Optional[List]
|
|
) -> Future:
|
|
runner = NPBackupRunner(repo_config=repo_config)
|
|
runner.verbose = True
|
|
result = runner.restore(snapshot, target, restore_includes)
|
|
THREAD_SHARED_DICT["exec_time"] = runner.exec_time
|
|
return result
|
|
|
|
|
|
def restore_window(
|
|
repo_config: dict, snapshot_id: str, restore_include: List[str]
|
|
) -> None:
|
|
left_col = [
|
|
[
|
|
sg.Text(_t("main_gui.destination_folder")),
|
|
sg.In(size=(25, 1), enable_events=True, key="-RESTORE-FOLDER-"),
|
|
sg.FolderBrowse(),
|
|
],
|
|
# Do not show which folder gets to get restored since we already make that selection
|
|
# [sg.Text(_t("main_gui.only_include")), sg.Text(includes, size=(25, 1))],
|
|
[
|
|
sg.Button(_t("main_gui.restore"), key="restore"),
|
|
sg.Button(_t("generic.cancel"), key="cancel"),
|
|
],
|
|
]
|
|
|
|
layout = [[sg.Column(left_col, element_justification="C")]]
|
|
window = sg.Window(
|
|
_t("main_gui.restoration"), layout=layout, grab_anywhere=True, keep_on_top=False
|
|
)
|
|
while True:
|
|
event, values = window.read()
|
|
if event in (sg.WIN_CLOSED, sg.WIN_CLOSE_ATTEMPTED_EVENT, "cancel"):
|
|
break
|
|
if event == "restore":
|
|
# We get a thread result, hence pylint will complain the thread isn't a tuple
|
|
# pylint: disable=E1101 (no-member)
|
|
thread = _restore_window(
|
|
repo_config=repo_config,
|
|
snapshot=snapshot_id,
|
|
target=values["-RESTORE-FOLDER-"],
|
|
restore_includes=restore_include,
|
|
)
|
|
while not thread.done() and not thread.cancelled():
|
|
sg.PopupAnimated(
|
|
LOADER_ANIMATION,
|
|
message="{}...".format(_t("main_gui.restore_in_progress")),
|
|
time_between_frames=50,
|
|
background_color=GUI_LOADER_COLOR,
|
|
text_color=GUI_LOADER_TEXT_COLOR,
|
|
)
|
|
sg.PopupAnimated(None)
|
|
|
|
result = thread.result()
|
|
try:
|
|
exec_time = THREAD_SHARED_DICT["exec_time"]
|
|
except KeyError:
|
|
exec_time = "N/A"
|
|
if result:
|
|
sg.Popup(
|
|
_t("main_gui.restore_done", seconds=exec_time), keep_on_top=True
|
|
)
|
|
else:
|
|
sg.PopupError(
|
|
_t("main_gui.restore_failed", seconds=exec_time), keep_on_top=True
|
|
)
|
|
break
|
|
window.close()
|
|
|
|
|
|
@threaded
|
|
def _gui_backup(repo_config, stdout) -> Future:
|
|
runner = NPBackupRunner(repo_config=repo_config)
|
|
runner.verbose = (
|
|
True # We must use verbose so we get progress output from ResticRunner
|
|
)
|
|
runner.stdout = stdout
|
|
result = runner.backup(
|
|
force=True,
|
|
) # Since we run manually, force backup regardless of recent backup state
|
|
THREAD_SHARED_DICT["exec_time"] = runner.exec_time
|
|
return result
|
|
|
|
|
|
def select_config_file():
|
|
"""
|
|
Option to select a configuration file
|
|
"""
|
|
layout = [
|
|
[sg.Text(_t("main_gui.select_config_file")), sg.Input(key="-config_file-"), sg.FileBrowse(_t("generic.select_file"))],
|
|
[sg.Button(_t("generic.cancel"), key="-CANCEL-"), sg.Button(_t("generic.accept"), key="-ACCEPT-")]
|
|
]
|
|
window = sg.Window("Configuration File", layout=layout)
|
|
while True:
|
|
event, values = window.read()
|
|
if event in [sg.WIN_X_EVENT, sg.WIN_CLOSED, '-CANCEL-']:
|
|
break
|
|
if event == '-ACCEPT-':
|
|
config_file = Path(values["-config_file-"])
|
|
if not config_file.exists():
|
|
sg.PopupError(_t("generic.file_does_not_exist"))
|
|
continue
|
|
config = npbackup.configuration._load_config_file(config_file)
|
|
if not config:
|
|
sg.PopupError(_t("generic.bad_file"))
|
|
continue
|
|
return config_file
|
|
|
|
|
|
def _main_gui():
|
|
config_file = Path(f'{CURRENT_DIR}/npbackup.conf')
|
|
if not config_file.exists():
|
|
while True:
|
|
config_file = select_config_file()
|
|
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)
|
|
# TODO add a repo selector
|
|
repo_config, inherit_config = 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'))
|
|
|
|
right_click_menu = ["", [_t("generic.destination")]]
|
|
headings = [
|
|
"ID ",
|
|
"Date ",
|
|
"Hostname ",
|
|
"User ",
|
|
"Tags ",
|
|
]
|
|
|
|
layout = [
|
|
[
|
|
sg.Column(
|
|
[
|
|
[
|
|
sg.Column(
|
|
[[sg.Image(data=OEM_LOGO)]], vertical_alignment="top"
|
|
),
|
|
sg.Column(
|
|
[
|
|
[sg.Text(OEM_STRING, font="Arial 14")],
|
|
[sg.Text("{}: ".format(_t("main_gui.backup_state")))],
|
|
[
|
|
sg.Button(
|
|
_t("generic.unknown"),
|
|
key="state-button",
|
|
button_color=("white", "grey"),
|
|
)
|
|
],
|
|
],
|
|
justification="C",
|
|
element_justification="C",
|
|
vertical_alignment="top",
|
|
),
|
|
],
|
|
[
|
|
sg.Text(_t("main_gui.backup_list_to")),
|
|
sg.Combo(repo_list, key="-active_repo-", default_value=repo_list[0], enable_events=True),
|
|
sg.Text(f"Type {backend_type}", key="-backend_type-")
|
|
],
|
|
[
|
|
sg.Table(
|
|
values=[[]],
|
|
headings=headings,
|
|
auto_size_columns=True,
|
|
justification="left",
|
|
key="snapshot-list",
|
|
select_mode="extended",
|
|
)
|
|
],
|
|
[
|
|
sg.Button(_t("main_gui.launch_backup"), key="launch-backup"),
|
|
sg.Button(_t("main_gui.see_content"), key="see-content"),
|
|
sg.Button(_t("generic.forget"), key="forget"),
|
|
sg.Button(_t("main_gui.operations"), key="operations"),
|
|
sg.Button(_t("generic.configure"), key="configure"),
|
|
sg.Button(_t("generic.about"), key="about"),
|
|
sg.Button(_t("generic.quit"), key="-EXIT-"),
|
|
],
|
|
],
|
|
element_justification="C",
|
|
)
|
|
]
|
|
]
|
|
|
|
window = sg.Window(
|
|
"npbackup",
|
|
layout,
|
|
default_element_size=(12, 1),
|
|
text_justification="r",
|
|
auto_size_text=True,
|
|
auto_size_buttons=True,
|
|
no_titlebar=False,
|
|
grab_anywhere=False,
|
|
keep_on_top=False,
|
|
alpha_channel=0.9,
|
|
default_button_element_size=(12, 1),
|
|
right_click_menu=right_click_menu,
|
|
finalize=True,
|
|
)
|
|
|
|
# Auto reisze table to window size
|
|
window["snapshot-list"].expand(True, True)
|
|
|
|
window.read(timeout=1)
|
|
current_state, backup_tz, snapshot_list = get_gui_data(repo_config)
|
|
_gui_update_state(window, current_state, backup_tz, snapshot_list)
|
|
while True:
|
|
event, values = window.read(timeout=60000)
|
|
|
|
if event in [sg.WIN_X_EVENT, sg.WIN_CLOSED, '-EXIT-']:
|
|
break
|
|
if event == "-active_repo-":
|
|
active_repo = values['-active_repo-']
|
|
if full_config.g(f"repos.{active_repo}"):
|
|
repo_config = npbackup.configuration.get_repo_config(full_config, active_repo)
|
|
current_state, backup_tz, snapshot_list = get_gui_data(repo_config)
|
|
else:
|
|
sg.PopupError("Repo not existent in config")
|
|
continue
|
|
if event == "launch-backup":
|
|
progress_windows_layout = [
|
|
[
|
|
sg.Multiline(
|
|
size=(80, 10), key="progress", expand_x=True, expand_y=True
|
|
)
|
|
]
|
|
]
|
|
progress_window = sg.Window(
|
|
_t("main_gui.backup_activity"),
|
|
layout=progress_windows_layout,
|
|
finalize=True,
|
|
)
|
|
# We need to read that window at least once fopr it to exist
|
|
progress_window.read(timeout=1)
|
|
stdout = queue.Queue()
|
|
|
|
# let's use a mutable so the backup thread can modify it
|
|
# We get a thread result, hence pylint will complain the thread isn't a tuple
|
|
# pylint: disable=E1101 (no-member)
|
|
thread = _gui_backup(repo_config=repo_config, stdout=stdout)
|
|
while not thread.done() and not thread.cancelled():
|
|
try:
|
|
stdout_line = stdout.get(timeout=0.01)
|
|
except queue.Empty:
|
|
pass
|
|
else:
|
|
if stdout_line:
|
|
progress_window["progress"].Update(stdout_line)
|
|
sg.PopupAnimated(
|
|
LOADER_ANIMATION,
|
|
message="{}...".format(_t("main_gui.backup_in_progress")),
|
|
time_between_frames=50,
|
|
background_color=GUI_LOADER_COLOR,
|
|
text_color=GUI_LOADER_TEXT_COLOR,
|
|
)
|
|
sg.PopupAnimated(None)
|
|
result = thread.result()
|
|
try:
|
|
exec_time = THREAD_SHARED_DICT["exec_time"]
|
|
except KeyError:
|
|
exec_time = "N/A"
|
|
current_state, backup_tz, snapshot_list = get_gui_data(repo_config)
|
|
_gui_update_state(window, current_state, backup_tz, snapshot_list)
|
|
if not result:
|
|
sg.PopupError(
|
|
_t("main_gui.backup_failed", seconds=exec_time), keep_on_top=True
|
|
)
|
|
else:
|
|
sg.Popup(
|
|
_t("main_gui.backup_done", seconds=exec_time), keep_on_top=True
|
|
)
|
|
progress_window.close()
|
|
continue
|
|
if event == "see-content":
|
|
if not values["snapshot-list"]:
|
|
sg.Popup(_t("main_gui.select_backup"), keep_on_top=True)
|
|
continue
|
|
print(values["snapshot-list"])
|
|
if len(values["snapshot-list"]) > 1:
|
|
sg.Popup(_t("main_gui.select_only_one_snapshot"))
|
|
continue
|
|
snapshot_to_see = snapshot_list[values["snapshot-list"][0]][0]
|
|
ls_window(repo_config, snapshot_to_see)
|
|
if event == "forget":
|
|
if not values["snapshot-list"]:
|
|
sg.Popup(_t("main_gui.select_backup"), keep_on_top=True)
|
|
continue
|
|
snapshots_to_forget = []
|
|
for row in values["snapshot-list"]:
|
|
snapshots_to_forget.append(snapshot_list[row][0])
|
|
forget_snapshot(repo_config, snapshots_to_forget)
|
|
# Make sure we trigger a GUI refresh after forgetting snapshots
|
|
event = "state-button"
|
|
if event == "operations":
|
|
full_config = operations_gui(full_config, config_file)
|
|
event = "state-button"
|
|
if event == "configure":
|
|
full_config = config_gui(full_config, config_file)
|
|
# Make sure we trigger a GUI refresh when configuration is changed
|
|
event = "state-button"
|
|
if event == _t("generic.destination"):
|
|
try:
|
|
if backend_type:
|
|
if backend_type in ["REST", "SFTP"]:
|
|
destination_string = repo_config.g("repo_uri").split(
|
|
"@"
|
|
)[-1]
|
|
else:
|
|
destination_string = repo_config.g("repo_uri")
|
|
sg.PopupNoFrame(destination_string)
|
|
except (TypeError, KeyError):
|
|
sg.PopupNoFrame(_t("main_gui.unknown_repo"))
|
|
if event == "about":
|
|
_about_gui(version_string, repo_config)
|
|
if event == "state-button":
|
|
current_state, backup_tz, snapshot_list = get_gui_data(repo_config)
|
|
_gui_update_state(window, current_state, backup_tz, snapshot_list)
|
|
if current_state is None:
|
|
sg.Popup(_t("main_gui.cannot_get_repo_status"))
|
|
|
|
|
|
def main_gui():
|
|
try:
|
|
_main_gui()
|
|
except _tkinter.TclError as exc:
|
|
logger.critical(f'Tkinter error: "{exc}". Is this a headless server ?')
|
|
sys.exit(250) |