Add helper class to manage Borg version and supported features. (#250)

* Add helper class to manage Borg version and supported features. Fixes #205

* Add setuptools as dependency to ensure pkg_resources is available.

* Review fixes, check for ZStd support, refactor some relative imports.

* Repo-add dialog: Disable Blake2 options if not available.

* When disabling compression algos, select by value, not index.
This commit is contained in:
Manuel Riel 2019-04-14 14:40:29 +08:00 committed by GitHub
parent 15aebc6ceb
commit b9486d003c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 79 additions and 32 deletions

View File

@ -39,6 +39,7 @@ install_requires =
apscheduler
psutil
qdarkstyle
setuptools
secretstorage; sys_platform != 'darwin'
pyobjc-core; sys_platform == 'darwin'
pyobjc-framework-Cocoa; sys_platform == 'darwin'

View File

@ -10,7 +10,7 @@ from .models import BackupProfileModel, SettingsModel
from .qt_single_application import QtSingleApplication
from .scheduler import VortaScheduler
from .tray_menu import TrayMenu
from .utils import parse_args, set_tray_icon
from .utils import parse_args, set_tray_icon, borg_compat
from .views.main_window import MainWindow
from vorta.borg.version import BorgVersionThread
@ -30,7 +30,6 @@ class VortaApp(QtSingleApplication):
backup_finished_event = QtCore.pyqtSignal(dict)
backup_cancelled_event = QtCore.pyqtSignal()
backup_log_event = QtCore.pyqtSignal(str)
borg_details = {'version': '0.0', 'path': 'borg'}
def __init__(self, args_raw, single_app=False):
@ -112,7 +111,7 @@ class VortaApp(QtSingleApplication):
thread.start()
def set_borg_details_result(self, result):
self.borg_details['version'] = result['data']['version']
self.borg_details['path'] = result['data']['path']
borg_compat.set_version(result['data']['version'], result['data']['path'])
if self._main_window_exists():
self.main_window.miscTab.set_borg_details(self.borg_details)
self.main_window.miscTab.set_borg_details(borg_compat.version, borg_compat.path)
self.main_window.repoTab.toggle_available_compression()

View File

@ -0,0 +1,25 @@
from pkg_resources import parse_version
MIN_BORG_FOR_FEATURE = {
'BLAKE2': parse_version('1.1.4'),
'ZSTD': parse_version('1.1.4'),
# add new version-checks here.
}
class BorgCompatibility:
"""
An internal class that keeps details of the Borg version
in use and allows checking for specific features. Could be used
to customize Borg commands by version in the future.
"""
version = '0.0'
path = ''
def set_version(self, version, path):
self.version = version
self.path = path
def check(self, feature_name):
return parse_version(self.version) >= MIN_BORG_FOR_FEATURE[feature_name]

View File

@ -3,9 +3,9 @@ import tempfile
from dateutil import parser
import subprocess
from ..i18n import trans_late
from ..utils import get_current_wifi, format_archive_name
from ..models import SourceFileModel, ArchiveModel, WifiSettingModel, RepoModel
from vorta.i18n import trans_late
from vorta.utils import get_current_wifi, format_archive_name, borg_compat
from vorta.models import SourceFileModel, ArchiveModel, WifiSettingModel, RepoModel
from .borg_thread import BorgThread
@ -97,6 +97,10 @@ class BorgCreateThread(BorgThread):
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', '--info', '--log-json', '--json', '--filter=AM', '-C', profile.compression]
# Add excludes

View File

@ -22,10 +22,13 @@ from PyQt5.QtGui import QIcon
from PyQt5 import QtCore
from vorta.keyring.abc import VortaKeyring
from vorta.log import logger
from vorta.borg._compatibility import BorgCompatibility
keyring = VortaKeyring.get_keyring()
logger.info('Using %s Keyring implementation.', keyring.__class__.__name__)
borg_compat = BorgCompatibility()
def nested_dict():
"""

View File

@ -2,17 +2,17 @@ import sys
from PyQt5.QtWidgets import QShortcut
from PyQt5 import uic, QtCore
from PyQt5.QtGui import QKeySequence
from vorta.utils import get_asset, borg_compat
from vorta.models import BackupProfileModel
from vorta.borg.borg_thread import BorgThread
from vorta.views.utils import get_theme_class
from .repo_tab import RepoTab
from .source_tab import SourceTab
from .archive_tab import ArchiveTab
from .schedule_tab import ScheduleTab
from .misc_tab import MiscTab
from .profile_add_edit_dialog import AddProfileWindow, EditProfileWindow
from ..utils import get_asset
from ..models import BackupProfileModel
from vorta.borg.borg_thread import BorgThread
from vorta.views.utils import get_theme_class
uifile = get_asset('UI/mainwindow.ui')
MainWindowUI, MainWindowBase = uic.loadUiType(uifile, from_imports=True, import_from=get_theme_class())
@ -34,7 +34,7 @@ class MainWindow(MainWindowBase, MainWindowUI):
self.archiveTab = ArchiveTab(self.archiveTabSlot)
self.scheduleTab = ScheduleTab(self.scheduleTabSlot)
self.miscTab = MiscTab(self.miscTabSlot)
self.miscTab.set_borg_details(self.app.borg_details)
self.miscTab.set_borg_details(borg_compat.version, borg_compat.path)
self.tabWidget.setCurrentIndex(0)
self.repoTab.repo_changed.connect(self.archiveTab.populate_from_profile)

View File

@ -39,6 +39,6 @@ class MiscTab(MiscTabBase, MiscTabUI, BackupProfileMixin):
if key == 'autostart':
open_app_at_startup(new_value)
def set_borg_details(self, borg_details):
self.borgVersion.setText(borg_details['version'])
self.borgPath.setText(borg_details['path'])
def set_borg_details(self, version, path):
self.borgVersion.setText(version)
self.borgPath.setText(path)

View File

@ -1,7 +1,7 @@
import re
from PyQt5 import uic
from ..utils import get_private_keys, get_asset, choose_file_dialog
from vorta.utils import get_private_keys, get_asset, choose_file_dialog, borg_compat
from vorta.borg.init import BorgInitThread
from vorta.borg.info import BorgInfoThread
from vorta.views.utils import get_theme_class
@ -85,16 +85,21 @@ class AddRepoWindow(AddRepoBase, AddRepoUI):
self._set_status(self.tr('Unable to add your repository.'))
def init_encryption(self):
self.encryptionComboBox.addItem(self.tr('Repokey-Blake2 (Recommended, key stored in repository)'),
'repokey-blake2')
self.encryptionComboBox.addItem(self.tr('Repokey'),
'repokey')
self.encryptionComboBox.addItem(self.tr('Keyfile-Blake2 (Key stored in home directory)'),
'keyfile-blake2')
self.encryptionComboBox.addItem(self.tr('Keyfile'),
'keyfile')
self.encryptionComboBox.addItem(self.tr('None (not recommended)'),
'none')
encryption_algos = [
['Repokey-Blake2 (Recommended, key stored in repository)', 'repokey-blake2'],
['Repokey', 'repokey'],
['Keyfile-Blake2 (Key stored in home directory)', 'keyfile-blake2'],
['Keyfile', 'keyfile'],
['None (not recommended)', 'none']
]
for desc, name in encryption_algos:
self.encryptionComboBox.addItem(self.tr(desc), name)
if not borg_compat.check('BLAKE2'):
self.encryptionComboBox.model().item(0).setEnabled(False)
self.encryptionComboBox.model().item(2).setEnabled(False)
self.encryptionComboBox.setCurrentIndex(1)
def init_ssh_key(self):
keys = get_private_keys()

View File

@ -2,11 +2,11 @@ import os
from PyQt5 import uic, QtCore
from PyQt5.QtWidgets import QApplication, QMessageBox
from ..models import RepoModel, ArchiveModel, BackupProfileMixin
from .repo_add_dialog import AddRepoWindow, ExistingRepoWindow
from ..utils import pretty_bytes, get_private_keys, get_asset
from vorta.models import RepoModel, ArchiveModel, BackupProfileMixin
from vorta.utils import pretty_bytes, get_private_keys, get_asset, borg_compat
from .ssh_dialog import SSHAddWindow
from vorta.views.utils import get_theme_class
from .repo_add_dialog import AddRepoWindow, ExistingRepoWindow
from .utils import get_theme_class
uifile = get_asset('UI/repotab.ui')
RepoUI, RepoBase = uic.loadUiType(uifile, from_imports=True, import_from=get_theme_class())
@ -39,6 +39,7 @@ class RepoTab(RepoBase, RepoUI, BackupProfileMixin):
self.repoCompression.addItem(self.tr('LZ4 (modern, default)'), 'lz4')
self.repoCompression.addItem(self.tr('Zstandard Level 3 (modern)'), 'zstd,3')
self.repoCompression.addItem(self.tr('Zstandard Level 8 (modern)'), 'zstd,8')
# zlib and lzma come from python stdlib and are there (and in borg) since long.
# but maybe not much reason to start with these nowadays, considering zstd supports
# a very wide range of compression levels and has great speed. if speed is more
@ -48,6 +49,9 @@ class RepoTab(RepoBase, RepoUI, BackupProfileMixin):
self.repoCompression.addItem(self.tr('No Compression'), 'none')
self.repoCompression.currentIndexChanged.connect(self.compression_select_action)
self.toggle_available_compression()
self.repoCompression.currentIndexChanged.connect(self.compression_select_action)
self.init_ssh()
self.sshComboBox.currentIndexChanged.connect(self.ssh_select_action)
self.sshKeyToClipboardButton.clicked.connect(self.ssh_copy_to_clipboard_action)
@ -88,6 +92,12 @@ class RepoTab(RepoBase, RepoUI, BackupProfileMixin):
for key in keys:
self.sshComboBox.addItem(f'{key["filename"]} ({key["format"]})', key['filename'])
def toggle_available_compression(self):
use_zstd = borg_compat.check('ZSTD')
for algo in ['zstd,3', 'zstd,8']:
ix = self.repoCompression.findData(algo)
self.repoCompression.model().item(ix).setEnabled(use_zstd)
def ssh_select_action(self, index):
if index == 1:
ssh_add_window = SSHAddWindow()