#! /usr/bin/env python
# -*- coding: ISO-8859-1 -*-
#
# UnderCoverServer.py
# (C)2003 Romain GUY <romain.guy@jext.org>
# Licensed under LGPL
#
# 08/26/2003 - 21:09:48

"""
UnderCoverServer is an automated backup utility target to be used with a NetBIOS/Samba file server.
[more to come]
"""

##########################################################################################
# MODULE INFORMATIONS
##########################################################################################

__author__    = "Romain GUY (romain.guy@jext.org)"
__version__   = "0.1"
__copyright__ = "Copyright (C)2003 Romain GUY"
__license__   = "LGPL"

##########################################################################################
# IMPORTS
##########################################################################################

import logging
import os
import os.path
import SimpleXMLRPCServer
import threading
import time

##########################################################################################
# GLOBALS
##########################################################################################

try:
  True, False
except:
  True, False = 1, 0

##########################################################################################
# CLASSES
##########################################################################################

class UnderCoverServer(object):
  """
    The UnderCover server. [more to come]
  """

  ########################################################################################
  # CONSTRUCTORS
  ########################################################################################
  
  def __init__(self, deleteRemote = False, serverPort = 4620, logFileTTL = 15, chunkSize = 32768):
    """
      When creating a new UnderCover server, you can specify the server port (which is
      defaulted to 4620), the log files time to live (which default value is 15 minutes),
      and the chunk size for the file copy operation (by default, data are copied by
      chunks of 32 768 bytes).
    """
    self._deleteRemote          = deleteRemote
    self._serverPort            = serverPort
    self._logFileTTL            = logFileTTL
    self._chunkSize             = chunkSize
    self._handler               = None
    self._logger, self._handler = self._setupLogger()

    self._stopEvent     = threading.Event()
    self._sleepPeriod   = 3.0
    self._isServerAlive = True

    self._xmlrpcServerThread = threading.Thread(target = self._setupXMLRPCServer)
    self._xmlrpcServerThread.setDaemon(True)
    self._xmlrpcServerThread.start()

    self._serverThread = threading.Thread(target = self._serverLoop)
    self._serverThread.start()
  # end of __init__

  ########################################################################################
  # PRIVATE METHODS
  ########################################################################################

  def _serverLoop(self):
    """
      Keeps the server alive.
    """
    while self._isServerAlive:
      self._stopEvent.wait(self._sleepPeriod)

  def _setupXMLRPCServer(self):
    """
      Creates a new XML-RPC server for this current UnderCoverInstance.
    """
    try:
      self._xmlrpcServer = SimpleXMLRPCServer.SimpleXMLRPCServer(("localhost", self._serverPort))
      self._xmlrpcServer.register_instance(self)
      self._xmlrpcServer.serve_forever()
    except Exception, e:
      self._logger.error("Could not start the XML-RPC server !")
      self._logger.error("Exception: %s !" % (e,))

  def _setupLogger(self):
    """
      Installs the logger. The log file is overwritten if _logFileTTL minutes have passed
      since the last logging operation.
    """
    if self._handler: self._handler.close()

    logFile = "undercoverserver.log"
    if os.path.exists(logFile):
      if ((time.time() - os.path.getctime(logFile)) / 60) > self._logFileTTL:
        os.remove(logFile)

    logger    = logging.getLogger('UnderCover')
    handler   = logging.FileHandler(logFile)
    formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')

    handler.setFormatter(formatter)
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)

    return logger, handler
  # end of _setupLogger
  
  def _addFile(self, args, dirname, names):
    """
      Adds every file from a given directory into a list. The list is referenced by the
      second item in the "args" tuple. The first item of this tuple references the common
      root which is substracted from the paths.
    """
    for name in names: args[1].append(os.path.join(dirname[len(args[0]) + 1:], name))
  # end of _addFile

  def _difference(self, list1, list2):
    """
      Returns a list which is the difference between list1 and list2. That is to say, it
      returns list1 - list2.
    """
    return [element for element in list1 if not element in list2]
  # end of _difference

  def _copy(self, source, dest):
    """
      Copy the source file/directory to the dest file/directory.
    """
  
    if os.path.isdir(source):
      if not os.path.exists(dest): os.mkdir(dest)
    else:
      sourceFile = open(source, "rb")
      destFile   = open(dest,   "wb")
      data       = sourceFile.read(self._chunkSize)

      while data != "":
        destFile.write(data)
        data = sourceFile.read(self._chunkSize)

      destFile.close()
      sourceFile.close()
  # end of _copy

  def _removeDir(self, args, dirname, names):
    """
      Removes all the files from a given directory and its subdirectories.
    """
    for name in names:
      file = os.path.join(dirname, name)
      if os.path.isfile(file):
        os.remove(file)
  # end of _removeDir

  ########################################################################################
  # BACKUP PUBLIC METHODS
  ########################################################################################
  
  def backup(self, remotePath, localPaths):
    """
      Performs the backup of a list of directories.
    """
    retValue     = True

    self._logger.info("** STARTING BACKUP **")
    for path in localPaths:
      retValue = retValue and self.backupDirectory(remotePath, path, self._deleteRemote)

    return retValue
  # end of backup

  def backupDirectory(self, remotePath, localPath, deleteRemote = False):
    """
      Performs the backup of the local path on the remote server into the specified remote
      directory. If deleteRemote is set to true, files which exists on the remote server
      and that are no longer available locally will be removed.
    """
    self._logger.info("Root backup:\t%s" % (remotePath,))
    self._logger.info("Backup dir:\t%s" % (localPath,))

    try:
      _localContent  = []
      _remoteContent = []
    
      if remotePath[-1] == '\\': remotePath = remotePath[:-1]
      if localPath[-1]  == '\\': localPath  = localPath[:-1]
  
      if not os.path.exists(localPath):
        self._logger.error("Unknown dir:\t\t%s" % (localPath))
        return
  
      remotePath = os.path.join(remotePath, os.path.split(localPath)[1])
      if not os.path.exists(remotePath): os.mkdir(remotePath)
  
      os.path.walk(localPath,  self._addFile, (localPath,  _localContent ))
      os.path.walk(remotePath, self._addFile, (remotePath, _remoteContent))
    
      newFiles        = self._difference(_localContent, _remoteContent)
      remoteOnlyFiles = self._difference(_remoteContent, _localContent)
      existingFiles   = self._difference(_localContent, newFiles)
      modifiedFiles   = filter(lambda file: os.path.getmtime(os.path.join(localPath,  file)) > \
                                            os.path.getmtime(os.path.join(remotePath, file)), \
                               existingFiles)
    
      del existingFiles
  
      for file in newFiles:
        path = os.path.join(localPath, file)
        self._logger.info("New %s:\t\t%s" % (os.path.isdir(path) and "dir" or "file", file))
        self._copy(path, os.path.join(remotePath, file))
  
      for file in modifiedFiles:
        path = os.path.join(localPath, file)
        self._logger.info("Modified %s:\t%s" % (os.path.isdir(path) and "dir" or "file", file))
        self._copy(path, os.path.join(remotePath, file))
  
      if deleteRemote:
        for file in remoteOnlyFiles:
          file = os.path.join(remotePath, file)
          if os.path.exists(file):
            if os.path.isdir(file):
              self._logger.info("Removed dir:\t%s" % (file,))
              os.path.walk(file, self._removeDir, None)
              os.removedirs(file)
            else:
              self._logger.info("Removed file:\t%s" % (file,))
              os.remove(file)

    except Exception, e:
      self._logger.error("Could not backup directory %s" % (localPath,))
      self._logger.error("Exception: %s" % (e,))
      return False

    return True
  # end of backupDirectory

  ########################################################################################
  # SERVER PUBLIC METHODS
  ########################################################################################

  def stop(self):
    """
      Kills the XML-RPC server.
    """
    self._isServerAlive = False

    return True
  # end of stop

##########################################################################################
# TEST/DEBUG PROGRAM
##########################################################################################

if __name__ == '__main__':
  print "You should not be reading this. Please run undercover.py instead."

  _remotePath = "\\\\server\\systemwide\\archives"
  _localPaths = ["D:\\Mes Documents\\Personnel\\Textes\\Divers"]

  uc = UnderCoverServer()
  uc.backup(_remotePath, _localPaths)

# end of file