#!/usr/bin/python

# phone_homer.py
#       --copyright--                   Copyright 2007 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       December 22, 2006       bar
#       August 26, 2007         bar     don't download old versions
#       October 29, 2007        bar     handle case of when local copy of the file doesn't match the MD5 or SHA1
#       November 18, 2007       bar     turn on doxygen
#       November 20, 2007       bar     comments
#       November 21, 2007       bar     another comment
#       November 27, 2007       bar     insert boilerplate copyright
#       May 17, 2008            bar     email adr
#       May 6, 2009             bar     show_info to phone_homer to url_getter
#       May 19, 2009            bar     use thang pointer rather than a special _stop flag for aborting
#       June 4, 2009            bar     able to run under python 2.6.2, with hashlib
#       September 14, 2010      bar     send the user name in the initial version.htm query
#       October 1, 2010         bar     print current version
#                                       strip leading V's in the version
#       December 9, 2010        bar     swap the logic behind download_count and files - only tell owner names of valid downloaded files, but all downloaded files are counted
#                                       fix a number of things with file comparisons, etc.
#       May 27, 2012            bar     doxygen namespace
#       August 12, 2012         bar     put name in thread
#       June 9, 2013            bar     don't download .exe files except to win32 and don't download .deb files except to linux systems
#       May 28, 2014            bar     put thread id in threads
#       July 7, 2018            bar     pyflakes
#       --eodstamps--
##      \file
#       \namespace              tzpython.phone_homer
#
#
#       Get a given version.htm type file and do what comes naturally.
#
#

import      os
import      re
import      sys
import      threading

try :
    import  hashlib
except      ImportError       :
    import  md5
    import  sha
    class   a_hashlib(object) :
        def md5(me, s  = "")  :
            return(md5.new(s))
        def sha1(me, s = "")  :
            return(sha.new(s))
        pass
    hashlib = a_hashlib()

import      getpass
import      urllib
import      urllib2
import      urlparse
import      url_getter

import      tzlib
import      replace_file



##                      Parse a version number in the description file on the server.
version_re              = re.compile(r"<VERSION\s*>([^<]+)</VERSION\s*>",                                                       re.IGNORECASE)

##                      Parse the file information from the server's description file (returns two groups: the target file's checksum and the target file's URL) (used by set_version_htm_sha1.py)
files_re                = re.compile(r"<FILE(?:\s+(?:MD5|SHA1)\s*=\s*[\"\']?([a-f0-9]{32,40})[\"\']?)?\s*>([^<]+)</FILE\s*>",   re.IGNORECASE)



##                      String to help using logic with MD5 value parsing.
md5_sub_re_str          = r'<FILE(?:\s+(?:MD5|SHA1)\s*=\s*[\"\']?[a-f0-9]{32}[\"\']?)?\s*>%s</FILE\s*>'

##                      String to help using logic with SHA1 value parsing.
sha1_sub_re_str         = r'<FILE(?:\s+(?:MD5|SHA1)\s*=\s*[\"\']?[a-f0-9]{40}[\"\']?)?\s*>%s</FILE\s*>'

##                      Looser MD5 recognition regx string.
md5_sub_str             = '<FILE MD5 ="%s">%s</FILE>'

##                      Looser SH1 recognition regx string.
sha1_sub_str            = '<FILE SHA1="%s">%s</FILE>'


class   a_phone_homer_exception(Exception) :
        """ Named exception. """
        pass


def     clean_version(vs) :
        return(vs.lower().lstrip("v ").strip())



def check_data(fd, cs, fn, pstr = None) :
    cs  = cs.lower()

    if  len(cs) == hashlib.md5().digest_size * 2 :
        if  hashlib.md5(fd).hexdigest().lower()  == cs :

            return(True)

        if  pstr != None :
            print "%smd5  mismatch %s %s %s" % ( pstr, hashlib.md5(fd).hexdigest().lower(),  cs, fn )
            pass
        pass

    if  len(cs) == hashlib.sha1().digest_size * 2 :
        if  hashlib.sha1(fd).hexdigest().lower() == cs :

            return(True)

        if  pstr != None :
            print "%ssha1 mismatch %s %s %s" % ( pstr, hashlib.sha1(fd).hexdigest().lower(), cs, fn )
        pass

    return(False)




class   a_phone_homer(threading.Thread) :
    """ Class to create an object that looks on a server for a file description file and downloads any new file(s) described there. """


    def __init__(me, thang, version_htm_url, current_version, dir_to_stash_new_version_files = ".", timeout = None, add_version_to_file_name = False, show_info = False) :
        """ Constructor. """

        threading.Thread.__init__(me, name = __file__)

        dir_to_stash_new_version_files  = dir_to_stash_new_version_files or "."

        me.thang            = thang or me
        me.version_htm_url  = version_htm_url
        me.current_version  = clean_version(current_version)
        me.to_dir           = dir_to_stash_new_version_files
        me.timeout          = timeout
        me.add_version      = add_version_to_file_name
        me.show_info        = show_info
        me.tid              = None

        me.lock             = threading.RLock()

        me.setDaemon(True)                                  # so that we can kick out of the program while the thread is running

        if  not os.path.isdir(me.to_dir) :
            raise a_phone_homer_exception("Dir %s does not exist!" % me.to_dir)

        pass



    def run(me) :
        """ Owner object called start on us. Do the thread. """

        me.tid              = tzlib.get_tid()
        new_version         = ""
        files               = []
        download_count      = 0

        parts               = list(urlparse.urlsplit(me.version_htm_url))
        parts[3]            = parts[3] or "user=%s" % getpass.getuser()
        url                 = urlparse.urlunsplit(parts)

        vs                  = url_getter.url_open_read_with_timeout(url, show_info = me.show_info)
        if  me.show_info :
            print "vs", vs
        if  vs :
            g   = version_re.search(vs)
            if  g :
                new_version = clean_version(g.group(1))

                if  me.show_info :
                    print "new_version", new_version, "current version", me.current_version

                if  new_version <= me.current_version :                 # this version or old version on the server?
                    new_version  = ""                                   # tell caller that we found nothing to do
                else :
                    gfiles  = files_re.findall(vs)

                    if  me.show_info :
                        print "files to download", gfiles

                    for flsha1 in gfiles :
                        if  not me.thang :
                            break

                        cs      = flsha1[0]
                        if  (len(cs) == 0) or (len(cs) == hashlib.md5().digest_size * 2) or (len(cs) == hashlib.sha1().digest_size * 2) :

                            furl    = flsha1[1]

                            ( scheme, host, path, q, frag ) = urlparse.urlsplit(furl)

                            path    = urllib.url2pathname(path)
                            fn      = os.path.split(path)[1]
                            if  me.add_version :
                                ( nm, ext ) = os.path.splitext(fn)
                                nvs = new_version.replace('.', '_')
                                if  not (nm.endswith(new_version) or nm.endswith(nvs)) :
                                    fn      = nm + "_" + nvs + ext
                                pass
                            fn      = os.path.join(me.to_dir, fn)

                            ext     = os.path.splitext(fn)[1].lower()
                            if  (ext == '.exe') and (sys.platform.find('win32') < 0) :
                                fn  = ''
                            if  (ext == '.deb') and (sys.platform.find('linux') < 0) :      # !!!! should be Debian linux, and do Fedora .rpm, too
                                fn  = ''

                            if  fn and os.path.isfile(fn) :
                                fi  = open(fn, "rb")
                                fd  = fi.read()
                                fi.close()

                                ps  = (me.show_info and "local copy") or None
                                if  check_data(fd, cs, fn, ps) :
                                    files.append(fn)
                                    fn  = ""
                                pass

                            if  fn  :
                                url = urlparse.urljoin(me.version_htm_url, furl)
                                fd  = url_getter.url_open_read_with_timeout(url, me.timeout)
                                if  not me.thang :
                                    break

                                if  fd :
                                    download_count += 1
                                    ps          = None
                                    if  me.show_info :
                                        ps      = ""
                                    if  check_data(fd, cs, fn, ps) :
                                        tfn     = fn + ".tmp"
                                        if  os.path.isfile(tfn) :
                                            os.remove(tfn)
                                        fo      = open(tfn, "wb")               # !!!! could collide with another instance of ourself but they both should agree
                                        fo.write(fd)
                                        fo.close()
                                        replace_file.replace_file(fn, tfn, fn + ".bak")
                                        files.append(fn)
                                    pass
                                pass
                            pass
                        pass
                    pass
                pass
            pass


        t           = me.thang
        me.thang    = None
        if  t and t.phone_homer_done :
            t.phone_homer_done(new_version, files, download_count)              # call back to tell the caller what we got
        pass


    def phone_homer_done(me, new_version, files, download_count) :              # stub
        #   new_version     empty string or the version in the version file on the server
        #   files           array of correct files downloaded either this run or previously some time from the server
        #   download_count  count of files downloaded from the server this time, whether the files were ok or not
        pass


    def realm_user_password(me, realm, user, password) :
        """ In case the server is basic password protected, set the user name and password needed to access the server. """

        password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()

        # Add the username and password.
        # If we knew the realm, we could use it instead of ``None``.
        parts       = list(urlparse.urlsplit(me.version_htm_url))
        parts[2]    = parts[3]  = parts[4]  = ""
        burl        = urlparse.urlunsplit(parts)
        password_mgr.add_password(realm, burl, user, password)

        handler = urllib2.HTTPBasicAuthHandler(password_mgr)

        # create "opener" (OpenerDirector instance)
        opener  = urllib2.build_opener(handler)

        # Install the opener.
        # Now all calls to urllib2.urlopen use our opener.
        urllib2.install_opener(opener)


    def stop(me) :
        """ Try to stop us. Effect is not immediate. """

        me.thang    = None


    pass            # a_phone_homer



#
#
#
if  __name__ == '__main__' :

    import  time

    import  TZCommandLineAtFile


    program_name    = sys.argv.pop(0)

    TZCommandLineAtFile.expand_at_sign_command_line_files(sys.argv)

    url     = "http://www.tranzoa.net/~alex/phone_homer_version.htm"
    if  len(sys.argv) :
        url = sys.argv.pop(0)

    cver    = "1.00"
    if  len(sys.argv) :
        cver    = sys.argv.pop(0)

    to_dir  = None
    if  len(sys.argv) :
        to_dir  = sys.argv.pop(0)

    class   a_thang :
        def __init__(me) :
            """ Constructor. """
            pass

        def phone_homer_done(me, version, files, download_count) :
            me.phone_homer  = None
            print   "version", version, "files", files, "download_count", download_count

        pass


    me  = a_thang()

    me.phone_homer  = a_phone_homer(me, url, cver, to_dir)
    me.phone_homer.start()

    while   me.phone_homer :
        print ".",
        time.sleep(0.5)

    pass



##      Public things.
__ALL__ = [
                'a_phone_homer',
                'a_phone_homer_exception',

                'version_re',
                'files_re',

                'md5_sub_re_str',
                'sha1_sub_re_str',
                'md5_sub_str',
                'sha1_sub_str',
          ]



#
#
#
# eof
