1
0
Fork 0
mirror of https://github.com/borgbase/vorta synced 2025-03-08 21:05:56 +00:00

v0.5.2 Bugfixes and new Misc Settings Tab (#71)

* Fix uneven vspace. Fixes #67

* Add Python 3.7 to Travis. Use tox to test multiple Python versions. Fixes #72

* Add command line option to avoid forking and open main window while debugging. Fixes #73

* Use slug of profile name as archive prefix. Fixes #46

* Add settings tab. Add light system tray icon option. Fixes #56 and #74

* Incorporate review by @ThomasWaldmann
This commit is contained in:
Manuel Riel 2018-12-04 10:58:12 +08:00 committed by GitHub
parent d84d158541
commit 30c6549f0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 452 additions and 209 deletions

View file

@ -33,7 +33,8 @@ dist: trusty
env:
global:
- SETUP_XVFB=true
- PYTHON=3.6.3
- PYTHON36=3.6.3
- PYTHON37=3.7.1
matrix:
include:
@ -48,19 +49,20 @@ matrix:
install:
- |
if [ $TRAVIS_OS_NAME = "linux" ]; then
#curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
#git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv
#git clone git://github.com/pyenv/pyenv-update.git $(pyenv root)/plugins/pyenv-update
#export PATH="/home/travis/.pyenv/shims:${PATH}"
export DISPLAY=:99.0
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset
sleep 3
# cd /opt/pyenv && git pull origin master
pyenv install -s $PYTHON36
eval "$(pyenv init -)"
pyenv shell $PYTHON36
elif [ $TRAVIS_OS_NAME = "osx" ]; then
brew upgrade pyenv
pyenv install -s $PYTHON37
pyenv install -s $PYTHON36
eval "$(pyenv init -)"
pyenv shell $PYTHON36 $PYTHON37
fi
pyenv install -s $PYTHON
eval "$(pyenv init -)"
pyenv shell $PYTHON
- pip install -U setuptools pip
- pip install .
@ -69,12 +71,10 @@ install:
before_script:
- if [ $TRAVIS_OS_NAME = "linux" ]; then (herbstluftwm )& fi
- if [ $TRAVIS_OS_NAME = "osx" ]; then (sudo Xvfb :99 -ac -screen 0 1024x768x8 )& fi
- sleep 3
script:
- pytest --forked
- if [ $TRAVIS_OS_NAME = "linux" ]; then tox -e flake8; fi
- tox
#after_script:
#- |

View file

@ -68,7 +68,8 @@ max-line-length = 120
exclude = build,dist,.git,.idea,.cache,.tox,.eggs
[tox:tox]
envlist = py36,flake8
envlist = py36,py37,flake8
skip_missing_interpreters = true
[testenv]
deps =
@ -78,6 +79,7 @@ deps =
pytest-xdist
pytest-faulthandler
commands=pytest
passenv = DISPLAY
[testenv:flake8]
deps =

View file

@ -8,9 +8,22 @@ from vorta.config import SETTINGS_DIR
from vorta.updater import get_updater
import vorta.sentry
import vorta.log
from vorta.utils import parse_args
def main():
args = parse_args()
frozen_binary = getattr(sys, 'frozen', False)
# Don't fork if user specifies it or when running from onedir app bundle on macOS.
if args.foreground or (frozen_binary and sys.platform == 'darwin'):
pass
else:
print('Forking to background (see system tray).')
if os.fork():
sys.exit()
# Send crashes to Sentry.
if not os.environ.get('NO_SENTRY', False):
vorta.sentry.init()
@ -21,6 +34,7 @@ def main():
app = VortaApp(sys.argv, single_app=True)
app.updater = get_updater()
sys.exit(app.exec_())

View file

@ -4,14 +4,13 @@ import fcntl
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QIcon
from .tray_menu import TrayMenu
from .scheduler import VortaScheduler
from .models import BackupProfileModel
from .borg.create import BorgCreateThread
from .views.main_window import MainWindow
from .utils import get_asset
from .utils import parse_args, set_tray_icon
from vorta.config import SETTINGS_DIR
@ -28,7 +27,7 @@ class VortaApp(QApplication):
backup_cancelled_event = QtCore.pyqtSignal()
backup_log_event = QtCore.pyqtSignal(str)
def __init__(self, args, single_app=False):
def __init__(self, args_raw, single_app=False):
# Ensure only one app instance is running.
# From https://stackoverflow.com/questions/220525/
@ -43,14 +42,17 @@ class VortaApp(QApplication):
print('An instance of Vorta is already running.')
sys.exit(1)
super().__init__(args)
super().__init__(args_raw)
self.setQuitOnLastWindowClosed(False)
self.scheduler = VortaScheduler(self)
# Prepare tray and main window
self.tray = TrayMenu(self)
self.main_window = MainWindow(self)
# self.main_window.show()
args = parse_args()
if args.foreground:
self.main_window.show()
self.backup_started_event.connect(self.backup_started_event_response)
self.backup_finished_event.connect(self.backup_finished_event_response)
@ -73,14 +75,11 @@ class VortaApp(QApplication):
self.main_window.raise_()
def backup_started_event_response(self):
icon = QIcon(get_asset('icons/hdd-o-active.png'))
self.tray.setIcon(icon)
set_tray_icon(self.tray, active=True)
def backup_finished_event_response(self):
icon = QIcon(get_asset('icons/hdd-o.png'))
self.tray.setIcon(icon)
set_tray_icon(self.tray)
self.main_window.scheduleTab._draw_next_scheduled_backup()
def backup_cancelled_event_response(self):
icon = QIcon(get_asset('icons/hdd-o.png'))
self.tray.setIcon(icon)
set_tray_icon(self.tray)

View file

@ -54,7 +54,7 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>5</number>
<number>12</number>
</property>
<property name="bottomMargin">
<number>0</number>
@ -92,7 +92,7 @@
<item>
<widget class="QToolButton" name="profileRenameButton">
<property name="icon">
<iconset>
<iconset resource="../icons/collection.qrc">
<normaloff>:/icons/edit.svg</normaloff>:/icons/edit.svg</iconset>
</property>
<property name="iconSize">
@ -109,7 +109,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset>
<iconset resource="../icons/collection.qrc">
<normaloff>:/icons/trash.svg</normaloff>:/icons/trash.svg</iconset>
</property>
</widget>
@ -132,7 +132,7 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>3</number>
<number>4</number>
</property>
<property name="documentMode">
<bool>false</bool>
@ -166,6 +166,11 @@
<string>Archives</string>
</attribute>
</widget>
<widget class="QWidget" name="miscTabSlot">
<attribute name="title">
<string>Misc</string>
</attribute>
</widget>
</widget>
</item>
<item>
@ -181,7 +186,7 @@
<property name="minimumSize">
<size>
<width>0</width>
<height>45</height>
<height>55</height>
</size>
</property>
<property name="font">

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>791</width>
<height>497</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="checkboxLayout">
<property name="topMargin">
<number>10</number>
</property>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Version:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="versionLabel">
<property name="text">
<string>0.0</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -17,6 +17,19 @@
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" alignment="Qt::AlignHCenter">
<widget class="QLabel" name="title">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Initialize New Backup Repository</string>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
@ -139,24 +152,6 @@
</item>
</widget>
</item>
<item row="6" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="saveButton">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="1">
<widget class="QLabel" name="errorText">
<property name="sizePolicy">
@ -193,21 +188,26 @@
</property>
</widget>
</item>
<item row="6" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="saveButton">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="0" column="0" alignment="Qt::AlignHCenter">
<widget class="QLabel" name="title">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Initialize New Backup Repository</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources>

View file

@ -24,39 +24,108 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_7">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Configure your backup repository (you can add a new or existing repository). For remote repositories, you will need a SSH key to log in without a password (if you already have a key, just keep it at the default).</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="topMargin">
<number>5</number>
<number>0</number>
</property>
<property name="rightMargin">
<number>25</number>
</property>
<item row="2" column="0">
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QComboBox" name="repoCompression"/>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">margin-bottom: 10</string>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Remote or local backup repository. For secure remote backups, try &lt;a href=&quot;https://www.borgbase.com/?utm_source=vorta&amp;utm_medium=app&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;BorgBase&lt;/span&gt;&lt;/a&gt;. 100GB free during Beta.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">margin-bottom: 10</string>
</property>
<property name="text">
<string>To securely access remote repositories. Keep default to use all your existing keys. Or create new key.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Compression:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Repository:</string>
@ -64,10 +133,46 @@
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="sshComboBox"/>
</item>
<item>
<widget class="QToolButton" name="sshKeyToClipboardButton">
<property name="toolTip">
<string>Copy public SSH key to clipboard.</string>
</property>
<property name="statusTip">
<string/>
</property>
<property name="text">
<string>Copy</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/icons/copy.svg</normaloff>:/icons/copy.svg</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="arrowType">
<enum>Qt::NoArrow</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="repoSelector">
<property name="sizePolicy">
@ -102,97 +207,28 @@
</item>
</layout>
</item>
<item row="3" column="1">
<widget class="QLabel" name="label_5">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">margin-bottom: 10</string>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Remote or local backup repository. For secure remote backups, try &lt;a href=&quot;https://www.borgbase.com/?utm_source=vorta&amp;utm_medium=app&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;BorgBase&lt;/span&gt;&lt;/a&gt;. 100GB free during Beta.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Compression:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="repoCompression"/>
</item>
<item row="4" column="0">
<item row="2" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>SSH Key:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="sshComboBox"/>
</item>
<item>
<widget class="QToolButton" name="sshKeyToClipboardButton">
<property name="toolTip">
<string>Copy public SSH key to clipboard.</string>
</property>
<property name="statusTip">
<string/>
</property>
<property name="text">
<string>Copy</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/icons/copy.svg</normaloff>:/icons/copy.svg</iconset>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="arrowType">
<enum>Qt::NoArrow</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="1">
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">margin-bottom: 10</string>
</property>
<property name="text">
<string>To securely access remote repositories. Keep default to use all your existing keys. Or create new key.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QFormLayout" name="repoStats">
<property name="fieldGrowthPolicy">

View file

@ -29,7 +29,7 @@ font-weight: bold;
}</string>
</property>
<property name="currentIndex">
<number>2</number>
<number>1</number>
</property>
<widget class="QWidget" name="schedule">
<property name="geometry">
@ -353,7 +353,7 @@ font-weight: bold;
<widget class="QListWidget" name="wifiListWidget"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<widget class="QLabel" name="wifiListLabel">
<property name="text">
<string>Allowed Networks:</string>
</property>

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -103,7 +103,7 @@ class BorgCreateThread(BorgThread):
cmd.extend(['--exclude-if-present', f.strip()])
# Add repo url and source dirs.
cmd.append(f"{profile.repo.url}::{platform.node()}-{profile.id}-{dt.now().isoformat(timespec='seconds')}")
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):
cmd.append(f.dir)

View file

@ -32,7 +32,7 @@ class BorgPruneThread(BorgThread):
'--keep-weekly', str(profile.prune_week),
'--keep-monthly', str(profile.prune_month),
'--keep-yearly', str(profile.prune_year),
'--prefix', f'{platform.node()}-'
'--prefix', f'{platform.node()}-{profile.slug()}'
]
if profile.prune_keep_within:
pruning_opts += ['--keep-within', profile.prune_keep_within]

View file

@ -4,10 +4,12 @@ This module provides the app's data store using Peewee with SQLite.
At the bottom there is a simple schema migration system.
"""
import peewee as pw
import sys
import json
import peewee as pw
from datetime import datetime, timedelta
from playhouse.migrate import SqliteMigrator, migrate
from vorta.utils import slugify
SCHEMA_VERSION = 8
@ -82,6 +84,9 @@ class BackupProfileModel(pw.Model):
def refresh(self):
return type(self).get(self._pk_expr())
def slug(self):
return slugify(self.name)
class Meta:
database = db
@ -148,16 +153,21 @@ class SchemaVersion(pw.Model):
database = db
class SettingsModel(pw.Model):
"""App settings unrelated to a single profile or repo"""
key = pw.CharField(unique=True)
value = pw.BooleanField()
label = pw.CharField()
type = pw.CharField()
class Meta:
database = db
class BackupProfileMixin:
"""Extend to support multiple profiles later."""
def profile(self):
return BackupProfileModel.get(id=self.window().current_profile.id)
# app = QApplication.instance()
# main_window = hasattr(app, 'main_window')
# if main_window:
# return app.main_window.current_profile
# else:
# return BackupProfileModel.select().first()
def _apply_schema_update(current_schema, version_after, *operations):
@ -171,13 +181,33 @@ 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,
db.create_tables([RepoModel, RepoPassword, BackupProfileModel, SourceDirModel, SettingsModel,
ArchiveModel, WifiSettingModel, EventLogModel, SchemaVersion])
if BackupProfileModel.select().count() == 0:
default_profile = BackupProfileModel(name='Default Profile')
default_profile.save()
# Default settings
settings = [
{'key': 'use_light_icon', 'value': False, 'type': 'checkbox',
'label': 'Use light system tray icon (applies after restart, useful for dark themes).'}
]
if sys.platform == 'darwin':
settings += [
{'key': 'autostart', 'value': False, 'type': 'checkbox',
'label': 'Add Vorta to Login Items in Preferences > Users and Groups > Login Items.'},
{'key': 'enable_notifications', 'value': True, 'type': 'checkbox',
'label': 'Display notifications when background tasks fail.'},
{'key': 'check_for_updates', 'value': True, 'type': 'checkbox',
'label': 'Check for updates on startup.'},
]
for setting in settings: # Create missing settings and update labels.
s, created = SettingsModel.get_or_create(key=setting['key'], defaults=setting)
s.label = setting['label']
s.save()
# Delete old log entries after 3 months.
three_months_ago = datetime.now() - timedelta(days=180)
EventLogModel.delete().where(EventLogModel.start_time < three_months_ago)

View file

@ -1,16 +1,15 @@
from PyQt5.QtWidgets import QMenu, QSystemTrayIcon
from PyQt5.QtGui import QIcon
from .utils import get_asset
from .borg.borg_thread import BorgThread
from .models import BackupProfileModel
from .utils import set_tray_icon
class TrayMenu(QSystemTrayIcon):
def __init__(self, parent=None):
icon = QIcon(get_asset('icons/hdd-o.png'))
QSystemTrayIcon.__init__(self, icon, parent)
QSystemTrayIcon.__init__(self, parent)
self.app = parent
set_tray_icon(self)
menu = QMenu()
# Workaround to get `activated` signal on Unity: https://stackoverflow.com/a/43683895/3983708

View file

@ -1,6 +1,9 @@
import os
import sys
import plistlib
import argparse
import unicodedata
import re
from collections import defaultdict
from functools import reduce
@ -11,10 +14,10 @@ from paramiko.ecdsakey import ECDSAKey
from paramiko.ed25519key import Ed25519Key
from paramiko import SSHException
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtGui import QIcon
from PyQt5 import QtCore
import subprocess
import keyring
from .models import WifiSettingModel
class VortaKeyring(keyring.backend.KeyringBackend):
@ -140,6 +143,8 @@ def get_asset(path):
def get_sorted_wifis(profile):
"""Get SSIDs from OS and merge with settings in DB."""
from vorta.models import WifiSettingModel
if sys.platform == 'darwin':
plist_path = '/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist'
plist_file = open(plist_path, 'rb')
@ -185,3 +190,32 @@ def get_current_wifi():
split_line = line.strip().split(':')
if split_line[0] == 'SSID':
return split_line[1].strip()
def parse_args():
parser = argparse.ArgumentParser(description='Vorta Backup GUI for Borg.')
parser.add_argument('--foreground', '-f',
action='store_true',
help="Don't fork into background and open main window on startup.")
return parser.parse_args()
def slugify(value):
"""
Converts to lowercase, removes non-word characters (alphanumerics and
underscores) and converts spaces to hyphens. Also strips leading and
trailing whitespace.
Copied from Django.
"""
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
value = re.sub(r'[^\w\s-]', '', value).strip().lower()
return re.sub(r'[-\s]+', '-', value)
def set_tray_icon(tray, active=False):
from vorta.models import SettingsModel
use_light_style = SettingsModel.get(key='use_light_icon').value
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)

View file

@ -1,3 +1,4 @@
import sys
from PyQt5.QtWidgets import QShortcut
from PyQt5 import uic, QtCore
from PyQt5.QtGui import QKeySequence
@ -6,6 +7,7 @@ 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
@ -30,6 +32,7 @@ class MainWindow(MainWindowBase, MainWindowUI):
self.sourceTab = SourceTab(self.sourceTabSlot)
self.archiveTab = ArchiveTab(self.archiveTabSlot)
self.scheduleTab = ScheduleTab(self.scheduleTabSlot)
self.miscTabSlot = MiscTab(self.miscTabSlot)
self.tabWidget.setCurrentIndex(0)
self.repoTab.repo_changed.connect(self.archiveTab.populate_from_profile)
@ -56,6 +59,14 @@ class MainWindow(MainWindowBase, MainWindowUI):
self.profileRenameButton.clicked.connect(self.profile_rename_action)
self.profileDeleteButton.clicked.connect(self.profile_delete_action)
# OS-specific startup options:
if sys.platform != 'darwin':
# Hide Wifi-rule section in schedule tab.
self.scheduleTab.wifiListLabel.hide()
self.scheduleTab.wifiListWidget.hide()
self.scheduleTab.page_2.hide()
self.scheduleTab.toolBox.removeItem(1)
# Connect to existing thread.
if BorgThread.is_running():
self.createStartBtn.setEnabled(False)

View file

@ -0,0 +1,28 @@
from PyQt5 import uic
from PyQt5.QtWidgets import QCheckBox
from vorta.utils import get_asset
from vorta.models import SettingsModel
from vorta._version import __version__
uifile = get_asset('UI/misctab.ui')
MiscTabUI, MiscTabBase = uic.loadUiType(uifile, from_imports=True, import_from='vorta.views')
class MiscTab(MiscTabBase, MiscTabUI):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(parent)
self.versionLabel.setText(__version__)
for setting in SettingsModel.select().where(SettingsModel.type == 'checkbox'):
b = QCheckBox(setting.label)
b.setCheckState(setting.value)
b.setTristate(False)
b.stateChanged.connect(lambda v, key=setting.key: self.save_setting(key, v))
self.checkboxLayout.addWidget(b)
def save_setting(self, key, new_value):
setting = SettingsModel.get(key=key)
setting.value = bool(new_value)
setting.save()

View file

@ -34,7 +34,7 @@ class AddProfileWindow(AddProfileBase, AddProfileUI):
def validate(self):
name = self.profileNameField.text()
# Name as entered?
# A name was entered?
if len(name) == 0:
self._set_status('Please enter a profile name.')
return False

View file

@ -45,7 +45,6 @@ class SourceTab(SourceBase, SourceUI, BackupProfileMixin):
item = "directory" if want_folder else "file"
dialog = choose_folder_dialog(self, "Choose %s to back up" % item, want_folder=want_folder)
self._file_dialog = dialog # for pytest
dialog.open(receive)
def source_remove(self):

View file

@ -28,10 +28,11 @@ 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', repo=new_repo)
source_dir = SourceDirModel(dir='/tmp/another', repo=new_repo)
source_dir.save()
app = VortaApp([])
app.main_window.show()
qtbot.addWidget(app.main_window)
return app
@ -39,7 +40,7 @@ def app(tmpdir, qtbot):
@pytest.fixture
def choose_folder_dialog(*args):
class MockFileDialog:
def __init__(self, *args):
def __init__(self, *args, **kwargs):
pass
def open(self, func):

View file

@ -98,7 +98,7 @@ def test_archive_mount(app, qtbot, mocker, borg_json_output, monkeypatch, choose
)
qtbot.mouseClick(tab.mountButton, QtCore.Qt.LeftButton)
qtbot.waitUntil(lambda: tab.mountErrors.text().startswith('Mounted'), timeout=1000)
qtbot.waitUntil(lambda: tab.mountErrors.text().startswith('Mounted'), timeout=5000)
qtbot.mouseClick(tab.mountButton, QtCore.Qt.LeftButton)
# qtbot.waitUntil(lambda: tab.mountErrors.text() == 'No active Borg mounts found.')
@ -127,7 +127,7 @@ def test_archive_extract(app, qtbot, mocker, borg_json_output, monkeypatch):
mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result)
qtbot.mouseClick(tab.extractButton, QtCore.Qt.LeftButton)
qtbot.waitUntil(lambda: hasattr(tab, '_window'))
qtbot.waitUntil(lambda: hasattr(tab, '_window'), timeout=5000)
assert tab._window.treeView.model().rootItem.childItems[0].data(0) == 'Users'
tab._window.treeView.model().rootItem.childItems[0].load_children()

View file

@ -1,6 +1,7 @@
import os
import uuid
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMessageBox
from PyQt5.QtWidgets import QMessageBox
import vorta.borg.borg_thread
import vorta.models
@ -9,10 +10,12 @@ from vorta.views.ssh_dialog import SSHAddWindow
from vorta.models import EventLogModel, RepoModel, ArchiveModel
def test_repo_add(app, qtbot, mocker, borg_json_output):
def test_repo_add_failures(app, qtbot, mocker, borg_json_output):
# Add new repo window
main = app.main_window
add_repo_window = AddRepoWindow(main.repoTab)
add_repo_window = AddRepoWindow(main)
qtbot.addWidget(add_repo_window)
qtbot.keyClicks(add_repo_window.repoURL, 'aaa')
qtbot.mouseClick(add_repo_window.saveButton, QtCore.Qt.LeftButton)
assert add_repo_window.errorText.text().startswith('Please enter a valid')
@ -21,6 +24,15 @@ def test_repo_add(app, qtbot, mocker, borg_json_output):
qtbot.mouseClick(add_repo_window.saveButton, QtCore.Qt.LeftButton)
assert add_repo_window.errorText.text() == 'Please use a longer password.'
def test_repo_add_success(app, qtbot, mocker, borg_json_output):
# Add new repo window
main = app.main_window
add_repo_window = AddRepoWindow(main)
qtbot.addWidget(add_repo_window)
test_repo_url = f'vorta-test-repo.{uuid.uuid4()}.com:repo' # Random repo URL to avoid macOS keychain
qtbot.keyClicks(add_repo_window.repoURL, test_repo_url)
qtbot.keyClicks(add_repo_window.passwordLineEdit, 'long-password-long')
stdout, stderr = borg_json_output('info')
@ -34,8 +46,9 @@ def test_repo_add(app, qtbot, mocker, borg_json_output):
main.repoTab.process_new_repo(blocker.args[0])
# assert EventLogModel.select().count() == 2
assert RepoModel.get(id=2).url == 'aaabbb.com:repo'
qtbot.waitUntil(lambda: EventLogModel.select().count() == 2)
assert EventLogModel.select().count() == 2
assert RepoModel.get(id=2).url == test_repo_url
def test_repo_unlink(app, qtbot, monkeypatch):
@ -70,9 +83,6 @@ def test_ssh_dialog(qtbot, tmpdir):
assert pub_tmpfile_content.startswith('ssh-ed25519')
qtbot.waitUntil(lambda: ssh_dialog.errors.text().startswith('New key was copied'))
clipboard = QApplication.clipboard()
assert clipboard.text().startswith('ssh-ed25519')
qtbot.mouseClick(ssh_dialog.generateButton, QtCore.Qt.LeftButton)
qtbot.waitUntil(lambda: ssh_dialog.errors.text().startswith('Key file already'))

View file

@ -1,18 +1,18 @@
import logging
from PyQt5 import QtCore
import vorta.models
import vorta.views
def test_add_folder(app, qtbot, tmpdir):
def test_add_folder(app, qtbot, tmpdir, monkeypatch, choose_folder_dialog):
monkeypatch.setattr(
vorta.views.source_tab, "choose_folder_dialog", choose_folder_dialog
)
main = app.main_window
main.tabWidget.setCurrentIndex(1)
tab = main.sourceTab
qtbot.mouseClick(tab.sourceAddFolder, QtCore.Qt.LeftButton)
qtbot.waitUntil(lambda: len(tab._file_dialog.selectedFiles()) > 0, timeout=3000)
tab._file_dialog.accept()
qtbot.waitUntil(lambda: tab.sourceDirectoriesWidget.count() == 2)
for src in vorta.models.SourceDirModel.select():