summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2019-01-03 11:48:16 +0100
committerAnton Khirnov <anton@khirnov.net>2019-01-03 13:23:51 +0100
commit4cc19180a559aa2b2645586d015b3e9e892b93af (patch)
tree3dcf0db0e02c610c576cace7878f91292b1ac467
Initial commit.
Support for a local target only.
-rw-r--r--__init__.py0
-rwxr-xr-xexample.py54
-rw-r--r--repository.py81
-rw-r--r--targets.py45
4 files changed, 180 insertions, 0 deletions
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/__init__.py
diff --git a/example.py b/example.py
new file mode 100755
index 0000000..6fa6c0d
--- /dev/null
+++ b/example.py
@@ -0,0 +1,54 @@
+#!/usr/bin/python3
+
+import argparse
+import sys
+
+from bupper.repository import Repo
+from bupper.targets import TargetLocal
+
+# define the backup targets
+tgts = (
+ #TargetLocal('local', dirs = ['/usr/local/share']),
+)
+
+def list_targets(tgts):
+ for tgt in tgts:
+ sys.stdout.write('%s\n' % tgt.name)
+
+# parse the commandline
+parser = argparse.ArgumentParser()
+parser.add_argument('-l', '--list-targets', action = 'store_true')
+parser.add_argument('targets', nargs = argparse.REMAINDER)
+args = parser.parse_args()
+
+if args.list_targets:
+ list_targets(tgts)
+ sys.exit(0)
+
+if len(args.targets) > 0:
+ tgts_run = []
+ for requested in args.targets:
+ try:
+ tgts_run.append(next(filter(lambda tgt: tgt.name == requested, tgts)))
+ except StopIteration:
+ sys.stderr.write('Requested target "%s" not defined\n' % requested);
+ sys.exit(1)
+else:
+ tgts_run = tgts
+
+repo = Repo()
+res = repo.backup(tgts_run)
+if not res.all_ok:
+ sys.stderr.write('Error while backing up:\n')
+ for tgt, tgt_res in zip(tgts, res.target_results):
+ if tgt_res.retcode == 0:
+ continue
+
+ sys.stderr.write('Backing up target "%s" failed with code %d, bup output:\n' %
+ (tgt.name, tgt_res.retcode))
+ sys.stderr.write(tgt_res.output.decode('utf-8', errors = 'backslashreplace') + '\n')
+
+ if res.par2_result.retcode != 0:
+ sys.stderr.write('Generating par2 recovery information failed with code %d, bup output:\n' %
+ (res.par2_result.retcode))
+ sys.stderr.write(res.par2_result.output.decode('utf-8', errors = 'backslashreplace') + '\n')
diff --git a/repository.py b/repository.py
new file mode 100644
index 0000000..98a4984
--- /dev/null
+++ b/repository.py
@@ -0,0 +1,81 @@
+import fcntl
+import os
+import os.path
+import subprocess
+
+class StepResult:
+ retcode = None
+ output = None
+ def __init__(self, retcode = 0, output = None):
+ self.retcode = retcode
+ self.output = output
+
+class BackupResult:
+ target_results = None
+ par2_result = None
+
+ def __init__(self):
+ self.target_results = []
+ self.par2_result = StepResult()
+
+ @property
+ def all_ok(self):
+ return (all(map(lambda tgtres: tgtres.retcode == 0, self.target_results)) and
+ self.par2_result.retcode == 0)
+
+
+class Repo:
+ """
+ A single Bup repository into which the data will be backed up, plus a
+ separate directory for extra runtime data.
+
+ :param str bup_dir: path to the bup repository, defaults to BUP_DIR or ~/.bup
+ :param str data_dir: path to the directory for storing the runtime data,
+ defaults to ~/.local/var/bupper
+ """
+ bup_dir = None
+ data_dir = None
+ lock_name = 'lock'
+
+ def __init__(self, bup_dir = None, data_dir = None):
+ if bup_dir is None:
+ if 'BUP_DIR' in os.environ:
+ bup_dir = os.environ['BUP_DIR']
+ else:
+ bup_dir = os.path.expanduser('~/.bup')
+
+ if data_dir is None:
+ data_dir = os.path.expanduser('~/.local/var/bupper/')
+
+ # create the data dir, if it does not already exist
+ os.makedirs(data_dir, 0o700, exist_ok = True)
+
+ self.bup_dir = bup_dir
+ self.data_dir = data_dir
+
+ def backup(self, tgts, gen_par2 = True):
+ """
+ Backup the supplied targets.
+
+ :param list of Target tgts: List of targets to back up.
+ :param bool gen_par2: Whether to generate par2 recovery information
+ after the backup concludes'
+ """
+ with open(os.path.join(self.data_dir, self.lock_name), 'w') as lockfile:
+ result = BackupResult()
+
+ fcntl.lockf(lockfile, fcntl.LOCK_EX)
+ try:
+ for tgt in tgts:
+ res = tgt.save(self.data_dir)
+ result.target_results.append(res)
+
+ if gen_par2:
+ res = subprocess.run(['bup', 'fsck', '-g'],
+ capture_output = True)
+ result.par2_result = StepResult(res.returncode,
+ res.stderr + res.stdout)
+ finally:
+ fcntl.lockf(lockfile, fcntl.LOCK_UN)
+
+ return result
diff --git a/targets.py b/targets.py
new file mode 100644
index 0000000..31964a1
--- /dev/null
+++ b/targets.py
@@ -0,0 +1,45 @@
+
+from abc import ABC, abstractmethod
+import re
+import subprocess
+
+from . import repository
+
+class Target(ABC):
+ name = None
+ dirs = None
+ excludes = None
+ def __init__(self, name, dirs, excludes = None):
+ if excludes is None:
+ excludes = []
+
+ self.name = name
+ self.dirs = dirs
+ self.excludes = excludes
+
+ @abstractmethod
+ def save(self, data_dir):
+ pass
+
+class TargetLocal(Target):
+ def save(self, data_dir):
+ cmd = ['bup', 'index', '--update', '--one-file-system']
+ cmd.extend(['--exclude=%s' % e for e in self.excludes])
+ cmd.extend(self.dirs)
+ res_idx = subprocess.run(cmd, capture_output = True)
+
+ cmd = ['bup', 'save', '-n', self.name] + self.dirs
+ res_save = subprocess.run(cmd, capture_output = True)
+
+ retcode = 0
+ output = b''
+ if res_idx.returncode != 0:
+ retcode = res_idx.returncode
+ output += res_idx.stderr + res_idx.stdout
+ if res_save.returncode != 0:
+ retcode = res_save.returncode
+ output += res_save.stderr + res_save.stdout
+
+ result = repository.StepResult(retcode, output)
+
+ return result