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