5
\$\begingroup\$

Today I found myself in need of programmatically getting a recursive list of the files on a remote FTP server to look for some data archives. Much to my surprise, I could not find this functionality implemented in some kind of Python library. So I decided to write a simple recursive version of ftplib.mlsd, that can be found below:

ftphelper.py

def recursive_mlsd(ftp_object, path="", maxdepth=None):
    """Run the FTP's MLSD command recursively

    The MLSD is returned as a list of tuples with (name, properties) for each
    object found on the FTP server. This function adds the non-standard
    property "children" which is then again an MLSD listing, possibly with more
    "children".

    Parameters
    ----------
    ftp_object: ftplib.FTP or ftplib.FTP_TLS
        the (authenticated) FTP client object used to make the calls to the
        server
    path: str
        path to start the recursive listing from
    maxdepth: {None, int}, optional
        maximum recursion depth, has to be >= 0 or None (i.e. no limit).

    Returns
    -------
    list
        the recursive directory listing

    See also
    --------
    ftplib.FTP.mlsd : the non-recursive version of this function
    """
    if maxdepth is not None:
        maxdepth = int(maxdepth)
        if maxdepth < 0:
            raise ValueError("maxdepth is supposed to be >= 0")

    def _inner(path_, depth_):
        if maxdepth is not None and depth_ > maxdepth:
            return

        inner_mlsd = list(ftp_object.mlsd(path=path_))
        for name, properties in inner_mlsd:
            if properties["type"] == "dir":
                rec_path = path_+"/"+name if path_ else name
                res = _inner(rec_path, depth_+1)
                if res is not None:
                    properties["children"] = res

        return inner_mlsd

    return _inner(path, 0)

A very basic example usage could be like:

import ftplib
import getpass
import pprint
import sys

import ftphelper


host = "ftp1.at.proftpd.org"   # from the Python docs
directory = "historic"
print(f"Login to '{host}' to list '{directory}'", file=sys.stderr)

default_user = ""   # getpass.getuser() could also be used where appropriate
prompt = f"Username (default: '{default_user}'): "
if sys.stdout.isatty():
    user = input(prompt)
else:
    print(prompt, end="", file=sys.stderr)
    user = input()
user = user.strip()
user = user if user else default_user

with ftplib.FTP(host=host) as ftp:
    ftp.login(user=user, passwd=getpass.getpass())
    listing = ftphelper.recursive_mlsd(ftp, directory)

pp = pprint.PrettyPrinter(indent=2)
pp.pprint(listing)

As always, every kind of feedback is welcome and much appreciated.

\$\endgroup\$
5
\$\begingroup\$

Seems good except for a few small improvements:

  • path_+"/"+name is better perceived with f-string formatting: f'{path_}/{name}
  • user if user else default_user is a verbose version of user or default_user
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ user or default_user is a cool "trick". Will definitely use it in the future! \$\endgroup\$ – AlexV Nov 19 '19 at 20:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.