From 38a3a6bfc785acad85ff0b80727ae5b0ff825bb7 Mon Sep 17 00:00:00 2001 From: Andreas Zweili Date: Mon, 18 Feb 2019 13:00:11 +0100 Subject: [PATCH] Add background functionality --- borg_qt/borg_qt.py | 16 +++- borg_qt/config.py | 157 +++++++++++++++++++++++++++++++++- borg_qt/helper.py | 14 +++ borg_qt/main_window.py | 8 ++ borg_qt/static/UI/Settings.ui | 124 +++++++++++++++++++++++++++ borg_qt/systemd.py | 31 +++++++ tests/test_systemd.py | 63 ++++++++++++++ tests/testcase.py | 6 ++ 8 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 borg_qt/systemd.py create mode 100644 tests/test_systemd.py diff --git a/borg_qt/borg_qt.py b/borg_qt/borg_qt.py index 18ed0b2..bce3391 100755 --- a/borg_qt/borg_qt.py +++ b/borg_qt/borg_qt.py @@ -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_()) diff --git a/borg_qt/config.py b/borg_qt/config.py index f77f517..86328cd 100644 --- a/borg_qt/config.py +++ b/borg_qt/config.py @@ -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: diff --git a/borg_qt/helper.py b/borg_qt/helper.py index c77ae11..3ef0114 100644 --- a/borg_qt/helper.py +++ b/borg_qt/helper.py @@ -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 diff --git a/borg_qt/main_window.py b/borg_qt/main_window.py index 8dbe708..026aabb 100644 --- a/borg_qt/main_window.py +++ b/borg_qt/main_window.py @@ -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) diff --git a/borg_qt/static/UI/Settings.ui b/borg_qt/static/UI/Settings.ui index c9de613..2c64089 100644 --- a/borg_qt/static/UI/Settings.ui +++ b/borg_qt/static/UI/Settings.ui @@ -314,6 +314,130 @@ + + + Schedule + + + + + 10 + 10 + 541 + 328 + + + + + + + + 16777215 + 30 + + + + Enable + + + false + + + false + + + + + + + Predefined Schedule + + + true + + + + + + + + + + + + + + + + 0 + 0 + 0 + 2000 + 1 + 1 + + + + hh:mm + + + + + + + Weekday: + + + + + + + At: + + + + + + + Month: + + + + + + + + + + Date: + + + + + + + 0 + + + 31 + + + 0 + + + + + + + Custom Schedule + + + + + + + + diff --git a/borg_qt/systemd.py b/borg_qt/systemd.py new file mode 100644 index 0000000..72262a8 --- /dev/null +++ b/borg_qt/systemd.py @@ -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]) diff --git a/tests/test_systemd.py b/tests/test_systemd.py new file mode 100644 index 0000000..38e247e --- /dev/null +++ b/tests/test_systemd.py @@ -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)) diff --git a/tests/testcase.py b/tests/testcase.py index b4f0b11..ced8c72 100644 --- a/tests/testcase.py +++ b/tests/testcase.py @@ -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)