work around disappearing gui problem :-)
[enigma2-plugins.git] / ftpbrowser / src / FTPBrowser.py
1 # for localized messages
2 from . import _
3
4 # Core
5 from enigma import RT_HALIGN_LEFT, eListboxPythonMultiContent
6
7 # Tools
8 from Tools.Directories import SCOPE_SKIN_IMAGE, resolveFilename
9 from Tools.LoadPixmap import LoadPixmap
10 from Tools.Notifications import AddPopup, AddNotificationWithCallback
11
12 # GUI (Screens)
13 from Screens.Screen import Screen
14 from Screens.HelpMenu import HelpableScreen
15 from Screens.MessageBox import MessageBox
16 from Screens.ChoiceBox import ChoiceBox
17 from Screens.InfoBarGenerics import InfoBarNotifications
18 from FTPServerManager import FTPServerManager
19 from FTPQueueManager import FTPQueueManager
20 from NTIVirtualKeyBoard import NTIVirtualKeyBoard
21
22 # GUI (Components)
23 from Components.ActionMap import ActionMap, HelpableActionMap
24 from Components.Label import Label
25 from Components.FileList import FileList, FileEntryComponent, EXTENSIONS
26 from Components.Button import Button
27 from VariableProgressSource import VariableProgressSource
28
29 # FTP Client
30 from twisted.internet import reactor, defer
31 from twisted.internet.protocol import Protocol, ClientCreator
32 from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
33 from twisted.protocols.basic import FileSender
34
35 # System
36 from os import path as os_path, unlink as os_unlink, rename as os_rename, \
37                 listdir as os_listdir
38 from time import time
39 import re
40
41 def FTPFileEntryComponent(file, directory):
42         isDir = True if file['filetype'] == 'd' else False
43         name = file['filename']
44         absolute = directory + name
45         if isDir:
46                 absolute += '/'
47
48         res = [
49                 (absolute, isDir, file['size']),
50                 (eListboxPythonMultiContent.TYPE_TEXT, 35, 1, 470, 20, 0, RT_HALIGN_LEFT, name)
51         ]
52         if isDir:
53                 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/directory.png"))
54         else:
55                 extension = name.split('.')
56                 extension = extension[-1].lower()
57                 if EXTENSIONS.has_key(extension):
58                         png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/" + EXTENSIONS[extension] + ".png"))
59                 else:
60                         png = None
61         if png is not None:
62                 res.append((eListboxPythonMultiContent.TYPE_PIXMAP_ALPHATEST, 10, 2, 20, 20, png))
63
64         return res
65
66 class ModifiedFTPFileListProtocol(FTPFileListProtocol):
67     fileLinePattern = re.compile(
68         r'^(?P<filetype>.)(?P<perms>.{9})\s+(?P<nlinks>\d*)\s*'
69         r'(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)\s+'
70         r'(?P<date>...\s+\d+\s+[\d:]+)\s+(?P<filename>.*?)'
71         r'( -> (?P<linktarget>[^\r]*))?\r?$'
72     )
73
74 class FTPFileList(FileList):
75         def __init__(self):
76                 self.ftpclient = None
77                 self.select = None
78                 self.isValid = False
79                 FileList.__init__(self, "/")
80
81         def changeDir(self, directory, select = None):
82                 if not directory:
83                         return
84
85                 if self.ftpclient is None:
86                         self.list = []
87                         self.l.setList(self.list)
88                         return
89
90                 self.current_directory = directory
91                 self.select = select
92
93                 self.filelist = ModifiedFTPFileListProtocol()
94                 d = self.ftpclient.list(directory, self.filelist)
95                 d.addCallback(self.listRcvd).addErrback(self.listFailed)
96
97         def listRcvd(self, *args):
98                 # TODO: is any of the 'advanced' features useful (and more of all can they be implemented) here?
99                 list = [FTPFileEntryComponent(file, self.current_directory) for file in self.filelist.files]
100                 list.sort(key = lambda x: (not x[0][1], x[0][0]))
101                 if self.current_directory != "/":
102                         list.insert(0, FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True))
103
104                 self.isValid = True
105                 self.l.setList(list)
106                 self.list = list
107
108                 select = self.select
109                 if select is not None:
110                         i = 0
111                         self.moveToIndex(0)
112                         for x in list:
113                                 p = x[0][0]
114
115                                 if p == select:
116                                         self.moveToIndex(i)
117                                         break
118                                 i += 1
119
120         def listFailed(self, *args):
121                 # XXX: we might end up here if login fails, we might want to add some check for this (e.g. send a dummy command before doing actual work)
122                 if self.current_directory != "/":
123                         self.list = [
124                                 FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True),
125                                 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
126                         ]
127                 else:
128                         self.list = [
129                                 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
130                         ]
131
132                 self.isValid = False
133                 self.l.setList(self.list)
134
135 class FTPBrowser(Screen, Protocol, InfoBarNotifications, HelpableScreen):
136         skin = """
137                 <screen name="FTPBrowser" position="center,center" size="560,440" title="FTP Browser">
138                         <widget name="localText" position="20,10" size="200,20" font="Regular;18" />
139                         <widget name="local" position="20,40" size="255,320" scrollbarMode="showOnDemand" />
140                         <widget name="remoteText" position="285,10" size="200,20" font="Regular;18" />
141                         <widget name="remote" position="285,40" size="255,320" scrollbarMode="showOnDemand" />
142                         <widget name="eta" position="20,360" size="200,30" font="Regular;23" />
143                         <widget name="speed" position="330,360" size="200,30" halign="right" font="Regular;23" />
144                         <widget source="progress" render="Progress" position="20,390" size="520,10" />
145                         <ePixmap name="green" position="10,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
146                         <ePixmap name="yellow" position="180,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
147                         <ePixmap name="blue" position="350,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
148                         <widget name="key_green" position="10,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
149                         <widget name="key_yellow" position="180,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
150                         <widget name="key_blue" position="350,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
151                         <ePixmap position="515,408" zPosition="1" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on" />
152                 </screen>"""
153
154         def __init__(self, session):
155                 Screen.__init__(self, session)
156                 HelpableScreen.__init__(self)
157                 InfoBarNotifications.__init__(self)
158                 self.ftpclient = None
159                 self.queueManagerInstance = None
160                 self.file = None
161                 self.queue = None
162                 self.currlist = "local"
163
164                 # # NOTE: having self.checkNotifications in onExecBegin might make our gui
165                 # disappear, so let's move it to onShow
166                 self.onExecBegin.remove(self.checkNotifications)
167                 self.onShow.append(self.checkNotifications)
168
169                 # Init what we need for dl progress
170                 self.currentLength = 0
171                 self.lastLength = 0
172                 self.lastTime = 0
173                 self.lastApprox = 0
174                 self.fileSize = 0
175
176                 self["localText"] = Label(_("Local"))
177                 self["local"] = FileList("/media/hdd/", showMountpoints = False)
178                 self["remoteText"] = Label(_("Remote (not connected)"))
179                 self["remote"] = FTPFileList()
180                 self["eta"] = Label("")
181                 self["speed"] = Label("")
182                 self["progress"] = VariableProgressSource()
183                 self["key_red"] = Button(_("Exit"))
184                 self["key_green"] = Button(_("Rename"))
185                 self["key_yellow"] = Button(_("Delete"))
186                 self["key_blue"] = Button(_("Upload"))
187
188                 self.server = None
189
190                 self["ftpbrowserBaseActions"] = HelpableActionMap(self, "ftpbrowserBaseActions",
191                         {
192                                 "ok": (self.ok, _("enter directory/get file/put file")),
193                                 "cancel": (self.cancel , _("close")),
194                                 "menu": (self.menu, _("open menu")),
195                         }, -2)
196
197                 self["ftpbrowserListActions"] = HelpableActionMap(self, "ftpbrowserListActions",
198                         {
199                                 "channelUp": (self.setLocal, _("Select local file list")),
200                                 "channelDown": (self.setRemote, _("Select remote file list")),
201                         })
202
203                 self["actions"] = ActionMap(["ftpbrowserDirectionActions", "ColorActions"],
204                         {
205                                 "up": self.up,
206                                 "down": self.down,
207                                 "left": self.left,
208                                 "right": self.right,
209                                 "green": self.rename,
210                                 "yellow": self.delete,
211                                 "blue": self.transfer,
212                         }, -2)
213
214                 self.onExecBegin.append(self.reinitialize)
215
216         def reinitialize(self):
217                 # NOTE: this will clear the remote file list if we are not currently connected. this behavior is intended.
218                 # XXX: but do we also want to do this when we just returned from a notification?
219                 try:
220                         self["remote"].refresh()
221                 except AttributeError, ae:
222                         # NOTE: we assume the connection was timed out by the server
223                         self.ftpclient = None
224                         self["remote"].ftpclient = None
225                         self["remote"].refresh()
226
227                 self["local"].refresh()
228
229                 if not self.ftpclient:
230                         self.connect(self.server)
231                 # XXX: Actually everything else should be taken care of... recheck this!
232
233         def serverManagerCallback(self, uri):
234                 if uri:
235                         self.connect(uri)
236
237         def serverManager(self):
238                 self.session.openWithCallback(
239                         self.serverManagerCallback,
240                         FTPServerManager,
241                 )
242
243         def queueManagerCallback(self):
244                 self.queueManagerInstance = None
245
246         def queueManager(self):
247                 self.queueManagerInstance = self.session.openWithCallback(
248                         self.queueManagerCallback,
249                         FTPQueueManager,
250                         self.queue,
251                 )
252
253         def menuCallback(self, ret):
254                 ret and ret[1]()
255
256         def menu(self):
257                 self.session.openWithCallback(
258                         self.menuCallback,
259                         ChoiceBox,
260                         list = [
261                                 (_("Server Manager"), self.serverManager),
262                                 (_("Queue Manager"), self.queueManager),
263                         ]
264                 )
265
266         def setLocal(self):
267                 self.currlist = "local"
268                 self["key_blue"].setText(_("Upload"))
269
270         def setRemote(self):
271                 self.currlist = "remote"
272                 self["key_blue"].setText(_("Download"))
273
274         def okQuestion(self, res = None):
275                 if res:
276                         self.ok(force = True)
277
278         def getRemoteFile(self):
279                 remoteFile = self["remote"].getSelection()
280                 if not remoteFile or not remoteFile[0]:
281                         return None, None, None
282
283                 absRemoteFile = remoteFile[0]
284                 if remoteFile[1]:
285                         fileName = absRemoteFile.split('/')[-2]
286                 else:
287                         fileName = absRemoteFile.split('/')[-1]
288
289                 if len(remoteFile) == 3:
290                         fileSize = remoteFile[2]
291                 else:
292                         fileSize = 0
293
294                 return absRemoteFile, fileName, fileSize
295
296         def getLocalFile(self):
297                 # XXX: isn't this supposed to be an absolute filename? well, it's not for me :-/
298                 localFile = self["local"].getSelection()
299                 if not localFile:
300                         return None, None
301
302                 if localFile[1]:
303                         absLocalFile = localFile[0]
304                         fileName = absLocalFile.split('/')[-2]
305                 else:
306                         fileName = localFile[0]
307                         absLocalFile = self["local"].getCurrentDirectory() + fileName
308
309                 return absLocalFile, fileName
310
311         def renameCallback(self, newName = None):
312                 if not newName:
313                         return
314
315                 if self.currlist == "remote":
316                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
317                         if not fileName:
318                                 return
319
320                         directory = self["remote"].getCurrentDirectory()
321                         sep = '/' if directory != '/' else ''
322                         newRemoteFile = directory + sep + newName
323
324                         def callback(ret = None):
325                                 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
326                         def errback(ret = None):
327                                 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
328
329                         self.ftpclient.rename(absRemoteFile, newRemoteFile).addCallback(callback).addErrback(errback)
330                 else:
331                         assert(self.currlist == "local")
332                         absLocalFile, fileName = self.getLocalFile()
333                         if not fileName:
334                                 return
335
336                         directory = self["local"].getCurrentDirectory()
337                         newLocalFile = os_path.join(directory, newName)
338
339                         try:
340                                 os_rename(absLocalFile, newLocalFile)
341                         except OSError, ose:
342                                 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
343                         else:
344                                 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
345
346         def rename(self):
347                 if not self.ftpclient or self.queue:
348                         return
349
350                 if self.currlist == "remote":
351                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
352                         if not fileName:
353                                 return
354                 else:
355                         assert(self.currlist == "local")
356                         absLocalFile, fileName = self.getLocalFile()
357                         if not fileName:
358                                 return
359
360                 self.session.openWithCallback(
361                         self.renameCallback,
362                         NTIVirtualKeyBoard,
363                         title = _("Enter new filename:"),
364                         text = fileName,
365                 )
366
367         def deleteConfirmed(self, ret):
368                 if not ret:
369                         return
370
371                 if self.currlist == "remote":
372                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
373                         if not fileName:
374                                 return
375
376                         def callback(ret = None):
377                                 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
378                         def errback(ret = None):
379                                 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
380
381                         self.ftpclient.removeFile(absRemoteFile).addCallback(callback).addErrback(errback)
382                 else:
383                         assert(self.currlist == "local")
384                         absLocalFile, fileName = self.getLocalFile()
385                         if not fileName:
386                                 return
387
388                         try:
389                                 os_unlink(absLocalFile)
390                         except OSError, oe:
391                                 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
392                         else:
393                                 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
394
395         def delete(self):
396                 if not self.ftpclient or self.queue:
397                         return
398
399                 if self.currlist == "remote":
400                         if self["remote"].canDescent():
401                                 self.session.open(
402                                         MessageBox,
403                                         _("Removing directories is not supported."),
404                                         MessageBox.TYPE_WARNING
405                                 )
406                                 return
407
408                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
409                         if not fileName:
410                                 return
411                 else:
412                         assert(self.currlist == "local")
413                         if self["local"].canDescent():
414                                 self.session.open(
415                                         MessageBox,
416                                         _("Removing directories is not supported."),
417                                         MessageBox.TYPE_WARNING
418                                 )
419                                 return
420
421                         absLocalFile, fileName = self.getLocalFile()
422                         if not fileName:
423                                 return
424
425                 self.session.openWithCallback(
426                         self.deleteConfirmed,
427                         MessageBox,
428                         _("Are you sure you want to delete %s?") % (fileName)
429                 )
430
431         def transferListRcvd(self, res, filelist):
432                 remoteDirectory, _, _ = self.getRemoteFile()
433                 localDirectory = self["local"].getCurrentDirectory()
434
435                 self.queue = [(True, remoteDirectory + file["filename"], localDirectory + file["filename"], file["size"]) for file in filelist.files if file["filetype"] == "-"]
436                 self.nextQueue()
437         
438         def nextQueue(self):
439                 if self.queue:
440                         # NOTE: put this transfer back if there already is an active one,
441                         # it will be picked up again when the active transfer is done
442                         if self.file:
443                                 return
444
445                         top = self.queue[0]
446                         del self.queue[0]
447                         if top[0]:
448                                 self.getFile(*top[1:])
449                         else:
450                                 self.putFile(*top[1:])
451                 elif self.queue is not None:
452                         self.queue = None
453                         AddPopup(_("Queue processed."), MessageBox.TYPE_INFO, -1)
454
455                 if self.queueManagerInstance:
456                         self.queueManagerInstance.updateList(self.queue)
457
458         def transferListFailed(self, res = None):
459                 self.queue = None
460                 AddPopup(_("Could not obtain list of files."), MessageBox.TYPE_ERROR, -1)
461
462         def transfer(self):
463                 if not self.ftpclient or self.queue:
464                         return
465
466                 if self.currlist == "remote":
467                         # single file transfer is implemented in self.ok
468                         if not self["remote"].canDescent():
469                                 return self.ok()
470                         else:
471                                 absRemoteFile, fileName, fileSize = self.getRemoteFile()
472                                 if not fileName:
473                                         return
474
475                                 filelist = ModifiedFTPFileListProtocol()
476                                 d = self.ftpclient.list(absRemoteFile, filelist)
477                                 d.addCallback(self.transferListRcvd, filelist).addErrback(self.transferListFailed)
478                 else:
479                         assert(self.currlist == "local")
480                         # single file transfer is implemented in self.ok
481                         if not self["local"].canDescent():
482                                 return self.ok()
483                         else:
484                                 localDirectory, _ = self.getLocalFile()
485                                 remoteDirectory = self["remote"].getCurrentDirectory()
486
487                                 def remoteFileExists(absName):
488                                         for file in self["remote"].getFileList():
489                                                 if file[0][0] == absName:
490                                                         return True
491                                         return False
492
493                                 self.queue = [(False, remoteDirectory + file, localDirectory + file, remoteFileExists(remoteDirectory + file)) for file in os_listdir(localDirectory) if os_path.isfile(localDirectory + file)]
494                                 self.nextQueue()
495
496
497         def getFileCallback(self, ret, absRemoteFile, absLocalFile, fileSize):
498                 if not ret:
499                         self.nextQueue()
500                 else:
501                         self.getFile(absRemoteFile, absLocalFile, fileSize, force=True)
502
503         def getFile(self, absRemoteFile, absLocalFile, fileSize, force=False):
504                 if not force and os_path.exists(absLocalFile):
505                         fileName = absRemoteFile.split('/')[-1]
506                         AddNotificationWithCallback(
507                                 lambda ret: self.getFileCallback(ret, absRemoteFile, absLocalFile, fileSize),
508                                 MessageBox,
509                                 _("A file with this name (%s) already exists locally.\nDo you want to overwrite it?") % (fileName),
510                         )
511                 else:
512                         self.currentLength = 0
513                         self.lastLength = 0
514                         self.lastTime = 0
515                         self.lastApprox = 0
516                         self.fileSize = fileSize
517
518                         try:
519                                 self.file = open(absLocalFile, 'w')
520                         except IOError, ie:
521                                 # TODO: handle this
522                                 raise ie
523                         else:
524                                 d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
525                                 d.addCallback(self.getFinished).addErrback(self.getFailed)
526
527         def putFileCallback(self, ret, absRemoteFile, absLocalFile, remoteFileExists):
528                 if not ret:
529                         self.nextQueue()
530                 else:
531                         self.putFile(absRemoteFile, absLocalFile, remoteFileExists, force=True)
532
533         def putFile(self, absRemoteFile, absLocalFile, remoteFileExists, force=False):
534                 if not force and remoteFileExists:
535                         fileName = absRemoteFile.split('/')[-1]
536                         AddNotificationWithCallback(
537                                 lambda ret: self.putFileCallback(ret, absRemoteFile, absLocalFile, remoteFileExists),
538                                 MessageBox,
539                                 _("A file with this name (%s) already exists on the remote host.\nDo you want to overwrite it?") % (fileName),
540                         )
541                 else:
542                         self.currentLength = 0
543                         self.lastLength = 0
544                         self.lastTime = 0
545                         self.lastApprox = 0
546
547                         def sendfile(consumer, fileObj):
548                                 FileSender().beginFileTransfer(fileObj, consumer, transform = self.putProgress).addCallback(  
549                                         lambda _: consumer.finish()).addCallback(
550                                         self.putComplete).addErrback(self.putFailed)
551
552                         try:
553                                 self.fileSize = int(os_path.getsize(absLocalFile))
554                                 self.file = open(absLocalFile, 'rb')
555                         except (IOError, OSError), e:
556                                 # TODO: handle this
557                                 raise e
558                         else:
559                                 dC, dL = self.ftpclient.storeFile(absRemoteFile)
560                                 dC.addCallback(sendfile, self.file)
561
562         def ok(self, force = False):
563                 if self.queue:
564                         return
565
566                 if self.currlist == "remote":
567                         if not self.ftpclient:
568                                 return
569
570                         # Get file/change directory
571                         if self["remote"].canDescent():
572                                 self["remote"].descent()
573                         else:
574                                 if self.file:
575                                         self.session.open(
576                                                 MessageBox,
577                                                 _("There already is an active transfer."),
578                                                 type = MessageBox.TYPE_WARNING
579                                         )
580                                         return
581
582                                 absRemoteFile, fileName, fileSize = self.getRemoteFile()
583                                 if not fileName:
584                                         return
585
586                                 absLocalFile = self["local"].getCurrentDirectory() + fileName
587
588                                 self.getFile(absRemoteFile, absLocalFile, fileSize)
589                 else:
590                         # Put file/change directory
591                         assert(self.currlist == "local")
592                         if self["local"].canDescent():
593                                 self["local"].descent()
594                         else:
595                                 if not self.ftpclient:
596                                         return
597
598                                 if self.file:
599                                         self.session.open(
600                                                 MessageBox,
601                                                 _("There already is an active transfer."),
602                                                 type = MessageBox.TYPE_WARNING
603                                         )
604                                         return
605
606                                 if not self["remote"].isValid:
607                                         return
608
609                                 absLocalFile, fileName = self.getLocalFile()
610                                 if not fileName:
611                                         return
612
613                                 def remoteFileExists(absName):
614                                         for file in self["remote"].getFileList():
615                                                 if file[0][0] == absName:
616                                                         return True
617                                         return False
618
619                                 absRemoteFile = self["remote"].getCurrentDirectory() + fileName
620                                 self.putFile(absRemoteFile, absLocalFile, remoteFileExists(absRemoteFile))
621
622         def transferFinished(self, msg, type, toRefresh):
623                 AddPopup(msg, type, -1)
624
625                 self["eta"].setText("")
626                 self["speed"].setText("")
627                 self["progress"].invalidate()
628                 self[toRefresh].refresh()
629                 self.file.close()
630                 self.file = None
631
632         def putComplete(self, *args):
633                 if self.queue is not None:
634                         self.file.close()
635                         self.file = None
636
637                         self.nextQueue()
638                 else:
639                         self.transferFinished(
640                                 _("Upload finished."),
641                                 MessageBox.TYPE_INFO,
642                                 "remote"
643                         )
644
645         def putFailed(self, *args):
646                 # NOTE: we continue uploading but notify the user of every error though
647                 # we only display one success notification
648                 self.transferFinished(
649                         _("Error during download."),
650                         MessageBox.TYPE_ERROR,
651                         "remote"
652                 )
653                 if self.queue is not None:
654                         self.nextQueue()
655
656         def getFinished(self, *args):
657                 if self.queue is not None:
658                         self.file.close()
659                         self.file = None
660
661                         self.nextQueue()
662                 else:
663                         self.transferFinished(
664                                 _("Download finished."),
665                                 MessageBox.TYPE_INFO,
666                                 "local"
667                         )
668
669         def getFailed(self, *args):
670                 # NOTE: we continue downloading but notify the user of every error though
671                 # we only display one success notification
672                 self.transferFinished(
673                         _("Error during download."),
674                         MessageBox.TYPE_ERROR,
675                         "local"
676                 )
677                 if self.queue is not None:
678                         self.nextQueue()
679
680         def putProgress(self, chunk):
681                 self.currentLength += len(chunk)
682                 self.gotProgress(self.currentLength, self.fileSize)
683                 return chunk
684
685         def gotProgress(self, pos, max):
686                 self["progress"].writeValues(pos, max)
687
688                 newTime = time()
689                 # Check if we're called the first time (got total)
690                 lastTime = self.lastTime
691                 if lastTime == 0:
692                         self.lastTime = newTime
693
694                 # We dont want to update more often than every two sec (could be done by a timer, but this should give a more accurate result though it might lag)
695                 elif int(newTime - lastTime) >= 2:
696                         lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
697
698                         secLen = int(round(((max-pos) / 1024) / lastApprox))
699                         self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
700                         self["speed"].setText(_("%d kb/s") % (lastApprox))
701
702                         self.lastApprox = lastApprox
703                         self.lastLength = pos
704                         self.lastTime = newTime
705
706         def dataReceived(self, data):
707                 if not self.file:
708                         return
709
710                 self.currentLength += len(data)
711                 self.gotProgress(self.currentLength, self.fileSize)
712
713                 try:
714                         self.file.write(data)
715                 except IOError, ie:
716                         # TODO: handle this
717                         self.file = None
718                         raise ie
719
720         def cancelQuestion(self, res = None):
721                 res = res and res[1]
722                 if res:
723                         if res == 1:
724                                 self.file.close()
725                                 self.file = None
726                                 self.disconnect()
727                         self.close()
728
729         def cancel(self):
730                 if self.file is not None:
731                         self.session.openWithCallback(
732                                 self.cancelQuestion,
733                                 ChoiceBox,
734                                 title = _("A transfer is currently in progress.\nWhat do you want to do?"),
735                                 list = (
736                                         (_("Run in Background"), 2),
737                                         (_("Abort transfer"), 1),
738                                         (_("Cancel"), 0)
739                                 )
740                         )
741                         return
742
743                 self.disconnect()
744                 self.close()
745
746         def up(self):
747                 self[self.currlist].up()
748
749         def down(self):
750                 self[self.currlist].down()
751
752         def left(self):
753                 self[self.currlist].pageUp()
754
755         def right(self):
756                 self[self.currlist].pageDown()
757
758         def disconnect(self):
759                 if self.ftpclient:
760                         # XXX: according to the docs we should wait for the servers answer to our quit request, we just hope everything goes well here
761                         self.ftpclient.quit()
762                         self.ftpclient = None
763                         self["remote"].ftpclient = None
764                 self["remoteText"].setText(_("Remote (not connected)"))
765
766         def connectWrapper(self, ret):
767                 if ret:
768                         self.connect(ret[1])
769
770         def connect(self, server):
771                 self.disconnect()
772
773                 self.server = server
774
775                 if not server:
776                         return
777
778                 username = server.getUsername()
779                 if not username:
780                         username = 'anonymous'
781                         password = 'my@email.com'
782                 else:
783                         password = server.getPassword()
784
785                 host = server.getAddress()
786                 passive = server.getPassive()
787                 port = server.getPort()
788                 timeout = 30 # TODO: make configurable
789
790                 # XXX: we might want to add a guard so we don't try to connect to another host while a previous attempt is not timed out
791
792                 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
793                 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
794
795         def controlConnectionMade(self, ftpclient):
796                 print "[FTPBrowser] connection established"
797                 self.ftpclient = ftpclient
798                 self["remote"].ftpclient = ftpclient
799                 self["remoteText"].setText(_("Remote"))
800
801                 self["remote"].changeDir(self.server.getPath())
802
803         def connectionFailed(self, *args):
804                 print "[FTPBrowser] connection failed", args
805
806                 self.server = None
807                 self["remoteText"].setText(_("Remote (not connected)"))
808                 self.session.open(
809                                 MessageBox,
810                                 _("Could not connect to ftp server!"),
811                                 type = MessageBox.TYPE_ERROR,
812                                 timeout = 3,
813                 )
814