def delete_action(self): # Since this function modify the UI, we can't put the whole function in a JobQUeue. params = BorgDeleteJob.prepare(self.profile()) if not params['ok']: self._set_status(params['message']) return self.archive_name = self.selected_archive_name() if self.archive_name is not None: if not self.confirm_dialog( trans_late('ArchiveTab', "Confirm deletion"), trans_late( 'ArchiveTab', "Are you sure you want to delete the archive?")): return params['cmd'][-1] += f'::{self.archive_name}' job = BorgDeleteJob(params['cmd'], params, self.profile().repo.id) job.updated.connect(self._set_status) job.result.connect(self.delete_result) self._toggle_all_buttons(False) self.app.jobs_manager.add_job(job) else: self._set_status(self.tr("No archive selected"))
def prepare(cls, profile): """ Prepare for running Borg. This function in the base class should be called from all subclasses and calls that define their own `cmd`. The `prepare()` step does these things: - validate if all conditions to run command are met - build borg command `prepare()` is run 2x. First at the global level and then for each subcommand. :return: dict(ok: book, message: str) """ ret = {'ok': False} # Do checks to see if running Borg is possible. if cls.is_running(): ret['message'] = trans_late('messages', 'Backup is already in progress.') return ret if cls.prepare_bin() is None: ret['message'] = trans_late('messages', 'Borg binary was not found.') return ret if profile.repo is None: ret['message'] = trans_late('messages', 'Add a backup repository first.') return ret # Try to get password from chosen keyring backend. logger.debug("Using %s keyring to store passwords.", keyring.__class__.__name__) ret['password'] = keyring.get_password('vorta-repo', profile.repo.url) # Try to fall back to DB Keyring, if we use the system keychain. if ret['password'] is None and keyring.is_primary: logger.debug( 'Password not found in primary keyring. Falling back to VortaDBKeyring.' ) ret['password'] = VortaDBKeyring().get_password( 'vorta-repo', profile.repo.url) # Give warning and continue if password is found there. if ret['password'] is not None: logger.warning( 'Found password in database, but secure storage was available. ' 'Consider re-adding the repo to use it.') ret['ssh_key'] = profile.ssh_key ret['repo_id'] = profile.repo.id ret['repo_url'] = profile.repo.url ret['extra_borg_arguments'] = profile.repo.extra_borg_arguments ret['profile_name'] = profile.name ret['ok'] = True return ret
def __init__(self, cmd, params, site="default"): """ Thread to run Borg operations in. :param cmd: Borg command line :param params: Pass options that were used to build cmd and may be needed to process the result. :param site: For scheduler. Only one job can run per site at one time. Site is usually the repository ID, or 'default' for misc Borg commands. """ super().__init__() self.site_id = site self.app = QApplication.instance() # Declare labels here for translation self.category_label = {"files": trans_late("BorgJob", "Files"), "original": trans_late("BorgJob", "Original"), "deduplicated": trans_late("BorgJob", "Deduplicated"), "compressed": trans_late("BorgJob", "Compressed"), } cmd[0] = self.prepare_bin() # Add extra Borg args to command. Never pass None. extra_args_str = params.get('extra_borg_arguments') if extra_args_str is not None and len(extra_args_str) > 0: extra_args = shlex.split(extra_args_str) cmd = cmd[:2] + extra_args + cmd[2:] env = os.environ.copy() env['BORG_HOSTNAME_IS_UNIQUE'] = '1' env['BORG_RELOCATED_REPO_ACCESS_IS_OK'] = '1' env['BORG_RSH'] = 'ssh' if 'additional_env' in params: env = {**env, **params['additional_env']} password = params.get('password') if password is not None: env['BORG_PASSPHRASE'] = password else: env['BORG_PASSPHRASE'] = '9999999' # Set dummy password to avoid prompt. if env.get('BORG_PASSCOMMAND', False): env.pop('BORG_PASSPHRASE', None) # Unset passphrase ssh_key = params.get('ssh_key') if ssh_key is not None: ssh_key_path = os.path.expanduser(f'~/.ssh/{ssh_key}') env['BORG_RSH'] += f' -i {ssh_key_path}' self.env = env self.cmd = cmd self.cwd = params.get('cwd', None) self.params = params self.process = None
def closeEvent(self, event): if not is_system_tray_available() and not self.tests_running: run_in_background = QMessageBox.question( self, trans_late("MainWindow QMessagebox", "Quit"), trans_late("MainWindow QMessagebox", "Should Vorta continue to run in the background?"), QMessageBox.Yes | QMessageBox.No) if run_in_background == QMessageBox.No: self.app.quit() event.accept()
def display_password_backend(encryption): ''' Display password backend message based off current keyring ''' # flake8: noqa E501 if encryption != 'none': keyring = VortaKeyring.get_keyring() return trans_late( 'utils', "Storing the password in your password manager." ) if keyring.is_primary else trans_late( 'utils', 'Saving the password to disk. To store password more securely install a supported secret store such as KeepassXC' ) else: return ""
def prepare(cls, profile): """ Prepare for running Borg. This function in the base class should be called from all subclasses and calls that define their own `cmd`. The `prepare()` step does these things: - validate if all conditions to run command are met - build borg command `prepare()` is run 2x. First at the global level and then for each subcommand. :return: dict(ok: book, message: str) """ ret = {'ok': False} # Do checks to see if running Borg is possible. if cls.is_running(): ret['message'] = trans_late('messages', 'Backup is already in progress.') return ret if cls.prepare_bin() is None: ret['message'] = trans_late('messages', 'Borg binary was not found.') return ret if profile.repo is None: ret['message'] = trans_late('messages', 'Add a backup repository first.') return ret # Try to get password from chosen keyring backend. try: ret['password'] = keyring.get_password("vorta-repo", profile.repo.url) except Exception: ret['message'] = trans_late( 'messages', 'Please make sure you grant Vorta permission to use the Keychain.' ) return ret ret['ssh_key'] = profile.ssh_key ret['repo_id'] = profile.repo.id ret['repo_url'] = profile.repo.url ret['profile_name'] = profile.name ret['ok'] = True return ret
def validate_passwords(first_pass, second_pass): ''' Validates the password for borg, do not use on single fields ''' pass_equal = first_pass == second_pass pass_long = len(first_pass) > 8 if not pass_long and not pass_equal: return trans_late( 'utils', "Passwords must be identical and greater than 8 characters long.") if not pass_equal: return trans_late('utils', "Passwords must be identical.") if not pass_long: return trans_late('utils', "Passwords must be greater than 8 characters long.") return ""
def closeEvent(self, event): # Save window state in SettingsModel SettingsModel.update({SettingsModel.str_value: str(self.width())})\ .where(SettingsModel.key == 'previous_window_width')\ .execute() SettingsModel.update({SettingsModel.str_value: str(self.height())})\ .where(SettingsModel.key == 'previous_window_height')\ .execute() if not is_system_tray_available(): run_in_background = QMessageBox.question( self, trans_late("MainWindow QMessagebox", "Quit"), trans_late("MainWindow QMessagebox", "Should Vorta continue to run in the background?"), QMessageBox.Yes | QMessageBox.No) if run_in_background == QMessageBox.No: self.app.quit() event.accept()
def exception_handler(type, value, tb): from traceback import format_exception from PyQt5.QtWidgets import QMessageBox logger.critical( "Uncaught exception, file a report at https://github.com/borgbase/vorta/issues/new", exc_info=(type, value, tb)) full_exception = ''.join(format_exception(type, value, tb)) title = trans_late('app', 'Fatal Error') error_message = trans_late( 'app', 'Uncaught exception, please file a report with this text at\n' 'https://github.com/borgbase/vorta/issues/new\n') if app: QMessageBox.critical( None, translate('app', title), translate('app', error_message) + full_exception) else: # Crashed before app startup, cannot translate sys.exit(1)
def prepare(cls): ret = {'ok': False} if cls.prepare_bin() is None: ret['message'] = trans_late('messages', 'Borg binary was not found.') return ret ret['cmd'] = ['borg', '--version'] ret['ok'] = True return ret
def get_misc_settings(): # Default settings for all platforms. settings = [ { 'key': 'use_light_icon', 'value': False, 'type': 'checkbox', 'label': trans_late('settings', 'Use light system tray icon (applies after restart)') }, { 'key': 'use_dark_theme', 'value': False, 'type': 'checkbox', 'label': trans_late('settings', 'Use dark theme (applies after restart)') }, { 'key': 'enable_notifications', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Display notifications when background tasks fail') }, { 'key': 'enable_notifications_success', 'value': False, 'type': 'checkbox', 'label': trans_late('settings', 'Also notify about successful background tasks') }, { 'key': 'autostart', 'value': False, 'type': 'checkbox', 'label': trans_late('settings', 'Automatically start Vorta at login') }, { 'key': 'foreground', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Open main window on startup') }, ] if sys.platform == 'darwin': settings += [ { 'key': 'check_for_updates', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Check for updates on startup') }, { 'key': 'updates_include_beta', 'value': False, 'type': 'checkbox', 'label': trans_late('settings', 'Include pre-release versions when checking for updates') }, ] return settings
def delete_action(self): params = BorgDeleteThread.prepare(self.profile()) if not params['ok']: self._set_status(params['message']) return archive_name = self.selected_archive_name() if archive_name is not None: if not self.confirm_dialog(trans_late('ArchiveTab', "Confirm deletion"), trans_late('ArchiveTab', "Are you sure you want to delete the archive?")): return params['cmd'][-1] += f'::{archive_name}' thread = BorgDeleteThread(params['cmd'], params, parent=self) thread.updated.connect(self._set_status) thread.result.connect(self.delete_result) self._toggle_all_buttons(False) thread.start() else: self._set_status(self.tr("No archive selected"))
def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.edited_profile = None self.buttonBox.rejected.connect(self.close) self.buttonBox.accepted.connect(self.save) self.profileNameField.textChanged.connect(self.button_validation) self.buttonBox.button(QDialogButtonBox.Save).setText(self.tr("Save")) self.buttonBox.button(QDialogButtonBox.Cancel).setText( self.tr("Cancel")) self.name_blank = trans_late('AddProfileWindow', 'Please enter a profile name.') self.name_exists = trans_late( 'AddProfileWindow', 'A profile with this name already exists.') # Call validate to set inital messages self.buttonBox.button(QDialogButtonBox.Save).setEnabled( self.validate())
def prepare(cls, profile): ret = super().prepare(profile) if not ret['ok']: return ret else: ret['ok'] = False # Set back to false, so we can do our own checks here. if not borg_compat.check('COMPACT_SUBCOMMAND'): ret['ok'] = False ret['message'] = trans_late( 'messages', 'This feature needs Borg 1.2.0 or higher.') return ret cmd = ['borg', '--info', '--log-json', 'compact', '--cleanup-commits'] cmd.append(f'{profile.repo.url}') ret['ok'] = True ret['cmd'] = cmd return ret
def prepare(cls, params): """ Used to validate existing repository when added. """ # Build fake profile because we don't have it in the DB yet. Assume unencrypted. profile = FakeProfile( 999, FakeRepo(params['repo_url'], 999, params['extra_borg_arguments'], 'none'), 'New Repo', params['ssh_key']) ret = super().prepare(profile) if not ret['ok']: return ret else: ret['ok'] = False # Set back to false, so we can do our own checks here. cmd = ["borg", "info", "--info", "--json", "--log-json"] cmd.append(profile.repo.url) ret['additional_env'] = { 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': "yes", 'BORG_RSH': 'ssh -oStrictHostKeyChecking=no' } ret['password'] = params[ 'password'] # Empty password is '', which disables prompt if params['password'] != '': # Cannot tell if repo has encryption, assuming based off of password if not cls.keyring.is_unlocked: ret['message'] = trans_late( 'messages', 'Please unlock your password manager.') return ret ret['ok'] = True ret['cmd'] = cmd return ret
def get_misc_settings(): # Default settings for all platforms. settings = [ { 'key': 'enable_notifications', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Display notifications when background tasks fail') }, { 'key': 'enable_notifications_success', 'value': False, 'type': 'checkbox', 'label': trans_late('settings', 'Also notify about successful background tasks') }, { 'key': 'autostart', 'value': False, 'type': 'checkbox', 'label': trans_late('settings', 'Automatically start Vorta at login') }, { 'key': 'foreground', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Open main window on startup') }, { 'key': 'get_srcpath_datasize', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Get statistics of file/folder when added') }, { 'key': 'previous_profile_id', 'str_value': '1', 'type': 'internal', 'label': 'Previously selected profile' }, { 'key': 'previous_window_width', 'str_value': '800', 'type': 'internal', 'label': 'Previous window width' }, { 'key': 'previous_window_height', 'str_value': '600', 'type': 'internal', 'label': 'Previous window height' }, ] if sys.platform == 'darwin': settings += [ { 'key': 'check_for_updates', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Check for updates on startup') }, { 'key': 'updates_include_beta', 'value': False, 'type': 'checkbox', 'label': trans_late( 'settings', 'Include pre-release versions when checking for updates') }, ] return settings
def get_backend_warning(self): if self.is_system: return trans_late('utils', 'Storing password in your password manager.') else: return trans_late('utils', 'Saving password with Vorta settings.')
def prepare(cls, profile): """ Prepare for running Borg. This function in the base class should be called from all subclasses and calls that define their own `cmd`. The `prepare()` step does these things: - validate if all conditions to run command are met - build borg command `prepare()` is run 2x. First at the global level and then for each subcommand. :return: dict(ok: book, message: str) """ ret = {'ok': False} if cls.prepare_bin() is None: ret['message'] = trans_late('messages', 'Borg binary was not found.') return ret if profile.repo is None: ret['message'] = trans_late('messages', 'Add a backup repository first.') return ret if not borg_compat.check('JSON_LOG'): ret['message'] = trans_late('messages', 'Your Borg version is too old. >=1.1.0 is required.') return ret # Try to get password from chosen keyring backend. with keyring_lock: cls.keyring = VortaKeyring.get_keyring() logger.debug("Using %s keyring to store passwords.", cls.keyring.__class__.__name__) ret['password'] = cls.keyring.get_password('vorta-repo', profile.repo.url) # Check if keyring is locked if profile.repo.encryption != 'none' and not cls.keyring.is_unlocked: ret['message'] = trans_late('messages', 'Please unlock your system password manager or disable it under Misc') return ret # Try to fall back to DB Keyring, if we use the system keychain. if ret['password'] is None and cls.keyring.is_system: logger.debug('Password not found in primary keyring. Falling back to VortaDBKeyring.') ret['password'] = VortaDBKeyring().get_password('vorta-repo', profile.repo.url) # Give warning and continue if password is found there. if ret['password'] is not None: logger.warning('Found password in database, but secure storage was available. ' 'Consider re-adding the repo to use it.') # Password is required for encryption, cannot continue if ret['password'] is None and not isinstance(profile.repo, FakeRepo) and profile.repo.encryption != 'none': ret['message'] = trans_late( 'messages', "Your repo passphrase was stored in a password manager which is no longer available.\n" "Try unlinking and re-adding your repo.") return ret ret['ssh_key'] = profile.ssh_key ret['repo_id'] = profile.repo.id ret['repo_url'] = profile.repo.url ret['extra_borg_arguments'] = profile.repo.extra_borg_arguments ret['profile_name'] = profile.name ret['profile_id'] = profile.id ret['ok'] = True return ret
def prepare(cls, profile): """ `borg create` is called from different places and needs some preparation. Centralize it here and return the required arguments to the caller. """ ret = super().prepare(profile) if not ret['ok']: return ret else: ret['ok'] = False # Set back to False, so we can do our own checks here. n_backup_folders = SourceFileModel.select().count() if n_backup_folders == 0: ret['message'] = trans_late('messages', 'Add some folders to back up first.') return ret network_status_monitor = get_network_status_monitor() current_wifi = network_status_monitor.get_current_wifi() if current_wifi is not None: wifi_is_disallowed = WifiSettingModel.select().where( (WifiSettingModel.ssid == current_wifi) & ( WifiSettingModel.allowed == False # noqa ) & (WifiSettingModel.profile == profile)) if wifi_is_disallowed.count() > 0 and profile.repo.is_remote_repo( ): ret['message'] = trans_late('messages', 'Current Wifi is not allowed.') return ret if profile.repo.is_remote_repo() and profile.dont_run_on_metered_networks \ and network_status_monitor.is_network_metered(): ret['message'] = trans_late( 'messages', 'Not running backup over metered connection.') return ret ret['profile'] = profile ret['repo'] = profile.repo # Run user-supplied pre-backup command if cls.pre_post_backup_cmd(ret) != 0: ret['message'] = trans_late( 'messages', 'Pre-backup command returned non-zero exit code.') return ret if not profile.repo.is_remote_repo() and not os.path.exists( profile.repo.url): ret['message'] = trans_late('messages', 'Repo folder not mounted or moved.') return ret if 'zstd' in profile.compression and not borg_compat.check('ZSTD'): ret['message'] = trans_late( 'messages', 'Your current Borg version does not support ZStd compression.') return ret cmd = [ 'borg', 'create', '--list', '--progress', '--info', '--log-json', '--json', '--filter=AM', '-C', profile.compression, ] # Add excludes # Partly inspired by borgmatic/borgmatic/borg/create.py if profile.exclude_patterns is not None: exclude_dirs = [] for p in profile.exclude_patterns.split('\n'): if p.strip(): expanded_directory = os.path.expanduser(p.strip()) exclude_dirs.append(expanded_directory) if exclude_dirs: pattern_file = tempfile.NamedTemporaryFile('w', delete=False) pattern_file.write('\n'.join(exclude_dirs)) pattern_file.flush() cmd.extend(['--exclude-from', pattern_file.name]) if profile.exclude_if_present is not None: for f in profile.exclude_if_present.split('\n'): if f.strip(): cmd.extend(['--exclude-if-present', f.strip()]) # Add repo url and source dirs. new_archive_name = format_archive_name(profile, profile.new_archive_name) cmd.append(f"{profile.repo.url}::{new_archive_name}") for f in SourceFileModel.select().where( SourceFileModel.profile == profile.id): cmd.append(f.dir) ret['message'] = trans_late('messages', 'Starting backup...') ret['ok'] = True ret['cmd'] = cmd return ret
def get_misc_settings(): ''' Global settings that apply per platform ''' # Default settings for all platforms. settings = [ { 'key': 'enable_notifications', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Display notifications when background tasks fail') }, { 'key': 'enable_notifications_success', 'value': False, 'type': 'checkbox', 'label': trans_late('settings', 'Also notify about successful background tasks') }, { 'key': 'autostart', 'value': False, 'type': 'checkbox', 'label': trans_late('settings', 'Automatically start Vorta at login') }, { 'key': 'foreground', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Open main window on startup') }, { 'key': 'get_srcpath_datasize', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Get statistics of file/folder when added') }, { 'key': 'use_system_keyring', 'value': True, 'type': 'checkbox', 'label': trans_late( 'settings', 'Store repository passwords in system keychain, if available.') }, { 'key': 'override_mount_permissions', 'value': False, 'type': 'checkbox', 'label': trans_late( 'settings', 'Try to replace existing permissions when mounting an archive.' ) }, { 'key': 'previous_profile_id', 'str_value': '1', 'type': 'internal', 'label': 'Previously selected profile' }, { 'key': 'previous_window_width', 'str_value': '800', 'type': 'internal', 'label': 'Previous window width' }, { 'key': 'previous_window_height', 'str_value': '600', 'type': 'internal', 'label': 'Previous window height' }, ] if sys.platform == 'darwin': settings += [ { 'key': 'check_for_updates', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Check for updates on startup') }, { 'key': 'updates_include_beta', 'value': False, 'type': 'checkbox', 'label': trans_late( 'settings', 'Include pre-release versions when checking for updates') }, ] else: settings += [{ 'key': 'enable_background_question', 'value': True, 'type': 'checkbox', 'label': trans_late('settings', 'Display background exit dialog') }, { 'key': 'disable_background_state', 'value': False, 'type': 'internal', 'label': 'Previous background exit button state' }] return settings