Add background functionality
This commit is contained in:
parent
0fa077989d
commit
38a3a6bfc7
|
@ -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_())
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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])
|
|
@ -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))
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue