add backup functionality

This commit is contained in:
Andreas Zweili 2019-02-02 11:27:38 +01:00
parent a37125ec87
commit d82f2da824
6 changed files with 286 additions and 3 deletions

127
borg_qt/borg_interface.py Normal file
View File

@ -0,0 +1,127 @@
import subprocess
import json
from PyQt5.QtCore import QThread
from helper import BorgException
from progress import ProgressDialog
def _process_json_error(json_err):
if json_err:
error = json_err.splitlines()[0]
if 'stale' in error:
pass
else:
err = json.loads(error)
raise BorgException(err['message'])
def _process_json_archives(json_output):
archives = []
if json_output:
output = json.loads(json_output)
for i in output['archives']:
archives.append(i)
return archives
def _process_json_repo_stats(json_output):
if json_output:
output = json.loads(json_output)
stats = output['cache']['stats']
return stats
def _get_json_archives():
p = subprocess.Popen(['borg', 'list', '--log-json', '--json'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf8')
json_output, json_err = p.communicate()
_process_json_error(json_err)
return json_output
def _process_prefix(prefix):
if prefix:
return prefix + "_"
else:
return ""
def _process_includes(includes):
return ' '.join(includes)
def _process_excludes(excludes):
if excludes:
processed_items = []
for item in excludes:
processed_items.append('--exclude=' + '"' + item + '"')
return ' '.join(processed_items)
else:
return ""
class BackupThread(QThread):
def __init__(self, includes, excludes=None, prefix=None):
super().__init__()
self.includes = _process_includes(includes)
self.excludes = _process_excludes(excludes)
self.prefix = _process_prefix(prefix)
def stop(self):
self.p.kill()
self.json_err = None
def run(self):
"""Function to create a backup with borg.
Args:
prefix (str) the prefix for the archive name.
raw_includes (list) a list of all the paths to backup.
raw_excludes (list) a list of all the paths to exclude from the backup.
"""
self.p = subprocess.Popen(['borg', 'create', '--log-json', '--json',
('::'
+ self.prefix
+ '{now:%Y-%m-%d_%H:%M:%S}'),
self.includes,
self.excludes],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf8')
self.json_output, self.json_err = self.p.communicate()
self.p.wait()
_process_json_error(self.json_err)
def backup(includes, excludes=None, prefix=None):
thread = BackupThread(includes, excludes=excludes, prefix=prefix)
dialog = ProgressDialog(thread)
dialog.label_info.setText("creating a backup.")
dialog.exec_()
def background_backup(includes, excludes=None, prefix=None):
thread = BackupThread(includes, excludes=excludes, prefix=prefix)
thread.run()
def get_archives():
"""Returns a list of all the archives in the repository."""
return _process_json_archives(_get_json_archives())
def get_repository_stats():
p = subprocess.Popen(['borg', 'info', '--log-json', '--json'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf8')
json_output, json_err = p.communicate()
_process_json_error(json_err)
return _process_json_repo_stats(json_output)

View File

@ -1,3 +1,4 @@
import math
from PyQt5.QtWidgets import QMessageBox
@ -13,3 +14,15 @@ def show_error(e):
message.setWindowTitle("Borg-Qt Error")
message.setInformativeText(e.args[0])
message.exec_()
# taken from here: https://stackoverflow.com/a/14822210/7723859
def convert_size(size_bytes):
"""A function to display file sizes in human readable form."""
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1000, i)
s = round(size_bytes / p, 2)
return "%s %s" % (s, size_name[i])

View File

@ -3,10 +3,11 @@ import sys
from PyQt5 import uic
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtWidgets import QMainWindow, QFileSystemModel
from config import Config
from helper import BorgException, show_error
from helper import BorgException, show_error, convert_size
import borg_interface as borg
class MainWindow(QMainWindow):
@ -28,8 +29,24 @@ class MainWindow(QMainWindow):
# Create a Config object for storing the configuration.
self.config = Config()
# File tree
model = QFileSystemModel()
# model.setRootPath('/')
model.setRootPath(os.getenv('HOME'))
self.treeview_files.setModel(model)
self.treeview_files.expandAll()
self.treeview_files.setIndentation(20)
self.treeview_files.setColumnHidden(1, True)
self.treeview_files.setColumnHidden(2, True)
self.treeview_files.setColumnHidden(3, True)
# return the clicking on an item in the tree
self.treeview_files.clicked.connect(self.get_selected_path)
self.list_archive.setSortingEnabled(True)
# Connecting actions and buttons.
self.action_settings.triggered.connect(self.show_settings)
self.action_backup.triggered.connect(self.create_backup)
def start(self):
"""This method is intendet to be used only once at the application
@ -38,6 +55,8 @@ class MainWindow(QMainWindow):
try:
self.config.read()
self.config._set_environment_variables()
self._update_archives()
self._update_repository_stats()
except BorgException as e:
show_error(e)
sys.exit(1)
@ -46,3 +65,59 @@ class MainWindow(QMainWindow):
"""Display the settings dialog."""
self.config.set_form_values()
self.config.exec_()
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)
def _check_path(self):
message = ("Please select a file or directory "
"before creating a backup.")
if not hasattr(self, 'src_path'):
raise BorgException(message)
def create_backup(self):
"""Creates a backup of the selected item in the treeview."""
try:
self._check_path()
borg.backup([self.src_path], excludes=self.config.excludes,
prefix=self.config.prefix)
self.update_archives()
self.update_repository_stats()
except BorgException as e:
show_error(e)
def _update_archives(self):
"""Lists all the archive names in the UI."""
self.list_archive.clear()
archive_names = []
for archive in borg.get_archives():
archive_names.append(archive['name'])
self.list_archive.addItems(archive_names)
def update_archives(self):
"""Lists all the archive names in the UI."""
try:
self._update_archives()
except BorgException as e:
show_error(e)
def _update_repository_stats(self):
stats = borg.get_repository_stats()
self.label_repo_original_size.setText(
"Original Size: "
+ convert_size(stats['total_size']))
self.label_repo_compressed_size.setText(
"Compressed Size: "
+ convert_size(stats['total_csize']))
self.label_repo_deduplicated_size.setText(
"Deduplicated Size: "
+ convert_size(stats['unique_csize']))
def update_repository_stats(self):
try:
self._update_repository_stats()
except BorgException as e:
show_error(e)

23
borg_qt/progress.py Normal file
View File

@ -0,0 +1,23 @@
import os
import time
from PyQt5.QtWidgets import QDialog
from PyQt5 import uic
class ProgressDialog(QDialog):
"""The main window of the application. It provides the various functions to
control BorgBackup."""
def __init__(self, thread):
super(ProgressDialog, self).__init__()
# Load the UI file to get the dialogs layout.
dir_path = os.path.dirname(os.path.realpath(__file__))
ui_path = os.path.join(dir_path + '/static/UI/Progress.ui')
uic.loadUi(ui_path, self)
self.thread = thread
self.thread.finished.connect(self.close)
self.thread.start()
def reject(self):
self.thread.stop()
super().reject()

View File

@ -80,8 +80,11 @@
<height>23</height>
</rect>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>24</number>
<number>-1</number>
</property>
</widget>
</widget>

42
tests/test_borg.py Normal file
View File

@ -0,0 +1,42 @@
import os
import sys
import subprocess
from time import strftime
import shutil
from unittest.mock import MagicMock
from PyQt5.QtWidgets import QApplication
import context
from testcase import BorgQtTestCase
import borg_interface as borg
app = QApplication(sys.argv)
class BorgQtBackupTestCase(BorgQtTestCase):
def setUp(self):
super().setUp()
self.repository_path = '/tmp/test-borgqt'
os.environ['BORG_REPO'] = self.repository_path
os.environ['BORG_PASSPHRASE'] = 'foo'
os.environ['BORG_DISPLAY_PASSPHRASE'] = 'no'
subprocess.run(['borg', 'init',
'--encryption=repokey-blake2'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
def tearDown(self):
if os.path.exists(self.repository_path):
shutil.rmtree(self.repository_path)
def test_backup(self):
borg.background_backup(['.'])
output = subprocess.check_output(['borg', 'list'], encoding='utf8')
self.assertNotEqual(-1, output.find(strftime('%Y-%m-%d_%H:')))
def test_backup_with_prefix(self):
borg.background_backup(['.'], prefix='test')
output = subprocess.check_output(['borg', 'list'], encoding='utf8')
self.assertNotEqual(-1, output.find(strftime('test_%Y-%m-%d_%H:')))