diff --git a/.gitignore b/.gitignore index f17514b350743284e10a589bd1cf776827dd431d..6dbaf651ca64862752e3852b82841391a76d88c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *~ __pycache__ -md5backup \ No newline at end of file +md5backup +sha512backup diff --git a/Makefile b/Makefile index 65b0ae4368e5357a22c32a31c8f04871dda7badf..d2416a7e150613805d9fe0e640361834f19ce15d 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,16 @@ -md5backup: md5backup.py $(sort $(wildcard *.py)) Makefile +SOURCE_md5backup = config.py loghandler.py md5toc.py primary.py secondary.py +SOURCE_hashbackup = command.py config.py loghandler.py hashtoc.py \ + primary.py secondary.py + + +all: md5backup sha512backup + +md5backup: hashbackup.py $(SOURCE_hashbackup) Makefile + apa -o $@ $(filter %.py, $^) + +sha512backup: hashbackup.py $(SOURCE_hashbackup) Makefile apa -o $@ $(filter %.py, $^) +.PHONY: test +test: all + $(MAKE) -C test test diff --git a/command.py b/command.py new file mode 100644 index 0000000000000000000000000000000000000000..bbd077ed68e298aa8cb87e1f10922b71c0cde798 --- /dev/null +++ b/command.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 + +class Command: + + def __init__(self, command): + self.result = [ command ] + pass + + def __iter__(self): + return iter(self.result) + + def arg(self, arg): + self.result.append(arg) + return self + + def flag(self, flag, condition=True): + condition and self.result.append(flag) + return self + + def option(self, name, value=None, kind=str): + isinstance(value, kind) and self.result.extend([name, str(value)]) + return self diff --git a/config.py b/config.py index 08f06dca7f1a90f4c6293151e4c49de0873287ce..54aba87dcca3f88ca8b28abca044626f978e3dd4 100755 --- a/config.py +++ b/config.py @@ -13,12 +13,12 @@ def checkDirectory(s, l, t): (lineno(l, s), t[0])) _subdirectory_ = ( - Regex('[a-zA-Z0-9/]+') | + Regex('[._a-zA-Z0-9/]+') | QuotedString('"') | QuotedString("'") ).setParseAction(checkSubdirectory) _directory_ = ( - Regex('[a-zA-Z0-9/]+') | + Regex('[._a-zA-Z0-9/]+') | QuotedString('"') | QuotedString("'") ).setParseAction(checkDirectory) @@ -82,7 +82,17 @@ def parse(s): if __name__ == '__main__': import sys - config1 = parse(open(sys.argv[1]).read()) - config2 = parse(open(sys.argv[1]).read()) + from pyparsing import ParseException + + config = open(sys.argv[1]).read() + try: + config1 = parse(config) + except ParseException as e: + lines = config.split('\n') + print('\n'.join(lines[0:e.lineno])) + print('%s^ %s' % (' ' * e.col, e)) + raise + pass + config2 = parse(config) print(config1.dump()) print(config1.asList() == config2.asList()) diff --git a/hashbackup.py b/hashbackup.py new file mode 100644 index 0000000000000000000000000000000000000000..f24b28ac5ca0c6204c8338b6843dec6ecdae252e --- /dev/null +++ b/hashbackup.py @@ -0,0 +1,76 @@ +#!/usr/bin/python3 + +import argparse +import sys +import config +import primary +import secondary +import re +import os + +def main(): + parser = argparse.ArgumentParser()#usage="%(prog)s [options]") +# parser = argparse.ArgumentParser(prog='PROG', add_help=False) + parser.add_argument('--debug', action='store_true', + help='debug actions') + parser.add_argument('--xattr', action='store_true', + help='let hashtoc store HASH in extended attribute') + parser.add_argument('--max-age', + metavar='SECONDS', + help='maximum SECONDS since last HASH calculation') + parser.add_argument('--jobs', + metavar='CPU', + help='number of CPUs for HASH calculation') + parser.add_argument('--lookahead', + metavar='SIZE', + help='lookahead buffer size for HASH calculation') + group_primary = parser.add_argument_group('primary') + group_primary.add_argument('--primary', nargs='+', + metavar='CONFIG', + help='backup as specified in CONFIG') + group_primary.add_argument('--identity', + metavar='IDENTITY', + help='ssh IDENTITY for connecting (-i)') + group_primary.add_argument('--user', + metavar='USER', + help='ssh USER for connecting (-l)') + group_secondary = parser.add_argument_group('secondary') + group_secondary.add_argument('--secondary', nargs=3, + metavar=('SOCKET', 'MOUNT', 'PATH'), + help='backup from SOCKET to MOUNT/PATH2') + + options = parser.parse_args(sys.argv[1:]) + if not (options.primary == None) ^ (options.secondary == None): + parser.print_help() + print('Either --primary or --secondary should be specified') + exit(1) + pass + + m = re.match('^(.*)backup', os.path.basename(sys.argv[0])) + if m: + hash_name = m.group(1) + pass + + if options.primary: + done = set() + for path in options.primary: + if path in done: + continue + done.add(path) + primary.do_backup(hash_name=hash_name, + options=options, + config=config.parse(open(path).read())) + pass + pass + + elif options.secondary: + secondary.do_backup(hash_name=hash_name, + options=options, + socket_path=options.secondary[0], + mount=options.secondary[1], + path=options.secondary[2]) + pass + pass + +if __name__ == '__main__': + main() diff --git a/hashtoc.py b/hashtoc.py new file mode 100644 index 0000000000000000000000000000000000000000..25d3522d5484d4562173b3f95d69a985b7af0b58 --- /dev/null +++ b/hashtoc.py @@ -0,0 +1,65 @@ +#!/usr/bin/python3 + +import subprocess +import os +import collections +import re +import sys + +def readline_toc(f): + buf = f.read(4096) + m = re.match(b'^#fields:[ ]*[^\0x00\n\r]+([\x00\r\n]+)', buf) + if not m: + raise Exception("Not a valid TOC file '%s'" % f) + terminator = m.group(1) + while True: + pos = buf.find(terminator) + if pos == -1: + tmp = f.read(4096) + if len(tmp) == 0: + raise EOFError() + buf += tmp + continue + yield buf[0:pos] + buf = buf[pos+len(terminator):] + +class HashTOC: + + def __init__(self, stream, rename={}): + def readtoc(f): + for l in readline_toc(f): + if not l.startswith(b'#'): + data = l.split(b':', N-1) + yield tocEntry(*data) + continue + tmp = [ s.strip().decode('utf8') for s in l.split(b':') ] + if tmp[0].startswith('#fields'): + fields = [ rename.get(f) or f for f in tmp[1:] ] + print(fields, file=sys.stderr) + tocEntry = collections.namedtuple('TOCEntry', fields) + N = len(tmp) - 1 + pass + elif tmp[0].startswith('#endTOC'): + while True: + yield tocEntry(*[ None for e in tocEntry._fields ]) + + else: + #print('X',tmp, file=sys.stderr) + pass + pass + raise Exception("Incorrect termination of TOC file '%s'" % f) + + self.reader = readtoc(stream) + self.next() + + def next(self): + data = self.reader.__next__() + self.fields = data._fields + for k,v in zip(data._fields, data): + setattr(self, k, v) + pass + pass + + + + diff --git a/md5backup.py b/md5backup.py deleted file mode 100755 index 79e373236868e6cab50dcdcd9b87a4c4c2717431..0000000000000000000000000000000000000000 --- a/md5backup.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/python3 - -import argparse -import sys -import config -import primary -import secondary - -if __name__ == '__main__': - argParser = argparse.ArgumentParser(usage="%(prog)s [options]") - group = argParser.add_mutually_exclusive_group(required=True) - group.add_argument('--primary', nargs='*', - metavar='CONFIG', - help='backup as specified in CONFIG') - group.add_argument('--secondary', nargs=3, - metavar=('SOCKET', 'MOUNT', 'PATH'), - help='backup from SOCKET to MOUNT/PATH2') - argParser.add_argument('--xattr', action='store_true', - help='let md5toc store MD5 in extended attribute') - argParser.add_argument('--max-age', - metavar='SECONDS', - help='maximum SECONDS since last MD5 calculation') - argParser.add_argument('--user', - metavar='USER', - help='ssh USER for secondary (-l)') - argParser.add_argument('--identity', - metavar='IDENTITY', - help='ssh IDENTITY for secondary (-i)') - argParser.add_argument('--debug', action='store_true', - help='debug actions') - - options = argParser.parse_args(sys.argv[1:]) - - if options.primary: - done = set() - for path in options.primary: - if path in done: - continue - done.add(path) - primary.do_backup(options=options, - config=config.parse(open(path).read())) - - if options.secondary: - secondary.do_backup(options=options, - socket_path=options.secondary[0], - mount=options.secondary[1], - path=options.secondary[2]) - diff --git a/md5toc.py b/md5toc.py deleted file mode 100644 index 64cc6c045864c70f7633534a97ef464cd6e40863..0000000000000000000000000000000000000000 --- a/md5toc.py +++ /dev/null @@ -1,50 +0,0 @@ -import os -import subprocess -import atexit - -class MD5TOC: - def __init__(self, fd): - self.more = True - def read_entry(): - data = b'' - while True: - if not b'\n' in data: - tmp = fd.read(4096) - if len(tmp) == 0: - raise Exception('Premature end of file') - data += tmp - continue - l,data = data.split(b'\n', 1) - if l.startswith(b'#fields:'): - self.labels = list(map(lambda s: s.strip().decode(), - l.split(b':')[1:])) - elif l.startswith(b'#endTOC'): - for k in self.labels: - setattr(self, k, None) - break - elif l.startswith(b'#'): - pass - else: - result = list(zip(self.labels, - l.split(b':', len(self.labels) - 1))) - for k,v in result: - setattr(self, k, v) - yield result - while True: - self.more = False - yield None - - self.read_entry = read_entry() - self.next() - - def next(self): - try: - return self.read_entry.__next__() - except AttributeError: - return self.read_entry.next() - - - def __repr__(self): - return 'MD5TOC(%s)' % ",".join(map(lambda k: "%s=%s" % - (k, getattr(self, k)), - self.labels)) diff --git a/primary.py b/primary.py index 2be17f34e3fd980daa0e9095b9f652057f9ca8d2..bbf7314bd5e366bc97ca646d50e4c23d4ffcf6f4 100644 --- a/primary.py +++ b/primary.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import atexit +import command import config import loghandler import netifaces @@ -18,6 +19,7 @@ def cond_unlink(path, log): log.DEBUG('removed %s' % path) except FileNotFoundError: pass + pass class AddrInfo: @@ -36,7 +38,8 @@ class AddrInfo: class Server: - def __init__(self, options, config, entry, path, uuid, log): + def __init__(self, hash_name, options, config, entry, path, uuid, log): + self.hash_name = hash_name self.options = options self.config = config self.entry = entry @@ -45,7 +48,7 @@ class Server: self.uuid = uuid self.socket_path = '/tmp/%s_server' % (self.uuid) self.mutex = threading.Lock() - self.thread_md5 = None + self.thread_hash = None self.thread_star = None self.thread_server = threading.Thread(daemon=True, target=self.run) self.thread_server.start() @@ -55,59 +58,59 @@ class Server: server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) server.bind(self.socket_path) server.listen(2) - config_MD5_socket,_ = server.accept() - self.log.DEBUG('config+MD5', config_MD5_socket) - c = config_MD5_socket.makefile('r').read() + config_HASH_socket,_ = server.accept() + self.log.DEBUG('config+HASH', config_HASH_socket) + c = config_HASH_socket.makefile('r').read() if config.parse(c).asList() != self.config.asList(): raise Exception('Configuration differs') with self.mutex: - self.thread_md5 = threading.Thread(daemon=True, - target=self.send_MD5, - args=(config_MD5_socket,)) - self.thread_md5.start() + self.thread_hash = threading.Thread(daemon=True, + target=self.send_HASH, + args=(config_HASH_socket,)) + self.thread_hash.start() star_socket,_ = server.accept() self.log.DEBUG('CPIO', star_socket) with self.mutex: - self.thread_md5 = threading.Thread(daemon=True, + self.thread_hash = threading.Thread(daemon=True, target=self.run_star, args=(star_socket,)) - self.thread_md5.start() + self.thread_hash.start() cond_unlink(self.socket_path, self.log) def pending(self): with self.mutex: - return ((self.thread_md5 and self.thread_md5.thread.is_alive()) or + return ((self.thread_hash and self.thread_hash.thread.is_alive()) or (self.thread_star and self.thread_star.thread.is_alive()) or (self.thread_server.is_alive())) - def send_MD5(self, config_MD5): - self.log.DEBUG('send_MD5', + def send_HASH(self, config_HASH): + self.log.DEBUG('send_HASH', self.config.primary.mount.path, self.path) - cmd = ( - [ '/usr/bin/md5toc' ] + - ( self.options.xattr and - [ '--xattr' ] or []) + - ( self.options.xattr and self.options.max_age and - [ '--max-age', self.options.max_age ] or []) + - [ '.' ] - ) + cmd = list( command.Command('/usr/bin/hashtoc') + .flag('--%s' % self.hash_name) + .flag('--zero-terminated') + .flag('--xattr', self.options.xattr) + .option('--max-age', self.options.max_age) + .option('--jobs', self.options.jobs) + .option('--lookahead', self.options.lookahead) + .arg('.') ) cwd = os.path.join(self.config.primary.mount.path, self.path) - stdout = config_MD5.makefile('wb') + stdout = config_HASH.makefile('wb') try: subprocess.check_call(cmd, cwd=cwd, stdout=stdout) finally: - config_MD5.shutdown(socket.SHUT_RDWR) - config_MD5.close() + config_HASH.shutdown(socket.SHUT_RDWR) + config_HASH.close() def run_star(self, star_socket): self.log.DEBUG('START run_star', self.config.primary.mount.path, self.path) - cmd = [ '/bin/star', '-c', '-acl', '-Hexustar', '-dump', '-list=-', - '-no-statistics' ] + cmd = [ '/bin/star', '-c', '-acl', '-Hexustar', '-dump', + '-read0', '-list=-', '-no-statistics' ] cwd = os.path.join(self.config.primary.mount.path, self.path) stdin = star_socket.makefile('rb') stdout = star_socket.makefile('wb') @@ -123,7 +126,8 @@ class Server: class Client: - def __init__(self, options, config, entry, log): + def __init__(self, hash_name, options, config, entry, log): + self.hash_name = hash_name self.options = options self.config = config self.entry = entry @@ -149,40 +153,48 @@ class Client: self.entry.mount.path, path) self.log.MESSAGE('START %s' % (readable)) - server = Server(options=self.options, + server = Server(hash_name=self.hash_name, + options=self.options, config=self.config, entry=self.entry, path=path, uuid=self.uuid, log=self.server_log) socket_path = '/tmp/%s_client' % (self.uuid) - cmd = ( - [ 'ssh', '-n', self.entry.mount.host ] + - ( self.options.user and - [ '-l', self.options.user ] or []) + - ( self.options.identity and - [ '-i', self.options.identity ] or []) + - [ '-R', '%s:%s' % (socket_path, server.socket_path) ] + - [ os.path.realpath(sys.argv[0]), - '--secondary', socket_path, self.entry.mount.path, path ] + - ( self.options.debug and - [ '--debug' ] or []) + - ( self.options.xattr and - [ '--xattr' ] or []) + - ( self.options.xattr and self.options.max_age and - [ '--max-age', self.options.max_age ] or []) - ) + cmd = list( command.Command('/bin/ssh') + .flag('-y') + .flag('-n') + .arg(self.entry.mount.host) + .option('-l', self.options.user) + .option('-i', self.options.identity) + .option('-R', '%s:%s' % (socket_path, + server.socket_path)) + .arg(os.path.realpath(sys.argv[0])) + .flag('--secondary') + .arg(socket_path) + .arg(self.entry.mount.path) + .arg(path) + .flag('--debug', self.options.debug) + .flag('--xattr', self.options.xattr) + .option('--max-age', self. options.max_age) + .option('--jobs', self. options.jobs) + .option('--lookahead', self. options.lookahead) ) self.log.DEBUG('CMD="%s"' % (' '.join(cmd))) stdout = loghandler.LOG(parent=self.log, prefix='STDOUT ') stderr = loghandler.LOG(parent=self.log, prefix='STDERR ') - subprocess.check_call(cmd, - stdout=stdout.makefile(encoding='utf-8'), - stderr=stderr.makefile(encoding='utf-8')) + try: + subprocess.check_call(cmd, + stdout=stdout.makefile(encoding='utf-8'), + stderr=stderr.makefile(encoding='utf-8')) + except: + self.log.MESSAGE('DIED %s' % (readable)) + + exit(1) time.sleep(1) self.log.MESSAGE('DONE %s' % (readable)) time.sleep(1) -def do_backup(options, config): +def do_backup(hash_name, options, config): def is_primary(): node = set() for i in netifaces.interfaces(): @@ -202,7 +214,8 @@ def do_backup(options, config): log = loghandler.LOG(loghandler.LOG_WARNING) for b in config.secondary.backup: - client = [ Client(options=options, + client = [ Client(hash_name=hash_name, + options=options, config=config, entry=e, log=log) for e in b.entry ] diff --git a/secondary.py b/secondary.py index c308633a83b7b999243197add23d450936ace646..632be0efec9d86fd15038a1c71d02acc95d76625 100644 --- a/secondary.py +++ b/secondary.py @@ -1,11 +1,12 @@ #!/usr/bin/python3 import atexit -import md5toc +import command +import hashtoc +import loghandler import os import socket import subprocess -import loghandler import time def cond_unlink(path, log): @@ -58,7 +59,7 @@ class Backup: stdin=self.primary_out) atexit.register(cond_kill, self.extract) # Make sure that the generated star archive is not empty - self.primary_in.write(b'.\n') + self.primary_in.write(b'.\0') def close(self): self.primary_in.flush() @@ -69,9 +70,9 @@ class Backup: if src.name != dst.name: raise Exception('Names differ: %s, %s' % (src, dst)) dst_path = os.path.join(self.dst_root, dst.name) - if src.kind != dst.kind or src.md5 != dst.md5 or src.size != dst.size: + if src.kind != dst.kind or src.sum != dst.sum or src.size != dst.size: self.log.DEBUG('Replace...', src.name, dst.name, - src.md5, dst.md5, src.size, dst.size) + src.sum, dst.sum, src.size, dst.size) self.status.replaced += 1 self.delete(dst) self.add(src) @@ -125,9 +126,9 @@ class Backup: parent = os.path.dirname(src.name) while len(parent) != 0: # Make sure directories get the correct modes - self.primary_in.write(parent + b'\n') + self.primary_in.write(parent + b'\0') parent = os.path.dirname(parent) - self.primary_in.write(src.name + b'\n') + self.primary_in.write(src.name + b'\0') def delete(self, dst): self.log.DEBUG('Delete:', dst.name) @@ -140,7 +141,7 @@ class Backup: os.rename(dst_path, trash_path) -def do_backup(options, socket_path, mount, path): +def do_backup(hash_name, options, socket_path, mount, path): if options.debug: log = loghandler.LOG(loghandler.LOG_DEBUG) else: @@ -153,28 +154,28 @@ def do_backup(options, socket_path, mount, path): if not os.path.exists(config_path): raise Exception('"%s" does not exists' % (config_path)) - # Connect to server config/md5toc socket - config_md5 = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - config_md5.connect(socket_path) + # Connect to server config/hashtoc socket + config_hash = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + config_hash.connect(socket_path) # Send secondary config to primary - config_md5.makefile('w').write(open(config_path).read()) - config_md5.shutdown(socket.SHUT_WR) + config_hash.makefile('w').write(open(config_path).read()) + config_hash.shutdown(socket.SHUT_WR) # Make ready to read primary TOC (src) - src = md5toc.MD5TOC(config_md5.makefile('rb')) + src = hashtoc.HashTOC(config_hash.makefile('rb'), rename={hash_name:'sum'}) - # Create secondary md5toc (dst) - cmd = ( - [ '/usr/bin/md5toc' ] + - ( options.xattr and [ '--xattr' ] or []) + - ( options.xattr and options.max_age and [ '--max-age', options.max_age ] - or []) + - [ '.' ] - ) + # Create secondary hashtoc (dst) + cmd = ( command.Command('/usr/bin/hashtoc') + .flag('--%s' % hash_name) + .flag('--zero-terminated') + .option('--max-age', options.max_age) + .option('--jobs', options.jobs) + .option('--lookahead', options.lookahead) + .arg('.') ) p = subprocess.Popen(cmd, cwd=os.path.join(mount, path), stdout=subprocess.PIPE) atexit.register(cond_kill, p) - dst = md5toc.MD5TOC(p.stdout) + dst = hashtoc.HashTOC(p.stdout, rename={hash_name:'sum'}) # Connect to server star socket primary_star = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -212,6 +213,6 @@ def do_backup(options, socket_path, mount, path): backup.close() - log.DEBUG('md5toc result', p.wait()) - config_md5.shutdown(socket.SHUT_RD) - config_md5.close() + log.DEBUG('hashtoc result', p.wait()) + config_hash.shutdown(socket.SHUT_RD) + config_hash.close() diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..cb1c665c4127629ead01ba7556728b38b93982fe --- /dev/null +++ b/test/Makefile @@ -0,0 +1,11 @@ +PYTESTS = $(wildcard test_*.py) +SHTESTS = $(wildcard test_*.sh) + +test: $(PYTESTS:%.py=%) $(SHTESTS:%.sh=%) + +%: %.py + PYTHONPATH=.. ./$< + +%: %.sh + ./$< + diff --git a/test/test_command.py b/test/test_command.py new file mode 100755 index 0000000000000000000000000000000000000000..092439cc16d4b5b037c8b51a5e451116438ca7b1 --- /dev/null +++ b/test/test_command.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 + +import command + +if __name__ == '__main__': + a = ( command.Command('test') + .flag('--flag_uncond') + .flag('--flag_cond_set_true', True) + .flag('--flag_cond_set_one', 1) + .flag('--flag_cond_set_object', object()) + .flag('--flag_cond_unset_none', None) + .flag('--flag_cond_unset_false', False) + .flag('--flag_cond_unset_zero', 0) + .option('--opt_set', 'opt') + .option('--opt_set_empty', '') + .option('--opt_set_zero', 0, int) + .option('--opt_unset_true', True) + .option('--opt_unset_none', None) + .option('--opt_unset_false', False) + .option('--opt_unset_zero', 0) + .arg('arg')) + expected = [ 'test', + '--flag_uncond', + '--flag_cond_set_true', + '--flag_cond_set_one', + '--flag_cond_set_object', + '--opt_set', 'opt', + '--opt_set_empty', '', + '--opt_set_zero', '0', + 'arg' ] + assert list(a) == expected, 'Differs:\n %s\n %s' % (list(a), expected) diff --git a/test/test_hashtoc.py b/test/test_hashtoc.py new file mode 100755 index 0000000000000000000000000000000000000000..3590e5c4e55af7191565bc10b30179bb9b77cb3a --- /dev/null +++ b/test/test_hashtoc.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 + +import hashtoc +import subprocess + +if __name__ == '__main__': + cmd = [ '/usr/bin/hashtoc', '--xattr', '--zero-terminated', + '--sha512', '.' ] + p = subprocess.Popen(cmd, + cwd='.', + stdout=subprocess.PIPE) + toc = hashtoc.HashTOC(p.stdout, rename={'sha512':'hash'}) + print(dir(toc)) + while toc.name: + print(toc.name, toc.size, toc.mtime, toc.kind, toc.hash[0:7]) + toc.next() + pass + p.wait() diff --git a/test/test_sha512backup.sh b/test/test_sha512backup.sh new file mode 100755 index 0000000000000000000000000000000000000000..9fe15c1dfcb27fdcdeb56d51619d627344bef258 --- /dev/null +++ b/test/test_sha512backup.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +set -e + +TESTDIR=$(mktemp -d /var/tmp/$(basename $0).XXX) +cleanup() { + set -x + rm -rf ${TESTDIR} +} +trap cleanup EXIT + +config() { +cat<<EOF +primary { + attributes "rw,usrquota" + $(hostname):$(realpath ..) + + export { + test [ + "@control-client" "rw,insecure,sync" + "@control-server" "rw,no_root_squash,sync" + ] + .git [ + "@control-client" "rw,insecure,sync" + "@control-server" "rw,no_root_squash,sync" + ] + } +} + +secondary { + backup { + $(hostname):${TESTDIR}/1 [ test ] + $(hostname):${TESTDIR}/2 [ .git/refs ] + } +} +EOF +} + +mkdir -p "${TESTDIR}/1/test" +mkdir -p "${TESTDIR}/1/TRASH" +mkdir -p "${TESTDIR}/2/.git/refs" +mkdir -p "${TESTDIR}/2/TRASH" +config > "${TESTDIR}/1/TOTALBACKUP.config" +config > "${TESTDIR}/2/TOTALBACKUP.config" + +#config +#../config.py <(config) + +../sha512backup --debug --xattr --primary <(config) +../sha512backup --debug --xattr --primary <(config) +find ${TESTDIR} +../md5backup --debug --xattr --primary <(config) +find ${TESTDIR}