mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
Issue #11072: added MLSD command (RFC-3659) support to ftplib.
This commit is contained in:
parent
0872816dc1
commit
d78def9474
4 changed files with 139 additions and 8 deletions
|
@ -254,13 +254,12 @@ followed by ``lines`` for the text version or ``binary`` for the binary version.
|
||||||
|
|
||||||
Retrieve a file or directory listing in ASCII transfer mode. *cmd* should be
|
Retrieve a file or directory listing in ASCII transfer mode. *cmd* should be
|
||||||
an appropriate ``RETR`` command (see :meth:`retrbinary`) or a command such as
|
an appropriate ``RETR`` command (see :meth:`retrbinary`) or a command such as
|
||||||
``LIST``, ``NLST`` or ``MLSD`` (usually just the string ``'LIST'``).
|
``LIST`` or ``NLST`` (usually just the string ``'LIST'``).
|
||||||
``LIST`` retrieves a list of files and information about those files.
|
``LIST`` retrieves a list of files and information about those files.
|
||||||
``NLST`` retrieves a list of file names. On some servers, ``MLSD`` retrieves
|
``NLST`` retrieves a list of file names.
|
||||||
a machine readable list of files and information about those files. The
|
The *callback* function is called for each line with a string argument
|
||||||
*callback* function is called for each line with a string argument containing
|
containing the line with the trailing CRLF stripped. The default *callback*
|
||||||
the line with the trailing CRLF stripped. The default *callback* prints the
|
prints the line to ``sys.stdout``.
|
||||||
line to ``sys.stdout``.
|
|
||||||
|
|
||||||
|
|
||||||
.. method:: FTP.set_pasv(boolean)
|
.. method:: FTP.set_pasv(boolean)
|
||||||
|
@ -320,6 +319,20 @@ followed by ``lines`` for the text version or ``binary`` for the binary version.
|
||||||
in :meth:`transfercmd`.
|
in :meth:`transfercmd`.
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: FTP.mlsd(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.
|
||||||
|
|
||||||
|
.. versionadded:: 3.3
|
||||||
|
|
||||||
|
|
||||||
.. method:: FTP.nlst(argument[, ...])
|
.. method:: FTP.nlst(argument[, ...])
|
||||||
|
|
||||||
Return a list of file names as returned by the ``NLST`` command. The
|
Return a list of file names as returned by the ``NLST`` command. The
|
||||||
|
@ -327,6 +340,8 @@ followed by ``lines`` for the text version or ``binary`` for the binary version.
|
||||||
directory). Multiple arguments can be used to pass non-standard options to
|
directory). Multiple arguments can be used to pass non-standard options to
|
||||||
the ``NLST`` command.
|
the ``NLST`` command.
|
||||||
|
|
||||||
|
.. deprecated:: 3.3 use :meth:`mlsd` instead
|
||||||
|
|
||||||
|
|
||||||
.. method:: FTP.dir(argument[, ...])
|
.. method:: FTP.dir(argument[, ...])
|
||||||
|
|
||||||
|
@ -337,6 +352,8 @@ followed by ``lines`` for the text version or ``binary`` for the binary version.
|
||||||
as a *callback* function as for :meth:`retrlines`; the default prints to
|
as a *callback* function as for :meth:`retrlines`; the default prints to
|
||||||
``sys.stdout``. This method returns ``None``.
|
``sys.stdout``. This method returns ``None``.
|
||||||
|
|
||||||
|
.. deprecated:: 3.3 use :meth:`mlsd` instead
|
||||||
|
|
||||||
|
|
||||||
.. method:: FTP.rename(fromname, toname)
|
.. method:: FTP.rename(fromname, toname)
|
||||||
|
|
||||||
|
|
|
@ -426,7 +426,7 @@ class FTP:
|
||||||
"""Retrieve data in line mode. A new port is created for you.
|
"""Retrieve data in line mode. A new port is created for you.
|
||||||
|
|
||||||
Args:
|
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
|
callback: An optional single parameter callable that is called
|
||||||
for each line with the trailing CRLF stripped.
|
for each line with the trailing CRLF stripped.
|
||||||
[default: print_line()]
|
[default: print_line()]
|
||||||
|
@ -527,6 +527,34 @@ class FTP:
|
||||||
cmd = cmd + (' ' + arg)
|
cmd = cmd + (' ' + arg)
|
||||||
self.retrlines(cmd, func)
|
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):
|
def rename(self, fromname, toname):
|
||||||
'''Rename a file.'''
|
'''Rename a file.'''
|
||||||
resp = self.sendcmd('RNFR ' + fromname)
|
resp = self.sendcmd('RNFR ' + fromname)
|
||||||
|
|
|
@ -22,10 +22,25 @@ from test.support import HOST
|
||||||
threading = support.import_module('threading')
|
threading = support.import_module('threading')
|
||||||
|
|
||||||
# the dummy data returned by server over the data channel when
|
# the dummy data returned by server over the data channel when
|
||||||
# RETR, LIST and NLST commands are issued
|
# RETR, LIST, NLST, MLSD commands are issued
|
||||||
RETR_DATA = 'abcde12345\r\n' * 1000
|
RETR_DATA = 'abcde12345\r\n' * 1000
|
||||||
LIST_DATA = 'foo\r\nbar\r\n'
|
LIST_DATA = 'foo\r\nbar\r\n'
|
||||||
NLST_DATA = 'foo\r\nbar\r\n'
|
NLST_DATA = 'foo\r\nbar\r\n'
|
||||||
|
MLSD_DATA = ("type=cdir;perm=el;unique==keVO1+ZF4; test\r\n"
|
||||||
|
"type=pdir;perm=e;unique==keVO1+d?3; ..\r\n"
|
||||||
|
"type=OS.unix=slink:/foobar;perm=;unique==keVO1+4G4; foobar\r\n"
|
||||||
|
"type=OS.unix=chr-13/29;perm=;unique==keVO1+5G4; device\r\n"
|
||||||
|
"type=OS.unix=blk-11/108;perm=;unique==keVO1+6G4; block\r\n"
|
||||||
|
"type=file;perm=awr;unique==keVO1+8G4; writable\r\n"
|
||||||
|
"type=dir;perm=cpmel;unique==keVO1+7G4; promiscuous\r\n"
|
||||||
|
"type=dir;perm=;unique==keVO1+1t2; no-exec\r\n"
|
||||||
|
"type=file;perm=r;unique==keVO1+EG4; two words\r\n"
|
||||||
|
"type=file;perm=r;unique==keVO1+IH4; leading space\r\n"
|
||||||
|
"type=file;perm=r;unique==keVO1+1G4; file1\r\n"
|
||||||
|
"type=dir;perm=cpmel;unique==keVO1+7G4; incoming\r\n"
|
||||||
|
"type=file;perm=r;unique==keVO1+1G4; file2\r\n"
|
||||||
|
"type=file;perm=r;unique==keVO1+1G4; file3\r\n"
|
||||||
|
"type=file;perm=r;unique==keVO1+1G4; file4\r\n")
|
||||||
|
|
||||||
|
|
||||||
class DummyDTPHandler(asynchat.async_chat):
|
class DummyDTPHandler(asynchat.async_chat):
|
||||||
|
@ -49,6 +64,11 @@ class DummyDTPHandler(asynchat.async_chat):
|
||||||
self.dtp_conn_closed = True
|
self.dtp_conn_closed = True
|
||||||
|
|
||||||
def push(self, what):
|
def push(self, what):
|
||||||
|
if self.baseclass.next_data is not None:
|
||||||
|
what = self.baseclass.next_data
|
||||||
|
self.baseclass.next_data = None
|
||||||
|
if not what:
|
||||||
|
return self.close_when_done()
|
||||||
super(DummyDTPHandler, self).push(what.encode('ascii'))
|
super(DummyDTPHandler, self).push(what.encode('ascii'))
|
||||||
|
|
||||||
def handle_error(self):
|
def handle_error(self):
|
||||||
|
@ -67,6 +87,7 @@ class DummyFTPHandler(asynchat.async_chat):
|
||||||
self.last_received_cmd = None
|
self.last_received_cmd = None
|
||||||
self.last_received_data = ''
|
self.last_received_data = ''
|
||||||
self.next_response = ''
|
self.next_response = ''
|
||||||
|
self.next_data = None
|
||||||
self.rest = None
|
self.rest = None
|
||||||
self.push('220 welcome')
|
self.push('220 welcome')
|
||||||
|
|
||||||
|
@ -208,6 +229,14 @@ class DummyFTPHandler(asynchat.async_chat):
|
||||||
self.dtp.push(NLST_DATA)
|
self.dtp.push(NLST_DATA)
|
||||||
self.dtp.close_when_done()
|
self.dtp.close_when_done()
|
||||||
|
|
||||||
|
def cmd_opts(self, arg):
|
||||||
|
self.push('200 opts ok')
|
||||||
|
|
||||||
|
def cmd_mlsd(self, arg):
|
||||||
|
self.push('125 mlsd ok')
|
||||||
|
self.dtp.push(MLSD_DATA)
|
||||||
|
self.dtp.close_when_done()
|
||||||
|
|
||||||
|
|
||||||
class DummyFTPServer(asyncore.dispatcher, threading.Thread):
|
class DummyFTPServer(asyncore.dispatcher, threading.Thread):
|
||||||
|
|
||||||
|
@ -550,6 +579,61 @@ class TestFTPClass(TestCase):
|
||||||
self.client.dir(lambda x: l.append(x))
|
self.client.dir(lambda x: l.append(x))
|
||||||
self.assertEqual(''.join(l), LIST_DATA.replace('\r\n', ''))
|
self.assertEqual(''.join(l), LIST_DATA.replace('\r\n', ''))
|
||||||
|
|
||||||
|
def test_mlsd(self):
|
||||||
|
list(self.client.mlsd())
|
||||||
|
list(self.client.mlsd(path='/'))
|
||||||
|
list(self.client.mlsd(path='/', facts=['size', 'type']))
|
||||||
|
|
||||||
|
ls = list(self.client.mlsd())
|
||||||
|
for name, facts in ls:
|
||||||
|
self.assertTrue(name)
|
||||||
|
self.assertTrue('type' in facts)
|
||||||
|
self.assertTrue('perm' in facts)
|
||||||
|
self.assertTrue('unique' in facts)
|
||||||
|
|
||||||
|
def set_data(data):
|
||||||
|
self.server.handler_instance.next_data = data
|
||||||
|
|
||||||
|
def test_entry(line, type=None, perm=None, unique=None, name=None):
|
||||||
|
type = 'type' if type is None else type
|
||||||
|
perm = 'perm' if perm is None else perm
|
||||||
|
unique = 'unique' if unique is None else unique
|
||||||
|
name = 'name' if name is None else name
|
||||||
|
set_data(line)
|
||||||
|
_name, facts = next(self.client.mlsd())
|
||||||
|
self.assertEqual(_name, name)
|
||||||
|
self.assertEqual(facts['type'], type)
|
||||||
|
self.assertEqual(facts['perm'], perm)
|
||||||
|
self.assertEqual(facts['unique'], unique)
|
||||||
|
|
||||||
|
# plain
|
||||||
|
test_entry('type=type;perm=perm;unique=unique; name\r\n')
|
||||||
|
# "=" in fact value
|
||||||
|
test_entry('type=ty=pe;perm=perm;unique=unique; name\r\n', type="ty=pe")
|
||||||
|
test_entry('type==type;perm=perm;unique=unique; name\r\n', type="=type")
|
||||||
|
test_entry('type=t=y=pe;perm=perm;unique=unique; name\r\n', type="t=y=pe")
|
||||||
|
test_entry('type=====;perm=perm;unique=unique; name\r\n', type="====")
|
||||||
|
# spaces in name
|
||||||
|
test_entry('type=type;perm=perm;unique=unique; na me\r\n', name="na me")
|
||||||
|
test_entry('type=type;perm=perm;unique=unique; name \r\n', name="name ")
|
||||||
|
test_entry('type=type;perm=perm;unique=unique; name\r\n', name=" name")
|
||||||
|
test_entry('type=type;perm=perm;unique=unique; n am e\r\n', name="n am e")
|
||||||
|
# ";" in name
|
||||||
|
test_entry('type=type;perm=perm;unique=unique; na;me\r\n', name="na;me")
|
||||||
|
test_entry('type=type;perm=perm;unique=unique; ;name\r\n', name=";name")
|
||||||
|
test_entry('type=type;perm=perm;unique=unique; ;name;\r\n', name=";name;")
|
||||||
|
test_entry('type=type;perm=perm;unique=unique; ;;;;\r\n', name=";;;;")
|
||||||
|
# case sensitiveness
|
||||||
|
set_data('Type=type;TyPe=perm;UNIQUE=unique; name\r\n')
|
||||||
|
_name, facts = next(self.client.mlsd())
|
||||||
|
[self.assertTrue(x.islower()) for x in facts.keys()]
|
||||||
|
# no data (directory empty)
|
||||||
|
set_data('')
|
||||||
|
self.assertRaises(StopIteration, next, self.client.mlsd())
|
||||||
|
set_data('')
|
||||||
|
for x in self.client.mlsd():
|
||||||
|
self.fail("unexpected data %s" % data)
|
||||||
|
|
||||||
def test_makeport(self):
|
def test_makeport(self):
|
||||||
with self.client.makeport():
|
with self.client.makeport():
|
||||||
# IPv4 is in use, just make sure send_eprt has not been used
|
# IPv4 is in use, just make sure send_eprt has not been used
|
||||||
|
|
|
@ -140,6 +140,8 @@ Core and Builtins
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #11072: added MLSD command (RFC-3659) support to ftplib.
|
||||||
|
|
||||||
- Issue #8808: The IMAP4_SSL constructor now allows passing an SSLContext
|
- Issue #8808: The IMAP4_SSL constructor now allows passing an SSLContext
|
||||||
parameter to control parameters of the secure channel. Patch by Sijin
|
parameter to control parameters of the secure channel. Patch by Sijin
|
||||||
Joseph.
|
Joseph.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue