Subclass QApplication.

This commit is contained in:
Manu 2018-10-31 19:14:12 +08:00
parent 7549e73189
commit c896388ae8
10 changed files with 266 additions and 115 deletions

View File

@ -35,6 +35,11 @@ $ brew cask install qt-creator
$ brew install qt
```
To run directly:
```
PYTHONPATH=src python -m vorta
```
To build a binary package:
```
$ pyinstaller --clean --noconfirm vorta.spec

View File

@ -1,8 +1,8 @@
import sys
import os
import peewee
from PyQt5.QtWidgets import QApplication
# Send crashes to Sentry
if getattr(sys, 'frozen', False):
import sentry_sdk
sentry_sdk.init("https://a4a23df3e44743d5b5c5f06417a9a809@sentry.io/1311799")
@ -11,24 +11,14 @@ if getattr(sys, 'frozen', False):
from vorta.utils import get_asset
sys.path.append(os.path.dirname(get_asset('icons/collection.rc')))
from vorta.tray_menu import TrayMenu
from vorta.scheduler import init_scheduler
import vorta.models
from vorta.application import VortaApp
from vorta.config import SETTINGS_DIR
# Init database
sqlite_db = peewee.SqliteDatabase(os.path.join(SETTINGS_DIR, 'settings.db'))
vorta.models.init_db(sqlite_db)
app = QApplication(sys.argv)
app.thread = None
app.setQuitOnLastWindowClosed(False)
app.scheduler = init_scheduler()
TrayMenu(app)
app.profile = vorta.models.BackupProfileModel.get(id=1)
if not getattr(sys, 'frozen', False):
from .views.main_window import MainWindow
ex = MainWindow(app)
ex.show()
app = VortaApp(sys.argv)
sys.exit(app.exec_())

42
src/vorta/application.py Normal file
View File

@ -0,0 +1,42 @@
import sys
from PyQt5.QtWidgets import QApplication
from .tray_menu import TrayMenu
from .scheduler import VortaScheduler
from .models import BackupProfileModel
from .borg_runner import BorgThread
from .views.main_window import MainWindow
class VortaApp(QApplication):
def __init__(self, args):
super().__init__(args)
self.thread = None
self.setQuitOnLastWindowClosed(False)
self.scheduler = VortaScheduler(self)
self.profile = BackupProfileModel.get(id=1)
# Prepare tray and connect events.
self.tray = TrayMenu(self)
self.tray.start_backup.connect(self.on_create_backup)
self.tray.open_main_window.connect(self.on_open_main_window)
# Prepare main window
self.main_window = MainWindow(self)
if not getattr(sys, 'frozen', False):
self.main_window.show()
def on_create_backup(self):
if self.thread and self.app.isRunning():
self.app.process.kill()
self.app.terminate()
else:
msg = BorgThread.prepare_runner()
if msg['ok']:
self.thread = BorgThread(msg['cmd'], msg['params'])
self.thread.start()
# TODO: error dialog
def on_open_main_window(self):
self.main_window.show()

View File

@ -43,7 +43,7 @@
<number>10</number>
</property>
<property name="topMargin">
<number>20</number>
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>

View File

@ -60,7 +60,7 @@ font-weight: bold;
<enum>QLayout::SetFixedSize</enum>
</property>
<property name="bottomMargin">
<number>10</number>
<number>7</number>
</property>
<item>
<widget class="QRadioButton" name="scheduleOffRadio">
@ -71,7 +71,7 @@ font-weight: bold;
</sizepolicy>
</property>
<property name="text">
<string>Backup Manually</string>
<string>Backup Manually.</string>
</property>
</widget>
</item>
@ -180,7 +180,7 @@ font-weight: bold;
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="topMargin">
<number>10</number>
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="validationCheckBox">
@ -188,7 +188,10 @@ font-weight: bold;
<string>Validate repository data every</string>
</property>
<property name="checked">
<bool>true</bool>
<bool>false</bool>
</property>
<property name="tristate">
<bool>false</bool>
</property>
</widget>
</item>
@ -234,6 +237,124 @@ font-weight: bold;
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="pruneCheckBox">
<property name="text">
<string>Prune old Snapshots</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;(&lt;a href=&quot;https://borgbackup.readthedocs.io/en/stable/usage.html#borg-prune&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;More&lt;/span&gt;&lt;/a&gt;)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_12">
<property name="text">
<string>Keep</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="prune_hour"/>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>hourly,</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="prune_day"/>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>daily,</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="prune_week"/>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>weekly,</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="prune_month"/>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>monthly and</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="prune_year"/>
</item>
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>annual snapshots.</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">

View File

@ -51,6 +51,12 @@ class BackupProfileModel(peewee.Model):
schedule_fixed_minute = peewee.IntegerField(default=42)
validation_on = peewee.BooleanField(default=True)
validation_weeks = peewee.IntegerField(default=3)
prune_on = peewee.BooleanField(default=False)
prune_hour = peewee.IntegerField(default=2)
prune_day = peewee.IntegerField(default=7)
prune_week = peewee.IntegerField(default=4)
prune_month = peewee.IntegerField(default=6)
prune_year = peewee.IntegerField(default=2)
class Meta:
database = db

View File

@ -1,39 +1,30 @@
from apscheduler.schedulers.qt import QtScheduler
from apscheduler.triggers import cron
from PyQt5.QtWidgets import QApplication
from PyQt5 import QtCore
from .models import BackupProfileModel, EventLogModel
from .borg_runner import BorgThread
def create_backup_task():
msg = BorgThread.prepare_runner()
if msg['ok']:
t = BorgThread(None, msg['cmd'], msg['params'])
t.start()
t.wait()
else:
error_log = EventLogModel(category='borg-factory', message=msg['message'])
error_log.save()
class VortaScheduler(QtScheduler):
def __init__(self, parent):
super().__init__()
self.app = parent
def reload(self):
self.remove_all_jobs()
profile = self.app.profile
if profile.schedule_mode == 'off':
return None
elif profile.schedule_mode == 'interval':
trigger = cron.CronTrigger(hour=f'*/{profile.schedule_interval_hours}',
minute=profile.schedule_interval_minutes)
elif profile.schedule_mode == 'fixed':
trigger = cron.CronTrigger(hour=profile.schedule_fixed_hour,
minute=profile.schedule_fixed_minute)
def init_scheduler():
s = QtScheduler()
app = QApplication.instance()
if hasattr(app, 'scheduler') and app.scheduler is not None:
app.scheduler.shutdown()
self.add_job(self.app.on_create_backup, trigger, id='create-backup', misfire_grace_time=180)
self.start()
profile = BackupProfileModel.get(id=1)
if profile.schedule_mode == 'off':
return None
elif profile.schedule_mode == 'interval':
trigger = cron.CronTrigger(hour=f'*/{profile.schedule_interval_hours}',
minute=profile.schedule_interval_minutes)
elif profile.schedule_mode == 'fixed':
trigger = cron.CronTrigger(hour=profile.schedule_fixed_hour,
minute=profile.schedule_fixed_minute)
s.add_job(create_backup_task, trigger, id='create-backup', misfire_grace_time=180)
s.start()
return s
def next_job(self):
if self.get_jobs():
job = self.scheduler.get_job('create-backup')
return f"Next run: {job.next_run_time.strftime('%Y-%m-%d %H:%M')}"
else:
return 'Manual Backups'

View File

@ -5,23 +5,26 @@ from PyQt5.QtGui import QIcon
from .utils import get_asset
from .config import remove_config
from .borg_runner import BorgThread
class TrayMenu(QSystemTrayIcon):
start_backup = QtCore.pyqtSignal()
open_main_window = QtCore.pyqtSignal()
def __init__(self, parent=None):
icon = QIcon(get_asset('icons/hdd-o.png'))
QSystemTrayIcon.__init__(self, icon, parent)
self.app = parent
menu = QMenu()
self.status = menu.addAction(self._get_scheduler_status())
self.status = menu.addAction(self.app.scheduler.next_job())
self.status.setEnabled(False)
self.create_action = menu.addAction("Backup Now")
self.create_action.triggered.connect(self.on_create_backup)
self.create_action.triggered.connect(self.start_backup.emit)
settings_action = menu.addAction("Settings")
settings_action.triggered.connect(self.on_settings_action)
settings_action.triggered.connect(self.open_main_window.emit)
menu.addSeparator()
@ -37,27 +40,12 @@ class TrayMenu(QSystemTrayIcon):
self.setVisible(True)
self.show()
def on_settings_action(self):
ex = MainWindow(self.app)
ex.show()
def on_exit_action(self):
QApplication.instance().quit()
self.app.quit()
def on_reset(self):
remove_config()
QApplication.instance().quit()
def on_create_backup(self):
if self.app.thread and self.app.thread.isRunning():
self.app.thread.process.kill()
self.app.thread.terminate()
else:
msg = BorgThread.prepare_runner()
if msg['ok']:
self.app.thread = BorgThread(msg['cmd'], msg['params'])
self.app.thread.start()
# TODO: error dialog
self.app.quit()
def on_user_click(self):
"""Adjust labels to reflect current status."""
@ -65,12 +53,5 @@ class TrayMenu(QSystemTrayIcon):
self.status.setText('Backup in Progress')
self.create_action.setText('Cancel Backup')
else:
self.status.setText(self._get_scheduler_status())
self.status.setText(self.app.scheduler.next_job())
self.create_action.setText('Backup Now')
def _get_scheduler_status(self):
if self.app.scheduler is not None:
job = self.app.scheduler.get_job('create-backup')
return f"Next run: {job.next_run_time.strftime('%Y-%m-%d %H:%M')}"
else:
return 'No backups scheduled'

View File

@ -1,19 +1,22 @@
from PyQt5 import uic, QtCore
from PyQt5.QtWidgets import QListWidgetItem, QApplication, QTableView, QHeaderView, QTableWidgetItem
from ..utils import get_asset, get_sorted_wifis
from ..scheduler import init_scheduler
from ..models import EventLogModel
from ..models import EventLogModel, WifiSettingModel
uifile = get_asset('UI/scheduletab.ui')
ScheduleUI, ScheduleBase = uic.loadUiType(uifile)
class ScheduleTab(ScheduleBase, ScheduleUI):
prune_intervals = ['hour', 'day', 'week', 'month', 'year']
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(parent)
self.profile = self.window().profile
self.app = QApplication.instance()
# Set scheduler values
self.schedulerRadioMapping = {
'off': self.scheduleOffRadio,
'interval': self.scheduleIntervalRadio,
@ -26,9 +29,19 @@ class ScheduleTab(ScheduleBase, ScheduleUI):
self.scheduleFixedTime.setTime(
QtCore.QTime(self.profile.schedule_fixed_hour, self.profile.schedule_fixed_minute))
# Set checking options
self.validationCheckBox.setCheckState(self.profile.validation_on)
self.validationCheckBox.setTristate(False)
self.validationSpinBox.setValue(self.profile.validation_weeks)
# Set pruning options
self.pruneCheckBox.setCheckState(self.profile.prune_on)
for i in self.prune_intervals:
getattr(self, f'prune_{i}').setValue(getattr(self.profile, f'prune_{i}'))
self.scheduleApplyButton.clicked.connect(self.on_scheduler_apply)
self.set_next_backup_datetime()
self.nextBackupDateTimeLabel.setText(self.app.scheduler.next_job())
self.init_wifi()
self.init_logs()
@ -42,6 +55,12 @@ class ScheduleTab(ScheduleBase, ScheduleUI):
else:
item.setCheckState(QtCore.Qt.Unchecked)
self.wifiListWidget.addItem(item)
self.wifiListWidget.itemChanged.connect(self.save_wifi_item)
def save_wifi_item(self, item):
db_item = WifiSettingModel.get(ssid=item.text(), profile=self.profile)
db_item.allowed = item.isSelected()
db_item.save()
def init_logs(self):
header = self.logTableWidget.horizontalHeader()
@ -64,15 +83,17 @@ class ScheduleTab(ScheduleBase, ScheduleUI):
self.logTableWidget.setItem(row, 4, QTableWidgetItem(str(log_line.returncode)))
self.logTableWidget.setRowCount(len(event_logs))
def set_next_backup_datetime(self):
if self.app.scheduler is not None:
job = self.app.scheduler.get_job('create-backup')
self.nextBackupDateTimeLabel.setText(job.next_run_time.strftime('%Y-%m-%d %H:%M'))
else:
self.nextBackupDateTimeLabel.setText('Off')
self.nextBackupDateTimeLabel.repaint()
def on_scheduler_apply(self):
# Save checking options
self.profile.validation_weeks = self.validationSpinBox.value()
self.profile.validation_on = self.validationCheckBox.isChecked()
# Save pruning options
self.profile.prune_on = self.pruneCheckBox.isChecked()
for i in self.prune_intervals:
setattr(self.profile, f'prune_{i}', getattr(self, f'prune_{i}').value())
# Save scheduler timing and activate if needed.
for label, obj in self.schedulerRadioMapping.items():
if obj.isChecked():
self.profile.schedule_mode = label
@ -81,10 +102,6 @@ class ScheduleTab(ScheduleBase, ScheduleUI):
qtime = self.scheduleFixedTime.time()
self.profile.schedule_fixed_hour, self.profile.schedule_fixed_minute = qtime.hour(), qtime.minute()
self.profile.save()
self.app.scheduler = init_scheduler()
self.set_next_backup_datetime()
def init_log(self):
pass
self.app.scheduler.reload()
self.nextBackupDateTimeLabel.setText(self.app.scheduler.next_job())
self.nextBackupDateTimeLabel.repaint()

View File

@ -2,44 +2,42 @@
import pytest
import io
import peewee
from PyQt5.QtWidgets import QApplication
from PyQt5 import QtCore
from PyQt5.QtWidgets import QMenu, QApplication, QSystemTrayIcon
import vorta.borg_runner
import vorta.models
from vorta.views.main_window import MainWindow
from vorta.application import VortaApp
from vorta.views.repo_add import AddRepoWindow
from vorta.models import EventLogModel, RepoModel
from vorta.tray_menu import TrayMenu
from vorta.scheduler import init_scheduler
from vorta.models import BackupProfileModel, EventLogModel, RepoModel
@pytest.fixture()
def main(tmpdir, qtbot):
def app(tmpdir):
tmp_db = tmpdir.join('settings.sqlite')
mock_db = peewee.SqliteDatabase(str(tmp_db))
vorta.models.init_db(mock_db)
return VortaApp([])
app = QApplication([])
app.thread = None
app.setQuitOnLastWindowClosed(False)
app.scheduler = init_scheduler()
TrayMenu(app)
app.profile = BackupProfileModel.get(id=1)
main = MainWindow(app)
main.show()
@pytest.fixture()
def main(app, qtbot):
main = app.main_window
qtbot.addWidget(main)
return main
# def test_tray(app, qtbot):
# # app.tray.activated.emit(QSystemTrayIcon.Context)
# menu = app.tray.contextMenu()
# qtbot.addWidget(menu)
# menu.popup(QtCore.QPoint())
def test_repo_tab(main, qtbot, mocker):
def test_repo_tab(main, qtbot):
qtbot.mouseClick(main.createStartBtn, QtCore.Qt.LeftButton)
assert main.createProgressText.text() == 'Add a remote backup repository first.'
qtbot.mouseClick(main.createStartBtn, QtCore.Qt.LeftButton)
def test_repo_add(main, qtbot, mocker):
# Add new repo window