#
# mkbackup/ntbackup.py
#
# ntbackup frontend
#

import os, sys, subprocess, time
from datetime import datetime, timedelta

from archiver import *

import cron


# ===========================================================================
class NtBackup(Archiver):
    # http://support.microsoft.com/kb/814583
    # http://support.microsoft.com/default.aspx?scid=kb;en-us;233427
    #
    # Normal - selected files, marking their archive attributes
    # Copy - selected files without marking their archive attributes. This is good for making tape copies that do not interfere with archive backups, since it does not set the archive attribute.
    # Incremental - selected files, marking their archive attributes, but only backs up the ones that have changed since the last backup.
    # Differential - selected files, NOT marking their archive attributes, but only backs up the ones that have changed since the last backup.
    # Daily - only backs up files that have changed that day, does not mark their archive attributes.
    #
    # ntbackup stuff
    #   http://www.fishbrains.com/2007/11/12/utilizing-the-built-in-windows-backup-ntbackupexe-for-windows/
    #   http://episteme.arstechnica.com/eve/forums/a/tpc/f/12009443/m/165002540831
    
    name='ntbackup'
    exe='ntbackup.exe'
    ev_logtype='Application'
    ev_source='NTBackup'
    
    boolean={ True: 'yes', False:'no'}
    
    types=dict(normal='normal', full='normal', incremental='incremental', inc='incremental', differential='differential', diff='differential', copy='copy', daily='daily')

    # -----------------------------------------------------------------------
    def __init__(self):
        Archiver.__init__(self)
        
        self.selection=None
        self.verify=None
        self.restricted=None
        self.logdir=None
        self.program_exe=None
        self.type=None
        self.target=None
        
        
    # -----------------------------------------------------------------------
    def load(self, job, log):
        
        errors, warnings, extra={}, {}, ''

        #
        # check job config
        #
        
        self.selection=job.get('selection', None)
        if not self.selection:
            errors['selection']='option mandatory'
        elif self.selection.startswith('@'):
            if not os.path.isfile(self.selection[1:]):
                errors['selection']='file not found: %s' % (self.selection[1:],)
        elif os.path.isfile(self.selection) and self.selection[-4].lower()=='.bks':
            error['selection']='looks like a ".bks" file and need to be prefixed by a @'
        elif not os.path.isdir(self.selection) and not os.path.isfile(self.selection):
            errors['selection']='file or directory not found'

        self.verify=boolean.get(job.get('verify', 'no').lower(), None)
        if self.verify==None:
            errors['verify']='boolean must have value in (on, yes, true, 1, off, no, false and 0)'

        self.restricted=boolean.get(job.get('restricted', 'no').lower(), None)
        if self.restricted==None:
            errors['restricted']='boolean must have value in (on, yes, true, 1, off, no, false and 0)'

        self.logdir=job.get('logdir', None)
        if not self.logdir:
            self.logdir=os.path.join(os.environ.get('USERPROFILE'), 'Local Settings\Application Data\Microsoft\Windows NT\NTBackup\data')
            # dont check if logdir exist here because ntbackup.exe will create it
        elif not os.path.isdir(logdir):
            # I want to be sure the user know what he is doing
            errors['logdir']='directory not found'
    
        self.program_exe=job.get('ntbackup', None)
        if not self.program_exe:
            self.program_exe=self.name
            # TODO: search in %PATH%
        elif os.path.basename(self.program_exe)!=self.program_exe and not os.path.isfile(self.program_exe):
            errors['ntbackup']='file not found'

        self.destination=job.get('destination', None)
        if not self.destination:
            errors['destination']='option mandatory'
        else:
            self.destination=self.destination.replace('\n','')
            try:
                self.destination=Destinations(self.destination, self)
            except (DestinationSyntaxError, cron.CronException), e:
                errors['destination']='syntax error: %s' % (str(e), )

        if not errors:
            # C:\WINDOWS\system32\ntbackup.exe backup "@m:\asx\src\magik\job1.bks" /a /d "Set created 14/11/2009 at 2:29" /v:no /r:no /rs:no /hc:off /m normal /j "backup" /l:s /f "s:\Backup.bkf"
            self.type, self.target=self.destination.match(datetime.now())
    
            if self.types!=None and self.types!='none':
                target_dir=os.path.dirname(self.target)
                
                self.cmd_args=[ self.program_exe, 'backup', self.selection, '/J', job['name'], '/M', self.type, '/F', self.target, '/rs:no', ]
                if job.get('description', None): args.extend(['/d', job.get('description')])
                self.cmd_args.append('/v:'+self.boolean[self.verify])
                self.cmd_args.append('/r:'+self.boolean[self.restricted])
                self.cmd_args.append('/l:s') # logging [s]ummary
                # '/hc:off'
            
                cmdline=' '.join(self.cmd_args)
                log.info('cmdline=%s', cmdline)
                extra+='cmdline=%s\n' % '  '.join(self.cmd_args)
    
        return errors, warnings, extra
        
    # -----------------------------------------------------------------------
    def run(self, command, job, log):

        import windows

        target_dir=os.path.dirname(self.target)
        
        start=int(time.time())
        # FIXME: shoud the self.cmd_args be enccoded ? 
        process=subprocess.Popen(self.cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        std_out, std_err=process.communicate()
        end=int(time.time())
        
        logfile, last=None, 0
        for filename in os.listdir(self.logdir):
            full_filename=os.path.join(self.logdir, filename)
            current=os.stat(full_filename).st_mtime
            if last<current:
                logfile, last=full_filename, current
                
        ev_status, ev_out=windows.ReadEvLog(NtBackup.ev_logtype, NtBackup.ev_source, start)
        
        status=u''
        status+='status=%s\r\n' % ev_status
#        status+='version=%s\r\n' % __version__
        status+='exit_code=%r\r\n' % process.returncode
        status+='start=%s\r\n' % time.ctime(start)
        status+='end=%s\r\n' % time.ctime(end)
        status+='logfile=%s\r\n' % logfile
        
        status+='selection=%s\r\n' % self.selection
        status+='target=%s\r\n' % self.target
        try:
            stat=os.stat(target)
        except Exception, e:
            log.error('checking target file: %s', e)
            status+='target_stat_err=%s\r\n' % e
        else:
            status+='target_size=%d\r\n' % stat.st_size
            status+='target_mtime_epoch=%d\r\n' % stat.st_mtime
            status+='target_mtime=%s\r\n' % time.ctime(stat.st_mtime)
        
        try:
            import win32file
            free_bytes, total_bytes, total_free_bytes=win32file.GetDiskFreeSpaceEx(target_dir)
        except Exception, e:
            log.error('checking target directory: %s', e)
            status+='target_free_space=%s\r\n' % e
        else:
            status+='target_free_space=%d\r\n' % total_free_bytes

        status+='start_epoch=%d\r\n' % start
        status+='end_epoch=%d\r\n' % end
        status+='cmdline=%s\r\n' % ' '.join(self.cmd_args)

        for line in status.split('\n'):
            if line:
                log.info('    %s', line)
        
        #
        # dir of target directory
        #
        try: 
            dir_out=u'  epoch   |          time          |      size     |    filename\r\n'
            for filename in os.listdir(target_dir): # if target_dir is unicode, return os.listdir return unicode
                full_filename=os.path.join(target_dir, filename)
                stat=os.stat(full_filename)
                if os.path.isdir(full_filename):
                    size='<DIR>'
                else:
                    size='%15d' % stat.st_size 
                dir_out+='%d %s %15s %s\r\n' % (stat.st_mtime, time.ctime(stat.st_mtime), size, filename)
        except Exception, e:
            log.error('listing target directory: %s', e)
                

        attachements=[ # the type, subtype and coding is not used
                       ('log.txt', logfile, None, 'text', 'plain', 'utf-16'),
                       ('dir.txt', None, dir_out.encode('utf-8'), 'text', 'plain', 'utf-8'),
                       ('evlog.txt', None, ev_out.encode('utf-8'), 'text', 'plain', 'utf-8'),
                       ('config.ini', job['config'], None, 'text', 'plain', job['config_encoding']),
                       ('status.txt', None, status.encode('utf-8'), 'text', 'plain', 'utf-8'),
                    ]
        if self.selection.startswith('@'):
            attachements.append(('selection.bks', self.selection[1:], None, 'text', 'plain', 'utf-16'))
        if std_out:
            attachements.append(('stdout.txt', None, std_out, 'text', 'plain', 'cp850'))
        if std_err:
            attachements.append(('stderr.txt', None, std_err, 'text', 'plain', 'cp850'))
        
        msg_body=job['msg_header']+'\n'+status
        subject='MKBACKUP BACKUP %s %s' % (ev_status, job['name'])
        sendmail(job['sender'], job['recipients'], subject, msg_body, attachements, job['smtp_host'], job['smtp_port'])

