mirror of https://github.com/borgbase/vorta
322 lines
12 KiB
Python
322 lines
12 KiB
Python
import os
|
|
from pathlib import PurePath
|
|
|
|
from PyQt6 import QtCore, uic
|
|
from PyQt6.QtCore import QMimeData, QUrl
|
|
from PyQt6.QtWidgets import QApplication, QLayout, QMenu, QMessageBox
|
|
|
|
from vorta.store.models import ArchiveModel, BackupProfileMixin, RepoModel
|
|
from vorta.utils import borg_compat, get_asset, get_private_keys, pretty_bytes
|
|
|
|
from .repo_add_dialog import AddRepoWindow, ExistingRepoWindow
|
|
from .ssh_dialog import SSHAddWindow
|
|
from .utils import get_colored_icon
|
|
|
|
uifile = get_asset('UI/repotab.ui')
|
|
RepoUI, RepoBase = uic.loadUiType(uifile)
|
|
|
|
|
|
class RepoTab(RepoBase, RepoUI, BackupProfileMixin):
|
|
repo_changed = QtCore.pyqtSignal()
|
|
repo_added = QtCore.pyqtSignal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setupUi(parent)
|
|
|
|
# Populate dropdowns
|
|
self.repoRemoveToolbutton.clicked.connect(self.repo_unlink_action)
|
|
self.copyURLbutton.clicked.connect(self.copy_URL_action)
|
|
|
|
# init repo add button
|
|
self.menuAddRepo = QMenu(self.bAddRepo)
|
|
|
|
self.menuAddRepo.addAction(self.tr("New Repository…"), self.new_repo)
|
|
self.menuAddRepo.addAction(self.tr("Existing Repository…"), self.add_existing_repo)
|
|
|
|
self.bAddRepo.setMenu(self.menuAddRepo)
|
|
|
|
# note: it is hard to describe these algorithms with attributes like low/medium/high
|
|
# compression or speed on a unified scale. this is not 1-dimensional and also depends
|
|
# on the input data. so we just tell what we know for sure.
|
|
# "auto" is used for some slower / older algorithms to avoid wasting a lot of time
|
|
# on uncompressible data.
|
|
self.repoCompression.addItem(self.tr('LZ4 (modern, default)'), 'lz4')
|
|
self.repoCompression.addItem(self.tr('Zstandard Level 3 (modern)'), 'zstd,3')
|
|
self.repoCompression.addItem(self.tr('Zstandard Level 8 (modern)'), 'zstd,8')
|
|
|
|
# zlib and lzma come from python stdlib and are there (and in borg) since long.
|
|
# but maybe not much reason to start with these nowadays, considering zstd supports
|
|
# a very wide range of compression levels and has great speed. if speed is more
|
|
# important than compression, lz4 is even a little better.
|
|
self.repoCompression.addItem(self.tr('ZLIB Level 6 (auto, legacy)'), 'auto,zlib,6')
|
|
self.repoCompression.addItem(self.tr('LZMA Level 6 (auto, legacy)'), 'auto,lzma,6')
|
|
self.repoCompression.addItem(self.tr('No Compression'), 'none')
|
|
self.repoCompression.currentIndexChanged.connect(self.compression_select_action)
|
|
|
|
self.toggle_available_compression()
|
|
self.repoCompression.currentIndexChanged.connect(self.compression_select_action)
|
|
|
|
self.init_ssh()
|
|
self.sshComboBox.currentIndexChanged.connect(self.ssh_select_action)
|
|
self.sshKeyToClipboardButton.clicked.connect(self.ssh_copy_to_clipboard_action)
|
|
self.bAddSSHKey.clicked.connect(self.create_ssh_key)
|
|
|
|
self.set_icons()
|
|
|
|
# Connect to palette change
|
|
QApplication.instance().paletteChanged.connect(lambda p: self.set_icons())
|
|
|
|
self.populate_from_profile() # needs init of ssh and compression items
|
|
|
|
def set_icons(self):
|
|
self.bAddSSHKey.setIcon(get_colored_icon("plus"))
|
|
self.bAddRepo.setIcon(get_colored_icon("plus"))
|
|
self.repoRemoveToolbutton.setIcon(get_colored_icon('unlink'))
|
|
self.sshKeyToClipboardButton.setIcon(get_colored_icon('copy'))
|
|
self.copyURLbutton.setIcon(get_colored_icon('copy'))
|
|
|
|
def set_repos(self):
|
|
self.repoSelector.clear()
|
|
self.repoSelector.addItem(self.tr('No repository selected'), None)
|
|
for repo in RepoModel.select():
|
|
self.repoSelector.addItem(repo.url, repo.id)
|
|
|
|
def populate_from_profile(self):
|
|
try:
|
|
self.repoSelector.currentIndexChanged.disconnect(self.repo_select_action)
|
|
except TypeError: # raised when signal is not connected
|
|
pass
|
|
|
|
# populate repositories
|
|
self.set_repos()
|
|
|
|
# load profile configuration
|
|
profile = self.profile()
|
|
if profile.repo:
|
|
self.repoSelector.setCurrentIndex(self.repoSelector.findData(profile.repo.id))
|
|
else:
|
|
self.repoSelector.setCurrentIndex(0)
|
|
|
|
self.repoCompression.setCurrentIndex(self.repoCompression.findData(profile.compression))
|
|
self.sshComboBox.setCurrentIndex(self.sshComboBox.findData(profile.ssh_key))
|
|
self.init_repo_stats()
|
|
|
|
self.repoSelector.currentIndexChanged.connect(self.repo_select_action)
|
|
|
|
def init_repo_stats(self):
|
|
"""Set the strings of the repo stats labels."""
|
|
# prepare translations
|
|
na = self.tr('N/A', "Not available.")
|
|
no_repo_selected = self.tr("Select a repository first.")
|
|
refresh = self.tr("Try refreshing the metadata of any archive.")
|
|
|
|
# set labels
|
|
repo: RepoModel = self.profile().repo
|
|
if repo is not None:
|
|
# remove *unset* item
|
|
self.repoSelector.removeItem(self.repoSelector.findData(None))
|
|
|
|
# Start with every element enabled, then disable SSH-related if relevant
|
|
for child in self.frameRepoSettings.children():
|
|
child.setEnabled(True)
|
|
# local repo doesn't use ssh
|
|
ssh_enabled = repo.is_remote_repo()
|
|
# self.bAddSSHKey.setEnabled(ssh_enabled)
|
|
# otherwise one cannot add a ssh key for adding a repo
|
|
self.sshComboBox.setEnabled(ssh_enabled)
|
|
self.sshKeyToClipboardButton.setEnabled(ssh_enabled)
|
|
|
|
# update stats
|
|
if repo.unique_csize is not None:
|
|
self.sizeCompressed.setText(pretty_bytes(repo.unique_csize))
|
|
self.sizeCompressed.setToolTip('')
|
|
else:
|
|
self.sizeCompressed.setText(na)
|
|
self.sizeCompressed.setToolTip(refresh)
|
|
|
|
if repo.unique_size is not None:
|
|
self.sizeDeduplicated.setText(pretty_bytes(repo.unique_size))
|
|
self.sizeDeduplicated.setToolTip('')
|
|
else:
|
|
self.sizeDeduplicated.setText(na)
|
|
self.sizeDeduplicated.setToolTip(refresh)
|
|
|
|
if repo.total_size is not None:
|
|
self.sizeOriginal.setText(pretty_bytes(repo.total_size))
|
|
self.sizeOriginal.setToolTip('')
|
|
else:
|
|
self.sizeOriginal.setText(na)
|
|
self.sizeOriginal.setToolTip(refresh)
|
|
|
|
self.repoEncryption.setText(str(repo.encryption))
|
|
else:
|
|
# Compression and SSH key are only valid entries for a repo
|
|
# Yet Add SSH key button must be enabled for bootstrapping
|
|
for child in self.frameRepoSettings.children():
|
|
if not isinstance(child, QLayout):
|
|
child.setEnabled(False)
|
|
self.bAddSSHKey.setEnabled(True)
|
|
|
|
# unset stats
|
|
self.sizeCompressed.setText(na)
|
|
self.sizeCompressed.setToolTip(no_repo_selected)
|
|
|
|
self.sizeDeduplicated.setText(na)
|
|
self.sizeDeduplicated.setToolTip(no_repo_selected)
|
|
|
|
self.sizeOriginal.setText(na)
|
|
self.sizeOriginal.setToolTip(no_repo_selected)
|
|
|
|
self.repoEncryption.setText(na)
|
|
self.repoEncryption.setToolTip(no_repo_selected)
|
|
|
|
self.repo_changed.emit()
|
|
|
|
def init_ssh(self):
|
|
keys = get_private_keys()
|
|
self.sshComboBox.clear()
|
|
self.sshComboBox.addItem(self.tr('Automatically choose SSH Key (default)'), None)
|
|
for key in keys:
|
|
self.sshComboBox.addItem(f'{key}', key)
|
|
|
|
def toggle_available_compression(self):
|
|
use_zstd = borg_compat.check('ZSTD')
|
|
for algo in ['zstd,3', 'zstd,8']:
|
|
ix = self.repoCompression.findData(algo)
|
|
self.repoCompression.model().item(ix).setEnabled(use_zstd)
|
|
|
|
def ssh_select_action(self, index):
|
|
profile = self.profile()
|
|
profile.ssh_key = self.sshComboBox.itemData(index)
|
|
profile.save()
|
|
|
|
def create_ssh_key(self):
|
|
"""Open a dialog to create an ssh key."""
|
|
ssh_add_window = SSHAddWindow()
|
|
self._window = ssh_add_window # For tests
|
|
ssh_add_window.setParent(self, QtCore.Qt.WindowType.Sheet)
|
|
ssh_add_window.accepted.connect(self.init_ssh)
|
|
# ssh_add_window.rejected.connect(lambda: self.sshComboBox.setCurrentIndex(0))
|
|
ssh_add_window.open()
|
|
|
|
def ssh_copy_to_clipboard_action(self):
|
|
msg = QMessageBox()
|
|
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
|
|
msg.setParent(self, QtCore.Qt.WindowType.Sheet)
|
|
|
|
index = self.sshComboBox.currentIndex()
|
|
if index > 0:
|
|
ssh_key_filename = self.sshComboBox.itemData(index)
|
|
ssh_key_path = os.path.expanduser(f'~/.ssh/{ssh_key_filename}.pub')
|
|
if os.path.isfile(ssh_key_path):
|
|
pub_key = open(ssh_key_path).read().strip()
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(pub_key)
|
|
|
|
msg.setWindowTitle(self.tr("Public Key Copied to Clipboard"))
|
|
msg.setText(
|
|
self.tr(
|
|
"The selected public SSH key was copied to the clipboard. "
|
|
"Use it to set up remote repo permissions."
|
|
)
|
|
)
|
|
|
|
else:
|
|
msg.setText(self.tr("Could not find public key."))
|
|
else:
|
|
msg.setText(self.tr("Select a public key from the dropdown first."))
|
|
msg.show()
|
|
|
|
def compression_select_action(self, index):
|
|
profile = self.profile()
|
|
profile.compression = self.repoCompression.currentData()
|
|
profile.save()
|
|
|
|
def new_repo(self):
|
|
"""Open a dialog to create a new repo and add it to vorta."""
|
|
window = AddRepoWindow()
|
|
self._window = window # For tests
|
|
window.setParent(self, QtCore.Qt.WindowType.Sheet)
|
|
window.added_repo.connect(self.process_new_repo)
|
|
# window.rejected.connect(lambda: self.repoSelector.setCurrentIndex(0))
|
|
window.open()
|
|
|
|
def add_existing_repo(self):
|
|
"""Open a dialog to add a existing repo to vorta."""
|
|
window = ExistingRepoWindow()
|
|
self._window = window # For tests
|
|
window.setParent(self, QtCore.Qt.WindowType.Sheet)
|
|
window.added_repo.connect(self.process_new_repo)
|
|
# window.rejected.connect(lambda: self.repoSelector.setCurrentIndex(0))
|
|
window.open()
|
|
|
|
def repo_select_action(self):
|
|
profile = self.profile()
|
|
profile.repo = self.repoSelector.currentData()
|
|
profile.save()
|
|
self.init_repo_stats()
|
|
|
|
def process_new_repo(self, result):
|
|
if result['returncode'] == 0:
|
|
new_repo = RepoModel.get(url=result['params']['repo_url'])
|
|
profile = self.profile()
|
|
profile.repo = new_repo.id
|
|
profile.save()
|
|
|
|
self.set_repos()
|
|
self.repoSelector.setCurrentIndex(self.repoSelector.count() - 1)
|
|
self.repo_added.emit()
|
|
self.init_repo_stats()
|
|
|
|
def repo_unlink_action(self):
|
|
profile = self.profile()
|
|
self.init_repo_stats()
|
|
|
|
msg = QMessageBox()
|
|
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
|
|
msg.setParent(self, QtCore.Qt.WindowType.Sheet)
|
|
|
|
selected_repo_id = self.repoSelector.currentData()
|
|
selected_repo_index = self.repoSelector.currentIndex()
|
|
|
|
if not selected_repo_id:
|
|
# QComboBox is empty / repo unset
|
|
return
|
|
|
|
repo = RepoModel.get(id=selected_repo_id)
|
|
ArchiveModel.delete().where(ArchiveModel.repo_id == repo.id).execute()
|
|
profile.repo = None
|
|
profile.save()
|
|
|
|
repo.delete_instance(recursive=True) # This also deletes archives.
|
|
self.repoSelector.setCurrentIndex(0)
|
|
self.repoSelector.removeItem(selected_repo_index)
|
|
|
|
msg.setWindowTitle(self.tr('Repository was Unlinked'))
|
|
msg.setText(self.tr('You can always connect it again later.'))
|
|
msg.show()
|
|
|
|
self.repo_changed.emit()
|
|
self.populate_from_profile()
|
|
|
|
def copy_URL_action(self):
|
|
selected_repo_id = self.repoSelector.currentData()
|
|
if not selected_repo_id:
|
|
# QComboBox is empty / repo unset
|
|
return
|
|
|
|
repo = RepoModel.get(id=selected_repo_id)
|
|
|
|
url = repo.url
|
|
data = QMimeData()
|
|
|
|
if not repo.is_remote_repo():
|
|
path = PurePath(url)
|
|
data.setUrls([QUrl(path.as_uri())])
|
|
data.setText(str(path))
|
|
else:
|
|
data.setText(url)
|
|
|
|
QApplication.clipboard().setMimeData(data)
|