Modes = ["full", "incr", "diff"]
+
+class Options:
+ dryrun = False
+
+
class Epoch:
units = {
def isRipe(self, oldest, now):
- if self.unit==None:
+ if self.unit == None:
return True
delta = now-oldest
@staticmethod
def fromDirName(dirname):
- [strdate, strtime, epoch, mode] = dirname.split("-")
+ [strdate, strtime, epoch, mode] = dirname.split("-")
- if not mode in Modes:
- raise ValueError("Invalid mode: " + mode)
+ 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]))
+ 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)
+ return Backup(date, epoch, mode)
def __repr__(self):
return "[date: " + self.date.ctime() + \
agestr = "(%s h)" % int(total_hours)
else:
agestr = "(%s d)" % age.days
- return "%16s %7s %10s %4s" % (
+ return "%16s %9s %10s %4s" % (
self.date.strftime("%Y-%m-%d %H:%M"), agestr,
self.epoch, self.mode)
self.sets = []
self.checksum = None
self.lastchecksum = None
- self.epochs = Epochs = { "sporadic" : Epoch() }
+ self.epochs = { "sporadic" : Epoch() }
def __repr__(self):
def _read_global(self, config, sec):
for opt in config.options(sec):
- if opt=="backupdir":
+ 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":
+ 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":
+ 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))
e = Epoch()
if name in self.epochs:
raise Config.ReadError("Epoch '{0}' already defined.".format(name))
+ p = re.compile(r'^\w+$')
+ if not p.match(name):
+ raise Config.ReadError("Epoch name '{0}' does not only " + \
+ "comprise alphanumeric characters.".format(name))
if name in Epoch.units:
e.unit = name
for opt in config.options(sec):
- if opt=="numkeeps":
+ if opt == "numkeeps":
try:
e.numkeeps = int(config.getint(sec, opt))
except ValueError:
if e.numkeeps <= 0:
raise Config.ReadError("Non-positive numkeeps '{0}' given.".format(e.numkeeps))
- elif opt=="mode":
+ 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":
+ 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)
+ td = config.get(sec, opt)
try:
mult, unit = Epoch.parseTimedelta(td)
e.unit = unit
def _read_set(self, config, sec):
name = sec[4:].strip()
+ p = re.compile(r'^\w+$')
+ if not p.match(name):
+ raise Config.ReadError("Set name '{0}' does not only " + \
+ "comprise alphanumeric characters.".format(name))
+
dirs = []
excludes = []
if opt.startswith("dir"):
dirs += [config.get(sec, opt)]
elif opt.startswith("exclude"):
- excludes += [config.get(sec,opt)]
+ excludes += [config.get(sec, opt)]
else:
raise Config.ReadError("Unknown option '" + opt + "'.")
for sec in config.sections():
- if sec=="global":
+ if sec == "global":
self._read_global(config, sec)
elif sec.startswith("epoch "):
return [ d for d in dirs if os.path.isdir(os.path.join(basedir, d)) ]
- def listOldBackups(self):
+ def listExistingBackups(self):
"""Returns a list of old backups."""
backups = []
continue
# Get backups of that epoch
- byepoch = list(sorted( [ b for b in backups if b.epoch==e], \
+ byepoch = list(sorted( [ b for b in backups if b.epoch == e], \
key=lambda b: b.date))
# If there are any, determine the latest
taropts = []
# Tar is verbose is sitarba is verbose
- if LogConf.con.level <= logging.INFO:
+ if LogConf.con.level <= logging.DEBUG:
taropts += ["--verbose"]
# Add the since date, if given
# Read stdout and stderr of tarp
errmsg = b""
while tarp.poll() == None:
- rd,wr,ex = select.select([tarp.stdout, tarp.stderr], [], [], 0.05)
+ 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:
then use mode for given epoch. Use given mode otherwise."""
now = datetime.datetime.now()
- oldbackups = self.listOldBackups()
+ oldbackups = self.listExistingBackups()
# Get epoch of backup
if epoch == None:
oldfullbackups = [ b for b in oldbackups if b.mode == "full" ]
# No old full backups existing
- if mode != "full" and len(oldfullbackups)==0:
+ if mode != "full" and len(oldfullbackups) == 0:
logging.info("No full backups existing. Making a full backup.")
# Checksum changed -> self.config file changed
if since != None:
logging.debug("Making backup relative to " + since.ctime())
+ if Options.dryrun:
+ return
+
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) )
+ tmpdirname = dirname + ("-%x" % int(random.random()*2e16) )
targetdir = os.path.join(basedir, tmpdirname)
- os.mkdir( targetdir )
+ os.mkdir(targetdir)
# Add file logger
logfile = logging.getLogger("backuplog")
- fil = logging.FileHandler( os.path.join(targetdir, "log") )
+ fil = logging.FileHandler(os.path.join(targetdir, "log"))
fil.setLevel(logging.DEBUG)
logfile.addHandler(fil)
self.backupFileSet(s, targetdir, excludes, since)
logfile.info("Stopped: " + datetime.datetime.now().ctime())
+ fil.close()
# Rename backup directory to final name
os.rename( targetdir, os.path.join(basedir, dirname) )
"""Prune old backup files"""
allDirs = sorted(self.listAllDirs())
- # Collect all directories not matching backup name
+ # Collect all directories that are removed
removeDirs = [ d for d in allDirs if not Backup.isBackupDir(d) ]
- # Get all directories which are kept
- backups = self.listOldBackups()
- keepdirs = []
+ # Get all backups
+ backups = self.listExistingBackups()
+ # Group backups by epoch and sort them by age
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() }
+ key=lambda b : b.date, reverse=True)) \
+ for e in self.conf.getRealEpochsSorted() }
+ # If we have too many backups of a specific epoch --> add them to remove list
for e in byepoch:
epoch = self.conf.epochs[e]
old = byepoch[e][epoch.numkeeps:]
logging.info("No stale/outdated entries to remove.")
return
+ if Options.dryrun:
+ return
+
basedir = self.conf.backupdir
yesno = self.ask_user_yesno("Remove entries marked by '*'? [y, N] ")
if yesno == "y":
try:
shutil.rmtree(os.path.join(basedir, d))
except OSError as e:
- logging.error("Error when removing '%s': %s" % (d,e.strerror) )
+ logging.error("Error when removing '%s': %s" % (d, e.strerror) )
def ask_user_yesno(self, question):
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(" -n, --dry-run don't do anything, just tell what would be done")
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")
logging.error("Unknown mode '" + mode + "'.")
exit(1)
+ elif opt in ["-n", "--dry-run"]:
+ Options.dryrun = True
+
elif opt in ["-e", "--epoch"]:
i += 1
epoch = sys.argv[i]
logging.debug("Config: " + str(man.conf))
- if epoch!=None and not epoch in man.conf.epochs.keys():
+ if epoch != None and not epoch in man.conf.epochs.keys():
logging.error("Unknown epoch '" + epoch + "'.")
exit(1)
man.backup(epoch, mode)
if cmd == "list":
- for b in sorted(man.listOldBackups(), key=lambda b: b.date):
+ for b in sorted(man.listExistingBackups(), key=lambda b: b.date):
print(b.colAlignedString())
if cmd == "prune":