Wed, 19 Sep 2012 20:19:49 +0200
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()