diff --git a/README.md b/README.md
index 0fe45b2..370c0d7 100644
--- a/README.md
+++ b/README.md
@@ -101,9 +101,12 @@ features here: [todos.md](docs/todos.md)
## Used packages
-- [PyQt5](https://pyqt.readthedocs.io/en/latest/) - the GUI framework
-- [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/) - used for
- creating the binary
+- [PyQt5](https://pyqt.readthedocs.io/en/latest/) - the GUI framework
+- [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/) - used for
+ 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
diff --git a/borg_qt/borg_interface.py b/borg_qt/borg_interface.py
index d460f39..6d499e8 100644
--- a/borg_qt/borg_interface.py
+++ b/borg_qt/borg_interface.py
@@ -99,7 +99,7 @@ class BackupThread(BorgQtThread):
self.command = ['borg', 'create', '--log-json', '--json',
('::'
+ self.prefix
- + '{now:%Y-%m-%d_%H:%M:%S}')]
+ + '{now:%Y-%m-%d_%H:%M:%S,%f}')]
self.command.extend(self.includes)
if self.excludes:
self.command.extend(self.excludes)
@@ -190,3 +190,24 @@ class MountThread(BorgQtThread):
def create_command(self):
self.command = ['borg', 'mount', '--log-json',
('::' + 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
diff --git a/borg_qt/config.py b/borg_qt/config.py
index 9317b85..dec0296 100644
--- a/borg_qt/config.py
+++ b/borg_qt/config.py
@@ -126,6 +126,14 @@ class Config(QDialog):
def hide_help(self):
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):
"""Gets the provided option from the configparser object."""
if option in self.config['borgqt']:
@@ -141,6 +149,14 @@ class Config(QDialog):
else:
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):
"""searches for the configuration file and returns its full path."""
home = os.environ['HOME']
@@ -313,6 +329,14 @@ class Config(QDialog):
self.schedule_predefined_name, Qt.MatchFixedString)
self.combo_schedule_predefined.setCurrentIndex(index)
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):
"""Writes the changed options back into the configparser object."""
@@ -357,6 +381,17 @@ class Config(QDialog):
self.config['borgqt']['excludes'] = json.dumps(excludes,
indent=4,
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()
# create and enable the required systemd files
diff --git a/borg_qt/main_window.py b/borg_qt/main_window.py
index d53b5d3..0568156 100644
--- a/borg_qt/main_window.py
+++ b/borg_qt/main_window.py
@@ -99,10 +99,13 @@ class MainWindow(QMainWindow):
def background_backup(self):
self.config.read()
self.config._set_environment_variables()
- thread = borg.BackupThread(self.config.includes,
+ backup_thread = borg.BackupThread(self.config.includes,
excludes=self.config.excludes,
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):
"""returns the path of the item selected in the file tree."""
@@ -117,20 +120,23 @@ class MainWindow(QMainWindow):
def create_backup(self):
"""Creates a backup of the selected item in the treeview."""
- if self.mount_paths:
- if self.yes_no("To create an archive you need to unmout all "
- "archives. Do you want to continue?"):
- self._umount_archives()
- else:
- return
+ if not self._check_mounts():
+ return
try:
self._check_path()
- thread = borg.BackupThread([self.src_path],
+ backup_thread = borg.BackupThread([self.src_path],
excludes=self.config.excludes,
prefix=self.config.prefix)
- dialog = ProgressDialog(thread)
- dialog.label_info.setText("Borg-Qt is currently creating an archive.")
- dialog.exec_()
+ backup_dialog = ProgressDialog(backup_thread)
+ backup_dialog.label_info.setText("Borg-Qt is currently creating an"
+ " 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()
except BorgException as e:
show_error(e)
@@ -183,6 +189,8 @@ class MainWindow(QMainWindow):
def delete_backup(self):
"""Deletes the selected archive from the repository."""
+ if not self._check_mounts():
+ return
try:
archive_name = self.selected_archive
except AttributeError:
@@ -198,7 +206,7 @@ class MainWindow(QMainWindow):
thread = borg.DeleteThread(archive_name)
dialog = ProgressDialog(thread)
dialog.label_info.setText(
- "Borg-Qt is currently deleting a backup.")
+ "Borg-Qt is currently deleting an archive.")
dialog.exec_()
self.update_ui()
except BorgException as e:
@@ -266,6 +274,15 @@ class MainWindow(QMainWindow):
# Opens the path in a file manager
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):
"""Simple yes/no dialog.
diff --git a/borg_qt/static/UI/MainWindow.ui b/borg_qt/static/UI/MainWindow.ui
index ca251c3..7d0c30e 100644
--- a/borg_qt/static/UI/MainWindow.ui
+++ b/borg_qt/static/UI/MainWindow.ui
@@ -56,7 +56,7 @@
- Archives
+ Archives in Repository:
diff --git a/borg_qt/static/UI/Settings.ui b/borg_qt/static/UI/Settings.ui
index 2c64089..bbf3246 100644
--- a/borg_qt/static/UI/Settings.ui
+++ b/borg_qt/static/UI/Settings.ui
@@ -32,7 +32,7 @@
false
-
+
10
@@ -438,6 +438,131 @@
+
+
+ Retention Policy
+
+
+
+
+ 10
+ 10
+ 541
+ 191
+
+
+
+ -
+
+
+
+ 16777215
+ 30
+
+
+
+ Enable
+
+
+ false
+
+
+ false
+
+
+
+ -
+
+
-
+
+
+ Daily:
+
+
+
+ -
+
+
+ 0
+
+
+ 7
+
+
+
+ -
+
+
+ 0
+
+
+ 4
+
+
+
+ -
+
+
+ Hourly:
+
+
+
+ -
+
+
+ Weekly:
+
+
+
+ -
+
+
+ Monthly:
+
+
+
+ -
+
+
+ 0
+
+
+ 12
+
+
+
+ -
+
+
+ 0
+
+
+ 24
+
+
+
+ -
+
+
+ 0
+
+
+ 1
+
+
+
+ -
+
+
+ Yearly:
+
+
+
+
+
+
+
+
diff --git a/docs/borg_qt.conf.example b/docs/borg_qt.conf.example
index ad72be3..d4790b3 100644
--- a/docs/borg_qt.conf.example
+++ b/docs/borg_qt.conf.example
@@ -26,6 +26,14 @@ schedule_month = 0
schedule_time = 12:00:00
schedule_predefined_enabled = True
schedule_predefined_name = hourly
+retention_policy_enabled = False
+retention_policy = {
+ "hourly": "24",
+ "daily": "7",
+ "weekly": "4",
+ "monthly": "12",
+ "yearly": "1"
+ }
[borgqt]
includes = [
diff --git a/requirements.txt b/requirements.txt
index 1368bdd..b03964d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
pyinstaller
PyQt5
-borgbackup[fuse]==1.1.8
pytest
+pytest-cov
diff --git a/tests/conftest.py b/tests/conftest.py
index 1c2e03c..d76caf5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -67,3 +67,15 @@ def archives(repository):
def target_path(tmpdir):
yield 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
diff --git a/tests/test_borg.py b/tests/test_borg.py
index 92d4c69..248b737 100644
--- a/tests/test_borg.py
+++ b/tests/test_borg.py
@@ -57,3 +57,13 @@ def test_mount(target_path, archives):
assert os.path.exists(
os.path.join(mount_path, os.path.realpath(__file__)))
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)
+
diff --git a/tests/test_config.py b/tests/test_config.py
index 98c6be5..db06162 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -121,3 +121,12 @@ def test_exclude_remove(form):
form.config.list_exclude.setCurrentRow(0)
form.config.remove_exclude()
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'