summaryrefslogtreecommitdiff
path: root/Lib/ftplib.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/ftplib.py')
-rw-r--r--Lib/ftplib.py341
1 files changed, 200 insertions, 141 deletions
diff --git a/Lib/ftplib.py b/Lib/ftplib.py
index a62218b333..741e64794e 100644
--- a/Lib/ftplib.py
+++ b/Lib/ftplib.py
@@ -38,13 +38,7 @@ python ftplib.py -d localhost -l -p -l
import os
import sys
-
-# Import SOCKS module if it exists, else standard socket module socket
-try:
- import SOCKS; socket = SOCKS; del SOCKS # import SOCKS as socket
- from socket import getfqdn; socket.getfqdn = getfqdn; del getfqdn
-except ImportError:
- import socket
+import socket
from socket import _GLOBAL_DEFAULT_TIMEOUT
__all__ = ["FTP","Netrc"]
@@ -72,6 +66,7 @@ all_errors = (Error, IOError, EOFError)
# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
CRLF = '\r\n'
+B_CRLF = b'\r\n'
# The class itself
class FTP:
@@ -96,7 +91,7 @@ class FTP:
below for details).
The download/upload functions first issue appropriate TYPE
and PORT or PASV commands.
-'''
+ '''
debugging = 0
host = ''
@@ -105,23 +100,41 @@ class FTP:
file = None
welcome = None
passiveserver = 1
+ encoding = "latin-1"
# 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='',
- timeout=_GLOBAL_DEFAULT_TIMEOUT):
+ timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None):
+ self.source_address = source_address
self.timeout = timeout
if host:
self.connect(host)
if user:
self.login(user, passwd, acct)
- def connect(self, host='', port=0, timeout=-999):
+ def __enter__(self):
+ return self
+
+ # Context management protocol: try to quit() if active
+ def __exit__(self, *args):
+ if self.sock is not None:
+ try:
+ self.quit()
+ except (socket.error, EOFError):
+ pass
+ finally:
+ if self.sock is not None:
+ self.close()
+
+ def connect(self, host='', port=0, timeout=-999, source_address=None):
'''Connect to host. Arguments are:
- host: hostname to connect to (string, default previous host)
- port: port to connect to (integer, default previous port)
+ - source_address: a 2-tuple (host, port) for the socket to bind
+ to as its source address before connecting.
'''
if host != '':
self.host = host
@@ -129,9 +142,12 @@ class FTP:
self.port = port
if timeout != -999:
self.timeout = timeout
- self.sock = socket.create_connection((self.host, self.port), self.timeout)
+ if source_address is not None:
+ self.source_address = source_address
+ self.sock = socket.create_connection((self.host, self.port), self.timeout,
+ source_address=self.source_address)
self.af = self.sock.family
- self.file = self.sock.makefile('rb')
+ self.file = self.sock.makefile('r', encoding=self.encoding)
self.welcome = self.getresp()
return self.welcome
@@ -139,7 +155,7 @@ class FTP:
'''Get the welcome message from the server.
(this is read and squirreled away by connect())'''
if self.debugging:
- print '*welcome*', self.sanitize(self.welcome)
+ print('*welcome*', self.sanitize(self.welcome))
return self.welcome
def set_debuglevel(self, level):
@@ -159,22 +175,20 @@ class FTP:
# 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
+ if s[:5] in {'pass ', 'PASS '}:
+ i = len(s.rstrip('\r\n'))
s = s[:5] + '*'*(i-5) + s[i:]
return repr(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.sendall(line)
+ if self.debugging > 1: print('*put*', self.sanitize(line))
+ self.sock.sendall(line.encode(self.encoding))
# Internal: send one command to the server (through putline())
def putcmd(self, line):
- if self.debugging: print '*cmd*', self.sanitize(line)
+ if self.debugging: print('*cmd*', self.sanitize(line))
self.putline(line)
# Internal: return one line from the server, stripping CRLF.
@@ -182,7 +196,7 @@ class FTP:
def getline(self):
line = self.file.readline()
if self.debugging > 1:
- print '*get*', self.sanitize(line)
+ 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]
@@ -208,22 +222,22 @@ class FTP:
# Raise various errors if the response indicates an error
def getresp(self):
resp = self.getmultiline()
- if self.debugging: print '*resp*', self.sanitize(resp)
+ if self.debugging: print('*resp*', self.sanitize(resp))
self.lastresp = resp[:3]
c = resp[:1]
- if c in ('1', '2', '3'):
+ if c in {'1', '2', '3'}:
return resp
if c == '4':
- raise error_temp, resp
+ raise error_temp(resp)
if c == '5':
- raise error_perm, resp
- raise error_proto, resp
+ raise error_perm(resp)
+ raise error_proto(resp)
def voidresp(self):
"""Expect a response beginning with '2'."""
resp = self.getresp()
if resp[:1] != '2':
- raise error_reply, resp
+ raise error_reply(resp)
return resp
def abort(self):
@@ -231,12 +245,13 @@ class FTP:
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)
+ line = b'ABOR' + B_CRLF
+ if self.debugging > 1: print('*put urgent*', self.sanitize(line))
self.sock.sendall(line, MSG_OOB)
resp = self.getmultiline()
- if resp[:3] not in ('426', '225', '226'):
- raise error_proto, resp
+ if resp[:3] not in {'426', '225', '226'}:
+ raise error_proto(resp)
+ return resp
def sendcmd(self, cmd):
'''Send a command and return the response.'''
@@ -266,7 +281,7 @@ class FTP:
if self.af == socket.AF_INET6:
af = 2
if af == 0:
- raise error_proto, 'unsupported address family'
+ raise error_proto('unsupported address family')
fields = ['', repr(af), host, repr(port), '']
cmd = 'EPRT ' + '|'.join(fields)
return self.voidcmd(cmd)
@@ -280,7 +295,8 @@ class FTP:
try:
sock = socket.socket(af, socktype, proto)
sock.bind(sa)
- except socket.error, err:
+ except socket.error as _:
+ err = _
if sock:
sock.close()
sock = None
@@ -291,6 +307,7 @@ class FTP:
raise err
else:
raise socket.error("getaddrinfo returns an empty list")
+ raise socket.error(msg)
sock.listen(1)
port = sock.getsockname()[1] # Get proper port
host = self.sock.getsockname()[0] # Get proper host
@@ -327,7 +344,8 @@ class FTP:
size = None
if self.passiveserver:
host, port = self.makepasv()
- conn = socket.create_connection((host, port), self.timeout)
+ conn = socket.create_connection((host, port), self.timeout,
+ source_address=self.source_address)
try:
if rest is not None:
self.sendcmd("REST %s" % rest)
@@ -341,13 +359,12 @@ class FTP:
if resp[0] == '2':
resp = self.getresp()
if resp[0] != '1':
- raise error_reply, resp
+ raise error_reply(resp)
except:
conn.close()
raise
else:
- sock = self.makeport()
- try:
+ with self.makeport() as sock:
if rest is not None:
self.sendcmd("REST %s" % rest)
resp = self.sendcmd(cmd)
@@ -355,12 +372,10 @@ class FTP:
if resp[0] == '2':
resp = self.getresp()
if resp[0] != '1':
- raise error_reply, resp
+ raise error_reply(resp)
conn, sockaddr = sock.accept()
if self.timeout is not _GLOBAL_DEFAULT_TIMEOUT:
conn.settimeout(self.timeout)
- finally:
- sock.close()
if resp[:3] == '150':
# this is conditional in case we received a 125
size = parse150(resp)
@@ -375,7 +390,7 @@ class FTP:
if not user: user = 'anonymous'
if not passwd: passwd = ''
if not acct: acct = ''
- if user == 'anonymous' and passwd in ('', '-'):
+ if user == 'anonymous' and passwd in {'', '-'}:
# If there is no anonymous ftp password specified
# then we'll just use anonymous@
# We don't send any other thing because:
@@ -388,7 +403,7 @@ class FTP:
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
+ raise error_reply(resp)
return resp
def retrbinary(self, cmd, callback, blocksize=8192, rest=None):
@@ -406,20 +421,19 @@ class FTP:
The response code.
"""
self.voidcmd('TYPE I')
- conn = self.transfercmd(cmd, rest)
- while 1:
- data = conn.recv(blocksize)
- if not data:
- break
- callback(data)
- conn.close()
+ with self.transfercmd(cmd, rest) as conn:
+ while 1:
+ data = conn.recv(blocksize)
+ if not data:
+ break
+ callback(data)
return self.voidresp()
def retrlines(self, cmd, callback = None):
"""Retrieve data in line mode. A new port is created for you.
Args:
- cmd: A RETR, LIST, NLST, or MLSD command.
+ cmd: A RETR, LIST, or NLST command.
callback: An optional single parameter callable that is called
for each line with the trailing CRLF stripped.
[default: print_line()]
@@ -429,20 +443,18 @@ class FTP:
"""
if callback is None: callback = print_line
resp = self.sendcmd('TYPE A')
- conn = self.transfercmd(cmd)
- fp = conn.makefile('rb')
- while 1:
- line = fp.readline()
- if self.debugging > 2: print '*retr*', repr(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()
+ with self.transfercmd(cmd) as conn, \
+ conn.makefile('r', encoding=self.encoding) as fp:
+ while 1:
+ line = fp.readline()
+ if self.debugging > 2: print('*retr*', repr(line))
+ if not line:
+ break
+ if line[-2:] == CRLF:
+ line = line[:-2]
+ elif line[-1:] == '\n':
+ line = line[:-1]
+ callback(line)
return self.voidresp()
def storbinary(self, cmd, fp, blocksize=8192, callback=None, rest=None):
@@ -461,13 +473,12 @@ class FTP:
The response code.
"""
self.voidcmd('TYPE I')
- conn = self.transfercmd(cmd, rest)
- while 1:
- buf = fp.read(blocksize)
- if not buf: break
- conn.sendall(buf)
- if callback: callback(buf)
- conn.close()
+ with self.transfercmd(cmd, rest) as conn:
+ while 1:
+ buf = fp.read(blocksize)
+ if not buf: break
+ conn.sendall(buf)
+ if callback: callback(buf)
return self.voidresp()
def storlines(self, cmd, fp, callback=None):
@@ -483,16 +494,15 @@ class FTP:
The response code.
"""
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.sendall(buf)
- if callback: callback(buf)
- conn.close()
+ with self.transfercmd(cmd) as conn:
+ while 1:
+ buf = fp.readline()
+ if not buf: break
+ if buf[-2:] != B_CRLF:
+ if buf[-1] in B_CRLF: buf = buf[:-1]
+ buf = buf + B_CRLF
+ conn.sendall(buf)
+ if callback: callback(buf)
return self.voidresp()
def acct(self, password):
@@ -524,27 +534,55 @@ class FTP:
cmd = cmd + (' ' + arg)
self.retrlines(cmd, func)
+ def mlsd(self, path="", facts=[]):
+ '''List a directory in a standardized format by using MLSD
+ command (RFC-3659). If path is omitted the current directory
+ is assumed. "facts" is a list of strings representing the type
+ of information desired (e.g. ["type", "size", "perm"]).
+
+ Return a generator object yielding a tuple of two elements
+ for every file found in path.
+ First element is the file name, the second one is a dictionary
+ including a variable number of "facts" depending on the server
+ and whether "facts" argument has been provided.
+ '''
+ if facts:
+ self.sendcmd("OPTS MLST " + ";".join(facts) + ";")
+ if path:
+ cmd = "MLSD %s" % path
+ else:
+ cmd = "MLSD"
+ lines = []
+ self.retrlines(cmd, lines.append)
+ for line in lines:
+ facts_found, _, name = line.rstrip(CRLF).partition(' ')
+ entry = {}
+ for fact in facts_found[:-1].split(";"):
+ key, _, value = fact.partition("=")
+ entry[key.lower()] = value
+ yield (name, entry)
+
def rename(self, fromname, toname):
'''Rename a file.'''
resp = self.sendcmd('RNFR ' + fromname)
if resp[0] != '3':
- raise error_reply, resp
+ raise error_reply(resp)
return self.voidcmd('RNTO ' + toname)
def delete(self, filename):
'''Delete a file.'''
resp = self.sendcmd('DELE ' + filename)
- if resp[:3] in ('250', '200'):
+ if resp[:3] in {'250', '200'}:
return resp
else:
- raise error_reply, resp
+ raise error_reply(resp)
def cwd(self, dirname):
'''Change to a directory.'''
if dirname == '..':
try:
return self.voidcmd('CDUP')
- except error_perm, msg:
+ except error_perm as msg:
if msg.args[0][:3] != '500':
raise
elif dirname == '':
@@ -558,14 +596,15 @@ class FTP:
resp = self.sendcmd('SIZE ' + filename)
if resp[:3] == '213':
s = resp[3:].strip()
- try:
- return int(s)
- except (OverflowError, ValueError):
- return long(s)
+ return int(s)
def mkd(self, dirname):
'''Make a directory, return its full pathname.'''
- resp = self.sendcmd('MKD ' + dirname)
+ resp = self.voidcmd('MKD ' + dirname)
+ # fix around non-compliant implementations such as IIS shipped
+ # with Windows server 2003
+ if not resp.startswith('257'):
+ return ''
return parse257(resp)
def rmd(self, dirname):
@@ -574,7 +613,11 @@ class FTP:
def pwd(self):
'''Return current working directory.'''
- resp = self.sendcmd('PWD')
+ resp = self.voidcmd('PWD')
+ # fix around non-compliant implementations such as IIS shipped
+ # with Windows server 2003
+ if not resp.startswith('257'):
+ return ''
return parse257(resp)
def quit(self):
@@ -632,11 +675,19 @@ else:
ssl_version = ssl.PROTOCOL_TLSv1
def __init__(self, host='', user='', passwd='', acct='', keyfile=None,
- certfile=None, timeout=_GLOBAL_DEFAULT_TIMEOUT):
+ certfile=None, context=None,
+ timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None):
+ if context is not None and keyfile is not None:
+ raise ValueError("context and keyfile arguments are mutually "
+ "exclusive")
+ if context is not None and certfile is not None:
+ raise ValueError("context and certfile arguments are mutually "
+ "exclusive")
self.keyfile = keyfile
self.certfile = certfile
+ self.context = context
self._prot_p = False
- FTP.__init__(self, host, user, passwd, acct, timeout)
+ FTP.__init__(self, host, user, passwd, acct, timeout, source_address)
def login(self, user='', passwd='', acct='', secure=True):
if secure and not isinstance(self.sock, ssl.SSLSocket):
@@ -651,9 +702,21 @@ else:
resp = self.voidcmd('AUTH TLS')
else:
resp = self.voidcmd('AUTH SSL')
- self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile,
- ssl_version=self.ssl_version)
- self.file = self.sock.makefile(mode='rb')
+ if self.context is not None:
+ self.sock = self.context.wrap_socket(self.sock)
+ else:
+ self.sock = ssl.wrap_socket(self.sock, self.keyfile,
+ self.certfile,
+ ssl_version=self.ssl_version)
+ self.file = self.sock.makefile(mode='r', encoding=self.encoding)
+ return resp
+
+ def ccc(self):
+ '''Switch back to a clear-text control connection.'''
+ if not isinstance(self.sock, ssl.SSLSocket):
+ raise ValueError("not using TLS")
+ resp = self.voidcmd('CCC')
+ self.sock = self.sock.unwrap()
return resp
def prot_p(self):
@@ -683,14 +746,16 @@ else:
def ntransfercmd(self, cmd, rest=None):
conn, size = FTP.ntransfercmd(self, cmd, rest)
if self._prot_p:
- conn = ssl.wrap_socket(conn, self.keyfile, self.certfile,
- ssl_version=self.ssl_version)
+ if self.context is not None:
+ conn = self.context.wrap_socket(conn)
+ else:
+ conn = ssl.wrap_socket(conn, self.keyfile, self.certfile,
+ ssl_version=self.ssl_version)
return conn, size
def retrbinary(self, cmd, callback, blocksize=8192, rest=None):
self.voidcmd('TYPE I')
- conn = self.transfercmd(cmd, rest)
- try:
+ with self.transfercmd(cmd, rest) as conn:
while 1:
data = conn.recv(blocksize)
if not data:
@@ -699,19 +764,17 @@ else:
# shutdown ssl layer
if isinstance(conn, ssl.SSLSocket):
conn.unwrap()
- finally:
- conn.close()
return self.voidresp()
def retrlines(self, cmd, callback = None):
if callback is None: callback = print_line
resp = self.sendcmd('TYPE A')
conn = self.transfercmd(cmd)
- fp = conn.makefile('rb')
- try:
+ fp = conn.makefile('r', encoding=self.encoding)
+ with fp, conn:
while 1:
line = fp.readline()
- if self.debugging > 2: print '*retr*', repr(line)
+ if self.debugging > 2: print('*retr*', repr(line))
if not line:
break
if line[-2:] == CRLF:
@@ -722,15 +785,11 @@ else:
# shutdown ssl layer
if isinstance(conn, ssl.SSLSocket):
conn.unwrap()
- finally:
- fp.close()
- conn.close()
return self.voidresp()
def storbinary(self, cmd, fp, blocksize=8192, callback=None, rest=None):
self.voidcmd('TYPE I')
- conn = self.transfercmd(cmd, rest)
- try:
+ with self.transfercmd(cmd, rest) as conn:
while 1:
buf = fp.read(blocksize)
if not buf: break
@@ -739,29 +798,33 @@ else:
# shutdown ssl layer
if isinstance(conn, ssl.SSLSocket):
conn.unwrap()
- finally:
- conn.close()
return self.voidresp()
def storlines(self, cmd, fp, callback=None):
self.voidcmd('TYPE A')
- conn = self.transfercmd(cmd)
- try:
+ with self.transfercmd(cmd) as conn:
while 1:
buf = fp.readline()
if not buf: break
- if buf[-2:] != CRLF:
- if buf[-1] in CRLF: buf = buf[:-1]
- buf = buf + CRLF
+ if buf[-2:] != B_CRLF:
+ if buf[-1] in B_CRLF: buf = buf[:-1]
+ buf = buf + B_CRLF
conn.sendall(buf)
if callback: callback(buf)
# shutdown ssl layer
if isinstance(conn, ssl.SSLSocket):
conn.unwrap()
- finally:
- conn.close()
return self.voidresp()
+ def abort(self):
+ # overridden as we can't pass MSG_OOB flag to sendall()
+ line = b'ABOR' + B_CRLF
+ self.sock.sendall(line)
+ resp = self.getmultiline()
+ if resp[:3] not in {'426', '225', '226'}:
+ raise error_proto(resp)
+ return resp
+
__all__.append('FTP_TLS')
all_errors = (Error, IOError, EOFError, ssl.SSLError)
@@ -774,19 +837,16 @@ def parse150(resp):
be present in the 150 message.
'''
if resp[:3] != '150':
- raise error_reply, resp
+ raise error_reply(resp)
global _150_re
if _150_re is None:
import re
- _150_re = re.compile("150 .* \((\d+) bytes\)", re.IGNORECASE)
+ _150_re = re.compile(
+ "150 .* \((\d+) bytes\)", re.IGNORECASE | re.ASCII)
m = _150_re.match(resp)
if not m:
return None
- s = m.group(1)
- try:
- return int(s)
- except (OverflowError, ValueError):
- return long(s)
+ return int(m.group(1))
_227_re = None
@@ -797,14 +857,14 @@ def parse227(resp):
Return ('host.addr.as.numbers', port#) tuple.'''
if resp[:3] != '227':
- raise error_reply, resp
+ raise error_reply(resp)
global _227_re
if _227_re is None:
import re
- _227_re = re.compile(r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)')
+ _227_re = re.compile(r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)', re.ASCII)
m = _227_re.search(resp)
if not m:
- raise error_proto, resp
+ raise error_proto(resp)
numbers = m.groups()
host = '.'.join(numbers[:4])
port = (int(numbers[4]) << 8) + int(numbers[5])
@@ -817,17 +877,17 @@ def parse229(resp, peer):
Return ('host.addr.as.numbers', port#) tuple.'''
if resp[:3] != '229':
- raise error_reply, resp
+ raise error_reply(resp)
left = resp.find('(')
- if left < 0: raise error_proto, resp
+ if left < 0: raise error_proto(resp)
right = resp.find(')', left + 1)
if right < 0:
- raise error_proto, resp # should contain '(|||port|)'
+ raise error_proto(resp) # should contain '(|||port|)'
if resp[left + 1] != resp[right - 1]:
- raise error_proto, resp
+ raise error_proto(resp)
parts = resp[left + 1:right].split(resp[left+1])
if len(parts) != 5:
- raise error_proto, resp
+ raise error_proto(resp)
host = peer[0]
port = int(parts[3])
return host, port
@@ -839,7 +899,7 @@ def parse257(resp):
Returns the directoryname in the 257 reply.'''
if resp[:3] != '257':
- raise error_reply, resp
+ raise error_reply(resp)
if resp[3:5] != ' "':
return '' # Not compliant to RFC 959, but UNIX ftpd does this
dirname = ''
@@ -858,7 +918,7 @@ def parse257(resp):
def print_line(line):
'''Default retrlines callback to print a line.'''
- print line
+ print(line)
def ftpcp(source, sourcename, target, targetname = '', type = 'I'):
@@ -873,9 +933,9 @@ def ftpcp(source, sourcename, target, targetname = '', type = 'I'):
# 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
+ 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
+ if sreply[:3] not in {'125', '150'}: raise error_proto # RFC 959
source.voidresp()
target.voidresp()
@@ -898,8 +958,7 @@ class Netrc:
filename = os.path.join(os.environ["HOME"],
".netrc")
else:
- raise IOError, \
- "specify file to load or set $HOME"
+ raise IOError("specify file to load or set $HOME")
self.__hosts = {}
self.__macros = {}
fp = open(filename, "r")
@@ -997,7 +1056,7 @@ def test():
'''
if len(sys.argv) < 2:
- print test.__doc__
+ print(test.__doc__)
sys.exit(0)
debugging = 0