Merge branch 'master' into package
This commit is contained in:
commit
874a0bfafb
|
@ -101,9 +101,12 @@ features here: [todos.md](docs/todos.md)
|
||||||
|
|
||||||
## Used packages
|
## Used packages
|
||||||
|
|
||||||
- [PyQt5](https://pyqt.readthedocs.io/en/latest/) - the GUI framework
|
- [PyQt5](https://pyqt.readthedocs.io/en/latest/) - the GUI framework
|
||||||
- [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/) - used for
|
- [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/) - used for
|
||||||
creating the binary
|
creating the binary
|
||||||
|
- [pytest](https://docs.pytest.org/en/latest/) - used for testing
|
||||||
|
- [pytest-cov](https://pytest-cov.readthedocs.io/en/latest/) - used for
|
||||||
|
coverage analysis
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ class BackupThread(BorgQtThread):
|
||||||
self.command = ['borg', 'create', '--log-json', '--json',
|
self.command = ['borg', 'create', '--log-json', '--json',
|
||||||
('::'
|
('::'
|
||||||
+ self.prefix
|
+ self.prefix
|
||||||
+ '{now:%Y-%m-%d_%H:%M:%S}')]
|
+ '{now:%Y-%m-%d_%H:%M:%S,%f}')]
|
||||||
self.command.extend(self.includes)
|
self.command.extend(self.includes)
|
||||||
if self.excludes:
|
if self.excludes:
|
||||||
self.command.extend(self.excludes)
|
self.command.extend(self.excludes)
|
||||||
|
@ -190,3 +190,24 @@ class MountThread(BorgQtThread):
|
||||||
def create_command(self):
|
def create_command(self):
|
||||||
self.command = ['borg', 'mount', '--log-json',
|
self.command = ['borg', 'mount', '--log-json',
|
||||||
('::' + self.archive_name), self.mount_path]
|
('::' + self.archive_name), self.mount_path]
|
||||||
|
|
||||||
|
|
||||||
|
class PruneThread(BorgQtThread):
|
||||||
|
"""Prunes the repository according to the given retention policy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
policy (dict) the name of the archive to restore.
|
||||||
|
"""
|
||||||
|
def __init__(self, policy):
|
||||||
|
self.policy = self._process_policy(policy)
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def create_command(self):
|
||||||
|
self.command = ['borg', 'prune', '--log-json']
|
||||||
|
self.command.extend(self.policy)
|
||||||
|
|
||||||
|
def _process_policy(self, raw_policy):
|
||||||
|
policy = []
|
||||||
|
for key, value in raw_policy.items():
|
||||||
|
policy.append('--keep-' + key + "=" + value)
|
||||||
|
return policy
|
||||||
|
|
|
@ -126,6 +126,14 @@ class Config(QDialog):
|
||||||
def hide_help(self):
|
def hide_help(self):
|
||||||
return util.strtobool(self._return_single_option('hide_help'))
|
return util.strtobool(self._return_single_option('hide_help'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def retention_policy_enabled(self):
|
||||||
|
return util.strtobool(self._return_single_option('retention_policy_enabled'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def retention_policy(self):
|
||||||
|
return self._return_dict_option('retention_policy')
|
||||||
|
|
||||||
def _return_single_option(self, option):
|
def _return_single_option(self, option):
|
||||||
"""Gets the provided option from the configparser object."""
|
"""Gets the provided option from the configparser object."""
|
||||||
if option in self.config['borgqt']:
|
if option in self.config['borgqt']:
|
||||||
|
@ -141,6 +149,14 @@ class Config(QDialog):
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def _return_dict_option(self, option):
|
||||||
|
"""Reads the provided option from the configparser object and returns
|
||||||
|
it as a dict."""
|
||||||
|
if option in self.config['borgqt']:
|
||||||
|
return json.loads(self.config['borgqt'][option])
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
def _get_path(self):
|
def _get_path(self):
|
||||||
"""searches for the configuration file and returns its full path."""
|
"""searches for the configuration file and returns its full path."""
|
||||||
home = os.environ['HOME']
|
home = os.environ['HOME']
|
||||||
|
@ -313,6 +329,14 @@ class Config(QDialog):
|
||||||
self.schedule_predefined_name, Qt.MatchFixedString)
|
self.schedule_predefined_name, Qt.MatchFixedString)
|
||||||
self.combo_schedule_predefined.setCurrentIndex(index)
|
self.combo_schedule_predefined.setCurrentIndex(index)
|
||||||
self.spin_schedule_date.setValue(self.schedule_date)
|
self.spin_schedule_date.setValue(self.schedule_date)
|
||||||
|
if self.retention_policy_enabled:
|
||||||
|
self.check_policy_enabled.setChecked(True)
|
||||||
|
policy = self.retention_policy
|
||||||
|
self.spin_policy_hourly.setValue(int(policy['hourly']))
|
||||||
|
self.spin_policy_daily.setValue(int(policy['daily']))
|
||||||
|
self.spin_policy_weekly.setValue(int(policy['weekly']))
|
||||||
|
self.spin_policy_monthly.setValue(int(policy['monthly']))
|
||||||
|
self.spin_policy_yearly.setValue(int(policy['yearly']))
|
||||||
|
|
||||||
def apply_options(self):
|
def apply_options(self):
|
||||||
"""Writes the changed options back into the configparser object."""
|
"""Writes the changed options back into the configparser object."""
|
||||||
|
@ -357,6 +381,17 @@ class Config(QDialog):
|
||||||
self.config['borgqt']['excludes'] = json.dumps(excludes,
|
self.config['borgqt']['excludes'] = json.dumps(excludes,
|
||||||
indent=4,
|
indent=4,
|
||||||
sort_keys=True)
|
sort_keys=True)
|
||||||
|
self.config['borgqt']['retention_policy_enabled'] = (
|
||||||
|
str(self.check_policy_enabled.isChecked()))
|
||||||
|
retention_policy = {}
|
||||||
|
retention_policy['hourly'] = self.spin_policy_hourly.text()
|
||||||
|
retention_policy['daily'] = self.spin_policy_daily.text()
|
||||||
|
retention_policy['weekly'] = self.spin_policy_weekly.text()
|
||||||
|
retention_policy['monthly'] = self.spin_policy_monthly.text()
|
||||||
|
retention_policy['yearly'] = self.spin_policy_yearly.text()
|
||||||
|
self.config['borgqt']['retention_policy'] = json.dumps(retention_policy,
|
||||||
|
indent=4,
|
||||||
|
sort_keys=True)
|
||||||
self._set_environment_variables()
|
self._set_environment_variables()
|
||||||
|
|
||||||
# create and enable the required systemd files
|
# create and enable the required systemd files
|
||||||
|
|
|
@ -99,10 +99,13 @@ class MainWindow(QMainWindow):
|
||||||
def background_backup(self):
|
def background_backup(self):
|
||||||
self.config.read()
|
self.config.read()
|
||||||
self.config._set_environment_variables()
|
self.config._set_environment_variables()
|
||||||
thread = borg.BackupThread(self.config.includes,
|
backup_thread = borg.BackupThread(self.config.includes,
|
||||||
excludes=self.config.excludes,
|
excludes=self.config.excludes,
|
||||||
prefix=self.config.prefix)
|
prefix=self.config.prefix)
|
||||||
thread.run()
|
backup_thread.run()
|
||||||
|
if self.config.retention_policy_enabled:
|
||||||
|
prune_thread = borg.PruneThread(self.config.retention_policy)
|
||||||
|
prune_thread.run()
|
||||||
|
|
||||||
def get_selected_path(self, signal):
|
def get_selected_path(self, signal):
|
||||||
"""returns the path of the item selected in the file tree."""
|
"""returns the path of the item selected in the file tree."""
|
||||||
|
@ -117,20 +120,23 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def create_backup(self):
|
def create_backup(self):
|
||||||
"""Creates a backup of the selected item in the treeview."""
|
"""Creates a backup of the selected item in the treeview."""
|
||||||
if self.mount_paths:
|
if not self._check_mounts():
|
||||||
if self.yes_no("To create an archive you need to unmout all "
|
return
|
||||||
"archives. Do you want to continue?"):
|
|
||||||
self._umount_archives()
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
self._check_path()
|
self._check_path()
|
||||||
thread = borg.BackupThread([self.src_path],
|
backup_thread = borg.BackupThread([self.src_path],
|
||||||
excludes=self.config.excludes,
|
excludes=self.config.excludes,
|
||||||
prefix=self.config.prefix)
|
prefix=self.config.prefix)
|
||||||
dialog = ProgressDialog(thread)
|
backup_dialog = ProgressDialog(backup_thread)
|
||||||
dialog.label_info.setText("Borg-Qt is currently creating an archive.")
|
backup_dialog.label_info.setText("Borg-Qt is currently creating an"
|
||||||
dialog.exec_()
|
" archive.")
|
||||||
|
backup_dialog.exec_()
|
||||||
|
if self.config.retention_policy_enabled:
|
||||||
|
prune_thread = borg.PruneThread(self.config.retention_policy)
|
||||||
|
prune_dialog = ProgressDialog(prune_thread)
|
||||||
|
prune_dialog.label_info.setText("Borg-Qt is currently pruning "
|
||||||
|
"the repository.")
|
||||||
|
prune_dialog.exec_()
|
||||||
self.update_ui()
|
self.update_ui()
|
||||||
except BorgException as e:
|
except BorgException as e:
|
||||||
show_error(e)
|
show_error(e)
|
||||||
|
@ -183,6 +189,8 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def delete_backup(self):
|
def delete_backup(self):
|
||||||
"""Deletes the selected archive from the repository."""
|
"""Deletes the selected archive from the repository."""
|
||||||
|
if not self._check_mounts():
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
archive_name = self.selected_archive
|
archive_name = self.selected_archive
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -198,7 +206,7 @@ class MainWindow(QMainWindow):
|
||||||
thread = borg.DeleteThread(archive_name)
|
thread = borg.DeleteThread(archive_name)
|
||||||
dialog = ProgressDialog(thread)
|
dialog = ProgressDialog(thread)
|
||||||
dialog.label_info.setText(
|
dialog.label_info.setText(
|
||||||
"Borg-Qt is currently deleting a backup.")
|
"Borg-Qt is currently deleting an archive.")
|
||||||
dialog.exec_()
|
dialog.exec_()
|
||||||
self.update_ui()
|
self.update_ui()
|
||||||
except BorgException as e:
|
except BorgException as e:
|
||||||
|
@ -266,6 +274,15 @@ class MainWindow(QMainWindow):
|
||||||
# Opens the path in a file manager
|
# Opens the path in a file manager
|
||||||
open_path(mount_path)
|
open_path(mount_path)
|
||||||
|
|
||||||
|
def _check_mounts(self):
|
||||||
|
if self.mount_paths:
|
||||||
|
if self.yes_no("To proceed you need to unmout all "
|
||||||
|
"archives. Do you want to continue?"):
|
||||||
|
self._umount_archives()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def yes_no(self, question):
|
def yes_no(self, question):
|
||||||
"""Simple yes/no dialog.
|
"""Simple yes/no dialog.
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Archives</string>
|
<string>Archives in Repository:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QTabWidget" name="tab_widget">
|
<widget class="QTabWidget" name="tab_policy">
|
||||||
<property name="geometry">
|
<property name="geometry">
|
||||||
<rect>
|
<rect>
|
||||||
<x>10</x>
|
<x>10</x>
|
||||||
|
@ -438,6 +438,131 @@
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QWidget" name="tab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Retention Policy</string>
|
||||||
|
</attribute>
|
||||||
|
<widget class="QWidget" name="verticalLayoutWidget_4">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>10</y>
|
||||||
|
<width>541</width>
|
||||||
|
<height>191</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="layout_policy">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="check_policy_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>
|
||||||
|
<layout class="QGridLayout" name="layout_policy_details">
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_policy_daily">
|
||||||
|
<property name="text">
|
||||||
|
<string>Daily:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QSpinBox" name="spin_policy_daily">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>7</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QSpinBox" name="spin_policy_weekly">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>4</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_policy_hourly">
|
||||||
|
<property name="text">
|
||||||
|
<string>Hourly:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_policy_weekly">
|
||||||
|
<property name="text">
|
||||||
|
<string>Weekly:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="label_policy_monthly">
|
||||||
|
<property name="text">
|
||||||
|
<string>Monthly:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QSpinBox" name="spin_policy_monthly">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>12</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QSpinBox" name="spin_policy_hourly">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>24</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1">
|
||||||
|
<widget class="QSpinBox" name="spin_policy_yearly">
|
||||||
|
<property name="minimum">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="label_policy_yearly">
|
||||||
|
<property name="text">
|
||||||
|
<string>Yearly:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
|
|
@ -26,6 +26,14 @@ schedule_month = 0
|
||||||
schedule_time = 12:00:00
|
schedule_time = 12:00:00
|
||||||
schedule_predefined_enabled = True
|
schedule_predefined_enabled = True
|
||||||
schedule_predefined_name = hourly
|
schedule_predefined_name = hourly
|
||||||
|
retention_policy_enabled = False
|
||||||
|
retention_policy = {
|
||||||
|
"hourly": "24",
|
||||||
|
"daily": "7",
|
||||||
|
"weekly": "4",
|
||||||
|
"monthly": "12",
|
||||||
|
"yearly": "1"
|
||||||
|
}
|
||||||
|
|
||||||
[borgqt]
|
[borgqt]
|
||||||
includes = [
|
includes = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
pyinstaller
|
pyinstaller
|
||||||
PyQt5
|
PyQt5
|
||||||
borgbackup[fuse]==1.1.8
|
|
||||||
pytest
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
|
|
@ -67,3 +67,15 @@ def archives(repository):
|
||||||
def target_path(tmpdir):
|
def target_path(tmpdir):
|
||||||
yield str(tmpdir)
|
yield str(tmpdir)
|
||||||
remove_path(str(tmpdir))
|
remove_path(str(tmpdir))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def create_archive():
|
||||||
|
def _create_archive(number_of_turns):
|
||||||
|
while number_of_turns > 0:
|
||||||
|
backup_thread = borg.BackupThread(['.'])
|
||||||
|
backup_thread.run()
|
||||||
|
number_of_turns -= 1
|
||||||
|
list_thread = borg.ListThread()
|
||||||
|
return list_thread.run()
|
||||||
|
return _create_archive
|
||||||
|
|
|
@ -57,3 +57,13 @@ def test_mount(target_path, archives):
|
||||||
assert os.path.exists(
|
assert os.path.exists(
|
||||||
os.path.join(mount_path, os.path.realpath(__file__)))
|
os.path.join(mount_path, os.path.realpath(__file__)))
|
||||||
os.system('borg umount ' + mount_path)
|
os.system('borg umount ' + mount_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prune(repository, create_archive):
|
||||||
|
archive_list = create_archive(2)
|
||||||
|
thread = borg.PruneThread({'hourly': '1'})
|
||||||
|
thread.run()
|
||||||
|
list_thread = borg.ListThread()
|
||||||
|
repo_archives = list_thread.run()
|
||||||
|
assert len(archive_list) > len(repo_archives)
|
||||||
|
|
||||||
|
|
|
@ -121,3 +121,12 @@ def test_exclude_remove(form):
|
||||||
form.config.list_exclude.setCurrentRow(0)
|
form.config.list_exclude.setCurrentRow(0)
|
||||||
form.config.remove_exclude()
|
form.config.remove_exclude()
|
||||||
assert (counter >= form.config.list_exclude.count())
|
assert (counter >= form.config.list_exclude.count())
|
||||||
|
|
||||||
|
|
||||||
|
def test_retention_config(form):
|
||||||
|
assert form.config.retention_policy_enabled == False
|
||||||
|
assert form.config.retention_policy['hourly'] == '24'
|
||||||
|
assert form.config.retention_policy['daily'] == '7'
|
||||||
|
assert form.config.retention_policy['weekly'] == '4'
|
||||||
|
assert form.config.retention_policy['monthly'] == '12'
|
||||||
|
assert form.config.retention_policy['yearly'] == '1'
|
||||||
|
|
Loading…
Reference in New Issue