WebBrowser/Sync/FtpSyncHandler.py

branch
QtWebEngine
changeset 4774
2c6ffa778c3b
parent 4631
5c1a96925da4
child 4868
985d275502c8
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/WebBrowser/Sync/FtpSyncHandler.py	Fri Feb 26 20:16:59 2016 +0100
@@ -0,0 +1,414 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2012 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a synchronization handler using FTP.
+"""
+
+from __future__ import unicode_literals
+
+import ftplib
+import io
+
+from PyQt5.QtCore import pyqtSignal, QTimer, QFileInfo, QCoreApplication, \
+    QByteArray
+
+from E5Network.E5Ftp import E5Ftp, E5FtpProxyType, E5FtpProxyError
+
+from .SyncHandler import SyncHandler
+
+from WebBrowser.WebBrowserWindow import WebBrowserWindow
+
+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(FtpSyncHandler, self).__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.getWebBrowser("SyncEnabled"):
+            return
+        
+        self.__state = "initializing"
+        self.__forceUpload = forceUpload
+        
+        self.__dirLineParser = FtpDirLineParser()
+        self.__remoteFilesFound = {}
+        
+        self.__idleTimer = QTimer(self)
+        self.__idleTimer.setInterval(
+            Preferences.getWebBrowser("SyncFtpIdleTimeout") * 1000)
+        self.__idleTimer.timeout.connect(self.__idleTimeout)
+        
+        self.__ftp = E5Ftp()
+        
+        # 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 __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 + (E5FtpProxyError,)) 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)
+        """
+        self.__ftp.connect(
+            Preferences.getWebBrowser("SyncFtpServer"),
+            Preferences.getWebBrowser("SyncFtpPort"),
+            timeout=5)
+        self.__ftp.login(
+            Preferences.getWebBrowser("SyncFtpUser"),
+            Preferences.getWebBrowser("SyncFtpPassword"))
+        self.__connected = True
+        return True
+    
+    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.getWebBrowser("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 = err.args[0].strip()[:3]
+                if code == "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
+            (integer)
+        """
+        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)
+        @return flag indicating success (boolean)
+        """
+        res = False
+        data = self.readFile(fileName, type_)
+        if data.isEmpty():
+            self.syncStatus.emit(type_, self._messages[type_]["LocalMissing"])
+            self.syncFinished.emit(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)
+                res = True
+            except ftplib.all_errors as err:
+                self.syncStatus.emit(type_, str(err))
+                self.syncFinished.emit(type_, False, False)
+        return res
+    
+    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.tr("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.getWebBrowser("SyncBookmarks"):
+            self.__initialSyncFile(
+                "bookmarks",
+                WebBrowserWindow.bookmarksManager().getFileName())
+        
+        # History
+        if Preferences.getWebBrowser("SyncHistory"):
+            self.__initialSyncFile(
+                "history",
+                WebBrowserWindow.historyManager().getFileName())
+        
+        # Passwords
+        if Preferences.getWebBrowser("SyncPasswords"):
+            self.__initialSyncFile(
+                "passwords",
+                WebBrowserWindow.passwordManager().getFileName())
+        
+        # TODO: UserAgents
+        # User Agent Settings
+##        if Preferences.getWebBrowser("SyncUserAgents"):
+##            self.__initialSyncFile(
+##                "useragents",
+##                WebBrowserWindow.userAgentsManager().getFileName())
+        
+        # TODO: SpeedDial
+        # Speed Dial Settings
+##        if Preferences.getWebBrowser("SyncSpeedDial"):
+##            self.__initialSyncFile(
+##                "speeddial",
+##                WebBrowserWindow.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 or self.__ftp.sock is None:
+            ok = self.__connectAndLogin()
+            if not ok:
+                self.syncStatus.emit(
+                    type_, self.tr("Cannot log in to FTP host."))
+                return
+        
+        # upload the changed file
+        self.__state = "uploading"
+        self.syncStatus.emit(type_, self._messages[type_]["Uploading"])
+        if self.__uploadFile(type_, fileName):
+            self.syncStatus.emit(
+                type_, self.tr("Synchronization finished."))
+        self.__state = "idle"
+    
+    def syncBookmarks(self):
+        """
+        Public method to synchronize the bookmarks.
+        """
+        self.__syncFile(
+            "bookmarks",
+            WebBrowserWindow.bookmarksManager().getFileName())
+    
+    def syncHistory(self):
+        """
+        Public method to synchronize the history.
+        """
+        self.__syncFile(
+            "history",
+            WebBrowserWindow.historyManager().getFileName())
+    
+    def syncPasswords(self):
+        """
+        Public method to synchronize the passwords.
+        """
+        self.__syncFile(
+            "passwords",
+            WebBrowserWindow.passwordManager().getFileName())
+    
+    # TODO: UserAgents
+    def syncUserAgents(self):
+        """
+        Public method to synchronize the user agents.
+        """
+##        self.__syncFile(
+##            "useragents",
+##            WebBrowserWindow.userAgentsManager().getFileName())
+    
+    # TODO: SpeedDial
+    def syncSpeedDial(self):
+        """
+        Public method to synchronize the speed dial data.
+        """
+##        self.__syncFile(
+##            "speeddial",
+##            WebBrowserWindow.speedDial().getFileName())
+    
+    def shutdown(self):
+        """
+        Public method to shut down the handler.
+        """
+        if self.__idleTimer.isActive():
+            self.__idleTimer.stop()
+        
+        try:
+            if self.__connected:
+                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 = err.args[0].strip()[:3]
+                if code == "421":
+                    self.__connected = False
+            except IOError:
+                self.__connected = False

eric ide

mercurial