Skip to content

Commit 1f63ede

Browse files
Merge New/Existing repo dialogs into one (#1799)
Probes with borg info first, connects or offers to init accordingly. Closes #1799
1 parent bc70866 commit 1f63ede

4 files changed

Lines changed: 181 additions & 70 deletions

File tree

src/vorta/views/repo_add_dialog.py

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
QDialogButtonBox,
88
QFormLayout,
99
QLabel,
10+
QMessageBox,
1011
QSizePolicy,
1112
)
1213

@@ -15,7 +16,7 @@
1516
from vorta.keyring.abc import VortaKeyring
1617
from vorta.store.models import RepoModel
1718
from vorta.utils import borg_compat, choose_file_dialog, get_asset, get_private_keys
18-
from vorta.views.partials.password_input import PasswordInput, PasswordLineEdit
19+
from vorta.views.partials.password_input import PasswordInput
1920
from vorta.views.utils import get_colored_icon
2021

2122
uifile = get_asset('UI/repo_add.ui')
@@ -131,7 +132,8 @@ def values(self):
131132
class AddRepoWindow(RepoWindow):
132133
def __init__(self, parent=None):
133134
super().__init__(parent)
134-
self.setWindowTitle("Add New Repository")
135+
self.setWindowTitle("Add Repository")
136+
self.title.setText(self.tr('Add Repository'))
135137

136138
self.passwordInput = PasswordInput()
137139
self.passwordInput.add_form_to_layout(self.repoDataFormLayout)
@@ -213,44 +215,56 @@ def validate(self):
213215
return super().validate() and self.passwordInput.validate()
214216

215217
def run(self):
216-
if self.validate():
217-
params = BorgInitJob.prepare(self.values)
218-
if params['ok']:
219-
self.saveButton.setEnabled(False)
220-
job = BorgInitJob(params['cmd'], params)
221-
job.updated.connect(self._set_status)
222-
job.result.connect(self.run_result)
223-
QApplication.instance().jobs_manager.add_job(job)
224-
else:
225-
self._set_status(params['message'])
226-
227-
228-
class ExistingRepoWindow(RepoWindow):
229-
def __init__(self):
230-
super().__init__()
231-
self.title.setText(self.tr('Connect to existing Repository'))
232-
self.setWindowTitle("Add Existing Repository")
233-
234-
self.passwordLabel = QLabel(self.tr('Password:'))
235-
self.passwordInput = PasswordLineEdit(placeholder_text=self.tr("Enter the encryption passphrase"))
236-
self.repoDataFormLayout.addRow(self.passwordLabel, self.passwordInput)
237-
238-
def set_password(self, URL):
239-
'''Autofill password from keyring only if current entry is empty'''
240-
password = VortaKeyring.get_keyring().get_password('vorta-repo', URL)
241-
if password and self.passwordInput.get_password() == "":
242-
self._set_status(self.tr("Autofilled password from password manager."))
243-
self.passwordInput.setText(password)
218+
if not RepoWindow.validate(self):
219+
return
220+
221+
self.saveButton.setEnabled(False)
222+
self._set_status(self.tr('Checking repository…'))
223+
params = BorgInfoRepoJob.prepare(self.values)
224+
if params['ok']:
225+
job = BorgInfoRepoJob(params['cmd'], params)
226+
job.updated.connect(self._set_status)
227+
job.result.connect(self._probe_result)
228+
QApplication.instance().jobs_manager.add_job(job)
229+
else:
230+
self.saveButton.setEnabled(True)
231+
self._set_status(params['message'])
244232

245-
def run(self):
246-
if self.validate():
247-
params = BorgInfoRepoJob.prepare(self.values)
248-
if params['ok']:
249-
self.saveButton.setEnabled(False)
250-
job = BorgInfoRepoJob(params['cmd'], params)
251-
job.updated.connect(self._set_status)
252-
job.result.connect(self.run_result)
253-
self.thread = job # Keep reference for tests
254-
QApplication.instance().jobs_manager.add_job(job)
233+
def _probe_result(self, result):
234+
if result['returncode'] == 0:
235+
self.saveButton.setEnabled(True)
236+
self.added_repo.emit(result)
237+
self.accept()
238+
else:
239+
error_msgs = ' '.join(msg for _, msg in result.get('errors', []))
240+
if 'does not exist' in error_msgs.lower() or 'is not a valid repository' in error_msgs.lower():
241+
reply = QMessageBox.question(
242+
self,
243+
self.tr('Repository not found'),
244+
self.tr('No repository found at this location. Initialize a new one?'),
245+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
246+
QMessageBox.StandardButton.No,
247+
)
248+
if reply == QMessageBox.StandardButton.Yes:
249+
if not self.passwordInput.validate():
250+
self.saveButton.setEnabled(True)
251+
return
252+
self._init_repo()
253+
else:
254+
self.saveButton.setEnabled(True)
255+
self._set_status(self.tr('Unable to add your repository.'))
255256
else:
256-
self._set_status(params['message'])
257+
self.saveButton.setEnabled(True)
258+
self._set_status(self.tr('Unable to add your repository.'))
259+
260+
def _init_repo(self):
261+
self._set_status(self.tr('Initializing new repository…'))
262+
params = BorgInitJob.prepare(self.values)
263+
if params['ok']:
264+
job = BorgInitJob(params['cmd'], params)
265+
job.updated.connect(self._set_status)
266+
job.result.connect(self.run_result)
267+
QApplication.instance().jobs_manager.add_job(job)
268+
else:
269+
self.saveButton.setEnabled(True)
270+
self._set_status(params['message'])

src/vorta/views/repo_tab.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from vorta.store.models import ArchiveModel, BackupProfileMixin, RepoModel
99
from vorta.utils import borg_compat, get_asset, get_private_keys, pretty_bytes
1010

11-
from .repo_add_dialog import AddRepoWindow, ExistingRepoWindow
11+
from .repo_add_dialog import AddRepoWindow
1212
from .repo_change_passphrase import ChangeBorgPassphraseWindow
1313
from .ssh_dialog import SSHAddWindow
1414
from .utils import get_colored_icon
@@ -28,13 +28,7 @@ def __init__(self, parent=None):
2828
# Populate dropdowns
2929
self.copyURLbutton.clicked.connect(self.copy_URL_action)
3030

31-
# init repo add button
32-
self.menuAddRepo = QMenu(self.bAddRepo)
33-
34-
self.menuAddRepo.addAction(self.tr("New Repository…"), self.new_repo)
35-
self.menuAddRepo.addAction(self.tr("Existing Repository…"), self.add_existing_repo)
36-
37-
self.bAddRepo.setMenu(self.menuAddRepo)
31+
self.bAddRepo.clicked.connect(self.add_repo)
3832

3933
# init repo util button
4034
self.menuRepoUtil = QMenu(self.bRepoUtil)
@@ -268,22 +262,11 @@ def compression_select_action(self, index):
268262
profile.compression = self.repoCompression.currentData()
269263
profile.save()
270264

271-
def new_repo(self):
272-
"""Open a dialog to create a new repo and add it to vorta."""
265+
def add_repo(self):
273266
window = AddRepoWindow()
274267
self._window = window # For tests
275268
window.setParent(self, QtCore.Qt.WindowType.Sheet)
276269
window.added_repo.connect(self.process_new_repo)
277-
# window.rejected.connect(lambda: self.repoSelector.setCurrentIndex(0))
278-
window.open()
279-
280-
def add_existing_repo(self):
281-
"""Open a dialog to add a existing repo to vorta."""
282-
window = ExistingRepoWindow()
283-
self._window = window # For tests
284-
window.setParent(self, QtCore.Qt.WindowType.Sheet)
285-
window.added_repo.connect(self.process_new_repo)
286-
# window.rejected.connect(lambda: self.repoSelector.setCurrentIndex(0))
287270
window.open()
288271

289272
def repo_select_action(self):

tests/integration/test_init.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
def test_create_repo(qapp, qtbot, monkeypatch, choose_file_dialog, tmpdir):
2222
"""Test initializing a new repository"""
2323
main = qapp.main_window
24-
main.repoTab.new_repo()
24+
main.repoTab.add_repo()
2525
add_repo_window = main.repoTab._window
2626
main.show()
2727

@@ -44,6 +44,9 @@ def test_create_repo(qapp, qtbot, monkeypatch, choose_file_dialog, tmpdir):
4444
qtbot.keyClicks(add_repo_window.passwordInput.passwordLineEdit, LONG_PASSWORD)
4545
qtbot.keyClicks(add_repo_window.passwordInput.confirmLineEdit, LONG_PASSWORD)
4646

47+
# borg info fails for new/empty directories, triggering an init confirmation prompt
48+
monkeypatch.setattr(QMessageBox, "question", lambda *args, **kwargs: QMessageBox.StandardButton.Yes)
49+
4750
initial_count = main.repoTab.repoSelector.count()
4851
add_repo_window.run()
4952

@@ -88,7 +91,7 @@ def test_add_existing_repo(qapp, qtbot, monkeypatch, choose_file_dialog):
8891
)
8992

9093
# add existing repo again
91-
main.repoTab.add_existing_repo()
94+
main.repoTab.add_repo()
9295
add_repo_window = main.repoTab._window
9396

9497
monkeypatch.setattr(

tests/unit/test_repo.py

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import os
23
import uuid
34
from typing import Any, Dict
@@ -10,6 +11,7 @@
1011
import vorta.borg.borg_job
1112
from vorta.keyring.abc import VortaKeyring
1213
from vorta.store.models import ArchiveModel, EventLogModel, RepoModel
14+
from vorta.views.repo_add_dialog import AddRepoWindow
1315

1416
LONG_PASSWORD = 'long-password-long'
1517
SHORT_PASSWORD = 'hunter2'
@@ -28,7 +30,7 @@ def test_new_repo_password_validation(qapp, qtbot, borg_json_output, first_passw
2830
# Add new repo window
2931
main = qapp.main_window
3032
tab = main.repoTab
31-
tab.new_repo()
33+
tab.add_repo()
3234
add_repo_window = tab._window
3335
qtbot.addWidget(add_repo_window)
3436

@@ -41,15 +43,15 @@ def test_new_repo_password_validation(qapp, qtbot, borg_json_output, first_passw
4143
@pytest.mark.parametrize(
4244
"repo_name, error_text",
4345
[
44-
('test_repo_name', ''), # valid repo name
45-
('a' * 64, ''), # also valid (<=64 characters)
46+
('test_repo_name', 'Checking repository\u2026'), # valid repo name
47+
('a' * 64, 'Checking repository\u2026'), # also valid (<=64 characters)
4648
('a' * 65, 'Repository name must be less than 65 characters.'), # not valid (>64 characters)
4749
],
4850
)
4951
def test_repo_add_name_validation(qapp, qtbot, borg_json_output, repo_name, error_text):
5052
main = qapp.main_window
5153
tab = main.repoTab
52-
tab.new_repo()
54+
tab.add_repo()
5355
add_repo_window = tab._window
5456
test_repo_url = f'vorta-test-repo.{uuid.uuid4()}.com:repo' # Random repo URL to avoid macOS keychain
5557
qtbot.addWidget(add_repo_window)
@@ -81,7 +83,7 @@ def test_repo_unlink(qapp, qtbot, monkeypatch):
8183
def test_password_autofill(qapp, qtbot):
8284
main = qapp.main_window
8385
tab = main.repoTab
84-
tab.new_repo()
86+
tab.add_repo()
8587
add_repo_window = tab._window
8688
test_repo_url = f'vorta-test-repo.{uuid.uuid4()}.com:repo' # Random repo URL to avoid macOS keychain
8789

@@ -97,7 +99,7 @@ def test_password_autofill(qapp, qtbot):
9799
def test_repo_add_failure(qapp, qtbot, borg_json_output):
98100
main = qapp.main_window
99101
tab = main.repoTab
100-
tab.new_repo()
102+
tab.add_repo()
101103
add_repo_window = tab._window
102104
qtbot.addWidget(add_repo_window)
103105

@@ -110,7 +112,7 @@ def test_repo_add_failure(qapp, qtbot, borg_json_output):
110112
def test_repo_add_success(qapp, qtbot, mocker, borg_json_output):
111113
main = qapp.main_window
112114
tab = main.repoTab
113-
tab.new_repo()
115+
tab.add_repo()
114116
add_repo_window = tab._window
115117
test_repo_url = f'vorta-test-repo.{uuid.uuid4()}.com:repo' # Random repo URL to avoid macOS keychain
116118
test_repo_name = 'Test Repo'
@@ -359,3 +361,112 @@ def test_handle_passphrase_change_result(qapp, qtbot, mocker, result, expected_t
359361
mock_instance.setWindowTitle.assert_called_once_with(tab.tr(expected_title))
360362
mock_instance.setText.assert_called_once_with(tab.tr(expected_text))
361363
mock_instance.show.assert_called_once()
364+
365+
366+
@pytest.mark.parametrize(
367+
"error_message",
368+
[
369+
'Repository /tmp/nonexistent-repo does not exist.',
370+
'/tmp/nonexistent-repo is not a valid repository. Check repo config.',
371+
],
372+
)
373+
def test_add_repo_not_found_offers_init(qapp, qtbot, mocker, error_message):
374+
main = qapp.main_window
375+
tab = main.repoTab
376+
tab.add_repo()
377+
window = tab._window
378+
379+
mocker.patch.object(
380+
QMessageBox,
381+
'question',
382+
return_value=QMessageBox.StandardButton.Yes,
383+
)
384+
mock_init = mocker.patch.object(window, '_init_repo')
385+
386+
qtbot.keyClicks(window.passwordInput.passwordLineEdit, 'long-password-long')
387+
qtbot.keyClicks(window.passwordInput.confirmLineEdit, 'long-password-long')
388+
window.repoURL.setText('/tmp/nonexistent-repo')
389+
window.repoName.setText('Test Repo')
390+
window.is_remote_repo = False
391+
392+
result = {
393+
'returncode': 2,
394+
'cmd': ['borg', 'info', '/tmp/nonexistent-repo'],
395+
'errors': [(logging.ERROR, error_message)],
396+
'params': {'repo_url': '/tmp/nonexistent-repo', 'profile_name': 'Default'},
397+
'data': '',
398+
}
399+
window._probe_result(result)
400+
401+
QMessageBox.question.assert_called_once()
402+
mock_init.assert_called_once()
403+
404+
405+
def test_add_repo_not_found_user_declines(qapp, qtbot, mocker):
406+
main = qapp.main_window
407+
tab = main.repoTab
408+
tab.add_repo()
409+
window = tab._window
410+
411+
mocker.patch.object(
412+
QMessageBox,
413+
'question',
414+
return_value=QMessageBox.StandardButton.No,
415+
)
416+
417+
window.repoURL.setText('/tmp/nonexistent-repo')
418+
window.is_remote_repo = False
419+
420+
result = {
421+
'returncode': 2,
422+
'cmd': ['borg', 'info', '/tmp/nonexistent-repo'],
423+
'errors': [(logging.ERROR, 'Repository /tmp/nonexistent-repo does not exist.')],
424+
'params': {'repo_url': '/tmp/nonexistent-repo', 'profile_name': 'Default'},
425+
'data': '',
426+
}
427+
window._probe_result(result)
428+
429+
assert window.errorText.text() == 'Unable to add your repository.'
430+
431+
432+
def test_add_repo_other_error_no_init_offer(qapp, qtbot, mocker):
433+
main = qapp.main_window
434+
tab = main.repoTab
435+
tab.add_repo()
436+
window = tab._window
437+
438+
mock_question = mocker.patch.object(QMessageBox, 'question')
439+
440+
window.repoURL.setText('host.example.com:repo')
441+
window.is_remote_repo = True
442+
443+
result = {
444+
'returncode': 2,
445+
'cmd': ['borg', 'info', 'host.example.com:repo'],
446+
'errors': [(logging.ERROR, 'Connection refused')],
447+
'params': {'repo_url': 'host.example.com:repo', 'profile_name': 'Default'},
448+
'data': '',
449+
}
450+
window._probe_result(result)
451+
452+
mock_question.assert_not_called()
453+
assert window.errorText.text() == 'Unable to add your repository.'
454+
455+
456+
def test_add_repo_probe_succeeds_connects(qapp, qtbot):
457+
window = AddRepoWindow()
458+
459+
signal_received = []
460+
window.added_repo.connect(lambda r: signal_received.append(r))
461+
462+
result = {
463+
'returncode': 0,
464+
'cmd': ['borg', 'info', '/tmp/existing-repo'],
465+
'errors': [],
466+
'params': {'repo_url': '/tmp/existing-repo', 'repo_name': 'Test'},
467+
'data': {},
468+
}
469+
window._probe_result(result)
470+
471+
assert len(signal_received) == 1
472+
assert signal_received[0]['returncode'] == 0

0 commit comments

Comments
 (0)