]> git.phdru.name Git - m_lib.git/blob - m_lib/net/ftp/ftpscan.py
319660be9d2147c8090f86dd69dd1daa32b9cfe6
[m_lib.git] / m_lib / net / ftp / ftpscan.py
1 #! /usr/bin/env python
2 """Recursive FTP scanners"""
3
4
5 import ftplib
6 from m_lib.net.ftp.ftpparse import ftpparse
7
8
9 class FtpScanError(Exception): pass
10 ftpscan_error_mark = object() # error marker
11
12
13 class GetFiles:
14    def __init__(self):
15       self.entries = []
16
17    def __call__(self, line):
18       entry = ftpparse(line)
19       if entry:
20          self.entries.append(entry)
21
22    def filter(self, file_type):
23       return filter(lambda e, file_type=file_type: e.file_type == file_type,
24          self.entries)
25
26    def files(self):
27       return self.filter('f')
28
29    def directories(self):
30       return filter(lambda e: e.name not in (".", ".."), self.filter('d'))
31
32
33 class ReconnectingFTPCallWrapper:
34    retries = 10 # retries per function call
35
36    def __init__(self, wrapper, func):
37       self.wrapper = wrapper
38       self.func = func
39
40    def __call__(self, *params, **kw):
41       wrapper = self.wrapper
42       func = self.func
43
44       for retry in range(self.retries):
45          try:
46             return func(*params, **kw)
47          except EOFError:
48             pass
49
50          ftp_dir = wrapper._ftp_dir
51          wrapper._tree.append((ftpscan_error_mark, "Connection reset by peer at directory `%s'. Reconnecting..." % ftp_dir))
52
53          ftp = wrapper._ftp
54          ftp.close()
55
56          ftp.connect(wrapper._ftp_server, wrapper._ftp_port)
57          ftp.login(wrapper._login, wrapper._password)
58          ftp.cwd(ftp_dir)
59
60 class ReconnectingFTPWrapper:
61    ReconnectingFTPCallWrapperClass = ReconnectingFTPCallWrapper
62
63    def __init__(self, ftp, ftp_server, ftp_port=None, login=None, password=None, ftp_dir='/', tree=None):
64       self._ftp = ftp
65       self._ftp_server = ftp_server
66       self._ftp_port = ftp_port
67       self._login = login
68       self._password = password
69       ftp_dir = [''] + [name for name in ftp_dir.split('/') if name] # remove double slashes //
70       self._ftp_dir = '/'.join(ftp_dir)
71       self._tree = tree
72
73    def cwd(self, new_cwd, do_ftp=True):
74       ftp_dir = self._ftp_dir.split('/')
75       if new_cwd == "..":
76          del ftp_dir[-1]
77       else:
78          ftp_dir.append(new_cwd)
79       self._ftp_dir = '/'.join(ftp_dir)
80       if do_ftp: self._wrap(self._ftp.cwd)(new_cwd)
81
82    def __getattr__(self, attr):
83       value = getattr(self._ftp, attr)
84       if callable(value):
85          return self._wrap(value)
86       return value
87
88    def _wrap(self, func):
89       return self.ReconnectingFTPCallWrapperClass(self, func)
90
91
92 def _traverse_ftp(ftp, tree, ftp_dir):
93    get_files = GetFiles()
94    try:
95       ftp.dir(get_files)
96    except ftplib.all_errors, msg:
97       tree.append((ftpscan_error_mark, "Cannot list directory `%s': %s" % (ftp_dir, msg)))
98       return
99    files = get_files.files()
100    directories = get_files.directories()
101
102    if ftp_dir and ftp_dir[-1] == '/':
103       ftp_dir = ftp_dir[:-1] # Prevent paths to contain double slashes //
104
105    tree.append((ftp_dir, files))
106
107    for d in directories:
108       name = d.name
109       full_path = ftp_dir + '/' + name
110       try:
111          ftp.cwd(name)
112       except ftplib.error_perm, msg:
113          tree.append((ftpscan_error_mark, "Cannot enter directory `%s': %s" % (full_path, msg)))
114          if isinstance(ftp, ReconnectingFTPWrapper):
115             ftp.cwd("..", False)
116       except ftplib.all_errors, msg:
117          tree.append((ftpscan_error_mark, "Cannot enter directory `%s': %s" % (full_path, msg)))
118       else:
119          _traverse_ftp(ftp, tree, full_path)
120          ftp.cwd("..")
121
122
123 def ftpscan1(ftp_server, ftp_port=None, login=None, password=None,
124       ftp_dir='/', passive=None, FTPClass=ftplib.FTP, reconnect=False,
125       ReconnectingFTPWrapperClass=ReconnectingFTPWrapper):
126    """Recursive FTP scan using one-by-one directory traversing. It is slow
127    but robust - it works with all but very broken FTP servers.
128    """
129    tree = []
130    ftp = FTPClass()
131    if passive is not None:
132       ftp.set_pasv(passive)
133    if reconnect:
134       ftp = ReconnectingFTPWrapperClass(ftp, ftp_server, ftp_port, login, password, ftp_dir, tree)
135    ftp.connect(ftp_server, ftp_port)
136    ftp.login(login, password)
137    if ftp_dir <> '/':
138       ftp.cwd(ftp_dir)
139
140    _traverse_ftp(ftp, tree, ftp_dir)
141    ftp.quit()
142
143    return tree
144
145
146 def ftpscanrecursive(ftp_server, ftp_port=None, login=None, password=None,
147       ftp_dir='/', passive=None, FTPClass=ftplib.FTP, reconnect=False):
148    """
149    Recursive FTP scan using fast LIST -R command. Not all servers supports
150    this, though.
151    """
152    ftp = FTPClass()
153    if passive is not None:
154       ftp.set_pasv(passive)
155    ftp.connect(ftp_server, ftp_port)
156    ftp.login(login, password)
157    if ftp_dir <> '/':
158       ftp.cwd(ftp_dir)
159
160    lines = []
161    try:
162       ftp.dir("-R", lines.append)
163    except ftplib.error_perm:
164       # The server does not implement LIST -R and
165       # treats -R as a name of a directory (-:
166       ftp.quit()
167       raise FtpScanError, "the server does not implement recursive listing"
168    ftp.quit()
169
170    tree = []
171    current_dir = ftp_dir
172    files = []
173
174    for line in lines:
175       if line:
176          if line[-1] == ':' and not line.startswith("-rw-"): # directory
177             tree.append((current_dir, files))
178             if line[:2] == "./":
179                line = line[1:] # remove leading dot
180             elif line[0] <> '/':
181                line = '/' + line
182             current_dir = line[:-1]
183             files = []
184          else:
185             if not line.startswith("total "):
186                entry = ftpparse(line)
187                if entry:
188                   if entry.file_type == 'f':
189                      files.append(entry)
190                else:
191                   tree.append((ftpscan_error_mark, "Unrecognised line: `%s'" % line))
192    tree.append((current_dir, files))
193
194    if len(tree) == 1:
195       raise FtpScanError, "the server ignores -R in LIST"
196
197    return tree
198
199
200 def ftpscan(ftp_server, ftp_port=None, login=None, password=None,
201       ftp_dir='/', passive=None, FTPClass=ftplib.FTP):
202    try:
203       return ftpscanrecursive(ftp_server, ftp_port, login, password, ftp_dir, passive, FTPClass)
204    except FtpScanError:
205       try:
206          return ftpscan1(ftp_server, ftp_port, login, password, ftp_dir, passive, FTPClass)
207       except EOFError:
208          return ftpscan1(ftp_server, ftp_port, login, password, ftp_dir, passive, FTPClass, True)
209    except EOFError:
210       return ftpscan1(ftp_server, ftp_port, login, password, ftp_dir, passive, FTPClass, True)
211
212
213 def test(ftp_server, func, passive=None, reconnect=False):
214    from time import time
215    start_time = time()
216
217    tree = func(ftp_server, passive=passive, reconnect=reconnect)
218
219    stop_time = time()
220    print stop_time - start_time
221
222    logfname = "%s.list" % ftp_server
223    log = open(logfname, 'w')
224
225    for ftp_dir, files in tree:
226       if ftp_dir == ftpscan_error_mark:
227          log.write("Error:\n")
228          log.write(files)
229          log.write('\n')
230       else:
231          log.write(ftp_dir + '\n')
232          for _file in files:
233             log.write("    ")
234             log.write(_file.name + '\n')
235
236
237 def usage(code=0):
238    sys.stderr.write("Usage: %s [-a|-p] [hostname]\n" % sys.argv[0])
239    sys.exit(code)
240
241 if __name__ == "__main__":
242    import sys
243    from getopt import getopt, GetoptError
244
245    try:
246       options, arguments = getopt(sys.argv[1:], "hap",
247          ["help", "active", "passive"])
248    except GetoptError:
249       usage(1)
250
251    passive = None
252
253    for option, value in options:
254       if option in ("-h", "--help"):
255          usage()
256       elif option in ("-a", "--active"):
257          passive = False
258       elif option in ("-p", "--passive"):
259          passive = True
260       else:
261          usage(2)
262
263    l = len(arguments)
264    if (l == 0):
265       ftp_server = "localhost"
266    elif l > 1:
267       usage()
268    else:
269       ftp_server = arguments[0]
270
271    print "Scanning", ftp_server
272    try:
273       test(ftp_server, ftpscanrecursive, passive)
274    except FtpScanError, msg:
275       print "Rescanning due to the error:", msg
276       try:
277          test(ftp_server, ftpscan1, passive)
278       except EOFError:
279          print "Rescanning due to the error: connection reset by peer"
280          test(ftp_server, ftpscan1, passive, True)
281    except EOFError:
282       print "Rescanning due to the error: connection reset by peer"
283       test(ftp_server, ftpscan1, passive, True)