Kaydet (Commit) d78def94 authored tarafından Giampaolo Rodola''s avatar Giampaolo Rodola'

Issue #11072: added MLSD command (RFC-3659) support to ftplib.

üst 0872816d
...@@ -254,13 +254,12 @@ followed by ``lines`` for the text version or ``binary`` for the binary version. ...@@ -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. ...@@ -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. ...@@ -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. ...@@ -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: ...@@ -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: ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment