1    #!/usr/bin/env python
       2    #
       3    # 'homepage.py' = 'Organize http://www.lafn.org/~cymbala/index.html'
       4    #
       5    #  See also: http://packages.debian.org/unstable/web/sitecopy.html
       6    #
       7    #  Time-stamp: <2003-08-07 14:34:06 cymbala>
       8    #     Started:  2000-07-06
       9    #
      10    #  THIS SCRIPT MAKES SURE FILES ON INTERNET WEB SITE ARE SYNCHRONIZED
      11    #  WITH ORIGINAL FILES ON LAPTOP.
      12    #
      13    #  Scheduled upload:
      14    #    ~root    crontab: 58 3 * * * pon; sleep 15m; poff
      15    #    ~cymbala crontab: 59 3 * * * python ~/Db/Homepage/homepage.py
      16    # --------------------------------------------------------------------
      17    
      18    import os
      19    import cPickle
      20    import fileinput
      21    import ftplib
      22    import pre
      23    import sys
      24    import string
      25    import tempfile
      26    import time
      27    import types
      28    
      29    
      30    HOME_DIR = os.path.dirname(os.path.expanduser('~/'))
      31    WORK_DIR = os.path.dirname(os.path.join(HOME_DIR, 'Db/Homepage/'))
      32    HTML = 'html'
      33    XML = 'xml'
      34    
      35    
      36    # Pick debug level (does not need FTP connection), or "None".
      37    #           Another way to debug is to make MAX_BYTES very low.
      38    debug = 'p' # w/ paging
      39    debug = 't'
      40    debug = None
      41    
      42    # Display paths and filenames as they are modified.
      43    #                   Not related to plain 'debug'.
      44    debug_names = 't'
      45    debug_names = None
      46    
      47    
      48    # Pieces used twice or more in "information":
      49    PYLOG = 'homepage.log'
      50    PYNAME = 'homepage.py'
      51    PYDATA = os.path.join(WORK_DIR, 'homepage_py.tab')
      52    PICKLE_NAME = 'homepage.cpickle'
      53    
      54    
      55    info = {
      56        None: None,
      57        	# Comments OK after comma ...
      58    
      59        'DEBUG': debug,
      60        'DEBUG_NAMES': debug_names,
      61    
      62        'GOOGLE_URL': 'http://www.google.com/',
      63        	# groups.google.com has a radio-button to switch between the two.
      64    
      65        'PYLOG': PYLOG,
      66        'PYLOG_PATHFILE': os.path.join(WORK_DIR, PYLOG),
      67        	# ... Log file.
      68    
      69        'LS_SWITCHES': '--dereference ',
      70        	# ... note traililng space.
      71    
      72        'PICKLE_PATHFILE': os.path.join(WORK_DIR, PICKLE_NAME),
      73        	# Use a pickle to store data between executions.
      74    
      75        'PYDATA': PYDATA,
      76        'PYNAME': PYNAME,
      77        'PY_PATHFILE': os.path.join(WORK_DIR, PYNAME),
      78        'PYDATA_HTML': 'homeptab.' + HTML,
      79        'PYNAME_HTML': 'homep_py.' + HTML,
      80        'PYLOG_HTML': 'homep_lg.' + HTML,
      81        	# ... name of this script, and
      82        	# ... location and name of this script, and
      83        	# ... name of this script as an HTML file.
      84    
      85        'PY2HTML': '/usr/local/lib/site-python/lpy.py',
      86        'PY2HTML_OPTIONS': '-noindex',
      87        	# ... Use a Python script to convert any Python script to HTML.
      88    
      89        'SYS_PATH_APPEND': ['~/Db', '~/Db/Homepage'],
      90        	# Additional places for import to find stuff.
      91    
      92        'HOME_DIR': HOME_DIR,
      93        'WORK_DIR': WORK_DIR,
      94        	# Working and home directories.
      95    
      96        'HTML': HTML,
      97        'XML': XML,
      98        None: None
      99    }
     100    
     101    
     102    #  Dictionary that specifies which files from here are to be uploaded.
     103    #  Entries are in: "homepage_py.tab".
     104    #  NOTE: 'homepage.py.html' (not 'homepage.py') is uploaded due to PY2HTML.
     105    #
     106    info['Here2There'] = {}
     107    
     108    
     109    #
     110    class Homepage:
     111        """Upload files to Web site and modify on-the-fly."""
     112    
     113        def listl(self, mystring):
     114            listing = os.popen(string.join(
     115                ['ls -l', info['LS_SWITCHES'], mystring]))
     116            dir_lines = listing.readlines()
     117            listing.close()
     118            return dir_lines
     119        
     120        def listl_dict(self, listl_raw):
     121            """Parse output from "dir" (either local or remote "dir")."""
     122    
     123            # - listl_raw is raw output from LS command.
     124            # - dict is dictionary for storing final results.
     125            dict = {}
     126            for line in listl_raw:
     127                parts = string.split(line)
     128    
     129                # ['-rw-r--r--',
     130                #  '1', '7695', '1010', '3289',
     131                #  'Jul', '18', '02:27', 'homepage.py']
     132                #
     133                if len(parts) == 9:
     134                    #
     135                    # Ignore permissions and user/group info.
     136                    # Keep type.
     137                    parts[0:4] = [parts[0][0], '', '', '']
     138                    
     139                    # Record subdirectories separate from files.
     140                    #
     141                    # match_obj = self.re_Subdirectory.search(parts[0])
     142                    # if match_obj == None:
     143                    #
     144                    dict[parts[len(parts) -1]] = parts
     145                    pass
     146                pass
     147            # ---
     148            return dict
     149    
     150        def LogMsg(self, message):
     151            """Message handler (linefeed is appended by this attribute)."""
     152    
     153            # 1.
     154            # Write to file (assuming message does not have ending '\n'):
     155            # file.write(message + '\n')
     156            self.LOG.write(message + '\n')
     157            
     158            # 2.
     159            # Print to std-out (usually '*Python Output*' buffer):
     160            print message
     161    
     162            # ---
     163            pass
     164    
     165        def debug_names_section_footer(self):
     166            if info['DEBUG_NAMES']:
     167                msg = '_____ 999> '
     168                self.LogMsg(string.ljust(msg, 32))
     169                pass
     170            pass
     171    
     172        def Add2Here2There(self, first_arg, second_arg):
     173            """Add key/value pair to Here2There dictionary."""
     174    
     175            # If order of columns is reversed, just reverse this:
     176            info['Here2There'][first_arg] = second_arg
     177            pass
     178        
     179        def __init__(self, info):
     180            """What to do when class instantiated."""
     181    
     182            info['re'] = {}
     183            info['re']['comment'] = pre.compile('^[\s]*#')
     184            info['re']['blank_line'] = pre.compile('^[\s]*$')
     185            info['re']['number'] = pre.compile('^[0-9]+$')
     186            info['re']['html_ext'] = pre.compile('\.' + info['HTML'] + '$')
     187            info['re']['xml_ext'] = pre.compile('\.' + info['XML'] + '$')
     188            info['re']['placeholder'] = pre.compile('__([a-z]+(_[a-z]+)*)__',
     189                                                    pre.I)
     190    
     191            # Log file.
     192            info['PYLOG_PATHFILE_OLD'] = None
     193            if os.path.isfile(info['PYLOG_PATHFILE']):
     194                info['PYLOG_PATHFILE_OLD'] = info['PYLOG_PATHFILE'] + '.old'
     195                os.rename(info['PYLOG_PATHFILE'],
     196                          info['PYLOG_PATHFILE_OLD'])
     197                self.Add2Here2There(info['PYLOG_PATHFILE_OLD'],
     198                                    info['PYLOG_HTML'])
     199                pass
     200            #
     201            # OPEN LOG FILE.
     202            self.LOG = open(info['PYLOG_PATHFILE'], 'w')
     203    
     204            # Read program data from external file.
     205            if os.path.isfile(info['PYDATA']):
     206                for line in fileinput.input(info['PYDATA']):
     207                    if (not info['re']['comment'].match(line)) and \
                       (not info['re']['blank_line'].match(line)):
     209                        #
     210                        if line[-1] == '\n': line = line[:-1]
     211                        data = string.split(line, '\t')
     212    
     213                        if not data[0] in info.keys():
     214                            if len(data) == 2:
     215                                spam = data[1]
     216                                if info['re']['number'].match(spam):
     217                                    spam = int(spam)
     218                                    pass
     219                                info[data[0]] = spam
     220                                pass
     221                            elif len(data) == 3:
     222                                spam = data[2]
     223                                if info['re']['number'].match(spam):
     224                                    spam = int(spam)
     225                                    pass
     226                                info[data[0]] = {data[1]: spam}
     227                                pass
     228                            else:
     229                                raise 'Too many values: ' + line
     230                            pass
     231                        else:
     232                            # Repeating keys (2nd+ occurrences).
     233                            # Convert to a list:
     234                            if type(info[data[0]]) == types.StringType:
     235                                info[data[0]] = [info[data[0]]]
     236                                pass
     237    
     238                            if type(info[data[0]]) == types.DictType:
     239                                if len(data) == 3:
     240                                    spam = data[2]
     241                                    if info['re']['number'].match(spam):
     242                                        spam = int(spam)
     243                                        pass
     244                                    if (data[1] in info[data[0]].keys()) and \
                                       (info[data[0]][data[1]] == spam):
     246                                        raise 'Key already exists: ' + line
     247                                    else:
     248                                        info[data[0]][data[1]] = spam
     249                                        pass
     250                                    pass
     251                                else:
     252                                    raise 'Need exactly three parts: ' + line
     253                                pass
     254                            elif type(info[data[0]]) == types.ListType:
     255                                if len(data) == 2:
     256                                    spam = data[1]
     257                                    if info['re']['number'].match(spam):
     258                                        spam = int(spam)
     259                                        pass
     260                                    info[data[0]].append(spam)
     261                                    pass
     262                                else:
     263                                    raise 'Need exactly two parts: ' + line
     264                            else:
     265                                raise 'Unknown type for: ' + data[0]
     266                            pass
     267                        pass
     268                    pass
     269                pass
     270    
     271            # Absolute exceptions.
     272            #
     273            # 2001.12.04
     274            # info['MAX_BYTES_EXCEPTIONS'].append(info['PYNAME'])
     275            
     276            # Upload just one copy of this script.
     277            info['PYNAME_HTML_ABSOLUTE'] = 'http://' + \
                                        info['SYSTEM_NAME'] + \
                                        '/~cymbala/' + \
                                        info['PYNAME_HTML']
     281            if info['USER_NAME'] == 'cymbala':
     282                self.Add2Here2There(info['PYNAME'], info['PYNAME_HTML'])
     283                pass
     284            self.Add2Here2There(info['PYDATA'], info['PYDATA_HTML'])
     285    
     286            #
     287            self.month_day_year = time.strftime(
     288                "%B %d, %Y", time.localtime(time.time()))
     289    
     290            # Depth to recurse looking for subdirectories on site.
     291            info['DIR_SITE_recurse_depth'] = 5
     292    
     293            # Position of byte-count in "ls -l" output.
     294            info['DIR_byte_position'] = -5
     295    
     296            # Append paths to sys.
     297            for spam in info['SYS_PATH_APPEND']:
     298                sys.path.append(os.path.expanduser(spam))
     299                pass
     300    
     301            # Import user-defined stuff after sys-paths appended.
     302            import db_user_def
     303            self.db_user_def = db_user_def
     304    
     305            # Directory listing variables.
     306            self.DIR_ = ['DIR_SITE_past',
     307                         'DIR_LOCAL_past',
     308                         'DIR_SITE_current',
     309                         'DIR_LOCAL_current']
     310            self.DIR_.sort()
     311    
     312            # Keep track of subdirectories to update DIR_SITE_current later.
     313            info['DIR_SITE_received_upload'] = []
     314    
     315            pass
     316    
     317        def pickle(self, garnish):
     318            """Pickle to store info between executions."""
     319    
     320            self.LogMsg('Pickling: ' + garnish[0])
     321            # [0] is action (WRITE, READ, TRANSFER).
     322            # [1] is object to WRITE.
     323    
     324            if garnish[0] == 'WRITE':
     325                spam = open(info['PICKLE_PATHFILE'], 'w')
     326                p = cPickle.Pickler(spam)
     327                p.dump(garnish[1])
     328                spam.close()
     329                pass
     330            #
     331            elif garnish[0] == 'READ':
     332                spam = open(info['PICKLE_PATHFILE'], 'r')
     333                u = cPickle.Unpickler(spam)
     334                burp = u.load()
     335                spam.close()
     336                if info['DEBUG'] == 'p':
     337                    self.LogMsg(str(burp))
     338                    self.__call_paging_program__()
     339                    raise str(burp)
     340                else:
     341                    return burp
     342                #
     343            elif garnish[0] == 'TRANSFER':
     344                # Get from pre-existing pickle if parameters match.
     345                self.cucumber = self.pickle(['READ'])
     346                if (self.cucumber['USER_NAME'] == info['USER_NAME'] and
     347                    self.cucumber['SYSTEM_NAME'] == info['SYSTEM_NAME']):
     348                    pass
     349                else:
     350                    raise 'Conflicting USER_NAME and/or SYSTEM_NAME!'
     351    
     352                # Before!
     353                # 1 of 2: LOCAL.
     354                if not info.has_key('DIR_LOCAL_past'):
     355                    info['DIR_LOCAL_past'] = self.cucumber['DIR_LOCAL_current']
     356                    pass
     357                # 2 of 2: SITE.
     358                if not info.has_key('DIR_SITE_past'):
     359                    info['DIR_SITE_past'] = self.cucumber['DIR_SITE_current']
     360                    pass
     361                #
     362                # To prevent making a subdirectory on site that already
     363                # exists, create a tree with all branches.
     364                info['DIR_SITE_past_tree'] = self.branches(
     365                    info['DIR_SITE_past'].keys())
     366    
     367                # Increment iteration.
     368                info['ITERATION'] = self.cucumber['ITERATION'] + 1
     369                
     370                pass
     371            #
     372            else:
     373                raise 'Unrecognized garnish: ' + str(garnish)
     374    
     375            # ---
     376            self.LogMsg('  ...done.')
     377            pass
     378    
     379        def __call_paging_program__(self):
     380            self.LOG.close()
     381            os.system('less ' + info['PYLOG_PATHFILE'])
     382            pass
     383    
     384        def __call_post__(self):
     385            """What to do at end of __call__."""
     386    
     387            self.LogMsg('')        
     388    
     389            # Store pickle.
     390            self.pickle(['WRITE', info])
     391    
     392            # Print combinations of:
     393            #  - SITE/LOCAL
     394            #  - before/current
     395            # and number of items in each.
     396            #
     397            self.LogMsg('')
     398            for x in self.DIR_:
     399                y_keys = info[x].keys()
     400                y_keys.sort()
     401                for y in y_keys:
     402                    spam = x + ': ' + y + ': ' + str(len(info[x][y]))
     403                    self.LogMsg(spam)
     404                    pass
     405                self.LogMsg('--')
     406                pass
     407    
     408            self.LogMsg('')
     409            self.LogMsg("Summing bytes stored on internet...")
     410            info['BYTES_ON_INTERNET'] = self.reap_bytes(info['DIR_SITE_current'])
     411            self.LogMsg("Total bytes stored by your ISP: " +
     412                       str(info['BYTES_ON_INTERNET']))
     413            self.LogMsg('* * *')
     414            
     415            # --- THE END.
     416            # If executed within Emacs, less output goes to '*Python Output*'.
     417            self.__call_paging_program__()
     418    
     419            # ---
     420            pass
     421    
     422        def normalize_file_names(self):
     423            """Translate user-specified files into actually-existing files."""
     424    
     425            self.LogMsg('Normalizing file names...')
     426            self.LogMsg('')
     427            
     428            # (1 of 5): Expand '~' to home directory.
     429            for i in info['Here2There'].keys():
     430                if i[0] == '~':
     431                    new_i = os.path.expanduser(i)
     432                    if new_i in info['Here2There'].keys():
     433                        raise 'Key already exists: ' + new_i
     434                        pass
     435                    info['Here2There'][new_i] = info['Here2There'][i]
     436                    del info['Here2There'][i]
     437                    if info['DEBUG_NAMES']:
     438                        self.LogMsg(' here 111> ' + i + ' --> ' + new_i)
     439                        pass
     440                    pass
     441                pass
     442            self.debug_names_section_footer()
     443    
     444            # (2 of 5): Ensure path begins with "/"
     445            for i in info['Here2There'].keys():
     446                if not i[0] == '/':
     447                    new_i = os.path.join(info['WORK_DIR'], i)
     448                    info['Here2There'][new_i] = info['Here2There'][i]
     449                    del info['Here2There'][i]
     450                    if info['DEBUG_NAMES']:
     451                        msg = ' here 222> ' + i + ' --> '
     452                        self.LogMsg(string.ljust(msg, 25) + new_i)
     453                        pass
     454                    pass
     455                pass
     456            self.debug_names_section_footer()
     457    
     458            # (3 of 5): Convert directory name to list of HTML files.
     459            for i in info['Here2There'].keys():
     460                if string.strip(info['Here2There'][i]) == '.':
     461                    if os.path.isdir(i):
     462                        dict = self.listl_dict(self.listl(i))
     463                        if info['DEBUG_NAMES']:
     464                            msg = ' here 333> EXPANDING TO FILE LIST: ' + i
     465                            self.LogMsg('\n' + msg)
     466                        for j in dict.keys():
     467                            # What if two files in same place w/ .xml and .html?
     468                            #
     469                            match_obj_ht = info['re']['html_ext'].search(j)
     470                            match_obj_x = info['re']['xml_ext'].search(j)
     471                            here = os.path.join(i, j)
     472                            if match_obj_ht:
     473                                there = '='
     474                                pass
     475                            elif match_obj_x:
     476                                there = j[:-len(match_obj_x.group(0))+1] + \
                                        info['HTML']
     478                                pass
     479                            if match_obj_ht or match_obj_x:
     480                                info['Here2There'][here] = there
     481                                if info['DEBUG_NAMES']:
     482                                    msg = '      333> ' + here + ' == ' + there
     483                                    self.LogMsg(msg)
     484                                    pass
     485                                pass
     486                            pass
     487                        del info['Here2There'][i]
     488                        pass
     489                    else:
     490                        raise 'Not a directory: ' + i
     491                    pass
     492                pass
     493            self.debug_names_section_footer()
     494    
     495            # (4 of 5): Change '=' to filename.
     496            for i in info['Here2There'].keys():
     497                j = string.strip(info['Here2There'][i])
     498                if j == '=':
     499                    old_j = j
     500                    j = os.path.basename(i)
     501                    info['Here2There'][i] = j
     502                    if info['DEBUG_NAMES']:
     503                        msg = 'there 444> ' + old_j + ' --> ' + j
     504                        self.LogMsg(string.ljust(msg, 28) + ' '*5 + \
                                    'in: ' + os.path.dirname(i))
     506                        pass
     507                    pass
     508                pass
     509            self.debug_names_section_footer()
     510    
     511            # (5 of 5): Add path to there if missing
     512            for i in info['Here2There'].keys():
     513                j = info['Here2There'][i]
     514                if string.find(j, '/') == -1:
     515                    old_j = j
     516                    j = os.path.join(self.relative_path(i,
     517                                                        [info['WORK_DIR'],
     518                                                         info['HOME_DIR']]), j)
     519                    #
     520                    # if not j[0] == '.': j = os.path.join('.', j)
     521                    if j[:2] == './': j = j[2:]
     522                    info['Here2There'][i] = j
     523                    if info['DEBUG_NAMES']:
     524                        msg = 'there 555> ' + old_j + ' --> '
     525                        self.LogMsg(string.ljust(msg, 34) + j)
     526                        pass
     527                    pass
     528                pass
     529            self.debug_names_section_footer()
     530    
     531            # 888: Final display.
     532            if info['DEBUG_NAMES']:
     533                keys = info['Here2There'].keys()
     534                keys.sort()
     535                for i in keys:
     536                    msg = '888> ' + i
     537                    self.LogMsg(string.ljust(msg, 40) + \
                                ' --> ' + info['Here2There'][i])
     539                    pass
     540                pass
     541            self.debug_names_section_footer()
     542    
     543            # Create DIRS for SITE and LOCAL
     544            info['DIRS_LOCAL'] = []
     545            keys = info['Here2There'].keys()
     546            keys.sort()
     547            if info['DEBUG_NAMES']: self.LogMsg('')
     548            for i in keys:
     549                j = self.relative_path(i, [info['HOME_DIR']])
     550                if not j in info['DIRS_LOCAL']:
     551                    info['DIRS_LOCAL'].append(j)
     552                    if info['DEBUG_NAMES']:
     553                        msg = ' DIRS_LOCAL> ' + j
     554                        self.LogMsg(string.ljust(msg, 40) + ' ' + i)
     555                        pass
     556                pass
     557            self.debug_names_section_footer()
     558            #
     559            info['DIRS_SITE'] = []
     560            keys = info['Here2There'].keys()
     561            keys.sort()
     562            if info['DEBUG_NAMES']: self.LogMsg('')
     563            for i in keys:
     564                j = self.relative_path(info['Here2There'][i],
     565                                             [info['WORK_DIR'],
     566                                              info['HOME_DIR']])
     567                if not j in info['DIRS_SITE']:
     568                    info['DIRS_SITE'].append(j)
     569                    if info['DEBUG_NAMES']:
     570                        msg = ' DIRS_SITE> ' + j
     571                        self.LogMsg(string.ljust(msg, 40) + ' ' + i)
     572                        pass
     573                pass
     574            self.debug_names_section_footer()
     575            if info['DEBUG_NAMES']: self.LogMsg('')
     576            
     577            pass
     578    
     579        def __call__(self):
     580            """Default attribute when no attribute used after class instance."""
     581    
     582            # Prior to establishing connection.
     583            self.LogMsg('')
     584            self.LogMsg(self.month_day_year)
     585            self.LogMsg(info['PY_PATHFILE'])
     586            self.LogMsg('')
     587    
     588            # Convert user-specified file names into ones that actually-exist.
     589            self.normalize_file_names()
     590    
     591            # Prepare to connect
     592            self.session_prepare()
     593    
     594            # Make FTP connection.
     595            self.session_create()
     596    
     597            # Read existing pickle.
     598            self.pickle(['TRANSFER'])
     599    
     600            # Directory listings.
     601            self.session_listings()
     602    
     603            # Upload!
     604            self.upload()
     605    
     606            # Quit or close FTP connection.
     607            self.session_quit()
     608    
     609            # Last step of __call__.
     610            self.__call_post__()
     611    
     612            # ---
     613            pass
     614    
     615        def lookup_edb(self, dictionary):
     616            """Get info from tab-delimited files w/ EDB entry form."""
     617            
     618            # Where to look-up password:
     619            db = 'private'
     620            fmt = os.path.join(os.path.expanduser('~/Db/'), db + '.fmt')
     621            dat = os.path.join(os.path.expanduser('~/Db/'), db + '.dat')
     622    
     623            # Get names of fields in private.dat 
     624            private_field_names = self.db_user_def.edb_field_names(fmt)
     625            # ...gives:
     626            # {7: 'verify-date', 6: 'notes', 5: 'secret-stuff', 4: 'why',
     627            #  3: 'where', 2: 'when', 1: 'what', 0: 'who'}
     628    
     629            # Return string.
     630            return self.db_user_def.get_tab_data('secret-stuff',
     631                                            private_field_names,
     632                                            dictionary,
     633                                            dat)
     634    	
     635        def reap_bytes(self, DIR_dict):
     636            """Count number of bytes in DIR dictionary."""
     637    
     638            bytes = 0
     639            for i in DIR_dict.keys():
     640                for j in DIR_dict[i].keys():
     641                    n = int(DIR_dict[i][j][info['DIR_byte_position']])
     642                    bytes = bytes + n
     643                    pass
     644                pass
     645    
     646            # ---
     647            return bytes
     648    
     649        def session_prepare(self):
     650            """What to do before creating FTP session."""
     651    
     652            # Keep track of subdirectories on site in a sequences that can
     653            # be quickly examined.
     654            info['SITE_DIRS'] = []
     655    
     656            if (not os.path.isfile(info['PICKLE_PATHFILE']) and
     657                info['DEBUG']):
     658                raise 'Cannot execute in debug mode if pickle does not exist.'
     659                pass
     660    
     661            # Expressions.
     662            self.capsule_expressions()
     663    
     664            # XML substitutions.
     665            self.capsule_xml_subs()
     666            
     667            # Always upload index.
     668            pathfile = os.path.expanduser(info['INDEX_LOC'])
     669            self.LogMsg('Touching ' + pathfile + '...')
     670            self.LogMsg('')
     671            exec_string = 'touch ' + pathfile
     672            os.system(exec_string)
     673    
     674            # ---
     675            pass
     676    
     677        def session_create(self):
     678            # Look-up the password:
     679            site = info['SYSTEM_NAME']
     680            user = info['USER_NAME']
     681    
     682            # Secret stuff:
     683            passwords = {}
     684            passwords[(site, user)] = self.lookup_edb(
     685                {'where': site, 'who': user})
     686    
     687            # Make connection.
     688            if not info['DEBUG']:
     689                try:
     690                    self.session.getwelcome()
     691                except:
     692                    self.LogMsg('Opening session...')
     693                    self.LogMsg('  SITE: ' + site)
     694                    self.LogMsg('  USER: ' + user)
     695                    self.LogMsg('')
     696                    #
     697                    self.LogMsg('  ftplib.FTP(...)')
     698                    self.session = ftplib.FTP(site)
     699                    #
     700                    self.LogMsg('  self.session.login(...)')
     701                    self.session.login(user, passwords[(site, user)])
     702                    self.LogMsg('')
     703    
     704                    # If this is first iteration, pretend that current contents
     705                    # of Web site are same as past contents.
     706    
     707                    if not os.path.isfile(info['PICKLE_PATHFILE']):
     708                        # Setting ITERATION to -1 will cause directory listing to
     709                        # happen when FTP connection created (no debug mode).
     710                        info['ITERATION'] = -1
     711                        
     712                        # Get listings from Web site and local computer.
     713                        self.session_listings()
     714    
     715                        self.pickle(['WRITE', info])
     716                        self.LogMsg('New pickle started (first iteration).')
     717                        pass
     718                    
     719                    pass
     720                pass
     721            else:
     722                self.LogMsg('NOT OPENING an FTP session... DEBUG MODE!)\n ')
     723                pass
     724    
     725            # ---
     726            pass
     727    
     728        def relative_path(self, path, leaders_list):
     729            """Return relative path beginning w/ first subdirectory."""
     730    	mystring = os.path.dirname(path)
     731            
     732            # MUST look for longer paths before shorter ones.
     733            # Any string in leaders_list will potentially be removed.
     734            lengths = {}
     735            for i in leaders_list:
     736                lengths[len(i)] = i
     737                pass
     738            keys = lengths.keys()
     739            keys.reverse()
     740            for i in keys:
     741                j = lengths[i]
     742                if mystring[:len(j)] == j:
     743                    mystring = mystring[len(j):]
     744                    pass
     745                if mystring == '': mystring = './'
     746                pass
     747            if mystring[0] == '/': mystring = mystring[1:]
     748            if mystring[:2] == './': mystring = mystring[2:]
     749            if mystring == '': mystring = '.'
     750            return mystring
     751    
     752        def branches(self, mylist):
     753            """Return same list with branches expanded."""
     754    
     755            self.LogMsg('Expanding branches...')
     756    
     757            new_list = []
     758            for item in mylist:
     759                parts = string.split(item, '/')
     760                for i in range(len(parts)):
     761                    spam = ''
     762                    for j in range(i):
     763                        spam = os.path.join(spam, parts[j])
     764                        new_list.append(spam)
     765                        pass
     766                    pass
     767                pass
     768    
     769            # ---
     770            return new_list
     771    
     772        def mkdir_site(self, dirnms):
     773            """Make subdirectories on Web."""
     774    
     775            candidates = []
     776            for dirnm in dirnms:
     777                parts = string.split(dirnm, '/')
     778                candidate = ''
     779                for i in range(len(parts)):
     780                    candidate = os.path.join(candidate, parts[i])
     781                    candidates.append(candidate)
     782                    pass
     783                pass
     784            candidates.sort()
     785            for dirnm in candidates:
     786                if not dirnm in info['SITE_DIRS']:
     787                    self.LogMsg(" ...creating directory: " + dirnm)
     788                    self.session.mkd(dirnm)
     789                    # Execution halts if .mkd not successful.
     790                    info['SITE_DIRS'].append(dirnm)
     791                    pass
     792                pass
     793            # ---
     794            pass
     795        
     796        def listing_local(self):
     797            """Get listing from local computer (including random files)."""
     798    
     799            self.LogMsg('')
     800            self.LogMsg('Getting listing from local computer...')
     801            dict = {}
     802            dirnms = info['DIRS_LOCAL']
     803            dirnms.sort()
     804            for dirnm in dirnms:
     805                dir_j = os.path.join(info['HOME_DIR'], dirnm)
     806                self.LogMsg(' ... ' + dir_j)
     807                if os.path.isdir(dir_j):
     808                    dir_lines = self.listl(dir_j)
     809                    dir_lines_parsed = self.listl_dict(dir_lines)
     810                    dict[dirnm] = dir_lines_parsed
     811                    pass
     812                else:
     813                    raise 'Directory not found: ' + dir_j
     814                pass
     815    
     816            self.LogMsg('')
     817            return dict
     818    
     819        def listing_site(self, action, dirnm):
     820            """Get listing from Web."""
     821            
     822            if action == '__call__':
     823                dict = {}
     824    
     825                # Start with parent directory (ignore dirnm in __call__).
     826                path = '.'
     827                dict[path] = self.listing_site('__dir__', path)
     828    
     829                for n in range(info['DIR_SITE_recurse_depth']):
     830                    # 5: [0, 1, 2, 3, 4]
     831                    paths = info['SITE_DIRS']
     832                    paths.sort()
     833                    for path in paths:
     834                        parts = string.split(path, '/')
     835                        if len(parts) == n:
     836                            self.LogMsg('Getting listing from Web... ' + path)
     837                            #
     838                            # This next line may make additions to 'SITE_DIRS':
     839                            dict[path] = self.listing_site('__dir__', path)
     840                            pass
     841                        pass
     842                    pass
     843                # Only append '.' after recursing.
     844                info['SITE_DIRS'].append('.')
     845                info['SITE_DIRS'].sort()
     846    
     847                # Do not assume subdirectories exist on Web.
     848                # Create new subdirectories as needed.
     849                self.mkdir_site(info['DIRS_SITE'])
     850                
     851                pass
     852            #
     853            elif action == '__dir__':
     854                dir_lines = []
     855                # Output from .dir attribute:
     856                #     empty directory: []
     857                #   missing directory: ['ftpd: DIR: No such file or directory']
     858                # non-empty directory: ['total N', '-rw-r--r-- ...']
     859                #
     860                
     861                self.session.dir(dirnm, dir_lines.append)
     862                # ['total 18',
     863                # '-rw-r--r--  1 7695  1010  3289 Jul 18 02:27 homepage.py',
     864                # '-rw-r--r--  1 7695  1010  4209 Jul 18 03:11 index.html']
     865                #
     866                dir_lines_parsed = self.listl_dict(dir_lines)
     867                
     868                # This is crucial to the "for... range" loop in '__call__'.
     869                for key in dir_lines_parsed.keys():
     870                    mylist = dir_lines_parsed[key]
     871                    if mylist[0] == 'd':
     872                        spam = os.path.join(dirnm, mylist[-1])
     873                        if dirnm == '.':
     874                            # Do not include leading '.'
     875                            spam = mylist[-1]
     876                            pass
     877                        #
     878                        info['SITE_DIRS'].append(spam)
     879                        pass
     880                    pass
     881                return dir_lines_parsed
     882            #
     883            else:
     884                raise 'Action not recognized: ' + action
     885            #
     886            return dict
     887    
     888        def session_listings(self):
     889            """Current directory listings."""
     890    
     891            # 1 of 2: LOCAL.
     892            info['DIR_LOCAL_current'] = self.listing_local()
     893    
     894            # 2 of 2: SITE.
     895            #
     896            # Debug?
     897            if info['DEBUG']:
     898                # Make a copy of before!
     899                # This assumes nothing changed on SITE.
     900                # This also happened when ITERATION was 0.
     901                info['DIR_SITE_current'] = info['DIR_SITE_past']
     902                pass
     903            else:
     904                info['DIR_SITE_current'] = self.listing_site('__call__', None)
     905                #
     906                self.LogMsg("Summing bytes stored on internet...")
     907                bytes = self.reap_bytes(info['DIR_SITE_current'])
     908                info['BYTES_ON_INTERNET'] = bytes
     909                self.LogMsg("Total bytes stored by your ISP: " +
     910                           str(info['BYTES_ON_INTERNET']))
     911                pass
     912            
     913            # ---
     914            pass
     915    
     916        def stor2DIR_SITEkey(self, my_arg):
     917            """Convert from STOR format to DIR_.key() format."""
     918            
     919            # Rules:
     920            # If just '.' keep it ('.' returns '.').
     921            # If more than just '.' then remove it ('./A/B/c.txt' returns 'A/B').
     922            my_arg = my_arg[0:string.rfind(my_arg, '/')]
     923            if (not my_arg == '.' and
     924                my_arg[0] == '.'
     925                ):
     926                my_arg = my_arg[2:]
     927                pass
     928    
     929            # -----
     930            return my_arg
     931    
     932        def upload(self):
     933            """Start of upload process."""
     934            # 1. file in Here2There not on site, upload it (by file).
     935            # 2. file modified on local computer, upload it.
     936    
     937            self.LogMsg('\n\n')
     938            self.upload_H2T()
     939            self.LogMsg('\n\n')
     940            self.upload_MOD()
     941    
     942            # ---
     943            pass
     944    
     945        def get_listl_info(self, dir_listing_name, dirnm, filename):
     946            """Return None or values from directory listing"""
     947            return_object = None
     948            if info[dir_listing_name].has_key(dirnm):
     949                if info[dir_listing_name][dirnm].has_key(filename):
     950                    return_object = info[dir_listing_name][dirnm][filename]
     951                    pass
     952                pass
     953            return return_object
     954        
     955        def upload_H2T(self):
     956            # 1.
     957            # Upload if in Here2There but not in DIR_SITE_current.
     958            #
     959            info['H2T_on_the_fly'] = []
     960    
     961            keys = info['Here2There'].keys()
     962            keys.sort()
     963            dirs_seen = []
     964            for key in keys:
     965                key_local = key
     966                key_site = info['Here2There'][key]
     967                key_local_nopath = string.split(key_local, '/')[-1]
     968                key_site_nopath = string.split(key_site, '/')[-1]
     969    
     970                # Trigger is "upload_switch".
     971                upload_switch = None
     972    
     973                dir_site = self.relative_path(key_site,
     974                                              [info['WORK_DIR'],
     975                                               info['HOME_DIR']])
     976                if not dir_site in dirs_seen:
     977                    self.LogMsg('')
     978                    self.LogMsg('CHECKING FOR Here2There NOT ON INTERNET: ' + \
                                dir_site)
     980                    dirs_seen.append(dir_site)
     981                    pass
     982    
     983                if not info['DIR_SITE_current'].has_key(
     984                    dir_site):
     985                    upload_switch = 't'
     986                    if info['DEBUG']:
     987                        spam = dir_site + ' not in DIR_SITE_current!'
     988                        self.LogMsg(' =-=-=-> ' + spam)
     989                        pass
     990                    pass
     991                elif not info['DIR_SITE_current'][dir_site].has_key(
     992                    key_site_nopath):
     993                    upload_switch = 't'
     994                    if info['DEBUG']:
     995                        spam = key_site_nopath + ' not in DIR_SITE_current['
     996                        spam = spam + dir_site + '] !'
     997                        self.LogMsg(' =-=-=-> ' + spam)
     998                        pass
     999                    pass
    1000    
    1001                if upload_switch:
    1002                    # Sometimes the file doesn't exist, for example
    1003                    # the HTML version of a Python script only exists
    1004                    # just prior to upload.
    1005                    info_now = None
    1006                    dir_local = self.relative_path(key_local,
    1007                                                   [info['HOME_DIR']])
    1008    
    1009                    spam_keys = info['DIR_LOCAL_current'][dir_local].keys()
    1010                    if key_local_nopath in spam_keys:
    1011                        info_now = info['DIR_LOCAL_current'][dir_local][
    1012                            key_local_nopath]
    1013                        pass
    1014    
    1015                    info['H2T_on_the_fly'].append(key_local)
    1016                    self.on_the_fly(key_local, key_site, info_now)
    1017                    pass
    1018                pass
    1019            pass
    1020    
    1021        def upload_MOD(self):
    1022            # 2.
    1023            # Look for files that have been modified.
    1024            #
    1025            Here2There_keys = info['Here2There'].keys()
    1026            dirnms = info['DIRS_LOCAL']
    1027            dirnms.sort()
    1028            for dir_local in dirnms:
    1029                self.LogMsg('')
    1030                self.LogMsg("CHECKING FOR MODIFIED FILES ON LOCAL: " + \
                            dir_local)
    1032    
    1033                filenames = info['DIR_LOCAL_current'][dir_local].keys()
    1034                filenames.sort()
    1035                for filename in filenames:
    1036    
    1037                    # This part is sensitive; must be a string that will
    1038                    # match exactly... no spurious '././' in pathfilename!
    1039                    #
    1040                    pathfilename = os.path.join(info['HOME_DIR'],
    1041                                                dir_local,
    1042                                                filename)
    1043                    if pathfilename in Here2There_keys:
    1044                        key_local = pathfilename
    1045                        key_site = info['Here2There'][pathfilename]
    1046                        
    1047                        # Trigger is "upload_switch".
    1048                        upload_switch = None
    1049    
    1050                        # Now and then.
    1051                        info_past = self.get_listl_info('DIR_LOCAL_past',
    1052                                                        dir_local, filename)
    1053                        info_now = self.get_listl_info('DIR_LOCAL_current',
    1054                                                       dir_local, filename)
    1055    
    1056                        # Upload modified files.
    1057                        if (info_past == None or
    1058                            info_now == None):
    1059                            upload_switch = 't'
    1060                            pass
    1061                        elif not info_past == info_now:
    1062                            upload_switch = 't'
    1063                            pass
    1064    
    1065                        if upload_switch:
    1066                            if not key_local in info['H2T_on_the_fly']:
    1067                                self.LogMsg('')
    1068                                self.LogMsg(' PAST> ' + str(info_past))
    1069                                self.LogMsg('  NOW> ' + str(info_now))
    1070                                self.on_the_fly(key_local, key_site, info_now)
    1071                                pass
    1072                            pass
    1073                        pass
    1074                    pass
    1075                pass
    1076            # ---
    1077            pass
    1078    
    1079        def on_the_fly(self, key_local, key_site, info_now):
    1080            """Very long attribute; calls modifications attribute."""
    1081    
    1082            self.LogMsg(' on_the_fly: ' + key_local + ' --> ' + key_site)
    1083    
    1084            if info['DEBUG']:
    1085                if info_now == None:
    1086                    self.LogMsg(' !! File has not been created yet. !! ')
    1087                    pass
    1088                else:
    1089                    self.LogMsg(str(info_now))
    1090                    pass
    1091                pass
    1092    
    1093            if info_now == None:
    1094                bytes = -1
    1095                pass
    1096            else:
    1097                bytes = int(info_now[info['DIR_byte_position']])
    1098                pass
    1099            #
    1100            # Put '#' in front of 'and' to exclude this script.
    1101            spam = string.split(key_local, '/')[-1]
    1102            if (bytes > info['MAX_BYTES']
    1103                and not spam in info['MAX_BYTES_EXCEPTIONS']
    1104                ):
    1105                self.LogMsg(' '*9 + 'NOT uploaded (too large): bytes: ' +
    1106                            str(bytes))
    1107                return
    1108    
    1109            # In addition, to "upload_switch", variable "new_file" is a trigger
    1110            # also. Value of "file2up" can be changed by "new_file".
    1111            #
    1112            new_file = None
    1113            del_f2u = None
    1114            file2up = key_local
    1115    
    1116            filename = os.path.basename(file2up)
    1117            # -----------------------------------------------------
    1118            # Last-second changes to file before upload.
    1119            #
    1120            # 1.    EXTENSION = .py
    1121            #
    1122            if file2up[-3:] == '.py':
    1123                file2up, new_file, del_f2u = self.modifications(key_site,
    1124                                                                file2up,
    1125                                                                'py')
    1126                pass
    1127            #
    1128            # 2.    EXTENSION = .xml        
    1129            #    Files ending in '.xml' (instead of using
    1130            #    jade to create .html) that are not stored as raw XML.
    1131            #
    1132            elif (file2up[-4:] == '.xml' and \
                  not key_site[-4:] == '.xml'):
    1134                file2up, new_file, del_f2u = self.modifications(key_site,
    1135                                                                file2up,
    1136                                                                'xml')
    1137                pass
    1138            #
    1139            # 3.    EXTENSION = .xml
    1140            #    Files ending in '.xml' that will be stored as raw XML.
    1141            elif (file2up[-4:] == '.xml' and \
                  key_site[-4:] == '.xml'):
    1143                file2up = file2up
    1144                pass
    1145            #
    1146            # 4.    EXTENSION = '.el' or '.txt' or ... to HTML.
    1147            #   Files ending in '.el' or '.txt' or ... to HTML.
    1148            #
    1149            elif ('.' + string.split(file2up, '.')[-1]
    1150                in info['EXTENSIONS_TXT2HTML']
    1151                ) and key_site[-5:] == '.' + info['HTML']:
    1152                #
    1153                file2up, new_file, del_f2u = self.modifications(key_site,
    1154                                                                file2up,
    1155                                                               'txt2html')
    1156                pass
    1157            #
    1158            # 999.  RANDOM FILES
    1159            #     Examples are ".emacs" and ".signature" and...
    1160            #
    1161            elif (filename[0] == '.' or
    1162                  filename == string.split(info['PYLOG_PATHFILE_OLD'], '/')[-1]
    1163                  ):
    1164                file2up, new_file, del_f2u = self.modifications(key_site,
    1165                                                                file2up,
    1166                                                                'dot_file')
    1167                pass
    1168            # --------------------------------------------
    1169    
    1170            # Variable 'new_file' contains modifications.
    1171            if not new_file == None:
    1172                # Usually 'file2up' is in ~/Db/Homepage and is just
    1173                # file name (no path); in this case it is a temporary file
    1174                # and has a path; see up_file too.
    1175                #
    1176                file2up = tempfile.mktemp()
    1177                spam = open(file2up, 'w')
    1178                spam.write(new_file)
    1179                spam.close()
    1180                new_file = None
    1181                pass
    1182    
    1183            # Finally!  Time to upload...
    1184            self.up_file(file2up, key_site)
    1185    
    1186            if del_f2u:
    1187                exec_string = string.join(["rm -f", file2up])
    1188                self.LogMsg(" ... " + exec_string)
    1189                #
    1190                if not info['DEBUG']:
    1191                    os.system(exec_string)
    1192                    pass
    1193                pass
    1194    
    1195            new_file = None
    1196            del_f2u = None
    1197            file2up = None
    1198            
    1199            # ---
    1200            pass
    1201    
    1202        def modifications(self, key_site, file2up, type):
    1203            """Make changes."""
    1204    
    1205            new_file = None
    1206            del_f2u = None  # Flag for deleting uploaded file.
    1207            
    1208            if type == 'py':
    1209                exec_string = string.join(['python',
    1210                                           info['PY2HTML']
    1211                                           ])
    1212                if info['PY2HTML_OPTIONS']:
    1213                    exec_string = string.join([exec_string,
    1214                                               info['PY2HTML_OPTIONS']
    1215                                               ])
    1216                    pass
    1217    
    1218                exec_string = string.join([exec_string,
    1219                                          file2up])
    1220                self.LogMsg(" ..... " + exec_string)
    1221                #
    1222                if not info['DEBUG']:
    1223                    os.system(exec_string)
    1224                    pass
    1225                file2up = file2up + '.' + info['HTML']
    1226                del_f2u = 't'
    1227                #
    1228                pass
    1229            #
    1230            elif type == 'xml':
    1231                # Convert general entities to numeric entities.
    1232                try:
    1233                    type(num_ents)
    1234                except:
    1235                    import num_ents
    1236                    myNumCharRef = num_ents.NumCharRef()
    1237                    pass
    1238                #
    1239                if not info['DEBUG']:
    1240                    new_file = myNumCharRef(
    1241                        file2up,
    1242                        '/usr/share/sgml/html/dtd/xml/1.0/xhtml.soc')
    1243    
    1244                    # XML substitutions.
    1245                    match_obj = info['re']['placeholder'].search(new_file)
    1246                    while not match_obj == None:
    1247                        new_file = pre.sub(match_obj.group(0),
    1248                                          self.xml_subs[match_obj.group(1)],
    1249                                          new_file)
    1250                        #
    1251                        # Do not need to check for unsuccessful substitutions
    1252                        # (KeyError will happen if placeholder is invalid).
    1253                        match_obj = info['re']['placeholder'].search(new_file)
    1254                        pass
    1255                    
    1256                    # .xml --> .html changes (regarding XHTML).
    1257                    # Delete <?xml?> declaration.
    1258                    if key_site[-5:] == '.' + info['HTML']:
    1259                        #
    1260                        # If DOCTYPE is html, just remove <?xml?>, otherwise
    1261                        # wrap whole thing in <pre></pre>.
    1262                        #
    1263                        spam = string.find(new_file, '<!DOCTYPE')
    1264                        if spam > -1:
    1265                            spam = string.split(new_file[spam:])[1]
    1266                            if string.upper(spam) == 'HTML':
    1267                                #
    1268                                # Can comments appear before doctype?
    1269                                spam = '<!-- XML declaration removed by \n'
    1270                                spam = spam + ' '*17 + info['WEB_HOME']
    1271                                spam = spam + info['PYNAME_HTML']
    1272                                spam = spam + ' -->\n'
    1273                                spam = ''
    1274                                new_file = pre.sub('^ *<\?xml[^>]*>',
    1275                                                  spam, new_file)
    1276                                pass
    1277                            pass
    1278                        else:
    1279                            new_file = self.read_file_return_with_pre(file2up)
    1280                            pass
    1281                        pass
    1282                    pass
    1283                pass
    1284            #
    1285            elif (type == 'txt2html' or
    1286                  type == 'dot_file' or
    1287                  type == 'pre_file'):
    1288                new_file = self.read_file_return_with_pre(file2up)
    1289                pass
    1290            #
    1291            else:
    1292                raise 'Unknown type: ' + type
    1293    
    1294            # If "new_file" is not empty, it contains contents to be
    1295            # uploaded.  Contents will need to be saved to a file, thus
    1296            # "file2up" will be changed by calling attribute.
    1297    
    1298            # -----
    1299            return file2up, new_file, del_f2u
    1300    
    1301        def up_file(self, file2up, key_site):
    1302            """Upload file."""
    1303    
    1304            stor_command = string.join(['STOR', key_site])
    1305            
    1306            if not info['DEBUG']:
    1307                #
    1308                if (not os.path.isfile(file2up)):
    1309                    self.LogMsg('     ' + 'FILE NOT FOUND.')
    1310                    pass
    1311                else:
    1312                    # UPLOAD FILE:
    1313                    local_object = open(file2up, 'r')
    1314                    self.LogMsg('     ' + stor_command)
    1315                    self.session.storbinary(stor_command,
    1316                                            local_object,
    1317                                            8*1024)
    1318                    local_object.close()
    1319                    #
    1320                    # Track subdirectories to update DIR_SITE_current later.
    1321                    key = self.stor2DIR_SITEkey(key_site)
    1322                    if not key in info['DIR_SITE_received_upload']:
    1323                        info['DIR_SITE_received_upload'].append(key)
    1324                        pass
    1325                    #
    1326                    pass
    1327                pass
    1328            else:
    1329                self.LogMsg(' '*30 + ' ... DEBUG, not uploaded.')
    1330                pass
    1331    
    1332            # ---
    1333            pass
    1334    
    1335        def session_quit(self):
    1336            """Quit FTP session."""
    1337    
    1338            self.LogMsg('')
    1339            self.LogMsg("PREPARING TO CLOSE CONNECTION...")
    1340    
    1341            # Get fresh listings from certain subdirectories.
    1342            for path in info['DIR_SITE_received_upload']:
    1343                info['DIR_SITE_current'][path] = self.listing_site(
    1344                    '__dir__', path)
    1345                pass
    1346    
    1347            if not info['DEBUG']:
    1348                self.session.quit()
    1349                pass
    1350    
    1351            # ---
    1352            pass
    1353    
    1354        def read_file_return_with_pre(self, file_name):
    1355            """Surround plain text file with HTML markup."""
    1356    
    1357            # Simply return "...<pre>", contents of file, and "</pre>...".
    1358            spam = open(file_name, 'r')
    1359            contents = spam.read()
    1360            spam.close()
    1361    
    1362            # Not so simple.  Do minimal conversion of character references.
    1363            #     (sect. 2.4 in www.w3.org/TR/1998/REC-xml-19980210).
    1364            # Order counts!
    1365            contents = string.replace(contents, '&', '&#38;')
    1366            contents = string.replace(contents, '<', '&#60;')
    1367            contents = string.replace(contents, '>', '&#62;')
    1368    
    1369            # -----
    1370            return self.html_pre_open + contents + self.html_pre_close
    1371    
    1372        def capsule_expressions(self):
    1373            """Regular expressions."""
    1374            
    1375            self.re_Subdirectory = pre.compile('^d')
    1376            self.re_XHTmlFile = pre.compile('(x|ht)ml$', pre.I)
    1377    
    1378            # ---
    1379            pass
    1380    
    1381        def capsule_xml_subs_final(self):
    1382            """SEE: capsule_xml_subs()."""
    1383            for xs0 in self.xml_subs.keys():
    1384                for xs1 in self.xml_subs.keys():
    1385                    self.xml_subs[xs0] = pre.sub('__' + xs1 + '__',
    1386                                                self.xml_subs[xs1],
    1387                                                self.xml_subs[xs0])
    1388                    pass
    1389                pass
    1390            pass
    1391    
    1392        # NOTE:
    1393        # This def is LAST because something about """quoting""" causes
    1394        # Emacs indent-for-tab-command to break.
    1395        def capsule_xml_subs(self):
    1396            """General substitutions that probably belong outside this script."""
    1397    
    1398            # All xml_subs keys are lowercase, except "Google". May like to
    1399            # check for (and reject) all uppercase keys.
    1400            # -----
    1401    
    1402            # <PRE>
    1403            self.html_pre_open = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
            <html>
              <head>
                <title></title>
              </head>
              <body>
                <pre text="""
     + '__body_attrs__' + ">"
    1410    
    1411            # </PRE>
    1412            self.html_pre_close = """\
                </pre>
              </body>
            </html>"""
    
    1416    
    1417    
    1418            # To trigger substitution, put key in .xml surrounded by
    1419            # double underscores.  WARNING: key will be used as part of a
    1420            # pattern used in pre.sub().
    1421    
    1422            self.xml_subs = {}
    1423    
    1424            # Assume ``[A-Z_]+'' keys from info are xml_subs:
    1425            for key in info.keys():
    1426                if not key == None:
    1427                    if pre.compile('^[A-Z_]+$').match(key):
    1428                        if type(info[key]) == types.StringType:
    1429                            self.xml_subs[key] = info[key]
    1430                            pass
    1431                        pass
    1432                    pass
    1433                pass
    1434    
    1435            # Replace __placeholder__ with HTML names of script and script's log.
    1436            self.xml_subs['pyname'] = info['PYNAME']
    1437            self.xml_subs['pydata_html'] = info['PYDATA_HTML']
    1438            self.xml_subs['pylog_html'] = info['PYLOG_HTML']
    1439            self.xml_subs['pyname_html'] = info['PYNAME_HTML']
    1440            #
    1441            # Example from index.xml ...
    1442            #   <a href="__PYLOG_HTML__" shape="rect">log file</a>).
    1443            
    1444                
    1445            # Replace __placeholder__ with non-breaking spaces.
    1446            self.xml_subs['nbsps'] = '&nbsp; &nbsp; &nbsp;'
    1447                    
    1448            # Replace __placeholder__ with my email address, as an anchor start.
    1449            self.xml_subs['mailto_me_href'] = info['MAILTO_ME_HREF']
    1450            self.xml_subs['mailto_me_href_addr'] = info['MAILTO_ME_HREF_ADDR']
    1451                
    1452            # Replace __placeholder__ with my GnuPG user ID.
    1453            self.xml_subs['pgp_userid'] = info['PGP_USERID']
    1454    
    1455            # Replace __placeholder__ with current date.
    1456            self.xml_subs['month_day_year'] = self.month_day_year
    1457    
    1458            # Replace __placeholder__ with home page anchor.
    1459            spam = """<a href="index.html" shape="rect"><b>Home</b></a>"""
    1460            self.xml_subs['home'] = spam
    1461    
    1462            # Replace __placeholder__ with body-tag attributes.
    1463            #      Other colors: <body bgcolor=#faebd7><!-- antique white -->
    1464            #
    1465            # This goes within 'text="__body_attrs__" !
    1466            spam = '#000000" bgcolor="' + '__BGCOLOR__' + """\"
            link="#0000FF" vlink="#800080" alink="#FF0000"""
    1468            self.xml_subs['body_attrs'] = spam
    1469    
    1470            # Replace __placeholder__ with Google anchor.
    1471            # Had to escape "#" inside string.
    1472            spam =        "<a href=\"" + info['GOOGLE_URL']
    1473            spam = spam + """\" shape="rect">"""
    1474            self.xml_subs['Google'] = spam + """
            	  <font color="#0039b6">G</font>
            	  <font color="#c41200">o</font>
            	  <font color="#f3c518">o</font>
            	  <font color="#0039b6">g</font>
            	  <font color="#30a72f">l</font>
            	  <font color="#c41200">e</font>
            	</a>"""
    
    1482           
    1483    
    1484            # Replace __placeholder__ with div.banner of hyperlinks.
    1485            #   Note this in <body><head>:
    1486            #   <link href="generic.css" type="text/css" rel="stylesheet"/>
    1487            #
    1488            # DO NOT USE "&mdash;" (Netscape 4.08 does not convert to "--").
    1489            #
    1490            self.xml_subs['div_banner_hyperlinks'] = """<div class="banner">
                  __home__  &nbsp;-&nbsp;
                  __Google__ &nbsp;-&nbsp;
                  <a href="hyplheal.html" shape="rect"><small>Health</small></a>
                    &nbsp;-&nbsp;
                  <a href="hyplpers.html" shape="rect"><small>Personal</small></a>
                    &nbsp;-&nbsp;
                  <a href="hyplpoli.html" shape="rect"><small>Political</small></a>
                    &nbsp;-&nbsp;
                  <a href="hypltech.html" shape="rect"><small>Technical</small></a>
                  </div>
                    """
    
    1502    
    1503            # Replace __placeholder__ with div.banner of just two hyperlinks.
    1504            #
    1505            self.xml_subs['div_banner_hyperlinks_brief'] = """<div class="banner">
                  __home__  &nbsp;-&nbsp;
                  __Google__
                  </div>
                    """
    
    1510    
    1511            # Replace __placeholder__ with a paragraph with links to this
    1512            # script and it's control and log files.
    1513            #
    1514            self.xml_subs['script_description_p'] = """
                <p align="center">
                  <small>Updated with
            	<a href="__PYNAME_HTML_ABSOLUTE__" shape="rect">a Python script</a>
            	(on __month_day_year__).
            
            	<br clear="none"/>
            	Latest <a href="homeptab.html" shape="rect">tab-delimited
            	  control file</a>.
            
            	<br clear="none"/>
            	Previous <a href="homep_lg.html" shape="rect">log file</a>.
            
                  </small></p>
            """
    
    1529    
    1530            # Replace __placeholder__ with div.navfooter stuff.
    1531            anchor = '	<a href="' + info['MAILTO_ME_HREF']
    1532            anchor = anchor + '" shape="rect">' + info['MAILTO_ME_NAME'] + '</a>'
    1533            #
    1534            self.xml_subs['div_navfooter'] = """    <hr class="hide"/>
                <div class="NAVFOOTER">
                  <!--
                  To show your readers that you have taken the care to
                  create an interoperable Web page, you may display this
                  icon on any page that validates.
                  -->
                  <a
            	 href="http://validator.w3.org/check/referer"
            	 shape="rect">
            	<img src="http://www.w3.org/Icons/valid-xhtml10"
            	     alt="Valid XHTML 1.0!" height="31"
            	     width="88" align="right" border="0" />
                  </a>
                  <a
            	 href="http://validator.w3.org/check/referer"
            	 shape="rect">Click here (or click icon) to validate markup.
                     </a>
                     
                  <br clear="none" />
                  http://&#160;
                  <a href="http://"""
     + info['SYSTEM_NAME'] + """\"
    	 shape="rect"> """ + info['SYSTEM_NAME'] + """ </a>
                  &#160;/&#160;
                  <a href=\""""
     + info['INDEX_WEB'] + """\"
    	 shape="rect">  ~""" + info['USER_NAME'] + """ </a>
                  <address>
                     Email: """
     + anchor + """
                  </address>
                    </div>
                    """
    
    1565    
    1566            # This must be after final addition to xml_subs (does single
    1567            # substitution inside dictionary, no recursion).
    1568            self.capsule_xml_subs_final()
    1569            # ---
    1570            pass
    1571    
    1572        # ===
    1573        pass
    1574    
    1575    # MAIN:
    1576    if not __name__ == 'the_name_I_cannot_specify':
    1577        maine = Homepage(info)
    1578        maine()
    1579        pass
    1580    
    1581    # -----------------------------------------------------------------------------
    1582    # Example of generating "booksred.html" using jade. ('\x22' = '"').
    1583    #
    1584    #  jade -d /usr/lib/dsssl/stylesheets/docbook/html/docbook.dsl
    1585    #       -t sgml -o booksred.html
    1586    #       /usr/lib/sgml/declaration/xml.decl booksred.xml
    1587    #  perl -- booksred.pl booksred.html >| foo.html
    1588    #  mv foo.html booksred.html
    1589    #
    1590    #    (Perl is simply to put "<BR\n>" before item number:
    1591    #        $/ = "<DT\n>";
    1592    #        while (<>) {
    1593    #            if (/^[0-9]+[.]/) { print "<BR\n>$_"; }
    1594    #            else { print "$_"; }
    1595    #        }
    1596    #    )
    1597    # -----------------------------------------------------------------------------
    1598    # NOTE: 'jade_run' is a shell script that can reduce uploads; it
    1599    #       sets-aside existing .html files, executes jade, and finally
    1600    #       it replaces new .html with old .html if running jade didn't
    1601    #       change a given file; that replacement undoes jade's modification
    1602    #       of date/time for a given file; this script won't upload a file
    1603    #       if date/time did not change between executions.
    1604    #
    1605    # -----------------------------------------------------------------------------
    1606    # To-do: - This uploads, but doesn't download files only on web site that
    1607    #          may have been uploaded without storing copy on laptop.
    1608    #        - log file created by this script is a kind of 'site map'.
    1609    #        - guarantee existence of public-domain links (e.g., t4700ct.html).
    1610    #        - if C-c C-c in Emacs, suppress set of output from *Python Output*.
    1611    #        - use "--full-time" with ``ls'' on local computer.
    1612    #        - make sure os.path.expanduser always used.
    1613    #        - this auto-uploads:
    1614    #            A['exim_d.html'] = 'exim_d.html'
    1615    #          but will this auto-upload to subdirectory Image?
    161