fix usage of choicebox
[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
11
12 # GUI (Screens)
13 from Screens.Screen import Screen
14 from Screens.MessageBox import MessageBox
15 from Screens.ChoiceBox import ChoiceBox
16 from Screens.InfoBarGenerics import InfoBarNotifications
17
18 # GUI (Components)
19 from Components.ActionMap import ActionMap, HelpableActionMap
20 from Components.Label import Label
21 from Components.FileList import FileList, FileEntryComponent, EXTENSIONS
22 from Components.Button import Button
23 from VariableProgressSource import VariableProgressSource
24
25 # FTP Client
26 from twisted.internet import reactor, defer
27 from twisted.internet.protocol import Protocol, ClientCreator
28 from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
29 from twisted.protocols.basic import FileSender
30
31 # For new and improved _parse
32 from urlparse import urlparse, urlunparse
33
34 # System
35 from os import path as os_path
36 from time import time
37
38 def _parse(url, defaultPort = None):
39         url = url.strip()
40         parsed = urlparse(url)
41         scheme = parsed[0]
42         path = urlunparse(('','')+parsed[2:])
43
44         if defaultPort is None:
45                 if scheme == 'https':
46                         defaultPort = 443
47                 elif scheme == 'ftp':
48                         defaultPort = 21
49                 else:
50                         defaultPort = 80
51
52         host, port = parsed[1], defaultPort
53
54         if '@' in host:
55                 username, host = host.split('@')
56                 if ':' in username:
57                         username, password = username.split(':')
58                 else:
59                         password = ""
60         else:
61                 username = ""
62                 password = ""
63
64         if ':' in host:
65                 host, port = host.split(':')
66                 port = int(port)
67
68         if path == "":
69                 path = "/"
70
71         return scheme, host, port, path, username, password
72
73 def FTPFileEntryComponent(file, directory):
74         isDir = True if file['filetype'] == 'd' else False
75         name = file['filename']
76         absolute = directory + name
77         if isDir:
78                 absolute += '/'
79
80         res = [
81                 (absolute, isDir, file['size']),
82                 (eListboxPythonMultiContent.TYPE_TEXT, 35, 1, 470, 20, 0, RT_HALIGN_LEFT, name)
83         ]
84         if isDir:
85                 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/directory.png"))
86         else:
87                 extension = name.split('.')
88                 extension = extension[-1].lower()
89                 if EXTENSIONS.has_key(extension):
90                         png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/" + EXTENSIONS[extension] + ".png"))
91                 else:
92                         png = None
93         if png is not None:
94                 res.append((eListboxPythonMultiContent.TYPE_PIXMAP_ALPHATEST, 10, 2, 20, 20, png))
95
96         return res
97
98 class FTPFileList(FileList):
99         def __init__(self):
100                 self.ftpclient = None
101                 self.select = None
102                 FileList.__init__(self, "/")
103
104         def changeDir(self, directory, select = None):
105                 if self.ftpclient is None:
106                         self.list = []
107                         self.l.setList(self.list)
108                         return
109
110                 self.current_directory = directory
111                 self.select = select
112
113                 self.filelist = FTPFileListProtocol()
114                 d = self.ftpclient.list(directory, self.filelist)
115                 d.addCallback(self.listRcvd).addErrback(self.listFailed)
116
117         def listRcvd(self, *args):
118                 # TODO: is any of the 'advanced' features useful (and more of all can they be implemented) here?
119                 list = [FTPFileEntryComponent(file, self.current_directory) for file in self.filelist.files]
120                 list.sort(key = lambda x: (not x[0][1], x[0][0]))
121                 if self.current_directory != "/":
122                         list.insert(0, FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True))
123
124                 self.l.setList(list)
125                 self.list = list
126
127                 select = self.select
128                 if select is not None:
129                         i = 0
130                         self.moveToIndex(0)
131                         for x in list:
132                                 p = x[0][0]
133
134                                 if p == select:
135                                         self.moveToIndex(i)
136                                         break
137                                 i += 1
138
139         def listFailed(self, *args):
140                 if self.current_directory != "/":
141                         self.list = [FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True)]
142                 else:
143                         self.list = []
144                 self.l.setList(self.list)
145
146 class FTPBrowser(Screen, Protocol, InfoBarNotifications):
147         skin = """
148                 <screen name="FTPBrowser" position="center,center" size="560,440" title="FTP Browser">
149                         <widget name="localText" position="20,10" size="200,20" font="Regular;18" />
150                         <widget name="local" position="20,40" size="255,320" scrollbarMode="showOnDemand" />
151                         <widget name="remoteText" position="285,10" size="200,20" font="Regular;18" />
152                         <widget name="remote" position="285,40" size="255,320" scrollbarMode="showOnDemand" />
153                         <widget name="eta" position="20,360" size="200,30" font="Regular;23" />
154                         <widget name="speed" position="330,360" size="200,30" halign="right" font="Regular;23" />
155                         <widget source="progress" render="Progress" position="20,390" size="520,10" />
156                         <ePixmap name="red" position="0,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on" />
157                         <ePixmap name="green" position="140,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
158                         <ePixmap name="yellow" position="280,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
159                         <ePixmap name="blue" position="420,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
160                         <widget name="key_red" position="0,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
161                         <widget name="key_green" position="140,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
162                         <widget name="key_yellow" position="280,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
163                         <widget name="key_blue" position="420,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
164                 </screen>"""
165
166         def __init__(self, session):
167                 Screen.__init__(self, session)
168                 InfoBarNotifications.__init__(self)
169                 self.ftpclient = None
170                 self.file = None
171                 self.currlist = "remote"
172
173                 # Init what we need for dl progress
174                 self.currentLength = 0
175                 self.lastLength = 0
176                 self.lastTime = 0
177                 self.lastApprox = 0
178                 self.fileSize = 0
179
180                 self["localText"] = Label(_("Local"))
181                 self["local"] = FileList("/media/hdd/", showMountpoints = False)
182                 self["remoteText"] = Label(_("Remote"))
183                 self["remote"] = FTPFileList()
184                 self["eta"] = Label("")
185                 self["speed"] = Label("")
186                 self["progress"] = VariableProgressSource()
187                 self["key_red"] = Button("")
188                 self["key_green"] = Button("")
189                 self["key_yellow"] = Button("")
190                 self["key_blue"] = Button("")
191
192                 self.URI = "ftp://root@localhost:21" # TODO: make configurable
193
194                 self["ftpbrowserBaseActions"] = HelpableActionMap(self, "ftpbrowserBaseActions",
195                         {
196                                 "ok": (self.ok, _("enter directory/get file/put file")),
197                                 "cancel": (self.cancel , _("close")),
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"],
207                         {
208                                 "up": self.up,
209                                 "down": self.down,
210                                 "left": self.left,
211                                 "right": self.right,
212                         }, -2)
213
214                 self.onExecBegin.append(self.reinitialize)
215
216         def reinitialize(self):
217                 # NOTE: this will clear the remote file list if we are not currently connected. this behavior is intended.
218                 self["remote"].refresh()
219                 self["local"].refresh()
220
221                 if not self.ftpclient:
222                         self.connect(self.URI)
223                 # XXX: Actually everything else should be taken care of... recheck this!
224
225         def setLocal(self):
226                 self.currlist = "local"
227
228         def setRemote(self):
229                 self.currlist = "remote"
230
231         def okQuestion(self, res = None):
232                 if res:
233                         self.ok(force = True)
234
235         def ok(self, force = False):
236                 if self.currlist == "remote":
237                         # Get file/change directory
238                         if self["remote"].canDescent():
239                                 self["remote"].descent()
240                         else:
241                                 if self.file:
242                                         self.session.open(
243                                                 MessageBox,
244                                                 _("There already is an active transfer."),
245                                                 type = MessageBox.TYPE_WARNING
246                                         )
247                                         return
248
249                                 remoteFile = self["remote"].getSelection()
250                                 if not remoteFile:
251                                         return
252
253                                 absRemoteFile = remoteFile[0]
254                                 fileName = absRemoteFile.split('/')[-1]
255                                 localFile = self["local"].getCurrentDirectory() + fileName
256                                 if not force and os_path.exists(localFile):
257                                         self.session.openWithCallback(
258                                                 self.okQuestion,
259                                                 MessageBox,
260                                                 _("A file with this name already exists locally.\nDo you want to overwrite it?"),
261                                         )
262                                 else:
263                                         self.currentLength = 0
264                                         self.lastLength = 0
265                                         self.lastTime = 0
266                                         self.lastApprox = 0
267                                         self.fileSize = remoteFile[2]
268
269                                         try:
270                                                 self.file = open(localFile, 'w')
271                                         except IOError, ie:
272                                                 # TODO: handle this
273                                                 raise ie
274                                         else:
275                                                 d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
276                                                 d.addCallback(self.getFinished).addErrback(self.getFailed)
277                 else:
278                         # Put file/change directory
279                         assert(self.currlist == "local")
280                         if self["local"].canDescent():
281                                 self["local"].descent()
282                         else:
283                                 if self.file:
284                                         self.session.open(
285                                                 MessageBox,
286                                                 _("There already is an active transfer."),
287                                                 type = MessageBox.TYPE_WARNING
288                                         )
289                                         return
290
291                                 localFile = self["local"].getSelection()
292                                 if not localFile:
293                                         return
294
295                                 def remoteFileExists(absName):
296                                         for file in self["remote"].getFileList():
297                                                 if file[0][0] == absName:
298                                                         return True
299                                         return False
300
301                                 # XXX: isn't this supposed to be an absolute filename? well, it's not for me :-/
302                                 fileName = localFile[0]
303                                 absLocalFile = self["local"].getCurrentDirectory() + fileName
304                                 directory = self["remote"].getCurrentDirectory()
305                                 sep = '/' if directory != '/' else ''
306                                 remoteFile = directory + sep + fileName
307                                 if not force and remoteFileExists(remoteFile):
308                                         self.session.openWithCallback(
309                                                 self.okQuestion,
310                                                 MessageBox,
311                                                 _("A file with this name already exists on the remote host.\nDo you want to overwrite it?"),
312                                         )
313                                 else:
314                                         self.currentLength = 0
315                                         self.lastLength = 0
316                                         self.lastTime = 0
317                                         self.lastApprox = 0
318
319                                         def sendfile(consumer, fileObj):
320                                                 FileSender().beginFileTransfer(fileObj, consumer, transform = self.putProgress).addCallback(  
321                                                         lambda _: consumer.finish()).addCallback(
322                                                         self.putComplete).addErrback(self.putFailed)
323
324                                         try:
325                                                 self.fileSize = int(os_path.getsize(absLocalFile))
326                                                 self.file = open(absLocalFile, 'rb')
327                                         except (IOError, OSError), e:
328                                                 # TODO: handle this
329                                                 raise e
330                                         else:
331                                                 dC, dL = self.ftpclient.storeFile(remoteFile)
332                                                 dC.addCallback(sendfile, self.file)
333
334         def transferFinished(self, msg, type, toRefresh):
335                 AddPopup(msg, type, -1)
336
337                 self["eta"].setText("")
338                 self["speed"].setText("")
339                 self["progress"].invalidate()
340                 self[toRefresh].refresh()
341                 self.file.close()
342                 self.file = None
343
344         def putComplete(self, *args):
345                 self.transferFinished(
346                         _("Upload finished."),
347                         MessageBox.TYPE_INFO,
348                         "remote"
349                 )
350
351         def putFailed(self, *args):
352                 self.transferFinished(
353                         _("Error during download."),
354                         MessageBox.TYPE_ERROR,
355                         "remote"
356                 )
357
358         def getFinished(self, *args):
359                 self.transferFinished(
360                         _("Download finished."),
361                         MessageBox.TYPE_INFO,
362                         "local"
363                 )
364
365         def getFailed(self, *args):
366                 self.transferFinished(
367                         _("Error during download."),
368                         MessageBox.TYPE_ERROR,
369                         "local"
370                 )
371
372         def putProgress(self, chunk):
373                 self.currentLength += len(chunk)
374                 self.gotProgress(self.currentLength, self.fileSize)
375                 return chunk
376
377         def gotProgress(self, pos, max):
378                 self["progress"].writeValues(pos, max)
379
380                 newTime = time()
381                 # Check if we're called the first time (got total)
382                 lastTime = self.lastTime
383                 if lastTime == 0:
384                         self.lastTime = newTime
385
386                 # 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)
387                 elif int(newTime - lastTime) >= 2:
388                         lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
389
390                         secLen = int(round(((max-pos) / 1024) / lastApprox))
391                         self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
392                         self["speed"].setText(_("%d kb/s") % (lastApprox))
393
394                         self.lastApprox = lastApprox
395                         self.lastLength = pos
396                         self.lastTime = newTime
397
398         def dataReceived(self, data):
399                 if not self.file:
400                         return
401
402                 self.currentLength += len(data)
403                 self.gotProgress(self.currentLength, self.fileSize)
404
405                 try:
406                         self.file.write(data)
407                 except IOError, ie:
408                         # TODO: handle this
409                         self.file = None
410                         raise ie
411
412         def cancelQuestion(self, res = None):
413                 res = res and res[1]
414                 if res:
415                         if res == 1:
416                                 self.file.close()
417                                 self.file = None
418                                 self.ftpclient.quit()
419                                 self.ftpclient = None
420                         self.close()
421
422         def cancel(self):
423                 if self.file is not None:
424                         self.session.openWithCallback(
425                                 self.cancelQuestion,
426                                 ChoiceBox,
427                                 title = _("A transfer is currently in progress.\nWhat do you want to do?"),
428                                 list = (
429                                         (_("Abort transfer"), 1),
430                                         (_("Run in Background"), 2),
431                                         (_("Cancel"), 0)
432                                 )
433                         )
434                         return
435
436                 self.ftpclient.quit()
437                 self.ftpclient = None
438                 self.close()
439
440         def up(self):
441                 self[self.currlist].up()
442
443         def down(self):
444                 self[self.currlist].down()
445
446         def left(self):
447                 self[self.currlist].pageUp()
448
449         def right(self):
450                 self[self.currlist].pageDown()
451
452         def connect(self, address):
453                 self.ftpclient = None
454                 self["remote"].ftpclient = None
455
456                 scheme, host, port, path, username, password = _parse(address)
457                 if not username:
458                         username = 'anonymous'
459                         password = 'my@email.com'
460
461                 timeout = 30 # TODO: make configurable
462                 passive = True # TODO: make configurable
463                 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
464
465                 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
466
467         def controlConnectionMade(self, ftpclient):
468                 print "[FTPBrowser] connection established"
469                 self.ftpclient = ftpclient
470                 self["remote"].ftpclient = ftpclient
471                 self["remote"].changeDir("/")
472
473         def connectionFailed(self, *args):
474                 print "[FTPBrowser] connection failed", args
475                 self.session.open(
476                                 MessageBox,
477                                 _("Could not connect to ftp server!"),
478                                 type = MessageBox.TYPE_ERROR,
479                                 timeout = 3,
480                 )
481