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.FileList import FileList, FileEntryComponent, EXTENSIONS
25 from Components.Sources.StaticText import StaticText
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, \
40 def FTPFileEntryComponent(file, directory):
41 isDir = True if file['filetype'] == 'd' else False
42 name = file['filename']
43 absolute = directory + name
48 (absolute, isDir, file['size']),
49 (eListboxPythonMultiContent.TYPE_TEXT, 35, 1, 470, 20, 0, RT_HALIGN_LEFT, name)
52 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/directory.png"))
54 extension = name.split('.')
55 extension = extension[-1].lower()
56 if EXTENSIONS.has_key(extension):
57 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/" + EXTENSIONS[extension] + ".png"))
61 res.append((eListboxPythonMultiContent.TYPE_PIXMAP_ALPHATEST, 10, 2, 20, 20, png))
65 class ModifiedFTPFileListProtocol(FTPFileListProtocol):
66 fileLinePattern = re.compile(
67 r'^(?P<filetype>.)(?P<perms>.{9})\s+(?P<nlinks>\d*)\s*'
68 r'(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)\s+'
69 r'(?P<date>...\s+\d+\s+[\d:]+)\s+(?P<filename>.*?)'
70 r'( -> (?P<linktarget>[^\r]*))?\r?$'
73 class FTPFileList(FileList):
78 FileList.__init__(self, "/")
80 def changeDir(self, directory, select = None):
84 if self.ftpclient is None:
86 self.l.setList(self.list)
89 self.current_directory = directory
92 self.filelist = ModifiedFTPFileListProtocol()
93 d = self.ftpclient.list(directory, self.filelist)
94 d.addCallback(self.listRcvd).addErrback(self.listFailed)
96 def listRcvd(self, *args):
97 # TODO: is any of the 'advanced' features useful (and more of all can they be implemented) here?
98 list = [FTPFileEntryComponent(file, self.current_directory) for file in self.filelist.files]
99 list.sort(key = lambda x: (not x[0][1], x[0][0]))
100 if self.current_directory != "/":
101 list.insert(0, FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True))
108 if select is not None:
119 def listFailed(self, *args):
120 # 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)
121 if self.current_directory != "/":
123 FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True),
124 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
128 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
132 self.l.setList(self.list)
134 class FTPBrowser(Screen, Protocol, InfoBarNotifications, HelpableScreen):
136 <screen name="FTPBrowser" position="center,center" size="600,440" title="FTP Browser">
137 <ePixmap position="0,0" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on" />
138 <ePixmap position="140,0" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
139 <ePixmap position="280,0" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
140 <ePixmap position="420,0" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
141 <ePixmap position="565,10" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on" />
142 <widget source="key_red" render="Label" position="0,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
143 <widget source="key_green" render="Label" position="140,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
144 <widget source="key_yellow" render="Label" position="280,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
145 <widget source="key_blue" render="Label" position="420,0" zPosition="1" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
146 <widget source="localText" render="Label" position="10,50" size="200,20" font="Regular;18" />
147 <widget name="local" position="10,80" size="290,320" scrollbarMode="showOnDemand" />
148 <widget source="remoteText" render="Label" position="300,50" size="200,20" font="Regular;18" />
149 <widget name="remote" position="300,80" size="290,320" scrollbarMode="showOnDemand" />
150 <widget source="eta" render="Label" position="20,410" size="200,30" font="Regular;23" />
151 <widget source="speed" render="Label" position="380,410" size="200,30" halign="right" font="Regular;23" />
152 <widget source="progress" render="Progress" position="20,440" size="560,10" />
155 def __init__(self, session):
156 Screen.__init__(self, session)
157 HelpableScreen.__init__(self)
158 InfoBarNotifications.__init__(self)
159 self.ftpclient = None
160 self.queueManagerInstance = None
163 self.currlist = "local"
165 # # NOTE: having self.checkNotifications in onExecBegin might make our gui
166 # disappear, so let's move it to onShow
167 self.onExecBegin.remove(self.checkNotifications)
168 self.onShow.append(self.checkNotifications)
170 # Init what we need for dl progress
171 self.currentLength = 0
177 self["localText"] = StaticText(_("Local"))
178 self["local"] = FileList("/media/hdd/", showMountpoints = False)
179 self["remoteText"] = StaticText(_("Remote (not connected)"))
180 self["remote"] = FTPFileList()
181 self["eta"] = StaticText("")
182 self["speed"] = StaticText("")
183 self["progress"] = VariableProgressSource()
184 self["key_red"] = StaticText(_("Exit"))
185 self["key_green"] = StaticText(_("Rename"))
186 self["key_yellow"] = StaticText(_("Delete"))
187 self["key_blue"] = StaticText(_("Upload"))
191 self["ftpbrowserBaseActions"] = HelpableActionMap(self, "ftpbrowserBaseActions",
193 "ok": (self.ok, _("enter directory/get file/put file")),
194 "cancel": (self.cancel , _("close")),
195 "menu": (self.menu, _("open menu")),
198 self["ftpbrowserListActions"] = HelpableActionMap(self, "ftpbrowserListActions",
200 "channelUp": (self.setLocal, _("Select local file list")),
201 "channelDown": (self.setRemote, _("Select remote file list")),
204 self["actions"] = ActionMap(["ftpbrowserDirectionActions", "ColorActions"],
210 "green": self.rename,
211 "yellow": self.delete,
212 "blue": self.transfer,
215 self.onExecBegin.append(self.reinitialize)
217 def reinitialize(self):
218 # NOTE: this will clear the remote file list if we are not currently connected. this behavior is intended.
219 # XXX: but do we also want to do this when we just returned from a notification?
221 self["remote"].refresh()
222 except AttributeError, ae:
223 # NOTE: we assume the connection was timed out by the server
224 self.ftpclient = None
225 self["remote"].ftpclient = None
226 self["remote"].refresh()
228 self["local"].refresh()
230 if not self.ftpclient:
231 self.connect(self.server)
232 # XXX: Actually everything else should be taken care of... recheck this!
234 def serverManagerCallback(self, uri):
238 def serverManager(self):
239 self.session.openWithCallback(
240 self.serverManagerCallback,
244 def queueManagerCallback(self):
245 self.queueManagerInstance = None
247 def queueManager(self):
248 self.queueManagerInstance = self.session.openWithCallback(
249 self.queueManagerCallback,
254 def menuCallback(self, ret):
258 self.session.openWithCallback(
262 (_("Server Manager"), self.serverManager),
263 (_("Queue Manager"), self.queueManager),
268 self.currlist = "local"
269 self["key_blue"].text = _("Upload")
272 self.currlist = "remote"
273 self["key_blue"].text = _("Download")
275 def okQuestion(self, res = None):
277 self.ok(force = True)
279 def getRemoteFile(self):
280 remoteFile = self["remote"].getSelection()
281 if not remoteFile or not remoteFile[0]:
282 return None, None, None
284 absRemoteFile = remoteFile[0]
286 fileName = absRemoteFile.split('/')[-2]
288 fileName = absRemoteFile.split('/')[-1]
290 if len(remoteFile) == 3:
291 fileSize = remoteFile[2]
295 return absRemoteFile, fileName, fileSize
297 def getLocalFile(self):
298 # XXX: isn't this supposed to be an absolute filename? well, it's not for me :-/
299 localFile = self["local"].getSelection()
304 absLocalFile = localFile[0]
305 fileName = absLocalFile.split('/')[-2]
307 fileName = localFile[0]
308 absLocalFile = self["local"].getCurrentDirectory() + fileName
310 return absLocalFile, fileName
312 def renameCallback(self, newName = None):
316 if self.currlist == "remote":
317 absRemoteFile, fileName, fileSize = self.getRemoteFile()
321 directory = self["remote"].getCurrentDirectory()
322 sep = '/' if directory != '/' else ''
323 newRemoteFile = directory + sep + newName
325 def callback(ret = None):
326 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
327 def errback(ret = None):
328 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
330 self.ftpclient.rename(absRemoteFile, newRemoteFile).addCallback(callback).addErrback(errback)
332 assert(self.currlist == "local")
333 absLocalFile, fileName = self.getLocalFile()
337 directory = self["local"].getCurrentDirectory()
338 newLocalFile = os_path.join(directory, newName)
341 os_rename(absLocalFile, newLocalFile)
343 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
345 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
351 if self.currlist == "remote":
352 if not self.ftpclient:
355 absRemoteFile, fileName, fileSize = self.getRemoteFile()
359 assert(self.currlist == "local")
360 absLocalFile, fileName = self.getLocalFile()
364 self.session.openWithCallback(
367 title = _("Enter new filename:"),
371 def deleteConfirmed(self, ret):
375 if self.currlist == "remote":
376 absRemoteFile, fileName, fileSize = self.getRemoteFile()
380 def callback(ret = None):
381 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
382 def errback(ret = None):
383 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
385 self.ftpclient.removeFile(absRemoteFile).addCallback(callback).addErrback(errback)
387 assert(self.currlist == "local")
388 absLocalFile, fileName = self.getLocalFile()
393 os_unlink(absLocalFile)
395 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
397 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
403 if self.currlist == "remote":
404 if not self.ftpclient:
407 if self["remote"].canDescent():
410 _("Removing directories is not supported."),
411 MessageBox.TYPE_WARNING
415 absRemoteFile, fileName, fileSize = self.getRemoteFile()
419 assert(self.currlist == "local")
420 if self["local"].canDescent():
423 _("Removing directories is not supported."),
424 MessageBox.TYPE_WARNING
428 absLocalFile, fileName = self.getLocalFile()
432 self.session.openWithCallback(
433 self.deleteConfirmed,
435 _("Are you sure you want to delete %s?") % (fileName)
438 def transferListRcvd(self, res, filelist):
439 remoteDirectory, _, _ = self.getRemoteFile()
440 localDirectory = self["local"].getCurrentDirectory()
442 self.queue = [(True, remoteDirectory + file["filename"], localDirectory + file["filename"], file["size"]) for file in filelist.files if file["filetype"] == "-"]
447 # NOTE: put this transfer back if there already is an active one,
448 # it will be picked up again when the active transfer is done
455 self.getFile(*top[1:])
457 self.putFile(*top[1:])
458 elif self.queue is not None:
460 self["eta"].text = ""
461 self["speed"].text = ""
462 self["progress"].invalidate()
463 AddPopup(_("Queue processed."), MessageBox.TYPE_INFO, -1)
465 if self.queueManagerInstance:
466 self.queueManagerInstance.updateList(self.queue)
468 def transferListFailed(self, res = None):
470 AddPopup(_("Could not obtain list of files."), MessageBox.TYPE_ERROR, -1)
473 if not self.ftpclient or self.queue:
476 if self.currlist == "remote":
477 # single file transfer is implemented in self.ok
478 if not self["remote"].canDescent():
481 absRemoteFile, fileName, fileSize = self.getRemoteFile()
485 filelist = ModifiedFTPFileListProtocol()
486 d = self.ftpclient.list(absRemoteFile, filelist)
487 d.addCallback(self.transferListRcvd, filelist).addErrback(self.transferListFailed)
489 assert(self.currlist == "local")
490 # single file transfer is implemented in self.ok
491 if not self["local"].canDescent():
494 localDirectory, _ = self.getLocalFile()
495 remoteDirectory = self["remote"].getCurrentDirectory()
497 def remoteFileExists(absName):
498 for file in self["remote"].getFileList():
499 if file[0][0] == absName:
503 self.queue = [(False, remoteDirectory + file, localDirectory + file, remoteFileExists(remoteDirectory + file)) for file in os_listdir(localDirectory) if os_path.isfile(localDirectory + file)]
507 def getFileCallback(self, ret, absRemoteFile, absLocalFile, fileSize):
511 self.getFile(absRemoteFile, absLocalFile, fileSize, force=True)
513 def getFile(self, absRemoteFile, absLocalFile, fileSize, force=False):
514 if not force and os_path.exists(absLocalFile):
515 fileName = absRemoteFile.split('/')[-1]
516 AddNotificationWithCallback(
517 lambda ret: self.getFileCallback(ret, absRemoteFile, absLocalFile, fileSize),
519 _("A file with this name (%s) already exists locally.\nDo you want to overwrite it?") % (fileName),
522 self.currentLength = 0
526 self.fileSize = fileSize
529 self.file = open(absLocalFile, 'w')
534 d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
535 d.addCallback(self.getFinished).addErrback(self.getFailed)
537 def putFileCallback(self, ret, absRemoteFile, absLocalFile, remoteFileExists):
541 self.putFile(absRemoteFile, absLocalFile, remoteFileExists, force=True)
543 def putFile(self, absRemoteFile, absLocalFile, remoteFileExists, force=False):
544 if not force and remoteFileExists:
545 fileName = absRemoteFile.split('/')[-1]
546 AddNotificationWithCallback(
547 lambda ret: self.putFileCallback(ret, absRemoteFile, absLocalFile, remoteFileExists),
549 _("A file with this name (%s) already exists on the remote host.\nDo you want to overwrite it?") % (fileName),
552 self.currentLength = 0
557 def sendfile(consumer, fileObj):
558 FileSender().beginFileTransfer(fileObj, consumer, transform = self.putProgress).addCallback(
559 lambda _: consumer.finish()).addCallback(
560 self.putComplete).addErrback(self.putFailed)
563 self.fileSize = int(os_path.getsize(absLocalFile))
564 self.file = open(absLocalFile, 'rb')
565 except (IOError, OSError), e:
569 dC, dL = self.ftpclient.storeFile(absRemoteFile)
570 dC.addCallback(sendfile, self.file)
572 def ok(self, force = False):
576 if self.currlist == "remote":
577 if not self.ftpclient:
580 # Get file/change directory
581 if self["remote"].canDescent():
582 self["remote"].descent()
587 _("There already is an active transfer."),
588 type = MessageBox.TYPE_WARNING
592 absRemoteFile, fileName, fileSize = self.getRemoteFile()
596 absLocalFile = self["local"].getCurrentDirectory() + fileName
598 self.getFile(absRemoteFile, absLocalFile, fileSize)
600 # Put file/change directory
601 assert(self.currlist == "local")
602 if self["local"].canDescent():
603 self["local"].descent()
605 if not self.ftpclient:
611 _("There already is an active transfer."),
612 type = MessageBox.TYPE_WARNING
616 if not self["remote"].isValid:
619 absLocalFile, fileName = self.getLocalFile()
623 def remoteFileExists(absName):
624 for file in self["remote"].getFileList():
625 if file[0][0] == absName:
629 absRemoteFile = self["remote"].getCurrentDirectory() + fileName
630 self.putFile(absRemoteFile, absLocalFile, remoteFileExists(absRemoteFile))
632 def transferFinished(self, msg, type, toRefresh):
633 AddPopup(msg, type, -1)
635 self["eta"].text = ""
636 self["speed"].text = ""
637 self["progress"].invalidate()
638 self[toRefresh].refresh()
642 def putComplete(self, *args):
643 if self.queue is not None:
649 self.transferFinished(
650 _("Upload finished."),
651 MessageBox.TYPE_INFO,
655 def putFailed(self, *args):
656 # NOTE: we continue uploading but notify the user of every error though
657 # we only display one success notification
658 self.transferFinished(
659 _("Error during download."),
660 MessageBox.TYPE_ERROR,
663 if self.queue is not None:
666 def getFinished(self, *args):
667 if self.queue is not None:
673 self.transferFinished(
674 _("Download finished."),
675 MessageBox.TYPE_INFO,
679 def getFailed(self, *args):
680 # NOTE: we continue downloading but notify the user of every error though
681 # we only display one success notification
682 self.transferFinished(
683 _("Error during download."),
684 MessageBox.TYPE_ERROR,
687 if self.queue is not None:
690 def putProgress(self, chunk):
691 self.currentLength += len(chunk)
692 self.gotProgress(self.currentLength, self.fileSize)
695 def gotProgress(self, pos, max):
696 self["progress"].writeValues(pos, max)
699 # Check if we're called the first time (got total)
700 lastTime = self.lastTime
702 self.lastTime = newTime
704 # 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)
705 elif int(newTime - lastTime) >= 2:
706 lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
708 secLen = int(round(((max-pos) / 1024) / lastApprox))
709 self["eta"].text = _("ETA %d:%02d min") % (secLen / 60, secLen % 60)
710 self["speed"].text = _("%d kb/s") % (lastApprox)
712 self.lastApprox = lastApprox
713 self.lastLength = pos
714 self.lastTime = newTime
716 def dataReceived(self, data):
720 self.currentLength += len(data)
721 self.gotProgress(self.currentLength, self.fileSize)
724 self.file.write(data)
730 def cancelQuestion(self, res = None):
740 if self.file is not None:
741 self.session.openWithCallback(
744 title = _("A transfer is currently in progress.\nWhat do you want to do?"),
746 (_("Run in Background"), 2),
747 (_("Abort transfer"), 1),
757 self[self.currlist].up()
760 self[self.currlist].down()
763 self[self.currlist].pageUp()
766 self[self.currlist].pageDown()
768 def disconnect(self):
770 # XXX: according to the docs we should wait for the servers answer to our quit request, we just hope everything goes well here
771 self.ftpclient.quit()
772 self.ftpclient = None
773 self["remote"].ftpclient = None
774 self["remoteText"].text = _("Remote (not connected)")
776 def connectWrapper(self, ret):
780 def connect(self, server):
788 username = server.getUsername()
790 username = 'anonymous'
791 password = 'my@email.com'
793 password = server.getPassword()
795 host = server.getAddress()
796 passive = server.getPassive()
797 port = server.getPort()
798 timeout = 30 # TODO: make configurable
800 # 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
802 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
803 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
805 def controlConnectionMade(self, ftpclient):
806 print "[FTPBrowser] connection established"
807 self.ftpclient = ftpclient
808 self["remote"].ftpclient = ftpclient
809 self["remoteText"].text = _("Remote")
811 self["remote"].changeDir(self.server.getPath())
813 def connectionFailed(self, *args):
814 print "[FTPBrowser] connection failed", args
817 self["remoteText"].text = _("Remote (not connected)")
820 _("Could not connect to ftp server!"),
821 type = MessageBox.TYPE_ERROR,