From 4cc19180a559aa2b2645586d015b3e9e892b93af Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Thu, 3 Jan 2019 11:48:16 +0100 Subject: Initial commit. Support for a local target only. --- __init__.py | 0 example.py | 54 +++++++++++++++++++++++++++++++++++++++ repository.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ targets.py | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 __init__.py create mode 100755 example.py create mode 100644 repository.py create mode 100644 targets.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 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 -- cgit v1.2.3