Mode = ["full", "incr", "diff"]
-Epoch = { \
+RealEpoch = { \
"hour" : datetime.timedelta(0, 3600), \
"day" : datetime.timedelta(1), \
"week" : datetime.timedelta(7), \
"month" : datetime.timedelta(30), \
"year" : datetime.timedelta(365) }
+Epoch = dict(RealEpoch, **{ \
+ "sporadic" : datetime.timedelta(0,0) \
+ })
+
+
class Backup:
"""A single backup has a date, an epoch and a mode."""
class Config:
"""Encapsules the configuration for the backup program."""
- class ReadException(Exception):
+ class ReadError(RuntimeError):
"""An exception raised when reading configurations."""
- pass
+ def __init__(self, value):
+ self.value = value
+ self.message = value
class FileSet:
"""A fileset has a name and a list of directories."""
def __str__(self):
return "[name: " + self.name + ", dirs: " + str(self.dirs) + "]"
- formats = ["tar.gz", "tar.bz2", "tar.xz" ]
+ formats = ["tar", "tar.gz", "tar.bz2", "tar.xz" ]
# Filename where checksum of config is saved
checksumfn = "checksum"
def __init__(self):
self.directory = "/media/backup"
self.format = self.formats[0]
- self.epochkeeps = { k : 0 for k in Epoch.keys() }
- self.epochmodes = { k : "full" for k in Epoch.keys() }
+ self.epochkeeps = { k : 0 for k in RealEpoch.keys() }
+ self.epochmodes = { k : "full" for k in RealEpoch.keys() }
self.exclpatterns = []
self.sets = []
self.checksum = None
"""Read configuration from file"""
if not os.path.isfile(filename):
- raise Config.ReadException("No file '" + filename + "'.")
+ raise Config.ReadError("Cannot read config file '" + filename + "'.")
config = configparser.RawConfigParser()
config.read(filename)
for reqsec in ["destination"]:
if not config.has_section(reqsec):
- raise Config.ReadException("Section '" + reqsec + "' is missing.")
+ raise Config.ReadError("Section '" + reqsec + "' is missing.")
self.directory = config.get("destination", "directory")
+ if not os.path.isdir(self.directory):
+ raise Config.ReadError("Directory '{0}' does not exist.".format(self.directory))
self.format = config.get("destination", "format")
if not self.format in Config.formats:
- raise Config.ReadException("Invalid 'format' given.")
+ raise Config.ReadError("Invalid 'format' given.")
if config.has_section("history"):
for opt in config.options("history"):
if opt.startswith("keep"):
epoch = opt[4:]
- if not epoch in Epoch.keys():
- raise Config.ReadException("Invalid option 'keep" + epoch + "'.")
- self.epochkeeps[epoch] = int(config.getint("history", opt))
+ if not epoch in RealEpoch.keys():
+ raise Config.ReadError("Invalid option 'keep" + epoch + "'.")
+ try:
+ self.epochkeeps[epoch] = int(config.getint("history", opt))
+ except ValueError:
+ raise Config.ReadError("Invalid integer given for '" + opt + "'.")
elif opt.startswith("mode"):
epoch = opt[4:]
- if not epoch in Epoch.keys():
- raise Config.ReadException("Invalid option 'mode" + epoch + "'.")
+ if not epoch in RealEpoch.keys():
+ raise Config.ReadError("Invalid option 'mode" + epoch + "'.")
self.epochmodes[epoch] = config.get("history", opt)
if not self.epochmodes[epoch] in Mode:
- raise Config.ReadException("Invalid mode given.")
+ raise Config.ReadError("Invalid mode given.")
else:
- raise Config.ReadException("Invalid option '" + opt + "'.")
-
+ raise Config.ReadError("Invalid option '" + opt + "'.")
+
if config.has_section("input"):
for opt in config.options("input"):
if opt.startswith("exclude"):
self.exclpatterns += [ config.get("input", opt) ]
else:
- raise Config.ReadException("Invalid option '" + opt + "'.")
+ raise Config.ReadError("Invalid option '" + opt + "'.")
for sec in config.sections():
if sec in ["destination", "history", "input"]:
for opt in config.options(sec):
if not opt.startswith("dir"):
- raise Config.ReadException("Unknown option '" + opt + "'.")
+ raise Config.ReadError("Unknown option '" + opt + "'.")
else:
dirs += [config.get(sec, opt)]
self.sets += [Config.FileSet(name, dirs)]
else:
- raise Config.ReadException("Unknown section '" + sec + "'.")
+ raise Config.ReadError("Unknown section '" + sec + "'.")
# Compute checksum of config file
m = hashlib.sha1()
def listAllDirs(self):
"""List all dirs in destination directory"""
-
+
# Get all entries
basedir = self.conf.directory
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."""
# Find the longest epoch for which we would like the make a backup
latest = datetime.datetime(1900, 1, 1)
- for timespan, e in reversed(sorted( [ (Epoch[e], e) for e in Epoch ] )):
+ for timespan, e in reversed(sorted( [ (Epoch[e], e) for e in RealEpoch ] )):
# We make backups of that epoch
if self.conf.epochkeeps[e] == 0:
continue
if since != None:
taropts += ["-N", since.strftime("%Y-%m-%d %H:%M:%S")]
-
+
for pat in self.conf.exclpatterns:
taropts += ["--exclude", pat]
tarargs = [tarpath] + taropts + ["-f", fsfn] + fileset.dirs
- print("tarargs: ", tarargs)
- tarp = subprocess.Popen( tarargs, \
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-
- while tarp.poll():
- l = tarp.stdout.readline()
- if len(l) > 0:
- print(l.decode(), end="")
- l = tarp.stderr.readline()
- if len(l) > 0:
- print(l.decode(), end="")
-
- for l in tarp.stdout.readlines():
- print(l.decode(), end="")
-
- for l in tarp.stderr.readlines():
- print(l.decode(), end="")
+ #print("tarargs: ", tarargs)
+ tarp = subprocess.Popen( tarargs )
rett = tarp.wait()
if rett != 0:
print(tarpath + " returned with exit status " + str(rett) + ":")
- print( tarp.stderr.read().decode() )
def backup(self, epoch=None, mode=None):
# No old full backups existing
if mode != "full" and len(oldfullbackups)==0:
- print("No full backups existing. Making a full backup.")
+ print("No full backups existing. Making a full backup.")
# Checksum changed -> self.config file changed
if self.conf.checksum != self.conf.lastchecksum:
f = open( os.path.join(basedir, self.conf.checksumfn), "w")
f.write( self.conf.checksum )
f.close()
-
+
def prune(self):
"""Prune old backup files"""
# Get all directories which are outdated
backups = self.listOldBackups()
byepoch = { e : list(sorted( [ b for b in backups if b.epoch == e ], \
- key=lambda b : b.date, reverse=True)) for e in Epoch }
+ key=lambda b : b.date, reverse=True)) for e in RealEpoch }
for e in byepoch:
keep = self.conf.epochkeeps[e]
old = byepoch[e][keep:]
print("shbackup - a simple backup solution.")
print("")
print("Usage:")
- print(" " + sys.argv[0] + " [-C <configfile>] [cmd]")
+ 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")
- print(" prune prune outdated/old backups")
+ print(" backup make a new backup, if necessary")
+ print(" list list all backups (default)")
+ print(" prune prune outdated/old backups")
print("")
print("Options:")
- print(" -C <configfile> use given configuration file")
- print(" default: /etc/shbackup.conf")
- print(" -m, --mode <mode> override mode: full, diff, or incr")
- print(" -e, --epoch <epoch> create backup for given epoch:")
- print(" year, month, week, day, hour")
- print(" -h, --help print this usage text")
+ print(" -h, --help print this usage text")
+ print(" -c, --conf <configfile> use given configuration file")
+ print(" default: /etc/shbackup.conf")
+ print(" -e, --epoch <epoch> force to create backup for given epoch:")
+ print(" year, month, week, day, hour, sporadic")
+ print(" -m, --mode <mode> override mode: full, diff, or incr")
if __name__ == "__main__":
printUsage()
exit(0)
- elif opt in ["-C", "--config"]:
+ elif opt in ["-c", "--conf"]:
i += 1
conffn = sys.argv[i]
if cmd == "prune":
man.prune()
- except Config.ReadException as e:
- print("Error reading config file: ", end="")
- for a in e.args:
- print(a, end=" ")
- print()
+ except (Config.ReadError, configparser.DuplicateOptionError) as e:
+ print("Error reading config file: " + e.message)