vorta/src/vorta/views/repo_add_dialog.py

258 lines
10 KiB
Python

import re
from PyQt6 import QtCore, uic
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QApplication, QDialogButtonBox, QLineEdit
from vorta.borg.info_repo import BorgInfoRepoJob
from vorta.borg.init import BorgInitJob
from vorta.i18n import translate
from vorta.keyring.abc import VortaKeyring
from vorta.store.models import RepoModel
from vorta.utils import (
borg_compat,
choose_file_dialog,
get_asset,
get_private_keys,
validate_passwords,
)
from vorta.views.utils import get_colored_icon
uifile = get_asset('UI/repoadd.ui')
AddRepoUI, AddRepoBase = uic.loadUiType(uifile)
class AddRepoWindow(AddRepoBase, AddRepoUI):
added_repo = QtCore.pyqtSignal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose)
self.result = None
self.is_remote_repo = True
# dialogButtonBox
self.saveButton = self.buttonBox.button(QDialogButtonBox.StandardButton.Ok)
self.saveButton.setText(self.tr("Add"))
self.buttonBox.rejected.connect(self.close)
self.buttonBox.accepted.connect(self.run)
self.chooseLocalFolderButton.clicked.connect(self.choose_local_backup_folder)
self.useRemoteRepoButton.clicked.connect(self.use_remote_repo_action)
self.repoURL.textChanged.connect(self.set_password)
self.passwordLineEdit.textChanged.connect(self.password_listener)
self.confirmLineEdit.textChanged.connect(self.password_listener)
self.encryptionComboBox.activated.connect(self.display_backend_warning)
# Add clickable icon to toggle password visibility to end of box
self.showHideAction = QAction(self.tr("Show my passwords"), self)
self.showHideAction.setCheckable(True)
self.showHideAction.toggled.connect(self.set_visibility)
self.passwordLineEdit.addAction(self.showHideAction, QLineEdit.ActionPosition.TrailingPosition)
self.tabWidget.setCurrentIndex(0)
self.init_encryption()
self.init_ssh_key()
self.set_icons()
self.display_backend_warning()
def retranslateUi(self, dialog):
"""Retranslate strings in ui."""
super().retranslateUi(dialog)
# setupUi calls retranslateUi
if hasattr(self, 'saveButton'):
self.saveButton.setText(self.tr("Add"))
def set_icons(self):
self.chooseLocalFolderButton.setIcon(get_colored_icon('folder-open'))
self.useRemoteRepoButton.setIcon(get_colored_icon('globe'))
self.showHideAction.setIcon(get_colored_icon("eye"))
@property
def values(self):
out = dict(
ssh_key=self.sshComboBox.currentData(),
repo_url=self.repoURL.text(),
password=self.passwordLineEdit.text(),
extra_borg_arguments=self.extraBorgArgumentsLineEdit.text(),
)
if self.__class__ == AddRepoWindow:
out['encryption'] = self.encryptionComboBox.currentData()
return out
def display_backend_warning(self):
'''Display password backend message based off current keyring'''
if self.encryptionComboBox.currentData() != 'none':
self.passwordLabel.setText(VortaKeyring.get_keyring().get_backend_warning())
def choose_local_backup_folder(self):
def receive():
folder = dialog.selectedFiles()
if folder:
self.repoURL.setText(folder[0])
self.repoURL.setEnabled(False)
self.sshComboBox.setEnabled(False)
self.repoLabel.setText(self.tr('Repository Path:'))
self.is_remote_repo = False
dialog = choose_file_dialog(self, self.tr("Choose Location of Borg Repository"))
dialog.open(receive)
def set_password(self, URL):
'''Autofill password from keyring only if current entry is empty'''
password = VortaKeyring.get_keyring().get_password('vorta-repo', URL)
if password and self.passwordLineEdit.text() == "":
self.passwordLabel.setText(self.tr("Autofilled password from password manager."))
self.passwordLineEdit.setText(password)
if self.__class__ == AddRepoWindow:
self.confirmLineEdit.setText(password)
def set_visibility(self, visible):
visibility = QLineEdit.EchoMode.Normal if visible else QLineEdit.EchoMode.Password
self.passwordLineEdit.setEchoMode(visibility)
self.confirmLineEdit.setEchoMode(visibility)
if visible:
self.showHideAction.setIcon(get_colored_icon("eye-slash"))
self.showHideAction.setText(self.tr("Hide my passwords"))
else:
self.showHideAction.setIcon(get_colored_icon("eye"))
self.showHideAction.setText(self.tr("Show my passwords"))
def use_remote_repo_action(self):
self.repoURL.setText('')
self.repoURL.setEnabled(True)
self.sshComboBox.setEnabled(True)
self.extraBorgArgumentsLineEdit.setText('')
self.repoLabel.setText(self.tr('Repository URL:'))
self.is_remote_repo = True
# No need to add this function to JobsManager because repo is set for the first time
def run(self):
if self.validate() and self.password_listener():
params = BorgInitJob.prepare(self.values)
if params['ok']:
self.saveButton.setEnabled(False)
job = BorgInitJob(params['cmd'], params)
job.updated.connect(self._set_status)
job.result.connect(self.run_result)
QApplication.instance().jobs_manager.add_job(job)
else:
self._set_status(params['message'])
def _set_status(self, text):
self.errorText.setText(text)
self.errorText.repaint()
def run_result(self, result):
self.saveButton.setEnabled(True)
if result['returncode'] == 0:
self.added_repo.emit(result)
self.accept()
else:
self._set_status(self.tr('Unable to add your repository.'))
def init_encryption(self):
if borg_compat.check('V2'):
encryption_algos = [
[
self.tr('Repokey-ChaCha20-Poly1305 (Recommended, key stored in repository)'),
'repokey-blake2-chacha20-poly1305',
],
[
self.tr('Keyfile-ChaCha20-Poly1305 (Key stored in home directory)'),
'keyfile-blake2-chacha20-poly1305',
],
[self.tr('Repokey-AES256-OCB'), 'repokey-blake2-aes-ocb'],
[self.tr('Keyfile-AES256-OCB'), 'keyfile-blake2-aes-ocb'],
[self.tr('None (not recommended)'), 'none'],
]
else:
encryption_algos = [
[self.tr('Repokey-Blake2 (Recommended, key stored in repository)'), 'repokey-blake2'],
[self.tr('Repokey'), 'repokey'],
[self.tr('Keyfile-Blake2 (Key stored in home directory)'), 'keyfile-blake2'],
[self.tr('Keyfile'), 'keyfile'],
[self.tr('None (not recommended)'), 'none'],
]
for desc, name in encryption_algos:
self.encryptionComboBox.addItem(desc, name)
if not borg_compat.check('BLAKE2'):
self.encryptionComboBox.model().item(0).setEnabled(False)
self.encryptionComboBox.model().item(2).setEnabled(False)
self.encryptionComboBox.setCurrentIndex(1)
def init_ssh_key(self):
keys = get_private_keys()
for key in keys:
self.sshComboBox.addItem(f'{key}', key)
def validate(self):
"""Pre-flight check for valid input and borg binary."""
if self.is_remote_repo and not re.match(r'.+:.+', self.values['repo_url']):
self._set_status(self.tr('Please enter a valid repo URL or select a local path.'))
return False
if RepoModel.get_or_none(RepoModel.url == self.values['repo_url']) is not None:
self._set_status(self.tr('This repo has already been added.'))
return False
return True
def password_listener(self):
'''Validates passwords only if its going to be used'''
if self.values['encryption'] == 'none':
self.passwordLabel.setText("")
return True
else:
firstPass = self.passwordLineEdit.text()
secondPass = self.confirmLineEdit.text()
msg = validate_passwords(firstPass, secondPass)
self.passwordLabel.setText(translate('utils', msg))
return not bool(msg)
class ExistingRepoWindow(AddRepoWindow):
def __init__(self):
super().__init__()
self.encryptionComboBox.hide()
self.encryptionLabel.hide()
self.title.setText(self.tr('Connect to existing Repository'))
self.showHideAction.setText(self.tr("Show my password"))
self.passwordLineEdit.textChanged.disconnect()
self.confirmLineEdit.textChanged.disconnect()
self.confirmLineEdit.hide()
self.confirmLabel.hide()
del self.confirmLineEdit
del self.confirmLabel
def set_visibility(self, visible):
visibility = QLineEdit.EchoMode.Normal if visible else QLineEdit.EchoMode.Password
self.passwordLineEdit.setEchoMode(visibility)
if visible:
self.showHideAction.setIcon(get_colored_icon("eye-slash"))
self.showHideAction.setText(self.tr("Hide my password"))
else:
self.showHideAction.setIcon(get_colored_icon("eye"))
self.showHideAction.setText(self.tr("Show my password"))
def run(self):
if self.validate():
params = BorgInfoRepoJob.prepare(self.values)
if params['ok']:
self.saveButton.setEnabled(False)
thread = BorgInfoRepoJob(params['cmd'], params)
thread.updated.connect(self._set_status)
thread.result.connect(self.run_result)
self.thread = thread # Needs to be connected to self for tests to work.
self.thread.run()
else:
self._set_status(params['message'])