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'])