def __init__(self, args_raw, single_app=False): super().__init__(APP_ID, args_raw) args = parse_args() if self.isRunning(): if single_app: self.sendMessage("open main window") print( 'An instance of Vorta is already running. Opening main window.' ) sys.exit() elif args.profile: self.sendMessage(f"create {args.profile}") print('Creating backup using existing Vorta instance.') sys.exit() elif args.profile: sys.exit('Vorta must already be running for --create to work') init_translations(self) self.setQuitOnLastWindowClosed(False) self.jobs_manager = JobsManager() self.scheduler = VortaScheduler() self.setApplicationName("Vorta") # Import profile from ~/.vorta-init.json or add empty "Default" profile. self.bootstrap_profile() # Prepare tray and main window self.tray = TrayMenu(self) self.main_window = MainWindow(self) if getattr(args, 'daemonize', False): pass elif SettingsModel.get(key='foreground').value: self.open_main_window_action() self.backup_started_event.connect(self.backup_started_event_response) self.backup_finished_event.connect(self.backup_finished_event_response) self.backup_cancelled_event.connect( self.backup_cancelled_event_response) self.message_received_event.connect( self.message_received_event_response) 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.set_borg_details_action() if sys.platform == 'darwin': self.check_darwin_permissions() self.installEventFilter(self)
def __init__(self, args_raw, single_app=False): super().__init__(APP_ID, args_raw) if self.isRunning() and single_app: self.sendMessage("open main window") print('An instance of Vorta is already running. Opening main window.') sys.exit() init_translations(self) self.setQuitOnLastWindowClosed(False) self.scheduler = VortaScheduler(self) # Prepare system tray icon self.tray = TrayMenu(self) args = parse_args() if getattr(args, 'daemonize', False): pass elif SettingsModel.get(key='foreground').value: self.open_main_window_action() self.backup_started_event.connect(self.backup_started_event_response) self.backup_finished_event.connect(self.backup_finished_event_response) self.backup_cancelled_event.connect(self.backup_cancelled_event_response) self.message_received_event.connect(self.message_received_event_response) self.set_borg_details_action() self.installEventFilter(self)
def test_fixed(clockmock, passed_time, scheduled, now, hour, minute): """Test scheduling in fixed mode.""" # setup scheduler = VortaScheduler() time = dt(2020, 5, 4, 0, 0) + now clockmock.now.return_value = time profile = BackupProfileModel.get(name=PROFILE_NAME) profile.schedule_make_up_missed = False profile.schedule_mode = FIXED_SCHEDULE profile.schedule_fixed_hour = hour profile.schedule_fixed_minute = minute profile.save() last_time = time - passed_time event = EventLogModel(subcommand='create', profile=profile.id, returncode=0, category='scheduled' if scheduled else '', start_time=last_time, end_time=last_time) event.save() # run test expected = time.replace(hour=hour, minute=minute) if time >= expected or last_time.date() == expected.date(): expected += td(days=1) scheduler.set_timer_for_profile(profile.id) assert scheduler.timers[profile.id]['dt'] == expected
def test_simple_schedule(clockmock): """Test a simple scheduling including `next_job` and `remove_job`.""" scheduler = VortaScheduler() # setup clockmock.now.return_value = dt(2020, 5, 6, 4, 30) profile = BackupProfileModel.get(name=PROFILE_NAME) profile.schedule_make_up_missed = False profile.schedule_mode = INTERVAL_SCHEDULE profile.schedule_interval_unit = 'hours' profile.schedule_interval_count = 3 profile.save() # test set timer and next_job scheduler.set_timer_for_profile(profile.id) assert len(scheduler.timers) == 1 assert scheduler.next_job() == '07:30 ({})'.format(PROFILE_NAME) assert scheduler.next_job_for_profile(profile.id) == '2020-05-06 07:30' # test remove_job and next_job scheduler.remove_job(profile.id) assert len(scheduler.timers) == 0 assert scheduler.next_job() == 'None scheduled' assert scheduler.next_job_for_profile(profile.id) == 'None scheduled'
def test_interval(clockmock, passed_time, scheduled, now, unit, count, added_time): """Test scheduling in interval mode.""" # setup scheduler = VortaScheduler() time = dt(2020, 5, 4, 0, 0) + now clockmock.now.return_value = time profile = BackupProfileModel.get(name=PROFILE_NAME) profile.schedule_make_up_missed = False profile.schedule_mode = INTERVAL_SCHEDULE profile.schedule_interval_unit = unit profile.schedule_interval_count = count profile.save() event = EventLogModel(subcommand='create', profile=profile.id, returncode=0, category='scheduled' if scheduled else '', start_time=time - passed_time, end_time=time - passed_time) event.save() # run test scheduler.set_timer_for_profile(profile.id) assert scheduler.timers[profile.id]['dt'] == time + added_time
def test_manual_mode(): """Test scheduling in manual mode.""" scheduler = VortaScheduler() # setup model profile = BackupProfileModel.get(name=PROFILE_NAME) profile.schedule_make_up_missed = False profile.schedule_mode = MANUAL_SCHEDULE profile.save() # test scheduler.set_timer_for_profile(profile.id) assert len(scheduler.timers) == 0
def test_first_fixed(clockmock, now, hour, minute, added_time): """Test scheduling in fixed mode without a previous backup.""" # setup scheduler = VortaScheduler() time = dt(2020, 5, 4, 0, 0) + now clockmock.now.return_value = time profile = BackupProfileModel.get(name=PROFILE_NAME) profile.schedule_make_up_missed = False profile.schedule_mode = FIXED_SCHEDULE profile.schedule_fixed_hour = hour profile.schedule_fixed_minute = minute profile.save() # run test scheduler.set_timer_for_profile(profile.id) assert scheduler.timers[profile.id]['dt'] == time + added_time
def test_first_interval(clockmock, now, unit, count, added_time): """Test scheduling in interval mode without a previous backup.""" # setup scheduler = VortaScheduler() time = dt(2020, 5, 4, 0, 0) + now clockmock.now.return_value = time profile = BackupProfileModel.get(name=PROFILE_NAME) profile.schedule_make_up_missed = False profile.schedule_mode = INTERVAL_SCHEDULE profile.schedule_interval_unit = unit profile.schedule_interval_count = count profile.save() # run test scheduler.set_timer_for_profile(profile.id) assert scheduler.timers[profile.id]['dt'] == time + added_time
def __init__(self, args_raw, single_app=False): super().__init__(APP_ID, args_raw) args = parse_args() if self.isRunning(): if single_app: self.sendMessage("open main window") print( 'An instance of Vorta is already running. Opening main window.' ) sys.exit() elif args.profile: self.sendMessage(f"create {args.profile}") print('Creating backup using existing Vorta instance.') sys.exit() elif args.profile: sys.exit('Vorta must already be running for --create to work') init_translations(self) self.setQuitOnLastWindowClosed(False) self.scheduler = VortaScheduler(self) self.setApplicationName("Vorta") # Prepare tray and main window self.tray = TrayMenu(self) self.main_window = MainWindow(self) if getattr(args, 'daemonize', False): pass elif SettingsModel.get(key='foreground').value: self.open_main_window_action() self.backup_started_event.connect(self.backup_started_event_response) self.backup_finished_event.connect(self.backup_finished_event_response) self.backup_cancelled_event.connect( self.backup_cancelled_event_response) self.message_received_event.connect( self.message_received_event_response) self.aboutToQuit.connect(cleanup_db) self.set_borg_details_action() self.installEventFilter(self)
class VortaApp(QtSingleApplication): """ All windows and QWidgets are children of this app. When running Borg-commands, the class `BorgThread` will emit events via the `VortaApp` class to which other windows will subscribe to. """ backup_started_event = QtCore.pyqtSignal() backup_finished_event = QtCore.pyqtSignal(dict) backup_cancelled_event = QtCore.pyqtSignal() backup_log_event = QtCore.pyqtSignal(str, dict) backup_progress_event = QtCore.pyqtSignal(str) def __init__(self, args_raw, single_app=False): super().__init__(APP_ID, args_raw) args = parse_args() if self.isRunning(): if single_app: self.sendMessage("open main window") print( 'An instance of Vorta is already running. Opening main window.' ) sys.exit() elif args.profile: self.sendMessage(f"create {args.profile}") print('Creating backup using existing Vorta instance.') sys.exit() elif args.profile: sys.exit('Vorta must already be running for --create to work') init_translations(self) self.setQuitOnLastWindowClosed(False) self.scheduler = VortaScheduler(self) self.setApplicationName("Vorta") # Prepare tray and main window self.tray = TrayMenu(self) self.main_window = MainWindow(self) if getattr(args, 'daemonize', False): pass elif SettingsModel.get(key='foreground').value: self.open_main_window_action() self.backup_started_event.connect(self.backup_started_event_response) self.backup_finished_event.connect(self.backup_finished_event_response) self.backup_cancelled_event.connect( self.backup_cancelled_event_response) self.message_received_event.connect( self.message_received_event_response) self.backup_log_event.connect(self.react_to_log) self.aboutToQuit.connect(self.quit_app_action) self.set_borg_details_action() self.installEventFilter(self) def create_backups_cmdline(self, profile_name): profile = BackupProfileModel.get_or_none(name=profile_name) if profile is not None: if profile.repo is None: logger.warning(f"Add a repository to {profile_name}") # Wait a bit in case something is running while BorgThread.is_running(): time.sleep(0.1) self.create_backup_action(profile_id=profile.id) else: logger.warning(f"Invalid profile name {profile_name}") def eventFilter(self, source, event): if event.type( ) == QtCore.QEvent.ApplicationPaletteChange and isinstance( source, MainWindow): self.main_window.set_icons() self.main_window.repoTab.set_icons() self.main_window.archiveTab.set_icons() self.main_window.scheduleTab.set_icons() if event.type( ) == QtCore.QEvent.ApplicationPaletteChange and source == self.tray.contextMenu( ): self.tray.set_tray_icon() return False def quit_app_action(self): self.backup_cancelled_event.emit() self.scheduler.shutdown() del self.main_window self.tray.deleteLater() del self.tray cleanup_db() def create_backup_action(self, profile_id=None): if not profile_id: profile_id = self.main_window.current_profile.id profile = BackupProfileModel.get(id=profile_id) msg = BorgCreateThread.prepare(profile) if msg['ok']: thread = BorgCreateThread(msg['cmd'], msg, parent=self) thread.start() else: notifier = VortaNotifications.pick() notifier.deliver(self.tr('Vorta Backup'), translate('messages', msg['message']), level='error') self.backup_progress_event.emit( translate('messages', msg['message'])) def open_main_window_action(self): self.main_window.show() self.main_window.raise_() self.main_window.activateWindow() def toggle_main_window_visibility(self): if self.main_window.isVisible(): self.main_window.close() else: self.open_main_window_action() def backup_started_event_response(self): self.tray.set_tray_icon(active=True) def backup_finished_event_response(self): self.tray.set_tray_icon() def backup_cancelled_event_response(self): self.tray.set_tray_icon() def message_received_event_response(self, message): if message == "open main window": self.open_main_window_action() elif message.startswith("create"): message = message[7:] # Remove create if BorgThread.is_running(): logger.warning("Cannot run while backups are already running") else: self.create_backups_cmdline(message) def set_borg_details_action(self): params = BorgVersionThread.prepare() if not params['ok']: self._alert_missing_borg() return thread = BorgVersionThread(params['cmd'], params, parent=self) thread.result.connect(self.set_borg_details_result) thread.start() def set_borg_details_result(self, result): """ Receive result from BorgVersionThread. If no valid version was found, display an error. """ if 'version' in result['data']: borg_compat.set_version(result['data']['version'], result['data']['path']) self.main_window.miscTab.set_borg_details(borg_compat.version, borg_compat.path) self.main_window.repoTab.toggle_available_compression() else: self._alert_missing_borg() def _alert_missing_borg(self): msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText(self.tr("No Borg Binary Found")) msg.setInformativeText( self.tr("Vorta was unable to locate a usable Borg Backup binary.")) msg.setStandardButtons(QMessageBox.Ok) msg.exec() def react_to_log(self, mgs, context): """ Trigger Vorta actions based on Borg logs. E.g. repo lock. """ msgid = context.get('msgid') if msgid == 'LockTimeout': profile = BackupProfileModel.get(name=context['profile_name']) repo_url = context.get('repo_url') msg = QMessageBox() msg.setWindowTitle(self.tr("Repository In Use")) msg.setIcon(QMessageBox.Critical) abortButton = msg.addButton(self.tr("Abort"), QMessageBox.RejectRole) msg.addButton(self.tr("Continue"), QMessageBox.AcceptRole) msg.setDefaultButton(abortButton) msg.setText( self. tr(f"The repository at {repo_url} might be in use elsewhere.")) msg.setInformativeText( self. tr("Only break the lock if you are certain no other Borg process " "on any machine is accessing the repository. Abort or break the lock?" )) msg.accepted.connect(lambda: self.break_lock(profile)) self._msg = msg msg.show() elif msgid == 'LockFailed': repo_url = context.get('repo_url') msg = QMessageBox() msg.setText( self. tr(f"You do not have permission to access the repository at {repo_url}. Gain access and try again." )) # noqa: E501 msg.setWindowTitle(self.tr("No Repository Permissions")) self._msg = msg msg.show() def break_lock(self, profile): params = BorgBreakThread.prepare(profile) if not params['ok']: self.set_progress(params['message']) return thread = BorgBreakThread(params['cmd'], params, parent=self) thread.start()
class VortaApp(QtSingleApplication): """ All windows and QWidgets are children of this app. When running Borg-commands, the class `BorgJob` will emit events via the `VortaApp` class to which other windows will subscribe to. """ backup_started_event = QtCore.pyqtSignal() backup_finished_event = QtCore.pyqtSignal(dict) backup_cancelled_event = QtCore.pyqtSignal() backup_log_event = QtCore.pyqtSignal(str, dict) backup_progress_event = QtCore.pyqtSignal(str) check_failed_event = QtCore.pyqtSignal(dict) def __init__(self, args_raw, single_app=False): super().__init__(APP_ID, args_raw) args = parse_args() if self.isRunning(): if single_app: self.sendMessage("open main window") print( 'An instance of Vorta is already running. Opening main window.' ) sys.exit() elif args.profile: self.sendMessage(f"create {args.profile}") print('Creating backup using existing Vorta instance.') sys.exit() elif args.profile: sys.exit('Vorta must already be running for --create to work') init_translations(self) self.setQuitOnLastWindowClosed(False) self.jobs_manager = JobsManager() self.scheduler = VortaScheduler() self.setApplicationName("Vorta") # Import profile from ~/.vorta-init.json or add empty "Default" profile. self.bootstrap_profile() # Prepare tray and main window self.tray = TrayMenu(self) self.main_window = MainWindow(self) if getattr(args, 'daemonize', False): pass elif SettingsModel.get(key='foreground').value: self.open_main_window_action() self.backup_started_event.connect(self.backup_started_event_response) self.backup_finished_event.connect(self.backup_finished_event_response) self.backup_cancelled_event.connect( self.backup_cancelled_event_response) self.message_received_event.connect( self.message_received_event_response) 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.set_borg_details_action() if sys.platform == 'darwin': self.check_darwin_permissions() self.installEventFilter(self) def create_backups_cmdline(self, profile_name): profile = BackupProfileModel.get_or_none(name=profile_name) if profile is not None: if profile.repo is None: logger.warning(f"Add a repository to {profile_name}") self.create_backup_action(profile_id=profile.id) else: logger.warning(f"Invalid profile name {profile_name}") def eventFilter(self, source, event): if event.type( ) == QtCore.QEvent.ApplicationPaletteChange and isinstance( source, MainWindow): self.main_window.set_icons() self.main_window.repoTab.set_icons() self.main_window.archiveTab.set_icons() self.main_window.scheduleTab.set_icons() if event.type( ) == QtCore.QEvent.ApplicationPaletteChange and source == self.tray.contextMenu( ): self.tray.set_tray_icon() return False def quit_app_action(self): self.backup_cancelled_event.emit() del self.main_window self.tray.deleteLater() del self.tray cleanup_db() def create_backup_action(self, profile_id=None): if not profile_id: profile_id = self.main_window.current_profile.id profile = BackupProfileModel.get(id=profile_id) msg = BorgCreateJob.prepare(profile) if msg['ok']: job = BorgCreateJob(msg['cmd'], msg, profile.repo.id) self.jobs_manager.add_job(job) else: notifier = VortaNotifications.pick() notifier.deliver(self.tr('Vorta Backup'), translate('messages', msg['message']), level='error') self.backup_progress_event.emit( translate('messages', msg['message'])) return None def open_main_window_action(self): self.main_window.show() self.main_window.raise_() self.main_window.activateWindow() def toggle_main_window_visibility(self): if self.main_window.isVisible(): self.main_window.close() else: self.open_main_window_action() def backup_started_event_response(self): self.tray.set_tray_icon(active=True) def backup_finished_event_response(self): if not self.jobs_manager.is_worker_running(): self.tray.set_tray_icon() def backup_cancelled_event_response(self): self.jobs_manager.cancel_all_jobs() self.tray.set_tray_icon() def message_received_event_response(self, message): if message == "open main window": self.open_main_window_action() elif message.startswith("create"): message = message[7:] # Remove create if self.jobs_manager.is_worker_running(): logger.warning("Cannot run while backups are already running") else: self.create_backups_cmdline(message) # 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() if not params['ok']: self._alert_missing_borg() return job = BorgVersionJob(params['cmd'], params) job.result.connect(self.set_borg_details_result) self.jobs_manager.add_job(job) def set_borg_details_result(self, result): """ Receive result from BorgVersionJob. If no valid version was found, display an error. """ if 'version' in result['data']: borg_compat.set_version(result['data']['version'], result['data']['path']) self.main_window.miscTab.set_borg_details(borg_compat.version, borg_compat.path) self.main_window.repoTab.toggle_available_compression() self.scheduler.reload_all_timers( ) # Start timer after Borg version is set. else: self._alert_missing_borg() def _alert_missing_borg(self): msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText(self.tr("No Borg Binary Found")) msg.setInformativeText( self.tr("Vorta was unable to locate a usable Borg Backup binary.")) msg.setStandardButtons(QMessageBox.Ok) msg.exec() def check_darwin_permissions(self): """ macOS restricts access to certain folders by default. For some folders, the user will get a prompt (e.g. Documents, Downloads), while others will cause file access errors. This function tries reading a file that is known to be restricted and warn the user about incomplete backups. """ test_path = os.path.expanduser('~/Library/Cookies') if os.path.exists(test_path) and not os.access(test_path, os.R_OK): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) msg.setText( self.tr("Vorta needs Full Disk Access for complete Backups")) msg.setInformativeText( self. tr("Without this, some files won't be accessible and you may end up with an incomplete " "backup. Please set <b>Full Disk Access</b> permission for Vorta in " "<a href='x-apple.systempreferences:com.apple.preference.security?Privacy'>" "System Preferences > Security & Privacy</a>.")) msg.setStandardButtons(QMessageBox.Ok) msg.exec() def react_to_log(self, mgs, context): """ Trigger Vorta actions based on Borg logs. E.g. repo lock. """ msgid = context.get('msgid') if msgid == 'LockTimeout': profile = BackupProfileModel.get(name=context['profile_name']) repo_url = context.get('repo_url') msg = QMessageBox() msg.setWindowTitle(self.tr("Repository In Use")) msg.setIcon(QMessageBox.Critical) abortButton = msg.addButton(self.tr("Abort"), QMessageBox.RejectRole) msg.addButton(self.tr("Continue"), QMessageBox.AcceptRole) msg.setDefaultButton(abortButton) msg.setText( self. tr(f"The repository at {repo_url} might be in use elsewhere.")) msg.setInformativeText( self. tr("Only break the lock if you are certain no other Borg process " "on any machine is accessing the repository. Abort or break the lock?" )) msg.accepted.connect(lambda: self.break_lock(profile)) self._msg = msg msg.show() elif msgid == 'LockFailed': repo_url = context.get('repo_url') msg = QMessageBox() msg.setText( self. tr(f"You do not have permission to access the repository at {repo_url}. Gain access and try again." )) # noqa: E501 msg.setWindowTitle(self.tr("No Repository Permissions")) self._msg = msg msg.show() def break_lock(self, profile): params = BorgBreakJob.prepare(profile) if not params['ok']: self.backup_progress_event.emit(params['message']) return job = BorgBreakJob(params['cmd'], params) self.jobs_manager.add_job(job) def bootstrap_profile(self, bootstrap_file=PROFILE_BOOTSTRAP_FILE): """ Make sure there is at least one profile when first starting Vorta. Will either import a profile placed in ~/.vorta-init.json or add an empty "Default" profile. """ if bootstrap_file.is_file(): try: profile_export = ProfileExport.from_json(bootstrap_file) profile = profile_export.to_db(overwrite_profile=True, overwrite_settings=True) except Exception as exception: double_newline = os.linesep + os.linesep QMessageBox.critical( None, self.tr('Failed to import profile'), "{}{}\"{}\"{}{}".format( self.tr('Failed to import a profile from {}:').format( bootstrap_file), double_newline, str(exception), double_newline, self.tr('Consider removing or repairing this file to ' 'get rid of this message.'), )) return bootstrap_file.unlink() notifier = VortaNotifications.pick() notifier.deliver(self.tr('Profile import successful!'), self.tr('Profile {} imported.').format( profile.name), level='info') logger.info('Profile {} imported.'.format(profile.name)) if BackupProfileModel.select().count() == 0: default_profile = BackupProfileModel(name='Default') default_profile.save() def check_failed_response(self, result: Dict[str, Any]): """ Process the signal that a repo consistency check failed. Displays a `QMessageBox` with an error message depending on the return code of the `BorgJob`. Parameters ---------- repo_url : str The url of the repo of concern """ # extract data from the params for the borg job repo_url = result['params']['repo_url'] returncode = result['returncode'] errors: List[Tuple[int, str]] = result['errors'] error_message = errors[0][1] if errors else '' # Switch over returncodes if returncode == 0: # No fail logger.warning( 'VortaApp.check_failed_response was called with returncode 0') elif returncode == 130: # Keyboard interupt pass else: # Real error # Create QMessageBox msg = QMessageBox() msg.setIcon(QMessageBox.Icon.Critical) # changed for warning msg.setStandardButtons(QMessageBox.Ok) msg.setWindowTitle(self.tr('Repo Check Failed')) if returncode == 1: # warning msg.setIcon(QMessageBox.Icon.Warning) text = self.tr( 'Borg exited with a warning message. See logs for details.' ) infotext = error_message elif returncode > 128: # 128+N - killed by signal N (e.g. 137 == kill -9) signal = returncode - 128 text = (self.tr( 'Repository data check for repo was killed by signal %s.') % (signal)) infotext = self.tr( 'The process running the check job got a kill signal. Try again.' ) else: # Real error text = (self.tr( 'Repository data check for repo %s failed. Error code %s') % (repo_url, returncode)) infotext = error_message + '\n' infotext += self.tr( 'Consider repairing or recreating the repository soon to avoid missing data.' ) msg.setText(text) msg.setInformativeText(infotext) # Display messagebox msg.exec()
class VortaApp(QtSingleApplication): """ All windows and QWidgets are children of this app. When running Borg-commands, the class `BorgThread` will emit events via the `VortaApp` class to which other windows will subscribe to. """ backup_started_event = QtCore.pyqtSignal() backup_finished_event = QtCore.pyqtSignal(dict) backup_cancelled_event = QtCore.pyqtSignal() backup_log_event = QtCore.pyqtSignal(str, dict) backup_progress_event = QtCore.pyqtSignal(str) def __init__(self, args_raw, single_app=False): super().__init__(APP_ID, args_raw) args = parse_args() if self.isRunning(): if single_app: self.sendMessage("open main window") print( 'An instance of Vorta is already running. Opening main window.' ) sys.exit() elif args.profile: self.sendMessage(f"create {args.profile}") print('Creating backup using existing Vorta instance.') sys.exit() elif args.profile: sys.exit('Vorta must already be running for --create to work') init_translations(self) self.setQuitOnLastWindowClosed(False) self.scheduler = VortaScheduler(self) self.setApplicationName("Vorta") # Import profile from ~/.vorta-init.json or add empty "Default" profile. self.bootstrap_profile() # Prepare tray and main window self.tray = TrayMenu(self) self.main_window = MainWindow(self) if getattr(args, 'daemonize', False): pass elif SettingsModel.get(key='foreground').value: self.open_main_window_action() self.backup_started_event.connect(self.backup_started_event_response) self.backup_finished_event.connect(self.backup_finished_event_response) self.backup_cancelled_event.connect( self.backup_cancelled_event_response) self.message_received_event.connect( self.message_received_event_response) self.backup_log_event.connect(self.react_to_log) self.aboutToQuit.connect(self.quit_app_action) self.set_borg_details_action() if sys.platform == 'darwin': self.check_darwin_permissions() self.installEventFilter(self) def create_backups_cmdline(self, profile_name): profile = BackupProfileModel.get_or_none(name=profile_name) if profile is not None: if profile.repo is None: logger.warning(f"Add a repository to {profile_name}") # Wait a bit in case something is running while BorgThread.is_running(): time.sleep(0.1) self.create_backup_action(profile_id=profile.id) else: logger.warning(f"Invalid profile name {profile_name}") def eventFilter(self, source, event): if event.type( ) == QtCore.QEvent.ApplicationPaletteChange and isinstance( source, MainWindow): self.main_window.set_icons() self.main_window.repoTab.set_icons() self.main_window.archiveTab.set_icons() self.main_window.scheduleTab.set_icons() if event.type( ) == QtCore.QEvent.ApplicationPaletteChange and source == self.tray.contextMenu( ): self.tray.set_tray_icon() return False def quit_app_action(self): self.backup_cancelled_event.emit() self.scheduler.shutdown() del self.main_window self.tray.deleteLater() del self.tray cleanup_db() def create_backup_action(self, profile_id=None): if not profile_id: profile_id = self.main_window.current_profile.id profile = BackupProfileModel.get(id=profile_id) msg = BorgCreateThread.prepare(profile) if msg['ok']: thread = BorgCreateThread(msg['cmd'], msg, parent=self) thread.start() else: notifier = VortaNotifications.pick() notifier.deliver(self.tr('Vorta Backup'), translate('messages', msg['message']), level='error') self.backup_progress_event.emit( translate('messages', msg['message'])) def open_main_window_action(self): self.main_window.show() self.main_window.raise_() self.main_window.activateWindow() def toggle_main_window_visibility(self): if self.main_window.isVisible(): self.main_window.close() else: self.open_main_window_action() def backup_started_event_response(self): self.tray.set_tray_icon(active=True) def backup_finished_event_response(self): self.tray.set_tray_icon() def backup_cancelled_event_response(self): self.tray.set_tray_icon() def message_received_event_response(self, message): if message == "open main window": self.open_main_window_action() elif message.startswith("create"): message = message[7:] # Remove create if BorgThread.is_running(): logger.warning("Cannot run while backups are already running") else: self.create_backups_cmdline(message) def set_borg_details_action(self): params = BorgVersionThread.prepare() if not params['ok']: self._alert_missing_borg() return thread = BorgVersionThread(params['cmd'], params, parent=self) thread.result.connect(self.set_borg_details_result) thread.start() def set_borg_details_result(self, result): """ Receive result from BorgVersionThread. If no valid version was found, display an error. """ if 'version' in result['data']: borg_compat.set_version(result['data']['version'], result['data']['path']) self.main_window.miscTab.set_borg_details(borg_compat.version, borg_compat.path) self.main_window.repoTab.toggle_available_compression() else: self._alert_missing_borg() def _alert_missing_borg(self): msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText(self.tr("No Borg Binary Found")) msg.setInformativeText( self.tr("Vorta was unable to locate a usable Borg Backup binary.")) msg.setStandardButtons(QMessageBox.Ok) msg.exec() def check_darwin_permissions(self): """ macOS restricts access to certain folders by default. For some folders, the user will get a prompt (e.g. Documents, Downloads), while others will cause file access errors. This function tries reading a file that is known to be restricted and warn the user about incomplete backups. """ test_path = os.path.expanduser('~/Library/Cookies') if os.path.exists(test_path) and not os.access(test_path, os.R_OK): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse) msg.setText( self.tr("Vorta needs Full Disk Access for complete Backups")) msg.setInformativeText( self. tr("Without this, some files won't be accessible and you may end up with an incomplete " "backup. Please set <b>Full Disk Access</b> permission for Vorta in " "<a href='x-apple.systempreferences:com.apple.preference.security?Privacy'>" "System Preferences > Security & Privacy</a>.")) msg.setStandardButtons(QMessageBox.Ok) msg.exec() def react_to_log(self, mgs, context): """ Trigger Vorta actions based on Borg logs. E.g. repo lock. """ msgid = context.get('msgid') if msgid == 'LockTimeout': profile = BackupProfileModel.get(name=context['profile_name']) repo_url = context.get('repo_url') msg = QMessageBox() msg.setWindowTitle(self.tr("Repository In Use")) msg.setIcon(QMessageBox.Critical) abortButton = msg.addButton(self.tr("Abort"), QMessageBox.RejectRole) msg.addButton(self.tr("Continue"), QMessageBox.AcceptRole) msg.setDefaultButton(abortButton) msg.setText( self. tr(f"The repository at {repo_url} might be in use elsewhere.")) msg.setInformativeText( self. tr("Only break the lock if you are certain no other Borg process " "on any machine is accessing the repository. Abort or break the lock?" )) msg.accepted.connect(lambda: self.break_lock(profile)) self._msg = msg msg.show() elif msgid == 'LockFailed': repo_url = context.get('repo_url') msg = QMessageBox() msg.setText( self. tr(f"You do not have permission to access the repository at {repo_url}. Gain access and try again." )) # noqa: E501 msg.setWindowTitle(self.tr("No Repository Permissions")) self._msg = msg msg.show() def break_lock(self, profile): params = BorgBreakThread.prepare(profile) if not params['ok']: self.backup_progress_event.emit(params['message']) return thread = BorgBreakThread(params['cmd'], params, parent=self) thread.start() def bootstrap_profile(self, bootstrap_file=PROFILE_BOOTSTRAP_FILE): """ Make sure there is at least one profile when first starting Vorta. Will either import a profile placed in ~/.vorta-init.json or add an empty "Default" profile. """ if bootstrap_file.is_file(): profile_export = ProfileExport.from_json(bootstrap_file) profile = profile_export.to_db(overwrite_profile=True, overwrite_settings=True) bootstrap_file.unlink() notifier = VortaNotifications.pick() notifier.deliver(self.tr('Profile import successful!'), self.tr('Profile {} imported.').format( profile.name), level='info') logger.info('Profile {} imported.'.format(profile.name)) if BackupProfileModel.select().count() == 0: default_profile = BackupProfileModel(name='Default') default_profile.save()