note which filelist is local/remote in skin
[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 from twisted.protocols.basic import FileSender
27
28 # For new and improved _parse
29 from urlparse import urlparse, urlunparse
30
31 # System
32 from os import path as os_path
33 from time import time
34
35 def _parse(url, defaultPort = None):
36         url = url.strip()
37         parsed = urlparse(url)
38         scheme = parsed[0]
39         path = urlunparse(('','')+parsed[2:])
40
41         if defaultPort is None:
42                 if scheme == 'https':
43                         defaultPort = 443
44                 elif scheme == 'ftp':
45                         defaultPort = 21
46                 else:
47                         defaultPort = 80
48
49         host, port = parsed[1], defaultPort
50
51         if '@' in host:
52                 username, host = host.split('@')
53                 if ':' in username:
54                         username, password = username.split(':')
55                 else:
56                         password = ""
57         else:
58                 username = ""
59                 password = ""
60
61         if ':' in host:
62                 host, port = host.split(':')
63                 port = int(port)
64
65         if path == "":
66                 path = "/"
67
68         return scheme, host, port, path, username, password
69
70 def FTPFileEntryComponent(file, directory):
71         isDir = True if file['filetype'] == 'd' else False
72         name = file['filename']
73         absolute = directory + name
74         if isDir:
75                 absolute += '/'
76
77         res = [
78                 (absolute, isDir, file['size']),
79                 (eListboxPythonMultiContent.TYPE_TEXT, 35, 1, 470, 20, 0, RT_HALIGN_LEFT, name)
80         ]
81         if isDir:
82                 png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/directory.png"))
83         else:
84                 extension = name.split('.')
85                 extension = extension[-1].lower()
86                 if EXTENSIONS.has_key(extension):
87                         png = LoadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, "extensions/" + EXTENSIONS[extension] + ".png"))
88                 else:
89                         png = None
90         if png is not None:
91                 res.append((eListboxPythonMultiContent.TYPE_PIXMAP_ALPHATEST, 10, 2, 20, 20, png))
92
93         return res
94
95 class FTPFileList(FileList):
96         def __init__(self):
97                 self.ftpclient = None
98                 self.select = None
99                 FileList.__init__(self, "/")
100
101         def changeDir(self, directory, select = None):
102                 if self.ftpclient is None:
103                         self.list = []
104                         self.l.setList(self.list)
105                         return
106
107                 self.current_directory = directory
108                 self.select = select
109
110                 self.filelist = FTPFileListProtocol()
111                 d = self.ftpclient.list(directory, self.filelist)
112                 d.addCallback(self.listRcvd).addErrback(self.listFailed)
113
114         def listRcvd(self, *args):
115                 # TODO: is any of the 'advanced' features useful (and more of all can they be implemented) here?
116                 list = [FTPFileEntryComponent(file, self.current_directory) for file in self.filelist.files]
117                 list.sort(key = lambda x: (not x[0][1], x[0][0]))
118                 if self.current_directory != "/":
119                         list.insert(0, FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True))
120
121                 self.l.setList(list)
122                 self.list = list
123
124                 select = self.select
125                 if select is not None:
126                         i = 0
127                         self.moveToIndex(0)
128                         for x in list:
129                                 p = x[0][0]
130
131                                 if p == select:
132                                         self.moveToIndex(i)
133                                         break
134                                 i += 1
135
136         def listFailed(self, *args):
137                 if self.current_directory != "/":
138                         self.list = [FileEntryComponent(name = "<" +_("Parent Directory") + ">", absolute = '/'.join(self.current_directory.split('/')[:-2]) + '/', isDir = True)]
139                 else:
140                         self.list = []
141                 self.l.setList(self.list)
142
143 class FTPBrowser(Screen, Protocol):
144         skin = """
145                 <screen name="FTPBrowser" position="center,center" size="560,440" title="FTP Browser">
146                         <widget name="localText" position="20,10" size="200,20" font="Regular;18" />
147                         <widget name="local" position="20,40" size="255,320" scrollbarMode="showOnDemand" />
148                         <widget name="remoteText" position="285,10" size="200,20" font="Regular;18" />
149                         <widget name="remote" position="285,40" size="255,320" scrollbarMode="showOnDemand" />
150                         <widget name="eta" position="20,360" size="200,30" font="Regular;23" />
151                         <widget name="speed" position="330,360" size="200,30" halign="right" font="Regular;23" />
152                         <widget source="progress" render="Progress" position="20,370" size="520,10" />
153                         <ePixmap name="red" position="0,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/red.png" transparent="1" alphatest="on" />
154                         <ePixmap name="green" position="140,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/green.png" transparent="1" alphatest="on" />
155                         <ePixmap name="yellow" position="280,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/yellow.png" transparent="1" alphatest="on" />
156                         <ePixmap name="blue" position="420,400" zPosition="4" size="140,40" pixmap="skin_default/buttons/blue.png" transparent="1" alphatest="on" />
157                         <widget name="key_red" position="0,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
158                         <widget name="key_green" position="140,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
159                         <widget name="key_yellow" position="280,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
160                         <widget name="key_blue" position="420,400" zPosition="5" size="140,40" valign="center" halign="center" font="Regular;21" transparent="1" foregroundColor="white" shadowColor="black" shadowOffset="-1,-1" />
161                 </screen>"""
162
163         def __init__(self, session):
164                 Screen.__init__(self, session)
165                 self.ftpclient = None
166                 self.file = None
167                 self.currlist = "remote"
168
169                 # Init what we need for dl progress
170                 self.currentLength = 0
171                 self.lastLength = 0
172                 self.lastTime = 0
173                 self.lastApprox = 0
174                 self.fileSize = 0
175
176                 self["localText"] = Label(_("Local"))
177                 self["local"] = FileList("/media/hdd/", showMountpoints = False)
178                 self["remoteText"] = Label(_("Remote"))
179                 self["remote"] = FTPFileList()
180                 self["eta"] = Label("")
181                 self["speed"] = Label("")
182                 self["progress"] = VariableProgressSource()
183                 self["key_red"] = Button("")
184                 self["key_green"] = Button("")
185                 self["key_yellow"] = Button("")
186                 self["key_blue"] = Button("")
187
188                 URI = "ftp://root@localhost:21" # TODO: make configurable
189                 self.connect(URI)
190
191                 self["ftpbrowserBaseActions"] = HelpableActionMap(self, "ftpbrowserBaseActions",
192                         {
193                                 "ok": (self.ok, _("enter directory/get file/put file")),
194                                 "cancel": (self.cancel , _("close")),
195                         }, -2)
196
197                 self["ftpbrowserListActions"] = HelpableActionMap(self, "ftpbrowserListActions",
198                         {
199                                 "channelUp": (self.setLocal, _("Select local file list")),
200                                 "channelDown": (self.setRemote, _("Select remote file list")),
201                         })
202
203                 self["actions"] = ActionMap(["ftpbrowserDirectionActions"],
204                         {
205                                 "up": self.up,
206                                 "down": self.down,
207                                 "left": self.left,
208                                 "right": self.right,
209                         }, -2)
210
211         def setLocal(self):
212                 self.currlist = "local"
213
214         def setRemote(self):
215                 self.currlist = "remote"
216
217         def okQuestion(self, res = None):
218                 if res:
219                         self.ok(force = True)
220
221         def ok(self, force = False):
222                 if self.currlist == "remote":
223                         # Get file/change directory
224                         if self["remote"].canDescent():
225                                 self["remote"].descent()
226                         else:
227                                 if self.file:
228                                         self.session.open(
229                                                 MessageBox,
230                                                 _("There already is an active transfer."),
231                                                 type = MessageBox.TYPE_WARNING
232                                         )
233                                         return
234
235                                 remoteFile = self["remote"].getSelection()
236                                 if not remoteFile:
237                                         return
238
239                                 absRemoteFile = remoteFile[0]
240                                 fileName = absRemoteFile.split('/')[-1]
241                                 localFile = self["local"].getCurrentDirectory() + fileName
242                                 if not force and os_path.exists(localFile):
243                                         self.session.openWithCallback(
244                                                 self.okQuestion,
245                                                 MessageBox,
246                                                 _("A file with this name already exists locally.\nDo you want to overwrite it?"),
247                                         )
248                                 else:
249                                         self.currentLength = 0
250                                         self.lastLength = 0
251                                         self.lastTime = 0
252                                         self.lastApprox = 0
253                                         self.fileSize = remoteFile[2]
254
255                                         try:
256                                                 self.file = open(localFile, 'w')
257                                         except IOError, ie:
258                                                 # TODO: handle this
259                                                 raise ie
260                                         else:
261                                                 d = self.ftpclient.retrieveFile(absRemoteFile, self, offset = 0)
262                                                 d.addCallback(self.getFinished).addErrback(self.getFailed)
263                 else:
264                         # Put file/change directory
265                         assert(self.currlist == "local")
266                         if self["local"].canDescent():
267                                 self["local"].descent()
268                         else:
269                                 if self.file:
270                                         self.session.open(
271                                                 MessageBox,
272                                                 _("There already is an active transfer."),
273                                                 type = MessageBox.TYPE_WARNING
274                                         )
275                                         return
276
277                                 localFile = self["local"].getSelection()
278                                 if not localFile:
279                                         return
280
281                                 def remoteFileExists(absName):
282                                         for file in self["remote"].getFileList():
283                                                 if file[0][0] == absName:
284                                                         return True
285                                         return False
286
287                                 # XXX: isn't this supposed to be an absolute filename? well, it's not for me :-/
288                                 fileName = localFile[0]
289                                 absLocalFile = self["local"].getCurrentDirectory() + fileName
290                                 directory = self["remote"].getCurrentDirectory()
291                                 sep = '/' if directory != '/' else ''
292                                 remoteFile = directory + sep + fileName
293                                 if not force and remoteFileExists(remoteFile):
294                                         self.session.openWithCallback(
295                                                 self.okQuestion,
296                                                 MessageBox,
297                                                 _("A file with this name already exists on the remote host.\nDo you want to overwrite it?"),
298                                         )
299                                 else:
300                                         self.currentLength = 0
301                                         self.lastLength = 0
302                                         self.lastTime = 0
303                                         self.lastApprox = 0
304
305                                         def sendfile(consumer, fileObj):
306                                                 FileSender().beginFileTransfer(fileObj, consumer, transform = self.putProgress).addCallback(  
307                                                         lambda _: consumer.finish()).addCallback(
308                                                         self.putComplete).addErrback(self.putFailed)
309
310                                         try:
311                                                 self.fileSize = int(os_path.getsize(absLocalFile))
312                                                 self.file = open(absLocalFile, 'rb')
313                                         except (IOError, OSError), e:
314                                                 # TODO: handle this
315                                                 raise e
316                                         else:
317                                                 dC, dL = self.ftpclient.storeFile(remoteFile)
318                                                 dC.addCallback(sendfile, self.file)
319
320         def transferFinished(self, msg, type, toRefresh):
321                 self.session.open(
322                         MessageBox,
323                         msg,
324                         type = type
325                 )
326
327                 self["eta"].setText("")
328                 self["speed"].setText("")
329                 self["progress"].invalidate()
330                 self[toRefresh].refresh()
331                 self.file.close()
332                 self.file = None
333
334         def putComplete(self, *args):
335                 self.transferFinished(
336                         _("Upload finished."),
337                         MessageBox.TYPE_INFO,
338                         "remote"
339                 )
340
341         def putFailed(self, *args):
342                 self.transferFinished(
343                         _("Error during download."),
344                         MessageBox.TYPE_ERROR,
345                         "remote"
346                 )
347
348         def getFinished(self, *args):
349                 self.transferFinished(
350                         _("Download finished."),
351                         MessageBox.TYPE_INFO,
352                         "local"
353                 )
354
355         def getFailed(self, *args):
356                 self.transferFinished(
357                         _("Error during download."),
358                         MessageBox.TYPE_ERROR,
359                         "local"
360                 )
361
362         def putProgress(self, chunk):
363                 self.currentLength += len(chunk)
364                 self.gotProgress(self.currentLength, self.fileSize)
365                 return chunk
366
367         def gotProgress(self, pos, max):
368                 self["progress"].writeValues(pos, max)
369
370                 newTime = time()
371                 # Check if we're called the first time (got total)
372                 lastTime = self.lastTime
373                 if lastTime == 0:
374                         self.lastTime = newTime
375
376                 # 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)
377                 elif int(newTime - lastTime) >= 2:
378                         lastApprox = round(((pos - self.lastLength) / (newTime - lastTime) / 1024), 2)
379
380                         secLen = int(round(((max-pos) / 1024) / lastApprox))
381                         self["eta"].setText(_("ETA %d:%02d min") % (secLen / 60, secLen % 60))
382                         self["speed"].setText(_("%d kb/s") % (lastApprox))
383
384                         self.lastApprox = lastApprox
385                         self.lastLength = pos
386                         self.lastTime = newTime
387
388         def dataReceived(self, data):
389                 if not self.file:
390                         return
391
392                 self.currentLength += len(data)
393                 self.gotProgress(self.currentLength, self.fileSize)
394
395                 try:
396                         self.file.write(data)
397                 except IOError, ie:
398                         # TODO: handle this
399                         self.file = None
400                         raise ie
401
402         def cancelQuestion(self, res = None):
403                 if res:
404                         self.file.close()
405                         self.file = None
406                         self.cancel()
407
408         def cancel(self):
409                 if self.file is not None:
410                         self.session.openWithCallback(
411                                 self.cancelQuestion,
412                                 MessageBox,
413                                 _("A transfer is currently in progress.\nAbort?"),
414                         )
415                         return
416
417                 self.ftpclient.quit()
418                 self.close()
419
420         def up(self):
421                 self[self.currlist].up()
422
423         def down(self):
424                 self[self.currlist].down()
425
426         def left(self):
427                 self[self.currlist].pageUp()
428
429         def right(self):
430                 self[self.currlist].pageDown()
431
432         def connect(self, address):
433                 self.ftpclient = None
434                 self["remote"].ftpclient = None
435
436                 scheme, host, port, path, username, password = _parse(address)
437                 if not username:
438                         username = 'anonymous'
439                         password = 'my@email.com'
440
441                 timeout = 30 # TODO: make configurable
442                 passive = True # TODO: make configurable
443                 creator = ClientCreator(reactor, FTPClient, username, password, passive = passive)
444
445                 creator.connectTCP(host, port, timeout).addCallback(self.controlConnectionMade).addErrback(self.connectionFailed)
446
447         def controlConnectionMade(self, ftpclient):
448                 print "[FTPBrowser] connection established"
449                 self.ftpclient = ftpclient
450                 self["remote"].ftpclient = ftpclient
451                 self["remote"].changeDir("/")
452
453         def connectionFailed(self, *args):
454                 print "[FTPBrowser] connection failed", args
455                 self.session.open(
456                                 MessageBox,
457                                 _("Could not connect to ftp server!"),
458                                 type = MessageBox.TYPE_ERROR,
459                                 timeout = 3,
460                 )
461