eric6/Helpviewer/Sync/FtpSyncHandler.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
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 import Helpviewer.HelpWindow
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.getHelp("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.getHelp("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.getHelp("SyncFtpServer"),
127 Preferences.getHelp("SyncFtpPort"),
128 timeout=5)
129 self.__ftp.login(
130 Preferences.getHelp("SyncFtpUser"),
131 Preferences.getHelp("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.getHelp("SyncFtpPath").replace("\\", "/").split("/")
145 if storePathList[0] == "":
146 storePathList.pop(0)
147 while storePathList:
148 path = storePathList[0]
149 try:
150 self.__ftp.cwd(path)
151 except ftplib.error_perm as err:
152 code = err.args[0].strip()[:3]
153 if code == "550":
154 # path does not exist, create it
155 self.__ftp.mkd(path)
156 self.__ftp.cwd(path)
157 else:
158 raise
159 storePathList.pop(0)
160
161 def __dirListCallback(self, line):
162 """
163 Private slot handling the receipt of directory listing lines.
164
165 @param line the received line of the directory listing (string)
166 """
167 try:
168 urlInfo = self.__dirLineParser.parseLine(line)
169 except FtpDirLineParserError:
170 # silently ignore parser errors
171 urlInfo = None
172
173 if urlInfo and urlInfo.isValid() and urlInfo.isFile():
174 if urlInfo.name() in self._remoteFiles.values():
175 self.__remoteFilesFound[urlInfo.name()] = \
176 urlInfo.lastModified()
177
178 QCoreApplication.processEvents()
179
180 def __downloadFile(self, type_, fileName, timestamp):
181 """
182 Private method to downlaod the given file.
183
184 @param type_ type of the synchronization event (string one
185 of "bookmarks", "history", "passwords", "useragents" or
186 "speeddial")
187 @param fileName name of the file to be downloaded (string)
188 @param timestamp time stamp in seconds of the file to be downloaded
189 (integer)
190 """
191 self.syncStatus.emit(type_, self._messages[type_]["RemoteExists"])
192 buffer = io.BytesIO()
193 try:
194 self.__ftp.retrbinary(
195 "RETR {0}".format(self._remoteFiles[type_]),
196 lambda x: self.__downloadFileCallback(buffer, x))
197 ok, error = self.writeFile(
198 QByteArray(buffer.getvalue()), fileName, type_, timestamp)
199 if not ok:
200 self.syncStatus.emit(type_, error)
201 self.syncFinished.emit(type_, ok, True)
202 except ftplib.all_errors as err:
203 self.syncStatus.emit(type_, str(err))
204 self.syncFinished.emit(type_, False, True)
205
206 def __downloadFileCallback(self, buffer, data):
207 """
208 Private method receiving the downloaded data.
209
210 @param buffer reference to the buffer (io.BytesIO)
211 @param data byte string to store in the buffer (bytes)
212 @return number of bytes written to the buffer (integer)
213 """
214 res = buffer.write(data)
215 QCoreApplication.processEvents()
216 return res
217
218 def __uploadFile(self, type_, fileName):
219 """
220 Private method to upload the given file.
221
222 @param type_ type of the synchronization event (string one
223 of "bookmarks", "history", "passwords", "useragents" or
224 "speeddial")
225 @param fileName name of the file to be uploaded (string)
226 @return flag indicating success (boolean)
227 """
228 res = False
229 data = self.readFile(fileName, type_)
230 if data.isEmpty():
231 self.syncStatus.emit(type_, self._messages[type_]["LocalMissing"])
232 self.syncFinished.emit(type_, False, False)
233 else:
234 buffer = io.BytesIO(data.data())
235 try:
236 self.__ftp.storbinary(
237 "STOR {0}".format(self._remoteFiles[type_]),
238 buffer,
239 callback=lambda x: QCoreApplication.processEvents())
240 self.syncFinished.emit(type_, True, False)
241 res = True
242 except ftplib.all_errors as err:
243 self.syncStatus.emit(type_, str(err))
244 self.syncFinished.emit(type_, False, False)
245 return res
246
247 def __initialSyncFile(self, type_, fileName):
248 """
249 Private method to do the initial synchronization of the given file.
250
251 @param type_ type of the synchronization event (string one
252 of "bookmarks", "history", "passwords", "useragents" or
253 "speeddial")
254 @param fileName name of the file to be synchronized (string)
255 """
256 if not self.__forceUpload and \
257 self._remoteFiles[type_] in self.__remoteFilesFound:
258 if QFileInfo(fileName).lastModified() < \
259 self.__remoteFilesFound[self._remoteFiles[type_]]:
260 self.__downloadFile(
261 type_, fileName,
262 self.__remoteFilesFound[self._remoteFiles[type_]]
263 .toTime_t())
264 else:
265 self.syncStatus.emit(
266 type_, self.tr("No synchronization required."))
267 self.syncFinished.emit(type_, True, True)
268 else:
269 if self._remoteFiles[type_] not in self.__remoteFilesFound:
270 self.syncStatus.emit(
271 type_, self._messages[type_]["RemoteMissing"])
272 else:
273 self.syncStatus.emit(
274 type_, self._messages[type_]["LocalNewer"])
275 self.__uploadFile(type_, fileName)
276
277 def __initialSync(self):
278 """
279 Private slot to do the initial synchronization.
280 """
281 # Bookmarks
282 if Preferences.getHelp("SyncBookmarks"):
283 self.__initialSyncFile(
284 "bookmarks",
285 Helpviewer.HelpWindow.HelpWindow.bookmarksManager()
286 .getFileName())
287
288 # History
289 if Preferences.getHelp("SyncHistory"):
290 self.__initialSyncFile(
291 "history",
292 Helpviewer.HelpWindow.HelpWindow.historyManager()
293 .getFileName())
294
295 # Passwords
296 if Preferences.getHelp("SyncPasswords"):
297 self.__initialSyncFile(
298 "passwords",
299 Helpviewer.HelpWindow.HelpWindow.passwordManager()
300 .getFileName())
301
302 # User Agent Settings
303 if Preferences.getHelp("SyncUserAgents"):
304 self.__initialSyncFile(
305 "useragents",
306 Helpviewer.HelpWindow.HelpWindow.userAgentsManager()
307 .getFileName())
308
309 # Speed Dial Settings
310 if Preferences.getHelp("SyncSpeedDial"):
311 self.__initialSyncFile(
312 "speeddial",
313 Helpviewer.HelpWindow.HelpWindow.speedDial().getFileName())
314
315 self.__forceUpload = False
316
317 def __syncFile(self, type_, fileName):
318 """
319 Private method to synchronize the given file.
320
321 @param type_ type of the synchronization event (string one
322 of "bookmarks", "history", "passwords", "useragents" or
323 "speeddial")
324 @param fileName name of the file to be synchronized (string)
325 """
326 if self.__state == "initializing":
327 return
328
329 # use idle timeout to check, if we are still connected
330 if self.__connected:
331 self.__idleTimeout()
332 if not self.__connected or self.__ftp.sock is None:
333 ok = self.__connectAndLogin()
334 if not ok:
335 self.syncStatus.emit(
336 type_, self.tr("Cannot log in to FTP host."))
337 return
338
339 # upload the changed file
340 self.__state = "uploading"
341 self.syncStatus.emit(type_, self._messages[type_]["Uploading"])
342 if self.__uploadFile(type_, fileName):
343 self.syncStatus.emit(
344 type_, self.tr("Synchronization finished."))
345 self.__state = "idle"
346
347 def syncBookmarks(self):
348 """
349 Public method to synchronize the bookmarks.
350 """
351 self.__syncFile(
352 "bookmarks",
353 Helpviewer.HelpWindow.HelpWindow.bookmarksManager().getFileName())
354
355 def syncHistory(self):
356 """
357 Public method to synchronize the history.
358 """
359 self.__syncFile(
360 "history",
361 Helpviewer.HelpWindow.HelpWindow.historyManager().getFileName())
362
363 def syncPasswords(self):
364 """
365 Public method to synchronize the passwords.
366 """
367 self.__syncFile(
368 "passwords",
369 Helpviewer.HelpWindow.HelpWindow.passwordManager().getFileName())
370
371 def syncUserAgents(self):
372 """
373 Public method to synchronize the user agents.
374 """
375 self.__syncFile(
376 "useragents",
377 Helpviewer.HelpWindow.HelpWindow.userAgentsManager().getFileName())
378
379 def syncSpeedDial(self):
380 """
381 Public method to synchronize the speed dial data.
382 """
383 self.__syncFile(
384 "speeddial",
385 Helpviewer.HelpWindow.HelpWindow.speedDial().getFileName())
386
387 def shutdown(self):
388 """
389 Public method to shut down the handler.
390 """
391 if self.__idleTimer.isActive():
392 self.__idleTimer.stop()
393
394 try:
395 if self.__connected:
396 self.__ftp.quit()
397 except ftplib.all_errors:
398 pass # ignore FTP errors because we are shutting down anyway
399 self.__connected = False
400
401 def __idleTimeout(self):
402 """
403 Private slot to prevent a disconnect from the server.
404 """
405 if self.__state == "idle" and self.__connected:
406 try:
407 self.__ftp.voidcmd("NOOP")
408 except ftplib.Error as err:
409 code = err.args[0].strip()[:3]
410 if code == "421":
411 self.__connected = False
412 except IOError:
413 self.__connected = False

eric ide

mercurial