Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions src/vorta/application.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple
Expand Down Expand Up @@ -258,13 +259,42 @@ def react_to_log(self, mgs, context):
msg.show()
elif msgid == 'LockFailed':
repo_url = context.get('repo_url')
borg_message = context.get('message', '')

# Borg reports any failure to create the lock file as LockFailed, so the
# errno in its message tells us what actually went wrong. Match the number
# and not the text after it, because that text comes from strerror and is
# localized. ENOSPC is 28 everywhere, EDQUOT is 122 on Linux and 69 on
# macOS/BSD. For a remote repository the errno originates on the server, so
# the local platform's errno constants are not a valid comparison here.
errno_match = re.search(r'\[Errno (\d+)\]', borg_message)
err = int(errno_match.group(1)) if errno_match else None

msg = QMessageBox()
msg.setText(
self.tr(
f"You do not have permission to access the repository at {repo_url}. Gain access and try again."
msg.setIcon(QMessageBox.Icon.Critical)
if err in (28, 122, 69):
msg.setWindowTitle(self.tr("Repository Storage Full"))
msg.setText(self.tr("The storage backing the repository at {} is full.").format(repo_url))
msg.setInformativeText(
self.tr(
"Borg could not create its lock file, so no operation can run — including pruning, "
"which would free space. Free up space outside the repository, then try again."
)
)
) # noqa: E501
msg.setWindowTitle(self.tr("No Repository Permissions"))
elif err in (1, 13):
msg.setWindowTitle(self.tr("No Repository Permissions"))
msg.setText(
self.tr(
"You do not have permission to access the repository at {}. Gain access and try again."
).format(repo_url)
)
else:
msg.setWindowTitle(self.tr("Repository Lock Failed"))
msg.setText(self.tr("Borg could not lock the repository at {}.").format(repo_url))

if borg_message:
msg.setDetailedText(borg_message)

self._msg = msg
msg.show()

Expand Down
1 change: 1 addition & 0 deletions src/vorta/borg/borg_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ def read_async(fd):
if parsed['type'] == 'log_message':
context = {
'msgid': parsed.get('msgid'),
'message': parsed.get('message', ''),
'repo_url': self.params['repo_url'],
'profile_name': self.params.get('profile_name'),
'cmd': self.params['cmd'][1],
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/borg_json_output/create_lockfail_stderr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type": "log_message", "time": 1786000100.0, "message": "Failed to create/acquire the lock /mnt/repo/lock.exclusive ([Errno 30] Read-only file system: '/mnt/repo/lock.exclusive.xn0egqgt.tmp').", "levelname": "ERROR", "name": "borg.archiver", "msgid": "LockFailed"}
{"type": "log_message", "time": 1786000100.5, "message": "Traceback (most recent call last):\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 4591, in main\n exit_code = archiver.run(args)\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 4523, in run\n return set_ec(func(args))\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 161, in wrapper\n with repository:\n File \"/usr/lib/python3/dist-packages/borg/repository.py\", line 190, in __enter__\n self.open(self.path, bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock)\n File \"/usr/lib/python3/dist-packages/borg/repository.py\", line 421, in open\n self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=hostname_is_unique()).acquire()\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 350, in acquire\n self._wait_for_readers_finishing(remove, sleep)\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 363, in _wait_for_readers_finishing\n self._lock.acquire()\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 138, in acquire\n raise LockFailed(self.path, str(err)) from None\nborg.locking.LockFailed: Failed to create/acquire the lock /mnt/repo/lock.exclusive ([Errno 30] Read-only file system: '/mnt/repo/lock.exclusive.xn0egqgt.tmp').\n\nPlatform: Linux github 6.9.0-30-generic #30-Ubuntu SMP Thu Jul 10 12:37:59 UTC 2025 x86_64\nLinux: Unknown Linux \nBorg: 1.4.5 Python: CPython 3.14.6 msgpack: 0.5.6\nPID: 64701 CWD: /home/user/Projects/vorta/tests/borg_json_output\nsys.argv: ['/usr/bin/borg', 'create', '--list', '--progress', '--info', '--log-json', '--json', '--filter=AM', '-C', 'lz4', '/mnt/repo::github-asdf-2026-08-14T00:05:49', '/home/user/bashrc']\nSSH_ORIGINAL_COMMAND: None\n", "levelname": "ERROR", "name": "borg.archiver"}
Empty file.
2 changes: 2 additions & 0 deletions tests/unit/borg_json_output/create_quota_stderr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type": "log_message", "time": 1786000000.0, "message": "Failed to create/acquire the lock /home/repo/lock.exclusive ([Errno 122] Disk quota exceeded: '/home/repo/lock.exclusive.xn0egqgt.tmp').", "levelname": "ERROR", "name": "borg.archiver", "msgid": "LockFailed"}
{"type": "log_message", "time": 1786000000.5, "message": "Traceback (most recent call last):\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 4591, in main\n exit_code = archiver.run(args)\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 4523, in run\n return set_ec(func(args))\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 161, in wrapper\n with repository:\n File \"/usr/lib/python3/dist-packages/borg/repository.py\", line 190, in __enter__\n self.open(self.path, bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock)\n File \"/usr/lib/python3/dist-packages/borg/repository.py\", line 421, in open\n self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=hostname_is_unique()).acquire()\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 350, in acquire\n self._wait_for_readers_finishing(remove, sleep)\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 363, in _wait_for_readers_finishing\n self._lock.acquire()\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 138, in acquire\n raise LockFailed(self.path, str(err)) from None\nborg.locking.LockFailed: Failed to create/acquire the lock /home/repo/lock.exclusive ([Errno 122] Disk quota exceeded: '/home/repo/lock.exclusive.xn0egqgt.tmp').\n\nPlatform: Linux github 6.9.0-30-generic #30-Ubuntu SMP Thu Jul 10 12:37:59 UTC 2025 x86_64\nLinux: Unknown Linux \nBorg: 1.4.5 Python: CPython 3.14.6 msgpack: 0.5.6\nPID: 64701 CWD: /home/user/Projects/vorta/tests/borg_json_output\nsys.argv: ['/usr/bin/borg', 'create', '--list', '--progress', '--info', '--log-json', '--json', '--filter=AM', '-C', 'lz4', '/home/repo::github-asdf-2026-08-14T00:05:49', '/home/user/bashrc']\nSSH_ORIGINAL_COMMAND: None\n", "levelname": "ERROR", "name": "borg.archiver"}
Empty file.
32 changes: 32 additions & 0 deletions tests/unit/test_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,38 @@ def test_create_perm_error(qapp, borg_json_output, mocker, qtbot):
del qapp._msg


def test_create_quota_error(qapp, borg_json_output, mocker, qtbot):
main = qapp.main_window
mocker.patch.object(vorta.application.QMessageBox, 'show')

stdout, stderr = borg_json_output('create_quota')
popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)
mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result)

qtbot.mouseClick(main.createStartBtn, QtCore.Qt.MouseButton.LeftButton)

qtbot.waitUntil(lambda: hasattr(qapp, '_msg'), **pytest._wait_defaults)
assert "is full" in qapp._msg.text()
assert "Disk quota exceeded" in qapp._msg.detailedText()
del qapp._msg


def test_create_lock_failed_generic(qapp, borg_json_output, mocker, qtbot):
main = qapp.main_window
mocker.patch.object(vorta.application.QMessageBox, 'show')

stdout, stderr = borg_json_output('create_lockfail')
popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)
mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result)

qtbot.mouseClick(main.createStartBtn, QtCore.Qt.MouseButton.LeftButton)

qtbot.waitUntil(lambda: hasattr(qapp, '_msg'), **pytest._wait_defaults)
assert "Borg could not lock the repository" in qapp._msg.text()
assert "Read-only file system" in qapp._msg.detailedText()
del qapp._msg


def test_create_lock(qapp, borg_json_output, mocker, qtbot):
main = qapp.main_window
mocker.patch.object(vorta.application.QMessageBox, 'show')
Expand Down