#!/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 " __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()