Helpviewer/Network/FtpReply.py

Wed, 19 Sep 2012 20:19:49 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 19 Sep 2012 20:19:49 +0200
changeset 2050
585f6646bf50
parent 2047
739aa1717df5
child 2053
ef81185e8b89
permissions
-rw-r--r--

Changed the FtpReply to support proxy access and to not show user and password in the URL.

# -*- coding: utf-8 -*-

# Copyright (c) 2010 - 2012 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a network reply class for FTP resources.
"""

import ftplib
import socket
import errno
import mimetypes

from PyQt4.QtCore import QByteArray, QIODevice, Qt, QUrl, QTimer, QBuffer, \
    QCoreApplication
from PyQt4.QtGui import QPixmap
from PyQt4.QtNetwork import QNetworkReply, QNetworkRequest, QNetworkProxyQuery, \
    QNetworkProxy, QAuthenticator
from PyQt4.QtWebKit import QWebSettings

import UI.PixmapCache
from Utilities.FtpUtilities import FtpDirLineParser, FtpDirLineParserError

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.
    """
    Monthnames2Int = {
        "Jan": 1,
        "Feb": 2,
        "Mar": 3,
        "Apr": 4,
        "May": 5,
        "Jun": 6,
        "Jul": 7,
        "Aug": 8,
        "Sep": 9,
        "Oct": 10,
        "Nov": 11,
        "Dec": 12,
    }
    
    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().__init__(parent)
        
        self.__manager = parent
        self.__handler = accessHandler
        
        self.__ftp = ftplib.FTP()
        
        self.__items = []
        self.__content = QByteArray()
        self.__units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
        self.__dirLineParser = FtpDirLineParser()
        
        if url.path() == "":
            url.setPath("/")
        self.setUrl(url)
        
        # do proxy setup
        self.__proxy = None
        query = QNetworkProxyQuery(url)
        proxyList = parent.proxyFactory().queryProxy(query)
        ftpProxy = QNetworkProxy()
        for proxy in proxyList:
            if proxy.type() == QNetworkProxy.NoProxy or \
               proxy.type() == QNetworkProxy.FtpCachingProxy:
                ftpProxy = proxy
                break
        if ftpProxy.type() == QNetworkProxy.DefaultProxy:
            self.setError(QNetworkReply.ProxyNotFoundError,
                          self.trUtf8("No suitable proxy found."))
            QTimer.singleShot(0, self.__errorSignals)
            return
        elif ftpProxy.type() == QNetworkProxy.FtpCachingProxy:
            self.__proxy = ftpProxy
        
        self.__loggingIn = False
        
        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):
        """
        Protected 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
    
    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:
                if self.__proxy:
                    self.__ftp.connect(self.__proxy.hostName(), self.__proxy.port())
                else:
                    self.__ftp.connect(
                        self.url().host(), self.url().port(ftplib.FTP_PORT), timeout=10)
                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.__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)
        """
        # 1. do proxy login, if a proxy is used
        if self.__proxy:
            try:
                self.__ftp.login(self.__proxy.user(), self.__proxy.password())
            except ftplib.error_perm as err:
                code, msg = err.args[0].split(None, 1)
                if code.strip() == "530":
                    auth = QAuthenticator()
                    auth.setOption("realm", self.__proxy.hostName())
                    self.__manager.proxyAuthenticationRequired.emit(self.__proxy, auth)
                    if not auth.isNull() and auth.user():
                        self.__proxy.setUser(auth.user())
                        self.__proxy.setPassword(auth.password())
                        return False, True
                return False, False
        
        # 2. do the real login
        try:
            if self.__proxy:
                loginName = "{0}@{1}".format(username, self.url().host())
                if self.url().port(ftplib.FTP_PORT) != ftplib.FTP_PORT:
                    loginName = "{0}:{1}".format(loginName, self.url().port())
            else:
                loginName = username
            self.__ftp.login(loginName, password)
            return True, False
        except ftplib.error_perm as err:
            code, msg = err.args[0].split(None, 1)
            if code.strip() == "530":
                # error 530 -> Login incorrect
                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()
                    auth.setOption("realm", self.url().host())
                    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
                else:
                    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)
        
        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.trUtf8(
                """  <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.trUtf8(
            """    <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.trUtf8("{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(),
                Qt.escape(item.name()),
                sizeStr,
                item.lastModified().toString("yyyy-MM-dd hh:mm"),
            )
            i = 1 - i
        
        content = ftpListPage_html.format(
            Qt.escape(baseUrl),
            "".join(linkClasses.values()),
            self.trUtf8("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