1 # for localized messages
5 from enigma import RT_HALIGN_LEFT, eListboxPythonMultiContent
8 from Tools.Directories import SCOPE_SKIN_IMAGE, resolveFilename
9 from Tools.LoadPixmap import LoadPixmap
10 from Tools.Notifications import AddPopup, AddNotificationWithCallback
13 from Screens.Screen import Screen
14 from Screens.HelpMenu import HelpableScreen
15 from Screens.MessageBox import MessageBox
16 from Screens.ChoiceBox import ChoiceBox
17 from Screens.InfoBarGenerics import InfoBarNotifications
18 from FTPServerManager import FTPServerManager
19 from NTIVirtualKeyBoard import NTIVirtualKeyBoard
22 from Components.ActionMap import ActionMap, HelpableActionMap
23 from Components.Label import Label
24 from Components.FileList import FileList, FileEntryComponent, EXTENSIONS
25 from Components.Button import Button
26 from VariableProgressSource import VariableProgressSource
29 from twisted.internet import reactor, defer
30 from twisted.internet.protocol import Protocol, ClientCreator
31 from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
32 from twisted.protocols.basic import FileSender
35 from os import path as os_path, unlink as os_unlink, rename as os_rename, \
39 def FTPFileEntryComponent(file, directory):
40 isDir = True if file['filetype'] == 'd' else False
41 name = file['filename']
42 absolute = directory + name
47 (absolute, isDir, file['size']),
48 (eListboxPythonMultiContent.TYPE_TEXT, 35, 1, 470, 20, 0, RT_HALIGN_LEFT, name)
51 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/directory.png"))
53 extension = name.split('.')
54 extension = extension[-1].lower()
55 if EXTENSIONS.has_key(extension):
56 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/" + EXTENSIONS[extension] + ".png"))
60 res.append((eListboxPythonMultiContent.TYPE_PIXMAP_ALPHATEST, 10, 2, 20, 20, png))
64 class FTPFileList(FileList):
69 FileList.__init__(self, "/")
71 def changeDir(self, directory, select = None):
75 if self.ftpclient is None:
77 self.l.setList(self.list)
80 self.current_directory = directory
83 self.filelist = FTPFileListProtocol()
84 d = self.ftpclient.list(directory, self.filelist)
85 d.addCallback(self.listRcvd).addErrback(self.listFailed)
87 def listRcvd(self, *args):
88 # TODO: is any of the 'advanced' features useful (and more of all can they be implemented) here?
89 list = [FTPFileEntryComponent(file, self.current_directory) for file in self.filelist.files]
90 list.sort(key = lambda x: (not x[0][1], x[0][0]))
91 if self.current_directory != "/":
92 list.insert(0, FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True))
99 if select is not None:
110 def listFailed(self, *args):
111 # 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)
112 if self.current_directory != "/":
114 FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True),
115 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
119 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
123 self.l.setList(self.list)
125 class FTPBrowser(Screen, Protocol, InfoBarNotifications, HelpableScreen):
127 <screen name="FTPBrowser" position="center,center" size="560,440" title="FTP Browser">
128 <widget name="localText" position="20,10" size="200,20" font="Regular;18" />
129 <widget name="local" position="20,40" size="255,320" scrollbarMode="showOnDemand" />
130 <widget name="remoteText" position="285,10" size="200,20" font="Regular;18" />
131 <widget name="remote" position="285,40" size="255,320" scrollbarMode="showOnDemand" />
132 <widget name="eta" position="20,360" size="200,30" font="Regular;23" />
133 <widget name="speed" position="330,360" size="200,30" halign="right" font="Regular;23" />
134 <widget source="progress" render="Progress" position="20,390" size="520,10" />
135 <ePixmap name="green" position="10,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
136 <ePixmap name="yellow" position="180,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
137 <ePixmap name="blue" position="350,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
138 <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" />
139 <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" />
140 <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" />
141 <ePixmap position="515,408" zPosition="1" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on" />
144 def __init__(self, session):
145 Screen.__init__(self, session)
146 HelpableScreen.__init__(self)
147 InfoBarNotifications.__init__(self)
148 self.ftpclient = None
151 self.currlist = "local"
153 # Init what we need for dl progress
154 self.currentLength = 0
160 self["localText"] = Label(_("Local"))
161 self["local"] = FileList("/media/hdd/", showMountpoints = False)
162 self["remoteText"] = Label(_("Remote (not connected)"))
163 self["remote"] = FTPFileList()
164 self["eta"] = Label("")
165 self["speed"] = Label("")
166 self["progress"] = VariableProgressSource()
167 self["key_red"] = Button(_("Exit"))
168 self["key_green"] = Button(_("Rename"))
169 self["key_yellow"] = Button(_("Delete"))
170 self["key_blue"] = Button(_("Download"))
174 self["ftpbrowserBaseActions"] = HelpableActionMap(self, "ftpbrowserBaseActions",
176 "ok": (self.ok, _("enter directory/get file/put file")),
177 "cancel": (self.cancel , _("close")),
178 "menu": (self.menu, _("open menu")),
181 self["ftpbrowserListActions"] = HelpableActionMap(self, "ftpbrowserListActions",
183 "channelUp": (self.setLocal, _("Select local file list")),
184 "channelDown": (self.setRemote, _("Select remote file list")),
187 self["actions"] = ActionMap(["ftpbrowserDirectionActions", "ColorActions"],
193 "green": self.rename,
194 "yellow": self.delete,
195 "blue": self.transfer,
198 self.onExecBegin.append(self.reinitialize)
200 def reinitialize(self):
201 # NOTE: this will clear the remote file list if we are not currently connected. this behavior is intended.
202 # XXX: but do we also want to do this when we just returned from a notification?
203 self["remote"].refresh()
204 self["local"].refresh()
206 if not self.ftpclient:
207 self.connect(self.server)
208 # XXX: Actually everything else should be taken care of... recheck this!
210 def managerCallback(self, uri):
215 self.session.openWithCallback(
216 self.managerCallback,
221 self.currlist = "local"
224 self.currlist = "remote"
226 def okQuestion(self, res = None):
228 self.ok(force = True)
230 def getRemoteFile(self):
231 remoteFile = self["remote"].getSelection()
232 if not remoteFile or not remoteFile[0]:
233 return None, None, None
235 absRemoteFile = remoteFile[0]
237 fileName = absRemoteFile.split('/')[-2]
239 fileName = absRemoteFile.split('/')[-1]
241 if len(remoteFile) == 3:
242 fileSize = remoteFile[2]
246 return absRemoteFile, fileName, fileSize
248 def getLocalFile(self):
249 # XXX: isn't this supposed to be an absolute filename? well, it's not for me :-/
250 localFile = self["local"].getSelection()
255 absLocalFile = localFile[0]
256 fileName = absLocalFile.split('/')[-2]
258 fileName = localFile[0]
259 absLocalFile = self["local"].getCurrentDirectory() + fileName
261 return absLocalFile, fileName
263 def renameCallback(self, newName = None):
267 if self.currlist == "remote":
268 absRemoteFile, fileName, fileSize = self.getRemoteFile()
272 directory = self["remote"].getCurrentDirectory()
273 sep = '/' if directory != '/' else ''
274 newRemoteFile = directory + sep + newName
276 def callback(ret = None):
277 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
278 def errback(ret = None):
279 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
281 self.ftpclient.rename(absRemoteFile, newRemoteFile).addCallback(callback).addErrback(errback)
283 assert(self.currlist == "local")
284 absLocalFile, fileName = self.getLocalFile()
288 directory = self["local"].getCurrentDirectory()
289 newLocalFile = os_path.join(directory, newName)
292 os_rename(absLocalFile, newLocalFile)
294 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
296 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
299 if not self.ftpclient or self.queue:
302 if self.currlist == "remote":
303 absRemoteFile, fileName, fileSize = self.getRemoteFile()
307 assert(self.currlist == "local")
308 absLocalFile, fileName = self.getLocalFile()
312 self.session.openWithCallback(
315 title = _("Enter new filename:"),
319 def deleteConfirmed(self, ret):
323 if self.currlist == "remote":
324 absRemoteFile, fileName, fileSize = self.getRemoteFile()
328 def callback(ret = None):
329 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
330 def errback(ret = None):
331 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
333 self.ftpclient.removeFile(absRemoteFile).addCallback(callback).addErrback(errback)
335 assert(self.currlist == "local")
336 absLocalFile, fileName = self.getLocalFile()
341 os_unlink(absLocalFile)
343 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
345 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
348 if not self.ftpclient or self.queue:
351 if self.currlist == "remote":
352 if self["remote"].canDescent():
355 _("Removing directories is not supported."),
356 MessageBox.TYPE_WARNING
360 absRemoteFile, fileName, fileSize = self.getRemoteFile()
364 assert(self.currlist == "local")
365 if self["local"].canDescent():
368 _("Removing directories is not supported."),
369 MessageBox.TYPE_WARNING
373 absLocalFile, fileName = self.getLocalFile()
377 self.session.openWithCallback(
378 self.deleteConfirmed,
380 _("Are you sure you want to delete %s?") % (fileName)
383 def transferListRcvd(self, res, filelist):
384 remoteDirectory, _, _ = self.getRemoteFile()
385 localDirectory = self["local"].getCurrentDirectory()
387 self.queue = [(True, remoteDirectory + file["filename"], localDirectory + file["filename"], file["size"]) for file in filelist.files if file["filetype"] == "-"]
393 self.queue = self.queue[1:]
395 self.getFile(*top[1:])
397 self.putFile(*top[1:])
399 def transferListFailed(self, res = None):
401 AddPopup(_("Could not obtain list of files."), MessageBox.TYPE_ERROR, -1)
404 if not self.ftpclient or self.queue:
407 if self.currlist == "remote":
408 # single file transfer is implemented in self.ok
409 if not self["remote"].canDescent():
412 absRemoteFile, fileName, fileSize = self.getRemoteFile()
416 filelist = FTPFileListProtocol()
417 d = self.ftpclient.list(absRemoteFile, filelist)
418 d.addCallback(self.transferListRcvd, filelist).addErrback(self.transferListFailed)
420 assert(self.currlist == "local")
421 # single file transfer is implemented in self.ok
422 if not self["local"].canDescent():
425 localDirectory, _ = self.getLocalFile()
426 remoteDirectory = self["remote"].getCurrentDirectory()
428 def remoteFileExists(absName):
429 for file in self["remote"].getFileList():
430 if file[0][0] == absName:
434 self.queue = [(False, remoteDirectory + file, localDirectory + file, remoteFileExists(remoteDirectory + file)) for file in os_listdir(localDirectory) if os_path.isfile(localDirectory + file)]
438 def getFileCallback(self, ret, absRemoteFile, absLocalFile, fileSize):
442 self.getFile(absRemoteFile, absLocalFile, fileSize, force=True)
444 def getFile(self, absRemoteFile, absLocalFile, fileSize, force=False):
445 if not force and os_path.exists(absLocalFile):
446 fileName = absRemoteFile.split('/')[-1]
447 AddNotificationWithCallback(
448 lambda ret: self.getFileCallback(ret, absRemoteFile, absLocalFile, fileSize),
450 _("A file with this name (%s) already exists locally.\nDo you want to overwrite it?") % (fileName),
453 self.currentLength = 0
457 self.fileSize = fileSize
460 self.file = open(absLocalFile, 'w')
465 d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
466 d.addCallback(self.getFinished).addErrback(self.getFailed)
468 def putFileCallback(self, ret, absRemoteFile, absLocalFile, remoteFileExists):
472 self.putFile(absRemoteFile, absLocalFile, remoteFileExists, force=True)
474 def putFile(self, absRemoteFile, absLocalFile, remoteFileExists, force=False):
475 if not force and remoteFileExists:
476 fileName = absRemoteFile.split('/')[-1]
477 AddNotificationWithCallback(
478 lambda ret: self.putFileCallback(ret, absRemoteFile, absLocalFile, remoteFileExists),
480 _("A file with this name (%s) already exists on the remote host.\nDo you want to overwrite it?") % (fileName),
483 self.currentLength = 0
488 def sendfile(consumer, fileObj):
489 FileSender().beginFileTransfer(fileObj, consumer, transform = self.putProgress).addCallback(
490 lambda _: consumer.finish()).addCallback(
491 self.putComplete).addErrback(self.putFailed)
494 self.fileSize = int(os_path.getsize(absLocalFile))
495 self.file = open(absLocalFile, 'rb')
496 except (IOError, OSError), e:
500 dC, dL = self.ftpclient.storeFile(absRemoteFile)
501 dC.addCallback(sendfile, self.file)
503 def ok(self, force = False):
507 if self.currlist == "remote":
508 if not self.ftpclient:
511 # Get file/change directory
512 if self["remote"].canDescent():
513 self["remote"].descent()
518 _("There already is an active transfer."),
519 type = MessageBox.TYPE_WARNING
523 absRemoteFile, fileName, fileSize = self.getRemoteFile()
527 absLocalFile = self["local"].getCurrentDirectory() + fileName
529 self.getFile(absRemoteFile, absLocalFile, fileSize)
531 # Put file/change directory
532 assert(self.currlist == "local")
533 if self["local"].canDescent():
534 self["local"].descent()
536 if not self.ftpclient:
542 _("There already is an active transfer."),
543 type = MessageBox.TYPE_WARNING
547 if not self["remote"].isValid:
550 absLocalFile, fileName = self.getLocalFile()
554 directory = self["remote"].getCurrentDirectory()
555 sep = '/' if directory != '/' else ''
556 absRemoteFile = directory + sep + fileName
558 def remoteFileExists(absName):
559 for file in self["remote"].getFileList():
560 if file[0][0] == absName:
564 self.putFile(absRemoteFile, absLocalFile, remoteFileExists(absRemoteFile))
566 def transferFinished(self, msg, type, toRefresh):
567 AddPopup(msg, type, -1)
569 self["eta"].setText("")
570 self["speed"].setText("")
571 self["progress"].invalidate()
572 self[toRefresh].refresh()
576 def putComplete(self, *args):
583 self.transferFinished(
584 _("Upload finished."),
585 MessageBox.TYPE_INFO,
589 def putFailed(self, *args):
590 # NOTE: we continue uploading but notify the user of every error though
591 # we only display one success notification
592 self.transferFinished(
593 _("Error during download."),
594 MessageBox.TYPE_ERROR,
600 def getFinished(self, *args):
607 self.transferFinished(
608 _("Download finished."),
609 MessageBox.TYPE_INFO,
613 def getFailed(self, *args):
614 # NOTE: we continue downloading but notify the user of every error though
615 # we only display one success notification
616 self.transferFinished(
617 _("Error during download."),
618 MessageBox.TYPE_ERROR,
624 def putProgress(self, chunk):
625 self.currentLength += len(chunk)
626 self.gotProgress(self.currentLength, self.fileSize)
629 def gotProgress(self, pos, max):
630 self["progress"].writeValues(pos, max)
633 # Check if we're called the first time (got total)
634 lastTime = self.lastTime
636 self.lastTime = newTime
638 # 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)
639 elif int(newTime - lastTime) >= 2:
640 lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
642 secLen = int(round(((max-pos) / 1024) / lastApprox))
643 self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
644 self["speed"].setText(_("%d kb/s") % (lastApprox))
646 self.lastApprox = lastApprox
647 self.lastLength = pos
648 self.lastTime = newTime
650 def dataReceived(self, data):
654 self.currentLength += len(data)
655 self.gotProgress(self.currentLength, self.fileSize)
658 self.file.write(data)
664 def cancelQuestion(self, res = None):
674 if self.file is not None:
675 self.session.openWithCallback(
678 title = _("A transfer is currently in progress.\nWhat do you want to do?"),
680 (_("Run in Background"), 2),
681 (_("Abort transfer"), 1),
691 self[self.currlist].up()
694 self[self.currlist].down()
697 self[self.currlist].pageUp()
700 self[self.currlist].pageDown()
702 def disconnect(self):
704 # XXX: according to the docs we should wait for the servers answer to our quit request, we just hope everything goes well here
705 self.ftpclient.quit()
706 self.ftpclient = None
707 self["remote"].ftpclient = None
708 self["remoteText"].setText(_("Remote (not connected)"))
710 def connectWrapper(self, ret):
714 def connect(self, server):
722 username = server.getUsername()
724 username = 'anonymous'
725 password = 'my@email.com'
727 password = server.getPassword()
729 host = server.getAddress()
730 passive = server.getPassive()
731 port = server.getPort()
732 timeout = 30 # TODO: make configurable
734 # 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
736 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
737 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
739 def controlConnectionMade(self, ftpclient):
740 print "[FTPBrowser] connection established"
741 self.ftpclient = ftpclient
742 self["remote"].ftpclient = ftpclient
743 self["remoteText"].setText(_("Remote"))
745 self["remote"].changeDir(self.server.getPath())
747 def connectionFailed(self, *args):
748 print "[FTPBrowser] connection failed", args
751 self["remoteText"].setText(_("Remote (not connected)"))
754 _("Could not connect to ftp server!"),
755 type = MessageBox.TYPE_ERROR,