# 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. import os import sys import string # Import SOCKS module if it exists, else standard socket module socket try: import SOCKS; socket = SOCKS except ImportError: import socket # Magic number from MSG_OOB = 0x1 # Process data out of band # The standard FTP server control port FTP_PORT = 21 # Exception raised when an error or invalid response is received error_reply = 'ftplib.error_reply' # unexpected [123]xx reply error_temp = 'ftplib.error_temp' # 4xx errors error_perm = 'ftplib.error_perm' # 5xx errors error_proto = 'ftplib.error_proto' # response does not begin with [1-5] # All exceptions (hopefully) that may be raised here and that aren't # (always) programming errors on our side all_errors = (error_reply, error_temp, error_perm, error_proto, \ socket.error, IOError, EOFError) # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF) CRLF = '\r\n' # The class itself class FTP: # New 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()) def __init__(self, host = '', user = '', passwd = '', acct = ''): # Initialize the instance to something mostly harmless self.debugging = 0 self.host = '' self.port = FTP_PORT self.sock = None self.file = None self.welcome = None if host: 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): if host: self.host = host if port: self.port = port self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect(self.host, self.port) 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): 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): self.debugging = level debug = set_debuglevel # Internal: "sanitize" a string for printing def sanitize(self, s): if s[:5] == 'pass ' or s[:5] == 'PASS ': i = len(s) while i > 5 and s[i-1] in '\r\n': i = i-1 s = s[:5] + '*'*(i-5) + s[i:] return `s` # Internal: send one line to the server, appending CRLF def putline(self, line): line = line + CRLF if self.debugging > 1: print '*put*', self.sanitize(line) self.sock.send(line) # Internal: send one command to the server (through putline()) def putcmd(self, line): if self.debugging: print '*cmd*', self.sanitize(line) self.putline(line) # Internal: return one line from the server, stripping CRLF. # Raise EOFError if the connection is closed def getline(self): line = self.file.readline() if self.debugging > 1: print '*get*', self.sanitize(line) if not line: raise EOFError if line[-2:] == CRLF: line = line[:-2] elif line[-1:] in CRLF: line = line[:-1] return line # Internal: get a response from the server, which may possibly # consist of multiple lines. Return a single string with no # trailing CRLF. If the response consists of multiple lines, # these are separated by '\n' characters in the string def getmultiline(self): line = self.getline() if line[3:4] == '-': code = line[:3] while 1: nextline = self.getline() line = line + ('\n' + nextline) if nextline[:3] == code and \ nextline[3:4] <> '-': break return line # Internal: get a response from the server. # Raise various errors if the response indicates an error def getresp(self): resp = self.getmultiline() if self.debugging: print '*resp*', self.sanitize(resp) self.lastresp = resp[:3] c = resp[:1] if c == '4': raise error_temp, resp if c == '5': raise error_perm, resp if c not in '123': raise error_proto, resp return resp # Expect a response beginning with '2' def voidresp(self): 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): line = 'ABOR' + CRLF if self.debugging > 1: print '*put urgent*', self.sanitize(line) self.sock.send(line, MSG_OOB) resp = self.getmultiline() if resp[:3] not in ('426', '226'): raise error_proto, resp # Send a command and return the response def sendcmd(self, cmd): self.putcmd(cmd) return self.getresp() # Send a command and expect a response beginning with '2' def voidcmd(self, cmd): self.putcmd(cmd) self.voidresp() # Send a PORT command with the current host and the given port number def sendport(self, host, port): 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): global nextport sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('', 0)) sock.listen(1) dummyhost, port = sock.getsockname() # Get proper port host, dummyport = self.sock.getsockname() # Get proper host 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() return conn # Login, default anonymous def login(self, user = '', passwd = '', acct = ''): if not user: user = 'anonymous' if user == 'anonymous' and passwd in ('', '-'): thishost = socket.gethostname() # Make sure it is fully qualified if not '.' in thishost: thisaddr = socket.gethostbyname(thishost) firstname, names, unused = \ socket.gethostbyaddr(thisaddr) names.insert(0, firstname) for name in names: if '.' in name: thishost = name break try: if os.environ.has_key('LOGNAME'): realuser = os.environ['LOGNAME'] elif os.environ.has_key('USER'): realuser = os.environ['USER'] else: realuser = 'anonymous' except AttributeError: # Not all systems have os.environ.... realuser = 'anonymous' passwd = passwd + realuser + '@' + thishost resp = self.sendcmd('USER ' + user) if resp[0] == '3': resp = self.sendcmd('PASS ' + passwd) if resp[0] == '3': resp = self.sendcmd('ACCT ' + acct) 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): self.voidcmd('TYPE I') conn = self.transfercmd(cmd) while 1: data = conn.recv(blocksize) if not data: break callback(data) 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): if not callback: callback = print_line resp = self.sendcmd('TYPE A') conn = self.transfercmd(cmd) fp = conn.makefile('r') while 1: line = fp.readline() if self.debugging > 2: print '*retr*', `line` if not line: break if line[-2:] == CRLF: line = line[:-2] elif line[:-1] == '\n': line = line[:-1] callback(line) fp.close() conn.close() self.voidresp() # Store a file in binary mode def storbinary(self, cmd, fp, blocksize): self.voidcmd('TYPE I') conn = self.transfercmd(cmd) while 1: buf = fp.read(blocksize) if not buf: break conn.send(buf) conn.close() self.voidresp() # Store a file in line mode def storlines(self, cmd, fp): self.voidcmd('TYPE A') conn = self.transfercmd(cmd) while 1: buf = fp.readline() if not buf: break if buf[-2:] <> CRLF: if buf[-1] in CRLF: buf = buf[:-1] buf = buf + CRLF conn.send(buf) conn.close() self.voidresp() # Return a list of files in a given directory (default the current) def nlst(self, *args): cmd = 'NLST' for arg in args: cmd = cmd + (' ' + arg) files = [] 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): cmd = 'LIST' func = None if args[-1:] and type(args[-1]) != type(''): args, func = args[:-1], args[-1] for arg in args: if arg: cmd = cmd + (' ' + arg) self.retrlines(cmd, func) # Rename a file def rename(self, fromname, toname): resp = self.sendcmd('RNFR ' + fromname) if resp[0] <> '3': raise error_reply, resp self.voidcmd('RNTO ' + toname) # Delete a file def delete(self, filename): resp = self.sendcmd('DELE ' + filename) if resp[:3] == '250': return elif resp[:1] == '5': raise error_perm, resp else: raise error_reply, resp # Change to a directory def cwd(self, dirname): if dirname == '..': try: self.voidcmd('CDUP') return except error_perm, msg: if msg[:3] != '500': raise error_perm, msg cmd = 'CWD ' + dirname self.voidcmd(cmd) # Retrieve the size of a file def size(self, filename): 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): resp = self.sendcmd('MKD ' + dirname) return parse257(resp) # Return current wording directory def pwd(self): resp = self.sendcmd('PWD') return parse257(resp) # Quit, and close the connection def quit(self): self.voidcmd('QUIT') self.close() # Close the connection without assuming anything about it def close(self): self.file.close() self.sock.close() del self.file, self.sock # Parse a response type 257 def parse257(resp): if resp[:3] <> '257': raise error_reply, resp if resp[3:5] <> ' "': return '' # Not compliant to RFC 959, but UNIX ftpd does this dirname = '' i = 5 n = len(resp) while i < n: c = resp[i] i = i+1 if c == '"': if i >= n or resp[i] <> '"': break i = i+1 dirname = dirname + c return dirname # Default retrlines callback to print a line def print_line(line): print line # Test program. # Usage: ftp [-d] host [-l[dir]] [-d[dir]] [file] ... def test(): import marshal debugging = 0 while sys.argv[1] == '-d': debugging = debugging+1 del sys.argv[1] host = sys.argv[1] ftp = FTP(host) ftp.set_debuglevel(debugging) ftp.login() for file in sys.argv[2:]: if file[:2] == '-l': ftp.dir(file[2:]) elif file[:2] == '-d': cmd = 'CWD' if file[2:]: cmd = cmd + ' ' + file[2:] resp = ftp.sendcmd(cmd) else: ftp.retrbinary('RETR ' + file, \ sys.stdout.write, 1024) ftp.quit() if __name__ == '__main__': test()