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