From d177eb7f43c4e36cfd6399696b153610c53cef12 Mon Sep 17 00:00:00 2001 From: Ahmed Hossam Date: Wed, 19 Mar 2025 06:25:14 +0200 Subject: [PATCH 1/7] make borg errors appear in exception dialog --- src/vorta/application.py | 10 ++++++++++ src/vorta/borg/borg_job.py | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/vorta/application.py b/src/vorta/application.py index d87830d91..3acd077fc 100644 --- a/src/vorta/application.py +++ b/src/vorta/application.py @@ -21,6 +21,7 @@ from vorta.store.models import BackupProfileModel, SettingsModel from vorta.tray_menu import TrayMenu from vorta.utils import borg_compat, parse_args +from vorta.views.exception_dialog import ExceptionDialog from vorta.views.main_window import MainWindow logger = logging.getLogger(__name__) @@ -43,6 +44,7 @@ class VortaApp(QtSingleApplication): backup_progress_event = QtCore.pyqtSignal(str) check_failed_event = QtCore.pyqtSignal(dict) profile_changed_event = QtCore.pyqtSignal() + error_signal = QtCore.pyqtSignal(str) def __init__(self, args_raw, single_app=False): super().__init__(str(APP_ID), args_raw) @@ -86,6 +88,7 @@ def __init__(self, args_raw, single_app=False): self.check_failed_event.connect(self.check_failed_response) self.backup_log_event.connect(self.react_to_log) self.aboutToQuit.connect(self.quit_app_action) + self.error_signal.connect(self.show_exception_dialog) self.set_borg_details_action() if sys.platform == 'darwin': self.check_darwin_permissions() @@ -157,6 +160,13 @@ def message_received_event_response(self, message): else: self.create_backups_cmdline(message) + def show_exception_dialog(self, error_msg): + exception_dialog = ExceptionDialog(error_msg) + exception_dialog.show() + exception_dialog.raise_() + exception_dialog.activateWindow() + exception_dialog.exec() + # No need to add this function to JobsManager because it doesn't require to lock a repo. def set_borg_details_action(self): params = BorgVersionJob.prepare() diff --git a/src/vorta/borg/borg_job.py b/src/vorta/borg/borg_job.py index 588ff2dd9..82e95aecb 100644 --- a/src/vorta/borg/borg_job.py +++ b/src/vorta/borg/borg_job.py @@ -263,6 +263,7 @@ def read_async(fd): return '' stdout = [] + error_msg = None while True: # Wait for new output select.select([p.stdout, p.stderr], [], [], 0.1) @@ -308,7 +309,7 @@ def read_async(fd): except json.decoder.JSONDecodeError: msg = line.strip() if msg: # Log only if there is something to log. - self.app.backup_log_event.emit(f'[{self.params["profile_name"]}] {msg}', {}) + error_msg = stderr logger.warning(msg) if p.poll() is not None: @@ -316,6 +317,9 @@ def read_async(fd): stdout.append(read_async(p.stdout)) break + if error_msg: + self.app.error_signal.emit(error_msg) + result = { 'params': self.params, 'returncode': self.process.returncode, From c92c758c837701e5fa6db038e26021fd3bef7f7b Mon Sep 17 00:00:00 2001 From: Ahmed Hossam Date: Wed, 19 Mar 2025 07:05:13 +0200 Subject: [PATCH 2/7] better progress event messages --- src/vorta/borg/borg_job.py | 3 +++ src/vorta/borg/delete.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/vorta/borg/borg_job.py b/src/vorta/borg/borg_job.py index 82e95aecb..8c3aa71b6 100644 --- a/src/vorta/borg/borg_job.py +++ b/src/vorta/borg/borg_job.py @@ -310,6 +310,9 @@ def read_async(fd): msg = line.strip() if msg: # Log only if there is something to log. error_msg = stderr + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] `{' '.join(self.cmd)}` has failed." + ) logger.warning(msg) if p.poll() is not None: diff --git a/src/vorta/borg/delete.py b/src/vorta/borg/delete.py index 5b1fe72f0..9bde39f52 100644 --- a/src/vorta/borg/delete.py +++ b/src/vorta/borg/delete.py @@ -22,7 +22,12 @@ def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) - self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Archive deleted.')}") + if result['returncode'] == 0: + self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Archive deleted.')}") + else: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] {self.tr('Archive delete has failed.')}" + ) @classmethod def prepare(cls, profile, archives: List[str]): From 41a021c4b4af2c0ff202cb2303aace55850ec751 Mon Sep 17 00:00:00 2001 From: Ahmed Hossam Date: Thu, 20 Mar 2025 01:26:42 +0200 Subject: [PATCH 3/7] better progress event messages for most commands --- src/vorta/application.py | 2 +- src/vorta/borg/borg_job.py | 9 +++------ src/vorta/borg/check.py | 6 +++--- src/vorta/borg/create.py | 24 +++++++++++------------- src/vorta/borg/delete.py | 13 +++++++++---- src/vorta/borg/diff.py | 16 +++++++++++++--- src/vorta/borg/extract.py | 16 +++++++++++++--- src/vorta/borg/info_archive.py | 14 +++++++++++++- src/vorta/borg/mount.py | 17 +++++++++++++++++ src/vorta/borg/prune.py | 11 +++++++++++ src/vorta/scheduler.py | 2 +- src/vorta/store/settings.py | 2 +- 12 files changed, 96 insertions(+), 36 deletions(-) diff --git a/src/vorta/application.py b/src/vorta/application.py index 3acd077fc..1bd8a50ef 100644 --- a/src/vorta/application.py +++ b/src/vorta/application.py @@ -299,7 +299,7 @@ def bootstrap_profile(self, bootstrap_file=None): double_newline, str(exception), double_newline, - self.tr('Consider removing or repairing this file to ' 'get rid of this message.'), + self.tr('Consider removing or repairing this file to get rid of this message.'), ), ) return diff --git a/src/vorta/borg/borg_job.py b/src/vorta/borg/borg_job.py index 8c3aa71b6..10a463d16 100644 --- a/src/vorta/borg/borg_job.py +++ b/src/vorta/borg/borg_job.py @@ -300,19 +300,16 @@ def read_async(fd): self.app.backup_log_event.emit(f'[{self.params["profile_name"]}] {parsed["message"]}', {}) elif parsed['type'] == 'archive_progress' and not parsed.get('finished', False): msg = ( - f"{translate('BorgJob','Files')}: {parsed['nfiles']}, " - f"{translate('BorgJob','Original')}: {pretty_bytes(parsed['original_size'])}, " + f"{translate('BorgJob', 'Files')}: {parsed['nfiles']}, " + f"{translate('BorgJob', 'Original')}: {pretty_bytes(parsed['original_size'])}, " # f"{translate('BorgJob','Compressed')}: {pretty_bytes(parsed['compressed_size'])}, " - f"{translate('BorgJob','Deduplicated')}: {pretty_bytes(parsed.get('deduplicated_size', 0))}" # noqa: E501 + f"{translate('BorgJob', 'Deduplicated')}: {pretty_bytes(parsed.get('deduplicated_size', 0))}" # noqa: E501 ) self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {msg}") except json.decoder.JSONDecodeError: msg = line.strip() if msg: # Log only if there is something to log. error_msg = stderr - self.app.backup_progress_event.emit( - f"[{self.params['profile_name']}] `{' '.join(self.cmd)}` has failed." - ) logger.warning(msg) if p.poll() is not None: diff --git a/src/vorta/borg/check.py b/src/vorta/borg/check.py index e25443fdf..cc6ee28f2 100644 --- a/src/vorta/borg/check.py +++ b/src/vorta/borg/check.py @@ -26,9 +26,9 @@ def finished_event(self, result: Dict[str, Any]): if result['returncode'] != 0: self.app.backup_progress_event.emit( f"[{self.params['profile_name']}] " - + translate('RepoCheckJob', 'Repo check failed. See the logs for details.').format( - config.LOG_DIR.as_uri() - ) + + translate( + 'RepoCheckJob', 'Repo check command has failed. See the logs for details.' + ).format(config.LOG_DIR.as_uri()) ) self.app.check_failed_event.emit(result) else: diff --git a/src/vorta/borg/create.py b/src/vorta/borg/create.py index 5cdb136ff..723152650 100644 --- a/src/vorta/borg/create.py +++ b/src/vorta/borg/create.py @@ -42,18 +42,6 @@ def process_result(self, result): repo.total_unique_chunks = stats['total_unique_chunks'] repo.save() - if result['returncode'] == 1: - self.app.backup_progress_event.emit( - f"[{self.params['profile_name']}] " - + translate( - 'BorgCreateJob', - 'Backup finished with warnings. See the logs for details.', - ).format(config.LOG_DIR.as_uri()) - ) - else: - self.app.backup_log_event.emit('', {}) - self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Backup finished.')}") - def progress_event(self, fmt): self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {fmt}") @@ -64,7 +52,17 @@ def started_event(self): def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) - self.pre_post_backup_cmd(self.params, cmd='post_backup_cmd', returncode=result['returncode']) + if result['returncode'] != 0: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] " + + translate( + 'BorgCreateJob', 'Backup finished with errors. See the logs for details.' + ).format(config.LOG_DIR.as_uri()) + ) + else: + self.app.backup_log_event.emit('', {}) + self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Backup finished.')}") + self.pre_post_backup_cmd(self.params, cmd='post_backup_cmd', returncode=result['returncode']) @classmethod def pre_post_backup_cmd(cls, params, cmd='pre_backup_cmd', returncode=0): diff --git a/src/vorta/borg/delete.py b/src/vorta/borg/delete.py index 9bde39f52..0dbb69dc5 100644 --- a/src/vorta/borg/delete.py +++ b/src/vorta/borg/delete.py @@ -1,5 +1,7 @@ from typing import List +from vorta import config +from vorta.i18n import translate from vorta.store.models import RepoModel from vorta.utils import borg_compat @@ -22,12 +24,15 @@ def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) - if result['returncode'] == 0: - self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Archive deleted.')}") - else: + if result['returncode'] != 0: self.app.backup_progress_event.emit( - f"[{self.params['profile_name']}] {self.tr('Archive delete has failed.')}" + f"[{self.params['profile_name']}] " + + translate( + 'BorgDeleteJob', 'Delete command has failed. See the logs for details.' + ).format(config.LOG_DIR.as_uri()) ) + else: + self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Archive deleted.')}") @classmethod def prepare(cls, profile, archives: List[str]): diff --git a/src/vorta/borg/diff.py b/src/vorta/borg/diff.py index cd09ff815..72f22ae79 100644 --- a/src/vorta/borg/diff.py +++ b/src/vorta/borg/diff.py @@ -1,3 +1,5 @@ +from vorta import config +from vorta.i18n import translate from vorta.utils import borg_compat from .borg_job import BorgJob @@ -12,10 +14,18 @@ def started_event(self): def finished_event(self, result): self.app.backup_finished_event.emit(result) - self.app.backup_progress_event.emit( - f"[{self.params['profile_name']}] {self.tr('Obtained differences between archives.')}" - ) self.result.emit(result) + if result['returncode'] != 0: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] " + + translate( + 'BorgDiffJob', 'Diff command has failed. See the logs for details.' + ).format(config.LOG_DIR.as_uri()) + ) + else: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] {self.tr('Obtained differences between archives.')}" + ) @classmethod def prepare(cls, profile, archive_name_1, archive_name_2): diff --git a/src/vorta/borg/extract.py b/src/vorta/borg/extract.py index 41108721f..3605d9f24 100644 --- a/src/vorta/borg/extract.py +++ b/src/vorta/borg/extract.py @@ -2,6 +2,8 @@ from PyQt6.QtCore import QModelIndex, Qt +from vorta import config +from vorta.i18n import translate from vorta.utils import borg_compat from vorta.views.extract_dialog import ExtractTree, FileData from vorta.views.partials.treemodel import FileSystemItem, path_to_str @@ -19,9 +21,17 @@ def started_event(self): def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) - self.app.backup_progress_event.emit( - f"[{self.params['profile_name']}] {self.tr('Restored files from archive.')}" - ) + if result['returncode'] != 0: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] " + + translate( + 'BorgExtractJob', 'Extract command has failed. See the logs for details.' + ).format(config.LOG_DIR.as_uri()) + ) + else: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] {self.tr('Restored files from archive.')}" + ) @classmethod def prepare(cls, profile, archive_name, model: ExtractTree, destination_folder): diff --git a/src/vorta/borg/info_archive.py b/src/vorta/borg/info_archive.py index afb94b2f3..763f6e9da 100644 --- a/src/vorta/borg/info_archive.py +++ b/src/vorta/borg/info_archive.py @@ -1,3 +1,5 @@ +from vorta import config +from vorta.i18n import translate from vorta.store.models import ArchiveModel, RepoModel from vorta.utils import borg_compat @@ -12,7 +14,17 @@ def started_event(self): def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) - self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Refreshing archive done.')}") + if result['returncode'] != 0: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] " + + translate( + 'BorgInfoArchiveJob', 'Info command has failed. See the logs for details.' + ).format(config.LOG_DIR.as_uri()) + ) + else: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] {self.tr('Refreshing archive done.')}" + ) @classmethod def prepare(cls, profile, archive_name): diff --git a/src/vorta/borg/mount.py b/src/vorta/borg/mount.py index 64e99a4ab..24086caa8 100644 --- a/src/vorta/borg/mount.py +++ b/src/vorta/borg/mount.py @@ -1,6 +1,8 @@ import logging import os +from vorta import config +from vorta.i18n import translate from vorta.store.models import SettingsModel from vorta.utils import SHELL_PATTERN_ELEMENT, borg_compat @@ -13,6 +15,21 @@ class BorgMountJob(BorgJob): def started_event(self): self.updated.emit(self.tr('Mounting archive into folder…')) + def finished_event(self, result): + self.app.backup_finished_event.emit(result) + self.result.emit(result) + if result['returncode'] != 0: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] " + + translate( + 'BorgMountJob', 'Mount command has failed. See the logs for details.' + ).format(config.LOG_DIR.as_uri()) + ) + else: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] {self.tr('Restored files from archive.')}" + ) + @classmethod def prepare(cls, profile, archive: str = None): ret = super().prepare(profile) diff --git a/src/vorta/borg/prune.py b/src/vorta/borg/prune.py index aba888fb3..fdf82677e 100644 --- a/src/vorta/borg/prune.py +++ b/src/vorta/borg/prune.py @@ -1,3 +1,5 @@ +from vorta import config +from vorta.i18n import translate from vorta.store.models import RepoModel from vorta.utils import borg_compat, format_archive_name @@ -21,6 +23,15 @@ def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Pruning done.')}") + if result['returncode'] != 0: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] " + + translate( + 'BorgPruneJob', 'Prune command has failed. See the logs for details.' + ).format(config.LOG_DIR.as_uri()) + ) + else: + self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Pruning completed.')}") @classmethod def prepare(cls, profile): diff --git a/src/vorta/scheduler.py b/src/vorta/scheduler.py index 9ff19681c..a90f1a5ba 100644 --- a/src/vorta/scheduler.py +++ b/src/vorta/scheduler.py @@ -348,7 +348,7 @@ def set_timer_for_profile(self, profile_id: int): else: # int to big to pass it to qt which expects a c++ int # wait 15 min for regular reschedule - logger.debug(f"Couldn't schedule for {next_time} because " f"timer value {timer_ms} too large.") + logger.debug(f"Couldn't schedule for {next_time} because timer value {timer_ms} too large.") self.timers[profile_id] = { 'dt': next_time, diff --git a/src/vorta/store/settings.py b/src/vorta/store/settings.py index 03a1cc543..6f0e3ee54 100644 --- a/src/vorta/store/settings.py +++ b/src/vorta/store/settings.py @@ -177,7 +177,7 @@ def get_misc_settings() -> List[Dict[str, str]]: 'type': 'checkbox', 'label': trans_late( 'settings', - "If the system tray isn't available, " "ask whether to continue in the background " "on exit", + "If the system tray isn't available, ask whether to continue in the background on exit", ), }, { From 88c0c181d9b2050e709146296e11fb03df70158c Mon Sep 17 00:00:00 2001 From: Ahmed Hossam Date: Thu, 20 Mar 2025 02:53:02 +0200 Subject: [PATCH 4/7] modify messages --- src/vorta/borg/mount.py | 4 ---- src/vorta/borg/prune.py | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/vorta/borg/mount.py b/src/vorta/borg/mount.py index 24086caa8..b8d53dcdd 100644 --- a/src/vorta/borg/mount.py +++ b/src/vorta/borg/mount.py @@ -25,10 +25,6 @@ def finished_event(self, result): 'BorgMountJob', 'Mount command has failed. See the logs for details.' ).format(config.LOG_DIR.as_uri()) ) - else: - self.app.backup_progress_event.emit( - f"[{self.params['profile_name']}] {self.tr('Restored files from archive.')}" - ) @classmethod def prepare(cls, profile, archive: str = None): diff --git a/src/vorta/borg/prune.py b/src/vorta/borg/prune.py index fdf82677e..aa48e4200 100644 --- a/src/vorta/borg/prune.py +++ b/src/vorta/borg/prune.py @@ -22,7 +22,6 @@ def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) - self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Pruning done.')}") if result['returncode'] != 0: self.app.backup_progress_event.emit( f"[{self.params['profile_name']}] " @@ -31,7 +30,7 @@ def finished_event(self, result): ).format(config.LOG_DIR.as_uri()) ) else: - self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Pruning completed.')}") + self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Pruning done.')}") @classmethod def prepare(cls, profile): From a4df1b50790e2a213f495e2c7ac074806d7b6a56 Mon Sep 17 00:00:00 2001 From: Ahmed Hossam Date: Thu, 20 Mar 2025 02:59:25 +0200 Subject: [PATCH 5/7] add finished_event for umount --- src/vorta/borg/umount.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/vorta/borg/umount.py b/src/vorta/borg/umount.py index cdec6e25d..f8d3934cd 100644 --- a/src/vorta/borg/umount.py +++ b/src/vorta/borg/umount.py @@ -2,6 +2,9 @@ import psutil +from vorta import config +from vorta.i18n import translate + from ..i18n import trans_late from .borg_job import BorgJob @@ -10,6 +13,17 @@ class BorgUmountJob(BorgJob): def started_event(self): self.updated.emit(self.tr('Unmounting archive…')) + def finished_event(self, result): + self.app.backup_finished_event.emit(result) + self.result.emit(result) + if result['returncode'] != 0: + self.app.backup_progress_event.emit( + f"[{self.params['profile_name']}] " + + translate( + 'BorgMountJob', 'Umount command has failed. See the logs for details.' + ).format(config.LOG_DIR.as_uri()) + ) + @classmethod def prepare(cls, profile, mount_point, archive_name=None): ret = super().prepare(profile) From f1b2db37c4baf5a3929bc6c597ca27dc0569f536 Mon Sep 17 00:00:00 2001 From: Ahmed Hossam Date: Thu, 20 Mar 2025 21:25:10 +0200 Subject: [PATCH 6/7] fix mount and umount test --- src/vorta/borg/mount.py | 1 - src/vorta/borg/umount.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/vorta/borg/mount.py b/src/vorta/borg/mount.py index b8d53dcdd..7e3b235dd 100644 --- a/src/vorta/borg/mount.py +++ b/src/vorta/borg/mount.py @@ -16,7 +16,6 @@ def started_event(self): self.updated.emit(self.tr('Mounting archive into folder…')) def finished_event(self, result): - self.app.backup_finished_event.emit(result) self.result.emit(result) if result['returncode'] != 0: self.app.backup_progress_event.emit( diff --git a/src/vorta/borg/umount.py b/src/vorta/borg/umount.py index f8d3934cd..7f6d8ebe0 100644 --- a/src/vorta/borg/umount.py +++ b/src/vorta/borg/umount.py @@ -14,7 +14,6 @@ def started_event(self): self.updated.emit(self.tr('Unmounting archive…')) def finished_event(self, result): - self.app.backup_finished_event.emit(result) self.result.emit(result) if result['returncode'] != 0: self.app.backup_progress_event.emit( From ec5e020a973bba8c7e47e7c29e63746cb71513f1 Mon Sep 17 00:00:00 2001 From: Ahmed Hossam Date: Thu, 20 Mar 2025 21:43:56 +0200 Subject: [PATCH 7/7] make error_msg appear only if returncode != 0 --- src/vorta/borg/borg_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vorta/borg/borg_job.py b/src/vorta/borg/borg_job.py index 10a463d16..e3f215642 100644 --- a/src/vorta/borg/borg_job.py +++ b/src/vorta/borg/borg_job.py @@ -317,7 +317,7 @@ def read_async(fd): stdout.append(read_async(p.stdout)) break - if error_msg: + if error_msg and self.process.returncode != 0: self.app.error_signal.emit(error_msg) result = {