|
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 |