X-Git-Url: https://git.sthu.org/?p=sitarba.git;a=blobdiff_plain;f=shbackup;h=17bd3318208b8ce01e6363c68a628e15a80a55a7;hp=ca00a1305bf04a3d6178cbd7ea1c24c244573a8b;hb=e56611c3e966f758889d7b49f9818a43ae90c5ec;hpb=62a41f19c5f25566b02fc5973510e1a1e4baa8d6 diff --git a/shbackup b/shbackup index ca00a13..17bd331 100755 --- a/shbackup +++ b/shbackup @@ -31,11 +31,31 @@ class Backup: 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.""" @@ -52,9 +72,11 @@ class Backup: 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.""" @@ -65,7 +87,7 @@ class Config: 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" @@ -92,20 +114,22 @@ class Config: """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"): @@ -113,24 +137,27 @@ class Config: 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"]: @@ -141,12 +168,12 @@ class Config: 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() @@ -168,8 +195,9 @@ class Config: 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) @@ -189,18 +217,7 @@ class BackupManager: 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 @@ -248,28 +265,12 @@ class BackupManager: 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): @@ -304,23 +305,29 @@ class BackupManager: 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) @@ -338,32 +345,54 @@ class BackupManager: 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""" @@ -380,12 +409,13 @@ def printUsage(): print(" prune prune outdated/old backups") print("") print("Options:") - print(" -C use given configuration file") + print(" -h, --help print this usage text") + print(" -c, --conf use given configuration file") print(" default: /etc/shbackup.conf") - print(" -m, --mode override mode: full, diff, or incr") print(" -e, --epoch force to create backup for given epoch:") print(" year, month, week, day, hour, sporadic") - print(" -h, --help print this usage text") + print(" -m, --mode override mode: full, diff, or incr") + print(" -y, --yes always assume 'yes' when user is asked") if __name__ == "__main__": @@ -394,6 +424,7 @@ if __name__ == "__main__": cmd = "list" mode = None epoch = None + yes = False i = 0 while i < len(sys.argv)-1: @@ -404,10 +435,13 @@ if __name__ == "__main__": printUsage() exit(0) - elif opt in ["-C", "--config"]: + elif opt in ["-c", "--conf"]: i += 1 conffn = sys.argv[i] + elif opt in ["-y", "--yes"]: + yes = True + elif opt in ["-m", "--mode"]: i += 1 mode = sys.argv[i] @@ -431,24 +465,20 @@ if __name__ == "__main__": 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)