fetch - a simple function using python's ftplib

Steven D. Majewski (sdm7g@elvis.med.virginia.edu)
Sat, 16 Oct 1993 18:37:13 -0400

I hadn't noticed ftplib was even there until it came up on the list.
So I hacked out a fetch function. The *intention* was to handle
URL's and/or the pathnames in T.Brannon's paper, but I didn't have
either at hand when I wrote this, so instead, it takes a pathname
of the form:
remote.host:/path/filename
and by default it will copy (binary) filename into local currect directory.

I could have hacked in a strip of the "anonymous@" part, but since
the example didn't show the format used if a password was also
needed, I didn't bother to do it. Maybe if I dig out a copy of the
URL format, I'll do something else with it. But then, maybe that's
already in demo/www/wwwlib.py. Well - I wanted to try out the ftplib
routines anyway - I need to write a program that backups up PC's
thru NCSA Telnet's FTP server to unix tape. That has been on my
wish list, but I haven't done anything about it - now that I know
ftplib is there, I'll probably give it a try when I have a bit of
spare time.

The error handling is not as clean and neat as I would probaly
want in a final version. This has been minimally tested.
( and not on anything that refuses connections --- I'm relying
that the FTP class methods will do the right thing. )

There is a question for Guido (or some other master of python internals)
burried in the code: When I moved mt test code into a test function
( so that I could have some cleanup - otherwise when I types 'q' to
more, my terminal got zonked! ) and out of the modules global scope,
I had to add the "global" declaration - otherwise I got a NameError.
I suppose this is a subtle effect of dynamic scoping and the way the
function is used as a callback function in another scope. But I would
welcome it if someone can explain what's going on in more detail.

- Steve Majewski (804-982-0831) <sdm7g@Virginia.EDU>
- UVA Department of Molecular Physiology and Biological Physics

------------------------
#!/usr/local/bin/python
#
# fetch : a simple higher level function to python's ftplib
#
# given a pathname of format: host.part:/path/filename
# 'fetch( pathname )' will fetch pathname into current local directory
# 'fetch( pathname, open( FNAME, 'w').write )' will copy remote pathname
# into local FNAME, and
# 'fetch( pathname, func ) will send blocks pathname to output function func.
# ( see test examples at end )
#
# other option arg is one of:
# 'ascii', 'text', 'mode=ascii', 'mode=text'
# which change the transfer mode from binary to text
# ( note that retrlines strips tailing newlines )
#
# options to be added later:
# FTP().login() supports optional args for USER & PASSWD -
# add check for optional args "user=" and "pass=" to fetch.
#
#
# shell usage:
# fetch.py remotepathnames...
#
#
# - S.D.Majewski/UVA <sdm7g@Virginia.EDU>

from ftplib import FTP
import string
import posixpath

def null(): pass
FUNC_TYPES = ( type( null ), type( type ) )
# to include both <type 'function'> and <type 'builtin_function_or_method'>

TEXT_MODES = ( 'text', 'ascii', 'mode=text', 'mode=ascii' )

def fetch( fname, *opts ):
mode = 'bin' # default transfer mode
func = None
if opts :
for arg in opts:
if type( arg ) in FUNC_TYPES : func = arg
elif arg in TEXT_MODES : mode = 'text'
host, dir, file = parse( fname )
if not func : func = open( file, 'w' ).write
ftp = FTP().init( host )
ftp.login()
ftp.cwd( dir )
print ftp.nlst( file ), 'mode='+mode # this can be removed
if mode == 'text' :
try:
ftp.retrlines( 'RETR ' + file, func )
except IOError:
ftp.abort()
ftp.quit()
raise IOError
elif mode == 'bin' :
try:
ftp.retrbinary( 'RETR '+ file, func, 512 )
except IOError:
ftp.abort()
ftp.quit()
raise IOError
ftp.quit()

def parse( fname ):
list = string.splitfields( fname, ':' )
if len( list ) <> 2 : raise 'NoHostPart'
host = list[0]
dir, file = posixpath.split( list[1] )
return ( host, dir, file )

# example usage
#

from posix import popen

def test1( ):
path = 'uvaarpa.virginia.edu:/pub/hosts'
fetch( path )

# "global m" appears to be necessary because mwrite callback
# function is called in a different context from the one
# in which it is defined. Without it, fetch will raise a
# NameError exception. ( The wonders of Dynamic scoping? )

def test2():
global m
m = popen( 'more', 'w' )
def mwrite( line ): m.write( line + '\n' )
try:
fetch( 'uvaarpa.virginia.edu:/pub/hosts', mwrite, 'text' )
finally: m.close()

def test3():
z = popen( 'zcat | more', 'w' )
try:
fetch( 'uvaarpa.virginia.edu:/pub/rfc/rfc822.Z', z.write )
finally: z.close()

import sys

if sys.argv[1:] :
for file in sys.argv[1:] :
fetch( file )

# ---------- end fetch.py