src/eric7/WebBrowser/Sync/FtpSyncHandler.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9186
0c28a1670e06
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2022 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 import pathlib
14
15 from PyQt6.QtCore import pyqtSignal, QTimer, QCoreApplication, QByteArray
16
17 from EricNetwork.EricFtp import EricFtp, EricFtpProxyType, EricFtpProxyError
18
19 from .SyncHandler import SyncHandler
20
21 from WebBrowser.WebBrowserWindow import WebBrowserWindow
22
23 import Preferences
24
25 from Utilities.FtpUtilities import FtpDirLineParser, FtpDirLineParserError
26
27
28 class FtpSyncHandler(SyncHandler):
29 """
30 Class implementing a synchronization handler using FTP.
31
32 @signal syncStatus(type_, message) emitted to indicate the synchronization
33 status (string one of "bookmarks", "history", "passwords",
34 "useragents" or "speeddial", string)
35 @signal syncError(message) emitted for a general error with the error
36 message (string)
37 @signal syncMessage(message) emitted to send a message about
38 synchronization (string)
39 @signal syncFinished(type_, done, download) emitted after a
40 synchronization has finished (string one of "bookmarks", "history",
41 "passwords", "useragents" or "speeddial", boolean, boolean)
42 """
43 syncStatus = pyqtSignal(str, str)
44 syncError = pyqtSignal(str)
45 syncMessage = pyqtSignal(str)
46 syncFinished = pyqtSignal(str, bool, bool)
47
48 def __init__(self, parent=None):
49 """
50 Constructor
51
52 @param parent reference to the parent object (QObject)
53 """
54 super().__init__(parent)
55
56 self.__state = "idle"
57 self.__forceUpload = False
58 self.__connected = False
59
60 self.__remoteFilesFound = {}
61
62 def initialLoadAndCheck(self, forceUpload):
63 """
64 Public method to do the initial check.
65
66 @param forceUpload flag indicating a forced upload of the files
67 (boolean)
68 """
69 if not Preferences.getWebBrowser("SyncEnabled"):
70 return
71
72 self.__state = "initializing"
73 self.__forceUpload = forceUpload
74
75 self.__dirLineParser = FtpDirLineParser()
76 self.__remoteFilesFound = {}
77
78 self.__idleTimer = QTimer(self)
79 self.__idleTimer.setInterval(
80 Preferences.getWebBrowser("SyncFtpIdleTimeout") * 1000)
81 self.__idleTimer.timeout.connect(self.__idleTimeout)
82
83 self.__ftp = EricFtp()
84
85 # do proxy setup
86 proxyType = (
87 EricFtpProxyType.NO_PROXY
88 if not Preferences.getUI("UseProxy") else
89 Preferences.getUI("ProxyType/Ftp")
90 )
91 if proxyType != EricFtpProxyType.NO_PROXY:
92 self.__ftp.setProxy(
93 proxyType,
94 Preferences.getUI("ProxyHost/Ftp"),
95 Preferences.getUI("ProxyPort/Ftp"))
96 if proxyType != EricFtpProxyType.NON_AUTHORIZING:
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 + (EricFtpProxyError,)) 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 = Preferences.getWebBrowser("SyncFtpPath").replace(
144 "\\", "/").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 (
174 urlInfo and
175 urlInfo.isValid() and
176 urlInfo.isFile() and
177 urlInfo.name() in self._remoteFiles.values()
178 ):
179 self.__remoteFilesFound[urlInfo.name()] = urlInfo.lastModified()
180
181 QCoreApplication.processEvents()
182
183 def __downloadFile(self, type_, fileName, timestamp):
184 """
185 Private method to downlaod the given file.
186
187 @param type_ type of the synchronization event (string one
188 of "bookmarks", "history", "passwords", "useragents" or
189 "speeddial")
190 @param fileName name of the file to be downloaded (string)
191 @param timestamp time stamp in seconds of the file to be downloaded
192 (integer)
193 """
194 self.syncStatus.emit(type_, self._messages[type_]["RemoteExists"])
195 buffer = io.BytesIO()
196 try:
197 self.__ftp.retrbinary(
198 "RETR {0}".format(self._remoteFiles[type_]),
199 lambda x: self.__downloadFileCallback(buffer, x))
200 ok, error = self.writeFile(
201 QByteArray(buffer.getvalue()), fileName, type_, timestamp)
202 if not ok:
203 self.syncStatus.emit(type_, error)
204 self.syncFinished.emit(type_, ok, True)
205 except ftplib.all_errors as err:
206 self.syncStatus.emit(type_, str(err))
207 self.syncFinished.emit(type_, False, True)
208
209 def __downloadFileCallback(self, buffer, data):
210 """
211 Private method receiving the downloaded data.
212
213 @param buffer reference to the buffer (io.BytesIO)
214 @param data byte string to store in the buffer (bytes)
215 @return number of bytes written to the buffer (integer)
216 """
217 res = buffer.write(data)
218 QCoreApplication.processEvents()
219 return res
220
221 def __uploadFile(self, type_, fileName):
222 """
223 Private method to upload the given file.
224
225 @param type_ type of the synchronization event (string one
226 of "bookmarks", "history", "passwords", "useragents" or
227 "speeddial")
228 @param fileName name of the file to be uploaded (string)
229 @return flag indicating success (boolean)
230 """
231 res = False
232 data = self.readFile(fileName, type_)
233 if data.isEmpty():
234 self.syncStatus.emit(type_, self._messages[type_]["LocalMissing"])
235 self.syncFinished.emit(type_, False, False)
236 else:
237 buffer = io.BytesIO(data.data())
238 try:
239 self.__ftp.storbinary(
240 "STOR {0}".format(self._remoteFiles[type_]),
241 buffer,
242 callback=lambda x: QCoreApplication.processEvents())
243 self.syncFinished.emit(type_, True, False)
244 res = True
245 except ftplib.all_errors as err:
246 self.syncStatus.emit(type_, str(err))
247 self.syncFinished.emit(type_, False, False)
248 return res
249
250 def __initialSyncFile(self, type_, fileName):
251 """
252 Private method to do the initial synchronization of the given file.
253
254 @param type_ type of the synchronization event (string one
255 of "bookmarks", "history", "passwords", "useragents" or
256 "speeddial")
257 @param fileName name of the file to be synchronized (string)
258 """
259 if (
260 not self.__forceUpload and
261 self._remoteFiles[type_] in self.__remoteFilesFound
262 ):
263 if (
264 not pathlib.Path(fileName).exists() or
265 pathlib.Path(fileName).stat().st_mtime <
266 self.__remoteFilesFound[
267 self._remoteFiles[type_].toSecsSinceEpoch()]
268 ):
269 self.__downloadFile(
270 type_, fileName,
271 self.__remoteFilesFound[self._remoteFiles[type_]]
272 .toTime_t())
273 else:
274 self.syncStatus.emit(
275 type_, self.tr("No synchronization required."))
276 self.syncFinished.emit(type_, True, True)
277 else:
278 if self._remoteFiles[type_] not in self.__remoteFilesFound:
279 self.syncStatus.emit(
280 type_, self._messages[type_]["RemoteMissing"])
281 else:
282 self.syncStatus.emit(
283 type_, self._messages[type_]["LocalNewer"])
284 self.__uploadFile(type_, fileName)
285
286 def __initialSync(self):
287 """
288 Private slot to do the initial synchronization.
289 """
290 # Bookmarks
291 if Preferences.getWebBrowser("SyncBookmarks"):
292 self.__initialSyncFile(
293 "bookmarks",
294 WebBrowserWindow.bookmarksManager().getFileName())
295
296 # History
297 if Preferences.getWebBrowser("SyncHistory"):
298 self.__initialSyncFile(
299 "history",
300 WebBrowserWindow.historyManager().getFileName())
301
302 # Passwords
303 if Preferences.getWebBrowser("SyncPasswords"):
304 self.__initialSyncFile(
305 "passwords",
306 WebBrowserWindow.passwordManager().getFileName())
307
308 # User Agent Settings
309 if Preferences.getWebBrowser("SyncUserAgents"):
310 self.__initialSyncFile(
311 "useragents",
312 WebBrowserWindow.userAgentsManager().getFileName())
313
314 # Speed Dial Settings
315 if Preferences.getWebBrowser("SyncSpeedDial"):
316 self.__initialSyncFile(
317 "speeddial",
318 WebBrowserWindow.speedDial().getFileName())
319
320 self.__forceUpload = False
321
322 def __syncFile(self, type_, fileName):
323 """
324 Private method to synchronize the given file.
325
326 @param type_ type of the synchronization event (string one
327 of "bookmarks", "history", "passwords", "useragents" or
328 "speeddial")
329 @param fileName name of the file to be synchronized (string)
330 """
331 if self.__state == "initializing":
332 return
333
334 # use idle timeout to check, if we are still connected
335 if self.__connected:
336 self.__idleTimeout()
337 if not self.__connected or self.__ftp.sock is None:
338 ok = self.__connectAndLogin()
339 if not ok:
340 self.syncStatus.emit(
341 type_, self.tr("Cannot log in to FTP host."))
342 return
343
344 # upload the changed file
345 self.__state = "uploading"
346 self.syncStatus.emit(type_, self._messages[type_]["Uploading"])
347 if self.__uploadFile(type_, fileName):
348 self.syncStatus.emit(
349 type_, self.tr("Synchronization finished."))
350 self.__state = "idle"
351
352 def syncBookmarks(self):
353 """
354 Public method to synchronize the bookmarks.
355 """
356 self.__syncFile(
357 "bookmarks",
358 WebBrowserWindow.bookmarksManager().getFileName())
359
360 def syncHistory(self):
361 """
362 Public method to synchronize the history.
363 """
364 self.__syncFile(
365 "history",
366 WebBrowserWindow.historyManager().getFileName())
367
368 def syncPasswords(self):
369 """
370 Public method to synchronize the passwords.
371 """
372 self.__syncFile(
373 "passwords",
374 WebBrowserWindow.passwordManager().getFileName())
375
376 def syncUserAgents(self):
377 """
378 Public method to synchronize the user agents.
379 """
380 self.__syncFile(
381 "useragents",
382 WebBrowserWindow.userAgentsManager().getFileName())
383
384 def syncSpeedDial(self):
385 """
386 Public method to synchronize the speed dial data.
387 """
388 self.__syncFile(
389 "speeddial",
390 WebBrowserWindow.speedDial().getFileName())
391
392 def shutdown(self):
393 """
394 Public method to shut down the handler.
395 """
396 if self.__idleTimer.isActive():
397 self.__idleTimer.stop()
398
399 with contextlib.suppress(ftplib.all_errors):
400 if self.__connected:
401 self.__ftp.quit()
402 self.__connected = False
403
404 def __idleTimeout(self):
405 """
406 Private slot to prevent a disconnect from the server.
407 """
408 if self.__state == "idle" and self.__connected:
409 try:
410 self.__ftp.voidcmd("NOOP")
411 except ftplib.Error as err:
412 code = err.args[0].strip()[:3]
413 if code == "421":
414 self.__connected = False
415 except OSError:
416 self.__connected = False

eric ide

mercurial