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