From 00610c729e3887f9d19e883c721400b3ce3e385c Mon Sep 17 00:00:00 2001 From: Stefan Huber Date: Mon, 21 May 2012 12:43:18 +0200 Subject: [PATCH] renaming shbackup to sitarba --- Makefile | 12 +- shbackup | 708 ---------------------------- shbackup.1 => sitarba.1 | 12 +- shbackup.conf => sitarba.conf | 2 +- shbackup.docbook => sitarba.docbook | 12 +- 5 files changed, 19 insertions(+), 727 deletions(-) delete mode 100755 shbackup rename shbackup.1 => sitarba.1 (89%) rename shbackup.conf => sitarba.conf (97%) rename shbackup.docbook => sitarba.docbook (92%) diff --git a/Makefile b/Makefile index 408a6ad..d565b15 100644 --- a/Makefile +++ b/Makefile @@ -2,20 +2,20 @@ PREFIX=/usr # Where the script binary should go BINPATH = $(PREFIX)/bin MANPATH = $(PREFIX)/share/man -CONFFILE = $(PREFIX)/share/shbackup.conf.sample +CONFFILE = $(PREFIX)/share/sitarba.conf.sample ###################################################################### all: install: all - install -m 0755 shbackup $(BINPATH)/shbackup - install -m 0644 shbackup.conf $(CONFFILE) - install -m 0644 shbackup.1 $(MANPATH)/man1/shbackup.1 + install -m 0755 sitarba $(BINPATH)/sitarba + install -m 0644 sitarba.conf $(CONFFILE) + install -m 0644 sitarba.1 $(MANPATH)/man1/sitarba.1 -manpage: shbackup.1 +manpage: sitarba.1 -shbackup.1: shbackup.docbook +sitarba.1: sitarba.docbook docbook2man.pl $< diff --git a/shbackup b/shbackup deleted file mode 100755 index e57e250..0000000 --- a/shbackup +++ /dev/null @@ -1,708 +0,0 @@ -#!/usr/bin/python3 -"""A simple backup solution.""" - -__version__ = "2.0" -__author__ = "Stefan Huber" - -import datetime -import os, shutil, sys -import configparser -import hashlib -import subprocess, fcntl, select -import random, re -import logging - - -Modes = ["full", "incr", "diff"] - -class Epoch: - - units = { - "hour" : datetime.timedelta(0, 3600), - "day" : datetime.timedelta(1), - "week" : datetime.timedelta(7), - "month" : datetime.timedelta(31), - "year" : datetime.timedelta(365) } - - def __init__(self, unit=None, mult=1, mode="full", numkeeps=None): - self.unit = unit - self.mult = mult - self.mode = mode - self.numkeeps = numkeeps - self.excludes = [] - - def __repr__(self): - return "[unit: " + repr(self.unit) + \ - ", mult:" + repr(self.mult) + \ - ", mode: " + repr(self.mode) + \ - ", numkeeps: " + repr(self.numkeeps) + \ - ", excludes: " + repr(self.excludes) + "]" - - def getTimeDelta(self): - if self.unit == None: - return None - return self.mult*Epoch.units[self.unit] - - def isRipe(self, oldest, now): - - if self.unit==None: - return True - - delta = now-oldest - mult = self.mult - - if delta >= self.getTimeDelta(): - return True - - if self.unit == "hour": - return abs(now.hour - oldest.hour) >= mult - elif self.unit == "day": - return abs(now.day - oldest.day) >= mult - elif self.unit == "week": - return abs(now.isocalendar()[1] - oldest.isocalendar()[1]) >= mult - elif self.unit == "month": - return abs(now.month - oldest.month) >= mult - elif self.unit == "year": - return abs(now.year - oldest.year) >= mult - - return None - - - @staticmethod - def parseTimedelta( deltastr ): - tokens = [ s.strip() for s in deltastr.split("*") ] - unit = None - mult = 1 - if len(tokens) == 1: - unit = tokens[0] - elif len(tokens) == 2: - mult = int(tokens[0]) - unit = tokens[1] - else: - raise ValueError("Invalid format: '{0}'".format(deltastr)) - - if not unit in Epoch.units: - raise ValueError("Unknown unit '{0}'".format(unit)) - - if mult <= 0: - raise ValueError("Non-positive factor '{0}' given.".format(mult)) - - return mult, unit - - - -class FileSet: - """A fileset has a name and a list of directories.""" - def __init__(self, name, dirs, excludes): - self.name = name - self.dirs = dirs - self.excludes = excludes - - def __repr__(self): - return "[name: " + self.name + \ - ", dirs: " + str(self.dirs) + \ - ", excludes: " + str(self.excludes) + "]" - - -class Backup: - """A single backup has a date, an epoch and a mode.""" - - def __init__(self, date, epoch, mode): - self.date = date - self.epoch = epoch - self.mode = mode - self.excludes = [] - - @staticmethod - def fromDirName(dirname): - [strdate, strtime, epoch, mode] = dirname.split("-") - - if not mode in Modes: - raise ValueError("Invalid mode: " + mode) - - date = datetime.datetime(int(strdate[0:4]), - int(strdate[4:6]), int(strdate[6:8]),\ - int(strtime[0:2]), int(strtime[2:4])) - - return Backup(date, epoch, mode) - - def __repr__(self): - return "[date: " + self.date.ctime() + \ - ", epoch: " + self.epoch + \ - ", mode: " + self.mode + "]" - - def colAlignedString(self): - age = datetime.datetime.now() - self.date - total_hours = age.total_seconds()/3600 - if total_hours <= 48: - agestr = "(%s h)" % int(total_hours) - else: - agestr = "(%s d)" % age.days - return "%16s %7s %10s %4s" % ( - self.date.strftime("%Y-%m-%d %H:%M"), agestr, - self.epoch, self.mode) - - @staticmethod - def getDirName(date, epoch, mode): - """Get directory name of backup by given properties.""" - return date.strftime("%Y%m%d-%H%M") + "-" + epoch + "-" + mode - - @staticmethod - def isBackupDir(dirname): - """Is directory a backup directory?""" - p = re.compile(r'^\d\d\d\d\d\d\d\d-\d\d\d\d-\w+-\w+$') - return p.match(dirname) - - - -class Config: - """Encapsules the configuration for the backup program.""" - - class ReadError(RuntimeError): - """An exception raised when reading configurations.""" - def __init__(self, value): - self.value = value - self.message = value - - - formats = ["tar", "tar.gz", "tar.bz2", "tar.xz" ] - - # Filename where checksum of config is saved - checksumfn = "checksum" - - def __init__(self): - self.backupdir = None - self.format = self.formats[1] - self.tarbin = "/bin/tar" - self.excludes = [] - self.sets = [] - self.checksum = None - self.lastchecksum = None - self.epochs = Epochs = { "sporadic" : Epoch() } - - - def __repr__(self): - return "[backupdir: " + self.backupdir + \ - ", format: " + self.format + \ - ", tarbin: " + self.tarbin + \ - ", excludes: " + repr(self.excludes) + \ - ", epochs: " + repr(self.epochs) + \ - ", sets: " + repr(self.sets) + "]" - - def getRealEpochsSorted(self): - """Return all epochs with have a non-None unit, sorted by - Epoch.getTimeDelta(), starting with the longest dela.""" - epochs = self.epochs - realepochs = [ e for e in epochs.keys() if epochs[e].unit != None ] - deltakey = lambda e: epochs[e].getTimeDelta() - realepochs.sort(key=deltakey, reverse=True) - return realepochs - - - def _read_global(self, config, sec): - for opt in config.options(sec): - if opt=="backupdir": - self.backupdir = config.get(sec, opt) - if not os.path.isdir(self.backupdir): - raise Config.ReadError("Backupdir '{0}' does not exist.".format(self.backupdir)) - elif opt=="format": - self.format = config.get(sec, opt) - if not self.format in Config.formats: - raise Config.ReadError("Invalid 'format' given.") - elif opt=="tarbin": - self.tarbin = config.get(sec, opt) - if not os.path.isfile(self.tarbin): - raise Config.ReadError("Tar binary '{0}' does not exist.".format(self.tarbin)) - elif opt.startswith("exclude"): - self.excludes += [ config.get(sec, opt) ] - else: - raise Config.ReadError("Unknown option '{0}'.".format(opt)) - - - def _read_epoch(self, config, sec): - name = sec[6:].strip() - e = Epoch() - if name in self.epochs: - raise Config.ReadError("Epoch '{0}' already defined.".format(name)) - if name in Epoch.units: - e.unit = name - - for opt in config.options(sec): - if opt=="numkeeps": - try: - e.numkeeps = int(config.getint(sec, opt)) - except ValueError: - raise Config.ReadError("Invalid integer given for '{0}'.".format(opt)) - if e.numkeeps <= 0: - raise Config.ReadError("Non-positive numkeeps '{0}' given.".format(e.numkeeps)) - - elif opt=="mode": - e.mode = config.get(sec, opt) - if not e.mode in Modes: - raise Config.ReadError("Invalid mode '{0}'.".format(e.mode)) - - elif opt=="timespan": - if name in Epoch.units: - raise Config.ReadError("The time delta of a standard epoch " + \ - "is not supposed to be redefined. ") - td = config.get(sec,opt) - try: - mult, unit = Epoch.parseTimedelta(td) - e.unit = unit - e.mult = mult - except ValueError as e: - raise Config.ReadError("Invalid timespan '{0}': {1}".format(td, str(e))) - - elif opt.startswith("exclude"): - e.excludes += [config.get(sec, opt)] - - else: - raise Config.ReadError("Unknown option '" + opt + "'.") - - if e.numkeeps == None: - raise Config.ReadError("No numkeeps set for epoch '{0}'.".format(name)) - - self.epochs[name] = e - - - def _read_set(self, config, sec): - name = sec[4:].strip() - dirs = [] - excludes = [] - - for opt in config.options(sec): - if opt.startswith("dir"): - dirs += [config.get(sec, opt)] - elif opt.startswith("exclude"): - excludes += [config.get(sec,opt)] - else: - raise Config.ReadError("Unknown option '" + opt + "'.") - - self.sets += [FileSet(name, dirs, excludes)] - - - def read(self, filename): - """Read configuration from file""" - - if not os.path.isfile(filename): - raise Config.ReadError("Cannot read config file '" + filename + "'.") - - config = configparser.RawConfigParser() - config.read(filename) - - for reqsec in ["global"]: - if not config.has_section(reqsec): - raise Config.ReadError("Mandatory section '" + reqsec + "' is missing.") - - for sec in config.sections(): - - if sec=="global": - self._read_global(config, sec) - - elif sec.startswith("epoch "): - self._read_epoch(config, sec) - - elif sec.startswith("set "): - self._read_set(config, sec) - - else: - raise Config.ReadError("Unknown section '" + sec + "'.") - - if self.backupdir == None: - raise Config.ReadError("No backup directory set.") - - - # Compute checksum of config file - m = hashlib.sha1() - f = open(filename, 'rb') - try: - m.update(f.read()) - self.checksum = m.hexdigest() - finally: - f.close() - - try: - f = open(os.path.join(self.backupdir, self.checksumfn), 'r') - self.lastchecksum = f.read().strip() - f.close() - except IOError: - self.lastchecksum = None - - -class BackupManager: - """List and create backups""" - - def __init__(self, conffn): - self.conf = Config() - self.conf.read(conffn) - - - def listAllDirs(self): - """List all dirs in backupdir""" - - # Get all entries - basedir = self.conf.backupdir - dirs = os.listdir(basedir) - # Filter directories - return [ d for d in dirs if os.path.isdir(os.path.join(basedir, d)) ] - - - def listOldBackups(self): - """Returns a list of old backups.""" - - backups = [] - - for entry in [ b for b in self.listAllDirs() if Backup.isBackupDir(b) ]: - backups += [ Backup.fromDirName(entry) ] - - return backups - - - def getDesiredEpochs(self, backups, now): - """Get desired epoch based on self.configuration and list of old backups""" - - # Find the longest epoch for which we would like the make a backup - latest = datetime.datetime(1900, 1, 1) - for e in self.conf.getRealEpochsSorted(): - epoch = self.conf.epochs[e] - if epoch.numkeeps <= 0: - continue - - # Get backups of that epoch - byepoch = list(sorted( [ b for b in backups if b.epoch==e], \ - key=lambda b: b.date)) - - # If there are any, determine the latest - if len(byepoch) > 0: - latest = max(latest, byepoch[-1].date ) - - if epoch.isRipe(latest, now): - return e - - # No backup is to be made - return None - - - - def backupFileSet(self, fileset, targetdir, excludes, since=None): - """Create an archive for given fileset at given target directory.""" - - logfile = logging.getLogger('backuplog') - logfile.info("Running file set: " + fileset.name) - - fsfn = os.path.join(targetdir, fileset.name) + "." + self.conf.format - taropts = [] - - # Add the since date, if given - if since != None: - taropts += ["-N", since.strftime("%Y-%m-%d %H:%M:%S")] - - # Add the exclude patterns - for pat in excludes: - taropts += ["--exclude", pat] - - #Add exclude patterns from fileset - for pat in fileset.excludes: - taropts += ["--exclude", pat] - - # Adding directories to backup - taropts += ["-C", "/"] + [ "./" + d.lstrip("/") for d in fileset.dirs] - - # Launch the tar process - tarargs = [self.conf.tarbin] + ["-cpvaf", fsfn] + taropts - logfile.debug("tar call: " + " ".join(tarargs)) - tarp = subprocess.Popen( tarargs, bufsize=-1, \ - stdout=subprocess.PIPE, stderr=subprocess.PIPE ) - - # Change tarp's stdout and stderr to non-blocking - for s in [tarp.stdout, tarp.stderr]: - fd = s.fileno() - fl = fcntl.fcntl(fd, fcntl.F_GETFL) - fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) - - # Read stdout and stderr of tarp - errmsg = b"" - while tarp.poll() == None: - rd,wr,ex = select.select([tarp.stdout, tarp.stderr], [], [], 0.05) - if tarp.stdout in rd: - logging.debug( tarp.stdout.readline()[:-1].decode() ) - if tarp.stderr in rd: - errmsg += tarp.stderr.read() - - # Get the remainging output of tarp - for l in tarp.stdout.readlines(): - logging.debug(l.decode().rstrip()) - errmsg += tarp.stderr.read() - - # Get return code of tarp - rett = tarp.wait() - if rett != 0: - for l in errmsg.decode().split("\n"): - logfile.error(l) - logfile.error(self.conf.tarbin + " returned with exit status " + \ - str(rett) + ".") - - - def backup(self, epoch=None, mode=None): - """Make a new backup, if necessary. If epoch is None then determine - desired epoch automatically. Use given epoch otherwise. If mode is None - then use mode for given epoch. Use given mode otherwise.""" - - now = datetime.datetime.now() - oldbackups = self.listOldBackups() - - # Get epoch of backup - if epoch == None: - epoch = self.getDesiredEpochs(oldbackups, now) - if epoch == None: - logging.info("No backup planned.") - return - - # Get mode of backup - if mode == None: - mode = self.conf.epochs[epoch].mode - logging.info("Making a backup. Epochs: " + epoch + ", mode: " + mode) - - oldfullbackups = [ b for b in oldbackups if b.mode == "full" ] - - # No old full backups existing - if mode != "full" and len(oldfullbackups)==0: - logging.info("No full backups existing. Making a full backup.") - - # Checksum changed -> self.config file changed - if self.conf.checksum != self.conf.lastchecksum and mode != "full": - logging.warning("Full backup recommended as config file has changed.") - - - # If we have a full backup, we backup everything - since = None - if mode == "diff": - since = sorted(oldfullbackups, key=lambda b: b.date)[-1].date - elif mode == "incr": - since = sorted(oldbackups, key=lambda b: b.date)[-1].date - - if since != None: - logging.debug("Making backup relative to " + since.ctime()) - - yesno = self.ask_user_yesno("Proceed? [Y, n] ") - if yesno == "n": - return - - # Create new backup directory - basedir = self.conf.backupdir - dirname = Backup.getDirName(now, epoch, mode) - tmpdirname = dirname + ("-%x" % (random.random()*2e16) ) - targetdir = os.path.join(basedir, tmpdirname) - os.mkdir( targetdir ) - - - # Add file logger - logfile = logging.getLogger("backuplog") - fil = logging.FileHandler( os.path.join(targetdir, "log") ) - fil.setLevel(logging.DEBUG) - logfile.addHandler(fil) - - logfile.info("Started: " + now.ctime()) - - # Backup all file sets - for s in self.conf.sets: - excludes = self.conf.excludes + self.conf.epochs[epoch].excludes - self.backupFileSet(s, targetdir, excludes, since) - - logfile.info("Stopped: " + datetime.datetime.now().ctime()) - - # Rename backup directory to final name - os.rename( targetdir, os.path.join(basedir, dirname) ) - - # We made a full backup -- recall checksum of config - if mode == "full": - f = open( os.path.join(basedir, self.conf.checksumfn), "w") - f.write( self.conf.checksum ) - f.close() - - - - def prune(self): - """Prune old backup files""" - - allDirs = sorted(self.listAllDirs()) - # Collect all directories not matching backup name - removeDirs = [ d for d in allDirs if not Backup.isBackupDir(d) ] - - # Get all directories which are kept - backups = self.listOldBackups() - keepdirs = [] - byepoch = { e : list(sorted( [ b for b in backups if b.epoch == e ], \ - key=lambda b : b.date, reverse=True)) for e in self.conf.getRealEpochsSorted() } - for e in byepoch: - epoch = self.conf.epochs[e] - old = byepoch[e][epoch.numkeeps:] - removeDirs += [ Backup.getDirName(b.date, b.epoch, b.mode) for b in old] - - - logging.info("List of stale/outdated entries:") - for d in allDirs: - msg = "" - if d in removeDirs: - msg = "[*] " - else: - msg = "[ ] " - - if Backup.isBackupDir(d): - msg += Backup.fromDirName(d).colAlignedString() - else: - msg += d - - logging.info(msg) - - # Check that dirs to be removed is in list of all dirs - for d in removeDirs: - assert( d in allDirs ) - - if len(removeDirs) == 0: - logging.info("No stale/outdated entries to remove.") - return - - basedir = self.conf.backupdir - yesno = self.ask_user_yesno("Remove entries marked by '*'? [y, N] ") - if yesno == "y": - for d in removeDirs: - try: - shutil.rmtree(os.path.join(basedir, d)) - except OSError as e: - logging.error("Error when removing '%s': %s" % (d,e.strerror) ) - - - def ask_user_yesno(self, question): - if LogConf.con.level <= logging.INFO: - return input(question) - else: - return "y" - - -def printUsage(): - """Print --help text""" - - print("shbackup - a simple backup solution.") - print("") - print("Usage:") - print(" " + sys.argv[0] + " {options} [cmd]") - print(" " + sys.argv[0] + " --help") - print("") - print("Commands:") - print(" backup make a new backup, if necessary") - print(" list list all backups (default)") - print(" prune prune outdated/old backups") - print("") - print("Options:") - print(" -h, --help print this usage text") - print(" -c, --conf FILE use given configuration file") - print(" default: /etc/shbackup.conf") - print(" -e, --epoch EPOCH force to create backup for given epoch, which") - print(" can be 'sporadic' or one of the configured epochs") - print(" -m, --mode MODE override mode: full, diff, or incr") - print(" -v, --verbose be more verbose and interact with user") - print(" --verbosity LEVEL set verbosity to LEVEL, which can be") - print(" error, warning, info, debug") - print(" -V, --version print version info") - - - -class LogConf: - """Encapsulates logging configuration""" - - con = logging.StreamHandler(sys.stderr) - - @classmethod - def setup(cls): - """Setup logging system""" - conlog = logging.getLogger() - conlog.setLevel(logging.DEBUG) - - cls.con.setLevel(logging.WARNING) - conlog.addHandler(cls.con) - - fillog = logging.getLogger("backuplog") - fillog.setLevel(logging.DEBUG) - - -if __name__ == "__main__": - - LogConf.setup() - - conffn = "/etc/shbackup.conf" - cmd = "list" - mode = None - epoch = None - - i = 0 - while i < len(sys.argv)-1: - i += 1 - opt = sys.argv[i] - - if opt in ["-h", "--help"]: - printUsage() - exit(0) - - elif opt in ["-c", "--conf"]: - i += 1 - conffn = sys.argv[i] - - elif opt in ["-V", "--version"]: - print("shbackup " + __version__) - exit(0) - - elif opt in ["-v", "--verbose"]: - LogConf.con.setLevel(logging.INFO) - - elif opt in ["--verbosity"]: - i += 1 - level = sys.argv[i] - numlevel = getattr(logging, level.upper(), None) - if not isinstance(numlevel, int): - raise ValueError('Invalid verbosity level: %s' % level) - LogConf.con.setLevel(numlevel) - - elif opt in ["-m", "--mode"]: - i += 1 - mode = sys.argv[i] - if not mode in Modes: - logging.error("Unknown mode '" + mode + "'.") - exit(1) - - elif opt in ["-e", "--epoch"]: - i += 1 - epoch = sys.argv[i] - - elif opt in ["backup", "list", "prune"]: - cmd = opt - - else: - logging.error("Unknown option: " + opt) - exit(1) - - try: - man = BackupManager(conffn) - - logging.debug("Config: " + str(man.conf)) - - if epoch!=None and not epoch in man.conf.epochs.keys(): - logging.error("Unknown epoch '" + epoch + "'.") - exit(1) - - if cmd == "backup": - man.backup(epoch, mode) - - if cmd == "list": - for b in sorted(man.listOldBackups(), key=lambda b: b.date): - print(b.colAlignedString()) - - if cmd == "prune": - man.prune() - - except (Config.ReadError, configparser.Error) as e: - logging.error("Error: " + e.message) - - - - diff --git a/shbackup.1 b/sitarba.1 similarity index 89% rename from shbackup.1 rename to sitarba.1 index e2b5fc6..07139cb 100644 --- a/shbackup.1 +++ b/sitarba.1 @@ -5,14 +5,14 @@ \\$2 \(la\\$1\(ra\\$3 .. .if \n(.g .mso www.tmac -.TH shbackup 1 2012-05-13 "" "" +.TH sitarba 1 2012-05-13 "" "" .SH NAME -shbackup \- a simple backup solution +sitarba \- a simple backup solution .SH SYNOPSIS 'nh .fi .ad l -\fBshbackup\fR \kx +\fBsitarba\fR \kx .if (\nx>(\n(.l/2)) .nr x (\n(.l/5) 'in \n(.iu+\nxu [options] {\fIcommand\fR} @@ -20,13 +20,13 @@ shbackup \- a simple backup solution .ad b 'hy .SH DESCRIPTION -\fBshbackup\fR is a simple backup solution +\fBsitarba\fR is a simple backup solution which packs user-defined file sets with tar. Each backup belongs to an epoch, which is 'sporadic' or one of the user-defined epochs. Standard epochs are year, month, week, day, or hour. The backup can be made either full, differential or incremental. -\fBshbackup\fR takes care for pruning old +\fBsitarba\fR takes care for pruning old backups. The user defines for every epoch the number of backups to keep, except for 'sporadic'. Sporadic backups need to be removed manually. @@ -47,7 +47,7 @@ Print out a help summary. .TP \*(T<\fB\-c, \-\-conf FILE\fR\*(T> Use given configuration file instead of -/etc/shbackup.conf. +/etc/sitarba.conf. .TP \*(T<\fB\-e, \-\-epoch EPOCH\fR\*(T> Do not determine epoch automatically, but diff --git a/shbackup.conf b/sitarba.conf similarity index 97% rename from shbackup.conf rename to sitarba.conf index 11bfa92..bd316a8 100644 --- a/shbackup.conf +++ b/sitarba.conf @@ -1,5 +1,5 @@ [global] -# Where shbackup stores the backups and other stuff +# Where sitarba stores the backups and other stuff backupdir = /media/backup # Which format should be used by tar? diff --git a/shbackup.docbook b/sitarba.docbook similarity index 92% rename from shbackup.docbook rename to sitarba.docbook index 36741d9..1eacde1 100644 --- a/shbackup.docbook +++ b/sitarba.docbook @@ -15,18 +15,18 @@ - shbackup + sitarba 1 - shbackup + sitarba a simple backup solution - shbackup + sitarba options command @@ -36,14 +36,14 @@ DESCRIPTION - shbackup is a simple backup solution + sitarba is a simple backup solution which packs user-defined file sets with tar. Each backup belongs to an epoch, which is 'sporadic' or one of the user-defined epochs. Standard epochs are year, month, week, day, or hour. The backup can be made either full, differential or incremental. - shbackup takes care for pruning old + sitarba takes care for pruning old backups. The user defines for every epoch the number of backups to keep, except for 'sporadic'. Sporadic backups need to be removed manually. @@ -98,7 +98,7 @@ Use given configuration file instead of - /etc/shbackup.conf. + /etc/sitarba.conf. -- 2.30.2