allow local rename/delete when not connected,
[vuplus_dvbapp-plugin] / 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 self.queue:
348                         return
349
350                 if self.currlist == "remote":
351                         if not self.ftpclient:
352                                 return
353
354                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
355                         if not fileName:
356                                 return
357                 else:
358                         assert(self.currlist == "local")
359                         absLocalFile, fileName = self.getLocalFile()
360                         if not fileName:
361                                 return
362
363                 self.session.openWithCallback(
364                         self.renameCallback,
365                         NTIVirtualKeyBoard,
366                         title = _("Enter new filename:"),
367                         text = fileName,
368                 )
369
370         def deleteConfirmed(self, ret):
371                 if not ret:
372                         return
373
374                 if self.currlist == "remote":
375                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
376                         if not fileName:
377                                 return
378
379                         def callback(ret = None):
380                                 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
381                         def errback(ret = None):
382                                 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
383
384                         self.ftpclient.removeFile(absRemoteFile).addCallback(callback).addErrback(errback)
385                 else:
386                         assert(self.currlist == "local")
387                         absLocalFile, fileName = self.getLocalFile()
388                         if not fileName:
389                                 return
390
391                         try:
392                                 os_unlink(absLocalFile)
393                         except OSError, oe:
394                                 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
395                         else:
396                                 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
397
398         def delete(self):
399                 if self.queue:
400                         return
401
402                 if self.currlist == "remote":
403                         if not self.ftpclient:
404                                 return
405
406                         if self["remote"].canDescent():
407                                 self.session.open(
408                                         MessageBox,
409                                         _("Removing directories is not supported."),
410                                         MessageBox.TYPE_WARNING
411                                 )
412                                 return
413
414                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
415                         if not fileName:
416                                 return
417                 else:
418                         assert(self.currlist == "local")
419                         if self["local"].canDescent():
420                                 self.session.open(
421                                         MessageBox,
422                                         _("Removing directories is not supported."),
423                                         MessageBox.TYPE_WARNING
424                                 )
425                                 return
426
427                         absLocalFile, fileName = self.getLocalFile()
428                         if not fileName:
429                                 return
430
431                 self.session.openWithCallback(
432                         self.deleteConfirmed,
433                         MessageBox,
434                         _("Are you sure you want to delete %s?") % (fileName)
435                 )
436
437         def transferListRcvd(self, res, filelist):
438                 remoteDirectory, _, _ = self.getRemoteFile()
439                 localDirectory = self["local"].getCurrentDirectory()
440
441                 self.queue = [(True, remoteDirectory + file["filename"], localDirectory + file["filename"], file["size"]) for file in filelist.files if file["filetype"] == "-"]
442                 self.nextQueue()
443         
444         def nextQueue(self):
445                 if self.queue:
446                         # NOTE: put this transfer back if there already is an active one,
447                         # it will be picked up again when the active transfer is done
448                         if self.file:
449                                 return
450
451                         top = self.queue[0]
452                         del self.queue[0]
453                         if top[0]:
454                                 self.getFile(*top[1:])
455                         else:
456                                 self.putFile(*top[1:])
457                 elif self.queue is not None:
458                         self.queue = None
459                         self["eta"].setText("")
460                         self["speed"].setText("")
461                         self["progress"].invalidate()
462                         AddPopup(_("Queue processed."), MessageBox.TYPE_INFO, -1)
463
464                 if self.queueManagerInstance:
465                         self.queueManagerInstance.updateList(self.queue)
466
467         def transferListFailed(self, res = None):
468                 self.queue = None
469                 AddPopup(_("Could not obtain list of files."), MessageBox.TYPE_ERROR, -1)
470
471         def transfer(self):
472                 if not self.ftpclient or self.queue:
473                         return
474
475                 if self.currlist == "remote":
476                         # single file transfer is implemented in self.ok
477                         if not self["remote"].canDescent():
478                                 return self.ok()
479                         else:
480                                 absRemoteFile, fileName, fileSize = self.getRemoteFile()
481                                 if not fileName:
482                                         return
483
484                                 filelist = ModifiedFTPFileListProtocol()
485                                 d = self.ftpclient.list(absRemoteFile, filelist)
486                                 d.addCallback(self.transferListRcvd, filelist).addErrback(self.transferListFailed)
487                 else:
488                         assert(self.currlist == "local")
489                         # single file transfer is implemented in self.ok
490                         if not self["local"].canDescent():
491                                 return self.ok()
492                         else:
493                                 localDirectory, _ = self.getLocalFile()
494                                 remoteDirectory = self["remote"].getCurrentDirectory()
495
496                                 def remoteFileExists(absName):
497                                         for file in self["remote"].getFileList():
498                                                 if file[0][0] == absName:
499                                                         return True
500                                         return False
501
502                                 self.queue = [(False, remoteDirectory + file, localDirectory + file, remoteFileExists(remoteDirectory + file)) for file in os_listdir(localDirectory) if os_path.isfile(localDirectory + file)]
503                                 self.nextQueue()
504
505
506         def getFileCallback(self, ret, absRemoteFile, absLocalFile, fileSize):
507                 if not ret:
508                         self.nextQueue()
509                 else:
510                         self.getFile(absRemoteFile, absLocalFile, fileSize, force=True)
511
512         def getFile(self, absRemoteFile, absLocalFile, fileSize, force=False):
513                 if not force and os_path.exists(absLocalFile):
514                         fileName = absRemoteFile.split('/')[-1]
515                         AddNotificationWithCallback(
516                                 lambda ret: self.getFileCallback(ret, absRemoteFile, absLocalFile, fileSize),
517                                 MessageBox,
518                                 _("A file with this name (%s) already exists locally.\nDo you want to overwrite it?") % (fileName),
519                         )
520                 else:
521                         self.currentLength = 0
522                         self.lastLength = 0
523                         self.lastTime = 0
524                         self.lastApprox = 0
525                         self.fileSize = fileSize
526
527                         try:
528                                 self.file = open(absLocalFile, 'w')
529                         except IOError, ie:
530                                 # TODO: handle this
531                                 raise ie
532                         else:
533                                 d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
534                                 d.addCallback(self.getFinished).addErrback(self.getFailed)
535
536         def putFileCallback(self, ret, absRemoteFile, absLocalFile, remoteFileExists):
537                 if not ret:
538                         self.nextQueue()
539                 else:
540                         self.putFile(absRemoteFile, absLocalFile, remoteFileExists, force=True)
541
542         def putFile(self, absRemoteFile, absLocalFile, remoteFileExists, force=False):
543                 if not force and remoteFileExists:
544                         fileName = absRemoteFile.split('/')[-1]
545                         AddNotificationWithCallback(
546                                 lambda ret: self.putFileCallback(ret, absRemoteFile, absLocalFile, remoteFileExists),
547                                 MessageBox,
548                                 _("A file with this name (%s) already exists on the remote host.\nDo you want to overwrite it?") % (fileName),
549                         )
550                 else:
551                         self.currentLength = 0
552                         self.lastLength = 0
553                         self.lastTime = 0
554                         self.lastApprox = 0
555
556                         def sendfile(consumer, fileObj):
557                                 FileSender().beginFileTransfer(fileObj, consumer, transform = self.putProgress).addCallback(  
558                                         lambda _: consumer.finish()).addCallback(
559                                         self.putComplete).addErrback(self.putFailed)
560
561                         try:
562                                 self.fileSize = int(os_path.getsize(absLocalFile))
563                                 self.file = open(absLocalFile, 'rb')
564                         except (IOError, OSError), e:
565                                 # TODO: handle this
566                                 raise e
567                         else:
568                                 dC, dL = self.ftpclient.storeFile(absRemoteFile)
569                                 dC.addCallback(sendfile, self.file)
570
571         def ok(self, force = False):
572                 if self.queue:
573                         return
574
575                 if self.currlist == "remote":
576                         if not self.ftpclient:
577                                 return
578
579                         # Get file/change directory
580                         if self["remote"].canDescent():
581                                 self["remote"].descent()
582                         else:
583                                 if self.file:
584                                         self.session.open(
585                                                 MessageBox,
586                                                 _("There already is an active transfer."),
587                                                 type = MessageBox.TYPE_WARNING
588                                         )
589                                         return
590
591                                 absRemoteFile, fileName, fileSize = self.getRemoteFile()
592                                 if not fileName:
593                                         return
594
595                                 absLocalFile = self["local"].getCurrentDirectory() + fileName
596
597                                 self.getFile(absRemoteFile, absLocalFile, fileSize)
598                 else:
599                         # Put file/change directory
600                         assert(self.currlist == "local")
601                         if self["local"].canDescent():
602                                 self["local"].descent()
603                         else:
604                                 if not self.ftpclient:
605                                         return
606
607                                 if self.file:
608                                         self.session.open(
609                                                 MessageBox,
610                                                 _("There already is an active transfer."),
611                                                 type = MessageBox.TYPE_WARNING
612                                         )
613                                         return
614
615                                 if not self["remote"].isValid:
616                                         return
617
618                                 absLocalFile, fileName = self.getLocalFile()
619                                 if not fileName:
620                                         return
621
622                                 def remoteFileExists(absName):
623                                         for file in self["remote"].getFileList():
624                                                 if file[0][0] == absName:
625                                                         return True
626                                         return False
627
628                                 absRemoteFile = self["remote"].getCurrentDirectory() + fileName
629                                 self.putFile(absRemoteFile, absLocalFile, remoteFileExists(absRemoteFile))
630
631         def transferFinished(self, msg, type, toRefresh):
632                 AddPopup(msg, type, -1)
633
634                 self["eta"].setText("")
635                 self["speed"].setText("")
636                 self["progress"].invalidate()
637                 self[toRefresh].refresh()
638                 self.file.close()
639                 self.file = None
640
641         def putComplete(self, *args):
642                 if self.queue is not None:
643                         self.file.close()
644                         self.file = None
645
646                         self.nextQueue()
647                 else:
648                         self.transferFinished(
649                                 _("Upload finished."),
650                                 MessageBox.TYPE_INFO,
651                                 "remote"
652                         )
653
654         def putFailed(self, *args):
655                 # NOTE: we continue uploading but notify the user of every error though
656                 # we only display one success notification
657                 self.transferFinished(
658                         _("Error during download."),
659                         MessageBox.TYPE_ERROR,
660                         "remote"
661                 )
662                 if self.queue is not None:
663                         self.nextQueue()
664
665         def getFinished(self, *args):
666                 if self.queue is not None:
667                         self.file.close()
668                         self.file = None
669
670                         self.nextQueue()
671                 else:
672                         self.transferFinished(
673                                 _("Download finished."),
674                                 MessageBox.TYPE_INFO,
675                                 "local"
676                         )
677
678         def getFailed(self, *args):
679                 # NOTE: we continue downloading but notify the user of every error though
680                 # we only display one success notification
681                 self.transferFinished(
682                         _("Error during download."),
683                         MessageBox.TYPE_ERROR,
684                         "local"
685                 )
686                 if self.queue is not None:
687                         self.nextQueue()
688
689         def putProgress(self, chunk):
690                 self.currentLength += len(chunk)
691                 self.gotProgress(self.currentLength, self.fileSize)
692                 return chunk
693
694         def gotProgress(self, pos, max):
695                 self["progress"].writeValues(pos, max)
696
697                 newTime = time()
698                 # Check if we're called the first time (got total)
699                 lastTime = self.lastTime
700                 if lastTime == 0:
701                         self.lastTime = newTime
702
703                 # 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)
704                 elif int(newTime - lastTime) >= 2:
705                         lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
706
707                         secLen = int(round(((max-pos) / 1024) / lastApprox))
708                         self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
709                         self["speed"].setText(_("%d kb/s") % (lastApprox))
710
711                         self.lastApprox = lastApprox
712                         self.lastLength = pos
713                         self.lastTime = newTime
714
715         def dataReceived(self, data):
716                 if not self.file:
717                         return
718
719                 self.currentLength += len(data)
720                 self.gotProgress(self.currentLength, self.fileSize)
721
722                 try:
723                         self.file.write(data)
724                 except IOError, ie:
725                         # TODO: handle this
726                         self.file = None
727                         raise ie
728
729         def cancelQuestion(self, res = None):
730                 res = res and res[1]
731                 if res:
732                         if res == 1:
733                                 self.file.close()
734                                 self.file = None
735                                 self.disconnect()
736                         self.close()
737
738         def cancel(self):
739                 if self.file is not None:
740                         self.session.openWithCallback(
741                                 self.cancelQuestion,
742                                 ChoiceBox,
743                                 title = _("A transfer is currently in progress.\nWhat do you want to do?"),
744                                 list = (
745                                         (_("Run in Background"), 2),
746                                         (_("Abort transfer"), 1),
747                                         (_("Cancel"), 0)
748                                 )
749                         )
750                         return
751
752                 self.disconnect()
753                 self.close()
754
755         def up(self):
756                 self[self.currlist].up()
757
758         def down(self):
759                 self[self.currlist].down()
760
761         def left(self):
762                 self[self.currlist].pageUp()
763
764         def right(self):
765                 self[self.currlist].pageDown()
766
767         def disconnect(self):
768                 if self.ftpclient:
769                         # XXX: according to the docs we should wait for the servers answer to our quit request, we just hope everything goes well here
770                         self.ftpclient.quit()
771                         self.ftpclient = None
772                         self["remote"].ftpclient = None
773                 self["remoteText"].setText(_("Remote (not connected)"))
774
775         def connectWrapper(self, ret):
776                 if ret:
777                         self.connect(ret[1])
778
779         def connect(self, server):
780                 self.disconnect()
781
782                 self.server = server
783
784                 if not server:
785                         return
786
787                 username = server.getUsername()
788                 if not username:
789                         username = 'anonymous'
790                         password = 'my@email.com'
791                 else:
792                         password = server.getPassword()
793
794                 host = server.getAddress()
795                 passive = server.getPassive()
796                 port = server.getPort()
797                 timeout = 30 # TODO: make configurable
798
799                 # 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
800
801                 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
802                 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
803
804         def controlConnectionMade(self, ftpclient):
805                 print "[FTPBrowser] connection established"
806                 self.ftpclient = ftpclient
807                 self["remote"].ftpclient = ftpclient
808                 self["remoteText"].setText(_("Remote"))
809
810                 self["remote"].changeDir(self.server.getPath())
811
812         def connectionFailed(self, *args):
813                 print "[FTPBrowser] connection failed", args
814
815                 self.server = None
816                 self["remoteText"].setText(_("Remote (not connected)"))
817                 self.session.open(
818                                 MessageBox,
819                                 _("Could not connect to ftp server!"),
820                                 type = MessageBox.TYPE_ERROR,
821                                 timeout = 3,
822                 )
823