Home | History | Annotate | Download | only in scripts
      1 #! /usr/bin/env python

      2 
      3 """Mirror a remote ftp subtree into a local directory tree.
      4 
      5 usage: ftpmirror [-v] [-q] [-i] [-m] [-n] [-r] [-s pat]
      6                  [-l username [-p passwd [-a account]]]
      7                  hostname[:port] [remotedir [localdir]]
      8 -v: verbose
      9 -q: quiet
     10 -i: interactive mode
     11 -m: macintosh server (NCSA telnet 2.4) (implies -n -s '*.o')
     12 -n: don't log in
     13 -r: remove local files/directories no longer pertinent
     14 -l username [-p passwd [-a account]]: login info (default .netrc or anonymous)
     15 -s pat: skip files matching pattern
     16 hostname: remote host w/ optional port separated by ':'
     17 remotedir: remote directory (default initial)
     18 localdir: local directory (default current)
     19 """
     20 
     21 import os
     22 import sys
     23 import time
     24 import getopt
     25 import ftplib
     26 import netrc
     27 from fnmatch import fnmatch
     28 
     29 # Print usage message and exit

     30 def usage(*args):
     31     sys.stdout = sys.stderr
     32     for msg in args: print msg
     33     print __doc__
     34     sys.exit(2)
     35 
     36 verbose = 1 # 0 for -q, 2 for -v

     37 interactive = 0
     38 mac = 0
     39 rmok = 0
     40 nologin = 0
     41 skippats = ['.', '..', '.mirrorinfo']
     42 
     43 # Main program: parse command line and start processing

     44 def main():
     45     global verbose, interactive, mac, rmok, nologin
     46     try:
     47         opts, args = getopt.getopt(sys.argv[1:], 'a:bil:mnp:qrs:v')
     48     except getopt.error, msg:
     49         usage(msg)
     50     login = ''
     51     passwd = ''
     52     account = ''
     53     if not args: usage('hostname missing')
     54     host = args[0]
     55     port = 0
     56     if ':' in host:
     57         host, port = host.split(':', 1)
     58         port = int(port)
     59     try:
     60         auth = netrc.netrc().authenticators(host)
     61         if auth is not None:
     62             login, account, passwd = auth
     63     except (netrc.NetrcParseError, IOError):
     64         pass
     65     for o, a in opts:
     66         if o == '-l': login = a
     67         if o == '-p': passwd = a
     68         if o == '-a': account = a
     69         if o == '-v': verbose = verbose + 1
     70         if o == '-q': verbose = 0
     71         if o == '-i': interactive = 1
     72         if o == '-m': mac = 1; nologin = 1; skippats.append('*.o')
     73         if o == '-n': nologin = 1
     74         if o == '-r': rmok = 1
     75         if o == '-s': skippats.append(a)
     76     remotedir = ''
     77     localdir = ''
     78     if args[1:]:
     79         remotedir = args[1]
     80         if args[2:]:
     81             localdir = args[2]
     82             if args[3:]: usage('too many arguments')
     83     #

     84     f = ftplib.FTP()
     85     if verbose: print "Connecting to '%s%s'..." % (host,
     86                                                    (port and ":%d"%port or ""))
     87     f.connect(host,port)
     88     if not nologin:
     89         if verbose:
     90             print 'Logging in as %r...' % (login or 'anonymous')
     91         f.login(login, passwd, account)
     92     if verbose: print 'OK.'
     93     pwd = f.pwd()
     94     if verbose > 1: print 'PWD =', repr(pwd)
     95     if remotedir:
     96         if verbose > 1: print 'cwd(%s)' % repr(remotedir)
     97         f.cwd(remotedir)
     98         if verbose > 1: print 'OK.'
     99         pwd = f.pwd()
    100         if verbose > 1: print 'PWD =', repr(pwd)
    101     #

    102     mirrorsubdir(f, localdir)
    103 
    104 # Core logic: mirror one subdirectory (recursively)

    105 def mirrorsubdir(f, localdir):
    106     pwd = f.pwd()
    107     if localdir and not os.path.isdir(localdir):
    108         if verbose: print 'Creating local directory', repr(localdir)
    109         try:
    110             makedir(localdir)
    111         except os.error, msg:
    112             print "Failed to establish local directory", repr(localdir)
    113             return
    114     infofilename = os.path.join(localdir, '.mirrorinfo')
    115     try:
    116         text = open(infofilename, 'r').read()
    117     except IOError, msg:
    118         text = '{}'
    119     try:
    120         info = eval(text)
    121     except (SyntaxError, NameError):
    122         print 'Bad mirror info in', repr(infofilename)
    123         info = {}
    124     subdirs = []
    125     listing = []
    126     if verbose: print 'Listing remote directory %r...' % (pwd,)
    127     f.retrlines('LIST', listing.append)
    128     filesfound = []
    129     for line in listing:
    130         if verbose > 1: print '-->', repr(line)
    131         if mac:
    132             # Mac listing has just filenames;

    133             # trailing / means subdirectory

    134             filename = line.strip()
    135             mode = '-'
    136             if filename[-1:] == '/':
    137                 filename = filename[:-1]
    138                 mode = 'd'
    139             infostuff = ''
    140         else:
    141             # Parse, assuming a UNIX listing

    142             words = line.split(None, 8)
    143             if len(words) < 6:
    144                 if verbose > 1: print 'Skipping short line'
    145                 continue
    146             filename = words[-1].lstrip()
    147             i = filename.find(" -> ")
    148             if i >= 0:
    149                 # words[0] had better start with 'l'...

    150                 if verbose > 1:
    151                     print 'Found symbolic link %r' % (filename,)
    152                 linkto = filename[i+4:]
    153                 filename = filename[:i]
    154             infostuff = words[-5:-1]
    155             mode = words[0]
    156         skip = 0
    157         for pat in skippats:
    158             if fnmatch(filename, pat):
    159                 if verbose > 1:
    160                     print 'Skip pattern', repr(pat),
    161                     print 'matches', repr(filename)
    162                 skip = 1
    163                 break
    164         if skip:
    165             continue
    166         if mode[0] == 'd':
    167             if verbose > 1:
    168                 print 'Remembering subdirectory', repr(filename)
    169             subdirs.append(filename)
    170             continue
    171         filesfound.append(filename)
    172         if info.has_key(filename) and info[filename] == infostuff:
    173             if verbose > 1:
    174                 print 'Already have this version of',repr(filename)
    175             continue
    176         fullname = os.path.join(localdir, filename)
    177         tempname = os.path.join(localdir, '@'+filename)
    178         if interactive:
    179             doit = askabout('file', filename, pwd)
    180             if not doit:
    181                 if not info.has_key(filename):
    182                     info[filename] = 'Not retrieved'
    183                 continue
    184         try:
    185             os.unlink(tempname)
    186         except os.error:
    187             pass
    188         if mode[0] == 'l':
    189             if verbose:
    190                 print "Creating symlink %r -> %r" % (filename, linkto)
    191             try:
    192                 os.symlink(linkto, tempname)
    193             except IOError, msg:
    194                 print "Can't create %r: %s" % (tempname, msg)
    195                 continue
    196         else:
    197             try:
    198                 fp = open(tempname, 'wb')
    199             except IOError, msg:
    200                 print "Can't create %r: %s" % (tempname, msg)
    201                 continue
    202             if verbose:
    203                 print 'Retrieving %r from %r as %r...' % (filename, pwd, fullname)
    204             if verbose:
    205                 fp1 = LoggingFile(fp, 1024, sys.stdout)
    206             else:
    207                 fp1 = fp
    208             t0 = time.time()
    209             try:
    210                 f.retrbinary('RETR ' + filename,
    211                              fp1.write, 8*1024)
    212             except ftplib.error_perm, msg:
    213                 print msg
    214             t1 = time.time()
    215             bytes = fp.tell()
    216             fp.close()
    217             if fp1 != fp:
    218                 fp1.close()
    219         try:
    220             os.unlink(fullname)
    221         except os.error:
    222             pass            # Ignore the error

    223         try:
    224             os.rename(tempname, fullname)
    225         except os.error, msg:
    226             print "Can't rename %r to %r: %s" % (tempname, fullname, msg)
    227             continue
    228         info[filename] = infostuff
    229         writedict(info, infofilename)
    230         if verbose and mode[0] != 'l':
    231             dt = t1 - t0
    232             kbytes = bytes / 1024.0
    233             print int(round(kbytes)),
    234             print 'Kbytes in',
    235             print int(round(dt)),
    236             print 'seconds',
    237             if t1 > t0:
    238                 print '(~%d Kbytes/sec)' % \
    239                           int(round(kbytes/dt),)
    240             print
    241     #

    242     # Remove files from info that are no longer remote

    243     deletions = 0
    244     for filename in info.keys():
    245         if filename not in filesfound:
    246             if verbose:
    247                 print "Removing obsolete info entry for",
    248                 print repr(filename), "in", repr(localdir or ".")
    249             del info[filename]
    250             deletions = deletions + 1
    251     if deletions:
    252         writedict(info, infofilename)
    253     #

    254     # Remove local files that are no longer in the remote directory

    255     try:
    256         if not localdir: names = os.listdir(os.curdir)
    257         else: names = os.listdir(localdir)
    258     except os.error:
    259         names = []
    260     for name in names:
    261         if name[0] == '.' or info.has_key(name) or name in subdirs:
    262             continue
    263         skip = 0
    264         for pat in skippats:
    265             if fnmatch(name, pat):
    266                 if verbose > 1:
    267                     print 'Skip pattern', repr(pat),
    268                     print 'matches', repr(name)
    269                 skip = 1
    270                 break
    271         if skip:
    272             continue
    273         fullname = os.path.join(localdir, name)
    274         if not rmok:
    275             if verbose:
    276                 print 'Local file', repr(fullname),
    277                 print 'is no longer pertinent'
    278             continue
    279         if verbose: print 'Removing local file/dir', repr(fullname)
    280         remove(fullname)
    281     #

    282     # Recursively mirror subdirectories

    283     for subdir in subdirs:
    284         if interactive:
    285             doit = askabout('subdirectory', subdir, pwd)
    286             if not doit: continue
    287         if verbose: print 'Processing subdirectory', repr(subdir)
    288         localsubdir = os.path.join(localdir, subdir)
    289         pwd = f.pwd()
    290         if verbose > 1:
    291             print 'Remote directory now:', repr(pwd)
    292             print 'Remote cwd', repr(subdir)
    293         try:
    294             f.cwd(subdir)
    295         except ftplib.error_perm, msg:
    296             print "Can't chdir to", repr(subdir), ":", repr(msg)
    297         else:
    298             if verbose: print 'Mirroring as', repr(localsubdir)
    299             mirrorsubdir(f, localsubdir)
    300             if verbose > 1: print 'Remote cwd ..'
    301             f.cwd('..')
    302         newpwd = f.pwd()
    303         if newpwd != pwd:
    304             print 'Ended up in wrong directory after cd + cd ..'
    305             print 'Giving up now.'
    306             break
    307         else:
    308             if verbose > 1: print 'OK.'
    309 
    310 # Helper to remove a file or directory tree

    311 def remove(fullname):
    312     if os.path.isdir(fullname) and not os.path.islink(fullname):
    313         try:
    314             names = os.listdir(fullname)
    315         except os.error:
    316             names = []
    317         ok = 1
    318         for name in names:
    319             if not remove(os.path.join(fullname, name)):
    320                 ok = 0
    321         if not ok:
    322             return 0
    323         try:
    324             os.rmdir(fullname)
    325         except os.error, msg:
    326             print "Can't remove local directory %r: %s" % (fullname, msg)
    327             return 0
    328     else:
    329         try:
    330             os.unlink(fullname)
    331         except os.error, msg:
    332             print "Can't remove local file %r: %s" % (fullname, msg)
    333             return 0
    334     return 1
    335 
    336 # Wrapper around a file for writing to write a hash sign every block.

    337 class LoggingFile:
    338     def __init__(self, fp, blocksize, outfp):
    339         self.fp = fp
    340         self.bytes = 0
    341         self.hashes = 0
    342         self.blocksize = blocksize
    343         self.outfp = outfp
    344     def write(self, data):
    345         self.bytes = self.bytes + len(data)
    346         hashes = int(self.bytes) / self.blocksize
    347         while hashes > self.hashes:
    348             self.outfp.write('#')
    349             self.outfp.flush()
    350             self.hashes = self.hashes + 1
    351         self.fp.write(data)
    352     def close(self):
    353         self.outfp.write('\n')
    354 
    355 # Ask permission to download a file.

    356 def askabout(filetype, filename, pwd):
    357     prompt = 'Retrieve %s %s from %s ? [ny] ' % (filetype, filename, pwd)
    358     while 1:
    359         reply = raw_input(prompt).strip().lower()
    360         if reply in ['y', 'ye', 'yes']:
    361             return 1
    362         if reply in ['', 'n', 'no', 'nop', 'nope']:
    363             return 0
    364         print 'Please answer yes or no.'
    365 
    366 # Create a directory if it doesn't exist.  Recursively create the

    367 # parent directory as well if needed.

    368 def makedir(pathname):
    369     if os.path.isdir(pathname):
    370         return
    371     dirname = os.path.dirname(pathname)
    372     if dirname: makedir(dirname)
    373     os.mkdir(pathname, 0777)
    374 
    375 # Write a dictionary to a file in a way that can be read back using

    376 # rval() but is still somewhat readable (i.e. not a single long line).

    377 # Also creates a backup file.

    378 def writedict(dict, filename):
    379     dir, fname = os.path.split(filename)
    380     tempname = os.path.join(dir, '@' + fname)
    381     backup = os.path.join(dir, fname + '~')
    382     try:
    383         os.unlink(backup)
    384     except os.error:
    385         pass
    386     fp = open(tempname, 'w')
    387     fp.write('{\n')
    388     for key, value in dict.items():
    389         fp.write('%r: %r,\n' % (key, value))
    390     fp.write('}\n')
    391     fp.close()
    392     try:
    393         os.rename(filename, backup)
    394     except os.error:
    395         pass
    396     os.rename(tempname, filename)
    397 
    398 
    399 if __name__ == '__main__':
    400     main()
    401