Beispiel #1
0
    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"))
Beispiel #2
0
    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
Beispiel #3
0
    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
Beispiel #4
0
 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()
Beispiel #5
0
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 ""
Beispiel #6
0
    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
Beispiel #7
0
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 ""
Beispiel #8
0
    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()
Beispiel #9
0
 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)
Beispiel #10
0
    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
Beispiel #11
0
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
Beispiel #12
0
    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"))
Beispiel #13
0
    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())
Beispiel #14
0
    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
Beispiel #15
0
    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
Beispiel #16
0
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
Beispiel #17
0
 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.')
Beispiel #18
0
    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
Beispiel #19
0
    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
Beispiel #20
0
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