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