#!/usr/bin/python
#
# Simple Backup suit
# 
# Running this command will execute a single backup run according to a configuration file
#
# Author: Aigars Mahinovs <aigarius@debian.org>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

import sys
import os
import errno
import atexit
import stat
import datetime
import time
import os.path
import cPickle as pickle
import shutil
import ConfigParser
import re
import socket
import tempfile
import upgrade_backups

try:
    import gnomevfs
except ImportError:
    import gnome.vfs as gnomevfs

class MyConfigParser(ConfigParser.ConfigParser):
	def optionxform(self, option):
	    return str( option )

# Define default values & load config file
# Default values

config = "/etc/sbackup.conf"
target = "/var/backup/"
increment = 0
lockfile = "/var/lock/sbackup.lock"
hostname = socket.gethostname()
maxincrement = 7
# Backup format: 1 - allways use .tar.gz
format=1

# directories allready added to the archive
dirs_in = ["/"]

dirconfig = { "/etc":1, "/home":1, "/usr/local": 1, "/root":1, "/var": 1, "/var/cache":0, "/var/spool":0, "/var/tmp":0 }
gexclude = [r"\.mp3",r"\.avi",r"\.mpeg",r"\.mkv",r"\.ogg", r"\.iso"]
maxsize = 10*1024*1024

os.umask( 077 )

conf = MyConfigParser()
conf.read( config )

if not conf.has_option( "general", "target" ):
    print "E: Configuration file '"+config+"' is no found. Aborting."
    sys.exit(1)    

if conf.has_option( "general", "target" ):
    target = conf.get( "general", "target" )
if conf.has_option( "general", "lockfile" ):
    lockfile = conf.get( "general", "lockfile" )
if conf.has_option( "general", "maxincrement" ):
    maxincrement = conf.getint( "general", "maxincrement" )
if conf.has_option( "general", "format" ):
    format = conf.getint( "general", "format" )
if conf.has_section( "dirconfig" ):
    dirconfig = dict(conf.items("dirconfig"))
for i in dirconfig:
    dirconfig[i]=int(dirconfig[i])
if conf.has_option( "exclude", "regex" ):
    gexclude = str(conf.get( "exclude", "regex" )).split(",")
if conf.has_option( "exclude", "maxsize" ):
    maxsize = conf.getint( "exclude", "maxsize" )

rexclude = [ re.compile(p) for p in gexclude ]

flist = False
flistid = 0
flist_name = ""
fprops = False

def btree_r_add( adir ):
	"""Add a directory to the btree with reversed recursion - take defaults from parent"""
	global btree
	
	adir2 = adir
	adir = adir.replace("//","/")
	while adir != adir2:
		adir2 = adir
		adir = adir.replace("//","/")	

	parent2 = os.path.split( adir )[0]
	parent = parent2
	if parent == "/":
		parent = ""
	if adir in btree:
		pass
	elif parent in btree:
		props = btree[parent]
		for child in os.listdir( parent2 ):
			btree[os.path.normpath(parent+"/"+child)] = props
		btree[parent] = (-1, btree[parent][1], btree[parent][2])
	else:
		btree_r_add( parent )
		props = btree[parent]
		for child in os.listdir( parent2 ):
			btree[os.path.normpath(parent+"/"+child)] = props
		btree[parent] = (-1, btree[parent][1], btree[parent][2])

def is_parent( parent, child ):
	""" Compares directories - returns child only if it is a child of the parent """
	if str(child)[0:len(parent)] == parent:
		return child

def do_backup_init( ):
	global flist, flistid, flist_name, fprops, fpropsid, fprops_name
	if local:
		(flistid, flist_name) = tempfile.mkstemp()
		flist = os.fdopen( flistid, "w" )
		fprops = open(tdir+"/fprops", "w")
	else:
		(flistid, flist_name) = tempfile.mkstemp()
		flist = os.fdopen( flistid, "w" )
		(fpropsid, fprops_name) = tempfile.mkstemp()
		fprops = os.fdopen( fpropsid, "w" )

def do_backup_finish( ):
	flist.close()
	fprops.close()
	tarline = "tar -czS -C / --no-recursion  -T "+flist_name+" "
	if local:
		tarline = tarline+" --force-local -f "+tdir+"/files.tgz"
		tarline = tarline+" 2>/dev/null"
		os.system( tarline )
		shutil.move( flist_name, tdir+"/flist" )
	else:
		tarline = tarline+" 2>/dev/null"
		turi = gnomevfs.URI( tdir+"/files.tgz" )
		tardst = gnomevfs.create( turi, 2 )
		tarsrc = os.popen( tarline )
		shutil.copyfileobj( tarsrc, tardst, 100*1024 )
		tarsrc.close()
		tardst.close()
		s1 = open( fprops_name, "r" )
		turi = gnomevfs.URI( tdir+"/fprops" )
		d1 = gnomevfs.create( turi, 2 )
		shutil.copyfileobj( s1, d1 )
		s1.close()
		d1.close()
		s2 = open( flist_name, "r" )
		turi = gnomevfs.URI( tdir+"/flist" )
		d2 = gnomevfs.create( turi, 2 )
		shutil.copyfileobj( s2, d2 )
		s2.close()
		d2.close()

def do_add_dir ( dirname, props ):
	do_add_file( dirname, props )

def do_add_file( dirname, props ):
	parent = os.path.split( dirname )[0]
	if not parent in dirs_in and parent != dirname:
		s = os.lstat( parent )
		do_add_dir( parent, str(s.st_mode)+str(s.st_uid)+str(s.st_gid)+str(s.st_size)+str(s.st_mtime))
	flist.write( dirname+"\n" )
	fprops.write( props+"\n" )
	dirs_in.append( dirname )

def do_backup( adir ):
	""" Finds all files to be backuped in the directory and calls respective backup suroutines """
	s = os.lstat(adir)
	parent = os.path.split( adir )[0]
	if not os.path.isdir(adir) or os.path.islink(adir):
		if s.st_size > maxsize or prev.count( adir+","+str(s.st_mode)+str(s.st_uid)+str(s.st_gid)+str(s.st_size)+str(s.st_mtime) ):
		    return []
		for r in rexclude:
		    if r.search( adir ):
			return []
		do_add_file( adir, str(s.st_mode)+str(s.st_uid)+str(s.st_gid)+str(s.st_size)+str(s.st_mtime) )
	else:
		if not increment:
			do_add_dir( adir, str(s.st_mode)+str(s.st_uid)+str(s.st_gid)+str(s.st_size)+str(s.st_mtime) )
		for child in os.listdir( adir ):
			if os.path.isdir( adir+"/"+child ) and not os.path.islink( adir+"/"+child ):
			    do_backup( adir+"/"+child )
			else:
			    s = os.lstat( adir+"/"+child )
			    if maxsize > 0 and s.st_size > maxsize:
				continue
			    if prev.count( adir+"/"+child+","+str(s.st_mode)+str(s.st_uid)+str(s.st_gid)+str(s.st_size)+str(s.st_mtime) ): 
				continue
			    skip=False
			    for r in rexclude:
				if r.search( adir+"/"+child ):
				    skip=True
			    if skip:
				continue
			    do_add_file( adir+"/"+child, str(s.st_mode)+str(s.st_uid)+str(s.st_gid)+str(s.st_size)+str(s.st_mtime) )

# End of helpfull functions :)
		
# Check our user id
if os.geteuid() != 0: sys.exit ("Currently backup is only runnable by root")

good = False

# Create the lockfile so noone disturbs us
try: 
	open( lockfile, "r" )
except IOError:
	good = True	
if not good : sys.exit ("E: Another Simple Backup daemon already running: exiting")

try:
	lock = open( lockfile, "w+" )
except IOError:
	print "E: Cann't create a lockfile: ", sys.exc_info()[1]
	sys.exit(1)

def exitfunc():
	# All done
	# Remove lockfile
	lock.close
	os.remove (lockfile)

atexit.register(exitfunc)

# Checking if the target directory is local or remote
local = True

try:
    if gnomevfs.get_uri_scheme( target ) == "file":
	target = gnomevfs.get_local_path_from_uri( target )
    else:
	local = False
except:
    pass

# Checking if the target directory exists (or can be created)
if local:
    try:
	os.makedirs( target, 0700 )
    except OSError:
	if not sys.exc_info()[1].errno == errno.EEXIST:
		print "E: Target directory doesn't exist and cann't be created: ", sys.exc_info()[1]
		sys.exit(1)
else:
    try:
	tinfo = gnomevfs.get_file_info( target )
    except:
	try: 
	    gnomevfs.make_directory( target, 0700 )
	    tinfo = gnomevfs.get_file_info( target )
	except:
	    print "E: Target directory doesn't exist and cann't be created: ", sys.exc_info()[1]
	    sys.exit(1)

    if tinfo.type == 2 and (tinfo.permissions / (8*8) )-(tinfo.permissions/(8*8*8)*8)  == 7:
	pass # a directory with full permissions
    else:
	try: 
	    gnomevfs.unlink( target ) 
	    gnomevfs.make_directory( target, 0700 )
	except:
	    print "E: Target directory isn't a directory and cann't be recreated: ", sys.exc_info()[1]
	    sys.exit(1)
	    

# Checking if it is writable directory

if local:
    if not (os.path.isdir( target ) and os.access( target, os.R_OK | os.W_OK | os.X_OK ) ):
	sys.exit( "E: Target directory is not writable (or not a directory)" )
else:
    pass #Done already

# Upgrade directories to new format

upgrader = upgrade_backups.SBUpgrade()
upgrader.upgrade_target( target )

# Determine whether to do a full or incremental backup

r = re.compile(r"^(\d{4})-(\d{2})-(\d{2})_(\d{2})[\:\.](\d{2})[\:\.](\d{2})\.\d+\..*?\.(.+)$")

if local:
    listing = os.listdir( target )
    listing = filter( r.search, listing )
else:
    d = gnomevfs.open_directory( target )
    listing = []
    for f in d:
	if f.type == 2 and f.name != "." and f.name != ".." and r.search( f.name ):
	    listing.append( f.name )

listing.sort()
listing.reverse()

# Check if these directories are complete and remove from the list those that are not
for adir in listing[:]:  #TODO v1 directories are ignored here
	if local and not os.access( target+"/"+adir+"/flist", os.F_OK ):
		listing.remove( adir )
	if not local and not gnomevfs.exists( target+"/"+adir+"/flist" ):
		listing.remove( adir )

prev = []
base = False

if listing == []:
    increment = 0	# No backups found -> make a full backup
else:
    m = r.search( listing[0] )
    if m.group( 7 ) == "ful":  # Last backup was full backup
	if (datetime.date.today() - datetime.date(int(m.group(1)),int(m.group(2)),int(m.group(3)) ) ).days <= maxincrement :
    	    # Less then maxincrement days passed since that -> make an increment
	    increment = time.mktime((int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4)),int(m.group(5)),int(m.group(6)),0,0,-1))
	    base = listing[0]
	    if local:		#TODO v1 directories are ignored here
		a = open(target+"/"+base+"/flist","r")
		b = open(target+"/"+base+"/fprops","r")
	    	for f in a:
			prev.append( f[:-1]+","+b.next()[:-1] )
	    	a.close()
	    	b.close()
	    else:
		prev = [ a+","+b for a,b in zip(str( gnomevfs.read_entire_file( target+"/"+base+"/flist" ) ).split( "\n" ), str( gnomevfs.read_entire_file( target+"/"+base+"/fprops" )).split( "\n" )) ]
	else:
	    increment = 0      # Too old -> make full backup
    else:
	r2 = re.compile(r"ful$")   # Last backup was an increment - lets search for the last full one
	for i in listing:
	    if local:
		a = open(target+"/"+i+"/flist","r")
		b = open(target+"/"+i+"/fprops","r")
	    	for f in a:
			prev.append( f[:-1]+","+b.next()[:-1] )
	    	a.close()
	    	b.close()
	    else:
		prev.extend( [ a+","+b for a,b in zip(str( gnomevfs.read_entire_file( target+"/"+i+"/flist" ) ).split( "\n" ), str( gnomevfs.read_entire_file( target+"/"+i+"/fprops" )).split( "\n" )) ] )
	    if r2.search( i ):
		m = r.search( i )
		if (datetime.date.today() - datetime.date(int(m.group(1)),int(m.group(2)),int(m.group(3)) ) ).days <= maxincrement :
		    # Last full backup is fresh -> make an increment
		    m = r.search( listing[0] )
		    increment = time.mktime((int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4)),int(m.group(5)),int(m.group(6)),0,0,-1))
		    base = listing[0]
		else:
		    increment = 0    # Last full backup is old -> make a full backup
		    prev = []
		break
	else:
	    increment = 0            # No full backup found 8) -> lets make a full backup to be safe
	    prev = []


# Determine and create backup target directory

tdir = target + "/" + datetime.datetime.now().isoformat("_").replace( ":", "." ) + "." + hostname + "."
if increment != 0:
	tdir = tdir + "inc/"
else:
	tdir = tdir + "ful/"

if local:
    os.makedirs( tdir, 0700 )
    f = open( tdir+"ver", 'w' )
else:
    gnomevfs.make_directory( tdir, 0700 )
    f = gnomevfs.create( tdir+"ver", 2 )

f.write( "1.2\n" )
f.close


# Create '.../base' here, if incremental backup

if base:
    if local:
	f = open( tdir+"base", 'w' )
    else:
	f = gnomevfs.create( tdir+"base", 2 )
    f.write( base+"\n" )
    f.close

tar = True

# Initiate backup tree structure

if dirconfig.has_key("/"):
	btree = { "": (dirconfig.pop("/"),0,[]) }
else:
	btree = { "": (0,0,[]) }

# Populate the backup tree structure
sdirs = dirconfig.keys()
sdirs.sort()
for adir in sdirs:
	btree_r_add( adir )
	btree.update( btree.fromkeys( [is_parent(adir, adir2) for adir2 in btree.keys()] , (dirconfig[adir],0,[]) ) )
	btree[os.path.normpath(adir)] = (dirconfig[adir],0,[])

# Remove target from the backup
if local:
	btree_r_add( target )
	btree.update( btree.fromkeys( [is_parent(target, adir2) for adir2 in btree.keys()] , (0,0,[]) ) )
	btree[os.path.normpath(target)] = (0,0,[])

# Write excludes
if local:
    pickle.dump( gexclude, open(tdir+"excludes","w") )
else:
    pickle.dump( gexclude, gnomevfs.create(tdir+"excludes", 2) )

# Backup list of installed packages (Debian only part)
command = "dpkg --get-selections"
s = os.popen( command )
if local:
    d = open( tdir+"packages", "w" )
else:
    d = gnomevfs.create( tdir+"packages", 2 )
shutil.copyfileobj( s, d )
s.close()
# End of Debian only part

# Make the backup ...
do_backup_init()
bdirs = btree.keys()
bdirs.sort()
for adir in bdirs:
	if adir == "":
		adir2 = "/"
	else:
		adir2 = adir
	if btree[adir][0] == 1 and os.path.exists( adir2 ):
		do_backup( adir2 )

do_backup_finish()
# ... done.

# Write statistics
# TODO for next versions #

sys.exit( 0 )

