import os, shutil, sys
import configparser
import hashlib
-import subprocess
+import subprocess, fcntl, select
import random, re
import logging
", mode: " + self.mode + "]"
def colAlignedString(self):
- return "%16s %8s %4s" % ( \
- self.date.strftime("%Y-%m-%d %H:%M"), self.epoch, self.mode)
+ age = datetime.datetime.now() - self.date
+ total_hours = age.total_seconds()/3600
+ if total_hours <= 48:
+ agestr = "(%s h)" % int(total_hours)
+ else:
+ agestr = "(%s d)" % age.days
+ return "%16s %7s %8s %4s" % ( \
+ self.date.strftime("%Y-%m-%d %H:%M"), agestr, self.epoch, self.mode)
@staticmethod
def getDirName(date, epoch, mode):
def backupFileSet(self, fileset, targetdir, since=None):
"""Create an archive for given fileset at given target directory."""
- logger = logging.getLogger('backup')
+ logfile = logging.getLogger('backuplog')
+ logfile.info("Running file set: " + fileset.name)
- logger.info("Running file set: " + fileset.name)
tarpath = "/bin/tar"
fsfn = os.path.join(targetdir, fileset.name) + "." + self.conf.format
- taropts = ["-cpva"]
+ taropts = []
+ # Add the since date, if given
if since != None:
taropts += ["-N", since.strftime("%Y-%m-%d %H:%M:%S")]
+ # Add the exclude patterns
for pat in self.conf.exclpatterns:
taropts += ["--exclude", pat]
- tarargs = [tarpath] + taropts + ["-f", fsfn] + fileset.dirs
- logger.debug("tar call: " + " ".join(tarargs))
+ # Adding directories to backup
+ taropts += ["-C", "/"] + [ "./" + d.lstrip("/") for d in fileset.dirs]
+
+ # Launch the tar process
+ tarargs = [tarpath] + ["-cpvaf", fsfn] + taropts
+ logfile.debug("tar call: " + " ".join(tarargs))
tarp = subprocess.Popen( tarargs, bufsize=-1, \
stdout=subprocess.PIPE, stderr=subprocess.PIPE )
- # Output stdout of tar
+ # Change tarp's stdout and stderr to non-blocking
+ for s in [tarp.stdout, tarp.stderr]:
+ fd = s.fileno()
+ fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+ fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+ # Read stdout and stderr of tarp
+ errmsg = b""
while tarp.poll() == None:
- l = tarp.stdout.readline()
- if l != "":
- logging.debug(l.decode().rstrip())
+ 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:
+ errmsg += tarp.stderr.read()
- # Output remaining output of tar
+ # Get the remainging output of tarp
for l in tarp.stdout.readlines():
logging.debug(l.decode().rstrip())
+ errmsg += tarp.stderr.read()
+ # Get return code of tarp
rett = tarp.wait()
if rett != 0:
- for l in tarp.stderr.readlines():
- logger.error( l.decode().strip().rstrip() )
- sys.stderr.write( tarp.stderr.read().decode() )
- logger.error(tarpath + " returned with exit status " + str(rett) + ".")
+ for l in errmsg.decode().split("\n"):
+ logfile.error(l)
+ logfile.error(tarpath + " returned with exit status " + str(rett) + ".")
def backup(self, epoch=None, mode=None):
os.mkdir( targetdir )
- logger = logging.getLogger('backup')
- ch = logging.FileHandler( os.path.join(targetdir, "log") )
- ch.setLevel(logging.INFO)
- logger.addHandler(ch)
- logger.info("Started: " + now.ctime())
+ # Add file logger
+ logfile = logging.getLogger("backuplog")
+ fil = logging.FileHandler( os.path.join(targetdir, "log") )
+ fil.setLevel(logging.DEBUG)
+ logfile.addHandler(fil)
+
+ logfile.info("Started: " + now.ctime())
# Backup all file sets
for s in self.conf.sets:
self.backupFileSet(s, targetdir, since)
- logger.info("Stopped: " + datetime.datetime.now().ctime())
+ logfile.info("Stopped: " + datetime.datetime.now().ctime())
# Rename backup directory to final name
os.rename( targetdir, os.path.join(basedir, dirname) )
yesno = self.ask_user_yesno("Remove entries marked by '*'? [y, N] ")
if yesno == "y":
for d in removeDirs:
- shutil.rmtree(os.path.join(basedir, d))
+ try:
+ shutil.rmtree(os.path.join(basedir, d))
+ except OSError as e:
+ logging.error("Error when removing '%s': %s" % (d,e.strerror) )
+
def ask_user_yesno(self, question):
- if logging.getLogger().isEnabledFor(logging.INFO):
+ if LogConf.con.level <= logging.INFO:
return input(question)
else:
return "y"
print(" -m, --mode <mode> override mode: full, diff, or incr")
print(" -v, --verbose be more verbose and interact with user")
print(" --verbosity LEVEL set verbosity to LEVEL, which can be")
- print(" warning, info, debug")
+ print(" error, warning, info, debug")
print(" -V, --version print version info")
+
+class LogConf:
+ """Encapsulates logging configuration"""
+
+ con = logging.StreamHandler(sys.stderr)
+
+ @classmethod
+ def setup(cls):
+ """Setup logging system"""
+ conlog = logging.getLogger()
+ conlog.setLevel(logging.DEBUG)
+
+ cls.con.setLevel(logging.WARNING)
+ conlog.addHandler(cls.con)
+
+ fillog = logging.getLogger("backuplog")
+ fillog.setLevel(logging.DEBUG)
+
+
if __name__ == "__main__":
- logging.basicConfig(format='%(message)s')
+ LogConf.setup()
+
conffn = "/etc/shbackup.conf"
cmd = "list"
mode = None
exit(0)
elif opt in ["-v", "--verbose"]:
- logging.getLogger().setLevel(logging.INFO)
+ LogConf.con.setLevel(logging.INFO)
elif opt in ["--verbosity"]:
i += 1
numlevel = getattr(logging, level.upper(), None)
if not isinstance(numlevel, int):
raise ValueError('Invalid verbosity level: %s' % level)
- logging.getLogger().setLevel(numlevel)
+ LogConf.con.setLevel(numlevel)
elif opt in ["-m", "--mode"]:
i += 1