#!/usr/bin/python # Recursively list FTP directory contents # # Copyright (C) 2013 Peter Wu # Prepend directory name to files and list files: # awk '/^Directory/{if($0 != "Directory /")dir=substr($0, index($0, "/"))}/^-/{n=index($0,$9);print substr($0,1, n - 1) dir "/" substr($0,n)}' from __future__ import print_function import sys from ftplib import FTP, all_errors import re from datetime import datetime, timezone, date # None = auto-detect (FEAT), True = use LIST, False = use MLSD use_list = None user = "anonymous" passwd = "anon" host = None port = 21 path = "/" patt_url = re.compile(r""" ^ (?:ftp://)? (?: (?P.+?) (?: :(?P.+?) )? @ )? (?P[a-zA-Z0-9.-]+) (:(?P\d+))? (?P/.*)? $ """, re.VERBOSE) if len(sys.argv) >= 2: m = patt_url.match(sys.argv[1]) if m: if m.group("user") is not None: user = m.group("user") if m.group("passwd") is not None: passwd = m.group("passwd") host = m.group("host") if m.group("port") is not None: port = int(m.group("port")) if m.group("path") is not None: path = m.group("path") if host is None: print("Usage: python", sys.argv[0], "[ftp://][user[:pass]@]ftp.example.com[:21][/path]", file=sys.stderr) sys.exit(1) # directories for which the contents needs to be retrieved dirs = [] dirs.append(path) patt_list = re.compile(r""" ^ .[-rwxXst]{9}\ + # permissions \d+\ + # links count \S+\ + # owner \S+\ + # group \d+\ + # size \S{3}\ + # month \d{2}\ + # day (?:\d{4,}|\d{2}:\d{2}) # year or time \ (.+?) # name (?:\ ->\ (.+))? # symlink [\r\n]* # line contains $ """, re.VERBOSE) def queue_dir(cwd, name): if cwd == "/": dir = "/" + name else: dir = cwd + "/" + name print("Queuing", dir, file=sys.stderr) dirs.append(dir) # LIST handler def get_ls_processor(cwd): def process_dir(line): print(line) if line[0] == 'd': m = patt_list.match(line) name = m.group(1) queue_dir(cwd, name) # elif line[0] == 'l': # m = patt_list.match(line) # print("Link", m.group(2)) return process_dir # MLSD handler def format_perms(mode): """Turns a numeric UNIX mode into human-readable form """ str = "" for i in range(0, 3): o = 0o100 >> (3 * i) str += "r" if mode & (4 * o) else "-" str += "w" if mode & (2 * o) else "-" if mode & (0o4000 >> i): # setuid, setgid or sticky bit if o == 0o001: # "world" str += "t" if mode & o else "T" else: str += "s" if mode & o else "S" else: str += "x" if mode & (1 * o) else "-" return str def format_type_fact(type): if type == "file": return "-" elif type in ("cdir", "pdir", "dir"): return "d" # TODO: handle OS.name=type else: return "?" def dt_from_ftp(timeval): timeval = timeval.split(".")[0] return datetime.strptime(timeval, "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc) def format_mlsd(name, facts): if "type" in facts: mode_desc = format_type_fact(facts["type"]) else: mode_desc = "?" if "unix.mode" in facts: perm = int(facts["unix.mode"], 8) mode_desc += format_perms(perm) else: mode_desc += "?" * 9 user = "?" if not "unix.owner" in facts else facts["unix.owner"] group = "?" if not "unix.group" in facts else facts["unix.group"] size = "" if not "size" in facts else int(facts["size"]) modtime = 0 if not "modify" in facts else dt_from_ftp(facts["modify"]) if date.today().year == modtime.year: date_str = modtime.strftime("%b %d %H:%M") else: date_str = modtime.strftime("%b %d %Y") line = mode_desc + " " #line += " {links:4s}".format(links=-1) line += " {user:8s} {group:8s} {size:8}".format(user=user, group=group, size=size) line += " " + date_str + " " + name return line def get_features(ftp): """Returns supported FTP features (FEAT) in a list""" # cut "211-Features" and "211 End" feats = ftp.sendcmd("FEAT").splitlines()[1:-1] return [ f.strip() for f in feats ] with FTP() as ftp: ftp.connect(host, port) ftp.login(user, passwd) print(ftp.getwelcome(), file=sys.stderr) feats = get_features(ftp) if use_list is None: # use list only if supported use_list = not any(f.startswith("MLST ") for f in feats) while dirs: dir = dirs.pop() try: ftp.cwd(dir) except all_errors as e: print("Failed to chdir to " + dir + ":" + str(e), file=sys.stderr) continue print("Directory", ftp.pwd()) if use_list: ftp.retrlines('LIST', get_ls_processor(dir)) else: for name, facts in ftp.mlsd(): if name not in (".", ".."): print(format_mlsd(name, facts)) if facts["type"] == "dir": queue_dir(dir, name) print("Queue size:", len(dirs), file=sys.stderr)