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