1 from __future__ import print_function
3 # for localized messages
7 from enigma import RT_HALIGN_LEFT, eListboxPythonMultiContent
10 from Tools.Directories import SCOPE_SKIN_IMAGE, resolveFilename
11 from Tools.LoadPixmap import LoadPixmap
12 from Tools.Notifications import AddPopup, AddNotificationWithCallback
15 from Screens.Screen import Screen
16 from Screens.HelpMenu import HelpableScreen
17 from Screens.MessageBox import MessageBox
18 from Screens.ChoiceBox import ChoiceBox
19 from Screens.InfoBarGenerics import InfoBarNotifications
20 from FTPServerManager import FTPServerManager
21 from FTPQueueManager import FTPQueueManager
22 from Plugins.SystemPlugins.Toolkit.NTIVirtualKeyBoard import NTIVirtualKeyBoard
25 from Components.ActionMap import ActionMap, HelpableActionMap
26 from Components.FileList import FileList, FileEntryComponent, EXTENSIONS
27 from Components.Sources.StaticText import StaticText
28 from VariableProgressSource import VariableProgressSource
31 from twisted.internet import reactor, defer
32 from twisted.internet.protocol import Protocol, ClientCreator
33 from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
34 from twisted.protocols.basic import FileSender
37 from os import path as os_path, unlink as os_unlink, rename as os_rename, \
42 def FTPFileEntryComponent(file, directory):
43 isDir = True if file['filetype'] == 'd' else False
44 name = file['filename']
45 absolute = directory + name
50 (absolute, isDir, file['size']),
51 (eListboxPythonMultiContent.TYPE_TEXT, 35, 1, 470, 20, 0, RT_HALIGN_LEFT, name)
54 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/directory.png"))
56 extension = name.split('.')
57 extension = extension[-1].lower()
58 if extension in EXTENSIONS:
59 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/" + EXTENSIONS[extension] + ".png"))
63 res.append((eListboxPythonMultiContent.TYPE_PIXMAP_ALPHATEST, 10, 2, 20, 20, png))
67 class ModifiedFTPFileListProtocol(FTPFileListProtocol):
68 fileLinePattern = re.compile(
69 r'^(?P<filetype>.)(?P<perms>.{9})\s+(?P<nlinks>\d*)\s*'
70 r'(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)\s+'
71 r'(?P<date>...\s+\d+\s+[\d:]+)\s+(?P<filename>.*?)'
72 r'( -> (?P<linktarget>[^\r]*))?\r?$'
75 class FTPFileList(FileList):
80 FileList.__init__(self, "/")
82 def changeDir(self, directory, select = None):
86 if self.ftpclient is None:
88 self.l.setList(self.list)
91 self.current_directory = directory
94 self.filelist = ModifiedFTPFileListProtocol()
95 d = self.ftpclient.list(directory, self.filelist)
96 d.addCallback(self.listRcvd).addErrback(self.listFailed)
98 def listRcvd(self, *args):
99 # TODO: is any of the 'advanced' features useful (and more of all can they be implemented) here?
100 list = [FTPFileEntryComponent(file, self.current_directory) for file in self.filelist.files]
101 list.sort(key = lambda x: (not x[0][1], x[0][0]))
102 if self.current_directory != "/":
103 list.insert(0, FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True))
110 if select is not None:
121 def listFailed(self, *args):
122 # 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)
123 if self.current_directory != "/":
125 FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True),
126 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
130 FileEntryComponent(name = "<" + _("Error") + ">", absolute = None, isDir = False),
134 self.l.setList(self.list)
136 class FTPBrowser(Screen, Protocol, InfoBarNotifications, HelpableScreen):
138 <screen name="FTPBrowser" position="center,center" size="600,440" title="FTP Browser">
139 <ePixmap position="0,0" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on" />
140 <ePixmap position="140,0" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
141 <ePixmap position="280,0" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
142 <ePixmap position="420,0" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
143 <ePixmap position="565,10" size="35,25" pixmap="skin_default/buttons/key_menu.png" alphatest="on" />
144 <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" />
145 <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" />
146 <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" />
147 <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" />
148 <widget source="localText" render="Label" position="10,50" size="200,20" font="Regular;18" />
149 <widget name="local" position="10,80" size="290,320" scrollbarMode="showOnDemand" />
150 <widget source="remoteText" render="Label" position="300,50" size="200,20" font="Regular;18" />
151 <widget name="remote" position="300,80" size="290,320" scrollbarMode="showOnDemand" />
152 <widget source="eta" render="Label" position="20,410" size="200,30" font="Regular;23" />
153 <widget source="speed" render="Label" position="380,410" size="200,30" halign="right" font="Regular;23" />
154 <widget source="progress" render="Progress" position="20,440" size="560,10" />
157 def __init__(self, session):
158 Screen.__init__(self, session)
159 HelpableScreen.__init__(self)
160 InfoBarNotifications.__init__(self)
161 self.ftpclient = None
162 self.queueManagerInstance = None
165 self.currlist = "local"
167 # # NOTE: having self.checkNotifications in onExecBegin might make our gui
168 # disappear, so let's move it to onShow
169 self.onExecBegin.remove(self.checkNotifications)
170 self.onShow.append(self.checkNotifications)
172 # Init what we need for dl progress
173 self.currentLength = 0
179 self["localText"] = StaticText(_("Local"))
180 self["local"] = FileList("/media/hdd/", showMountpoints = False)
181 self["remoteText"] = StaticText(_("Remote (not connected)"))
182 self["remote"] = FTPFileList()
183 self["eta"] = StaticText("")
184 self["speed"] = StaticText("")
185 self["progress"] = VariableProgressSource()
186 self["key_red"] = StaticText(_("Exit"))
187 self["key_green"] = StaticText(_("Rename"))
188 self["key_yellow"] = StaticText(_("Delete"))
189 self["key_blue"] = StaticText(_("Upload"))
193 self["ftpbrowserBaseActions"] = HelpableActionMap(self, "ftpbrowserBaseActions",
195 "ok": (self.ok, _("enter directory/get file/put file")),
196 "cancel": (self.cancel , _("close")),
197 "menu": (self.menu, _("open menu")),
200 self["ftpbrowserListActions"] = HelpableActionMap(self, "ftpbrowserListActions",
202 "channelUp": (self.setLocal, _("Select local file list")),
203 "channelDown": (self.setRemote, _("Select remote file list")),
206 self["actions"] = ActionMap(["ftpbrowserDirectionActions", "ColorActions"],
212 "green": self.rename,
213 "yellow": self.delete,
214 "blue": self.transfer,
217 self.onExecBegin.append(self.reinitialize)
219 def reinitialize(self):
220 # NOTE: this will clear the remote file list if we are not currently connected. this behavior is intended.
221 # XXX: but do we also want to do this when we just returned from a notification?
223 self["remote"].refresh()
224 except AttributeError as ae:
225 # NOTE: we assume the connection was timed out by the server
226 self.ftpclient = None
227 self["remote"].ftpclient = None
228 self["remote"].refresh()
230 self["local"].refresh()
232 if not self.ftpclient:
233 self.connect(self.server)
234 # XXX: Actually everything else should be taken care of... recheck this!
236 def serverManagerCallback(self, uri):
240 def serverManager(self):
241 self.session.openWithCallback(
242 self.serverManagerCallback,
246 def queueManagerCallback(self):
247 self.queueManagerInstance = None
249 def queueManager(self):
250 self.queueManagerInstance = self.session.openWithCallback(
251 self.queueManagerCallback,
256 def menuCallback(self, ret):
260 self.session.openWithCallback(
264 (_("Server Manager"), self.serverManager),
265 (_("Queue Manager"), self.queueManager),
270 self.currlist = "local"
271 self["key_blue"].text = _("Upload")
274 self.currlist = "remote"
275 self["key_blue"].text = _("Download")
277 def okQuestion(self, res = None):
279 self.ok(force = True)
281 def getRemoteFile(self):
282 remoteFile = self["remote"].getSelection()
283 if not remoteFile or not remoteFile[0]:
284 return None, None, None
286 absRemoteFile = remoteFile[0]
288 fileName = absRemoteFile.split('/')[-2]
290 fileName = absRemoteFile.split('/')[-1]
292 if len(remoteFile) == 3:
293 fileSize = remoteFile[2]
297 return absRemoteFile, fileName, fileSize
299 def getLocalFile(self):
300 # XXX: isn't this supposed to be an absolute filename? well, it's not for me :-/
301 localFile = self["local"].getSelection()
306 absLocalFile = localFile[0]
307 fileName = absLocalFile.split('/')[-2]
309 fileName = localFile[0]
310 absLocalFile = self["local"].getCurrentDirectory() + fileName
312 return absLocalFile, fileName
314 def renameCallback(self, newName = None):
318 if self.currlist == "remote":
319 absRemoteFile, fileName, fileSize = self.getRemoteFile()
323 directory = self["remote"].getCurrentDirectory()
324 sep = '/' if directory != '/' else ''
325 newRemoteFile = directory + sep + newName
327 def callback(ret = None):
328 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
329 def errback(ret = None):
330 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
332 self.ftpclient.rename(absRemoteFile, newRemoteFile).addCallback(callback).addErrback(errback)
334 assert(self.currlist == "local")
335 absLocalFile, fileName = self.getLocalFile()
339 directory = self["local"].getCurrentDirectory()
340 newLocalFile = os_path.join(directory, newName)
343 os_rename(absLocalFile, newLocalFile)
344 except OSError as ose:
345 AddPopup(_("Could not rename %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
347 AddPopup(_("Renamed %s to %s.") % (fileName, newName), MessageBox.TYPE_INFO, -1)
353 if self.currlist == "remote":
354 if not self.ftpclient:
357 absRemoteFile, fileName, fileSize = self.getRemoteFile()
361 assert(self.currlist == "local")
362 absLocalFile, fileName = self.getLocalFile()
366 self.session.openWithCallback(
369 title = _("Enter new filename:"),
373 def deleteConfirmed(self, ret):
377 if self.currlist == "remote":
378 absRemoteFile, fileName, fileSize = self.getRemoteFile()
382 def callback(ret = None):
383 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
384 def errback(ret = None):
385 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
387 self.ftpclient.removeFile(absRemoteFile).addCallback(callback).addErrback(errback)
389 assert(self.currlist == "local")
390 absLocalFile, fileName = self.getLocalFile()
395 os_unlink(absLocalFile)
396 except OSError as oe:
397 AddPopup(_("Could not delete %s.") % (fileName), MessageBox.TYPE_ERROR, -1)
399 AddPopup(_("Removed %s.") % (fileName), MessageBox.TYPE_INFO, -1)
405 if self.currlist == "remote":
406 if not self.ftpclient:
409 if self["remote"].canDescent():
412 _("Removing directories is not supported."),
413 MessageBox.TYPE_WARNING
417 absRemoteFile, fileName, fileSize = self.getRemoteFile()
421 assert(self.currlist == "local")
422 if self["local"].canDescent():
425 _("Removing directories is not supported."),
426 MessageBox.TYPE_WARNING
430 absLocalFile, fileName = self.getLocalFile()
434 self.session.openWithCallback(
435 self.deleteConfirmed,
437 _("Are you sure you want to delete %s?") % (fileName)
440 def transferListRcvd(self, res, filelist):
441 remoteDirectory, _, _ = self.getRemoteFile()
442 localDirectory = self["local"].getCurrentDirectory()
444 self.queue = [(True, remoteDirectory + file["filename"], localDirectory + file["filename"], file["size"]) for file in filelist.files if file["filetype"] == "-"]
449 # NOTE: put this transfer back if there already is an active one,
450 # it will be picked up again when the active transfer is done
457 self.getFile(*top[1:])
459 self.putFile(*top[1:])
460 elif self.queue is not None:
462 self["eta"].text = ""
463 self["speed"].text = ""
464 self["progress"].invalidate()
465 AddPopup(_("Queue processed."), MessageBox.TYPE_INFO, -1)
467 if self.queueManagerInstance:
468 self.queueManagerInstance.updateList(self.queue)
470 def transferListFailed(self, res = None):
472 AddPopup(_("Could not obtain list of files."), MessageBox.TYPE_ERROR, -1)
475 if not self.ftpclient or self.queue:
478 if self.currlist == "remote":
479 # single file transfer is implemented in self.ok
480 if not self["remote"].canDescent():
483 absRemoteFile, fileName, fileSize = self.getRemoteFile()
487 filelist = ModifiedFTPFileListProtocol()
488 d = self.ftpclient.list(absRemoteFile, filelist)
489 d.addCallback(self.transferListRcvd, filelist).addErrback(self.transferListFailed)
491 assert(self.currlist == "local")
492 # single file transfer is implemented in self.ok
493 if not self["local"].canDescent():
496 localDirectory, _ = self.getLocalFile()
497 remoteDirectory = self["remote"].getCurrentDirectory()
499 def remoteFileExists(absName):
500 for file in self["remote"].getFileList():
501 if file[0][0] == absName:
505 self.queue = [(False, remoteDirectory + file, localDirectory + file, remoteFileExists(remoteDirectory + file)) for file in os_listdir(localDirectory) if os_path.isfile(localDirectory + file)]
509 def getFileCallback(self, ret, absRemoteFile, absLocalFile, fileSize):
513 self.getFile(absRemoteFile, absLocalFile, fileSize, force=True)
515 def getFile(self, absRemoteFile, absLocalFile, fileSize, force=False):
516 if not force and os_path.exists(absLocalFile):
517 fileName = absRemoteFile.split('/')[-1]
518 AddNotificationWithCallback(
519 lambda ret: self.getFileCallback(ret, absRemoteFile, absLocalFile, fileSize),
521 _("A file with this name (%s) already exists locally.\nDo you want to overwrite it?") % (fileName),
524 self.currentLength = 0
528 self.fileSize = fileSize
531 self.file = open(absLocalFile, 'w')
532 except IOError as ie:
536 d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
537 d.addCallback(self.getFinished).addErrback(self.getFailed)
539 def putFileCallback(self, ret, absRemoteFile, absLocalFile, remoteFileExists):
543 self.putFile(absRemoteFile, absLocalFile, remoteFileExists, force=True)
545 def putFile(self, absRemoteFile, absLocalFile, remoteFileExists, force=False):
546 if not force and remoteFileExists:
547 fileName = absRemoteFile.split('/')[-1]
548 AddNotificationWithCallback(
549 lambda ret: self.putFileCallback(ret, absRemoteFile, absLocalFile, remoteFileExists),
551 _("A file with this name (%s) already exists on the remote host.\nDo you want to overwrite it?") % (fileName),
554 self.currentLength = 0
559 def sendfile(consumer, fileObj):
560 FileSender().beginFileTransfer(fileObj, consumer, transform = self.putProgress).addCallback(
561 lambda _: consumer.finish()).addCallback(
562 self.putComplete).addErrback(self.putFailed)
565 self.fileSize = int(os_path.getsize(absLocalFile))
566 self.file = open(absLocalFile, 'rb')
567 except (IOError, OSError) as e:
571 dC, dL = self.ftpclient.storeFile(absRemoteFile)
572 dC.addCallback(sendfile, self.file)
574 def ok(self, force = False):
578 if self.currlist == "remote":
579 if not self.ftpclient:
582 # Get file/change directory
583 if self["remote"].canDescent():
584 self["remote"].descent()
589 _("There already is an active transfer."),
590 type = MessageBox.TYPE_WARNING
594 absRemoteFile, fileName, fileSize = self.getRemoteFile()
598 absLocalFile = self["local"].getCurrentDirectory() + fileName
600 self.getFile(absRemoteFile, absLocalFile, fileSize)
602 # Put file/change directory
603 assert(self.currlist == "local")
604 if self["local"].canDescent():
605 self["local"].descent()
607 if not self.ftpclient:
613 _("There already is an active transfer."),
614 type = MessageBox.TYPE_WARNING
618 if not self["remote"].isValid:
621 absLocalFile, fileName = self.getLocalFile()
625 def remoteFileExists(absName):
626 for file in self["remote"].getFileList():
627 if file[0][0] == absName:
631 absRemoteFile = self["remote"].getCurrentDirectory() + fileName
632 self.putFile(absRemoteFile, absLocalFile, remoteFileExists(absRemoteFile))
634 def transferFinished(self, msg, type, toRefresh):
635 AddPopup(msg, type, -1)
637 self["eta"].text = ""
638 self["speed"].text = ""
639 self["progress"].invalidate()
640 self[toRefresh].refresh()
644 def putComplete(self, *args):
645 if self.queue is not None:
651 self.transferFinished(
652 _("Upload finished."),
653 MessageBox.TYPE_INFO,
657 def putFailed(self, *args):
658 # NOTE: we continue uploading but notify the user of every error though
659 # we only display one success notification
660 self.transferFinished(
661 _("Error during download."),
662 MessageBox.TYPE_ERROR,
665 if self.queue is not None:
668 def getFinished(self, *args):
669 if self.queue is not None:
675 self.transferFinished(
676 _("Download finished."),
677 MessageBox.TYPE_INFO,
681 def getFailed(self, *args):
682 # NOTE: we continue downloading but notify the user of every error though
683 # we only display one success notification
684 self.transferFinished(
685 _("Error during download."),
686 MessageBox.TYPE_ERROR,
689 if self.queue is not None:
692 def putProgress(self, chunk):
693 self.currentLength += len(chunk)
694 self.gotProgress(self.currentLength, self.fileSize)
697 def gotProgress(self, pos, max):
698 self["progress"].writeValues(pos, max)
701 # Check if we're called the first time (got total)
702 lastTime = self.lastTime
704 self.lastTime = newTime
706 # 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)
707 elif int(newTime - lastTime) >= 2:
708 lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
710 secLen = int(round(((max-pos) / 1024) / lastApprox))
711 self["eta"].text = _("ETA %d:%02d min") % (secLen / 60, secLen % 60)
712 self["speed"].text = _("%d kb/s") % (lastApprox)
714 self.lastApprox = lastApprox
715 self.lastLength = pos
716 self.lastTime = newTime
718 def dataReceived(self, data):
722 self.currentLength += len(data)
723 self.gotProgress(self.currentLength, self.fileSize)
726 self.file.write(data)
727 except IOError as ie:
732 def cancelQuestion(self, res = None):
742 if self.file is not None:
743 self.session.openWithCallback(
746 title = _("A transfer is currently in progress.\nWhat do you want to do?"),
748 (_("Run in Background"), 2),
749 (_("Abort transfer"), 1),
759 self[self.currlist].up()
762 self[self.currlist].down()
765 self[self.currlist].pageUp()
768 self[self.currlist].pageDown()
770 def disconnect(self):
772 # XXX: according to the docs we should wait for the servers answer to our quit request, we just hope everything goes well here
773 self.ftpclient.quit()
774 self.ftpclient = None
775 self["remote"].ftpclient = None
776 self["remoteText"].text = _("Remote (not connected)")
778 def connectWrapper(self, ret):
782 def connect(self, server):
790 username = server.getUsername()
792 username = 'anonymous'
793 password = 'my@email.com'
795 password = server.getPassword()
797 host = server.getAddress()
798 passive = server.getPassive()
799 port = server.getPort()
800 timeout = 30 # TODO: make configurable
802 # 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
804 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
805 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
807 def controlConnectionMade(self, ftpclient):
808 print("[FTPBrowser] connection established")
809 self.ftpclient = ftpclient
810 self["remote"].ftpclient = ftpclient
811 self["remoteText"].text = _("Remote")
813 self["remote"].changeDir(self.server.getPath())
815 def connectionFailed(self, *args):
816 print("[FTPBrowser] connection failed", args)
819 self["remoteText"].text = _("Remote (not connected)")
822 _("Could not connect to ftp server!"),
823 type = MessageBox.TYPE_ERROR,