#!/usr/bin/env python
#
# RTBackup - Real-time backup tool
#
# Real-time backup script which periodically checks modification time of each
# file. It creates a copy of the file if its mtime is newer than stored in the
# database, with preserve the file and parent directories permissions.
#
# Requirements: SQLite 2.8.x
#
# This software may be freely redistributed under the terms of the GNU
# General Public License (GPL).

### config
file_dir = "/directory/you/want/to/backup"
backup_dir = "/directory/where/backup/will/be/stored"
backup_delay = "15m"                # delay between backups (15 minutes)
date_format = "%Y-%m-%d_%H:%M:%S"   # backup directory format (YY-MM-DD_HH:MM:SS)
db_filepath = "fileserver.db"       # path to backup database


### code - do not change anything below here
__author__ = "Damian Pasternok <my_forename at pasternok.org>"
__version__ = "0.1"
__date__ = "2010/07/14"
__copyright__ = "Copyright (C) 2010 Damian Pasternok"
__license__ = "GPL"

import datetime
import os
import signal
import sqlite
import stat
import sys
import time

class DBsqlite:
    def __init__(self, db_file):
        self.__connection = sqlite.connect(db_file)
        self.__cursor = self.__connection.cursor()

    def db_query(self, query):
        self.__cursor.execute(query)
        self.__connection.commit()

    def db_query_result(self):
        return self.__cursor.fetchall()

    def db_close(self):
        self.__cursor.close()
        self.__connection.close()

class FileBackup(DBsqlite):
    def __init__(self, db_file):
        # current file list
        proc_find = os.popen("find \"%s\" -type f" % file_dir)
        filelist = []
        for i in proc_find.readlines():
            filelist.append(i.rstrip())
        tablename = os.path.splitext(db_file.upper())[0]

        # first run
        if not os.path.isfile(db_file):
            # create new database and table with new result if doesn't exist
            print "Creating new database %s..." % db_file
            DBsqlite.__init__(self, db_file)
            print "Creating new table %s..." % tablename
            self.db_query("CREATE TABLE %s (relative_file_path TEXT, file_backup_date FLOAT)" % tablename)
        else:
            DBsqlite.__init__(self, db_file)

        # common datetime for current backup
        self.current_datetime = self.__current_datetime()
        # check which files need update
        for i in filelist:
            self.db_query("SELECT * FROM %s WHERE relative_file_path=\"%s\""
                % (tablename, FileOper().file_relative_path(i, file_dir)))
            res = self.db_query_result()

            current_relative_file_path = FileOper().file_relative_path(i, file_dir)
            current_relative_file_dir = FileOper().file_dir(current_relative_file_path)
            # create new record if file doesn't exist in the database
            if not res:
                print "Adding %s with timestamp %f..." % (current_relative_file_path,
                    FileOper().file_mtime(i))
                self.db_query("INSERT INTO %s VALUES(\"%s\", %f)"
                    % (tablename, current_relative_file_path, FileOper().file_mtime(i)))
                ###
            # then backup modified files
            # [0][1] - file mtime (float)
            if res and round(FileOper().file_mtime(i), 5) > round(res[0][1], 5):
                # create directory tree if not exists
                if not os.path.isdir(self.__backup_new_file_path(current_relative_file_dir)):
                    print "Creating directory %s..." % FileOper().file_relative_path(
                        self.__backup_new_file_path(current_relative_file_dir), backup_dir)
                    os.makedirs(self.__backup_new_file_path(current_relative_file_dir))
                # do not override root dir permissions
                if current_relative_file_dir != "/":
                    self.__keep_dir_permissions(current_relative_file_dir)
                print "Copying %s into %s..." % (current_relative_file_path,
                FileOper().file_relative_path(self.__backup_new_file_path(current_relative_file_path),
                    backup_dir))
                os.popen("cp -ar \"%s\" \"%s\"" % (i, self.__backup_new_file_path(current_relative_file_path)))
                # do not allow to modify the file copy
                os.popen("chmod a-w \"%s\"" % self.__backup_new_file_path(current_relative_file_path))
                print "Updating timestamp in database - old: %f, new: %f" % (res[0][1],
                    FileOper().file_mtime(i))
                self.db_query("UPDATE %s SET file_backup_date=%f WHERE relative_file_path=\"%s\""
                    % (tablename, FileOper().file_mtime(i), FileOper().file_relative_path(i, file_dir)))

        self.db_close()

    def __current_datetime(self):
	new_datetime = datetime.datetime.now()
	print "New backup timestamp: %s [backup_delay=%s]" % (new_datetime, backup_delay)
        return new_datetime.strftime(date_format)

    def __backup_new_file_path(self, relative_file_dir):
        backup_file_dir = backup_dir
        backup_file_dir += "/"
        backup_file_dir += self.current_datetime
        backup_file_dir += relative_file_dir
        return backup_file_dir

    def __keep_dir_permissions(self, relative_file_dir):
        dir_tree = FileOper().file_dir_tree(relative_file_dir)
        if dir_tree:
            for i in dir_tree:
                full_file_dir = file_dir
                full_file_dir += i
                file_uid = os.stat(full_file_dir)[stat.ST_UID]
                file_gid = os.stat(full_file_dir)[stat.ST_GID]
                file_mode = os.stat(full_file_dir)[stat.ST_MODE]
                file_mode_human = oct(file_mode)[-4:]   # human readable file mode
                full_backup_dir = backup_dir+"/"        # (last four characters are permissions)
                full_backup_dir += self.current_datetime
                full_backup_dir += i
                print "Setting up [UID=%d GID=%d MODE=%s (human: %s)] for %s..." % (file_uid,
                file_gid, file_mode, file_mode_human, i)
                os.chown(full_backup_dir, file_uid, file_gid)
                os.chmod(full_backup_dir, file_mode)

class FileOper:
    def file_relative_path(self, file_path, exclude):
        return file_path.replace(exclude, '')

    def file_dir(self, file_path):
        return os.path.dirname(file_path)

    def file_dir_tree(self, file_dir):
        tree_elements = file_dir.split('/')
        dir_tree = []
        if len(tree_elements) > 1:
            for i in range(1, len(tree_elements)):
                tmp = ''
                for j in range(1, i+1):
                    tmp += "/%s" % tree_elements[j]
                dir_tree.append(tmp)
            return dir_tree
        else:
            return 0

    def file_mtime(self, file_path):
        return os.path.getmtime(file_path)

def sigint(signum, frame):
    print "\nCaught SIGINT. Bye."
    sys.exit()

def main():
    signal.signal(signal.SIGINT, sigint)
    print "%s [PID=%d] is up and running..." % (sys.argv[0], os.getpid())
    while True:
        backup = FileBackup(db_filepath)
        print
        os.popen("sleep %s" % backup_delay)

if __name__ == "__main__":
    main()