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