1 | #
|
---|
2 | # deluge/ui/web/webui.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 |
|
---|
25 | import os
|
---|
26 | import time
|
---|
27 | import locale
|
---|
28 | import shutil
|
---|
29 | import urllib
|
---|
30 | import gettext
|
---|
31 | import hashlib
|
---|
32 | import logging
|
---|
33 | import tempfile
|
---|
34 | import mimetypes
|
---|
35 | import pkg_resources
|
---|
36 |
|
---|
37 | from twisted.application import service, internet
|
---|
38 | from twisted.internet import reactor, error
|
---|
39 | from twisted.web import http, resource, server, static
|
---|
40 |
|
---|
41 | from deluge import common, component
|
---|
42 | from deluge.configmanager import ConfigManager
|
---|
43 | from deluge.log import setupLogger, LOG as _log
|
---|
44 | from deluge.ui import common as uicommon
|
---|
45 | from deluge.ui.tracker_icons import TrackerIcons
|
---|
46 | from deluge.ui.web.common import Template
|
---|
47 | from deluge.ui.web.json_api import JSON, WebApi
|
---|
48 | from deluge.ui.web.pluginmanager import PluginManager
|
---|
49 | log = logging.getLogger(__name__)
|
---|
50 |
|
---|
51 | # Initialize gettext
|
---|
52 | try:
|
---|
53 | locale.setlocale(locale.LC_ALL, "")
|
---|
54 | if hasattr(locale, "bindtextdomain"):
|
---|
55 | locale.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
---|
56 | if hasattr(locale, "textdomain"):
|
---|
57 | locale.textdomain("deluge")
|
---|
58 | gettext.bindtextdomain("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
---|
59 | gettext.textdomain("deluge")
|
---|
60 | gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
---|
61 | except Exception, e:
|
---|
62 | log.error("Unable to initialize gettext/locale: %s", e)
|
---|
63 |
|
---|
64 | _ = gettext.gettext
|
---|
65 |
|
---|
66 | current_dir = os.path.dirname(__file__)
|
---|
67 |
|
---|
68 | CONFIG_DEFAULTS = {
|
---|
69 | "port": 8112,
|
---|
70 | "theme": "slate",
|
---|
71 | "pwd_salt": "16f65d5c79b7e93278a28b60fed2431e",
|
---|
72 | "pwd_md5": "2c9baa929ca38fb5c9eb5b054474d1ce",
|
---|
73 | "base": "",
|
---|
74 | "sessions": [],
|
---|
75 | "sidebar_show_zero": False,
|
---|
76 | "sidebar_show_trackers": False,
|
---|
77 | "show_keyword_search": False,
|
---|
78 | "show_sidebar": True,
|
---|
79 | "https": False
|
---|
80 | }
|
---|
81 |
|
---|
82 | def rpath(path):
|
---|
83 | """Convert a relative path into an absolute path relative to the location
|
---|
84 | of this script.
|
---|
85 | """
|
---|
86 | return os.path.join(current_dir, path)
|
---|
87 |
|
---|
88 | class Config(resource.Resource):
|
---|
89 | """
|
---|
90 | Writes out a javascript file that contains the WebUI configuration
|
---|
91 | available as Deluge.Config.
|
---|
92 | """
|
---|
93 |
|
---|
94 | def render(self, request):
|
---|
95 | return """Deluge = {
|
---|
96 | author: 'Damien Churchill <damoxc@gmail.com>',
|
---|
97 | version: '1.2-dev',
|
---|
98 | config: %s
|
---|
99 | }""" % common.json.dumps(component.get("DelugeWeb").config.config)
|
---|
100 |
|
---|
101 | class GetText(resource.Resource):
|
---|
102 | def render(self, request):
|
---|
103 | request.setHeader("content-type", "text/javascript; encoding=utf-8")
|
---|
104 | template = Template(filename=rpath("gettext.js"))
|
---|
105 | return template.render()
|
---|
106 |
|
---|
107 | class Upload(resource.Resource):
|
---|
108 | """
|
---|
109 | Twisted Web resource to handle file uploads
|
---|
110 | """
|
---|
111 |
|
---|
112 | def render(self, request):
|
---|
113 | """
|
---|
114 | Saves all uploaded files to the disk and returns a list of filenames,
|
---|
115 | each on a new line.
|
---|
116 | """
|
---|
117 |
|
---|
118 | # Block all other HTTP methods.
|
---|
119 | if request.method != "POST":
|
---|
120 | request.setResponseCode(http.NOT_ALLOWED)
|
---|
121 | return ""
|
---|
122 |
|
---|
123 | if "file" not in request.args:
|
---|
124 | request.setResponseCode(http.OK)
|
---|
125 | return ""
|
---|
126 |
|
---|
127 | tempdir = os.path.join(tempfile.gettempdir(), "delugeweb")
|
---|
128 | if not os.path.isdir(tempdir):
|
---|
129 | os.mkdir(tempdir)
|
---|
130 |
|
---|
131 | filenames = []
|
---|
132 | for upload in request.args.get("file"):
|
---|
133 | fd, fn = tempfile.mkstemp('.torrent', dir=tempdir)
|
---|
134 | os.write(fd, upload)
|
---|
135 | os.close(fd)
|
---|
136 | filenames.append(fn)
|
---|
137 | request.setHeader("content-type", "text/plain")
|
---|
138 | request.setResponseCode(http.OK)
|
---|
139 | return "\n".join(filenames)
|
---|
140 |
|
---|
141 | class Render(resource.Resource):
|
---|
142 |
|
---|
143 | def getChild(self, path, request):
|
---|
144 | request.render_file = path
|
---|
145 | return self
|
---|
146 |
|
---|
147 | def render(self, request):
|
---|
148 | if not hasattr(request, "render_file"):
|
---|
149 | request.setResponseCode(http.INTERNAL_SERVER_ERROR)
|
---|
150 | return ""
|
---|
151 |
|
---|
152 | filename = os.path.join("render", request.render_file)
|
---|
153 | template = Template(filename=rpath(filename))
|
---|
154 | request.setHeader("content-type", "text/html")
|
---|
155 | request.setResponseCode(http.OK)
|
---|
156 | return template.render()
|
---|
157 |
|
---|
158 | class Tracker(resource.Resource):
|
---|
159 | tracker_icons = TrackerIcons()
|
---|
160 |
|
---|
161 | def getChild(self, path, request):
|
---|
162 | request.tracker_name = path
|
---|
163 | return self
|
---|
164 |
|
---|
165 | def render(self, request):
|
---|
166 | headers = {}
|
---|
167 | filename = self.tracker_icons.get(request.tracker_name)
|
---|
168 | if filename:
|
---|
169 | request.setHeader("cache-control",
|
---|
170 | "public, must-revalidate, max-age=86400")
|
---|
171 | if filename.endswith(".ico"):
|
---|
172 | request.setHeader("content-type", "image/x-icon")
|
---|
173 | elif filename.endwith(".png"):
|
---|
174 | request.setHeader("content-type", "image/png")
|
---|
175 | data = open(filename, "rb")
|
---|
176 | request.setResponseCode(http.OK)
|
---|
177 | return data.read()
|
---|
178 | else:
|
---|
179 | request.setResponseCode(http.NOT_FOUND)
|
---|
180 | return ""
|
---|
181 |
|
---|
182 | class Flag(resource.Resource):
|
---|
183 | def getChild(self, path, request):
|
---|
184 | request.country = path
|
---|
185 | return self
|
---|
186 |
|
---|
187 | def render(self, request):
|
---|
188 | headers = {}
|
---|
189 | path = ("data", "pixmaps", "flags", request.country.lower() + ".png")
|
---|
190 | filename = pkg_resources.resource_filename("deluge",
|
---|
191 | os.path.join(*path))
|
---|
192 | if os.path.exists(filename):
|
---|
193 | request.setHeader("cache-control",
|
---|
194 | "public, must-revalidate, max-age=86400")
|
---|
195 | request.setHeader("content-type", "image/png")
|
---|
196 | data = open(filename, "rb")
|
---|
197 | request.setResponseCode(http.OK)
|
---|
198 | return data.read()
|
---|
199 | else:
|
---|
200 | request.setResponseCode(http.NOT_FOUND)
|
---|
201 | return ""
|
---|
202 |
|
---|
203 | class LookupResource(resource.Resource, component.Component):
|
---|
204 |
|
---|
205 | def __init__(self, name, *directories):
|
---|
206 | resource.Resource.__init__(self)
|
---|
207 | component.Component.__init__(self, name)
|
---|
208 | self.__directories = directories
|
---|
209 |
|
---|
210 | @property
|
---|
211 | def directories(self):
|
---|
212 | return self.__directories
|
---|
213 |
|
---|
214 | def getChild(self, path, request):
|
---|
215 | request.path = path
|
---|
216 | return self
|
---|
217 |
|
---|
218 | def render(self, request):
|
---|
219 | log.debug("Requested path: '%s'", request.path)
|
---|
220 | for lookup in self.directories:
|
---|
221 | if request.path in os.listdir(lookup):
|
---|
222 | path = os.path.join(lookup, request.path)
|
---|
223 | log.debug("Serving path: '%s'", path)
|
---|
224 | mime_type = mimetypes.guess_type(path)
|
---|
225 | request.setHeader("content-type", mime_type[0])
|
---|
226 | return open(path, "rb").read()
|
---|
227 | request.setResponseCode(http.NOT_FOUND)
|
---|
228 | return "<h1>404 - Not Found</h1>"
|
---|
229 |
|
---|
230 | class TopLevel(resource.Resource):
|
---|
231 | addSlash = True
|
---|
232 |
|
---|
233 | __stylesheets = [
|
---|
234 | "/css/ext-all.css",
|
---|
235 | "/css/ext-extensions.css",
|
---|
236 | "/css/deluge.css"
|
---|
237 | ]
|
---|
238 |
|
---|
239 | __scripts = [
|
---|
240 | "/js/ext-base.js",
|
---|
241 | "/js/ext-all.js",
|
---|
242 | "/js/ext-extensions.js",
|
---|
243 | "/config.js",
|
---|
244 | "/gettext.js",
|
---|
245 | "/js/deluge-yc.js"
|
---|
246 | ]
|
---|
247 |
|
---|
248 | __debug_scripts = [
|
---|
249 | "/js/ext-base.js",
|
---|
250 | "/js/ext-all-debug.js",
|
---|
251 | "/js/ext-extensions-debug.js",
|
---|
252 | "/config.js",
|
---|
253 | "/gettext.js",
|
---|
254 | "/js/Deluge.js",
|
---|
255 | "/js/Deluge.Formatters.js",
|
---|
256 | "/js/Deluge.Menus.js",
|
---|
257 | "/js/Deluge.Events.js",
|
---|
258 | "/js/Deluge.Client.js",
|
---|
259 | "/js/Deluge.ConnectionManager.js",
|
---|
260 | "/js/Deluge.Details.js",
|
---|
261 | "/js/Deluge.Details.Status.js",
|
---|
262 | "/js/Deluge.Details.Details.js",
|
---|
263 | "/js/Deluge.Details.Files.js",
|
---|
264 | "/js/Deluge.Details.Peers.js",
|
---|
265 | "/js/Deluge.Details.Options.js",
|
---|
266 | "/js/Deluge.Keys.js",
|
---|
267 | "/js/Deluge.Login.js",
|
---|
268 | "/js/Deluge.Preferences.js",
|
---|
269 | "/js/Deluge.Preferences.Downloads.js",
|
---|
270 | "/js/Deluge.Preferences.Network.js",
|
---|
271 | "/js/Deluge.Preferences.Bandwidth.js",
|
---|
272 | "/js/Deluge.Preferences.Interface.js",
|
---|
273 | "/js/Deluge.Preferences.Other.js",
|
---|
274 | "/js/Deluge.Preferences.Daemon.js",
|
---|
275 | "/js/Deluge.Preferences.Queue.js",
|
---|
276 | "/js/Deluge.Preferences.Proxy.js",
|
---|
277 | "/js/Deluge.Preferences.Notification.js",
|
---|
278 | "/js/Deluge.Preferences.Plugins.js",
|
---|
279 | "/js/Deluge.Sidebar.js",
|
---|
280 | "/js/Deluge.Statusbar.js",
|
---|
281 | "/js/Deluge.Toolbar.js",
|
---|
282 | "/js/Deluge.Torrents.js",
|
---|
283 | "/js/Deluge.UI.js"
|
---|
284 | ]
|
---|
285 |
|
---|
286 | def __init__(self):
|
---|
287 | resource.Resource.__init__(self)
|
---|
288 | self.putChild("config.js", Config())
|
---|
289 | self.putChild("css", LookupResource("Css", rpath("css")))
|
---|
290 | self.putChild("gettext.js", GetText())
|
---|
291 | self.putChild("flag", Flag())
|
---|
292 | self.putChild("icons", LookupResource("Icons", rpath("icons")))
|
---|
293 | self.putChild("images", LookupResource("Images", rpath("images")))
|
---|
294 | self.putChild("js", LookupResource("Javascript", rpath("js")))
|
---|
295 | self.putChild("json", JSON())
|
---|
296 | self.putChild("upload", Upload())
|
---|
297 | self.putChild("render", Render())
|
---|
298 | self.putChild("themes", static.File(rpath("themes")))
|
---|
299 | self.putChild("tracker", Tracker())
|
---|
300 |
|
---|
301 | theme = component.get("DelugeWeb").config["theme"]
|
---|
302 | self.__stylesheets.append("/css/xtheme-%s.css" % theme)
|
---|
303 |
|
---|
304 | @property
|
---|
305 | def scripts(self):
|
---|
306 | return self.__scripts
|
---|
307 |
|
---|
308 | @property
|
---|
309 | def debug_scripts(self):
|
---|
310 | return self.__debug_scripts
|
---|
311 |
|
---|
312 | @property
|
---|
313 | def stylesheets(self):
|
---|
314 | return self.__stylesheets
|
---|
315 |
|
---|
316 | def getChild(self, path, request):
|
---|
317 | if path == "":
|
---|
318 | return self
|
---|
319 | else:
|
---|
320 | return resource.Resource.getChild(self, path, request)
|
---|
321 |
|
---|
322 | def render(self, request):
|
---|
323 | if request.args.get('debug', ['false'])[-1] == 'true':
|
---|
324 | scripts = self.debug_scripts[:]
|
---|
325 | else:
|
---|
326 | scripts = self.scripts[:]
|
---|
327 |
|
---|
328 | template = Template(filename=rpath("index.html"))
|
---|
329 | request.setHeader("content-type", "text/html; charset=utf-8")
|
---|
330 | return template.render(scripts=scripts, stylesheets=self.stylesheets)
|
---|
331 |
|
---|
332 | class DelugeWeb(component.Component):
|
---|
333 |
|
---|
334 | def __init__(self):
|
---|
335 | super(DelugeWeb, self).__init__("DelugeWeb")
|
---|
336 | self.config = ConfigManager("web.conf", CONFIG_DEFAULTS)
|
---|
337 |
|
---|
338 | self.top_level = TopLevel()
|
---|
339 | self.site = server.Site(self.top_level)
|
---|
340 | self.port = self.config["port"]
|
---|
341 | self.web_api = WebApi()
|
---|
342 |
|
---|
343 | # Since twisted assigns itself all the signals may as well make
|
---|
344 | # use of it.
|
---|
345 | reactor.addSystemEventTrigger("after", "shutdown", self.shutdown)
|
---|
346 |
|
---|
347 | # Initalize the plugins
|
---|
348 | self.plugins = PluginManager()
|
---|
349 |
|
---|
350 | def start(self):
|
---|
351 | log.info("%s %s.", _("Starting server in PID"), os.getpid())
|
---|
352 | reactor.listenTCP(self.port, self.site)
|
---|
353 | log.info("serving on %s:%s view at http://127.0.0.1:%s", "0.0.0.0",
|
---|
354 | self.port, self.port)
|
---|
355 | reactor.run()
|
---|
356 |
|
---|
357 | def shutdown(self):
|
---|
358 | log.info("Shutting down webserver")
|
---|
359 | log.debug("Saving configuration file")
|
---|
360 | self.config.save()
|
---|
361 |
|
---|
362 | if __name__ == "__builtin__":
|
---|
363 | deluge_web = DelugeWeb()
|
---|
364 | application = service.Application("DelugeWeb")
|
---|
365 | sc = service.IServiceCollection(application)
|
---|
366 | i = internet.TCPServer(deluge_web.port, deluge_web.site)
|
---|
367 | i.setServiceParent(sc)
|
---|
368 | elif __name__ == "__main__":
|
---|
369 | deluge_web = DelugeWeb()
|
---|
370 | deluge_web.start()
|
---|