source: deluge/ui/web/json_api.py@ a60bf9

2.0.x develop extjs4-port
Last change on this file since a60bf9 was a60bf9, checked in by Damien Churchill <damoc@gmail.com>, 16 years ago

fix displaying the host-list

  • Property mode set to 100644
File size: 18.5 KB
Line 
1#
2# deluge/ui/web/json_api.py
3#
4# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
5#
6# Deluge is free software.
7#
8# You may redistribute it and/or modify it under the terms of the
9# GNU General Public License, as published by the Free Software
10# Foundation; either version 3 of the License, or (at your option)
11# any later version.
12#
13# deluge is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16# See the GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with deluge. If not, write to:
20# The Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor
22# Boston, MA 02110-1301, USA.
23#
24
25import os
26import time
27import base64
28import urllib
29import hashlib
30import logging
31import tempfile
32
33from types import FunctionType
34from twisted.internet.defer import Deferred
35from twisted.web import http, resource, server
36
37from deluge import common, component
38from deluge.configmanager import ConfigManager
39from deluge.ui import common as uicommon
40from deluge.ui.client import client, Client
41from deluge.ui.web.auth import *
42from deluge.ui.web.common import _
43json = common.json
44
45log = logging.getLogger(__name__)
46
47def export(auth_level=AUTH_LEVEL_DEFAULT):
48 """
49 Decorator function to register an object's method as an RPC. The object
50 will need to be registered with an `:class:RPCServer` to be effective.
51
52 :param func: function, the function to export
53 :param auth_level: int, the auth level required to call this method
54
55 """
56 def wrap(func, *args, **kwargs):
57 func._json_export = True
58 func._json_auth_level = auth_level
59 return func
60
61 if type(auth_level) is FunctionType:
62 func = auth_level
63 auth_level = AUTH_LEVEL_DEFAULT
64 return wrap(func)
65 else:
66 return wrap
67
68class JSONException(Exception):
69 def __init__(self, inner_exception):
70 self.inner_exception = inner_exception
71 Exception.__init__(self, str(inner_exception))
72
73class JSON(resource.Resource, component.Component):
74 """
75 A Twisted Web resource that exposes a JSON-RPC interface for web clients
76 to use.
77 """
78
79 def __init__(self):
80 resource.Resource.__init__(self)
81 component.Component.__init__(self, "JSON")
82 self._remote_methods = []
83 self._local_methods = {}
84 client.disconnect_callback = self._on_client_disconnect
85
86 def connect(self, host="localhost", port=58846, username="", password=""):
87 """
88 Connects the client to a daemon
89 """
90 d = Deferred()
91 _d = client.connect(host, port, username, password)
92
93 def on_get_methods(methods):
94 """
95 Handles receiving the method names
96 """
97 self._remote_methods = methods
98 methods = list(self._remote_methods)
99 methods.extend(self._local_methods)
100 d.callback(methods)
101
102 def on_client_connected(connection_id):
103 """
104 Handles the client successfully connecting to the daemon and
105 invokes retrieving the method names.
106 """
107 d = client.daemon.get_method_list()
108 d.addCallback(on_get_methods)
109 component.get("PluginManager").start()
110 _d.addCallback(on_client_connected)
111 return d
112
113 def _on_client_disconnect(self, *args):
114 component.get("PluginManager").stop()
115
116 def _exec_local(self, method, params):
117 """
118 Handles executing all local methods.
119 """
120 if method == "system.listMethods":
121 d = Deferred()
122 methods = list(self._remote_methods)
123 methods.extend(self._local_methods)
124 d.callback(methods)
125 return d
126 elif method in self._local_methods:
127 # This will eventually process methods that the server adds
128 # and any plugins.
129 return self._local_methods[method](*params)
130 raise JSONException("Unknown system method")
131
132 def _exec_remote(self, method, params):
133 """
134 Executes methods using the Deluge client.
135 """
136 component, method = method.split(".")
137 return getattr(getattr(client, component), method)(*params)
138
139 def _handle_request(self, request):
140 """
141 Takes some json data as a string and attempts to decode it, and process
142 the rpc object that should be contained, returning a deferred for all
143 procedure calls and the request id.
144 """
145 request_id = None
146 try:
147 request = json.loads(request)
148 except ValueError:
149 raise JSONException("JSON not decodable")
150
151 if "method" not in request or "id" not in request or \
152 "params" not in request:
153 raise JSONException("Invalid JSON request")
154
155 method, params = request["method"], request["params"]
156 request_id = request["id"]
157
158 try:
159 if method.startswith("system."):
160 return self._exec_local(method, params), request_id
161 elif method in self._local_methods:
162 return self._exec_local(method, params), request_id
163 elif method in self._remote_methods:
164 return self._exec_remote(method, params), request_id
165 except Exception, e:
166 log.exception(e)
167 d = Deferred()
168 d.callback(None)
169 return d, request_id
170
171 def _on_rpc_request_finished(self, result, response, request):
172 """
173 Sends the response of any rpc calls back to the json-rpc client.
174 """
175 response["result"] = result
176 return self._send_response(request, response)
177
178 def _on_rpc_request_failed(self, reason, response, request):
179 """
180 Handles any failures that occured while making an rpc call.
181 """
182 print type(reason)
183 request.setResponseCode(http.INTERNAL_SERVER_ERROR)
184 return ""
185
186 def _on_json_request(self, request):
187 """
188 Handler to take the json data as a string and pass it on to the
189 _handle_request method for further processing.
190 """
191 log.debug("json-request: %s", request.json)
192 response = {"result": None, "error": None, "id": None}
193 d, response["id"] = self._handle_request(request.json)
194 d.addCallback(self._on_rpc_request_finished, response, request)
195 d.addErrback(self._on_rpc_request_failed, response, request)
196 return d
197
198 def _on_json_request_failed(self, reason, request):
199 """
200 Errback handler to return a HTTP code of 500.
201 """
202 log.exception(reason)
203 request.setResponseCode(http.INTERNAL_SERVER_ERROR)
204 return ""
205
206 def _send_response(self, request, response):
207 response = json.dumps(response)
208 request.setHeader("content-type", "application/x-json")
209 request.write(response)
210 request.finish()
211
212 def render(self, request):
213 """
214 Handles all the POST requests made to the /json controller.
215 """
216
217 if request.method != "POST":
218 request.setResponseCode(http.NOT_ALLOWED)
219 return ""
220
221 try:
222 request.content.seek(0)
223 request.json = request.content.read()
224 d = self._on_json_request(request)
225 return server.NOT_DONE_YET
226 except Exception, e:
227 return self._on_json_request_failed(e, request)
228
229 def register_object(self, obj, name=None):
230 """
231 Registers an object to export it's rpc methods. These methods should
232 be exported with the export decorator prior to registering the object.
233
234 :param obj: object, the object that we want to export
235 :param name: str, the name to use, if None, it will be the class name of the object
236 """
237 name = name or obj.__class__.__name__
238 name = name.lower()
239
240 for d in dir(obj):
241 if d[0] == "_":
242 continue
243 if getattr(getattr(obj, d), '_json_export', False):
244 log.debug("Registering method: %s", name + "." + d)
245 self._local_methods[name + "." + d] = getattr(obj, d)
246
247class JSONComponent(component.Component):
248 def __init__(self, name, interval=1, depend=None):
249 super(JSONComponent, self).__init__(name, interval, depend)
250 self._json = component.get("JSON")
251 self._json.register_object(self, name)
252
253
254DEFAULT_HOST = "127.0.0.1"
255DEFAULT_PORT = 58846
256
257DEFAULT_HOSTS = {
258 "hosts": [(hashlib.sha1(str(time.time())).hexdigest(),
259 DEFAULT_HOST, DEFAULT_PORT, "", "")]
260}
261HOSTLIST_ID = 0
262HOSTLIST_NAME = 1
263HOSTLIST_PORT = 2
264HOSTLIST_USER = 3
265HOSTLIST_PASS = 4
266
267HOSTS_ID = HOSTLIST_ID
268HOSTS_NAME = HOSTLIST_NAME
269HOSTS_PORT = HOSTLIST_PORT
270HOSTS_STATUS = 3
271HOSTS_INFO = 4
272
273FILES_KEYS = ["files", "file_progress", "file_priorities"]
274
275class WebApi(JSONComponent):
276 def __init__(self):
277 super(WebApi, self).__init__("Web")
278 self.host_list = ConfigManager("hostlist.conf.1.2", DEFAULT_HOSTS)
279
280 def get_host(self, connection_id):
281 for host in self.host_list["hosts"]:
282 if host[0] == connection_id:
283 return host
284
285 @export
286 def connect(self, host_id):
287 d = Deferred()
288 def on_connected(methods):
289 d.callback(methods)
290 for host in self.host_list["hosts"]:
291 if host_id != host[0]:
292 continue
293 self._json.connect(*host[1:]).addCallback(on_connected)
294 return d
295
296 @export
297 def connected(self):
298 d = Deferred()
299 d.callback(client.connected())
300 return d
301
302 @export
303 def disconnect(self):
304 d = Deferred()
305 client.disconnect()
306 d.callback(True)
307 return d
308
309 @export
310 def update_ui(self, keys, filter_dict):
311
312 ui_info = {
313 "torrents": None,
314 "filters": None,
315 "stats": None
316 }
317
318 d = Deferred()
319
320 log.info("Updating ui with keys '%r' and filters '%r'", keys,
321 filter_dict)
322
323 def got_stats(stats):
324 ui_info["stats"] = stats
325 d.callback(ui_info)
326
327 def got_filters(filters):
328 ui_info["filters"] = filters
329 client.core.get_stats().addCallback(got_stats)
330
331 def got_torrents(torrents):
332 ui_info["torrents"] = torrents
333 client.core.get_filter_tree().addCallback(got_filters)
334 client.core.get_torrents_status(filter_dict, keys).addCallback(got_torrents)
335 return d
336
337 def _on_got_files(self, torrent, d):
338 files = torrent.get("files")
339 file_progress = torrent.get("file_progress")
340 file_priorities = torrent.get("file_priorities")
341
342 paths = []
343 info = {}
344 for index, torrent_file in enumerate(files):
345 path = torrent_file["path"]
346 paths.append(path)
347 torrent_file["progress"] = file_progress[index]
348 torrent_file["priority"] = file_priorities[index]
349 torrent_file["index"] = index
350 info[path] = torrent_file
351
352 def walk(path, item):
353 if type(item) is dict:
354 return item
355 return [info[path]["index"], info[path]["size"],
356 info[path]["progress"], info[path]["priority"]]
357
358 file_tree = uicommon.FileTree(paths)
359 file_tree.walk(walk)
360 d.callback(file_tree.get_tree())
361
362 @export
363 def get_torrent_files(self, torrent_id):
364 main_deferred = Deferred()
365 d = client.core.get_torrent_status(torrent_id, FILES_KEYS)
366 d.addCallback(self._on_got_files, main_deferred)
367 return main_deferred
368
369 @export
370 def download_torrent_from_url(self, url):
371 """
372 input:
373 url: the url of the torrent to download
374
375 returns:
376 filename: the temporary file name of the torrent file
377 """
378 tmp_file = os.path.join(tempfile.gettempdir(), url.split("/")[-1])
379 filename, headers = urllib.urlretrieve(url, tmp_file)
380 log.debug("filename: %s", filename)
381 d = Deferred()
382 d.callback(filename)
383 return d
384
385 @export
386 def get_torrent_info(self, filename):
387 """
388 Goal:
389 allow the webui to retrieve data about the torrent
390
391 input:
392 filename: the filename of the torrent to gather info about
393
394 returns:
395 {
396 "filename": the torrent file
397 "name": the torrent name
398 "size": the total size of the torrent
399 "files": the files the torrent contains
400 "info_hash" the torrents info_hash
401 }
402 """
403 d = Deferred()
404 try:
405 torrent_info = uicommon.TorrentInfo(filename.strip())
406 d.callback(torrent_info.as_dict("name", "info_hash", "files_tree"))
407 except:
408 d.callback(False)
409 return d
410
411 @export
412 def add_torrents(self, torrents):
413 """
414 input:
415 torrents [{
416 path: the path of the torrent file,
417 options: the torrent options
418 }]
419 """
420 for torrent in torrents:
421 filename = os.path.basename(torrent["path"])
422 fdump = base64.encodestring(open(torrent["path"], "r").read())
423 log.info("Adding torrent from file `%s` with options `%r`",
424 filename, torrent["options"])
425 client.core.add_torrent_file(filename, fdump, torrent["options"])
426 d = Deferred()
427 d.callback(True)
428 return d
429
430 @export
431 def login(self, password):
432 """Method to allow the webui to authenticate
433 """
434 config = component.get("DelugeWeb").config
435 m = hashlib.md5()
436 m.update(config['pwd_salt'])
437 m.update(password)
438 d = Deferred()
439 d.callback(m.hexdigest() == config['pwd_md5'])
440 return d
441
442 @export
443 def get_hosts(self):
444 """Return the hosts in the hostlist"""
445 hosts = dict((host[HOSTLIST_ID], list(host[:])) for \
446 host in self.host_list["hosts"])
447
448 main_deferred = Deferred()
449 def run_check():
450 if all(map(lambda x: x[HOSTS_STATUS] is not None, hosts.values())):
451 main_deferred.callback(hosts.values())
452
453 def on_connect(connected, c, host_id):
454 def on_info(info, c):
455 hosts[host_id][HOSTS_STATUS] = _("Online")
456 hosts[host_id][HOSTS_INFO] = info
457 c.disconnect()
458 run_check()
459
460 def on_info_fail(reason):
461 hosts[host_id][HOSTS_STATUS] = _("Offline")
462 run_check()
463
464 if not connected:
465 hosts[host_id][HOSTS_STATUS] = _("Offline")
466 run_check()
467 return
468
469 d = c.daemon.info()
470 d.addCallback(on_info, c)
471 d.addErrback(on_info_fail)
472
473 def on_connect_failed(reason, host_id):
474 log.exception(reason)
475 hosts[host_id][HOSTS_STATUS] = _("Offline")
476 run_check()
477
478 for host in hosts.values():
479 host_id, host, port, user, password = host
480 hosts[host_id][HOSTS_STATUS:HOSTS_INFO] = (None, None)
481
482 if client.connected() and (host, port, "localclient" if not \
483 user and host in ("127.0.0.1", "localhost") else \
484 user) == client.connection_info():
485 def on_info(info):
486 hosts[host_id][HOSTS_INFO] = info
487 run_check()
488 hosts[host_id][HOSTS_STATUS] = _("Connected")
489 client.daemon.info().addCallback(on_info)
490 continue
491
492 c = Client()
493 d = c.connect(host, port, user, password)
494 d.addCallback(on_connect, c, host_id)
495 d.addErrback(on_connect_failed, host_id)
496 return main_deferred
497
498 @export
499 def stop_daemon(self, connection_id):
500 """
501 Stops a running daemon.
502
503 :param connection_id: str, the hash id of the connection
504
505 """
506 main_deferred = Deferred()
507 host = self.get_host(connection_id)
508 if not host:
509 main_deferred.callback((False, _("Daemon doesn't exist")))
510 return main_deferred
511
512 try:
513 def on_connect(connected, c):
514 if not connected:
515 main_deferred.callback((False, _("Daemon not running")))
516 return
517 c.daemon.shutdown()
518 main_deferred.callback((True, ))
519
520 def on_connect_failed(reason):
521 main_deferred.callback((False, reason))
522
523 host, port, user, password = host[1:5]
524 c = Client()
525 d = c.connect(host, port, user, password)
526 d.addCallback(on_connect, c)
527 d.addErrback(on_connect_failed)
528 except:
529 main_deferred.callback((False, "An error occured"))
530 return main_deferred
531
532 @export
533 def add_host(self, host, port, username="", password=""):
534 """
535 Adds a host to the list.
536
537 :param host: str, the hostname
538 :param port: int, the port
539 :param username: str, the username to login as
540 :param password: str, the password to login with
541
542 """
543 d = Deferred()
544 # Check to see if there is already an entry for this host and return
545 # if thats the case
546 for entry in self.host_list["hosts"]:
547 if (entry[0], entry[1], entry[2]) == (host, port, username):
548 d.callback((False, "Host already in the list"))
549
550 try:
551 port = int(port)
552 except:
553 d.callback((False, "Port is invalid"))
554 return d
555
556 # Host isn't in the list, so lets add it
557 connection_id = hashlib.sha1(str(time.time())).hexdigest()
558 self.host_list["hosts"].append([connection_id, host, port, username,
559 password])
560 self.host_list.save()
561 d.callback((True,))
562 return d
563
564 @export
565 def remove_host(self, connection_id):
566 """
567 Removes a host for the list
568
569 :param connection_Id: str, the hash id of the connection
570
571 """
572 d = Deferred()
573 host = self.get_host(connection_id)
574 if host is None:
575 d.callback(False)
576
577 self.host_list["hosts"].remove(host)
578 self.host_list.save()
579 d.callback(True)
580 return d
Note: See TracBrowser for help on using the repository browser.