add backup functionality
This commit is contained in:
parent
a37125ec87
commit
d82f2da824
|
@ -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)
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
|
@ -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>
|
||||
|
|
|
@ -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:')))
|
Loading…
Reference in New Issue