diff --git a/docs/api/http-server.rst b/docs/api/http-server.rst index 8a56f2d5..67c1d138 100644 --- a/docs/api/http-server.rst +++ b/docs/api/http-server.rst @@ -12,11 +12,14 @@ The HTTP server side API can be used to: - host static files for e.g. a Mopidy client written in pure JavaScript, - host a `Tornado `__ application, or -- host a WSGI application. +- host a WSGI application, including e.g. Flask applications. -To extend the web server, an extension needs to create a subclass of -:class:`mopidy.http.Router` and register the subclass with the extension -registry under the ``http:router`` key. +To host static files using the web server, an extension needs to register a +name and a file path in the extension registry under the ``http:static`` key. + +To extend the web server with a web application, an extension must create a +subclass of :class:`mopidy.http.Router` and register the subclass with the +extension registry under the ``http:router`` key. For details on how to make a Mopidy extension, see the :ref:`extensiondev` guide. @@ -25,14 +28,12 @@ guide. Static web client example ========================= -To serve static files, you just need to declare a -:attr:`~mopidy.http.Router.name` for your router and tell where the static -files are located by setting the :attr:`~mopidy.http.Router.static_file_path` -attribute on your router class. - -The :attr:`~mopidy.http.Router.name` attribute is used to build the URL to -make the files available on. By convention, it should be identical with the -extension's :attr:`~mopidy.ext.Extension.ext_name`, like in the examples here. +To serve static files, you just need to register an ``http:static`` dictionary +in the extension registry. The dictionary must have two keys: ``name`` and +``path``. The ``name`` is used to build the URL the static files will be +served on. By convention, it should be identical with the extension's +:attr:`~mopidy.ext.Extension.ext_name`, like in the following example. The +``path`` tells Mopidy where on the disk the static files are located. Assuming that the code below is located in the file :file:`mywebclient/__init__.py`, the files in the directory @@ -46,19 +47,17 @@ available at http://localhost:6680/mywebclient/foo.html. import os - from mopidy import ext, http - - - class MyStaticFilesRouter(http.Router): - name = 'mywebclient' - static_file_path = os.path.join(os.path.dirname(__file__), 'static') + from mopidy import ext class MyWebClientExtension(ext.Extension): ext_name = 'mywebclient' def setup(self, registry): - registry.add('http:router', MyStaticFilesRouter) + registry.add('http:static', { + 'name': 'mywebclient', + 'path': os.path.join(os.path.dirname(__file__), 'static'), + }) # See the Extension API for the full details on this class @@ -72,16 +71,18 @@ for Tornado request handlers. In the following example, we create a :class:`tornado.web.RequestHandler` called :class:`MyRequestHandler` that responds to HTTP GET requests with the -string ``Hello, world! This is Mopidy $version``. The router registers this -Tornado request handler on the root URL, ``/``. The URLs returned from +string ``Hello, world! This is Mopidy $version``. + +Then a :class:`~mopidy.http.Router` subclass registers this Tornado request +handler on the root URL, ``/``. The URLs returned from :meth:`~mopidy.http.Router.get_request_handlers` are combined with the :attr:`~mopidy.http.Router.name`` attribute of the router, so the full absolute URL for the request handler becomes ``/mywebclient/``. The router is added to the extension registry by -:meth:`MyWebClientExtension.setup`. When the extension is installed, Mopidy -will respond to requests to http://localhost:6680/mywebclient/ with the string -``Hello, world!``. +:meth:`MyWebClientExtension.setup` under the key ``http:router``. When the +extension is installed, Mopidy will respond to requests to +http://localhost:6680/mywebclient/ with the string ``Hello, world!``. :: diff --git a/mopidy/http/__init__.py b/mopidy/http/__init__.py index a465fb64..565a7d6a 100644 --- a/mopidy/http/__init__.py +++ b/mopidy/http/__init__.py @@ -38,6 +38,7 @@ class Extension(ext.Extension): from .handlers import MopidyHttpRouter HttpFrontend.routers = registry['http:router'] + HttpFrontend.statics = registry['http:static'] registry.add('frontend', HttpFrontend) registry.add('http:router', MopidyHttpRouter) diff --git a/mopidy/http/actor.py b/mopidy/http/actor.py index 77dc5599..2243fe9d 100644 --- a/mopidy/http/actor.py +++ b/mopidy/http/actor.py @@ -21,6 +21,7 @@ logger = logging.getLogger(__name__) class HttpFrontend(pykka.ThreadingActor, CoreListener): routers = [] + statics = [] def __init__(self, config, core): super(HttpFrontend, self).__init__() @@ -63,7 +64,8 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): def _get_request_handlers(self): request_handlers = [] - request_handlers.extend(self._get_extension_request_handlers()) + request_handlers.extend(self._get_router_request_handlers()) + request_handlers.extend(self._get_static_request_handlers()) # Either default Mopidy or user defined path to files static_dir = self.config['http']['static_dir'] @@ -79,7 +81,7 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): list((l[0], l[1]) for l in request_handlers)) return request_handlers - def _get_extension_request_handlers(self): + def _get_router_request_handlers(self): result = [] for router_class in self.routers: router = router_class(self.config, self.core) @@ -88,7 +90,21 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener): handler = list(handler) handler[0] = '/%s%s' % (router.name, handler[0]) result.append(tuple(handler)) - logger.info('Loaded HTTP extension: %s', router_class.__name__) + logger.debug('Loaded HTTP extension: %s', router.name) + return result + + def _get_static_request_handlers(self): + result = [] + for static in self.statics: + result.append(( + r'/%s/(.*)' % static['name'], + handlers.StaticFileHandler, + { + 'path': static['path'], + 'default_filename': 'index.html' + } + )) + logger.debug('Loaded HTTP extension: %s', static['name']) return result def _publish_zeroconf(self): diff --git a/tests/http/test_handlers.py b/tests/http/test_handlers.py index be7d67e5..8b918e41 100644 --- a/tests/http/test_handlers.py +++ b/tests/http/test_handlers.py @@ -21,6 +21,7 @@ class StaticFileHandlerTest(tornado.testing.AsyncHTTPTestCase): def test_static_handler(self): response = self.fetch('/test_router.py', method='GET') + self.assertEqual(200, response.code) self.assertEqual( response.headers['X-Mopidy-Version'], mopidy.__version__) self.assertEqual( @@ -29,6 +30,7 @@ class StaticFileHandlerTest(tornado.testing.AsyncHTTPTestCase): def test_static_default_filename(self): response = self.fetch('/', method='GET') + self.assertEqual(200, response.code) self.assertEqual( response.headers['X-Mopidy-Version'], mopidy.__version__) self.assertEqual( diff --git a/tests/http/test_server.py b/tests/http/test_server.py index bf89da00..8afe8759 100644 --- a/tests/http/test_server.py +++ b/tests/http/test_server.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import os + import mock import tornado.testing @@ -117,6 +119,35 @@ class HttpServerTest(tornado.testing.AsyncHTTPTestCase): self.assertIn('Content-Type', response.headers) +class HttpServerWithStaticFilesTest(tornado.testing.AsyncHTTPTestCase): + def get_app(self): + config = { + 'http': { + 'hostname': '127.0.0.1', + 'port': 6680, + 'static_dir': None, + 'zeroconf': '', + } + } + core = mock.Mock() + + http_frontend = actor.HttpFrontend(config=config, core=core) + http_frontend.statics = [ + dict(name='static', path=os.path.dirname(__file__)), + ] + + return tornado.web.Application(http_frontend._get_request_handlers()) + + def test_can_serve_static_files(self): + response = self.fetch('/static/test_server.py', method='GET') + + self.assertEqual(200, response.code) + self.assertEqual( + response.headers['X-Mopidy-Version'], mopidy.__version__) + self.assertEqual( + response.headers['Cache-Control'], 'no-cache') + + class WsgiAppRouter(http.Router): name = 'wsgi'