1
0
Fork 0
mirror of https://github.com/borgbase/vorta synced 2025-03-09 13:53:26 +00:00
* Apply selection status to children in restore-dialog. Fixes #69

* Backup triggered from system tray wont use correct profile. Fixes #78

* Use the term FILE if both FILE or FOLDER is meant. Fixes #55

* Implement update check on/off setting for macOS

* Implement autostart setting for macOS. Fixes #56
This commit is contained in:
Manuel Riel 2018-12-05 17:05:47 +08:00 committed by GitHub
parent 0f203101e7
commit 3c519e4928
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 131 additions and 84 deletions

View file

@ -57,6 +57,13 @@
<item>
<widget class="QTreeView" name="treeView"/>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Note: If you select a top-level folder and deselect its children, they will still be restored.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">

View file

@ -24,7 +24,7 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0">
<item>
<widget class="QListWidget" name="sourceDirectoriesWidget">
<widget class="QListWidget" name="sourceFilesWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>

View file

@ -44,6 +44,7 @@ class BorgThread(QtCore.QThread, BackupProfileMixin):
env = os.environ.copy()
env['BORG_HOSTNAME_IS_UNIQUE'] = '1'
env['BORG_RELOCATED_REPO_ACCESS_IS_OK'] = '1'
password = params.get('password')
if password is not None:
env['BORG_PASSPHRASE'] = password

View file

@ -5,7 +5,7 @@ from dateutil import parser
from datetime import datetime as dt
from ..utils import get_current_wifi
from ..models import SourceDirModel, ArchiveModel, WifiSettingModel, RepoModel
from ..models import SourceFileModel, ArchiveModel, WifiSettingModel, RepoModel
from .borg_thread import BorgThread
@ -56,7 +56,7 @@ class BorgCreateThread(BorgThread):
else:
ret['ok'] = False # Set back to false, so we can do our own checks here.
n_backup_folders = SourceDirModel.select().count()
n_backup_folders = SourceFileModel.select().count()
if n_backup_folders == 0:
ret['message'] = 'Add some folders to back up first.'
return ret
@ -105,7 +105,7 @@ class BorgCreateThread(BorgThread):
# Add repo url and source dirs.
cmd.append(f"{profile.repo.url}::{platform.node()}-{profile.slug()}-{dt.now().isoformat(timespec='seconds')}")
for f in SourceDirModel.select().where(SourceDirModel.profile == profile.id):
for f in SourceFileModel.select().where(SourceFileModel.profile == profile.id):
cmd.append(f.dir)
ret['message'] = 'Starting backup..'

View file

@ -91,7 +91,7 @@ class BackupProfileModel(pw.Model):
database = db
class SourceDirModel(pw.Model):
class SourceFileModel(pw.Model):
"""A folder to be backed up, related to a Backup Configuration."""
dir = pw.CharField()
profile = pw.ForeignKeyField(BackupProfileModel, default=1)
@ -99,6 +99,7 @@ class SourceDirModel(pw.Model):
class Meta:
database = db
table_name = 'sourcedirmodel'
class ArchiveModel(pw.Model):
@ -181,7 +182,7 @@ def _apply_schema_update(current_schema, version_after, *operations):
def init_db(con):
db.initialize(con)
db.connect()
db.create_tables([RepoModel, RepoPassword, BackupProfileModel, SourceDirModel, SettingsModel,
db.create_tables([RepoModel, RepoPassword, BackupProfileModel, SourceFileModel, SettingsModel,
ArchiveModel, WifiSettingModel, EventLogModel, SchemaVersion])
if BackupProfileModel.select().count() == 0:
@ -201,6 +202,8 @@ def init_db(con):
'label': 'Display notifications when background tasks fail.'},
{'key': 'check_for_updates', 'value': True, 'type': 'checkbox',
'label': 'Check for updates on startup.'},
{'key': 'updates_include_beta', 'value': False, 'type': 'checkbox',
'label': 'Include pre-release versions when checking for updates.'},
]
for setting in settings: # Create missing settings and update labels.
@ -242,7 +245,7 @@ def init_db(con):
if current_schema.version < 7:
_apply_schema_update(
current_schema, 7,
migrator.rename_column(SourceDirModel._meta.table_name, 'config_id', 'profile_id'),
migrator.rename_column(SourceFileModel._meta.table_name, 'config_id', 'profile_id'),
migrator.drop_column(EventLogModel._meta.table_name, 'profile_id'),
migrator.add_column(EventLogModel._meta.table_name, 'profile', pw.CharField(null=True))
)

View file

@ -39,12 +39,11 @@ class TrayMenu(QSystemTrayIcon):
profile_menu = menu.addMenu('Backup Now')
for profile in profiles:
new_item = profile_menu.addAction(profile.name)
new_item.setData(profile.id)
new_item.triggered.connect(lambda profile_id=profile.id: self.app.create_backup_action(profile_id))
new_item.triggered.connect(lambda state, i=profile.id: self.app.create_backup_action(i))
else:
profile = profiles.first()
profile_menu = menu.addAction('Backup Now')
profile_menu.triggered.connect(lambda profile_id=profile.id: self.app.create_backup_action(profile_id))
profile_menu.triggered.connect(lambda state, i=profile.id: self.app.create_backup_action(i))
settings_action = menu.addAction("Settings")
settings_action.triggered.connect(self.app.open_main_window_action)

View file

@ -1,5 +1,6 @@
import sys
import os
from vorta.models import SettingsModel
def get_updater():
@ -10,9 +11,14 @@ def get_updater():
bundle_path = os.path.join(os.path.dirname(sys.executable), os.pardir, 'Frameworks', 'Sparkle.framework')
loadBundle('Sparkle', globals(), bundle_path)
sparkle = SUUpdater.sharedUpdater() # noqa: F821
sparkle.setAutomaticallyChecksForUpdates_(True)
if SettingsModel.get(key='updates_include_beta').value:
sparkle.SharedUpdater.FeedURL = 'https://borgbase.github.io/vorta/appcast-pre.xml'
if SettingsModel.get(key='check_for_updates').value:
sparkle.setAutomaticallyChecksForUpdates_(True)
sparkle.checkForUpdatesInBackground()
sparkle.setAutomaticallyDownloadsUpdates_(False)
sparkle.checkForUpdatesInBackground()
return sparkle
else: # TODO: implement for Linux

View file

@ -79,7 +79,7 @@ def get_dict_from_list(dataDict, mapList):
return reduce(operator.getitem, mapList, dataDict)
def choose_folder_dialog(parent, title, want_folder=True):
def choose_file_dialog(parent, title, want_folder=True):
options = QFileDialog.Options()
if want_folder:
options |= QFileDialog.ShowDirsOnly
@ -219,3 +219,23 @@ def set_tray_icon(tray, active=False):
icon_name = f"icons/hdd-o{'-active' if active else ''}-{'light' if use_light_style else 'dark'}.png"
icon = QIcon(get_asset(icon_name))
tray.setIcon(icon)
def open_app_at_startup(enabled=True):
if sys.platform == 'darwin':
# From https://stackoverflow.com/questions/26213884/cocoa-add-app-to-startup-in-sandbox-using-pyobjc
from Foundation import NSDictionary
from Cocoa import NSBundle, NSURL
from CoreFoundation import kCFAllocatorDefault
from LaunchServices import (LSSharedFileListCreate, kLSSharedFileListSessionLoginItems,
LSSharedFileListInsertItemURL, kLSSharedFileListItemHidden,
kLSSharedFileListItemLast, LSSharedFileListItemRemove)
app_path = NSBundle.mainBundle().bundlePath()
url = NSURL.alloc().initFileURLWithPath_(app_path)
login_items = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, None)
props = NSDictionary.dictionaryWithObject_forKey_(True, kLSSharedFileListItemHidden)
new_item = LSSharedFileListInsertItemURL(login_items, kLSSharedFileListItemLast,
None, None, url, props, None)
if not enabled:
LSSharedFileListItemRemove(login_items, new_item)

View file

@ -11,7 +11,7 @@ from vorta.borg.mount import BorgMountThread
from vorta.borg.extract import BorgExtractThread
from vorta.borg.umount import BorgUmountThread
from vorta.views.extract_dialog import ExtractDialog
from vorta.utils import get_asset, pretty_bytes, choose_folder_dialog
from vorta.utils import get_asset, pretty_bytes, choose_file_dialog
from vorta.models import BackupProfileMixin, ArchiveModel
uifile = get_asset('UI/archivetab.ui')
@ -177,7 +177,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
thread.result.connect(self.mount_result)
thread.start()
dialog = choose_folder_dialog(self, "Choose Mount Point")
dialog = choose_file_dialog(self, "Choose Mount Point")
dialog.open(receive)
def mount_result(self, result):
@ -274,7 +274,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
else:
self._set_status(params['message'])
dialog = choose_folder_dialog(self, "Choose Extraction Point")
dialog = choose_file_dialog(self, "Choose Extraction Point")
dialog.open(receive)
def extract_archive_result(self, result):

View file

@ -56,56 +56,13 @@ class ExtractDialog(ExtractDialogBase, ExtractDialogUI):
self.selected = selected_files_folders
class FileItem:
def __init__(self, name, modified, size, parent=None):
self.parentItem = parent
self.itemData = [name, modified, size] # dt.strptime(modified, ISO_FORMAT)
self.checkedState = False
def childCount(self):
return 0
def columnCount(self):
return 3
def data(self, column):
if column == 1:
return self.itemData[column] # .strftime('%Y-%m-%dT%H:%M')
elif column == 2:
return pretty_bytes(self.itemData[column])
elif column == 0:
return self.itemData[column]
def parent(self):
return self.parentItem
def row(self):
return self.parentItem.childItems.index(self)
def setCheckedState(self, value):
if value == 2:
self.checkedState = True
selected_files_folders.add(
os.path.join(self.parentItem.path, self.parentItem.data(0), self.itemData[0]))
else:
self.checkedState = False
selected_files_folders.remove(
os.path.join(self.parentItem.path, self.parentItem.data(0), self.itemData[0]))
def getCheckedState(self):
if self.checkedState:
return Qt.Checked
else:
return Qt.Unchecked
class FolderItem(FileItem):
class FolderItem:
def __init__(self, path, name, modified, parent=None):
self.parentItem = parent
self.path = path
self.itemData = [name, modified]
self.checkedState = False
self.childItems = []
self.checkedState = False
# Pre-filter children
self._filtered_children = []
@ -114,6 +71,8 @@ class FolderItem(FileItem):
for root_folder in nested_file_list.keys():
self._filtered_children.append((0, '', root_folder, '', ))
else:
self.checkedState = parent.checkedState # If there is a parent, use its checked-status.
# This adds direct children.
self._filtered_children = [f for f in files_with_attributes if search_path == f[3]]
@ -142,6 +101,27 @@ class FolderItem(FileItem):
self.is_loaded = True
def setCheckedState(self, value):
if value == 2:
self.checkedState = True
selected_files_folders.add(
os.path.join(self.parentItem.path, self.parentItem.data(0), self.itemData[0]))
else:
self.checkedState = False
path_to_remove = os.path.join(self.parentItem.path, self.parentItem.data(0), self.itemData[0])
if path_to_remove in selected_files_folders:
selected_files_folders.remove(path_to_remove)
if hasattr(self, 'childItems'):
for child in self.childItems:
child.setCheckedState(value)
def getCheckedState(self):
if self.checkedState:
return Qt.Checked
else:
return Qt.Unchecked
def child(self, row):
return self.childItems[row]
@ -167,6 +147,33 @@ class FolderItem(FileItem):
return 0
class FileItem(FolderItem):
def __init__(self, name, modified, size, parent=None):
self.parentItem = parent
self.itemData = [name, modified, size] # dt.strptime(modified, ISO_FORMAT)
self.checkedState = parent.checkedState
def childCount(self):
return 0
def columnCount(self):
return 3
def data(self, column):
if column == 1:
return self.itemData[column] # .strftime('%Y-%m-%dT%H:%M')
elif column == 2:
return pretty_bytes(self.itemData[column])
elif column == 0:
return self.itemData[column]
def parent(self):
return self.parentItem
def row(self):
return self.parentItem.childItems.index(self)
class TreeModel(QAbstractItemModel):
column_names = ['Name', 'Modified', 'Size']
@ -196,6 +203,7 @@ class TreeModel(QAbstractItemModel):
if role == Qt.CheckStateRole:
item = index.internalPointer()
item.setCheckedState(value)
self.dataChanged.emit(QModelIndex(), QModelIndex(), [])
return True

View file

@ -1,6 +1,6 @@
from PyQt5 import uic
from PyQt5.QtWidgets import QCheckBox
from vorta.utils import get_asset
from vorta.utils import get_asset, open_app_at_startup
from vorta.models import SettingsModel
from vorta._version import __version__
@ -26,3 +26,6 @@ class MiscTab(MiscTabBase, MiscTabUI):
setting = SettingsModel.get(key=key)
setting.value = bool(new_value)
setting.save()
if key == 'autostart':
open_app_at_startup(new_value)

View file

@ -1,5 +1,5 @@
from PyQt5 import uic
from ..utils import get_private_keys, get_asset, choose_folder_dialog
from ..utils import get_private_keys, get_asset, choose_file_dialog
from vorta.borg.init import BorgInitThread
from vorta.borg.info import BorgInfoThread
@ -41,7 +41,7 @@ class AddRepoWindow(AddRepoBase, AddRepoUI):
self.sshComboBox.setEnabled(False)
self.repoLabel.setText('Repository Path:')
dialog = choose_folder_dialog(self, "Choose Location of Borg Repository")
dialog = choose_file_dialog(self, "Choose Location of Borg Repository")
dialog.open(receive)
def use_remote_repo_action(self):

View file

@ -1,6 +1,6 @@
from PyQt5 import uic
from ..models import SourceDirModel, BackupProfileMixin
from ..utils import get_asset, choose_folder_dialog
from ..models import SourceFileModel, BackupProfileMixin
from ..utils import get_asset, choose_file_dialog
uifile = get_asset('UI/sourcetab.ui')
SourceUI, SourceBase = uic.loadUiType(uifile)
@ -22,12 +22,12 @@ class SourceTab(SourceBase, SourceUI, BackupProfileMixin):
profile = self.profile()
self.excludePatternsField.textChanged.disconnect()
self.excludeIfPresentField.textChanged.disconnect()
self.sourceDirectoriesWidget.clear()
self.sourceFilesWidget.clear()
self.excludePatternsField.clear()
self.excludeIfPresentField.clear()
for source in SourceDirModel.select().where(SourceDirModel.profile == profile):
self.sourceDirectoriesWidget.addItem(source.dir)
for source in SourceFileModel.select().where(SourceFileModel.profile == profile):
self.sourceFilesWidget.addItem(source.dir)
self.excludePatternsField.appendPlainText(profile.exclude_patterns)
self.excludeIfPresentField.appendPlainText(profile.exclude_if_present)
@ -38,18 +38,18 @@ class SourceTab(SourceBase, SourceUI, BackupProfileMixin):
def receive():
dir = dialog.selectedFiles()
if dir:
new_source, created = SourceDirModel.get_or_create(dir=dir[0], profile=self.profile())
new_source, created = SourceFileModel.get_or_create(dir=dir[0], profile=self.profile())
if created:
self.sourceDirectoriesWidget.addItem(dir[0])
self.sourceFilesWidget.addItem(dir[0])
new_source.save()
item = "directory" if want_folder else "file"
dialog = choose_folder_dialog(self, "Choose %s to back up" % item, want_folder=want_folder)
dialog = choose_file_dialog(self, "Choose %s to back up" % item, want_folder=want_folder)
dialog.open(receive)
def source_remove(self):
item = self.sourceDirectoriesWidget.takeItem(self.sourceDirectoriesWidget.currentRow())
db_item = SourceDirModel.get(dir=item.text())
item = self.sourceFilesWidget.takeItem(self.sourceFilesWidget.currentRow())
db_item = SourceFileModel.get(dir=item.text())
db_item.delete_instance()
item = None

View file

@ -5,7 +5,7 @@ from datetime import datetime as dt
import vorta
from vorta.application import VortaApp
from vorta.models import RepoModel, SourceDirModel, ArchiveModel, BackupProfileModel
from vorta.models import RepoModel, SourceFileModel, ArchiveModel, BackupProfileModel
def pytest_configure(config):
@ -28,7 +28,7 @@ def app(tmpdir, qtbot):
test_archive = ArchiveModel(snapshot_id='99999', name='test-archive', time=dt(2000, 1, 1, 0, 0), repo=1)
test_archive.save()
source_dir = SourceDirModel(dir='/tmp/another', repo=new_repo)
source_dir = SourceFileModel(dir='/tmp/another', repo=new_repo)
source_dir.save()
app = VortaApp([])
@ -38,7 +38,7 @@ def app(tmpdir, qtbot):
@pytest.fixture
def choose_folder_dialog(*args):
def choose_file_dialog(*args):
class MockFileDialog:
def __init__(self, *args, **kwargs):
pass

View file

@ -71,10 +71,10 @@ def test_check(app, mocker, borg_json_output, qtbot):
qtbot.mouseClick(tab.checkButton, QtCore.Qt.LeftButton)
success_text = 'INFO: Archive consistency check complete'
qtbot.waitUntil(lambda: main.createProgressText.text().startswith(success_text))
qtbot.waitUntil(lambda: main.createProgressText.text().startswith(success_text), timeout=3000)
def test_archive_mount(app, qtbot, mocker, borg_json_output, monkeypatch, choose_folder_dialog):
def test_archive_mount(app, qtbot, mocker, borg_json_output, monkeypatch, choose_file_dialog):
def psutil_disk_partitions(**kwargs):
DiskPartitions = namedtuple('DiskPartitions', ['device', 'mountpoint'])
return [DiskPartitions('borgfs', '/tmp')]
@ -94,7 +94,7 @@ def test_archive_mount(app, qtbot, mocker, borg_json_output, monkeypatch, choose
mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result)
monkeypatch.setattr(
vorta.views.archive_tab, "choose_folder_dialog", choose_folder_dialog
vorta.views.archive_tab, "choose_file_dialog", choose_file_dialog
)
qtbot.mouseClick(tab.mountButton, QtCore.Qt.LeftButton)

View file

@ -4,16 +4,16 @@ import vorta.models
import vorta.views
def test_add_folder(app, qtbot, tmpdir, monkeypatch, choose_folder_dialog):
def test_add_folder(app, qtbot, tmpdir, monkeypatch, choose_file_dialog):
monkeypatch.setattr(
vorta.views.source_tab, "choose_folder_dialog", choose_folder_dialog
vorta.views.source_tab, "choose_file_dialog", choose_file_dialog
)
main = app.main_window
main.tabWidget.setCurrentIndex(1)
tab = main.sourceTab
qtbot.mouseClick(tab.sourceAddFolder, QtCore.Qt.LeftButton)
qtbot.waitUntil(lambda: tab.sourceDirectoriesWidget.count() == 2)
qtbot.waitUntil(lambda: tab.sourceFilesWidget.count() == 2)
for src in vorta.models.SourceDirModel.select():
for src in vorta.models.SourceFileModel.select():
logging.error(src.dir, src.profile)