src/eric7/WebBrowser/Sync/FtpSyncHandler.py

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

eric ide

mercurial