mirror of https://github.com/borgbase/vorta
Subclass QApplication.
This commit is contained in:
parent
7549e73189
commit
c896388ae8
|
@ -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
|
||||
|
|
|
@ -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_())
|
||||
|
|
|
@ -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()
|
|
@ -43,7 +43,7 @@
|
|||
<number>10</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>20</number>
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>10</number>
|
||||
|
|
|
@ -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><html><head/><body><p>(<a href="https://borgbackup.readthedocs.io/en/stable/usage.html#borg-prune"><span style=" text-decoration: underline; color:#0000ff;">More</span></a>)</p></body></html></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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue