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