Add background functionality

This commit is contained in:
Andreas Zweili 2019-02-18 13:00:11 +01:00
parent 0fa077989d
commit 38a3a6bfc7
8 changed files with 412 additions and 7 deletions

View File

@ -1,15 +1,23 @@
#!/usr/bin/env python
from PyQt5.QtWidgets import QApplication
import sys
from PyQt5.QtWidgets import QApplication
from main_window import MainWindow
from helper import get_parser
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
window.start()
parser = get_parser()
args = parser.parse_args()
sys.exit(app.exec_())
# only show the application if there's no background flag
if args.background:
window.background_backup()
else:
window.show()
window.start()
sys.exit(app.exec_())

View File

@ -1,15 +1,21 @@
import os
import configparser
import json
from distutils import util
from datetime import datetime
import subprocess
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog, QFileDialog
from PyQt5 import uic
from helper import BorgException
from systemd import SystemdFile
class Config(QDialog):
"""A class to read, display and write the Borg-Qt configuration."""
def __init__(self):
super(QDialog, self).__init__()
# Load the UI file to get the dialogs layout.
@ -28,13 +34,23 @@ class Config(QDialog):
self.button_restore_exclude_defaults.clicked.connect(
self.restore_exclude_defaults)
weekdays = ['', 'Monday', 'Thuesday', 'Weekdays', 'Thursday', 'Friday',
'Saturday', 'Sunday']
self.combo_schedule_weekday.addItems(weekdays)
months = ['', 'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November',
'December']
self.combo_schedule_month.addItems(months)
schedules = ['hourly', 'daily', 'weekly', 'monthly']
self.combo_schedule_predefined.addItems(schedules)
@property
def full_path(self):
"""returns the repository path or the repository server path if a
server was provided in the configuration."""
if self._return_single_option('repository_path'):
if self._return_single_option('server'):
return self._create_server_path()
return self._create_server_path()
else:
return self._return_single_option('repository_path')
else:
@ -72,6 +88,40 @@ class Config(QDialog):
def prefix(self):
return self._return_single_option('prefix')
@property
def schedule_enabled(self):
return util.strtobool(self._return_single_option('schedule_enabled'))
@property
def schedule_predefined_enabled(self):
return util.strtobool(
self._return_single_option('schedule_predefined_enabled'))
@property
def schedule_custom_enabled(self):
return util.strtobool(
self._return_single_option('schedule_custom_enabled'))
@property
def schedule_time(self):
return self._return_single_option('schedule_time')
@property
def schedule_weekday(self):
return int(self._return_single_option('schedule_weekday'))
@property
def schedule_month(self):
return int(self._return_single_option('schedule_month'))
@property
def schedule_date(self):
return int(self._return_single_option('schedule_date'))
@property
def schedule_predefined_name(self):
return self._return_single_option('schedule_predefined_name')
def _return_single_option(self, option):
"""Gets the provided option from the configparser object."""
if option in self.config['borgqt']:
@ -123,7 +173,7 @@ class Config(QDialog):
"""Qt dialog to select an exisiting file."""
dialog = QFileDialog
dialog.ExistingFile
file_path, ignore = dialog.getOpenFileName(
file_path, _ = dialog.getOpenFileName(
self, "Select Directory", os.getenv('HOME'), "All Files (*)")
return file_path
@ -134,6 +184,62 @@ class Config(QDialog):
return dialog.getExistingDirectory(
self, "Select Directory", os.getenv('HOME'))
def _create_service(self):
self.service = SystemdFile('borg_qt.service')
self.service.content['Service'] = {}
self.service.content['Unit']['Description'] = ("Runs Borg-Qt once in "
"the backround to take "
"a backup according to "
"the configuration.")
self.service.content['Service']['Type'] = 'oneshot'
process = subprocess.run(["which", "borg_qt"], stdout=subprocess.PIPE,
encoding='utf8')
output = process.stdout.strip()
self.service.content['Service']['ExecStart'] = output + ' -B'
self.service.write()
def _create_timer(self, schedule_interval):
self.timer = SystemdFile('borg_qt.timer')
self.timer.content['Timer'] = {}
self.timer.content['Install'] = {}
self.timer.content['Unit']['Description'] = ("Starts the "
"borg_qt.service "
"according to the "
"configured "
"schedule.")
self.timer.content['Timer']['OnCalendar'] = schedule_interval
self.timer.content['Timer']['Persistent'] = 'true'
self.timer.content['Install']['WantedBy'] = 'timers.target'
self.timer.write()
def _parse_schedule_interval(self):
if self.schedule_predefined_enabled:
return self.schedule_predefined_name
if self.schedule_custom_enabled:
if self.schedule_date > 0:
date = str(self.schedule_date)
else:
date = "*"
if self.schedule_month > 0:
month = str(self.schedule_month)
else:
month = "*"
if self.schedule_weekday > 0:
weekday = self.combo_schedule_weekday.currentText()
else:
weekday = ""
date_string = (weekday
+ " "
+ "*"
+ "-"
+ month
+ "-"
+ date
+ " "
+ self.schedule_time)
return date_string
def include_file(self):
"""add a file to the include list if the selected path is not empty."""
file_path = self._select_file()
@ -177,25 +283,57 @@ class Config(QDialog):
self.config.read(self.path)
def set_form_values(self):
# set the general tab values
self.line_edit_repository_path.setText(self.repository_path)
self.line_edit_password.setText(self.password)
self.line_edit_prefix.setText(self.prefix)
self.line_edit_server.setText(self.server)
self.line_edit_port.setText(self.port)
self.line_edit_user.setText(self.user)
# set the include tab values
self.list_include.clear()
self.list_include.addItems(self.includes)
# set the include tab values
self.list_exclude.clear()
self.list_exclude.addItems(self.excludes)
# set schedule tab values
if self.schedule_enabled:
self.check_schedule_enabled.setChecked(True)
if self.schedule_custom_enabled:
self.radio_schedule_custom_enabled.setChecked(True)
_time = datetime.strptime(self.schedule_time, '%H:%M:%S')
self.time_schedule_time.setTime(_time.time())
self.combo_schedule_weekday.setCurrentIndex(self.schedule_weekday)
self.combo_schedule_month.setCurrentIndex(self.schedule_month)
index = self.combo_schedule_predefined.findText(
self.schedule_predefined_name, Qt.MatchFixedString)
self.combo_schedule_predefined.setCurrentIndex(index)
self.spin_schedule_date.setValue(self.schedule_date)
def apply_options(self):
"""Writes the changed options back into the configparser object."""
self.config['borgqt']['repository_path'] = self.line_edit_repository_path.text()
self.config['borgqt']['repository_path'] = (
self.line_edit_repository_path.text())
self.config['borgqt']['password'] = self.line_edit_password.text()
self.config['borgqt']['prefix'] = self.line_edit_prefix.text()
self.config['borgqt']['server'] = self.line_edit_server.text()
self.config['borgqt']['port'] = self.line_edit_port.text()
self.config['borgqt']['user'] = self.line_edit_user.text()
self.config['borgqt']['schedule_enabled'] = (
str(self.check_schedule_enabled.isChecked()))
self.config['borgqt']['schedule_predefined_enabled'] = (
str(self.radio_schedule_predefined_enabled.isChecked()))
self.config['borgqt']['schedule_custom_enabled'] = (
str(self.radio_schedule_custom_enabled.isChecked()))
self.config['borgqt']['schedule_time'] = (
self.time_schedule_time.time().toString())
self.config['borgqt']['schedule_weekday'] = (
str(self.combo_schedule_weekday.currentIndex()))
self.config['borgqt']['schedule_month'] = (
str(self.combo_schedule_month.currentIndex()))
self.config['borgqt']['schedule_date'] = self.spin_schedule_date.text()
self.config['borgqt']['schedule_predefined_name'] = (
self.combo_schedule_predefined.currentText())
# Workaraound to get all items of a QListWidget as a list
excludes = []
@ -217,6 +355,19 @@ class Config(QDialog):
sort_keys=True)
self._set_environment_variables()
# create and enable the required systemd files
# if it is not enable make sure that the timer is disabled.
if self.schedule_enabled:
self._create_service()
self._create_timer(self._parse_schedule_interval())
self.timer.enable()
else:
active_timer_path = os.path.join(
os.environ['HOME'],
'.config/systemd/user/timer.targets.wants/borg_qt.timer')
if os.path.exists(active_timer_path):
self.timer.disable()
def write(self):
"""Write the configparser object back to the config file."""
with open(self.path, 'w+') as configfile:

View File

@ -1,3 +1,4 @@
import argparse
import os
import math
import shutil
@ -79,3 +80,16 @@ def check_path(path):
return True
exception = Exception("The selected path isn't writeable!")
show_error(exception)
def get_parser():
""" The argument parser of the command-line version """
parser = argparse.ArgumentParser(
description=('Create a backup in the background.'))
parser.add_argument(
'--background',
'-B',
help='Runs the application without showing the graphical interface.',
action='store_true')
return parser

View File

@ -89,6 +89,14 @@ class MainWindow(QMainWindow):
self.config.set_form_values()
self.config.exec_()
def background_backup(self):
self.config.read()
self.config._set_environment_variables()
thread = borg.BackupThread(self.config.includes,
excludes=self.config.excludes,
prefix=self.config.prefix)
thread.run()
def get_selected_path(self, signal):
"""returns the path of the item selected in the file tree."""
self.src_path = self.treeview_files.model().filePath(signal)

View File

@ -314,6 +314,130 @@
</property>
</widget>
</widget>
<widget class="QWidget" name="tab_schedule">
<attribute name="title">
<string>Schedule</string>
</attribute>
<widget class="QWidget" name="verticalLayoutWidget_2">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>541</width>
<height>328</height>
</rect>
</property>
<layout class="QVBoxLayout" name="layout_schedule">
<item>
<widget class="QCheckBox" name="check_schedule_enabled">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Enable</string>
</property>
<property name="autoExclusive">
<bool>false</bool>
</property>
<property name="tristate">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_schedule_predefined_enabled">
<property name="text">
<string>Predefined Schedule</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="combo_schedule_predefined"/>
</item>
<item>
<layout class="QGridLayout" name="layout_schedule_details">
<item row="2" column="1">
<widget class="QComboBox" name="combo_schedule_weekday"/>
</item>
<item row="1" column="1">
<widget class="QTimeEdit" name="time_schedule_time">
<property name="dateTime">
<datetime>
<hour>0</hour>
<minute>0</minute>
<second>0</second>
<year>2000</year>
<month>1</month>
<day>1</day>
</datetime>
</property>
<property name="displayFormat">
<string>hh:mm</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_schedule_weekday">
<property name="text">
<string>Weekday:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_schedule_time">
<property name="text">
<string>At:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_month">
<property name="text">
<string>Month:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="combo_schedule_month"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_schedule_date">
<property name="text">
<string>Date:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="spin_schedule_date">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>31</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QRadioButton" name="radio_schedule_custom_enabled">
<property name="text">
<string>Custom Schedule</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</widget>
</widget>
<resources/>

31
borg_qt/systemd.py Normal file
View File

@ -0,0 +1,31 @@
import configparser
import os
import subprocess
class SystemdFile():
def __init__(self, file_name):
self.file_name = file_name
self.path = os.path.join(os.environ['HOME'],
'.config/systemd/user/',
self.file_name)
self.content = configparser.ConfigParser()
self.content.optionxform = str
self.content['Unit'] = {}
def write(self):
with open(self.path, 'w+') as configfile:
self.content.write(configfile)
def enable(self):
subprocess.run(['systemctl', '--user', 'daemon-reload'])
subprocess.run(['systemctl', '--user', 'enable',
self.file_name])
subprocess.run(['systemctl', '--user', 'restart',
self.file_name])
def disable(self):
subprocess.run(['systemctl', '--user', 'disable',
self.file_name])
subprocess.run(['systemctl', '--user', 'stop',
self.file_name])

63
tests/test_systemd.py Normal file
View File

@ -0,0 +1,63 @@
import os
import unittest
from testcase import TestSystemd
import context
from systemd import SystemdFile
class TestSystemdUnit(TestSystemd):
def setUp(self):
self.path = os.path.join(os.environ['HOME'],
'.config/systemd/user/borg_qt.service')
def test_write_unit(self):
systemd_unit = SystemdFile('borg_qt.service')
systemd_unit.write()
self.assertTrue(os.path.exists(self.path))
class TestSystemdTimer(TestSystemd):
def setUp(self):
self.path = os.path.join(os.environ['HOME'],
'.config/systemd/user/borg_qt.timer')
def test_write_timer(self):
systemd_timer = SystemdFile('borg_qt.timer')
systemd_timer.write()
self.assertTrue(os.path.exists(self.path))
class TestSystemdEnabledTimer(TestSystemd):
def setUp(self):
self.symlink_path = os.path.join(os.environ['HOME'],
'.config/systemd/user/timers.target.wants/borg_qt.timer')
self.path = os.path.join(os.environ['HOME'],
'.config/systemd/user/borg_qt.timer')
def tearDown(self):
super().tearDown()
os.remove(self.symlink_path)
def test_enable_timer(self):
systemd_unit = SystemdFile('borg_qt.service')
systemd_timer = SystemdFile('borg_qt.timer')
systemd_unit.content['Unit'] = {}
systemd_unit.content['Unit']['After'] = 'default.target'
systemd_unit.content['Install'] = {}
systemd_unit.content['Install']['Wanted'] = 'default.target'
systemd_unit.content['Service'] = {}
systemd_unit.content['Service']['Type'] = 'forking'
systemd_unit.content['Service']['ExecStart'] = '/bin/echo "test"'
systemd_unit.write()
systemd_timer.content['Unit'] = {}
systemd_timer.content['Unit']['Description'] = 'Test Timer'
systemd_timer.content['Timer'] = {}
systemd_timer.content['Timer']['OnCalendar'] = 'daily'
systemd_timer.content['Timer']['Persistent'] = 'true'
systemd_timer.content['Install'] = {}
systemd_timer.content['Install']['WantedBy'] = 'timers.target'
systemd_timer.write()
systemd_timer.enable()
self.assertTrue(os.path.exists(self.symlink_path))

View File

@ -5,6 +5,7 @@ import shutil
import unittest
import warnings
import context
from main_window import MainWindow
from helper import remove_path
@ -39,3 +40,8 @@ class BorgInterfaceTest(unittest.TestCase):
def tearDown(self):
remove_path(self.repository_path)
class TestSystemd(unittest.TestCase):
def tearDown(self):
os.remove(self.path)