use different pattern to match response to LIST, this should allow displaying files...
[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                 # Init what we need for dl progress
165                 self.currentLength = 0
166                 self.lastLength = 0
167                 self.lastTime = 0
168                 self.lastApprox = 0
169                 self.fileSize = 0
170
171                 self["localText"] = Label(_("Local"))
172                 self["local"] = FileList("/media/hdd/", showMountpoints = False)
173                 self["remoteText"] = Label(_("Remote (not connected)"))
174                 self["remote"] = FTPFileList()
175                 self["eta"] = Label("")
176                 self["speed"] = Label("")
177                 self["progress"] = VariableProgressSource()
178                 self["key_red"] = Button(_("Exit"))
179                 self["key_green"] = Button(_("Rename"))
180                 self["key_yellow"] = Button(_("Delete"))
181                 self["key_blue"] = Button(_("Upload"))
182
183                 self.server = None
184
185                 self["ftpbrowserBaseActions"] = HelpableActionMap(self, "ftpbrowserBaseActions",
186                         {
187                                 "ok": (self.ok, _("enter directory/get file/put file")),
188                                 "cancel": (self.cancel , _("close")),
189                                 "menu": (self.menu, _("open menu")),
190                         }, -2)
191
192                 self["ftpbrowserListActions"] = HelpableActionMap(self, "ftpbrowserListActions",
193                         {
194                                 "channelUp": (self.setLocal, _("Select local file list")),
195                                 "channelDown": (self.setRemote, _("Select remote file list")),
196                         })
197
198                 self["actions"] = ActionMap(["ftpbrowserDirectionActions", "ColorActions"],
199                         {
200                                 "up": self.up,
201                                 "down": self.down,
202                                 "left": self.left,
203                                 "right": self.right,
204                                 "green": self.rename,
205                                 "yellow": self.delete,
206                                 "blue": self.transfer,
207                         }, -2)
208
209                 self.onExecBegin.append(self.reinitialize)
210
211         def reinitialize(self):
212                 # NOTE: this will clear the remote file list if we are not currently connected. this behavior is intended.
213                 # XXX: but do we also want to do this when we just returned from a notification?
214                 try:
215                         self["remote"].refresh()
216                 except AttributeError, ae:
217                         # NOTE: we assume the connection was timed out by the server
218                         self.ftpclient = None
219                         self["remote"].ftpclient = None
220                         self["remote"].refresh()
221
222                 self["local"].refresh()
223
224                 if not self.ftpclient:
225                         self.connect(self.server)
226                 # XXX: Actually everything else should be taken care of... recheck this!
227
228         def serverManagerCallback(self, uri):
229                 if uri:
230                         self.connect(uri)
231
232         def serverManager(self):
233                 self.session.openWithCallback(
234                         self.serverManagerCallback,
235                         FTPServerManager,
236                 )
237
238         def queueManagerCallback(self):
239                 self.queueManagerInstance = None
240
241         def queueManager(self):
242                 self.queueManagerInstance = self.session.openWithCallback(
243                         self.queueManagerCallback,
244                         FTPQueueManager,
245                         self.queue,
246                 )
247
248         def menuCallback(self, ret):
249                 ret and ret[1]()
250
251         def menu(self):
252                 self.session.openWithCallback(
253                         self.menuCallback,
254                         ChoiceBox,
255                         list = [
256                                 (_("Server Manager"), self.serverManager),
257                                 (_("Queue Manager"), self.queueManager),
258                         ]
259                 )
260
261         def setLocal(self):
262                 self.currlist = "local"
263                 self["key_blue"].setText(_("Upload"))
264
265         def setRemote(self):
266                 self.currlist = "remote"
267                 self["key_blue"].setText(_("Download"))
268
269         def okQuestion(self, res = None):
270                 if res:
271                         self.ok(force = True)
272
273         def getRemoteFile(self):
274                 remoteFile = self["remote"].getSelection()
275                 if not remoteFile or not remoteFile[0]:
276                         return None, None, None
277
278                 absRemoteFile = remoteFile[0]
279                 if remoteFile[1]:
280                         fileName = absRemoteFile.split('/')[-2]
281                 else:
282                         fileName = absRemoteFile.split('/')[-1]
283
284                 if len(remoteFile) == 3:
285                         fileSize = remoteFile[2]
286                 else:
287                         fileSize = 0
288
289                 return absRemoteFile, fileName, fileSize
290
291         def getLocalFile(self):
292                 # XXX: isn't this supposed to be an absolute filename? well, it's not for me :-/
293                 localFile = self["local"].getSelection()
294                 if not localFile:
295                         return None, None
296
297                 if localFile[1]:
298                         absLocalFile = localFile[0]
299                         fileName = absLocalFile.split('/')[-2]
300                 else:
301                         fileName = localFile[0]
302                         absLocalFile = self["local"].getCurrentDirectory() + fileName
303
304                 return absLocalFile, fileName
305
306         def renameCallback(self, newName = None):
307                 if not newName:
308                         return
309
310                 if self.currlist == "remote":
311                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
312                         if not fileName:
313                                 return
314
315                         directory = self["remote"].getCurrentDirectory()
316                         sep = '/' if directory != '/' else ''
317                         newRemoteFile = directory + sep + newName
318
319                         def callback(ret = None):
320                                 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
321                         def errback(ret = None):
322                                 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
323
324                         self.ftpclient.rename(absRemoteFile, newRemoteFile).addCallback(callback).addErrback(errback)
325                 else:
326                         assert(self.currlist == "local")
327                         absLocalFile, fileName = self.getLocalFile()
328                         if not fileName:
329                                 return
330
331                         directory = self["local"].getCurrentDirectory()
332                         newLocalFile = os_path.join(directory, newName)
333
334                         try:
335                                 os_rename(absLocalFile, newLocalFile)
336                         except OSError, ose:
337                                 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
338                         else:
339                                 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
340
341         def rename(self):
342                 if not self.ftpclient or self.queue:
343                         return
344
345                 if self.currlist == "remote":
346                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
347                         if not fileName:
348                                 return
349                 else:
350                         assert(self.currlist == "local")
351                         absLocalFile, fileName = self.getLocalFile()
352                         if not fileName:
353                                 return
354
355                 self.session.openWithCallback(
356                         self.renameCallback,
357                         NTIVirtualKeyBoard,
358                         title = _("Enter new filename:"),
359                         text = fileName,
360                 )
361
362         def deleteConfirmed(self, ret):
363                 if not ret:
364                         return
365
366                 if self.currlist == "remote":
367                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
368                         if not fileName:
369                                 return
370
371                         def callback(ret = None):
372                                 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
373                         def errback(ret = None):
374                                 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
375
376                         self.ftpclient.removeFile(absRemoteFile).addCallback(callback).addErrback(errback)
377                 else:
378                         assert(self.currlist == "local")
379                         absLocalFile, fileName = self.getLocalFile()
380                         if not fileName:
381                                 return
382
383                         try:
384                                 os_unlink(absLocalFile)
385                         except OSError, oe:
386                                 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
387                         else:
388                                 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
389
390         def delete(self):
391                 if not self.ftpclient or self.queue:
392                         return
393
394                 if self.currlist == "remote":
395                         if self["remote"].canDescent():
396                                 self.session.open(
397                                         MessageBox,
398                                         _("Removing directories is not supported."),
399                                         MessageBox.TYPE_WARNING
400                                 )
401                                 return
402
403                         absRemoteFile, fileName, fileSize = self.getRemoteFile()
404                         if not fileName:
405                                 return
406                 else:
407                         assert(self.currlist == "local")
408                         if self["local"].canDescent():
409                                 self.session.open(
410                                         MessageBox,
411                                         _("Removing directories is not supported."),
412                                         MessageBox.TYPE_WARNING
413                                 )
414                                 return
415
416                         absLocalFile, fileName = self.getLocalFile()
417                         if not fileName:
418                                 return
419
420                 self.session.openWithCallback(
421                         self.deleteConfirmed,
422                         MessageBox,
423                         _("Are you sure you want to delete %s?") % (fileName)
424                 )
425
426         def transferListRcvd(self, res, filelist):
427                 remoteDirectory, _, _ = self.getRemoteFile()
428                 localDirectory = self["local"].getCurrentDirectory()
429
430                 self.queue = [(True, remoteDirectory + file["filename"], localDirectory + file["filename"], file["size"]) for file in filelist.files if file["filetype"] == "-"]
431                 self.nextQueue()
432         
433         def nextQueue(self):
434                 if self.queue:
435                         # NOTE: put this transfer back if there already is an active one,
436                         # it will be picked up again when the active transfer is done
437                         if self.file:
438                                 return
439
440                         top = self.queue[0]
441                         del self.queue[0]
442                         if top[0]:
443                                 self.getFile(*top[1:])
444                         else:
445                                 self.putFile(*top[1:])
446                 elif self.queue is not None:
447                         self.queue = None
448                         AddPopup(_("Queue processed."), MessageBox.TYPE_INFO, -1)
449
450                 if self.queueManagerInstance:
451                         self.queueManagerInstance.updateList(self.queue)
452
453         def transferListFailed(self, res = None):
454                 self.queue = None
455                 AddPopup(_("Could not obtain list of files."), MessageBox.TYPE_ERROR, -1)
456
457         def transfer(self):
458                 if not self.ftpclient or self.queue:
459                         return
460
461                 if self.currlist == "remote":
462                         # single file transfer is implemented in self.ok
463                         if not self["remote"].canDescent():
464                                 return self.ok()
465                         else:
466                                 absRemoteFile, fileName, fileSize = self.getRemoteFile()
467                                 if not fileName:
468                                         return
469
470                                 filelist = ModifiedFTPFileListProtocol()
471                                 d = self.ftpclient.list(absRemoteFile, filelist)
472                                 d.addCallback(self.transferListRcvd, filelist).addErrback(self.transferListFailed)
473                 else:
474                         assert(self.currlist == "local")
475                         # single file transfer is implemented in self.ok
476                         if not self["local"].canDescent():
477                                 return self.ok()
478                         else:
479                                 localDirectory, _ = self.getLocalFile()
480                                 remoteDirectory = self["remote"].getCurrentDirectory()
481
482                                 def remoteFileExists(absName):
483                                         for file in self["remote"].getFileList():
484                                                 if file[0][0] == absName:
485                                                         return True
486                                         return False
487
488                                 self.queue = [(False, remoteDirectory + file, localDirectory + file, remoteFileExists(remoteDirectory + file)) for file in os_listdir(localDirectory) if os_path.isfile(localDirectory + file)]
489                                 self.nextQueue()
490
491
492         def getFileCallback(self, ret, absRemoteFile, absLocalFile, fileSize):
493                 if not ret:
494                         self.nextQueue()
495                 else:
496                         self.getFile(absRemoteFile, absLocalFile, fileSize, force=True)
497
498         def getFile(self, absRemoteFile, absLocalFile, fileSize, force=False):
499                 if not force and os_path.exists(absLocalFile):
500                         fileName = absRemoteFile.split('/')[-1]
501                         AddNotificationWithCallback(
502                                 lambda ret: self.getFileCallback(ret, absRemoteFile, absLocalFile, fileSize),
503                                 MessageBox,
504                                 _("A file with this name (%s) already exists locally.\nDo you want to overwrite it?") % (fileName),
505                         )
506                 else:
507                         self.currentLength = 0
508                         self.lastLength = 0
509                         self.lastTime = 0
510                         self.lastApprox = 0
511                         self.fileSize = fileSize
512
513                         try:
514                                 self.file = open(absLocalFile, 'w')
515                         except IOError, ie:
516                                 # TODO: handle this
517                                 raise ie
518                         else:
519                                 d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
520                                 d.addCallback(self.getFinished).addErrback(self.getFailed)
521
522         def putFileCallback(self, ret, absRemoteFile, absLocalFile, remoteFileExists):
523                 if not ret:
524                         self.nextQueue()
525                 else:
526                         self.putFile(absRemoteFile, absLocalFile, remoteFileExists, force=True)
527
528         def putFile(self, absRemoteFile, absLocalFile, remoteFileExists, force=False):
529                 if not force and remoteFileExists:
530                         fileName = absRemoteFile.split('/')[-1]
531                         AddNotificationWithCallback(
532                                 lambda ret: self.putFileCallback(ret, absRemoteFile, absLocalFile, remoteFileExists),
533                                 MessageBox,
534                                 _("A file with this name (%s) already exists on the remote host.\nDo you want to overwrite it?") % (fileName),
535                         )
536                 else:
537                         self.currentLength = 0
538                         self.lastLength = 0
539                         self.lastTime = 0
540                         self.lastApprox = 0
541
542                         def sendfile(consumer, fileObj):
543                                 FileSender().beginFileTransfer(fileObj, consumer, transform = self.putProgress).addCallback(  
544                                         lambda _: consumer.finish()).addCallback(
545                                         self.putComplete).addErrback(self.putFailed)
546
547                         try:
548                                 self.fileSize = int(os_path.getsize(absLocalFile))
549                                 self.file = open(absLocalFile, 'rb')
550                         except (IOError, OSError), e:
551                                 # TODO: handle this
552                                 raise e
553                         else:
554                                 dC, dL = self.ftpclient.storeFile(absRemoteFile)
555                                 dC.addCallback(sendfile, self.file)
556
557         def ok(self, force = False):
558                 if self.queue:
559                         return
560
561                 if self.currlist == "remote":
562                         if not self.ftpclient:
563                                 return
564
565                         # Get file/change directory
566                         if self["remote"].canDescent():
567                                 self["remote"].descent()
568                         else:
569                                 if self.file:
570                                         self.session.open(
571                                                 MessageBox,
572                                                 _("There already is an active transfer."),
573                                                 type = MessageBox.TYPE_WARNING
574                                         )
575                                         return
576
577                                 absRemoteFile, fileName, fileSize = self.getRemoteFile()
578                                 if not fileName:
579                                         return
580
581                                 absLocalFile = self["local"].getCurrentDirectory() + fileName
582
583                                 self.getFile(absRemoteFile, absLocalFile, fileSize)
584                 else:
585                         # Put file/change directory
586                         assert(self.currlist == "local")
587                         if self["local"].canDescent():
588                                 self["local"].descent()
589                         else:
590                                 if not self.ftpclient:
591                                         return
592
593                                 if self.file:
594                                         self.session.open(
595                                                 MessageBox,
596                                                 _("There already is an active transfer."),
597                                                 type = MessageBox.TYPE_WARNING
598                                         )
599                                         return
600
601                                 if not self["remote"].isValid:
602                                         return
603
604                                 absLocalFile, fileName = self.getLocalFile()
605                                 if not fileName:
606                                         return
607
608                                 def remoteFileExists(absName):
609                                         for file in self["remote"].getFileList():
610                                                 if file[0][0] == absName:
611                                                         return True
612                                         return False
613
614                                 absRemoteFile = self["remote"].getCurrentDirectory() + fileName
615                                 self.putFile(absRemoteFile, absLocalFile, remoteFileExists(absRemoteFile))
616
617         def transferFinished(self, msg, type, toRefresh):
618                 AddPopup(msg, type, -1)
619
620                 self["eta"].setText("")
621                 self["speed"].setText("")
622                 self["progress"].invalidate()
623                 self[toRefresh].refresh()
624                 self.file.close()
625                 self.file = None
626
627         def putComplete(self, *args):
628                 if self.queue is not None:
629                         self.file.close()
630                         self.file = None
631
632                         self.nextQueue()
633                 else:
634                         self.transferFinished(
635                                 _("Upload finished."),
636                                 MessageBox.TYPE_INFO,
637                                 "remote"
638                         )
639
640         def putFailed(self, *args):
641                 # NOTE: we continue uploading but notify the user of every error though
642                 # we only display one success notification
643                 self.transferFinished(
644                         _("Error during download."),
645                         MessageBox.TYPE_ERROR,
646                         "remote"
647                 )
648                 if self.queue is not None:
649                         self.nextQueue()
650
651         def getFinished(self, *args):
652                 if self.queue is not None:
653                         self.file.close()
654                         self.file = None
655
656                         self.nextQueue()
657                 else:
658                         self.transferFinished(
659                                 _("Download finished."),
660                                 MessageBox.TYPE_INFO,
661                                 "local"
662                         )
663
664         def getFailed(self, *args):
665                 # NOTE: we continue downloading but notify the user of every error though
666                 # we only display one success notification
667                 self.transferFinished(
668                         _("Error during download."),
669                         MessageBox.TYPE_ERROR,
670                         "local"
671                 )
672                 if self.queue is not None:
673                         self.nextQueue()
674
675         def putProgress(self, chunk):
676                 self.currentLength += len(chunk)
677                 self.gotProgress(self.currentLength, self.fileSize)
678                 return chunk
679
680         def gotProgress(self, pos, max):
681                 self["progress"].writeValues(pos, max)
682
683                 newTime = time()
684                 # Check if we're called the first time (got total)
685                 lastTime = self.lastTime
686                 if lastTime == 0:
687                         self.lastTime = newTime
688
689                 # 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)
690                 elif int(newTime - lastTime) >= 2:
691                         lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
692
693                         secLen = int(round(((max-pos) / 1024) / lastApprox))
694                         self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
695                         self["speed"].setText(_("%d kb/s") % (lastApprox))
696
697                         self.lastApprox = lastApprox
698                         self.lastLength = pos
699                         self.lastTime = newTime
700
701         def dataReceived(self, data):
702                 if not self.file:
703                         return
704
705                 self.currentLength += len(data)
706                 self.gotProgress(self.currentLength, self.fileSize)
707
708                 try:
709                         self.file.write(data)
710                 except IOError, ie:
711                         # TODO: handle this
712                         self.file = None
713                         raise ie
714
715         def cancelQuestion(self, res = None):
716                 res = res and res[1]
717                 if res:
718                         if res == 1:
719                                 self.file.close()
720                                 self.file = None
721                                 self.disconnect()
722                         self.close()
723
724         def cancel(self):
725                 if self.file is not None:
726                         self.session.openWithCallback(
727                                 self.cancelQuestion,
728                                 ChoiceBox,
729                                 title = _("A transfer is currently in progress.\nWhat do you want to do?"),
730                                 list = (
731                                         (_("Run in Background"), 2),
732                                         (_("Abort transfer"), 1),
733                                         (_("Cancel"), 0)
734                                 )
735                         )
736                         return
737
738                 self.disconnect()
739                 self.close()
740
741         def up(self):
742                 self[self.currlist].up()
743
744         def down(self):
745                 self[self.currlist].down()
746
747         def left(self):
748                 self[self.currlist].pageUp()
749
750         def right(self):
751                 self[self.currlist].pageDown()
752
753         def disconnect(self):
754                 if self.ftpclient:
755                         # XXX: according to the docs we should wait for the servers answer to our quit request, we just hope everything goes well here
756                         self.ftpclient.quit()
757                         self.ftpclient = None
758                         self["remote"].ftpclient = None
759                 self["remoteText"].setText(_("Remote (not connected)"))
760
761         def connectWrapper(self, ret):
762                 if ret:
763                         self.connect(ret[1])
764
765         def connect(self, server):
766                 self.disconnect()
767
768                 self.server = server
769
770                 if not server:
771                         return
772
773                 username = server.getUsername()
774                 if not username:
775                         username = 'anonymous'
776                         password = 'my@email.com'
777                 else:
778                         password = server.getPassword()
779
780                 host = server.getAddress()
781                 passive = server.getPassive()
782                 port = server.getPort()
783                 timeout = 30 # TODO: make configurable
784
785                 # 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
786
787                 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
788                 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
789
790         def controlConnectionMade(self, ftpclient):
791                 print "[FTPBrowser] connection established"
792                 self.ftpclient = ftpclient
793                 self["remote"].ftpclient = ftpclient
794                 self["remoteText"].setText(_("Remote"))
795
796                 self["remote"].changeDir(self.server.getPath())
797
798         def connectionFailed(self, *args):
799                 print "[FTPBrowser] connection failed", args
800
801                 self.server = None
802                 self["remoteText"].setText(_("Remote (not connected)"))
803                 self.session.open(
804                                 MessageBox,
805                                 _("Could not connect to ftp server!"),
806                                 type = MessageBox.TYPE_ERROR,
807                                 timeout = 3,
808                 )
809