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)