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