eric6/Helpviewer/Network/FtpReply.py

changeset 6942
2602857055c5
parent 6891
93f82da09f22
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Helpviewer/Network/FtpReply.py	Sun Apr 14 15:09:21 2019 +0200
@@ -0,0 +1,510 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2010 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a network reply class for FTP resources.
+"""
+
+from __future__ import unicode_literals
+try:
+    str = unicode
+except NameError:
+    pass
+
+import ftplib
+import socket
+import errno
+import mimetypes
+
+from PyQt5.QtCore import QByteArray, QIODevice, Qt, QUrl, QTimer, QBuffer, \
+    QCoreApplication
+from PyQt5.QtGui import QPixmap
+from PyQt5.QtWidgets import QDialog
+from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest, QAuthenticator
+from PyQt5.QtWebKit import QWebSettings
+
+from E5Network.E5Ftp import E5Ftp, E5FtpProxyError, E5FtpProxyType
+
+import UI.PixmapCache
+
+from Utilities.FtpUtilities import FtpDirLineParser, FtpDirLineParserError
+import Utilities
+
+import Preferences
+
+ftpListPage_html = """\
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+<title>{0}</title>
+<style type="text/css">
+body {{
+  padding: 3em 0em;
+  background: -webkit-gradient(linear, left top, left bottom, from(#85784A),
+                               to(#FDFDFD), color-stop(0.5, #FDFDFD));
+  background-repeat: repeat-x;
+}}
+#box {{
+  background: white;
+  border: 1px solid #85784A;
+  width: 80%;
+  padding: 30px;
+  margin: auto;
+  -webkit-border-radius: 0.8em;
+}}
+h1 {{
+  font-size: 130%;
+  font-weight: bold;
+  border-bottom: 1px solid #85784A;
+}}
+th {{
+  background-color: #B8B096;
+  color: black;
+}}
+table {{
+  border: solid 1px #85784A;
+  margin: 5px 0;
+  width: 100%;
+}}
+tr.odd {{
+  background-color: white;
+  color: black;
+}}
+tr.even {{
+  background-color: #CEC9B8;
+  color: black;
+}}
+.modified {{
+  text-align: left;
+  vertical-align: top;
+  white-space: nowrap;
+}}
+.size {{
+  text-align: right;
+  vertical-align: top;
+  white-space: nowrap;
+  padding-right: 22px;
+}}
+.name {{
+  text-align: left;
+  vertical-align: top;
+  white-space: pre-wrap;
+  width: 100%
+}}
+{1}
+</style>
+</head>
+<body>
+  <div id="box">
+  <h1>{2}</h1>
+{3}
+  <table align="center" cellspacing="0" width="90%">
+{4}
+  </table>
+  </div>
+</body>
+</html>
+"""
+
+
+class FtpReply(QNetworkReply):
+    """
+    Class implementing a network reply for FTP resources.
+    """
+    def __init__(self, url, accessHandler, parent=None):
+        """
+        Constructor
+        
+        @param url requested FTP URL (QUrl)
+        @param accessHandler reference to the access handler (FtpAccessHandler)
+        @param parent reference to the parent object (QObject)
+        """
+        super(FtpReply, self).__init__(parent)
+        
+        self.__manager = parent
+        self.__handler = accessHandler
+        
+        self.__ftp = E5Ftp()
+        
+        self.__items = []
+        self.__content = QByteArray()
+        self.__units = ["Bytes", "KB", "MB", "GB", "TB",
+                        "PB", "EB", "ZB", "YB"]
+        self.__dirLineParser = FtpDirLineParser()
+        self.__fileBytesReceived = 0
+        
+        if url.path() == "":
+            url.setPath("/")
+        self.setUrl(url)
+        
+        # do proxy setup
+        if not Preferences.getUI("UseProxy"):
+            proxyType = E5FtpProxyType.NoProxy
+        else:
+            proxyType = Preferences.getUI("ProxyType/Ftp")
+        if proxyType != E5FtpProxyType.NoProxy:
+            self.__ftp.setProxy(
+                proxyType,
+                Preferences.getUI("ProxyHost/Ftp"),
+                Preferences.getUI("ProxyPort/Ftp"))
+            if proxyType != E5FtpProxyType.NonAuthorizing:
+                self.__ftp.setProxyAuthentication(
+                    Preferences.getUI("ProxyUser/Ftp"),
+                    Preferences.getUI("ProxyPassword/Ftp"),
+                    Preferences.getUI("ProxyAccount/Ftp"))
+        
+        QTimer.singleShot(0, self.__doFtpCommands)
+    
+    def abort(self):
+        """
+        Public slot to abort the operation.
+        """
+        # do nothing
+        pass
+    
+    def bytesAvailable(self):
+        """
+        Public method to determined the bytes available for being read.
+        
+        @return bytes available (integer)
+        """
+        return self.__content.size()
+    
+    def isSequential(self):
+        """
+        Public method to check for sequential access.
+        
+        @return flag indicating sequential access (boolean)
+        """
+        return True
+    
+    def readData(self, maxlen):
+        """
+        Public method to retrieve data from the reply object.
+        
+        @param maxlen maximum number of bytes to read (integer)
+        @return string containing the data (bytes)
+        """
+        if self.__content.size():
+            len_ = min(maxlen, self.__content.size())
+            buffer = bytes(self.__content[:len_])
+            self.__content.remove(0, len_)
+            return buffer
+        else:
+            return b""
+    
+    def __doFtpCommands(self):
+        """
+        Private slot doing the sequence of FTP commands to get the requested
+        result.
+        """
+        retry = True
+        try:
+            username = self.url().userName()
+            password = self.url().password()
+            byAuth = False
+            
+            while retry:
+                try:
+                    self.__ftp.connect(self.url().host(),
+                                       self.url().port(ftplib.FTP_PORT),
+                                       timeout=10)
+                except E5FtpProxyError as err:
+                    self.setError(QNetworkReply.ProxyNotFoundError, str(err))
+                    self.error.emit(QNetworkReply.ProxyNotFoundError)
+                    self.finished.emit()
+                ok, retry = self.__doFtpLogin(username, password, byAuth)
+                if not ok and retry:
+                    auth = self.__handler.getAuthenticator(self.url().host())
+                    if auth and not auth.isNull() and auth.user():
+                        username = auth.user()
+                        password = auth.password()
+                        byAuth = True
+                    else:
+                        retry = False
+            if ok:
+                self.__ftp.retrlines("LIST " + self.url().path(),
+                                     self.__dirCallback)
+                if len(self.__items) == 1 and \
+                   self.__items[0].isFile():
+                    self.__fileBytesReceived = 0
+                    self.__setContent()
+                    self.__ftp.retrbinary(
+                        "RETR " + self.url().path(), self.__retrCallback)
+                    self.__content.append(512 * b' ')
+                    self.readyRead.emit()
+                else:
+                    self.__setListContent()
+                self.__ftp.quit()
+        except ftplib.all_errors as err:
+            if isinstance(err, socket.gaierror):
+                errCode = QNetworkReply.HostNotFoundError
+            elif isinstance(err, socket.error) and \
+                    err.errno == errno.ECONNREFUSED:
+                errCode = QNetworkReply.ConnectionRefusedError
+            else:
+                errCode = QNetworkReply.ProtocolFailure
+            self.setError(errCode, str(err))
+            self.error.emit(errCode)
+        self.finished.emit()
+    
+    def __doFtpLogin(self, username, password, byAuth=False):
+        """
+        Private method to do the FTP login with asking for a username and
+        password, if the login fails with an error 530.
+        
+        @param username user name to use for the login (string)
+        @param password password to use for the login (string)
+        @param byAuth flag indicating that the login data was provided by an
+            authenticator (boolean)
+        @return tuple of two flags indicating a successful login and
+            if the login should be retried (boolean, boolean)
+        """
+        try:
+            self.__ftp.login(username, password)
+            return True, False
+        except E5FtpProxyError as err:
+            code = str(err)[:3]
+            if code[1] == "5":
+                # could be a 530, check second line
+                lines = str(err).splitlines()
+                if lines[1][:3] == "530":
+                    if "usage" in "\n".join(lines[1:].lower()):
+                        # found a not supported proxy
+                        self.setError(
+                            QNetworkReply.ProxyConnectionRefusedError,
+                            self.tr("The proxy type seems to be wrong."
+                                    " If it is not in the list of"
+                                    " supported proxy types please report"
+                                    " it with the instructions given by"
+                                    " the proxy.\n{0}").format(
+                                "\n".join(lines[1:])))
+                        self.error.emit(
+                            QNetworkReply.ProxyConnectionRefusedError)
+                        return False, False
+                    else:
+                        from UI.AuthenticationDialog import \
+                            AuthenticationDialog
+                        info = self.tr(
+                            "<b>Connect to proxy '{0}' using:</b>")\
+                            .format(Utilities.html_encode(
+                                Preferences.getUI("ProxyHost/Ftp")))
+                        dlg = AuthenticationDialog(
+                            info, Preferences.getUI("ProxyUser/Ftp"), True)
+                        dlg.setData(Preferences.getUI("ProxyUser/Ftp"),
+                                    Preferences.getUI("ProxyPassword/Ftp"))
+                        if dlg.exec_() == QDialog.Accepted:
+                            username, password = dlg.getData()
+                            if dlg.shallSave():
+                                Preferences.setUI("ProxyUser/Ftp", username)
+                                Preferences.setUI(
+                                    "ProxyPassword/Ftp", password)
+                            self.__ftp.setProxyAuthentication(username,
+                                                              password)
+                            return False, True
+            return False, False
+        except (ftplib.error_perm, ftplib.error_temp) as err:
+            code = err.args[0].strip()[:3]
+            if code in ["530", "421"]:
+                # error 530 -> Login incorrect
+                # error 421 -> Login may be incorrect (reported by some
+                # proxies)
+                if byAuth:
+                    self.__handler.setAuthenticator(self.url().host(), None)
+                    auth = None
+                else:
+                    auth = self.__handler.getAuthenticator(self.url().host())
+                if not auth or auth.isNull() or not auth.user():
+                    auth = QAuthenticator()
+                    self.__manager.authenticationRequired.emit(self, auth)
+                    if not auth.isNull():
+                        if auth.user():
+                            self.__handler.setAuthenticator(self.url().host(),
+                                                            auth)
+                            return False, True
+                    return False, False
+                return False, True
+            else:
+                raise
+    
+    def __dirCallback(self, line):
+        """
+        Private slot handling the receipt of directory listings.
+        
+        @param line the received line of the directory listing (string)
+        """
+        try:
+            urlInfo = self.__dirLineParser.parseLine(line)
+        except FtpDirLineParserError:
+            # silently ignore parser errors
+            urlInfo = None
+        
+        if urlInfo:
+            self.__items.append(urlInfo)
+        
+        QCoreApplication.processEvents()
+    
+    def __retrCallback(self, data):
+        """
+        Private slot handling the reception of data.
+        
+        @param data data received from the FTP server (bytes)
+        """
+        self.__content += QByteArray(data)
+        self.__fileBytesReceived += len(data)
+        self.downloadProgress.emit(
+            self.__fileBytesReceived, self.__items[0].size())
+        self.readyRead.emit()
+        
+        QCoreApplication.processEvents()
+    
+    def __setContent(self):
+        """
+        Private method to finish the setup of the data.
+        """
+        mtype, encoding = mimetypes.guess_type(self.url().toString())
+        self.open(QIODevice.ReadOnly | QIODevice.Unbuffered)
+        self.setHeader(QNetworkRequest.ContentLengthHeader,
+                       self.__items[0].size())
+        if mtype:
+            self.setHeader(QNetworkRequest.ContentTypeHeader, mtype)
+        self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200)
+        self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok")
+        self.metaDataChanged.emit()
+    
+    def __cssLinkClass(self, icon, size=32):
+        """
+        Private method to generate a link class with an icon.
+        
+        @param icon icon to be included (QIcon)
+        @param size size of the icon to be generated (integer)
+        @return CSS class string (string)
+        """
+        cssString = \
+            """a.{{0}} {{{{\n"""\
+            """  padding-left: {0}px;\n"""\
+            """  background: transparent url(data:image/png;base64,{1})"""\
+            """ no-repeat center left;\n"""\
+            """  font-weight: bold;\n"""\
+            """}}}}\n"""
+        pixmap = icon.pixmap(size, size)
+        imageBuffer = QBuffer()
+        imageBuffer.open(QIODevice.ReadWrite)
+        if not pixmap.save(imageBuffer, "PNG"):
+            # write a blank pixmap on error
+            pixmap = QPixmap(size, size)
+            pixmap.fill(Qt.transparent)
+            imageBuffer.buffer().clear()
+            pixmap.save(imageBuffer, "PNG")
+        return cssString.format(
+            size + 4,
+            str(imageBuffer.buffer().toBase64(), encoding="ascii"))
+    
+    def __setListContent(self):
+        """
+        Private method to prepare the content for the reader.
+        """
+        u = self.url()
+        if not u.path().endswith("/"):
+            u.setPath(u.path() + "/")
+        
+        baseUrl = self.url().toString()
+        basePath = u.path()
+        
+        linkClasses = {}
+        iconSize = QWebSettings.globalSettings().fontSize(
+            QWebSettings.DefaultFontSize)
+        
+        parent = u.resolved(QUrl(".."))
+        if parent.isParentOf(u):
+            icon = UI.PixmapCache.getIcon("up.png")
+            linkClasses["link_parent"] = \
+                self.__cssLinkClass(icon, iconSize).format("link_parent")
+            parentStr = self.tr(
+                """  <p><a class="link_parent" href="{0}">"""
+                """Change to parent directory</a></p>"""
+            ).format(parent.toString())
+        else:
+            parentStr = ""
+        
+        row = \
+            """    <tr class="{0}">"""\
+            """<td class="name"><a class="{1}" href="{2}">{3}</a></td>"""\
+            """<td class="size">{4}</td>"""\
+            """<td class="modified">{5}</td>"""\
+            """</tr>\n"""
+        table = self.tr(
+            """    <tr>"""
+            """<th align="left">Name</th>"""
+            """<th>Size</th>"""
+            """<th align="left">Last modified</th>"""
+            """</tr>\n"""
+        )
+        
+        i = 0
+        for item in self.__items:
+            name = item.name()
+            if item.isDir() and not name.endswith("/"):
+                name += "/"
+            child = u.resolved(QUrl(name.replace(":", "%3A")))
+            
+            if item.isFile():
+                size = item.size()
+                unit = 0
+                while size:
+                    newSize = size // 1024
+                    if newSize and unit < len(self.__units):
+                        size = newSize
+                        unit += 1
+                    else:
+                        break
+                
+                sizeStr = self.tr("{0} {1}", "size unit")\
+                    .format(size, self.__units[unit])
+                linkClass = "link_file"
+                if linkClass not in linkClasses:
+                    icon = UI.PixmapCache.getIcon("fileMisc.png")
+                    linkClasses[linkClass] = \
+                        self.__cssLinkClass(icon, iconSize).format(linkClass)
+            else:
+                sizeStr = ""
+                linkClass = "link_dir"
+                if linkClass not in linkClasses:
+                    icon = UI.PixmapCache.getIcon("dirClosed.png")
+                    linkClasses[linkClass] = \
+                        self.__cssLinkClass(icon, iconSize).format(linkClass)
+            table += row.format(
+                i == 0 and "odd" or "even",
+                linkClass,
+                child.toString(),
+                Utilities.html_encode(item.name()),
+                sizeStr,
+                item.lastModified().toString("yyyy-MM-dd hh:mm"),
+            )
+            i = 1 - i
+        
+        content = ftpListPage_html.format(
+            Utilities.html_encode(baseUrl),
+            "".join(linkClasses.values()),
+            self.tr("Listing of {0}").format(basePath),
+            parentStr,
+            table
+        )
+        self.__content = QByteArray(content.encode("utf8"))
+        self.__content.append(512 * b' ')
+        
+        self.open(QIODevice.ReadOnly | QIODevice.Unbuffered)
+        self.setHeader(
+            QNetworkRequest.ContentTypeHeader, "text/html; charset=UTF-8")
+        self.setHeader(
+            QNetworkRequest.ContentLengthHeader, self.__content.size())
+        self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200)
+        self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, "Ok")
+        self.metaDataChanged.emit()
+        self.downloadProgress.emit(
+            self.__content.size(), self.__content.size())
+        self.readyRead.emit()

eric ide

mercurial