From c896388ae801dcdc0e6c533c615243e36febe4c3 Mon Sep 17 00:00:00 2001 From: Manu Date: Wed, 31 Oct 2018 19:14:12 +0800 Subject: [PATCH] Subclass QApplication. --- README.md | 5 ++ src/vorta/__main__.py | 18 +--- src/vorta/application.py | 42 ++++++++++ src/vorta/assets/UI/mainwindow.ui | 2 +- src/vorta/assets/UI/scheduletab.ui | 129 ++++++++++++++++++++++++++++- src/vorta/models.py | 6 ++ src/vorta/scheduler.py | 55 +++++------- src/vorta/tray_menu.py | 39 +++------ src/vorta/views/schedule_tab.py | 53 ++++++++---- tests/test_repo.py | 32 ++++--- 10 files changed, 266 insertions(+), 115 deletions(-) create mode 100644 src/vorta/application.py diff --git a/README.md b/README.md index 76da5b6a..fa9d70fe 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/vorta/__main__.py b/src/vorta/__main__.py index 0540b42c..a5fe34b7 100644 --- a/src/vorta/__main__.py +++ b/src/vorta/__main__.py @@ -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_()) diff --git a/src/vorta/application.py b/src/vorta/application.py new file mode 100644 index 00000000..dc99c31f --- /dev/null +++ b/src/vorta/application.py @@ -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() diff --git a/src/vorta/assets/UI/mainwindow.ui b/src/vorta/assets/UI/mainwindow.ui index 709f97ee..ac2acc0c 100644 --- a/src/vorta/assets/UI/mainwindow.ui +++ b/src/vorta/assets/UI/mainwindow.ui @@ -43,7 +43,7 @@ 10 - 20 + 10 10 diff --git a/src/vorta/assets/UI/scheduletab.ui b/src/vorta/assets/UI/scheduletab.ui index e63b6fa2..000973cf 100644 --- a/src/vorta/assets/UI/scheduletab.ui +++ b/src/vorta/assets/UI/scheduletab.ui @@ -60,7 +60,7 @@ font-weight: bold; QLayout::SetFixedSize - 10 + 7 @@ -71,7 +71,7 @@ font-weight: bold; - Backup Manually + Backup Manually. @@ -180,7 +180,7 @@ font-weight: bold; - 10 + 0 @@ -188,7 +188,10 @@ font-weight: bold; Validate repository data every - true + false + + + false @@ -234,6 +237,124 @@ font-weight: bold; + + + + 0 + + + + + Prune old Snapshots + + + + + + + <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> + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 0 + + + + + Keep + + + + + + + + + + hourly, + + + + + + + + + + daily, + + + + + + + + + + weekly, + + + + + + + + + + monthly and + + + + + + + + + + annual snapshots. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + diff --git a/src/vorta/models.py b/src/vorta/models.py index a7f8c75c..3ec18d0d 100644 --- a/src/vorta/models.py +++ b/src/vorta/models.py @@ -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 diff --git a/src/vorta/scheduler.py b/src/vorta/scheduler.py index a8de5bfb..a6831bc8 100644 --- a/src/vorta/scheduler.py +++ b/src/vorta/scheduler.py @@ -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' diff --git a/src/vorta/tray_menu.py b/src/vorta/tray_menu.py index 00a78492..ee77579f 100644 --- a/src/vorta/tray_menu.py +++ b/src/vorta/tray_menu.py @@ -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' diff --git a/src/vorta/views/schedule_tab.py b/src/vorta/views/schedule_tab.py index a03d5c9e..7124725a 100644 --- a/src/vorta/views/schedule_tab.py +++ b/src/vorta/views/schedule_tab.py @@ -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() diff --git a/tests/test_repo.py b/tests/test_repo.py index 2a7ae01f..7f7b7998 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -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