fix skin a bit
[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
11 # GUI (Screens)
12 from Screens.Screen import Screen
13 from Screens.MessageBox import MessageBox
14
15 # GUI (Components)
16 from Components.ActionMap import ActionMap, HelpableActionMap
17 from Components.Label import Label
18 from Components.FileList import FileList, FileEntryComponent, EXTENSIONS
19 from Components.Button import Button
20 from VariableProgressSource import VariableProgressSource
21
22 # FTP Client
23 from twisted.internet import reactor, defer
24 from twisted.internet.protocol import Protocol, ClientCreator
25 from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
26
27 # For new and improved _parse
28 from urlparse import urlparse, urlunparse
29
30 # System
31 from os import path as os_path
32 from time import time
33
34 def _parse(url, defaultPort = None):
35         url = url.strip()
36         parsed = urlparse(url)
37         scheme = parsed[0]
38         path = urlunparse(('','')+parsed[2:])
39
40         if defaultPort is None:
41                 if scheme == 'https':
42                         defaultPort = 443
43                 elif scheme == 'ftp':
44                         defaultPort = 21
45                 else:
46                         defaultPort = 80
47
48         host, port = parsed[1], defaultPort
49
50         if '@' in host:
51                 username, host = host.split('@')
52                 if ':' in username:
53                         username, password = username.split(':')
54                 else:
55                         password = ""
56         else:
57                 username = ""
58                 password = ""
59
60         if ':' in host:
61                 host, port = host.split(':')
62                 port = int(port)
63
64         if path == "":
65                 path = "/"
66
67         return scheme, host, port, path, username, password
68
69 def FTPFileEntryComponent(file, directory):
70         isDir = True if file['filetype'] == 'd' else False
71         name = file['filename']
72
73         sep = '/' if directory != '/' else ''
74         res = [ (directory + sep + name, isDir, file['size']) ]
75         res.append((eListboxPythonMultiContent.TYPE_TEXT, 35, 1, 470, 20, 0, RT_HALIGN_LEFT, name))
76         if isDir:
77                 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/directory.png"))
78         else:
79                 extension = name.split('.')
80                 extension = extension[-1].lower()
81                 if EXTENSIONS.has_key(extension):
82                         png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/" + EXTENSIONS[extension] + ".png"))
83                 else:
84                         png = None
85         if png is not None:
86                 res.append((eListboxPythonMultiContent.TYPE_PIXMAP_ALPHATEST, 10, 2, 20, 20, png))
87
88         return res
89
90 class FTPFileList(FileList):
91         def __init__(self):
92                 self.ftpclient = None
93                 self.select = None
94                 FileList.__init__(self, "/")
95
96         def changeDir(self, directory, select = None):
97                 if self.ftpclient is None:
98                         self.list = []
99                         self.l.setList(self.list)
100                         return
101
102                 self.current_directory = directory
103                 self.select = select
104
105                 self.filelist = FTPFileListProtocol()
106                 d = self.ftpclient.list(directory, self.filelist)
107                 d.addCallback(self.listRcvd).addErrback(self.listFailed)
108
109         def listRcvd(self, *args):
110                 # XXX: we might want to sort this list and/or implement any other feature than 'list directories'
111                 self.list = [FTPFileEntryComponent(file, self.current_directory) for file in self.filelist.files]
112                 if self.current_directory != "/":
113                         self.list.insert(0, FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-1]) + '/', isDir = True))
114                 self.l.setList(self.list)
115
116         def listFailed(self, *args):
117                 if self.current_directory != "/":
118                         self.list = [FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-1]) + '/', isDir = True)]
119                 else:
120                         self.list = []
121                 self.l.setList(self.list)
122
123 class FTPBrowser(Screen, Protocol):
124         skin = """
125                 <screen name="FTPBrowser" position="100,100" size="560,410" title="FTP Browser" >
126                         <widget name="local" position="20,10" size="255,320" scrollbarMode="showOnDemand" />
127                         <widget name="remote" position="285,10" size="255,320" scrollbarMode="showOnDemand" />
128                         <widget source="progress" render="Progress" position="20,360" size="520,10" />
129                         <widget name="eta" position="20,330" size="200,30" font="Regular;23" />
130                         <widget name="speed" position="330,330" size="200,30" halign="right" font="Regular;23" />
131                         <ePixmap name="red" position="0,370" zPosition="4" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on" />
132                         <ePixmap name="green" position="140,370" zPosition="4" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
133                         <ePixmap name="yellow" position="280,370" zPosition="4" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
134                         <ePixmap name="blue" position="420,370" zPosition="4" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
135                         <widget name="key_red" position="0,370" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
136                         <widget name="key_green" position="140,370" 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="280,370" 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="420,370" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
139                 </screen>"""
140
141         def __init__(self, session):
142                 Screen.__init__(self, session)
143                 self.ftpclient = None
144                 self.file = None
145                 self.currlist = "remote"
146
147                 # Init what we need for dl progress
148                 self.currentLength = 0
149                 self.lastLength = 0
150                 self.lastTime = 0
151                 self.lastApprox = 0
152                 self.fileSize = 0
153
154                 self["local"] = FileList("/media/hdd")
155                 self["remote"] = FTPFileList()
156                 self["eta"] = Label("")
157                 self["speed"] = Label("")
158                 self["progress"] = VariableProgressSource()
159                 self["key_red"] = Button("")
160                 self["key_green"] = Button("")
161                 self["key_yellow"] = Button("")
162                 self["key_blue"] = Button("")
163
164                 URI = "ftp://root@localhost:21" # TODO: make configurable
165                 self.connect(URI)
166
167                 self["ftpbrowserBaseActions"] = HelpableActionMap(self, "ftpbrowserBaseActions",
168                         {
169                                 "ok": (self.ok, _("enter directory/get file/put file")),
170                                 "cancel": (self.cancel , _("close")),
171                         }, -2)
172
173                 self["ftpbrowserListActions"] = HelpableActionMap(self, "ftpbrowserListActions",
174                         {
175                                 "channelUp": (self.setLocal, _("Select local file list")),
176                                 "channelDown": (self.setRemote, _("Select remote file list")),
177                         })
178
179                 self["actions"] = ActionMap(["ftpbrowserDirectionActions"],
180                         {
181                                 "up": self.up,
182                                 "down": self.down,
183                                 "left": self.left,
184                                 "right": self.right,
185                         }, -2)
186
187         def setLocal(self):
188                 self.currlist = "local"
189
190         def setRemote(self):
191                 self.currlist = "remote"
192
193         def okQuestion(self, res = None):
194                 if res:
195                         self.ok(force = True)
196
197         def ok(self, force = False):
198                 if self.currlist == "remote":
199                         # Get file/change directory
200                         if self["remote"].canDescent():
201                                 self["remote"].descent()
202                         else:
203                                 if self.file:
204                                         self.session.open(
205                                                 MessageBox,
206                                                 _("There already is an active transfer."),
207                                                 type = MessageBox.TYPE_WARNING
208                                         )
209                                         return
210
211                                 remoteFile = self["remote"].getSelection()
212                                 if not remoteFile:
213                                         return
214
215                                 absRemoteFile = remoteFile[0]
216                                 fileName = absRemoteFile.split('/')[-1]
217                                 localFile = self["local"].getCurrentDirectory() + fileName
218                                 if not force and os_path.exists(localFile):
219                                         self.session.openWithCallback(
220                                                 self.okQuestion,
221                                                 MessageBox,
222                                                 _("A file with this name already exists locally.\nDo you want to overwrite it?"),
223                                         )
224                                 else:
225                                         self.currentLength = 0
226                                         self.lastLength = 0
227                                         self.lastTime = 0
228                                         self.lastApprox = 0
229                                         self.fileSize = remoteFile[2]
230
231                                         try:
232                                                 self.file = open(localFile, 'w')
233                                         except IOError, ie:
234                                                 # TODO: handle this
235                                                 raise ie
236                                         d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
237                                         d.addCallback(self.getFinished).addErrback(self.getFailed)
238                 else:
239                         # Put file/change directory
240                         assert(self.currlist == "local")
241                         if self["local"].canDescent():
242                                 self["local"].descent()
243                         else:
244                                 self.session.open(
245                                         MessageBox,
246                                         _("Not yet implemented."),
247                                         type = MessageBox.TYPE_WARNING
248                                 )
249
250         def getFinished(self, *args):
251                 self.session.open(
252                         MessageBox,
253                         _("Download finished."),
254                         type = MessageBox.TYPE_INFO
255                 )
256
257                 self["eta"].setText("")
258                 self["speed"].setText("")
259                 self["progress"].invalidate()
260                 self.file.close()
261                 self.file = None
262
263         def getFailed(self, *args):
264                 self.session.open(
265                         MessageBox,
266                         _("Error during download."),
267                         type = MessageBox.TYPE_ERROR
268                 )
269
270                 self["eta"].setText("")
271                 self["speed"].setText("")
272                 self["progress"].writeValues(0, 0)
273                 self.file.close()
274                 self.file = None
275
276         def gotProgress(self, pos, max):
277                 self["progress"].writeValues(pos, max)
278
279                 newTime = time()
280                 # Check if we're called the first time (got total)
281                 lastTime = self.lastTime
282                 if lastTime == 0:
283                         self.lastTime = newTime
284
285                 # 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)
286                 elif int(newTime - lastTime) >= 1:
287                         lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
288
289                         secLen = int(round(((max-pos) / 1024) / lastApprox))
290                         self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
291                         self["speed"].setText(_("%d kb/s") % (lastApprox))
292
293                         self.lastApprox = lastApprox
294                         self.lastLength = pos
295                         self.lastTime = newTime
296
297         def dataReceived(self, data):
298                 if not self.file:
299                         return
300
301                 self.currentLength += len(data)
302                 self.gotProgress(self.currentLength, self.fileSize)
303
304                 try:
305                         self.file.write(data)
306                 except IOError, ie:
307                         # TODO: handle this
308                         self.file = None
309                         raise ie
310
311         def cancelQuestion(self, res = None):
312                 if res:
313                         self.file.close()
314                         self.file = None
315                         self.cancel()
316
317         def cancel(self):
318                 if self.file is not None:
319                         self.session.openWithCallback(
320                                 self.cancelQuestion,
321                                 MessageBox,
322                                 _("A transfer is currently in progress.\nAbort?"),
323                         )
324                         return
325
326                 self.ftpclient.quit()
327                 self.close()
328
329         def up(self):
330                 self[self.currlist].up()
331
332         def down(self):
333                 self[self.currlist].down()
334
335         def left(self):
336                 self[self.currlist].pageUp()
337
338         def right(self):
339                 self[self.currlist].pageDown()
340
341         def connect(self, address):
342                 self.ftpclient = None
343                 self["remote"].ftpclient = None
344
345                 scheme, host, port, path, username, password = _parse(address)
346                 if not username:
347                         username = 'anonymous'
348                         password = 'my@email.com'
349
350                 timeout = 30 # TODO: make configurable
351                 passive = True # TODO: make configurable
352                 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
353
354                 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
355
356         def controlConnectionMade(self, ftpclient):
357                 print "[FTPBrowser] connection established"
358                 self.ftpclient = ftpclient
359                 self["remote"].ftpclient = ftpclient
360                 self["remote"].changeDir("/")
361
362         def connectionFailed(self, *args):
363                 print "[FTPBrowser] connection failed", args
364                 self.session.open(
365                                 MessageBox,
366                                 _("Could not connect to ftp server!"),
367                                 type = MessageBox.TYPE_ERROR,
368                                 timeout = 3,
369                 )
370