diff options
-rw-r--r-- | Lib/ftplib.py | 262 |
1 files changed, 171 insertions, 91 deletions
diff --git a/Lib/ftplib.py b/Lib/ftplib.py index 59eef65..e42ed8c 100644 --- a/Lib/ftplib.py +++ b/Lib/ftplib.py @@ -1,35 +1,34 @@ -# An FTP client class. Based on RFC 959: File Transfer Protocol -# (FTP), by J. Postel and J. Reynolds - -# Changes and improvements suggested by Steve Majewski -# Modified by Jack to work on the mac. - - -# Example: -# -# >>> from ftplib import FTP -# >>> ftp = FTP('ftp.python.org') # connect to host, default port -# >>> ftp.login() # default, i.e.: user anonymous, passwd user@hostname -# >>> ftp.retrlines('LIST') # list directory contents -# 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 2 root wheel 1024 Jan 3 1994 bin -# drwxr-xr-x 2 root wheel 1024 Jan 3 1994 etc -# 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 6 1094 wheel 1024 Sep 13 19:07 pub -# drwxr-xr-x 3 root wheel 1024 Jan 3 1994 usr -# -rw-r--r-- 1 root root 312 Aug 1 1994 welcome.msg -# >>> ftp.quit() -# >>> -# -# 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 -# commands. +'''An FTP client class, and some helper functions. +Based on RFC 959: File Transfer Protocol +(FTP), by J. Postel and J. Reynolds + +Changes and improvements suggested by Steve Majewski. +Modified by Jack to work on the mac. +Modified by Siebren to support docstrings and PASV. + + +Example: + +>>> from ftplib import FTP +>>> ftp = FTP('ftp.python.org') # connect to host, default port +>>> ftp.login() # default, i.e.: user anonymous, passwd user@hostname +>>> ftp.retrlines('LIST') # list directory contents +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 2 root wheel 1024 Jan 3 1994 bin +drwxr-xr-x 2 root wheel 1024 Jan 3 1994 etc +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 6 1094 wheel 1024 Sep 13 19:07 pub +drwxr-xr-x 3 root wheel 1024 Jan 3 1994 usr +-rw-r--r-- 1 root root 312 Aug 1 1994 welcome.msg +>>> ftp.quit() +>>> + +A nice test that reveals some of the network dialogue would be: +python ftplib.py -d localhost -l -p -l +''' import os @@ -71,7 +70,23 @@ CRLF = '\r\n' # The class itself 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 # Optional arguments are host (for connect()), # and user, passwd, acct (for login()) @@ -87,32 +102,40 @@ class FTP: self.connect(host) 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): + '''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 port: self.port = port + self.passiveserver = 0 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect(self.host, self.port) - self.file = self.sock.makefile('rb') + self.file = self.sock.makefile('r') self.welcome = self.getresp() - # Get the welcome message from the server - # (this is read and squirreled away by connect()) def getwelcome(self): + '''Get the welcome message from the server. + (this is read and squirreled away by connect())''' if self.debugging: print '*welcome*', self.sanitize(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): + '''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 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 def sanitize(self, s): if s[:5] == 'pass ' or s[:5] == 'PASS ': @@ -175,17 +198,17 @@ class FTP: raise error_proto, resp return resp - # Expect a response beginning with '2' def voidresp(self): + """Expect a response beginning with '2'.""" resp = self.getresp() if resp[0] <> '2': 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): + '''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 if self.debugging > 1: print '*put urgent*', self.sanitize(line) self.sock.send(line, MSG_OOB) @@ -193,26 +216,26 @@ class FTP: if resp[:3] not in ('426', '226'): raise error_proto, resp - # Send a command and return the response def sendcmd(self, cmd): + '''Send a command and return the response.''' self.putcmd(cmd) return self.getresp() - # Send a command and expect a response beginning with '2' def voidcmd(self, cmd): + """Send a command and expect a response beginning with '2'.""" self.putcmd(cmd) self.voidresp() - # Send a PORT command with the current host and the given port number def sendport(self, host, port): + '''Send a PORT command with the current host and the given port number.''' hbytes = string.splitfields(host, '.') pbytes = [`port/256`, `port%256`] bytes = hbytes + pbytes cmd = 'PORT ' + string.joinfields(bytes, ',') self.voidcmd(cmd) - # Create a new socket and send a PORT command for it def makeport(self): + '''Create a new socket and send a PORT command for it.''' global nextport sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('', 0)) @@ -222,18 +245,30 @@ class FTP: resp = self.sendport(host, port) return sock - # Send a port command and a transfer command, accept the connection - # and return the socket for the connection def transfercmd(self, cmd): - sock = self.makeport() - resp = self.sendcmd(cmd) - if resp[0] <> '1': - raise error_reply, resp - conn, sockaddr = sock.accept() + '''Initiate a transfer over the data connection. + If the transfer is active, send a port command and + the transfer command, and accept the connection. + If the server is passive, send a pasv command, connect + 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 - # Login, default anonymous def login(self, user = '', passwd = '', acct = ''): + '''Login, default anonymous.''' if not user: user = 'anonymous' if user == 'anonymous' and passwd in ('', '-'): thishost = socket.gethostname() @@ -264,11 +299,11 @@ class FTP: if resp[0] <> '2': 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): + '''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') conn = self.transfercmd(cmd) while 1: @@ -279,16 +314,16 @@ class FTP: conn.close() 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): + '''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 resp = self.sendcmd('TYPE A') conn = self.transfercmd(cmd) - fp = conn.makefile('rb') + fp = conn.makefile('r') while 1: line = fp.readline() if self.debugging > 2: print '*retr*', `line` @@ -303,8 +338,8 @@ class FTP: conn.close() self.voidresp() - # Store a file in binary mode def storbinary(self, cmd, fp, blocksize): + '''Store a file in binary mode.''' self.voidcmd('TYPE I') conn = self.transfercmd(cmd) while 1: @@ -314,8 +349,8 @@ class FTP: conn.close() self.voidresp() - # Store a file in line mode def storlines(self, cmd, fp): + '''Store a file in line mode.''' self.voidcmd('TYPE A') conn = self.transfercmd(cmd) while 1: @@ -328,13 +363,13 @@ class FTP: conn.close() self.voidresp() - # Send new account name def acct(self, password): + '''Send new account name.''' cmd = 'ACCT ' + password self.voidcmd(cmd) - # Return a list of files in a given directory (default the current) def nlst(self, *args): + '''Return a list of files in a given directory (default the current).''' cmd = 'NLST' for arg in args: cmd = cmd + (' ' + arg) @@ -342,11 +377,12 @@ class FTP: self.retrlines(cmd, files.append) 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): + '''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' func = None if args[-1:] and type(args[-1]) != type(''): @@ -356,15 +392,15 @@ class FTP: cmd = cmd + (' ' + arg) self.retrlines(cmd, func) - # Rename a file def rename(self, fromname, toname): + '''Rename a file.''' resp = self.sendcmd('RNFR ' + fromname) if resp[0] <> '3': raise error_reply, resp self.voidcmd('RNTO ' + toname) - # Delete a file def delete(self, filename): + '''Delete a file.''' resp = self.sendcmd('DELE ' + filename) if resp[:3] == '250': return @@ -373,8 +409,8 @@ class FTP: else: raise error_reply, resp - # Change to a directory def cwd(self, dirname): + '''Change to a directory.''' if dirname == '..': try: self.voidcmd('CDUP') @@ -385,36 +421,61 @@ class FTP: cmd = 'CWD ' + dirname self.voidcmd(cmd) - # Retrieve the size of a file 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) if resp[:3] == '213': return string.atoi(string.strip(resp[3:])) - # Make a directory, return its full pathname def mkd(self, dirname): + '''Make a directory, return its full pathname.''' resp = self.sendcmd('MKD ' + dirname) return parse257(resp) - # Return current wording directory def pwd(self): + '''Return current working directory.''' resp = self.sendcmd('PWD') return parse257(resp) - # Quit, and close the connection def quit(self): + '''Quit, and close the connection.''' self.voidcmd('QUIT') self.close() - # Close the connection without assuming anything about it def close(self): + '''Close the connection without assuming anything about it.''' self.file.close() self.sock.close() 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): + '''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': raise error_reply, resp if resp[3:5] <> ' "': @@ -432,15 +493,32 @@ def parse257(resp): dirname = dirname + c return dirname -# Default retrlines callback to print a line def print_line(line): + '''Default retrlines callback to print a 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(): - import marshal + '''Test program. + Usage: ftp [-d] host [-l[dir]] [-d[dir]] [-p] [file] ...''' + debugging = 0 while sys.argv[1] == '-d': debugging = debugging+1 @@ -456,6 +534,8 @@ def test(): cmd = 'CWD' if file[2:]: cmd = cmd + ' ' + file[2:] resp = ftp.sendcmd(cmd) + elif file == '-p': + ftp.set_pasv(not ftp.passiveserver) else: ftp.retrbinary('RETR ' + file, \ sys.stdout.write, 1024) |