eric7/WebBrowser/Sync/FtpSyncHandler.py

branch
eric7
changeset 8312
800c432b34c8
parent 8283
3139cbc98a14
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a synchronization handler using FTP.
8 """
9
10 import ftplib # secok
11 import io
12 import contextlib
13
14 from PyQt5.QtCore import (
15 pyqtSignal, QTimer, QFileInfo, QCoreApplication, QByteArray
16 )
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().__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 @param 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 proxyType = (
88 E5FtpProxyType.NO_PROXY
89 if not Preferences.getUI("UseProxy") else
90 Preferences.getUI("ProxyType/Ftp")
91 )
92 if proxyType != E5FtpProxyType.NO_PROXY:
93 self.__ftp.setProxy(
94 proxyType,
95 Preferences.getUI("ProxyHost/Ftp"),
96 Preferences.getUI("ProxyPort/Ftp"))
97 if proxyType != E5FtpProxyType.NON_AUTHORIZING:
98 self.__ftp.setProxyAuthentication(
99 Preferences.getUI("ProxyUser/Ftp"),
100 Preferences.getUI("ProxyPassword/Ftp"),
101 Preferences.getUI("ProxyAccount/Ftp"))
102
103 QTimer.singleShot(0, self.__doFtpCommands)
104
105 def __doFtpCommands(self):
106 """
107 Private slot executing the sequence of FTP commands.
108 """
109 try:
110 ok = self.__connectAndLogin()
111 if ok:
112 self.__changeToStore()
113 self.__ftp.retrlines("LIST", self.__dirListCallback)
114 self.__initialSync()
115 self.__state = "idle"
116 self.__idleTimer.start()
117 except (ftplib.all_errors + (E5FtpProxyError,)) as err:
118 self.syncError.emit(str(err))
119
120 def __connectAndLogin(self):
121 """
122 Private method to connect to the FTP server and log in.
123
124 @return flag indicating a successful log in (boolean)
125 """
126 self.__ftp.connect(
127 Preferences.getWebBrowser("SyncFtpServer"),
128 Preferences.getWebBrowser("SyncFtpPort"),
129 timeout=5)
130 self.__ftp.login(
131 Preferences.getWebBrowser("SyncFtpUser"),
132 Preferences.getWebBrowser("SyncFtpPassword"))
133 self.__connected = True
134 return True
135
136 def __changeToStore(self):
137 """
138 Private slot to change to the storage directory.
139
140 This action will create the storage path on the server, if it
141 does not exist. Upon return, the current directory of the server
142 is the sync directory.
143 """
144 storePathList = Preferences.getWebBrowser("SyncFtpPath").replace(
145 "\\", "/").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 (
175 urlInfo and
176 urlInfo.isValid() and
177 urlInfo.isFile() and
178 urlInfo.name() in self._remoteFiles.values()
179 ):
180 self.__remoteFilesFound[urlInfo.name()] = urlInfo.lastModified()
181
182 QCoreApplication.processEvents()
183
184 def __downloadFile(self, type_, fileName, timestamp):
185 """
186 Private method to downlaod the given file.
187
188 @param type_ type of the synchronization event (string one
189 of "bookmarks", "history", "passwords", "useragents" or
190 "speeddial")
191 @param fileName name of the file to be downloaded (string)
192 @param timestamp time stamp in seconds of the file to be downloaded
193 (integer)
194 """
195 self.syncStatus.emit(type_, self._messages[type_]["RemoteExists"])
196 buffer = io.BytesIO()
197 try:
198 self.__ftp.retrbinary(
199 "RETR {0}".format(self._remoteFiles[type_]),
200 lambda x: self.__downloadFileCallback(buffer, x))
201 ok, error = self.writeFile(
202 QByteArray(buffer.getvalue()), fileName, type_, timestamp)
203 if not ok:
204 self.syncStatus.emit(type_, error)
205 self.syncFinished.emit(type_, ok, True)
206 except ftplib.all_errors as err:
207 self.syncStatus.emit(type_, str(err))
208 self.syncFinished.emit(type_, False, True)
209
210 def __downloadFileCallback(self, buffer, data):
211 """
212 Private method receiving the downloaded data.
213
214 @param buffer reference to the buffer (io.BytesIO)
215 @param data byte string to store in the buffer (bytes)
216 @return number of bytes written to the buffer (integer)
217 """
218 res = buffer.write(data)
219 QCoreApplication.processEvents()
220 return res
221
222 def __uploadFile(self, type_, fileName):
223 """
224 Private method to upload the given file.
225
226 @param type_ type of the synchronization event (string one
227 of "bookmarks", "history", "passwords", "useragents" or
228 "speeddial")
229 @param fileName name of the file to be uploaded (string)
230 @return flag indicating success (boolean)
231 """
232 res = False
233 data = self.readFile(fileName, type_)
234 if data.isEmpty():
235 self.syncStatus.emit(type_, self._messages[type_]["LocalMissing"])
236 self.syncFinished.emit(type_, False, False)
237 else:
238 buffer = io.BytesIO(data.data())
239 try:
240 self.__ftp.storbinary(
241 "STOR {0}".format(self._remoteFiles[type_]),
242 buffer,
243 callback=lambda x: QCoreApplication.processEvents())
244 self.syncFinished.emit(type_, True, False)
245 res = True
246 except ftplib.all_errors as err:
247 self.syncStatus.emit(type_, str(err))
248 self.syncFinished.emit(type_, False, False)
249 return res
250
251 def __initialSyncFile(self, type_, fileName):
252 """
253 Private method to do the initial synchronization of the given file.
254
255 @param type_ type of the synchronization event (string one
256 of "bookmarks", "history", "passwords", "useragents" or
257 "speeddial")
258 @param fileName name of the file to be synchronized (string)
259 """
260 if (
261 not self.__forceUpload and
262 self._remoteFiles[type_] in self.__remoteFilesFound
263 ):
264 if (
265 QFileInfo(fileName).lastModified() <
266 self.__remoteFilesFound[self._remoteFiles[type_]]
267 ):
268 self.__downloadFile(
269 type_, fileName,
270 self.__remoteFilesFound[self._remoteFiles[type_]]
271 .toTime_t())
272 else:
273 self.syncStatus.emit(
274 type_, self.tr("No synchronization required."))
275 self.syncFinished.emit(type_, True, True)
276 else:
277 if self._remoteFiles[type_] not in self.__remoteFilesFound:
278 self.syncStatus.emit(
279 type_, self._messages[type_]["RemoteMissing"])
280 else:
281 self.syncStatus.emit(
282 type_, self._messages[type_]["LocalNewer"])
283 self.__uploadFile(type_, fileName)
284
285 def __initialSync(self):
286 """
287 Private slot to do the initial synchronization.
288 """
289 # Bookmarks
290 if Preferences.getWebBrowser("SyncBookmarks"):
291 self.__initialSyncFile(
292 "bookmarks",
293 WebBrowserWindow.bookmarksManager().getFileName())
294
295 # History
296 if Preferences.getWebBrowser("SyncHistory"):
297 self.__initialSyncFile(
298 "history",
299 WebBrowserWindow.historyManager().getFileName())
300
301 # Passwords
302 if Preferences.getWebBrowser("SyncPasswords"):
303 self.__initialSyncFile(
304 "passwords",
305 WebBrowserWindow.passwordManager().getFileName())
306
307 # User Agent Settings
308 if Preferences.getWebBrowser("SyncUserAgents"):
309 self.__initialSyncFile(
310 "useragents",
311 WebBrowserWindow.userAgentsManager().getFileName())
312
313 # Speed Dial Settings
314 if Preferences.getWebBrowser("SyncSpeedDial"):
315 self.__initialSyncFile(
316 "speeddial",
317 WebBrowserWindow.speedDial().getFileName())
318
319 self.__forceUpload = False
320
321 def __syncFile(self, type_, fileName):
322 """
323 Private method to synchronize the given file.
324
325 @param type_ type of the synchronization event (string one
326 of "bookmarks", "history", "passwords", "useragents" or
327 "speeddial")
328 @param fileName name of the file to be synchronized (string)
329 """
330 if self.__state == "initializing":
331 return
332
333 # use idle timeout to check, if we are still connected
334 if self.__connected:
335 self.__idleTimeout()
336 if not self.__connected or self.__ftp.sock is None:
337 ok = self.__connectAndLogin()
338 if not ok:
339 self.syncStatus.emit(
340 type_, self.tr("Cannot log in to FTP host."))
341 return
342
343 # upload the changed file
344 self.__state = "uploading"
345 self.syncStatus.emit(type_, self._messages[type_]["Uploading"])
346 if self.__uploadFile(type_, fileName):
347 self.syncStatus.emit(
348 type_, self.tr("Synchronization finished."))
349 self.__state = "idle"
350
351 def syncBookmarks(self):
352 """
353 Public method to synchronize the bookmarks.
354 """
355 self.__syncFile(
356 "bookmarks",
357 WebBrowserWindow.bookmarksManager().getFileName())
358
359 def syncHistory(self):
360 """
361 Public method to synchronize the history.
362 """
363 self.__syncFile(
364 "history",
365 WebBrowserWindow.historyManager().getFileName())
366
367 def syncPasswords(self):
368 """
369 Public method to synchronize the passwords.
370 """
371 self.__syncFile(
372 "passwords",
373 WebBrowserWindow.passwordManager().getFileName())
374
375 def syncUserAgents(self):
376 """
377 Public method to synchronize the user agents.
378 """
379 self.__syncFile(
380 "useragents",
381 WebBrowserWindow.userAgentsManager().getFileName())
382
383 def syncSpeedDial(self):
384 """
385 Public method to synchronize the speed dial data.
386 """
387 self.__syncFile(
388 "speeddial",
389 WebBrowserWindow.speedDial().getFileName())
390
391 def shutdown(self):
392 """
393 Public method to shut down the handler.
394 """
395 if self.__idleTimer.isActive():
396 self.__idleTimer.stop()
397
398 with contextlib.suppress(ftplib.all_errors):
399 if self.__connected:
400 self.__ftp.quit()
401 self.__connected = False
402
403 def __idleTimeout(self):
404 """
405 Private slot to prevent a disconnect from the server.
406 """
407 if self.__state == "idle" and self.__connected:
408 try:
409 self.__ftp.voidcmd("NOOP")
410 except ftplib.Error as err:
411 code = err.args[0].strip()[:3]
412 if code == "421":
413 self.__connected = False
414 except OSError:
415 self.__connected = False

eric ide

mercurial