eric6/WebBrowser/Sync/FtpSyncHandler.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a synchronization handler using FTP.
8 """
9
10 from __future__ import unicode_literals
11
12 import ftplib
13 import io
14
15 from PyQt5.QtCore import pyqtSignal, QTimer, QFileInfo, QCoreApplication, \
16 QByteArray
17
18 from E5Network.E5Ftp import E5Ftp, E5FtpProxyType, E5FtpProxyError
19
20 from .SyncHandler import SyncHandler
21
22 from WebBrowser.WebBrowserWindow import WebBrowserWindow
23
24 import Preferences
25
26 from Utilities.FtpUtilities import FtpDirLineParser, FtpDirLineParserError
27
28
29 class FtpSyncHandler(SyncHandler):
30 """
31 Class implementing a synchronization handler using FTP.
32
33 @signal syncStatus(type_, message) emitted to indicate the synchronization
34 status (string one of "bookmarks", "history", "passwords",
35 "useragents" or "speeddial", string)
36 @signal syncError(message) emitted for a general error with the error
37 message (string)
38 @signal syncMessage(message) emitted to send a message about
39 synchronization (string)
40 @signal syncFinished(type_, done, download) emitted after a
41 synchronization has finished (string one of "bookmarks", "history",
42 "passwords", "useragents" or "speeddial", boolean, boolean)
43 """
44 syncStatus = pyqtSignal(str, str)
45 syncError = pyqtSignal(str)
46 syncMessage = pyqtSignal(str)
47 syncFinished = pyqtSignal(str, bool, bool)
48
49 def __init__(self, parent=None):
50 """
51 Constructor
52
53 @param parent reference to the parent object (QObject)
54 """
55 super(FtpSyncHandler, self).__init__(parent)
56
57 self.__state = "idle"
58 self.__forceUpload = False
59 self.__connected = False
60
61 self.__remoteFilesFound = {}
62
63 def initialLoadAndCheck(self, forceUpload):
64 """
65 Public method to do the initial check.
66
67 @keyparam forceUpload flag indicating a forced upload of the files
68 (boolean)
69 """
70 if not Preferences.getWebBrowser("SyncEnabled"):
71 return
72
73 self.__state = "initializing"
74 self.__forceUpload = forceUpload
75
76 self.__dirLineParser = FtpDirLineParser()
77 self.__remoteFilesFound = {}
78
79 self.__idleTimer = QTimer(self)
80 self.__idleTimer.setInterval(
81 Preferences.getWebBrowser("SyncFtpIdleTimeout") * 1000)
82 self.__idleTimer.timeout.connect(self.__idleTimeout)
83
84 self.__ftp = E5Ftp()
85
86 # do proxy setup
87 if not Preferences.getUI("UseProxy"):
88 proxyType = E5FtpProxyType.NoProxy
89 else:
90 proxyType = Preferences.getUI("ProxyType/Ftp")
91 if proxyType != E5FtpProxyType.NoProxy:
92 self.__ftp.setProxy(
93 proxyType,
94 Preferences.getUI("ProxyHost/Ftp"),
95 Preferences.getUI("ProxyPort/Ftp"))
96 if proxyType != E5FtpProxyType.NonAuthorizing:
97 self.__ftp.setProxyAuthentication(
98 Preferences.getUI("ProxyUser/Ftp"),
99 Preferences.getUI("ProxyPassword/Ftp"),
100 Preferences.getUI("ProxyAccount/Ftp"))
101
102 QTimer.singleShot(0, self.__doFtpCommands)
103
104 def __doFtpCommands(self):
105 """
106 Private slot executing the sequence of FTP commands.
107 """
108 try:
109 ok = self.__connectAndLogin()
110 if ok:
111 self.__changeToStore()
112 self.__ftp.retrlines("LIST", self.__dirListCallback)
113 self.__initialSync()
114 self.__state = "idle"
115 self.__idleTimer.start()
116 except (ftplib.all_errors + (E5FtpProxyError,)) as err:
117 self.syncError.emit(str(err))
118
119 def __connectAndLogin(self):
120 """
121 Private method to connect to the FTP server and log in.
122
123 @return flag indicating a successful log in (boolean)
124 """
125 self.__ftp.connect(
126 Preferences.getWebBrowser("SyncFtpServer"),
127 Preferences.getWebBrowser("SyncFtpPort"),
128 timeout=5)
129 self.__ftp.login(
130 Preferences.getWebBrowser("SyncFtpUser"),
131 Preferences.getWebBrowser("SyncFtpPassword"))
132 self.__connected = True
133 return True
134
135 def __changeToStore(self):
136 """
137 Private slot to change to the storage directory.
138
139 This action will create the storage path on the server, if it
140 does not exist. Upon return, the current directory of the server
141 is the sync directory.
142 """
143 storePathList = \
144 Preferences.getWebBrowser("SyncFtpPath")\
145 .replace("\\", "/").split("/")
146 if storePathList[0] == "":
147 storePathList.pop(0)
148 while storePathList:
149 path = storePathList[0]
150 try:
151 self.__ftp.cwd(path)
152 except ftplib.error_perm as err:
153 code = err.args[0].strip()[:3]
154 if code == "550":
155 # path does not exist, create it
156 self.__ftp.mkd(path)
157 self.__ftp.cwd(path)
158 else:
159 raise
160 storePathList.pop(0)
161
162 def __dirListCallback(self, line):
163 """
164 Private slot handling the receipt of directory listing lines.
165
166 @param line the received line of the directory listing (string)
167 """
168 try:
169 urlInfo = self.__dirLineParser.parseLine(line)
170 except FtpDirLineParserError:
171 # silently ignore parser errors
172 urlInfo = None
173
174 if urlInfo and urlInfo.isValid() and urlInfo.isFile():
175 if urlInfo.name() in self._remoteFiles.values():
176 self.__remoteFilesFound[urlInfo.name()] = \
177 urlInfo.lastModified()
178
179 QCoreApplication.processEvents()
180
181 def __downloadFile(self, type_, fileName, timestamp):
182 """
183 Private method to downlaod the given file.
184
185 @param type_ type of the synchronization event (string one
186 of "bookmarks", "history", "passwords", "useragents" or
187 "speeddial")
188 @param fileName name of the file to be downloaded (string)
189 @param timestamp time stamp in seconds of the file to be downloaded
190 (integer)
191 """
192 self.syncStatus.emit(type_, self._messages[type_]["RemoteExists"])
193 buffer = io.BytesIO()
194 try:
195 self.__ftp.retrbinary(
196 "RETR {0}".format(self._remoteFiles[type_]),
197 lambda x: self.__downloadFileCallback(buffer, x))
198 ok, error = self.writeFile(
199 QByteArray(buffer.getvalue()), fileName, type_, timestamp)
200 if not ok:
201 self.syncStatus.emit(type_, error)
202 self.syncFinished.emit(type_, ok, True)
203 except ftplib.all_errors as err:
204 self.syncStatus.emit(type_, str(err))
205 self.syncFinished.emit(type_, False, True)
206
207 def __downloadFileCallback(self, buffer, data):
208 """
209 Private method receiving the downloaded data.
210
211 @param buffer reference to the buffer (io.BytesIO)
212 @param data byte string to store in the buffer (bytes)
213 @return number of bytes written to the buffer (integer)
214 """
215 res = buffer.write(data)
216 QCoreApplication.processEvents()
217 return res
218
219 def __uploadFile(self, type_, fileName):
220 """
221 Private method to upload the given file.
222
223 @param type_ type of the synchronization event (string one
224 of "bookmarks", "history", "passwords", "useragents" or
225 "speeddial")
226 @param fileName name of the file to be uploaded (string)
227 @return flag indicating success (boolean)
228 """
229 res = False
230 data = self.readFile(fileName, type_)
231 if data.isEmpty():
232 self.syncStatus.emit(type_, self._messages[type_]["LocalMissing"])
233 self.syncFinished.emit(type_, False, False)
234 else:
235 buffer = io.BytesIO(data.data())
236 try:
237 self.__ftp.storbinary(
238 "STOR {0}".format(self._remoteFiles[type_]),
239 buffer,
240 callback=lambda x: QCoreApplication.processEvents())
241 self.syncFinished.emit(type_, True, False)
242 res = True
243 except ftplib.all_errors as err:
244 self.syncStatus.emit(type_, str(err))
245 self.syncFinished.emit(type_, False, False)
246 return res
247
248 def __initialSyncFile(self, type_, fileName):
249 """
250 Private method to do the initial synchronization of the given file.
251
252 @param type_ type of the synchronization event (string one
253 of "bookmarks", "history", "passwords", "useragents" or
254 "speeddial")
255 @param fileName name of the file to be synchronized (string)
256 """
257 if not self.__forceUpload and \
258 self._remoteFiles[type_] in self.__remoteFilesFound:
259 if QFileInfo(fileName).lastModified() < \
260 self.__remoteFilesFound[self._remoteFiles[type_]]:
261 self.__downloadFile(
262 type_, fileName,
263 self.__remoteFilesFound[self._remoteFiles[type_]]
264 .toTime_t())
265 else:
266 self.syncStatus.emit(
267 type_, self.tr("No synchronization required."))
268 self.syncFinished.emit(type_, True, True)
269 else:
270 if self._remoteFiles[type_] not in self.__remoteFilesFound:
271 self.syncStatus.emit(
272 type_, self._messages[type_]["RemoteMissing"])
273 else:
274 self.syncStatus.emit(
275 type_, self._messages[type_]["LocalNewer"])
276 self.__uploadFile(type_, fileName)
277
278 def __initialSync(self):
279 """
280 Private slot to do the initial synchronization.
281 """
282 # Bookmarks
283 if Preferences.getWebBrowser("SyncBookmarks"):
284 self.__initialSyncFile(
285 "bookmarks",
286 WebBrowserWindow.bookmarksManager().getFileName())
287
288 # History
289 if Preferences.getWebBrowser("SyncHistory"):
290 self.__initialSyncFile(
291 "history",
292 WebBrowserWindow.historyManager().getFileName())
293
294 # Passwords
295 if Preferences.getWebBrowser("SyncPasswords"):
296 self.__initialSyncFile(
297 "passwords",
298 WebBrowserWindow.passwordManager().getFileName())
299
300 # User Agent Settings
301 if Preferences.getWebBrowser("SyncUserAgents"):
302 self.__initialSyncFile(
303 "useragents",
304 WebBrowserWindow.userAgentsManager().getFileName())
305
306 # Speed Dial Settings
307 if Preferences.getWebBrowser("SyncSpeedDial"):
308 self.__initialSyncFile(
309 "speeddial",
310 WebBrowserWindow.speedDial().getFileName())
311
312 self.__forceUpload = False
313
314 def __syncFile(self, type_, fileName):
315 """
316 Private method to synchronize the given file.
317
318 @param type_ type of the synchronization event (string one
319 of "bookmarks", "history", "passwords", "useragents" or
320 "speeddial")
321 @param fileName name of the file to be synchronized (string)
322 """
323 if self.__state == "initializing":
324 return
325
326 # use idle timeout to check, if we are still connected
327 if self.__connected:
328 self.__idleTimeout()
329 if not self.__connected or self.__ftp.sock is None:
330 ok = self.__connectAndLogin()
331 if not ok:
332 self.syncStatus.emit(
333 type_, self.tr("Cannot log in to FTP host."))
334 return
335
336 # upload the changed file
337 self.__state = "uploading"
338 self.syncStatus.emit(type_, self._messages[type_]["Uploading"])
339 if self.__uploadFile(type_, fileName):
340 self.syncStatus.emit(
341 type_, self.tr("Synchronization finished."))
342 self.__state = "idle"
343
344 def syncBookmarks(self):
345 """
346 Public method to synchronize the bookmarks.
347 """
348 self.__syncFile(
349 "bookmarks",
350 WebBrowserWindow.bookmarksManager().getFileName())
351
352 def syncHistory(self):
353 """
354 Public method to synchronize the history.
355 """
356 self.__syncFile(
357 "history",
358 WebBrowserWindow.historyManager().getFileName())
359
360 def syncPasswords(self):
361 """
362 Public method to synchronize the passwords.
363 """
364 self.__syncFile(
365 "passwords",
366 WebBrowserWindow.passwordManager().getFileName())
367
368 def syncUserAgents(self):
369 """
370 Public method to synchronize the user agents.
371 """
372 self.__syncFile(
373 "useragents",
374 WebBrowserWindow.userAgentsManager().getFileName())
375
376 def syncSpeedDial(self):
377 """
378 Public method to synchronize the speed dial data.
379 """
380 self.__syncFile(
381 "speeddial",
382 WebBrowserWindow.speedDial().getFileName())
383
384 def shutdown(self):
385 """
386 Public method to shut down the handler.
387 """
388 if self.__idleTimer.isActive():
389 self.__idleTimer.stop()
390
391 try:
392 if self.__connected:
393 self.__ftp.quit()
394 except ftplib.all_errors:
395 pass # ignore FTP errors because we are shutting down anyway
396 self.__connected = False
397
398 def __idleTimeout(self):
399 """
400 Private slot to prevent a disconnect from the server.
401 """
402 if self.__state == "idle" and self.__connected:
403 try:
404 self.__ftp.voidcmd("NOOP")
405 except ftplib.Error as err:
406 code = err.args[0].strip()[:3]
407 if code == "421":
408 self.__connected = False
409 except IOError:
410 self.__connected = False

eric ide

mercurial