From b9d4d57f33bcb9dd1df6568fcfa2be84374f269f Mon Sep 17 00:00:00 2001 From: Manu Date: Mon, 29 Oct 2018 21:23:42 +0800 Subject: [PATCH] Add EventLog --- src/vorta/__main__.py | 7 + src/vorta/assets/UI/mainwindow.ui | 5 +- src/vorta/assets/UI/repoadd.ui | 7 +- src/vorta/assets/UI/scheduletab.ui | 423 ++++++++++++++-------- src/vorta/assets/icons/clock-o.svg | 2 + src/vorta/assets/icons/collection.qrc | 9 + src/vorta/assets/icons/server.svg | 2 + src/vorta/assets/icons/tasks.svg | 2 + src/vorta/assets/icons/wifi.svg | 2 + src/vorta/assets/icons/window-restore.svg | 2 + src/vorta/borg_runner.py | 14 +- src/vorta/models.py | 48 ++- src/vorta/scheduler.py | 23 +- src/vorta/utils.py | 16 +- src/vorta/views/repo_tab.py | 1 - src/vorta/views/schedule_tab.py | 75 +++- 16 files changed, 468 insertions(+), 170 deletions(-) create mode 100644 src/vorta/assets/icons/clock-o.svg create mode 100644 src/vorta/assets/icons/collection.qrc create mode 100644 src/vorta/assets/icons/server.svg create mode 100644 src/vorta/assets/icons/tasks.svg create mode 100644 src/vorta/assets/icons/wifi.svg create mode 100644 src/vorta/assets/icons/window-restore.svg diff --git a/src/vorta/__main__.py b/src/vorta/__main__.py index 3d43186c..a11a5784 100644 --- a/src/vorta/__main__.py +++ b/src/vorta/__main__.py @@ -2,11 +2,18 @@ import sys from PyQt5.QtWidgets import QApplication from vorta.tray_menu import TrayMenu from vorta.scheduler import init_scheduler +from vorta.models import BackupProfileModel app = QApplication(sys.argv) app.thread = None app.setQuitOnLastWindowClosed(False) TrayMenu(app) app.scheduler = init_scheduler() +app.profile = BackupProfileModel.get(id=1) + +if not getattr(sys, 'frozen', False): + from .views.main_window import MainWindow + ex = MainWindow() + ex.show() sys.exit(app.exec_()) diff --git a/src/vorta/assets/UI/mainwindow.ui b/src/vorta/assets/UI/mainwindow.ui index e11dee2d..a72e95cc 100644 --- a/src/vorta/assets/UI/mainwindow.ui +++ b/src/vorta/assets/UI/mainwindow.ui @@ -54,7 +54,10 @@ - 2 + 0 + + + false diff --git a/src/vorta/assets/UI/repoadd.ui b/src/vorta/assets/UI/repoadd.ui index c407f012..70984d86 100644 --- a/src/vorta/assets/UI/repoadd.ui +++ b/src/vorta/assets/UI/repoadd.ui @@ -100,8 +100,11 @@ Qt::PlainText + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + - true + false @@ -122,7 +125,7 @@ - + diff --git a/src/vorta/assets/UI/scheduletab.ui b/src/vorta/assets/UI/scheduletab.ui index c9760016..5c169691 100644 --- a/src/vorta/assets/UI/scheduletab.ui +++ b/src/vorta/assets/UI/scheduletab.ui @@ -22,8 +22,14 @@ false + + QToolBox::tab { +color: black; +font-weight: bold; +} + - 1 + 2 @@ -31,7 +37,7 @@ 0 0 663 - 415 + 381 @@ -40,140 +46,205 @@ false + + + ../icons/clock-o.svg../icons/clock-o.svg + Schedule - - - - 0 - 0 - 661 - 101 - - - - - 10 - - - - - QLayout::SetFixedSize - - - - - - 0 - 0 - - - - Backup Manually - - - - - - - - - - - - 0 - 0 - - - - Backup every - - - - - - - - - - - 0 - 0 - - - - hours at - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - minutes past the hour. - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Backup daily at - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - + + + + + QLayout::SetFixedSize + + + + + + 0 + 0 + + + + Backup Manually + + + + + + + + + + + + 0 + 0 + + + + Backup every + + + + + + + 1 + + + 48 + + + + + + + + 0 + 0 + + + + hours at + + + + + + + 59 + + + + + + + + 0 + 0 + + + + minutes past the hour. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Backup daily at + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + 0 + + + 0 + + + + + Apply + + + + + + + + 75 + true + + + + Next Backup: + + + + + + + Off + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + @@ -181,22 +252,82 @@ 0 0 663 - 415 + 381 - - Rules + + + ../icons/wifi.svg../icons/wifi.svg - - - - 10 - 30 - 256 - 192 - - - + + Networks + + + + + + + + + Allowed Networks: + + + + + + + + + ../icons/tasks.svg../icons/tasks.svg + + + Log + + + + + + + 11 + + + + false + + + true + + + false + + + + Time + + + + + Category + + + + + Subcommand + + + + + Message + + + + + Returncode + + + + + diff --git a/src/vorta/assets/icons/clock-o.svg b/src/vorta/assets/icons/clock-o.svg new file mode 100644 index 00000000..2f102fac --- /dev/null +++ b/src/vorta/assets/icons/clock-o.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/vorta/assets/icons/collection.qrc b/src/vorta/assets/icons/collection.qrc new file mode 100644 index 00000000..378b05de --- /dev/null +++ b/src/vorta/assets/icons/collection.qrc @@ -0,0 +1,9 @@ + + + wifi.svg + tasks.svg + clock-o.svg + server.svg + window-restore.svg + + diff --git a/src/vorta/assets/icons/server.svg b/src/vorta/assets/icons/server.svg new file mode 100644 index 00000000..870c3124 --- /dev/null +++ b/src/vorta/assets/icons/server.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/vorta/assets/icons/tasks.svg b/src/vorta/assets/icons/tasks.svg new file mode 100644 index 00000000..ba3b0444 --- /dev/null +++ b/src/vorta/assets/icons/tasks.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/vorta/assets/icons/wifi.svg b/src/vorta/assets/icons/wifi.svg new file mode 100644 index 00000000..42fdc06e --- /dev/null +++ b/src/vorta/assets/icons/wifi.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/vorta/assets/icons/window-restore.svg b/src/vorta/assets/icons/window-restore.svg new file mode 100644 index 00000000..55c8b43e --- /dev/null +++ b/src/vorta/assets/icons/window-restore.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/vorta/borg_runner.py b/src/vorta/borg_runner.py index dc8fdf8f..a92700e6 100644 --- a/src/vorta/borg_runner.py +++ b/src/vorta/borg_runner.py @@ -10,7 +10,7 @@ from PyQt5 import QtCore from PyQt5.QtWidgets import QApplication from subprocess import Popen, PIPE -from .models import SourceDirModel, BackupProfileModel +from .models import SourceDirModel, BackupProfileModel, EventLogModel class BorgThread(QtCore.QThread): @@ -31,18 +31,22 @@ class BorgThread(QtCore.QThread): env['BORG_HOSTNAME_IS_UNIQUE'] = '1' if params.get('password') and params['password']: env['BORG_PASSPHRASE'] = params['password'] + params['password'] = '***' env['BORG_RSH'] = 'ssh -oStrictHostKeyChecking=no' if params.get('ssh_key') and params['ssh_key']: env['BORG_RSH'] += f' -i ~/.ssh/{params["ssh_key"]}' self.env = env + self.profile = BackupProfileModel.get(id=1) self.params = params self.process = None def run(self): + log_entry = EventLogModel(category='borg-run', subcommand=self.cmd[1], params=self.params) + log_entry.save() self.process = Popen(self.cmd, stdout=PIPE, stderr=PIPE, bufsize=1, universal_newlines=True, env=self.env) - for line in iter(self.process.stderr.readline, b''): + for line in iter(self.process.stderr.readline, ''): try: parsed = json.loads(line) if parsed['type'] == 'log_message': @@ -57,12 +61,15 @@ class BorgThread(QtCore.QThread): result = { 'params': self.params, 'returncode': self.process.returncode, + 'cmd': self.cmd } try: result['data'] = json.loads(stdout) except: result['data'] = {} + log_entry.returncode = self.process.returncode + log_entry.save() self.result.emit(result) @classmethod @@ -80,9 +87,6 @@ class BorgThread(QtCore.QThread): params = {'password': keyring.get_password("vorta-repo", profile.repo.url)} - print(params) - - if app.thread and app.thread.isRunning(): ret['message'] = 'Backup is already in progress.' return ret diff --git a/src/vorta/models.py b/src/vorta/models.py index 7c5045cf..81c13705 100644 --- a/src/vorta/models.py +++ b/src/vorta/models.py @@ -1,8 +1,23 @@ import peewee import os +import json from datetime import datetime from .config import SETTINGS_DIR +class JSONField(peewee.TextField): + """ + Class to "fake" a JSON field with a text field. Not efficient but works nicely + + From: https://gist.github.com/rosscdh/f4f26758b0228f475b132c688f15af2b + """ + def db_value(self, value): + """Convert the python value for storage in the database.""" + return value if value is None else json.dumps(value) + + def python_value(self, value): + """Convert the database value to a pythonic value.""" + return value if value is None else json.loads(value) + db = peewee.SqliteDatabase(os.path.join(SETTINGS_DIR, 'settings.db')) @@ -29,6 +44,11 @@ class BackupProfileModel(peewee.Model): compression = peewee.CharField(default='lz4') exclude_patterns = peewee.TextField(null=True) exclude_if_present = peewee.TextField(null=True) + schedule_mode = peewee.CharField(default='off') + schedule_interval_hours = peewee.IntegerField(default=3) + schedule_interval_minutes = peewee.IntegerField(default=42) + schedule_fixed_hour = peewee.IntegerField(default=3) + schedule_fixed_minute = peewee.IntegerField(default=42) class Meta: database = db @@ -58,7 +78,33 @@ class SnapshotModel(peewee.Model): database = db +class WifiSettingModel(peewee.Model): + """Save Wifi Settings""" + ssid = peewee.CharField() + last_connected = peewee.DateTimeField() + allowed = peewee.BooleanField(default=True) + profile = peewee.ForeignKeyField(BackupProfileModel, default=1) + + class Meta: + database = db + + +class EventLogModel(peewee.Model): + """Keep a log of background jobs.""" + start_time = peewee.DateTimeField(default=datetime.utcnow) + category = peewee.CharField() + subcommand = peewee.CharField(null=True) + message = peewee.CharField(null=True) + returncode = peewee.IntegerField(default=1) + params = JSONField(null=True) + profile = peewee.ForeignKeyField(BackupProfileModel, default=1) + + class Meta: + database = db + + db.connect() -db.create_tables([RepoModel, BackupProfileModel, SourceDirModel, SnapshotModel]) +db.create_tables([RepoModel, BackupProfileModel, SourceDirModel, + SnapshotModel, WifiSettingModel, EventLogModel]) BackupProfileModel.get_or_create(id=1, name='Default') diff --git a/src/vorta/scheduler.py b/src/vorta/scheduler.py index 2e5bd938..f7791d5a 100644 --- a/src/vorta/scheduler.py +++ b/src/vorta/scheduler.py @@ -1,12 +1,31 @@ from apscheduler.schedulers.qt import QtScheduler from apscheduler.triggers import cron +from PyQt5.QtWidgets import QApplication + +from .models import BackupProfileModel + def tick(): - print('scheduler') + print('scheduler running') + def init_scheduler(): + app = QApplication.instance() + if hasattr(app, 'scheduler') and app.scheduler is not None: + app.scheduler.shutdown() + s = QtScheduler() - trigger = cron.CronTrigger(second='*/3') + + 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(tick, trigger, id='create-backup') s.start() return s diff --git a/src/vorta/utils.py b/src/vorta/utils.py index c317a79b..ee40653f 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -5,7 +5,9 @@ from paramiko.rsakey import RSAKey from paramiko.ecdsakey import ECDSAKey from paramiko.ed25519key import Ed25519Key from paramiko import SSHException +from PyQt5.QtWidgets import QApplication +from .models import WifiSettingModel def get_private_keys(): """Find SSH keys in standard folder.""" @@ -41,7 +43,7 @@ def prettyBytes(size): n = 0 Dic_powerN = {0: '', 1: 'K', 2: 'M', 3: 'G', 4: 'T'} while size > power: - size /= power + size /= power n += 1 return str(round(size))+Dic_powerN[n]+'B' @@ -58,12 +60,16 @@ def get_asset(path): def get_sorted_wifis(): """Get SSIDs from OS and merge with settings in DB.""" + app = QApplication.instance() - wifis = plistlib.load(open('/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist', 'rb'))['KnownNetworks'] - out = [] + plist_file = open('/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist', 'rb') + wifis = plistlib.load(plist_file)['KnownNetworks'] if wifis: for wifi in wifis.values(): timestamp = wifi['LastConnected'] ssid = wifi['SSIDString'] - out.append({'ssid': ssid, 'last_connected': timestamp, 'allowed': True}) - return out + WifiSettingModel.get_or_create(ssid=ssid, profile=app.profile, + defaults={'last_connected': timestamp, + 'allowed': True}) + + return WifiSettingModel.select().order_by(-WifiSettingModel.last_connected) diff --git a/src/vorta/views/repo_tab.py b/src/vorta/views/repo_tab.py index a4f8c657..f5bd3835 100644 --- a/src/vorta/views/repo_tab.py +++ b/src/vorta/views/repo_tab.py @@ -63,7 +63,6 @@ class RepoTab(RepoBase, RepoUI): else: self.profile.ssh_key = self.sshComboBox.itemData(index) self.profile.save() - print('set ssh key to', self.profile.ssh_key) def compression_select_action(self, index): diff --git a/src/vorta/views/schedule_tab.py b/src/vorta/views/schedule_tab.py index c0c508b5..6c0f97cb 100644 --- a/src/vorta/views/schedule_tab.py +++ b/src/vorta/views/schedule_tab.py @@ -1,27 +1,88 @@ -import plistlib - from PyQt5 import uic, QtCore -from PyQt5.QtWidgets import QFileDialog, QListWidgetItem -from ..models import SourceDirModel +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 uifile = get_asset('UI/scheduletab.ui') ScheduleUI, ScheduleBase = uic.loadUiType(uifile) - class ScheduleTab(ScheduleBase, ScheduleUI): def __init__(self, parent=None): super().__init__(parent) self.setupUi(parent) self.profile = self.window().profile + self.app = QApplication.instance() + self.schedulerRadioMapping = { + 'off': self.scheduleOffRadio, + 'interval': self.scheduleIntervalRadio, + 'fixed': self.scheduleFixedRadio + } + self.schedulerRadioMapping[self.profile.schedule_mode].setChecked(True) + + self.scheduleIntervalHours.setValue(self.profile.schedule_interval_hours) + self.scheduleIntervalMinutes.setValue(self.profile.schedule_interval_minutes) + self.scheduleFixedTime.setTime( + QtCore.QTime(self.profile.schedule_fixed_hour, self.profile.schedule_fixed_minute)) + + self.scheduleApplyButton.clicked.connect(self.on_scheduler_apply) + + self.set_next_backup_datetime() self.init_wifi() + self.init_logs() def init_wifi(self): for wifi in get_sorted_wifis(): item = QListWidgetItem() - item.setText(wifi['ssid']) + item.setText(wifi.ssid) item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) - if wifi['allowed']: + if wifi.allowed: item.setCheckState(QtCore.Qt.Checked) self.wifiListWidget.addItem(item) + + def init_logs(self): + header = self.logTableWidget.horizontalHeader() + header.setVisible(True) + [header.setSectionResizeMode(i, QHeaderView.ResizeToContents) for i in range(5)] + header.setSectionResizeMode(3, QHeaderView.Stretch) + + self.logTableWidget.setSelectionBehavior(QTableView.SelectRows) + self.logTableWidget.setEditTriggers(QTableView.NoEditTriggers) + + event_logs = [s for s in EventLogModel.select()] + + for row, log_line in enumerate(event_logs): + self.logTableWidget.insertRow(row) + formatted_time = log_line.start_time.strftime('%Y-%m-%d %H:%M') + self.logTableWidget.setItem(row, 0, QTableWidgetItem(formatted_time)) + self.logTableWidget.setItem(row, 1, QTableWidgetItem(log_line.category)) + self.logTableWidget.setItem(row, 2, QTableWidgetItem(log_line.subcommand)) + self.logTableWidget.setItem(row, 3, QTableWidgetItem(log_line.message)) + 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): + for label, obj in self.schedulerRadioMapping.items(): + if obj.isChecked(): + self.profile.schedule_mode = label + self.profile.schedule_interval_hours = self.scheduleIntervalHours.value() + self.profile.schedule_interval_minutes = self.scheduleIntervalMinutes.value() + 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