Docstringified and PASV support by Siebren (including new ftpcp() function).

This commit is contained in:
Guido van Rossum 1996-05-28 23:41:25 +00:00
parent 3ada87a508
commit d2560b0f2e

View file

@ -1,35 +1,34 @@
# An FTP client class. Based on RFC 959: File Transfer Protocol '''An FTP client class, and some helper functions.
# (FTP), by J. Postel and J. Reynolds Based on RFC 959: File Transfer Protocol
(FTP), by J. Postel and J. Reynolds
# Changes and improvements suggested by Steve Majewski Changes and improvements suggested by Steve Majewski.
# Modified by Jack to work on the mac. Modified by Jack to work on the mac.
Modified by Siebren to support docstrings and PASV.
# Example: Example:
#
# >>> from ftplib import FTP >>> from ftplib import FTP
# >>> ftp = FTP('ftp.python.org') # connect to host, default port >>> ftp = FTP('ftp.python.org') # connect to host, default port
# >>> ftp.login() # default, i.e.: user anonymous, passwd user@hostname >>> ftp.login() # default, i.e.: user anonymous, passwd user@hostname
# >>> ftp.retrlines('LIST') # list directory contents >>> ftp.retrlines('LIST') # list directory contents
# total 9 total 9
# drwxr-xr-x 8 root wheel 1024 Jan 3 1994 . drwxr-xr-x 8 root wheel 1024 Jan 3 1994 .
# drwxr-xr-x 8 root wheel 1024 Jan 3 1994 .. drwxr-xr-x 8 root wheel 1024 Jan 3 1994 ..
# drwxr-xr-x 2 root wheel 1024 Jan 3 1994 bin drwxr-xr-x 2 root wheel 1024 Jan 3 1994 bin
# drwxr-xr-x 2 root wheel 1024 Jan 3 1994 etc drwxr-xr-x 2 root wheel 1024 Jan 3 1994 etc
# d-wxrwxr-x 2 ftp wheel 1024 Sep 5 13:43 incoming d-wxrwxr-x 2 ftp wheel 1024 Sep 5 13:43 incoming
# drwxr-xr-x 2 root wheel 1024 Nov 17 1993 lib drwxr-xr-x 2 root wheel 1024 Nov 17 1993 lib
# drwxr-xr-x 6 1094 wheel 1024 Sep 13 19:07 pub drwxr-xr-x 6 1094 wheel 1024 Sep 13 19:07 pub
# drwxr-xr-x 3 root wheel 1024 Jan 3 1994 usr drwxr-xr-x 3 root wheel 1024 Jan 3 1994 usr
# -rw-r--r-- 1 root root 312 Aug 1 1994 welcome.msg -rw-r--r-- 1 root root 312 Aug 1 1994 welcome.msg
# >>> ftp.quit() >>> ftp.quit()
# >>> >>>
#
# To download a file, use ftp.retrlines('RETR ' + filename), A nice test that reveals some of the network dialogue would be:
# or ftp.retrbinary() with slightly different arguments. python ftplib.py -d localhost -l -p -l
# To upload a file, use ftp.storlines() or ftp.storbinary(), which have '''
# an open file as argument (see their definitions below for details).
# The download/upload functions first issue appropriate TYPE and PORT
# commands.
import os import os
@ -71,7 +70,23 @@ CRLF = '\r\n'
# The class itself # The class itself
class FTP: class FTP:
# New initialization method (called by class instantiation) '''An FTP client class.
To create a connection, call the class using these argument:
host, user, passwd, acct
These are all strings, and have default value ''.
Then use self.connect() with optional host and port argument.
To download a file, use ftp.retrlines('RETR ' + filename),
or ftp.retrbinary() with slightly different arguments.
To upload a file, use ftp.storlines() or ftp.storbinary(),
which have an open file as argument (see their definitions
below for details).
The download/upload functions first issue appropriate TYPE
and PORT or PASV commands.
'''
# Initialization method (called by class instantiation).
# Initialize host to localhost, port to standard ftp port # Initialize host to localhost, port to standard ftp port
# Optional arguments are host (for connect()), # Optional arguments are host (for connect()),
# and user, passwd, acct (for login()) # and user, passwd, acct (for login())
@ -87,32 +102,40 @@ class FTP:
self.connect(host) self.connect(host)
if user: self.login(user, passwd, acct) if user: self.login(user, passwd, acct)
# Connect to host. Arguments:
# - host: hostname to connect to (default previous host)
# - port: port to connect to (default previous port)
def connect(self, host = '', port = 0): def connect(self, host = '', port = 0):
'''Connect to host. Arguments are:
- host: hostname to connect to (string, default previous host)
- port: port to connect to (integer, default previous port)'''
if host: self.host = host if host: self.host = host
if port: self.port = port if port: self.port = port
self.passiveserver = 0
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect(self.host, self.port) self.sock.connect(self.host, self.port)
self.file = self.sock.makefile('rb') self.file = self.sock.makefile('r')
self.welcome = self.getresp() self.welcome = self.getresp()
# Get the welcome message from the server
# (this is read and squirreled away by connect())
def getwelcome(self): def getwelcome(self):
'''Get the welcome message from the server.
(this is read and squirreled away by connect())'''
if self.debugging: if self.debugging:
print '*welcome*', self.sanitize(self.welcome) print '*welcome*', self.sanitize(self.welcome)
return self.welcome return self.welcome
# Set the debugging level. Argument level means:
# 0: no debugging output (default)
# 1: print commands and responses but not body text etc.
# 2: also print raw lines read and sent before stripping CR/LF
def set_debuglevel(self, level): def set_debuglevel(self, level):
'''Set the debugging level.
The required argument level means:
0: no debugging output (default)
1: print commands and responses but not body text etc.
2: also print raw lines read and sent before stripping CR/LF'''
self.debugging = level self.debugging = level
debug = set_debuglevel debug = set_debuglevel
def set_pasv(self, val):
'''Use passive or active mode for data transfers.
With a false argument, use the normal PORT mode,
With a true argument, use the PASV command.'''
self.passiveserver = val
# Internal: "sanitize" a string for printing # Internal: "sanitize" a string for printing
def sanitize(self, s): def sanitize(self, s):
if s[:5] == 'pass ' or s[:5] == 'PASS ': if s[:5] == 'pass ' or s[:5] == 'PASS ':
@ -175,17 +198,17 @@ class FTP:
raise error_proto, resp raise error_proto, resp
return resp return resp
# Expect a response beginning with '2'
def voidresp(self): def voidresp(self):
"""Expect a response beginning with '2'."""
resp = self.getresp() resp = self.getresp()
if resp[0] <> '2': if resp[0] <> '2':
raise error_reply, resp raise error_reply, resp
# Abort a file transfer. Uses out-of-band data.
# This does not follow the procedure from the RFC to send Telnet
# IP and Synch; that doesn't seem to work with the servers I've
# tried. Instead, just send the ABOR command as OOB data.
def abort(self): def abort(self):
'''Abort a file transfer. Uses out-of-band data.
This does not follow the procedure from the RFC to send Telnet
IP and Synch; that doesn't seem to work with the servers I've
tried. Instead, just send the ABOR command as OOB data.'''
line = 'ABOR' + CRLF line = 'ABOR' + CRLF
if self.debugging > 1: print '*put urgent*', self.sanitize(line) if self.debugging > 1: print '*put urgent*', self.sanitize(line)
self.sock.send(line, MSG_OOB) self.sock.send(line, MSG_OOB)
@ -193,26 +216,26 @@ class FTP:
if resp[:3] not in ('426', '226'): if resp[:3] not in ('426', '226'):
raise error_proto, resp raise error_proto, resp
# Send a command and return the response
def sendcmd(self, cmd): def sendcmd(self, cmd):
'''Send a command and return the response.'''
self.putcmd(cmd) self.putcmd(cmd)
return self.getresp() return self.getresp()
# Send a command and expect a response beginning with '2'
def voidcmd(self, cmd): def voidcmd(self, cmd):
"""Send a command and expect a response beginning with '2'."""
self.putcmd(cmd) self.putcmd(cmd)
self.voidresp() self.voidresp()
# Send a PORT command with the current host and the given port number
def sendport(self, host, port): def sendport(self, host, port):
'''Send a PORT command with the current host and the given port number.'''
hbytes = string.splitfields(host, '.') hbytes = string.splitfields(host, '.')
pbytes = [`port/256`, `port%256`] pbytes = [`port/256`, `port%256`]
bytes = hbytes + pbytes bytes = hbytes + pbytes
cmd = 'PORT ' + string.joinfields(bytes, ',') cmd = 'PORT ' + string.joinfields(bytes, ',')
self.voidcmd(cmd) self.voidcmd(cmd)
# Create a new socket and send a PORT command for it
def makeport(self): def makeport(self):
'''Create a new socket and send a PORT command for it.'''
global nextport global nextport
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('', 0)) sock.bind(('', 0))
@ -222,18 +245,30 @@ class FTP:
resp = self.sendport(host, port) resp = self.sendport(host, port)
return sock return sock
# Send a port command and a transfer command, accept the connection
# and return the socket for the connection
def transfercmd(self, cmd): def transfercmd(self, cmd):
sock = self.makeport() '''Initiate a transfer over the data connection.
resp = self.sendcmd(cmd) If the transfer is active, send a port command and
if resp[0] <> '1': the transfer command, and accept the connection.
raise error_reply, resp If the server is passive, send a pasv command, connect
conn, sockaddr = sock.accept() to it, and start the transfer command.
Either way, return the socket for the connection'''
if self.passiveserver:
host, port = parse227(self.sendcmd('PASV'))
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn.connect(host, port)
resp = self.sendcmd(cmd)
if resp[0] <> '1':
raise error_reply, resp
else:
sock = self.makeport()
resp = self.sendcmd(cmd)
if resp[0] <> '1':
raise error_reply, resp
conn, sockaddr = sock.accept()
return conn return conn
# Login, default anonymous
def login(self, user = '', passwd = '', acct = ''): def login(self, user = '', passwd = '', acct = ''):
'''Login, default anonymous.'''
if not user: user = 'anonymous' if not user: user = 'anonymous'
if user == 'anonymous' and passwd in ('', '-'): if user == 'anonymous' and passwd in ('', '-'):
thishost = socket.gethostname() thishost = socket.gethostname()
@ -264,11 +299,11 @@ class FTP:
if resp[0] <> '2': if resp[0] <> '2':
raise error_reply, resp raise error_reply, resp
# Retrieve data in binary mode.
# The argument is a RETR command.
# The callback function is called for each block.
# This creates a new port for you
def retrbinary(self, cmd, callback, blocksize): def retrbinary(self, cmd, callback, blocksize):
'''Retrieve data in binary mode.
The argument is a RETR command.
The callback function is called for each block.
This creates a new port for you'''
self.voidcmd('TYPE I') self.voidcmd('TYPE I')
conn = self.transfercmd(cmd) conn = self.transfercmd(cmd)
while 1: while 1:
@ -279,16 +314,16 @@ class FTP:
conn.close() conn.close()
self.voidresp() self.voidresp()
# Retrieve data in line mode.
# The argument is a RETR or LIST command.
# The callback function is called for each line, with trailing
# CRLF stripped. This creates a new port for you.
# print_lines is the default callback
def retrlines(self, cmd, callback = None): def retrlines(self, cmd, callback = None):
'''Retrieve data in line mode.
The argument is a RETR or LIST command.
The callback function (2nd argument) is called for each line,
with trailing CRLF stripped. This creates a new port for you.
print_lines is the default callback.'''
if not callback: callback = print_line if not callback: callback = print_line
resp = self.sendcmd('TYPE A') resp = self.sendcmd('TYPE A')
conn = self.transfercmd(cmd) conn = self.transfercmd(cmd)
fp = conn.makefile('rb') fp = conn.makefile('r')
while 1: while 1:
line = fp.readline() line = fp.readline()
if self.debugging > 2: print '*retr*', `line` if self.debugging > 2: print '*retr*', `line`
@ -303,8 +338,8 @@ class FTP:
conn.close() conn.close()
self.voidresp() self.voidresp()
# Store a file in binary mode
def storbinary(self, cmd, fp, blocksize): def storbinary(self, cmd, fp, blocksize):
'''Store a file in binary mode.'''
self.voidcmd('TYPE I') self.voidcmd('TYPE I')
conn = self.transfercmd(cmd) conn = self.transfercmd(cmd)
while 1: while 1:
@ -314,8 +349,8 @@ class FTP:
conn.close() conn.close()
self.voidresp() self.voidresp()
# Store a file in line mode
def storlines(self, cmd, fp): def storlines(self, cmd, fp):
'''Store a file in line mode.'''
self.voidcmd('TYPE A') self.voidcmd('TYPE A')
conn = self.transfercmd(cmd) conn = self.transfercmd(cmd)
while 1: while 1:
@ -328,13 +363,13 @@ class FTP:
conn.close() conn.close()
self.voidresp() self.voidresp()
# Send new account name
def acct(self, password): def acct(self, password):
'''Send new account name.'''
cmd = 'ACCT ' + password cmd = 'ACCT ' + password
self.voidcmd(cmd) self.voidcmd(cmd)
# Return a list of files in a given directory (default the current)
def nlst(self, *args): def nlst(self, *args):
'''Return a list of files in a given directory (default the current).'''
cmd = 'NLST' cmd = 'NLST'
for arg in args: for arg in args:
cmd = cmd + (' ' + arg) cmd = cmd + (' ' + arg)
@ -342,11 +377,12 @@ class FTP:
self.retrlines(cmd, files.append) self.retrlines(cmd, files.append)
return files return files
# List a directory in long form. By default list current directory
# to stdout. Optional last argument is callback function;
# all non-empty arguments before it are concatenated to the
# LIST command. (This *should* only be used for a pathname.)
def dir(self, *args): def dir(self, *args):
'''List a directory in long form.
By default list current directory to stdout.
Optional last argument is callback function; all
non-empty arguments before it are concatenated to the
LIST command. (This *should* only be used for a pathname.)'''
cmd = 'LIST' cmd = 'LIST'
func = None func = None
if args[-1:] and type(args[-1]) != type(''): if args[-1:] and type(args[-1]) != type(''):
@ -356,15 +392,15 @@ class FTP:
cmd = cmd + (' ' + arg) cmd = cmd + (' ' + arg)
self.retrlines(cmd, func) self.retrlines(cmd, func)
# Rename a file
def rename(self, fromname, toname): def rename(self, fromname, toname):
'''Rename a file.'''
resp = self.sendcmd('RNFR ' + fromname) resp = self.sendcmd('RNFR ' + fromname)
if resp[0] <> '3': if resp[0] <> '3':
raise error_reply, resp raise error_reply, resp
self.voidcmd('RNTO ' + toname) self.voidcmd('RNTO ' + toname)
# Delete a file
def delete(self, filename): def delete(self, filename):
'''Delete a file.'''
resp = self.sendcmd('DELE ' + filename) resp = self.sendcmd('DELE ' + filename)
if resp[:3] == '250': if resp[:3] == '250':
return return
@ -373,8 +409,8 @@ class FTP:
else: else:
raise error_reply, resp raise error_reply, resp
# Change to a directory
def cwd(self, dirname): def cwd(self, dirname):
'''Change to a directory.'''
if dirname == '..': if dirname == '..':
try: try:
self.voidcmd('CDUP') self.voidcmd('CDUP')
@ -385,36 +421,61 @@ class FTP:
cmd = 'CWD ' + dirname cmd = 'CWD ' + dirname
self.voidcmd(cmd) self.voidcmd(cmd)
# Retrieve the size of a file
def size(self, filename): def size(self, filename):
'''Retrieve the size of a file.'''
# Note that the RFC doesn't say anything about 'SIZE'
resp = self.sendcmd('SIZE ' + filename) resp = self.sendcmd('SIZE ' + filename)
if resp[:3] == '213': if resp[:3] == '213':
return string.atoi(string.strip(resp[3:])) return string.atoi(string.strip(resp[3:]))
# Make a directory, return its full pathname
def mkd(self, dirname): def mkd(self, dirname):
'''Make a directory, return its full pathname.'''
resp = self.sendcmd('MKD ' + dirname) resp = self.sendcmd('MKD ' + dirname)
return parse257(resp) return parse257(resp)
# Return current wording directory
def pwd(self): def pwd(self):
'''Return current working directory.'''
resp = self.sendcmd('PWD') resp = self.sendcmd('PWD')
return parse257(resp) return parse257(resp)
# Quit, and close the connection
def quit(self): def quit(self):
'''Quit, and close the connection.'''
self.voidcmd('QUIT') self.voidcmd('QUIT')
self.close() self.close()
# Close the connection without assuming anything about it
def close(self): def close(self):
'''Close the connection without assuming anything about it.'''
self.file.close() self.file.close()
self.sock.close() self.sock.close()
del self.file, self.sock del self.file, self.sock
# Parse a response type 257 def parse227(resp):
'''Parse the '227' response for a PASV request.
Raises error_proto if it does not contain '(h1,h2,h3,h4,p1,p2)'
Return ('host.addr.as.numbers', port#) tuple.'''
if resp[:3] <> '227':
raise error_reply, resp
left = string.find(resp, '(')
if left < 0: raise error_proto, resp
right = string.find(resp, ')', left + 1)
if right < 0:
raise error_proto, resp # should contain '(h1,h2,h3,h4,p1,p2)'
numbers = string.split(resp[left+1:right], ',')
if len(numbers) <> 6:
raise error_proto, resp
host = string.join(numbers[:4], '.')
port = (string.atoi(numbers[4]) << 8) + string.atoi(numbers[5])
return host, port
# end parse227
def parse257(resp): def parse257(resp):
'''Parse the '257' response for a MKD or RMD request.
This is a response to a MKD or RMD request: a directory name.
Returns the directoryname in the 257 reply.'''
if resp[:3] <> '257': if resp[:3] <> '257':
raise error_reply, resp raise error_reply, resp
if resp[3:5] <> ' "': if resp[3:5] <> ' "':
@ -432,15 +493,32 @@ def parse257(resp):
dirname = dirname + c dirname = dirname + c
return dirname return dirname
# Default retrlines callback to print a line
def print_line(line): def print_line(line):
'''Default retrlines callback to print a line.'''
print line print line
def ftpcp(source, sourcename, target, targetname = '', type = 'I'):
'''Copy file from one FTP-instance to another.'''
if not targetname: targetname = sourcename
type = 'TYPE ' + type
source.voidcmd(type)
target.voidcmd(type)
sourcehost, sourceport = parse227(source.sendcmd('PASV'))
target.sendport(sourcehost, sourceport)
# RFC 959: the user must "listen" [...] BEFORE sending the
# transfer request.
# So: STOR before RETR, because here the target is a "user".
treply = target.sendcmd('STOR ' + targetname)
if treply[:3] not in ('125', '150'): raise error_proto # RFC 959
sreply = source.sendcmd('RETR ' + sourcename)
if sreply[:3] not in ('125', '150'): raise error_proto # RFC 959
source.voidresp()
target.voidresp()
# Test program.
# Usage: ftp [-d] host [-l[dir]] [-d[dir]] [file] ...
def test(): def test():
import marshal '''Test program.
Usage: ftp [-d] host [-l[dir]] [-d[dir]] [-p] [file] ...'''
debugging = 0 debugging = 0
while sys.argv[1] == '-d': while sys.argv[1] == '-d':
debugging = debugging+1 debugging = debugging+1
@ -456,6 +534,8 @@ def test():
cmd = 'CWD' cmd = 'CWD'
if file[2:]: cmd = cmd + ' ' + file[2:] if file[2:]: cmd = cmd + ' ' + file[2:]
resp = ftp.sendcmd(cmd) resp = ftp.sendcmd(cmd)
elif file == '-p':
ftp.set_pasv(not ftp.passiveserver)
else: else:
ftp.retrbinary('RETR ' + file, \ ftp.retrbinary('RETR ' + file, \
sys.stdout.write, 1024) sys.stdout.write, 1024)