diff --git a/src/vorta/application.py b/src/vorta/application.py index d87830d91..1bd8a50ef 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() @@ -289,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 588ff2dd9..e3f215642 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) @@ -299,16 +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. - 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 and self.process.returncode != 0: + self.app.error_signal.emit(error_msg) + result = { 'params': self.params, 'returncode': self.process.returncode, 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 5b1fe72f0..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,7 +24,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('Archive deleted.')}") + if result['returncode'] != 0: + self.app.backup_progress_event.emit( + 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..7e3b235dd 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,16 @@ class BorgMountJob(BorgJob): def started_event(self): self.updated.emit(self.tr('Mounting archive into folder…')) + def finished_event(self, 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()) + ) + @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..aa48e4200 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 @@ -20,7 +22,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 done.')}") @classmethod def prepare(cls, profile): diff --git a/src/vorta/borg/umount.py b/src/vorta/borg/umount.py index cdec6e25d..7f6d8ebe0 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,16 @@ class BorgUmountJob(BorgJob): def started_event(self): self.updated.emit(self.tr('Unmounting archive…')) + def finished_event(self, 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) 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", ), }, {