Fix str formatting, float given instead of int
[sitarba.git] / sitarba
diff --git a/sitarba b/sitarba
index e57e2508500fe22a966357413cb6d398c73a9f20..6f7cf5b69242c641ef61ab22c3d63e2cbe480b34 100755 (executable)
--- a/sitarba
+++ b/sitarba
@@ -15,6 +15,11 @@ import logging
 
 Modes = ["full", "incr", "diff"]
 
+
+class Options:
+    dryrun = False
+
+
 class Epoch:
 
     units = {
@@ -45,7 +50,7 @@ class Epoch:
 
     def isRipe(self, oldest, now):
 
-        if self.unit==None:
+        if self.unit == None:
             return True
 
         delta = now-oldest
@@ -115,16 +120,16 @@ class Backup:
 
     @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() + \
@@ -138,7 +143,7 @@ class Backup:
             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)
 
@@ -178,7 +183,7 @@ class Config:
         self.sets = []
         self.checksum = None
         self.lastchecksum = None
-        self.epochs = Epochs = { "sporadic" : Epoch() }
+        self.epochs = { "sporadic" : Epoch() }
 
 
     def __repr__(self):
@@ -201,15 +206,15 @@ class Config:
 
     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))
@@ -224,11 +229,15 @@ class Config:
         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:
@@ -236,16 +245,16 @@ class Config:
                 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
@@ -267,6 +276,11 @@ class Config:
 
     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 = []
 
@@ -274,7 +288,7 @@ class Config:
             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 + "'.")
 
@@ -296,7 +310,7 @@ class Config:
 
         for sec in config.sections():
 
-            if sec=="global":
+            if sec == "global":
                 self._read_global(config, sec)
 
             elif sec.startswith("epoch "):
@@ -347,7 +361,7 @@ class BackupManager:
         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 = []
@@ -369,7 +383,7 @@ class BackupManager:
                 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
@@ -393,6 +407,10 @@ class BackupManager:
         fsfn = os.path.join(targetdir, fileset.name) + "." + self.conf.format
         taropts = []
 
+        # Tar is verbose is sitarba is verbose
+        if LogConf.con.level <= logging.DEBUG:
+            taropts += ["--verbose"]
+
         # Add the since date, if given
         if since != None:
             taropts += ["-N", since.strftime("%Y-%m-%d %H:%M:%S")]
@@ -405,11 +423,12 @@ class BackupManager:
         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
+        tarargs = [self.conf.tarbin] + ["-cpaf", fsfn] + taropts
         logfile.debug("tar call: " + " ".join(tarargs))
         tarp = subprocess.Popen( tarargs, bufsize=-1, \
                 stdout=subprocess.PIPE, stderr=subprocess.PIPE )
@@ -423,7 +442,7 @@ class BackupManager:
         # 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:
@@ -449,7 +468,7 @@ class BackupManager:
         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:
@@ -466,7 +485,7 @@ class BackupManager:
         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
@@ -484,6 +503,9 @@ class BackupManager:
         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
@@ -491,14 +513,14 @@ class BackupManager:
         # 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)
 
@@ -510,6 +532,7 @@ class BackupManager:
             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) )
@@ -526,14 +549,16 @@ class BackupManager:
         """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:]
@@ -563,6 +588,9 @@ class BackupManager:
             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":
@@ -570,7 +598,7 @@ class BackupManager:
                 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):
@@ -583,7 +611,7 @@ class BackupManager:
 def printUsage():
     """Print --help text"""
 
-    print("shbackup - a simple backup solution.")
+    print("sitarba - a simple backup solution.")
     print("")
     print("Usage:")
     print("  " + sys.argv[0] + " {options} [cmd]")
@@ -597,10 +625,11 @@ def printUsage():
     print("Options:")
     print("  -h, --help                 print this usage text")
     print("  -c, --conf FILE            use given configuration file")
-    print("                             default: /etc/shbackup.conf")
+    print("                             default: /etc/sitarba.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("  -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")
@@ -630,7 +659,7 @@ if __name__ == "__main__":
 
     LogConf.setup()
 
-    conffn = "/etc/shbackup.conf"
+    conffn = "/etc/sitarba.conf"
     cmd = "list"
     mode = None
     epoch = None
@@ -649,7 +678,7 @@ if __name__ == "__main__":
             conffn = sys.argv[i]
 
         elif opt in ["-V", "--version"]:
-            print("shbackup " + __version__)
+            print("sitarba " + __version__)
             exit(0)
 
         elif opt in ["-v", "--verbose"]:
@@ -670,6 +699,9 @@ if __name__ == "__main__":
                 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]
@@ -686,7 +718,7 @@ if __name__ == "__main__":
 
         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)
 
@@ -694,7 +726,7 @@ if __name__ == "__main__":
             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":