eric6/Helpviewer/Network/FtpReply.py

changeset 6942
2602857055c5
parent 6891
93f82da09f22
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2010 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a network reply class for FTP resources.
8 """
9
10 from __future__ import unicode_literals
11 try:
12 str = unicode
13 except NameError:
14 pass
15
16 import ftplib
17 import socket
18 import errno
19 import mimetypes
20
21 from PyQt5.QtCore import QByteArray, QIODevice, Qt, QUrl, QTimer, QBuffer, \
22 QCoreApplication
23 from PyQt5.QtGui import QPixmap
24 from PyQt5.QtWidgets import QDialog
25 from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest, QAuthenticator
26 from PyQt5.QtWebKit import QWebSettings
27
28 from E5Network.E5Ftp import E5Ftp, E5FtpProxyError, E5FtpProxyType
29
30 import UI.PixmapCache
31
32 from Utilities.FtpUtilities import FtpDirLineParser, FtpDirLineParserError
33 import Utilities
34
35 import Preferences
36
37 ftpListPage_html = """\
38 <?xml version="1.0" encoding="UTF-8" ?>
39 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
40 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
41 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
42 <head>
43 <title>{0}</title>
44 <style type="text/css">
45 body {{
46 padding: 3em 0em;
47 background: -webkit-gradient(linear, left top, left bottom, from(#85784A),
48 to(#FDFDFD), color-stop(0.5, #FDFDFD));
49 background-repeat: repeat-x;
50 }}
51 #box {{
52 background: white;
53 border: 1px solid #85784A;
54 width: 80%;
55 padding: 30px;
56 margin: auto;
57 -webkit-border-radius: 0.8em;
58 }}
59 h1 {{
60 font-size: 130%;
61 font-weight: bold;
62 border-bottom: 1px solid #85784A;
63 }}
64 th {{
65 background-color: #B8B096;
66 color: black;
67 }}
68 table {{
69 border: solid 1px #85784A;
70 margin: 5px 0;
71 width: 100%;
72 }}
73 tr.odd {{
74 background-color: white;
75 color: black;
76 }}
77 tr.even {{
78 background-color: #CEC9B8;
79 color: black;
80 }}
81 .modified {{
82 text-align: left;
83 vertical-align: top;
84 white-space: nowrap;
85 }}
86 .size {{
87 text-align: right;
88 vertical-align: top;
89 white-space: nowrap;
90 padding-right: 22px;
91 }}
92 .name {{
93 text-align: left;
94 vertical-align: top;
95 white-space: pre-wrap;
96 width: 100%
97 }}
98 {1}
99 </style>
100 </head>
101 <body>
102 <div id="box">
103 <h1>{2}</h1>
104 {3}
105 <table align="center" cellspacing="0" width="90%">
106 {4}
107 </table>
108 </div>
109 </body>
110 </html>
111 """
112
113
114 class FtpReply(QNetworkReply):
115 """
116 Class implementing a network reply for FTP resources.
117 """
118 def __init__(self, url, accessHandler, parent=None):
119 """
120 Constructor
121
122 @param url requested FTP URL (QUrl)
123 @param accessHandler reference to the access handler (FtpAccessHandler)
124 @param parent reference to the parent object (QObject)
125 """
126 super(FtpReply, self).__init__(parent)
127
128 self.__manager = parent
129 self.__handler = accessHandler
130
131 self.__ftp = E5Ftp()
132
133 self.__items = []
134 self.__content = QByteArray()
135 self.__units = ["Bytes", "KB", "MB", "GB", "TB",
136 "PB", "EB", "ZB", "YB"]
137 self.__dirLineParser = FtpDirLineParser()
138 self.__fileBytesReceived = 0
139
140 if url.path() == "":
141 url.setPath("/")
142 self.setUrl(url)
143
144 # do proxy setup
145 if not Preferences.getUI("UseProxy"):
146 proxyType = E5FtpProxyType.NoProxy
147 else:
148 proxyType = Preferences.getUI("ProxyType/Ftp")
149 if proxyType != E5FtpProxyType.NoProxy:
150 self.__ftp.setProxy(
151 proxyType,
152 Preferences.getUI("ProxyHost/Ftp"),
153 Preferences.getUI("ProxyPort/Ftp"))
154 if proxyType != E5FtpProxyType.NonAuthorizing:
155 self.__ftp.setProxyAuthentication(
156 Preferences.getUI("ProxyUser/Ftp"),
157 Preferences.getUI("ProxyPassword/Ftp"),
158 Preferences.getUI("ProxyAccount/Ftp"))
159
160 QTimer.singleShot(0, self.__doFtpCommands)
161
162 def abort(self):
163 """
164 Public slot to abort the operation.
165 """
166 # do nothing
167 pass
168
169 def bytesAvailable(self):
170 """
171 Public method to determined the bytes available for being read.
172
173 @return bytes available (integer)
174 """
175 return self.__content.size()
176
177 def isSequential(self):
178 """
179 Public method to check for sequential access.
180
181 @return flag indicating sequential access (boolean)
182 """
183 return True
184
185 def readData(self, maxlen):
186 """
187 Public method to retrieve data from the reply object.
188
189 @param maxlen maximum number of bytes to read (integer)
190 @return string containing the data (bytes)
191 """
192 if self.__content.size():
193 len_ = min(maxlen, self.__content.size())
194 buffer = bytes(self.__content[:len_])
195 self.__content.remove(0, len_)
196 return buffer
197 else:
198 return b""
199
200 def __doFtpCommands(self):
201 """
202 Private slot doing the sequence of FTP commands to get the requested
203 result.
204 """
205 retry = True
206 try:
207 username = self.url().userName()
208 password = self.url().password()
209 byAuth = False
210
211 while retry:
212 try:
213 self.__ftp.connect(self.url().host(),
214 self.url().port(ftplib.FTP_PORT),
215 timeout=10)
216 except E5FtpProxyError as err:
217 self.setError(QNetworkReply.ProxyNotFoundError, str(err))
218 self.error.emit(QNetworkReply.ProxyNotFoundError)
219 self.finished.emit()
220 ok, retry = self.__doFtpLogin(username, password, byAuth)
221 if not ok and retry:
222 auth = self.__handler.getAuthenticator(self.url().host())
223 if auth and not auth.isNull() and auth.user():
224 username = auth.user()
225 password = auth.password()
226 byAuth = True
227 else:
228 retry = False
229 if ok:
230 self.__ftp.retrlines("LIST " + self.url().path(),
231 self.__dirCallback)
232 if len(self.__items) == 1 and \
233 self.__items[0].isFile():
234 self.__fileBytesReceived = 0
235 self.__setContent()
236 self.__ftp.retrbinary(
237 "RETR " + self.url().path(), self.__retrCallback)
238 self.__content.append(512 * b' ')
239 self.readyRead.emit()
240 else:
241 self.__setListContent()
242 self.__ftp.quit()
243 except ftplib.all_errors as err:
244 if isinstance(err, socket.gaierror):
245 errCode = QNetworkReply.HostNotFoundError
246 elif isinstance(err, socket.error) and \
247 err.errno == errno.ECONNREFUSED:
248 errCode = QNetworkReply.ConnectionRefusedError
249 else:
250 errCode = QNetworkReply.ProtocolFailure
251 self.setError(errCode, str(err))
252 self.error.emit(errCode)
253 self.finished.emit()
254
255 def __doFtpLogin(self, username, password, byAuth=False):
256 """
257 Private method to do the FTP login with asking for a username and
258 password, if the login fails with an error 530.
259
260 @param username user name to use for the login (string)
261 @param password password to use for the login (string)
262 @param byAuth flag indicating that the login data was provided by an
263 authenticator (boolean)
264 @return tuple of two flags indicating a successful login and
265 if the login should be retried (boolean, boolean)
266 """
267 try:
268 self.__ftp.login(username, password)
269 return True, False
270 except E5FtpProxyError as err:
271 code = str(err)[:3]
272 if code[1] == "5":
273 # could be a 530, check second line
274 lines = str(err).splitlines()
275 if lines[1][:3] == "530":
276 if "usage" in "\n".join(lines[1:].lower()):
277 # found a not supported proxy
278 self.setError(
279 QNetworkReply.ProxyConnectionRefusedError,
280 self.tr("The proxy type seems to be wrong."
281 " If it is not in the list of"
282 " supported proxy types please report"
283 " it with the instructions given by"
284 " the proxy.\n{0}").format(
285 "\n".join(lines[1:])))
286 self.error.emit(
287 QNetworkReply.ProxyConnectionRefusedError)
288 return False, False
289 else:
290 from UI.AuthenticationDialog import \
291 AuthenticationDialog
292 info = self.tr(
293 "<b>Connect to proxy '{0}' using:</b>")\
294 .format(Utilities.html_encode(
295 Preferences.getUI("ProxyHost/Ftp")))
296 dlg = AuthenticationDialog(
297 info, Preferences.getUI("ProxyUser/Ftp"), True)
298 dlg.setData(Preferences.getUI("ProxyUser/Ftp"),
299 Preferences.getUI("ProxyPassword/Ftp"))
300 if dlg.exec_() == QDialog.Accepted:
301 username, password = dlg.getData()
302 if dlg.shallSave():
303 Preferences.setUI("ProxyUser/Ftp", username)
304 Preferences.setUI(
305 "ProxyPassword/Ftp", password)
306 self.__ftp.setProxyAuthentication(username,
307 password)
308 return False, True
309 return False, False
310 except (ftplib.error_perm, ftplib.error_temp) as err:
311 code = err.args[0].strip()[:3]
312 if code in ["530", "421"]:
313 # error 530 -> Login incorrect
314 # error 421 -> Login may be incorrect (reported by some
315 # proxies)
316 if byAuth:
317 self.__handler.setAuthenticator(self.url().host(), None)
318 auth = None
319 else:
320 auth = self.__handler.getAuthenticator(self.url().host())
321 if not auth or auth.isNull() or not auth.user():
322 auth = QAuthenticator()
323 self.__manager.authenticationRequired.emit(self, auth)
324 if not auth.isNull():
325 if auth.user():
326 self.__handler.setAuthenticator(self.url().host(),
327 auth)
328 return False, True
329 return False, False
330 return False, True
331 else:
332 raise
333
334 def __dirCallback(self, line):
335 """
336 Private slot handling the receipt of directory listings.
337
338 @param line the received line of the directory listing (string)
339 """
340 try:
341 urlInfo = self.__dirLineParser.parseLine(line)
342 except FtpDirLineParserError:
343 # silently ignore parser errors
344 urlInfo = None
345
346 if urlInfo:
347 self.__items.append(urlInfo)
348
349 QCoreApplication.processEvents()
350
351 def __retrCallback(self, data):
352 """
353 Private slot handling the reception of data.
354
355 @param data data received from the FTP server (bytes)
356 """
357 self.__content += QByteArray(data)
358 self.__fileBytesReceived += len(data)
359 self.downloadProgress.emit(
360 self.__fileBytesReceived, self.__items[0].size())
361 self.readyRead.emit()
362
363 QCoreApplication.processEvents()
364
365 def __setContent(self):
366 """
367 Private method to finish the setup of the data.
368 """
369 mtype, encoding = mimetypes.guess_type(self.url().toString())
370 self.open(QIODevice.ReadOnly | QIODevice.Unbuffered)
371 self.setHeader(QNetworkRequest.ContentLengthHeader,
372 self.__items[0].size())
373 if mtype:
374 self.setHeader(QNetworkRequest.ContentTypeHeader, mtype)
375 self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200)
376 self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok")
377 self.metaDataChanged.emit()
378
379 def __cssLinkClass(self, icon, size=32):
380 """
381 Private method to generate a link class with an icon.
382
383 @param icon icon to be included (QIcon)
384 @param size size of the icon to be generated (integer)
385 @return CSS class string (string)
386 """
387 cssString = \
388 """a.{{0}} {{{{\n"""\
389 """ padding-left: {0}px;\n"""\
390 """ background: transparent url(data:image/png;base64,{1})"""\
391 """ no-repeat center left;\n"""\
392 """ font-weight: bold;\n"""\
393 """}}}}\n"""
394 pixmap = icon.pixmap(size, size)
395 imageBuffer = QBuffer()
396 imageBuffer.open(QIODevice.ReadWrite)
397 if not pixmap.save(imageBuffer, "PNG"):
398 # write a blank pixmap on error
399 pixmap = QPixmap(size, size)
400 pixmap.fill(Qt.transparent)
401 imageBuffer.buffer().clear()
402 pixmap.save(imageBuffer, "PNG")
403 return cssString.format(
404 size + 4,
405 str(imageBuffer.buffer().toBase64(), encoding="ascii"))
406
407 def __setListContent(self):
408 """
409 Private method to prepare the content for the reader.
410 """
411 u = self.url()
412 if not u.path().endswith("/"):
413 u.setPath(u.path() + "/")
414
415 baseUrl = self.url().toString()
416 basePath = u.path()
417
418 linkClasses = {}
419 iconSize = QWebSettings.globalSettings().fontSize(
420 QWebSettings.DefaultFontSize)
421
422 parent = u.resolved(QUrl(".."))
423 if parent.isParentOf(u):
424 icon = UI.PixmapCache.getIcon("up.png")
425 linkClasses["link_parent"] = \
426 self.__cssLinkClass(icon, iconSize).format("link_parent")
427 parentStr = self.tr(
428 """ <p><a class="link_parent" href="{0}">"""
429 """Change to parent directory</a></p>"""
430 ).format(parent.toString())
431 else:
432 parentStr = ""
433
434 row = \
435 """ <tr class="{0}">"""\
436 """<td class="name"><a class="{1}" href="{2}">{3}</a></td>"""\
437 """<td class="size">{4}</td>"""\
438 """<td class="modified">{5}</td>"""\
439 """</tr>\n"""
440 table = self.tr(
441 """ <tr>"""
442 """<th align="left">Name</th>"""
443 """<th>Size</th>"""
444 """<th align="left">Last modified</th>"""
445 """</tr>\n"""
446 )
447
448 i = 0
449 for item in self.__items:
450 name = item.name()
451 if item.isDir() and not name.endswith("/"):
452 name += "/"
453 child = u.resolved(QUrl(name.replace(":", "%3A")))
454
455 if item.isFile():
456 size = item.size()
457 unit = 0
458 while size:
459 newSize = size // 1024
460 if newSize and unit < len(self.__units):
461 size = newSize
462 unit += 1
463 else:
464 break
465
466 sizeStr = self.tr("{0} {1}", "size unit")\
467 .format(size, self.__units[unit])
468 linkClass = "link_file"
469 if linkClass not in linkClasses:
470 icon = UI.PixmapCache.getIcon("fileMisc.png")
471 linkClasses[linkClass] = \
472 self.__cssLinkClass(icon, iconSize).format(linkClass)
473 else:
474 sizeStr = ""
475 linkClass = "link_dir"
476 if linkClass not in linkClasses:
477 icon = UI.PixmapCache.getIcon("dirClosed.png")
478 linkClasses[linkClass] = \
479 self.__cssLinkClass(icon, iconSize).format(linkClass)
480 table += row.format(
481 i == 0 and "odd" or "even",
482 linkClass,
483 child.toString(),
484 Utilities.html_encode(item.name()),
485 sizeStr,
486 item.lastModified().toString("yyyy-MM-dd hh:mm"),
487 )
488 i = 1 - i
489
490 content = ftpListPage_html.format(
491 Utilities.html_encode(baseUrl),
492 "".join(linkClasses.values()),
493 self.tr("Listing of {0}").format(basePath),
494 parentStr,
495 table
496 )
497 self.__content = QByteArray(content.encode("utf8"))
498 self.__content.append(512 * b' ')
499
500 self.open(QIODevice.ReadOnly | QIODevice.Unbuffered)
501 self.setHeader(
502 QNetworkRequest.ContentTypeHeader, "text/html; charset=UTF-8")
503 self.setHeader(
504 QNetworkRequest.ContentLengthHeader, self.__content.size())
505 self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200)
506 self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok")
507 self.metaDataChanged.emit()
508 self.downloadProgress.emit(
509 self.__content.size(), self.__content.size())
510 self.readyRead.emit()

eric ide

mercurial