2019-02-02 11:27:38 +01:00
|
|
|
import subprocess
|
|
|
|
import json
|
|
|
|
|
|
|
|
from PyQt5.QtCore import QThread
|
|
|
|
|
2019-04-22 10:28:50 +02:00
|
|
|
from borg_qt.helper import BorgException, show_error
|
2019-02-02 11:27:38 +01:00
|
|
|
|
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
class BorgQtThread(QThread):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Provides the base for interfacing with borg. The method
|
|
|
|
self.create_command needs to be implemented on each child class in order to
|
|
|
|
make it work."""
|
2019-02-07 21:35:14 +01:00
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
2019-02-10 17:19:00 +01:00
|
|
|
self.create_process()
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def stop(self):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Kill the process when the thread stops."""
|
2019-02-07 21:35:14 +01:00
|
|
|
self.p.kill()
|
|
|
|
self.json_err = None
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-10 17:19:00 +01:00
|
|
|
def create_process(self):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Creates the process which executes borg."""
|
|
|
|
|
|
|
|
# self.create_command() needs to be implemented on each subclass.
|
2019-02-07 21:35:14 +01:00
|
|
|
self.create_command()
|
|
|
|
self.p = subprocess.Popen(self.command,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
encoding='utf8')
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def run(self):
|
|
|
|
self.json_output, self.json_err = self.p.communicate()
|
|
|
|
self.p.wait()
|
|
|
|
self.process_json_error(self.json_err)
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def process_json_error(self, json_err):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Looks in the returned json error string for errors and provides them
|
|
|
|
as BorgException in case there are any. Ignores errors about stale
|
|
|
|
locks of borg."""
|
2019-02-07 21:35:14 +01:00
|
|
|
if json_err:
|
|
|
|
error = json_err.splitlines()[0]
|
|
|
|
if 'stale' in error:
|
2019-02-16 10:16:07 +01:00
|
|
|
return
|
2019-02-07 21:35:14 +01:00
|
|
|
else:
|
|
|
|
err = json.loads(error)
|
2019-02-16 11:23:44 +01:00
|
|
|
raise BorgException(err['message'])
|
2019-02-02 11:27:38 +01:00
|
|
|
|
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
class ListThread(BorgQtThread):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Returns a list of all archives in the repository."""
|
2019-02-07 21:35:14 +01:00
|
|
|
def create_command(self):
|
|
|
|
self.command = ['borg', 'list', '--log-json', '--json']
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def run(self):
|
|
|
|
super().run()
|
|
|
|
self._process_json_archives()
|
|
|
|
return self.archives
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def _process_json_archives(self):
|
|
|
|
self.archives = []
|
|
|
|
if self.json_output:
|
|
|
|
output = json.loads(self.json_output)
|
|
|
|
for i in output['archives']:
|
|
|
|
self.archives.append(i)
|
2019-02-02 11:27:38 +01:00
|
|
|
|
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
class InfoThread(BorgQtThread):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Return the statistics about the current repository."""
|
2019-02-07 21:35:14 +01:00
|
|
|
def create_command(self):
|
|
|
|
self.command = ['borg', 'info', '--log-json', '--json']
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def run(self):
|
|
|
|
super().run()
|
|
|
|
self._process_json_repo_stats()
|
|
|
|
return self.stats
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def _process_json_repo_stats(self):
|
|
|
|
if self.json_output:
|
|
|
|
output = json.loads(self.json_output)
|
|
|
|
self.stats = output['cache']['stats']
|
2019-02-02 11:27:38 +01:00
|
|
|
|
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
class BackupThread(BorgQtThread):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Creates a backup with borg.
|
2019-02-02 14:54:37 +01:00
|
|
|
|
|
|
|
Args:
|
|
|
|
prefix (str) the prefix for the archive name.
|
|
|
|
includes (list) a list of all the paths to backup.
|
|
|
|
excludes (list) a list of all the paths to exclude from the backup.
|
|
|
|
"""
|
2019-02-02 11:27:38 +01:00
|
|
|
def __init__(self, includes, excludes=None, prefix=None):
|
2019-02-10 16:17:26 +01:00
|
|
|
self.includes = includes
|
2019-02-07 21:35:14 +01:00
|
|
|
self._process_excludes(excludes)
|
|
|
|
self._process_prefix(prefix)
|
2019-02-02 11:27:38 +01:00
|
|
|
super().__init__()
|
|
|
|
|
2019-02-04 16:24:12 +01:00
|
|
|
def create_command(self):
|
|
|
|
self.command = ['borg', 'create', '--log-json', '--json',
|
|
|
|
('::'
|
|
|
|
+ self.prefix
|
2019-04-28 20:01:17 +02:00
|
|
|
+ '{now:%Y-%m-%d_%H:%M:%S,%f}')]
|
2019-02-04 16:24:12 +01:00
|
|
|
self.command.extend(self.includes)
|
|
|
|
if self.excludes:
|
|
|
|
self.command.extend(self.excludes)
|
|
|
|
|
2019-02-16 11:29:30 +01:00
|
|
|
def run(self):
|
|
|
|
self.json_output, self.json_err = self.p.communicate()
|
|
|
|
self.p.wait()
|
|
|
|
try:
|
|
|
|
self.process_json_error(self.json_err)
|
|
|
|
except BorgException as e:
|
|
|
|
show_error(e)
|
|
|
|
self.stop()
|
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def _process_prefix(self, prefix):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Prepares the prefix for the final command."""
|
2019-02-07 21:35:14 +01:00
|
|
|
if prefix:
|
|
|
|
self.prefix = prefix + "_"
|
|
|
|
else:
|
|
|
|
self.prefix = ""
|
|
|
|
|
|
|
|
def _process_excludes(self, excludes):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Pairs every exclude with the required option for borg."""
|
2019-02-07 21:35:14 +01:00
|
|
|
processed_items = []
|
|
|
|
if excludes:
|
|
|
|
for item in excludes:
|
|
|
|
processed_items.extend(['-e', item])
|
|
|
|
self.excludes = processed_items
|
|
|
|
else:
|
|
|
|
self.excludes = processed_items
|
2019-02-02 11:27:38 +01:00
|
|
|
|
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
class RestoreThread(BorgQtThread):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Restores a backup with borg.
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-02 14:54:37 +01:00
|
|
|
Args:
|
|
|
|
archive_name (str) the name of the archive to restore.
|
|
|
|
restore_path (str) the path where to restore should get stored at.
|
|
|
|
"""
|
|
|
|
def __init__(self, archive_name, restore_path):
|
|
|
|
self.archive_name = archive_name
|
|
|
|
self.restore_path = restore_path
|
2019-02-07 21:35:14 +01:00
|
|
|
super().__init__()
|
2019-02-02 11:27:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def create_command(self):
|
|
|
|
self.command = ['borg', 'extract', '--log-json',
|
|
|
|
('::' + self.archive_name)]
|
2019-02-02 14:54:37 +01:00
|
|
|
|
2019-02-10 17:19:00 +01:00
|
|
|
def create_process(self):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""The create_process needs to get overwritten because borg restores
|
|
|
|
the archive into the current folder. Therefore the process needs to cd
|
|
|
|
into the target path."""
|
2019-02-07 21:35:14 +01:00
|
|
|
self.create_command()
|
|
|
|
self.p = subprocess.Popen(self.command,
|
2019-02-02 14:54:37 +01:00
|
|
|
cwd=self.restore_path,
|
|
|
|
stdin=subprocess.PIPE,
|
2019-02-02 16:37:38 +01:00
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
encoding='utf8')
|
|
|
|
|
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
class DeleteThread(BorgQtThread):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Deletes an archive from the repository.
|
2019-02-02 16:37:38 +01:00
|
|
|
|
|
|
|
Args:
|
2019-02-10 17:21:41 +01:00
|
|
|
archive_name (str) the name of the archive to delete.
|
2019-02-02 16:37:38 +01:00
|
|
|
"""
|
|
|
|
def __init__(self, archive_name):
|
|
|
|
self.archive_name = archive_name
|
2019-02-07 21:35:14 +01:00
|
|
|
super().__init__()
|
2019-02-02 16:37:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
def create_command(self):
|
|
|
|
self.command = ['borg', 'delete', '--log-json',
|
|
|
|
('::' + self.archive_name)]
|
2019-02-02 16:37:38 +01:00
|
|
|
|
2019-02-07 21:35:14 +01:00
|
|
|
|
|
|
|
class MountThread(BorgQtThread):
|
2019-02-10 17:21:41 +01:00
|
|
|
"""Mounts an archive at the given path.
|
2019-02-07 21:35:14 +01:00
|
|
|
|
|
|
|
Args:
|
|
|
|
archive_name (str) the name of the archive to restore.
|
2019-02-10 17:21:41 +01:00
|
|
|
mount_path (str) the target path to mount the archive at.
|
2019-02-07 21:35:14 +01:00
|
|
|
"""
|
|
|
|
def __init__(self, archive_name, mount_path):
|
|
|
|
self.archive_name = archive_name
|
|
|
|
self.mount_path = mount_path
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
def create_command(self):
|
|
|
|
self.command = ['borg', 'mount', '--log-json',
|
|
|
|
('::' + self.archive_name), self.mount_path]
|
2019-04-28 20:01:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
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
|