mirror of https://github.com/borgbase/vorta
Add Background notifications for Linux (#114)
- Add DBus-based notifications for Linux and BSD desktops (@bpereto) - Add module name to logging output (@bpereto)
This commit is contained in:
parent
51159c043d
commit
c5494f455c
|
@ -6,3 +6,4 @@ pytest-xdist
|
|||
pyinstaller
|
||||
tox
|
||||
bump2version
|
||||
flake8
|
||||
|
|
|
@ -32,7 +32,7 @@ font-weight: bold;
|
|||
}</string>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>3</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="schedule">
|
||||
<property name="geometry">
|
||||
|
@ -57,6 +57,9 @@ font-weight: bold;
|
|||
<string>Schedule</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="sizeConstraint">
|
||||
|
@ -285,6 +288,22 @@ font-weight: bold;
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="font">
|
||||
|
@ -427,6 +446,14 @@ font-weight: bold;
|
|||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_3">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>663</width>
|
||||
<height>354</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="icon">
|
||||
<iconset resource="../icons/collection.qrc">
|
||||
<normaloff>:/icons/terminal.svg</normaloff>:/icons/terminal.svg</iconset>
|
||||
|
|
|
@ -14,7 +14,7 @@ from ..models import EventLogModel, BackupProfileMixin
|
|||
from ..utils import keyring
|
||||
|
||||
mutex = QtCore.QMutex()
|
||||
logger = logging.getLogger('vorta')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BorgThread(QtCore.QThread, BackupProfileMixin):
|
||||
|
|
|
@ -13,8 +13,20 @@ from .config import LOG_DIR
|
|||
logger = logging.getLogger('vorta')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# create handlers
|
||||
fh = logging.FileHandler(os.path.join(LOG_DIR, 'vorta.log'))
|
||||
# fh.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.WARNING)
|
||||
|
||||
# create logging format
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
# apply formatter
|
||||
fh.setFormatter(formatter)
|
||||
ch.setFormatter(formatter)
|
||||
|
||||
# add handlers
|
||||
logger.addHandler(fh)
|
||||
logger.addHandler(ch)
|
||||
|
|
|
@ -207,14 +207,20 @@ def init_db(con):
|
|||
'value': True,
|
||||
'type': 'checkbox',
|
||||
'label': 'Send errors to Sentry. This helps us quickly find bugs.'
|
||||
},
|
||||
{
|
||||
'key': 'enable_notifications', 'value': True, 'type': 'checkbox',
|
||||
'label': 'Display notifications when background tasks fail.'
|
||||
},
|
||||
{
|
||||
'key': 'enable_notifications_success', 'value': False, 'type': 'checkbox',
|
||||
'label': 'Also notify about successful background tasks.'
|
||||
}
|
||||
]
|
||||
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.'},
|
||||
{'key': 'updates_include_beta', 'value': False, 'type': 'checkbox',
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import sys
|
||||
import logging
|
||||
from PyQt5 import QtCore, QtDBus
|
||||
from vorta.models import SettingsModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VortaNotifications:
|
||||
"""
|
||||
|
@ -12,33 +16,89 @@ class VortaNotifications:
|
|||
@classmethod
|
||||
def pick(cls):
|
||||
if sys.platform == 'darwin':
|
||||
return DarwinNotifications
|
||||
elif sys.platform == 'linux':
|
||||
return LinuxNotifications
|
||||
else: # Save to sqlite as fallback?
|
||||
return LinuxNotifications
|
||||
return DarwinNotifications()
|
||||
elif QtDBus.QDBusConnection.sessionBus().isConnected():
|
||||
return DBusNotifications()
|
||||
else:
|
||||
logger.warning('could not pick valid notification class')
|
||||
return cls()
|
||||
|
||||
def deliver(self, title, text, level='info'):
|
||||
"""Dummy notifier if we're not on macOS or Linux notifier isn't available."""
|
||||
pass
|
||||
|
||||
def notifications_suppressed(self, level):
|
||||
"""Decide if notification is sent or not based on settings and level."""
|
||||
if not SettingsModel.get(key='enable_notifications').value:
|
||||
logger.debug('notifications suppressed')
|
||||
return True
|
||||
if level == 'info' and not SettingsModel.get(key='enable_notifications_success').value:
|
||||
logger.debug('success notifications suppressed')
|
||||
return True
|
||||
|
||||
logger.debug('notification not suppressed')
|
||||
return False
|
||||
|
||||
|
||||
class DarwinNotifications(VortaNotifications):
|
||||
def deliver(self, title, text):
|
||||
if SettingsModel.get(key='enable_notifications').value:
|
||||
from Foundation import NSUserNotification
|
||||
from Foundation import NSUserNotificationCenter
|
||||
|
||||
notification = NSUserNotification.alloc().init()
|
||||
notification.setTitle_(title)
|
||||
notification.setInformativeText_(text)
|
||||
center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||
if center is not None: # Only works when run from app bundle.
|
||||
center.deliverNotification_(notification)
|
||||
|
||||
|
||||
class LinuxNotifications(VortaNotifications):
|
||||
"""
|
||||
Could use the Gnome libs or the binary
|
||||
|
||||
https://wiki.archlinux.org/index.php/Desktop_notifications#Python
|
||||
http://manpages.ubuntu.com/manpages/cosmic/man1/notify-send.1.html
|
||||
Notify via notification center and pyobjc bridge.
|
||||
"""
|
||||
def deliver(self, title, text):
|
||||
|
||||
def deliver(self, title, text, level='info'):
|
||||
if self.notifications_suppressed(level):
|
||||
return
|
||||
|
||||
from Foundation import NSUserNotification, NSUserNotificationCenter
|
||||
notification = NSUserNotification.alloc().init()
|
||||
notification.setTitle_(title)
|
||||
notification.setInformativeText_(text)
|
||||
center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||
if center is not None: # Only works when run from app bundle.
|
||||
return center.deliverNotification_(notification)
|
||||
|
||||
|
||||
class DBusNotifications(VortaNotifications):
|
||||
"""
|
||||
Use qt-dbus to send notifications.
|
||||
|
||||
Adapted from http://codito.in/notifications-in-qt-over-dbus/
|
||||
"""
|
||||
|
||||
URGENCY = {'info': 1, 'error': 2}
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _dbus_notify(self, header, msg, level='info'):
|
||||
item = "org.freedesktop.Notifications"
|
||||
path = "/org/freedesktop/Notifications"
|
||||
interface = "org.freedesktop.Notifications"
|
||||
app_name = "vorta"
|
||||
v = QtCore.QVariant(12321) # random int to identify all notifications
|
||||
if v.convert(QtCore.QVariant.UInt):
|
||||
id_replace = v
|
||||
icon = ""
|
||||
title = header
|
||||
text = msg
|
||||
actions_list = QtDBus.QDBusArgument([], QtCore.QMetaType.QStringList)
|
||||
hint = {'urgency': self.URGENCY[level]}
|
||||
time = 5000 # milliseconds for display timeout
|
||||
|
||||
bus = QtDBus.QDBusConnection.sessionBus()
|
||||
notify = QtDBus.QDBusInterface(item, path, interface, bus)
|
||||
if notify.isValid():
|
||||
x = notify.call(QtDBus.QDBus.AutoDetect, "Notify", app_name,
|
||||
id_replace, icon, title, text,
|
||||
actions_list, hint, time)
|
||||
if x.errorName():
|
||||
logger.warning("Failed to send notification!")
|
||||
logger.warning(x.errorMessage())
|
||||
else:
|
||||
logger.warning("Invalid dbus interface")
|
||||
|
||||
def deliver(self, title, text, level='info'):
|
||||
if self.notifications_suppressed(level):
|
||||
return
|
||||
|
||||
self._dbus_notify(title, text)
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import logging
|
||||
from datetime import date, timedelta
|
||||
|
||||
from apscheduler.schedulers.qt import QtScheduler
|
||||
from apscheduler.triggers import cron
|
||||
|
||||
from vorta.borg.create import BorgCreateThread
|
||||
from .models import BackupProfileModel, EventLogModel
|
||||
from vorta.borg.prune import BorgPruneThread
|
||||
from vorta.borg.list_repo import BorgListRepoThread
|
||||
from vorta.borg.check import BorgCheckThread
|
||||
from vorta.borg.create import BorgCreateThread
|
||||
from vorta.borg.list_repo import BorgListRepoThread
|
||||
from vorta.borg.prune import BorgPruneThread
|
||||
|
||||
from .models import BackupProfileModel, EventLogModel
|
||||
from .notifications import VortaNotifications
|
||||
|
||||
logger = logging.getLogger('vorta')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VortaScheduler(QtScheduler):
|
||||
|
@ -45,7 +46,7 @@ class VortaScheduler(QtScheduler):
|
|||
minute=profile.schedule_fixed_minute)
|
||||
if self.get_job(job_id) is not None and trigger is not None:
|
||||
self.reschedule_job(job_id, trigger=trigger)
|
||||
notifier = VortaNotifications.pick()()
|
||||
notifier = VortaNotifications.pick()
|
||||
notifier.deliver('Vorta Scheduler', 'Background scheduler was changed.')
|
||||
logger.debug('Job for profile %s was rescheduled.', profile.name)
|
||||
elif trigger is not None:
|
||||
|
@ -85,9 +86,12 @@ class VortaScheduler(QtScheduler):
|
|||
return job.next_run_time.strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
def create_backup(self, profile_id):
|
||||
notifier = VortaNotifications.pick()()
|
||||
notifier = VortaNotifications.pick()
|
||||
profile = BackupProfileModel.get(id=profile_id)
|
||||
|
||||
logger.info('Starting background backup for %s', profile.name)
|
||||
notifier.deliver('Vorta Backup', 'Starting background backup for %s.' % profile.name, level='info')
|
||||
|
||||
msg = BorgCreateThread.prepare(profile)
|
||||
if msg['ok']:
|
||||
logger.info('Preparation for backup successful.')
|
||||
|
@ -95,14 +99,16 @@ class VortaScheduler(QtScheduler):
|
|||
thread.start()
|
||||
thread.wait()
|
||||
if thread.process.returncode in [0, 1]:
|
||||
notifier.deliver('Vorta Backup', 'Backup successful for %s.' % profile.name, level='info')
|
||||
logger.info('Backup creation successful.')
|
||||
self.post_backup_tasks(profile_id)
|
||||
else:
|
||||
notifier.deliver('Vorta Backup', 'Error during backup creation.')
|
||||
notifier.deliver('Vorta Backup', 'Error during backup creation.', level='error')
|
||||
logger.error('Error during backup creation.')
|
||||
else:
|
||||
logger.error('Conditions for backup not met. Aborting.')
|
||||
logger.error(msg['message'])
|
||||
notifier.deliver('Vorta Backup', msg['message'])
|
||||
notifier.deliver('Vorta Backup', msg['message'], level='error')
|
||||
|
||||
def post_backup_tasks(self, profile_id):
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import sys
|
||||
import pytest
|
||||
from PyQt5 import QtDBus
|
||||
|
||||
import vorta.borg
|
||||
import vorta.models
|
||||
import vorta.notifications
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != 'linux', reason="DBus notifications only on Linux")
|
||||
def test_linux_background_notifications(app, mocker):
|
||||
"""We can't see notifications, but we watch for exceptions and errors."""
|
||||
|
||||
notifier = vorta.notifications.VortaNotifications.pick()
|
||||
assert isinstance(notifier, vorta.notifications.DBusNotifications)
|
||||
notifier.deliver('Vorta Test', 'test notification', level='error')
|
||||
|
||||
mocker.spy(QtDBus.QDBusInterface, 'call')
|
||||
notifier.deliver('Vorta Test', 'test notification', level='info') # fails if called.
|
||||
assert QtDBus.QDBusInterface.call.call_count == 0
|
Loading…
Reference in New Issue