self.epoch = epoch
self.mode = mode
+ @staticmethod
+ def fromDirName(dirname):
+ [strdate, strtime, epoch, mode] = dirname.split("-")
+
+ if not epoch in Epoch.keys():
+ raise ValueError("Invalid epoch: " + epoch)
+
+ if not mode in Mode:
+ 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 __str__(self):
return "[date: " + self.date.ctime() + \
", epoch: " + self.epoch + \
", mode: " + self.mode + "]"
+ def colAlignedString(self):
+ return "%16s %8s %4s" % ( \
+ self.date.strftime("%Y-%m-%d %H:%M"), self.epoch, self.mode)
+
@staticmethod
def getDirName(date, epoch, mode):
"""Get directory name of backup by given properties."""
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."""
"""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"):
if opt.startswith("keep"):
epoch = opt[4:]
if not epoch in RealEpoch.keys():
- raise Config.ReadException("Invalid option 'keep" + epoch + "'.")
- self.epochkeeps[epoch] = int(config.getint("history", opt))
+ 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 RealEpoch.keys():
- raise Config.ReadException("Invalid option 'mode" + epoch + "'.")
+ 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()
class BackupManager:
"""List and create backups"""
- def __init__(self, conffn):
+ def __init__(self, conffn, alwaysyes):
self.conf = Config()
+ self.alwaysyes = alwaysyes
self.conf.read(conffn)
backups = []
for entry in [ b for b in self.listAllDirs() if Backup.isBackupDir(b) ]:
- [strdate, strtime, epoch, mode] = entry.split("-")
-
- if not epoch in Epoch.keys():
- raise ValueError("Invalid epoch: " + epoch)
-
- if not mode in Mode:
- 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]))
- backups += [ Backup(date, epoch, mode) ]
+ backups += [ Backup.fromDirName(entry) ]
return backups
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):
if mode != "full":
print("** Warning: full backup recommended!")
- # Create new target directory
- basedir = self.conf.directory
- dirname = Backup.getDirName(now, epoch, mode)
- tmpdirname = dirname + ("-%x" % (random.random()*2e16) )
- targetdir = os.path.join(basedir, tmpdirname)
- os.mkdir( targetdir )
# If we have a full backup, we backup everything
since = None
-
- # Get latest full backup time
if mode == "diff":
since = sorted(oldfullbackups, key=lambda b: b.date)[-1].date
- # Get latest backup time
elif mode == "incr":
since = sorted(oldbackups, key=lambda b: b.date)[-1].date
+ if since != None:
+ print("Making backup relative to ", since.ctime())
+
+ yesno = self.ask_user_yesno("Proceed? [Y, n] ")
+ if yesno == "n":
+ return
+
+ # Create new target directory
+ basedir = self.conf.directory
+ dirname = Backup.getDirName(now, epoch, mode)
+ tmpdirname = dirname + ("-%x" % (random.random()*2e16) )
+ targetdir = os.path.join(basedir, tmpdirname)
+ os.mkdir( targetdir )
+
+
# Backup all file sets
for s in self.conf.sets:
self.backupFileSet(s, targetdir, since)
def prune(self):
"""Prune old backup files"""
+ allDirs = self.listAllDirs()
# Collect all directories not matching backup name
- dirs = [ d for d in self.listAllDirs() if not Backup.isBackupDir(d) ]
+ removeDirs = [ d for d in allDirs if not Backup.isBackupDir(d) ]
- # Get all directories which are outdated
+ # 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 RealEpoch }
for e in byepoch:
keep = self.conf.epochkeeps[e]
old = byepoch[e][keep:]
- dirs += [ Backup.getDirName(b.date, b.epoch, b.mode) for b in old]
+ removeDirs += [ Backup.getDirName(b.date, b.epoch, b.mode) for b in old]
- if len(dirs) == 0:
- print("No stale/outdated entries to remove.")
- return
print("List of stale/outdated entries:")
- for d in dirs:
- print(" " + d)
+ for d in allDirs:
+ if d in removeDirs:
+ print("[*] ", end="")
+ else:
+ print("[ ] ", end="")
+
+ if Backup.isBackupDir(d):
+ print( Backup.fromDirName(d).colAlignedString())
+ else:
+ print(d)
+
+ # Check that dirs to be removed is in list of all dirs
+ for d in removeDirs:
+ assert( d in allDirs )
+
+ if len(removeDirs) == 0:
+ print("No stale/outdated entries to remove.")
+ return
basedir = self.conf.directory
- yesno = input("Remove listed entries? [y, N] ")
+ yesno = self.ask_user_yesno("Remove entries marked by '*'? [y, N] ")
if yesno == "y":
- for d in dirs:
+ for d in removeDirs:
shutil.rmtree(os.path.join(basedir, d))
+ def ask_user_yesno(self, question):
+ if self.alwaysyes:
+ print(question + " y")
+ return "y"
+ else:
+ return input(question)
+
def printUsage():
"""Print --help text"""
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")
+ print(" -y, --yes always assume 'yes' when user is asked")
if __name__ == "__main__":
cmd = "list"
mode = None
epoch = None
+ yes = False
i = 0
while i < len(sys.argv)-1:
i += 1
conffn = sys.argv[i]
+ elif opt in ["-y", "--yes"]:
+ yes = True
+
elif opt in ["-m", "--mode"]:
i += 1
mode = sys.argv[i]
exit(1)
try:
- man = BackupManager(conffn)
+ man = BackupManager(conffn, yes)
if cmd == "backup":
man.backup(epoch, mode)
if cmd == "list":
for b in sorted(man.listOldBackups(), key=lambda b: b.date):
- print(b.date.strftime("%Y-%m-%d %H:%M") + \
- "\t" + b.epoch + "\t" + b.mode)
+ print(b.colAlignedString())
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)