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 FTPQueueManager import FTPQueueManager
20 from NTIVirtualKeyBoard import NTIVirtualKeyBoard
23 from Components.ActionMap import ActionMap, HelpableActionMap
24 from Components.Label import Label
25 from Components.FileList import FileList, FileEntryComponent, EXTENSIONS
26 from Components.Button import Button
27 from VariableProgressSource import VariableProgressSource
30 from twisted.internet import reactor, defer
31 from twisted.internet.protocol import Protocol, ClientCreator
32 from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
33 from twisted.protocols.basic import FileSender
36 from os import path as os_path, unlink as os_unlink, rename as os_rename, \
41 def FTPFileEntryComponent(file, directory):
42 isDir = True if file['filetype'] == 'd' else False
43 name = file['filename']
44 absolute = directory + name
49 (absolute, isDir, file['size']),
50 (eListboxPythonMultiContent.TYPE_TEXT, 35, 1, 470, 20, 0, RT_HALIGN_LEFT, name)
53 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/directory.png"))
55 extension = name.split('.')
56 extension = extension[-1].lower()
57 if EXTENSIONS.has_key(extension):
58 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/" + EXTENSIONS[extension] + ".png"))
62 res.append((eListboxPythonMultiContent.TYPE_PIXMAP_ALPHATEST, 10, 2, 20, 20, png))
66 class ModifiedFTPFileListProtocol(FTPFileListProtocol):
67 fileLinePattern = re.compile(
68 r'^(?P<filetype>.)(?P<perms>.{9})\s+(?P<nlinks>\d*)\s*'
69 r'(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)\s+'
70 r'(?P<date>...\s+\d+\s+[\d:]+)\s+(?P<filename>.*?)'
71 r'( -> (?P<linktarget>[^\r]*))?\r?$'
74 class FTPFileList(FileList):
79 FileList.__init__(self, "/")
81 def changeDir(self, directory, select = None):
85 if self.ftpclient is None:
87 self.l.setList(self.list)
90 self.current_directory = directory
93 self.filelist = ModifiedFTPFileListProtocol()
94 d = self.ftpclient.list(directory, self.filelist)
95 d.addCallback(self.listRcvd).addErrback(self.listFailed)
97 def listRcvd(self, *args):
98 # TODO: is any of the 'advanced' features useful (and more of all can they be implemented) here?
99 list = [FTPFileEntryComponent(file, self.current_directory) for file in self.filelist.files]
100 list.sort(key = lambda x: (not x[0][1], x[0][0]))
101 if self.current_directory != "/":
102 list.insert(0, FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True))
109 if select is not None:
120 def listFailed(self, *args):
121 # 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)
122 if self.current_directory != "/":
124 FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True),
125 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
129 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
133 self.l.setList(self.list)
135 class FTPBrowser(Screen, Protocol, InfoBarNotifications, HelpableScreen):
137 <screen name="FTPBrowser" position="center,center" size="560,440" title="FTP Browser">
138 <widget name="localText" position="20,10" size="200,20" font="Regular;18" />
139 <widget name="local" position="20,40" size="255,320" scrollbarMode="showOnDemand" />
140 <widget name="remoteText" position="285,10" size="200,20" font="Regular;18" />
141 <widget name="remote" position="285,40" size="255,320" scrollbarMode="showOnDemand" />
142 <widget name="eta" position="20,360" size="200,30" font="Regular;23" />
143 <widget name="speed" position="330,360" size="200,30" halign="right" font="Regular;23" />
144 <widget source="progress" render="Progress" position="20,390" size="520,10" />
145 <ePixmap name="green" position="10,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
146 <ePixmap name="yellow" position="180,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
147 <ePixmap name="blue" position="350,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
148 <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" />
149 <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" />
150 <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" />
151 <ePixmap position="515,408" zPosition="1" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on" />
154 def __init__(self, session):
155 Screen.__init__(self, session)
156 HelpableScreen.__init__(self)
157 InfoBarNotifications.__init__(self)
158 self.ftpclient = None
159 self.queueManagerInstance = None
162 self.currlist = "local"
164 # # NOTE: having self.checkNotifications in onExecBegin might make our gui
165 # disappear, so let's move it to onShow
166 self.onExecBegin.remove(self.checkNotifications)
167 self.onShow.append(self.checkNotifications)
169 # Init what we need for dl progress
170 self.currentLength = 0
176 self["localText"] = Label(_("Local"))
177 self["local"] = FileList("/media/hdd/", showMountpoints = False)
178 self["remoteText"] = Label(_("Remote (not connected)"))
179 self["remote"] = FTPFileList()
180 self["eta"] = Label("")
181 self["speed"] = Label("")
182 self["progress"] = VariableProgressSource()
183 self["key_red"] = Button(_("Exit"))
184 self["key_green"] = Button(_("Rename"))
185 self["key_yellow"] = Button(_("Delete"))
186 self["key_blue"] = Button(_("Upload"))
190 self["ftpbrowserBaseActions"] = HelpableActionMap(self, "ftpbrowserBaseActions",
192 "ok": (self.ok, _("enter directory/get file/put file")),
193 "cancel": (self.cancel , _("close")),
194 "menu": (self.menu, _("open menu")),
197 self["ftpbrowserListActions"] = HelpableActionMap(self, "ftpbrowserListActions",
199 "channelUp": (self.setLocal, _("Select local file list")),
200 "channelDown": (self.setRemote, _("Select remote file list")),
203 self["actions"] = ActionMap(["ftpbrowserDirectionActions", "ColorActions"],
209 "green": self.rename,
210 "yellow": self.delete,
211 "blue": self.transfer,
214 self.onExecBegin.append(self.reinitialize)
216 def reinitialize(self):
217 # NOTE: this will clear the remote file list if we are not currently connected. this behavior is intended.
218 # XXX: but do we also want to do this when we just returned from a notification?
220 self["remote"].refresh()
221 except AttributeError, ae:
222 # NOTE: we assume the connection was timed out by the server
223 self.ftpclient = None
224 self["remote"].ftpclient = None
225 self["remote"].refresh()
227 self["local"].refresh()
229 if not self.ftpclient:
230 self.connect(self.server)
231 # XXX: Actually everything else should be taken care of... recheck this!
233 def serverManagerCallback(self, uri):
237 def serverManager(self):
238 self.session.openWithCallback(
239 self.serverManagerCallback,
243 def queueManagerCallback(self):
244 self.queueManagerInstance = None
246 def queueManager(self):
247 self.queueManagerInstance = self.session.openWithCallback(
248 self.queueManagerCallback,
253 def menuCallback(self, ret):
257 self.session.openWithCallback(
261 (_("Server Manager"), self.serverManager),
262 (_("Queue Manager"), self.queueManager),
267 self.currlist = "local"
268 self["key_blue"].setText(_("Upload"))
271 self.currlist = "remote"
272 self["key_blue"].setText(_("Download"))
274 def okQuestion(self, res = None):
276 self.ok(force = True)
278 def getRemoteFile(self):
279 remoteFile = self["remote"].getSelection()
280 if not remoteFile or not remoteFile[0]:
281 return None, None, None
283 absRemoteFile = remoteFile[0]
285 fileName = absRemoteFile.split('/')[-2]
287 fileName = absRemoteFile.split('/')[-1]
289 if len(remoteFile) == 3:
290 fileSize = remoteFile[2]
294 return absRemoteFile, fileName, fileSize
296 def getLocalFile(self):
297 # XXX: isn't this supposed to be an absolute filename? well, it's not for me :-/
298 localFile = self["local"].getSelection()
303 absLocalFile = localFile[0]
304 fileName = absLocalFile.split('/')[-2]
306 fileName = localFile[0]
307 absLocalFile = self["local"].getCurrentDirectory() + fileName
309 return absLocalFile, fileName
311 def renameCallback(self, newName = None):
315 if self.currlist == "remote":
316 absRemoteFile, fileName, fileSize = self.getRemoteFile()
320 directory = self["remote"].getCurrentDirectory()
321 sep = '/' if directory != '/' else ''
322 newRemoteFile = directory + sep + newName
324 def callback(ret = None):
325 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
326 def errback(ret = None):
327 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
329 self.ftpclient.rename(absRemoteFile, newRemoteFile).addCallback(callback).addErrback(errback)
331 assert(self.currlist == "local")
332 absLocalFile, fileName = self.getLocalFile()
336 directory = self["local"].getCurrentDirectory()
337 newLocalFile = os_path.join(directory, newName)
340 os_rename(absLocalFile, newLocalFile)
342 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
344 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
350 if self.currlist == "remote":
351 if not self.ftpclient:
354 absRemoteFile, fileName, fileSize = self.getRemoteFile()
358 assert(self.currlist == "local")
359 absLocalFile, fileName = self.getLocalFile()
363 self.session.openWithCallback(
366 title = _("Enter new filename:"),
370 def deleteConfirmed(self, ret):
374 if self.currlist == "remote":
375 absRemoteFile, fileName, fileSize = self.getRemoteFile()
379 def callback(ret = None):
380 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
381 def errback(ret = None):
382 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
384 self.ftpclient.removeFile(absRemoteFile).addCallback(callback).addErrback(errback)
386 assert(self.currlist == "local")
387 absLocalFile, fileName = self.getLocalFile()
392 os_unlink(absLocalFile)
394 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
396 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
402 if self.currlist == "remote":
403 if not self.ftpclient:
406 if self["remote"].canDescent():
409 _("Removing directories is not supported."),
410 MessageBox.TYPE_WARNING
414 absRemoteFile, fileName, fileSize = self.getRemoteFile()
418 assert(self.currlist == "local")
419 if self["local"].canDescent():
422 _("Removing directories is not supported."),
423 MessageBox.TYPE_WARNING
427 absLocalFile, fileName = self.getLocalFile()
431 self.session.openWithCallback(
432 self.deleteConfirmed,
434 _("Are you sure you want to delete %s?") % (fileName)
437 def transferListRcvd(self, res, filelist):
438 remoteDirectory, _, _ = self.getRemoteFile()
439 localDirectory = self["local"].getCurrentDirectory()
441 self.queue = [(True, remoteDirectory + file["filename"], localDirectory + file["filename"], file["size"]) for file in filelist.files if file["filetype"] == "-"]
446 # NOTE: put this transfer back if there already is an active one,
447 # it will be picked up again when the active transfer is done
454 self.getFile(*top[1:])
456 self.putFile(*top[1:])
457 elif self.queue is not None:
459 self["eta"].setText("")
460 self["speed"].setText("")
461 self["progress"].invalidate()
462 AddPopup(_("Queue processed."), MessageBox.TYPE_INFO, -1)
464 if self.queueManagerInstance:
465 self.queueManagerInstance.updateList(self.queue)
467 def transferListFailed(self, res = None):
469 AddPopup(_("Could not obtain list of files."), MessageBox.TYPE_ERROR, -1)
472 if not self.ftpclient or self.queue:
475 if self.currlist == "remote":
476 # single file transfer is implemented in self.ok
477 if not self["remote"].canDescent():
480 absRemoteFile, fileName, fileSize = self.getRemoteFile()
484 filelist = ModifiedFTPFileListProtocol()
485 d = self.ftpclient.list(absRemoteFile, filelist)
486 d.addCallback(self.transferListRcvd, filelist).addErrback(self.transferListFailed)
488 assert(self.currlist == "local")
489 # single file transfer is implemented in self.ok
490 if not self["local"].canDescent():
493 localDirectory, _ = self.getLocalFile()
494 remoteDirectory = self["remote"].getCurrentDirectory()
496 def remoteFileExists(absName):
497 for file in self["remote"].getFileList():
498 if file[0][0] == absName:
502 self.queue = [(False, remoteDirectory + file, localDirectory + file, remoteFileExists(remoteDirectory + file)) for file in os_listdir(localDirectory) if os_path.isfile(localDirectory + file)]
506 def getFileCallback(self, ret, absRemoteFile, absLocalFile, fileSize):
510 self.getFile(absRemoteFile, absLocalFile, fileSize, force=True)
512 def getFile(self, absRemoteFile, absLocalFile, fileSize, force=False):
513 if not force and os_path.exists(absLocalFile):
514 fileName = absRemoteFile.split('/')[-1]
515 AddNotificationWithCallback(
516 lambda ret: self.getFileCallback(ret, absRemoteFile, absLocalFile, fileSize),
518 _("A file with this name (%s) already exists locally.\nDo you want to overwrite it?") % (fileName),
521 self.currentLength = 0
525 self.fileSize = fileSize
528 self.file = open(absLocalFile, 'w')
533 d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
534 d.addCallback(self.getFinished).addErrback(self.getFailed)
536 def putFileCallback(self, ret, absRemoteFile, absLocalFile, remoteFileExists):
540 self.putFile(absRemoteFile, absLocalFile, remoteFileExists, force=True)
542 def putFile(self, absRemoteFile, absLocalFile, remoteFileExists, force=False):
543 if not force and remoteFileExists:
544 fileName = absRemoteFile.split('/')[-1]
545 AddNotificationWithCallback(
546 lambda ret: self.putFileCallback(ret, absRemoteFile, absLocalFile, remoteFileExists),
548 _("A file with this name (%s) already exists on the remote host.\nDo you want to overwrite it?") % (fileName),
551 self.currentLength = 0
556 def sendfile(consumer, fileObj):
557 FileSender().beginFileTransfer(fileObj, consumer, transform = self.putProgress).addCallback(
558 lambda _: consumer.finish()).addCallback(
559 self.putComplete).addErrback(self.putFailed)
562 self.fileSize = int(os_path.getsize(absLocalFile))
563 self.file = open(absLocalFile, 'rb')
564 except (IOError, OSError), e:
568 dC, dL = self.ftpclient.storeFile(absRemoteFile)
569 dC.addCallback(sendfile, self.file)
571 def ok(self, force = False):
575 if self.currlist == "remote":
576 if not self.ftpclient:
579 # Get file/change directory
580 if self["remote"].canDescent():
581 self["remote"].descent()
586 _("There already is an active transfer."),
587 type = MessageBox.TYPE_WARNING
591 absRemoteFile, fileName, fileSize = self.getRemoteFile()
595 absLocalFile = self["local"].getCurrentDirectory() + fileName
597 self.getFile(absRemoteFile, absLocalFile, fileSize)
599 # Put file/change directory
600 assert(self.currlist == "local")
601 if self["local"].canDescent():
602 self["local"].descent()
604 if not self.ftpclient:
610 _("There already is an active transfer."),
611 type = MessageBox.TYPE_WARNING
615 if not self["remote"].isValid:
618 absLocalFile, fileName = self.getLocalFile()
622 def remoteFileExists(absName):
623 for file in self["remote"].getFileList():
624 if file[0][0] == absName:
628 absRemoteFile = self["remote"].getCurrentDirectory() + fileName
629 self.putFile(absRemoteFile, absLocalFile, remoteFileExists(absRemoteFile))
631 def transferFinished(self, msg, type, toRefresh):
632 AddPopup(msg, type, -1)
634 self["eta"].setText("")
635 self["speed"].setText("")
636 self["progress"].invalidate()
637 self[toRefresh].refresh()
641 def putComplete(self, *args):
642 if self.queue is not None:
648 self.transferFinished(
649 _("Upload finished."),
650 MessageBox.TYPE_INFO,
654 def putFailed(self, *args):
655 # NOTE: we continue uploading but notify the user of every error though
656 # we only display one success notification
657 self.transferFinished(
658 _("Error during download."),
659 MessageBox.TYPE_ERROR,
662 if self.queue is not None:
665 def getFinished(self, *args):
666 if self.queue is not None:
672 self.transferFinished(
673 _("Download finished."),
674 MessageBox.TYPE_INFO,
678 def getFailed(self, *args):
679 # NOTE: we continue downloading but notify the user of every error though
680 # we only display one success notification
681 self.transferFinished(
682 _("Error during download."),
683 MessageBox.TYPE_ERROR,
686 if self.queue is not None:
689 def putProgress(self, chunk):
690 self.currentLength += len(chunk)
691 self.gotProgress(self.currentLength, self.fileSize)
694 def gotProgress(self, pos, max):
695 self["progress"].writeValues(pos, max)
698 # Check if we're called the first time (got total)
699 lastTime = self.lastTime
701 self.lastTime = newTime
703 # 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)
704 elif int(newTime - lastTime) >= 2:
705 lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
707 secLen = int(round(((max-pos) / 1024) / lastApprox))
708 self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
709 self["speed"].setText(_("%d kb/s") % (lastApprox))
711 self.lastApprox = lastApprox
712 self.lastLength = pos
713 self.lastTime = newTime
715 def dataReceived(self, data):
719 self.currentLength += len(data)
720 self.gotProgress(self.currentLength, self.fileSize)
723 self.file.write(data)
729 def cancelQuestion(self, res = None):
739 if self.file is not None:
740 self.session.openWithCallback(
743 title = _("A transfer is currently in progress.\nWhat do you want to do?"),
745 (_("Run in Background"), 2),
746 (_("Abort transfer"), 1),
756 self[self.currlist].up()
759 self[self.currlist].down()
762 self[self.currlist].pageUp()
765 self[self.currlist].pageDown()
767 def disconnect(self):
769 # XXX: according to the docs we should wait for the servers answer to our quit request, we just hope everything goes well here
770 self.ftpclient.quit()
771 self.ftpclient = None
772 self["remote"].ftpclient = None
773 self["remoteText"].setText(_("Remote (not connected)"))
775 def connectWrapper(self, ret):
779 def connect(self, server):
787 username = server.getUsername()
789 username = 'anonymous'
790 password = 'my@email.com'
792 password = server.getPassword()
794 host = server.getAddress()
795 passive = server.getPassive()
796 port = server.getPort()
797 timeout = 30 # TODO: make configurable
799 # 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
801 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
802 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
804 def controlConnectionMade(self, ftpclient):
805 print "[FTPBrowser] connection established"
806 self.ftpclient = ftpclient
807 self["remote"].ftpclient = ftpclient
808 self["remoteText"].setText(_("Remote"))
810 self["remote"].changeDir(self.server.getPath())
812 def connectionFailed(self, *args):
813 print "[FTPBrowser] connection failed", args
816 self["remoteText"].setText(_("Remote (not connected)"))
819 _("Could not connect to ftp server!"),
820 type = MessageBox.TYPE_ERROR,