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