Helpviewer/Sync/FtpSyncHandler.py

Sun, 23 Sep 2012 18:53:36 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 23 Sep 2012 18:53:36 +0200
changeset 2061
b74f1d202ba7
parent 2060
1f3767746974
child 2062
f275d3afe038
permissions
-rw-r--r--

Fixed an issue with the FTP sync handler.

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

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

"""
Module implementing a synchronization handler using FTP.
"""

import ftplib
import io

from PyQt4.QtCore import pyqtSignal, QUrl, QTimer, QFileInfo, QCoreApplication, QByteArray
from PyQt4.QtNetwork import QNetworkProxyQuery, QNetworkProxy, QAuthenticator

from E5Network.E5NetworkProxyFactory import E5NetworkProxyFactory, \
    proxyAuthenticationRequired

from .SyncHandler import SyncHandler

import Helpviewer.HelpWindow

import Preferences

from Utilities.FtpUtilities import FtpDirLineParser, FtpDirLineParserError


class FtpSyncHandler(SyncHandler):
    """
    Class implementing a synchronization handler using FTP.
    
    @signal syncStatus(type_, message) emitted to indicate the synchronization
        status (string one of "bookmarks", "history", "passwords", "useragents" or
        "speeddial", string)
    @signal syncError(message) emitted for a general error with the error message (string)
    @signal syncMessage(message) emitted to send a message about synchronization (string)
    @signal syncFinished(type_, done, download) emitted after a synchronization has
        finished (string one of "bookmarks", "history", "passwords", "useragents" or
        "speeddial", boolean, boolean)
    """
    syncStatus = pyqtSignal(str, str)
    syncError = pyqtSignal(str)
    syncMessage = pyqtSignal(str)
    syncFinished = pyqtSignal(str, bool, bool)
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent object (QObject)
        """
        super().__init__(parent)
        
        self.__state = "idle"
        self.__forceUpload = False
        self.__connected = False
        
        self.__remoteFilesFound = {}
    
    def initialLoadAndCheck(self, forceUpload):
        """
        Public method to do the initial check.
        
        @keyparam forceUpload flag indicating a forced upload of the files (boolean)
        """
        if not Preferences.getHelp("SyncEnabled"):
            return
        
        self.__state = "initializing"
        self.__forceUpload = forceUpload
        
        self.__dirLineParser = FtpDirLineParser()
        self.__remoteFilesFound = {}
        
        self.__idleTimer = QTimer(self)
        self.__idleTimer.setInterval(Preferences.getHelp("SyncFtpIdleTimeout") * 1000)
        self.__idleTimer.timeout.connect(self.__idleTimeout)
        
        self.__ftp = ftplib.FTP()
        
        # do proxy setup
        self.__proxy = None
        url = QUrl("ftp://{0}:{1}".format(
            Preferences.getHelp("SyncFtpServer"),
            Preferences.getHelp("SyncFtpPort")
        ))
        query = QNetworkProxyQuery(url)
        proxyList = E5NetworkProxyFactory().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.syncError.emit(self.trUtf8("No suitable proxy found."))
            return
        elif ftpProxy.type() == QNetworkProxy.FtpCachingProxy:
            self.__proxy = ftpProxy
        
        QTimer.singleShot(0, self.__doFtpCommands)
    
    def __doFtpCommands(self):
        """
        Private slot executing the sequence of FTP commands.
        """
        try:
            ok = self.__connectAndLogin()
            if ok:
                self.__changeToStore()
                self.__ftp.retrlines("LIST", self.__dirListCallback)
                self.__initialSync()
                self.__state = "idle"
                self.__idleTimer.start()
        except ftplib.all_errors as err:
            self.syncError.emit(str(err))
    
    def __connectAndLogin(self):
        """
        Private method to connect to the FTP server and log in.
        
        @return flag indicating a successful log in (boolean)
        """
        retry = True
        while retry:
            if self.__proxy:
                self.__ftp.connect(
                    self.__proxy.hostName(),
                    self.__proxy.port(),
                    timeout=10)
            else:
                self.__ftp.connect(
                    Preferences.getHelp("SyncFtpServer"),
                    Preferences.getHelp("SyncFtpPort"),
                    timeout=10)
            ok, retry = self.__doFtpLogin(
                Preferences.getHelp("SyncFtpUser"),
                Preferences.getHelp("SyncFtpPassword"))
        self.__connected = ok
        if not ok:
            self.syncError.emit(self.trUtf8("Cannot log in to FTP host."))
        
        return ok
    
    def __doFtpLogin(self, username, password):
        """
        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)
        @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())
                    proxyAuthenticationRequired(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
        if self.__proxy:
            loginName = "{0}@{1}".format(
                username, Preferences.getHelp("SyncFtpServer"))
            if Preferences.getHelp("SyncFtpPort") != ftplib.FTP_PORT:
                loginName = "{0}:{1}".format(
                    loginName, Preferences.getHelp("SyncFtpPort"))
        else:
            loginName = username
        self.__ftp.login(loginName, password)
        return True, False
    
    def __changeToStore(self):
        """
        Private slot to change to the storage directory.
        
        This action will create the storage path on the server, if it
        does not exist. Upon return, the current directory of the server
        is the sync directory.
        """
        storePathList = \
            Preferences.getHelp("SyncFtpPath").replace("\\", "/").split("/")
        if storePathList[0] == "":
            storePathList.pop(0)
        while storePathList:
            path = storePathList[0]
            try:
                self.__ftp.cwd(path)
            except ftplib.error_perm as err:
                code, msg = err.args[0].split(None, 1)
                if code.strip() == "550":
                    # path does not exist, create it
                    self.__ftp.mkd(path)
                    self.__ftp.cwd(path)
                else:
                    raise
            storePathList.pop(0)
    
    def __dirListCallback(self, line):
        """
        Private slot handling the receipt of directory listing lines.
        
        @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 and urlInfo.isValid() and urlInfo.isFile():
            if urlInfo.name() in self._remoteFiles.values():
                self.__remoteFilesFound[urlInfo.name()] = urlInfo.lastModified()
        
        QCoreApplication.processEvents()
    
    def __downloadFile(self, type_, fileName, timestamp):
        """
        Private method to downlaod the given file.
        
        @param type_ type of the synchronization event (string one
            of "bookmarks", "history", "passwords", "useragents" or "speeddial")
        @param fileName name of the file to be downloaded (string)
        @param timestamp time stamp in seconds of the file to be downloaded (int)
        """
        self.syncStatus.emit(type_, self._messages[type_]["RemoteExists"])
        buffer = io.BytesIO()
        try:
            self.__ftp.retrbinary(
                "RETR {0}".format(self._remoteFiles[type_]),
                lambda x: self.__downloadFileCallback(buffer, x))
            ok, error = self.writeFile(
                QByteArray(buffer.getvalue()), fileName, type_, timestamp)
            if not ok:
                self.syncStatus.emit(type_, error)
            self.syncFinished.emit(type_, ok, True)
        except ftplib.all_errors as err:
            self.syncStatus.emit(type_, str(err))
            self.syncFinished.emit(type_, False, True)
    
    def __downloadFileCallback(self, buffer, data):
        """
        Private method receiving the downloaded data.
        
        @param buffer reference to the buffer (io.BytesIO)
        @param data byte string to store in the buffer (bytes)
        @return number of bytes written to the buffer (integer)
        """
        res = buffer.write(data)
        QCoreApplication.processEvents()
        return res
    
    def __uploadFile(self, type_, fileName):
        """
        Private method to upload the given file.
        
        @param type_ type of the synchronization event (string one
            of "bookmarks", "history", "passwords", "useragents" or "speeddial")
        @param fileName name of the file to be uploaded (string)
        """
        data = self.readFile(fileName, type_)
        if data.isEmpty():
            self.syncStatus.emit(type_, self._messages[type_]["LocalMissing"])
            self.syncFinished(type_, False, False)
        else:
            buffer = io.BytesIO(data.data())
            try:
                self.__ftp.storbinary(
                    "STOR {0}".format(self._remoteFiles[type_]),
                    buffer,
                    callback=lambda x: QCoreApplication.processEvents())
                self.syncFinished.emit(type_, True, False)
            except ftplib.all_errors as err:
                self.syncStatus.emit(type_, str(err))
                self.syncFinished.emit(type_, False, False)
    
    def __initialSyncFile(self, type_, fileName):
        """
        Private method to do the initial synchronization of the given file.
        
        @param type_ type of the synchronization event (string one
            of "bookmarks", "history", "passwords", "useragents" or "speeddial")
        @param fileName name of the file to be synchronized (string)
        """
        if not self.__forceUpload and \
           self._remoteFiles[type_] in self.__remoteFilesFound:
            if QFileInfo(fileName).lastModified() < \
               self.__remoteFilesFound[self._remoteFiles[type_]]:
                self.__downloadFile(type_, fileName,
                    self.__remoteFilesFound[self._remoteFiles[type_]].toTime_t())
            else:
                self.syncStatus.emit(type_, self.trUtf8("No synchronization required."))
                self.syncFinished.emit(type_, True, True)
        else:
            if self._remoteFiles[type_] not in self.__remoteFilesFound:
                self.syncStatus.emit(type_, self._messages[type_]["RemoteMissing"])
            else:
                self.syncStatus.emit(type_, self._messages[type_]["LocalNewer"])
            self.__uploadFile(type_, fileName)
    
    def __initialSync(self):
        """
        Private slot to do the initial synchronization.
        """
        # Bookmarks
        if Preferences.getHelp("SyncBookmarks"):
            self.__initialSyncFile("bookmarks",
                Helpviewer.HelpWindow.HelpWindow.bookmarksManager().getFileName())
        
        # History
        if Preferences.getHelp("SyncHistory"):
            self.__initialSyncFile("history",
                Helpviewer.HelpWindow.HelpWindow.historyManager().getFileName())
        
        # Passwords
        if Preferences.getHelp("SyncPasswords"):
            self.__initialSyncFile("passwords",
                Helpviewer.HelpWindow.HelpWindow.passwordManager().getFileName())
        
        # User Agent Settings
        if Preferences.getHelp("SyncUserAgents"):
            self.__initialSyncFile("useragents",
                Helpviewer.HelpWindow.HelpWindow.userAgentsManager().getFileName())
        
        # Speed Dial Settings
        if Preferences.getHelp("SyncSpeedDial"):
            self.__initialSyncFile("speeddial",
                Helpviewer.HelpWindow.HelpWindow.speedDial().getFileName())
        
        self.__forceUpload = False
    
    def __syncFile(self, type_, fileName):
        """
        Private method to synchronize the given file.
        
        @param type_ type of the synchronization event (string one
            of "bookmarks", "history", "passwords", "useragents" or "speeddial")
        @param fileName name of the file to be synchronized (string)
        """
        if self.__state == "initializing":
            return
        
        # use idle timeout to check, if we are still connected
        if self.__connected:
            self.__idleTimeout()
            if not self.__connected:
                ok = self.__connectAndLogin()
                if not ok:
                    self.syncStatus.emit(type_, self.trUtf8("Cannot log in to FTP host."))
                    return
        
        # upload the changed file
        self.__state = "uploading"
        self.syncStatus.emit(type_, self._messages[type_]["Uploading"])
        self.__uploadFile(type_, fileName)
        self.syncStatus.emit(type_, self.trUtf8("Synchronization finished."))
        self.__state = "idle"
    
    def syncBookmarks(self):
        """
        Public method to synchronize the bookmarks.
        """
        self.__syncFile("bookmarks",
            Helpviewer.HelpWindow.HelpWindow.bookmarksManager().getFileName())
    
    def syncHistory(self):
        """
        Public method to synchronize the history.
        """
        self.__syncFile("history",
            Helpviewer.HelpWindow.HelpWindow.historyManager().getFileName())
    
    def syncPasswords(self):
        """
        Public method to synchronize the passwords.
        """
        self.__syncFile("passwords",
            Helpviewer.HelpWindow.HelpWindow.passwordManager().getFileName())
    
    def syncUserAgents(self):
        """
        Public method to synchronize the user agents.
        """
        self.__syncFile("useragents",
            Helpviewer.HelpWindow.HelpWindow.userAgentsManager().getFileName())
    
    def syncSpeedDial(self):
        """
        Public method to synchronize the speed dial data.
        """
        self.__syncFile("speeddial",
            Helpviewer.HelpWindow.HelpWindow.speedDial().getFileName())
    
    def shutdown(self):
        """
        Public method to shut down the handler.
        """
        if self.__idleTimer.isActive():
            self.__idleTimer.stop()
        
        try:
            self.__ftp.quit()
        except ftplib.all_errors:
            pass    # ignore FTP errors because we are shutting down anyway
        self.__connected = False
    
    def __idleTimeout(self):
        """
        Private slot to prevent a disconnect from the server.
        """
        if self.__state == "idle" and self.__connected:
            try:
                self.__ftp.voidcmd("NOOP")
            except ftplib.Error as err:
                code, msg = err.args[0].split(None, 1)
                if code.strip() == "421":
                    self.__connected = False

eric ide

mercurial