diff --git a/src/vorta/assets/UI/extractdialog.ui b/src/vorta/assets/UI/extractdialog.ui
index 4d636c78..7d543f48 100644
--- a/src/vorta/assets/UI/extractdialog.ui
+++ b/src/vorta/assets/UI/extractdialog.ui
@@ -57,6 +57,13 @@
-
+ -
+
+
+ Note: If you select a top-level folder and deselect its children, they will still be restored.
+
+
+
-
diff --git a/src/vorta/assets/UI/sourcetab.ui b/src/vorta/assets/UI/sourcetab.ui
index b587ea99..6d871c35 100644
--- a/src/vorta/assets/UI/sourcetab.ui
+++ b/src/vorta/assets/UI/sourcetab.ui
@@ -24,7 +24,7 @@
-
-
-
+
0
diff --git a/src/vorta/borg/borg_thread.py b/src/vorta/borg/borg_thread.py
index 8230b2c9..87d29027 100644
--- a/src/vorta/borg/borg_thread.py
+++ b/src/vorta/borg/borg_thread.py
@@ -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
diff --git a/src/vorta/borg/create.py b/src/vorta/borg/create.py
index 17f8f33e..ea08ab92 100644
--- a/src/vorta/borg/create.py
+++ b/src/vorta/borg/create.py
@@ -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..'
diff --git a/src/vorta/models.py b/src/vorta/models.py
index a261c608..af4e992e 100644
--- a/src/vorta/models.py
+++ b/src/vorta/models.py
@@ -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))
)
diff --git a/src/vorta/tray_menu.py b/src/vorta/tray_menu.py
index 7a41257d..889ba825 100644
--- a/src/vorta/tray_menu.py
+++ b/src/vorta/tray_menu.py
@@ -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)
diff --git a/src/vorta/updater.py b/src/vorta/updater.py
index bc81b92f..475a2373 100644
--- a/src/vorta/updater.py
+++ b/src/vorta/updater.py
@@ -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
diff --git a/src/vorta/utils.py b/src/vorta/utils.py
index 99c41e97..33dfd4fe 100644
--- a/src/vorta/utils.py
+++ b/src/vorta/utils.py
@@ -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)
diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py
index 843f231e..8dd65afc 100644
--- a/src/vorta/views/archive_tab.py
+++ b/src/vorta/views/archive_tab.py
@@ -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):
diff --git a/src/vorta/views/extract_dialog.py b/src/vorta/views/extract_dialog.py
index a27425cb..ffd90b8e 100644
--- a/src/vorta/views/extract_dialog.py
+++ b/src/vorta/views/extract_dialog.py
@@ -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
diff --git a/src/vorta/views/misc_tab.py b/src/vorta/views/misc_tab.py
index 39b28792..c0cbc891 100644
--- a/src/vorta/views/misc_tab.py
+++ b/src/vorta/views/misc_tab.py
@@ -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)
diff --git a/src/vorta/views/repo_add_dialog.py b/src/vorta/views/repo_add_dialog.py
index c9276c8f..adf66236 100644
--- a/src/vorta/views/repo_add_dialog.py
+++ b/src/vorta/views/repo_add_dialog.py
@@ -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):
diff --git a/src/vorta/views/source_tab.py b/src/vorta/views/source_tab.py
index 104aa38e..45b0bee6 100644
--- a/src/vorta/views/source_tab.py
+++ b/src/vorta/views/source_tab.py
@@ -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
diff --git a/tests/conftest.py b/tests/conftest.py
index ac28e80a..58432b21 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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
diff --git a/tests/test_archives.py b/tests/test_archives.py
index 31993e29..693270b4 100644
--- a/tests/test_archives.py
+++ b/tests/test_archives.py
@@ -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)
diff --git a/tests/test_source.py b/tests/test_source.py
index 6820525d..8089d077 100644
--- a/tests/test_source.py
+++ b/tests/test_source.py
@@ -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)