'''
Name: srd_zip_fgdb.py

Overview: Provides class for zipping and unzipping FGDBs

Description:
    This class provides a set of methods to enable the zipping and
    unzipping of FGDBs. This is intended to facilitate the transfer
    of FGDB data. The following methods are provided:

    zipFGDB - will zip an FGDB to a zip archive

    zipAllFGDBs - zips all FGDBs contained in a directory. A
                  zip archive will be created for each FGDB found.

    unzipFGDB - extracts an FGDB from a zip archive.

    unzipAllFGDBs - unzips all zip archives containing an FGDB that
                    are located in a directory.

Examples:

        # All examples assume following import:
        import srd.srd_zip_fgdb as srd_zip_fgdb
         
        # Zip an FGDB to a specific subdirectory:
        # Resulting zip will have same name as the FGDB.
        
        zipDirPath = r'I:\AVIE_Acceptance\AVI_FMU\zip_files'
        zipObj = srd_zip_fgdb.SRD_Zip_FGDB()
        zipObj.zipFGDB(fgdbPath, zipDirPath)

        # Zip an FGDB to same directory as where the source
        # FGDB is located. Name of zip archive will be same
        # as FGDB with .zip extension:
        zipObj = srd_zip_fgdb.SRD_Zip_FGDB()
        zipObj.zipFGDB(fgdbPath)
        
        # Unzip FGDB to directory:
        # The tst.zip contains the zipped FGDB and the
        # destination directory is tmp. The resulting
        # FGDB name is same as original FGDB that was zipped.
        zipPathArg = r'D:\testData\fgdbZip\tst.zip'
        dirPathArg = r'D:\testData\fgdbZip\tmp'
        zipObj = srd_zip_fgdb.SRD_Zip_FGDB()
        zipObj.unzipFGDB(zipPathArg, dirPathArg)

        # Zip all FGDBs located in the dirPathArg directory
        # and place zip archives in zipPathArg
        dirPathArg = r'I:\AVIE_Acceptance\AVI_FMU'
        zipPathArg = r'I:\AVIE_Acceptance\AVI_FMU\zip_files'
        zipObj = srd_zip_fgdb.SRD_Zip_FGDB()
        zipObj.zipAllFDGBs(dirPathArg, zipPathArg)

        # Unzip all zip archives located in dirPathArg and
        # place the unzipped FGDBs in unzipPathArg. Overwrite
        # any existing FGDBs in this location.
        unzipPathArg = r'I:\AVIE_Acceptance\AVI_FMU\unzip_fgdb'
        dirPathArg = r'I:\AVIE_Acceptance\AVI_FMU\zip_files'
        overwriteFGDBArg = True
        zipObj = srd_zip_fgdb.SRD_Zip_FGDB()
        zipObj.unzipAllFDGBs(dirPathArg, unzipPathArg, overwriteFGDBArg)

        # Copy all FGDBs located in srcPathDir to the
        # destPathArg. If the FGDB already exists in the
        # destPathArg it will not be overwritten.
        srcPathArg = r'D:\testData\fgdbZip'
        destPathArg = r'D:\testData\fgdbZip\destDir'
        overwriteFGDBArg = False
        zipObj = srd_zip_fgdb.SRD_Zip_FGDB()
        zipObj.copyAllFDGBs(srcPathArg, destPathArg, overwriteFGDBArg)

Notes:

Author: Doug Crane
        July, 2012

Modifications:

'''

import os
import sys
import pdb
import zipfile
import time
import datetime
import arcpy

# General SRD utilities that may be needed.
from srd.srd_exception import *
import srd.srd_logging as srd_logging
import srd_misc

__all__ = ['SRD_Zip_FGDB']

# ====================================================================
class SRD_Zip_FGDB(object):

    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self):
        '''
        
        Parameters:
            
        Variables:

        Examples:

            
        '''

        self._logChn = srd_logging.SRD_Log()

        # A readme file will be added to zip file if following property set.
        self.readMePath = ''
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def isFGDB(self, fgdbPath):
        '''
        Returns True if determined to be FGDB, otherwise returns False

        Parameters:
            fdgbPath - path to FGDB to test.
        '''


        # Perform series of tests to determine if FGDB:
        
        if not fgdbPath.upper().endswith('.GDB'):
            return False
        
        desc = arcpy.Describe(fgdbPath)
        
        if desc.dataType.upper() <> 'WORKSPACE':
            return False
            
        if desc.workspaceType.upper() <> 'LOCALDATABASE':
            return False
            
        return True
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def zipAllFGDBs(self, fgdbPathList, zipPath):
        '''
        '''

        for fgdbPath in fgdbPathList:
            if not os.path.exists(fgdbPath):
                raise SRD_Exception('%s is not not found, cannot zip' % fgdbPath)
        
            # Only accept an FGDB for zipping
            if not self.isFGDB(fgdbPath):
                raise SRD_Exception('%s is not an FGDB, cannot zip' % fgdbPath)

        self._logChn.logMsg('Zipping FGDBs To: %s' % zipPath)
        
        openMode='w'
        # Open zipfile overwriting any existing file with same name.
        # To enable zipping of FGDBs over 2 gb the allowZip64 flag is used.
        zipObj = zipfile.ZipFile(zipPath, openMode,  allowZip64=True)

        # Use zipDirectory since it accomodates subdirectories. This is
        # not currently needed for FGDB but may change in future.
        excludeExtensionList = ('lock',)
        includeExtensionList = ()

        for fgdbPath in fgdbPathList:
            self._logChn.logMsg('Archiving: %s' % fgdbPath)
            self.zipDirectory(fgdbPath, os.path.basename(fgdbPath), zipObj, excludeExtensionList, includeExtensionList)

        # Option to include a readme file.
        if self.readMePath:
            zipObj.write(self.readMePath, '_ReadMe.txt', zipfile.ZIP_DEFLATED)
            
        zipObj.close()

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def zipFGDB(self, fgdbPath, zipPath='', appendMode=False):
        '''
        Zips the contents of an FGDB. If no zipPath is provided
        then the name of the zip file will be the same as the FGDB
        pointed to by fgdbPath. A .zip extension will be added
        to the end of the FGDB to create the zipPath.

        Parameters:
            fgdbPath - path to the FGDB to zip.
            
            zipPath - this is either the name of the path to the
                      zipfile to create or the directory where the
                      zipped FGDB will be located. It the zipPath is
                      a directory then it is assumed that you just want
                      the resulting zipped FGDB to be placed in this
                      directory. If it is not a directory then it assumes
                      that it is an actual path to a zip file and it will
                      be created according to its name. 
        '''

        if not os.path.exists(fgdbPath):
            raise SRD_Exception('%s is not not found, cannot zip' % fgdbPath)
        
        # Only accept an FGDB for zipping
        if not self.isFGDB(fgdbPath):
            raise SRD_Exception('%s is not an FGDB, cannot zip' % fgdbPath)

        # If no zip path is provided then a zipfile based on the name of
        # the FGDB is created. 
        if zipPath:
            # Use name of FGDB to create zipfile in directory
            # indicated by the zipPath parameter.
            if os.path.isdir(zipPath):
                fgdbName = os.path.basename(fgdbPath)
                zipPath = os.path.join(zipPath, '%s.zip' % fgdbName)
            else:
                # Ensure the .zip extension is included.
                if not zipPath.upper().endswith('.ZIP'):
                    zipPath = '%s.zip' % zipPath
        else:
            zipPath = '%s.zip' % fgdbPath

        self._logChn.logMsg('Zipping: %s to %s' % (fgdbPath, zipPath))
        
        # Open zipfile overwriting any existing file with same name.
        if appendMode:
            openMode='a'
        else:
            openMode='w'
        # Open zipfile overwriting any existing file with same name.
        # To enable zipping of FGDBs over 2 gb the allowZip64 flag is used.
        zipObj = zipfile.ZipFile(zipPath, openMode,  allowZip64=True)

        # Use zipDirectory since it accomodates subdirectories. This is
        # not currently needed for FGDB but may change in future.
        excludeExtensionList = ('lock',)
        includeExtensionList = ()

        self.zipDirectory(fgdbPath, os.path.basename(fgdbPath), zipObj, excludeExtensionList, includeExtensionList)

        # Option to include a readme file.
        if self.readMePath:
            zipObj.write(self.readMePath, '_ReadMe.txt', zipfile.ZIP_DEFLATED)
            
        zipObj.close()

        # Print out contents to validate ZIP.
##        file = zipfile.ZipFile(zipPath, "r", allowZip64=True)
##        for info in file.infolist():
##            print '%s %s %s %s ' % (info.filename, info.date_time, info.file_size, info.compress_size)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def zipAllFDGBs(self, dirPath, zipPath=''):
        '''
        Will zip all FGDBs contained within a directory and create a
        seperate zip archive for each FGDB.

        Parameters:
            dirPath - path where the FGDBs to zip are located.

            zipPath - path to where the zip archives will be created.
        '''

        if not os.path.isdir(dirPath):
            raise SRD_Exception('directory path: %s is not a directory, cannot zip' % dirPath)
            
        # Obtain list of all FGDBs in a directory.
        fgdbPathList = []
        for nameObj in os.listdir(dirPath):
            nameObjPath = os.path.join(dirPath, nameObj)
            if self.isFGDB(nameObjPath):
                fgdbPathList.append(nameObjPath)

        # Zip each FGDB
        if len(fgdbPathList) < 1:
            self._logChn.logMsg('*** No FGDBs found to zip')
        else:
            for fgdbPath in fgdbPathList:
                self.zipFGDB(fgdbPath, zipPath)
            
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def zipDirectory(self, dirPath, relativePath, zipObj, excludeExtensionList, includeExtensionList):
        '''
        Zips the contents of a directory into an existing zip object.
        The relativePath ensures that if the directory contains other
        subdirectories then the contents of zip file will also be relative
        allowing rebuilding of original structure when unzipped.

        Parameters:
            dirPath - path to the directory to add to zip file

            relativePath - the path to the root directory object from which
                           the zip process was started. In our case it will
                           be the base name of the FGDB.

            zibObj - zipfile object that has bee opened for writing.
            
        '''

        for dirObjName in os.listdir(dirPath):
            dirObjPath = os.path.join(dirPath, dirObjName)
            # If directory object is a directory then make recursive call.
            if os.path.isdir(dirObjPath):
                curRelativePath = os.path.join(relativePath, dirObjName)
                self.zipDirectory(dirObjPath, curRelativePath, zipObj, excludeExtensionList, includeExtensionList)
            else:

                # An inclusion or exclusion list can be associated with the zipping. These
                # list represents the file exensions that can be included or excluded for
                # backup. Only one of these lists can be active at a time with the exclusion
                # list taking precedence.
                if excludeExtensionList:
                    canZip = True
                    for ext in excludeExtensionList:
                        extMatch = '.%s' % ext.upper()
                        if dirObjName.upper().endswith(extMatch):
                            canZip = False
                else:
                    if includeExtensionList:
                        canZip = False
                        for ext in includeExtensionList:
                            extMatch = '.%s' % ext.upper()
                            if dirObjName.upper().endswith(extMatch):
                                canZip = True
                    else:
                        canZip = True
                        
                if canZip:
                    zipObj.write(dirObjPath, os.path.join(relativePath, dirObjName), zipfile.ZIP_DEFLATED)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def zippedFGDBName(self, zipPath):
        '''
        Returns the name of the FGDB represented by the zip file. It looks
        at the contents of the files contained in the archive and extracts
        the root directory for the first fileName which will be the FGDB that
        was originally zipped. If no FGDB could be determined then an empty
        string is returned.

        Parameters:
            zipPath - path to zip archive containing FGDB.
        '''

        fgdbName = ''

        # Ensure dealing with zip archive.
        if not os.path.exists(zipPath):
            raise SRD_Exception('ZIP File: %s does not exist' % zipPath)
        
        if not zipfile.is_zipfile(zipPath):
            raise SRD_Exception('File: %s is not a ZIP, cannot get FGDB Name' % zipPath)

        zipObj = zipfile.ZipFile(zipPath, 'r', allowZip64=True)
        for info in zipObj.infolist():
            if info.filename:
                fgdbName, fgdbFile= os.path.split(info.filename)
                break
        zipObj.close()
        
        return fgdbName
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def unzipFGDB(self, zipPath, dirPath='', overwriteFGDB=True):
        '''
        Unzips an FGDB that is contained in a zip archive. It is assumed
        that the zip archive only contains a single FGDB. The name
        of the FGDB to create is determined from the contents of the
        zip archive.

        Parameters:
            zipPath - path to zip archive containing the FGDB.

            dirPath - path to directory where FGDB will be unzipped.
                      (optional, if not passed then directory path
                       will be same as that of the directory that
                       zipPath is contained in)

            overwriteFGDB - if set to True then any existing FGDB
                            will be overwritten. If False and the
                            FGDB exists then a message is written
                            and method returns False.
                            (optional, default: True)
        '''

        # Ensure dealing with zip archive.
        if not os.path.exists(zipPath):
            raise SRD_Exception('ZIP File: %s does not exist' % zipPath)
        
        if not zipfile.is_zipfile(zipPath):
            raise SRD_Exception('File: %s is not a ZIP, cannot unzip' % zipPath)

        # If dirPath is passed then ensure it is directory.
        if dirPath:
            if not os.path.isdir(dirPath):
                raise SRD_Exception('Path: %s is not a directory, cannot unzip' % dirPath)
        else:
            # Default to same directory as the zip archive.
            dirPath = os.path.dirname(zipPath)
            
        fgdbName = self.zippedFGDBName(zipPath)
        fgdbPath = os.path.join(dirPath, fgdbName)

        # Check if the FGDB exists and if the overwriteFGDB is
        # true then delete otherwise throw exception.
        if arcpy.Exists(fgdbPath):
            if overwriteFGDB:
                arcpy.Delete_management(fgdbPath)
            else:
                self._logChn.logMsg('FGDB: %s already exists, cannot overwrite' % fgdbPath)
                return False

        self._logChn.logMsg('Unzipping: %s to %s' % (zipPath, fgdbPath))
        
        zipObj = zipfile.ZipFile(zipPath, 'r', allowZip64=True)

        # Unzip the contents of the zip file into the directory
        # that will represent the FGDB.
        for zipEntry in zipObj.namelist():
            
            # Check to see if the item was written to the zip file with an
            # archive name that includes a parent directory. If it does, create
            # the parent folder in the output workspace and then write the file,
            # otherwise, just write the file to the workspace.
            #
            if not zipEntry.endswith('/'): 
                root, name = os.path.split(zipEntry)
                directory = os.path.normpath(os.path.join(dirPath, root))
                    
                if not os.path.isdir(directory):
                    os.makedirs(directory)
                file(os.path.join(directory, name), 'wb').write(zipObj.read(zipEntry))

        zipObj.close()

        return True
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def unzipAllFDGBs(self, dirPath, unzipPath='', overwriteFGDB=True):
        '''
        Unzip all FGDBs contained in zip archives within a directory.

        Parameters:
            dirPath - path to directory containg the FGDB zip archives.

            unzipPath - path to where the FGDBs will be unzipped.
                        (optional, if not passed then directory path
                         will be same as that of the directory that
                         zipPath is contained in)

            overwriteFGDB - if set to True then any existing FGDB
                            will be overwritten. If False and the
                            FGDB exists then an exception is raised.
                            (optional, default: True)
            
        '''


        if not os.path.isdir(dirPath):
            raise SRD_Exception('directory path: %s is not a directory, cannot unzip' % dirPath)
            
        # Obtain list of all zip archives in directory.
        zipPathList = []
        for nameObj in os.listdir(dirPath):
            nameObjPath = os.path.join(dirPath, nameObj)
            if zipfile.is_zipfile(nameObjPath):
                zipPathList.append(nameObjPath)

        # Unzip each archive
        if len(zipPathList) < 1:
            self._logChn.logMsg('*** No zip archvies found to unzip')
        else:
            for zipPath in zipPathList:
                # Perform basic test to see if archive contains an FGDB.
                fgdbName = self.zippedFGDBName(zipPath)
                if fgdbName.upper().endswith('.GDB'):
                    self.unzipFGDB(zipPath, unzipPath, overwriteFGDB)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def copyAllFDGBs(self, srcDirPath, destDirPath, overwriteFGDB):
        '''
        Copies all FGDBs from one directory to the other.

        Parameters:
            srcDirPath - directory containing FGDBs to copy

            destDirPath - directory where FGDBs are to be copied to.

            overwriteFGDB - if set to True then any existing FGDB in the
                            destDirPath will be overwritten.
        '''

        if not os.path.isdir(srcDirPath):
            raise SRD_Exception('Directory path: %s is not a directory, cannot copy' % dirPath)

        if not os.path.isdir(destDirPath):
            raise SRD_Exception('Directory path: %s is not a directory, cannot copy' % dirPath)

        if srcDirPath.lower() == destDirPath.lower():
            raise SRD_Exception('Source and Destination directories cannot be same location')
        
        # Obtain list of all FGDBs in a directory.
        fgdbPathList = []
        for nameObj in os.listdir(srcDirPath):
            nameObjPath = os.path.join(srcDirPath, nameObj)
            if self.isFGDB(nameObjPath):
                fgdbPathList.append(nameObjPath)

        # Zip each FGDB
        if len(fgdbPathList) < 1:
            self._logChn.logMsg('*** No FGDBs found to zip')
        else:
            for fgdbPath in fgdbPathList:
                fgdbName = os.path.basename(fgdbPath)
                destFGDBPath = os.path.join(destDirPath, fgdbName)
                if overwriteFGDB:
                    copyFGDB = True
                    if arcpy.Exists(destFGDBPath):
                        self._logChn.logMsg('%s already exists, will overwrite' % destFGDBPath)
                        arcpy.Delete_management(destFGDBPath)
                else:
                    if arcpy.Exists(destFGDBPath):
                        self._logChn.logMsg('%s already exists, will not overwrite' % destFGDBPath)
                        copyFGDB = False
                    else:
                        copyFGDB = True

                if copyFGDB:
                    self._logChn.logMsg('Copying: %s to %s' % (fgdbPath, destFGDBPath))
                    arcpy.Copy_management(fgdbPath, destFGDBPath)
        
#====================================================================
if __name__ == '__main__':

    # Modify as required.
    arcpy.env.overwriteOutput = True
    # If using geographic features then may be best to modify these.
    arcpy.env.XYTolerance = .001
    arcpy.env.XYResolution = .0001
    arcpy.gp.logHistory = False
    
    try:

        ##============================================================
        ## YOUR TEST CODE HERE

        logChn = srd_logging.SRD_Log()

        myObj = SRD_Zip_FGDB()

##        fgdbPathArg = r'D:\testData\fgdbZip\A10.gdb'
##        zipPathArg = r'D:\testData\fgdbZip\tst'
##        zipPathArg = ''
##        zipPathArg = r'D:\testData\fgdbZip\zip_files'
##      
##        myObj.zipFGDB(fgdbPathArg, zipPathArg)

        zipPathArg = r'D:\testData\fgdbZip\tst.zip'
        dirPathArg = r'D:\testData\fgdbZip\tmp'
        dirPathArg = ''
        overwriteFGDBArg = True
##        myObj.unzipFGDB(zipPathArg, dirPathArg, overwriteFGDBArg)

        
##        fgdbPathArg = r'D:\testData\fgdbZip\A10.gdb'
##        print myObj.isFGDB(fgdbPathArg)

        dirPathArg = r'D:\testData\fgdbZip\unzip_fgdbs'
        zipPathArg = r'D:\testData\fgdbZip\zip_files'
##        myObj.zipAllFDGBs(dirPathArg, zipPathArg)

        unzipPathArg = r'D:\testData\fgdbZip\unzip_fgdbs'
        dirPathArg = r'D:\testData\fgdbZip\zip_files'
        overwriteFGDBArg = True
##        myObj.unzipAllFDGBs(dirPathArg, unzipPathArg, overwriteFGDBArg)

        srcPathArg = r'D:\testData\fgdbZip'
        destPathArg = r'D:\testData\fgdbZip\destDir'
        overwriteFGDBArg = False
        myObj.copyAllFDGBs(srcPathArg, destPathArg, overwriteFGDBArg)
        
        ##============================================================
        
    except SRD_Exception as e:
        logChn.logError(e)
        logChn.logError(srd_misc.exceptionDetails(sys.exc_info()[2]))
    except Exception as e:
        logChn.logError(e)
        logChn.logError(srd_misc.exceptionDetails(sys.exc_info()[2]))
    else:
        logChn.logMsg('\n*** %s completed normally\n' % sys.argv[0])
    finally:
        arcpy.ResetEnvironments()
