From 7a0ea38fb19e541ca3016420f322c9c55873536c Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 24 Mar 2020 14:41:09 -0700 Subject: [PATCH] web_server: Introduce component to handle special static file dirs Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- doc/dev/reference/components/index.rst | 1 + doc/dev/reference/components/staticfiles.rst | 7 +++ plinth/tests/test_web_server.py | 63 ++++++++++++++++++++ plinth/web_server.py | 56 ++++++++++++++++- 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 doc/dev/reference/components/staticfiles.rst create mode 100644 plinth/tests/test_web_server.py diff --git a/doc/dev/reference/components/index.rst b/doc/dev/reference/components/index.rst index 3e92003b7..3d0d33de5 100644 --- a/doc/dev/reference/components/index.rst +++ b/doc/dev/reference/components/index.rst @@ -14,6 +14,7 @@ Components frontpage domain letsencrypt + staticfiles Base Classes ^^^^^^^^^^^^ diff --git a/doc/dev/reference/components/staticfiles.rst b/doc/dev/reference/components/staticfiles.rst new file mode 100644 index 000000000..92421f7d7 --- /dev/null +++ b/doc/dev/reference/components/staticfiles.rst @@ -0,0 +1,7 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +StaticFiles +^^^^^^^^^^^ + +.. autoclass:: plinth.web_server.StaticFiles + :members: diff --git a/plinth/tests/test_web_server.py b/plinth/tests/test_web_server.py new file mode 100644 index 000000000..e6e5d5237 --- /dev/null +++ b/plinth/tests/test_web_server.py @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Tests for CherryPy web server setup and its components. +""" + +from unittest.mock import call, patch + +import pytest + +from plinth.web_server import StaticFiles + + +@pytest.fixture(autouse=True) +def fixture_cleanup_static_files(): + """Ensure that global list of static files is clean.""" + StaticFiles._all_instances = {} + + +def test_static_files_init(): + """Test that static files component is being initialized correctly.""" + component = StaticFiles('test-component') + assert component.component_id == 'test-component' + assert component.directory_map is None + + directory_map = {'/a': '/b'} + component = StaticFiles('test-component', directory_map) + assert component.directory_map == directory_map + + +def test_static_files_list(): + """Test that static files components can be listed properly.""" + component1 = StaticFiles('test-component1') + component2 = StaticFiles('test-component2') + + assert set(StaticFiles.list()) == {component1, component2} + + +@patch('cherrypy.tree.mount') +def test_static_files_mount(mount, load_cfg): + """Test that mounting on CherryPy works as expected.""" + directory_map = {'/a': '/b', '/c': '/d'} + component = StaticFiles('test-component', directory_map) + component.mount() + + calls = [ + call( + None, '/plinth/a', { + '/': { + 'tools.staticdir.root': '/b', + 'tools.staticdir.on': True, + 'tools.staticdir.dir': '.' + } + }), + call( + None, '/plinth/c', { + '/': { + 'tools.staticdir.root': '/d', + 'tools.staticdir.on': True, + 'tools.staticdir.dir': '.' + } + }) + ] + mount.assert_has_calls(calls) diff --git a/plinth/web_server.py b/plinth/web_server.py index 27d86ab15..59994af93 100644 --- a/plinth/web_server.py +++ b/plinth/web_server.py @@ -8,7 +8,7 @@ import os import cherrypy -from . import cfg, log, module_loader, web_framework +from . import app, cfg, log, module_loader, web_framework logger = logging.getLogger(__name__) @@ -72,6 +72,9 @@ def init(): urlprefix = "%s%s" % (web_framework.get_static_url(), module_name) _mount_static_directory(static_dir, urlprefix) + for component in StaticFiles.list(): + component.mount() + cherrypy.engine.signal_handler.subscribe() @@ -80,3 +83,54 @@ def run(on_web_server_stop): cherrypy.engine.start() cherrypy.engine.subscribe('stop', on_web_server_stop) cherrypy.engine.block() + + +class StaticFiles(app.FollowerComponent): + """Component to serve static files shipped with an app. + + Any files in /static directory will be automatically served on + /static// directory by FreedomBox. This allows each app to ship custom + static files that are served by the web server. + + However, in some rare circumstances, a system folder will need to be served + on a path for the app to work. This component allows declaring such + directories and the web paths they should be served on. + + """ + + _all_instances = {} + + def __init__(self, component_id, directory_map=None): + """Initialize the component. + + component_id should be a unique ID across all components of an app and + across all components. + + directory_map should be a dictionary with keys to be web paths and + values to be absolute path of the directory on disk to serve. The + static files from the directory are served over the given web path. The + web path will be prepended with the FreedomBox's configured base web + path. For example, {'/foo': '/usr/share/foo'} means that + '/usr/share/foo/bar.png' will be served over '/plinth/foo/bar.png' if + FreedomBox is configured to be served on '/plinth'. + + """ + super().__init__(component_id) + self.directory_map = directory_map + self._all_instances[component_id] = self + + @classmethod + def list(cls): + """Return a list of all instances.""" + return cls._all_instances.values() + + def mount(self): + """Perform configuration of the web server to handle static files. + + Called by web server abstraction layer after web server has been setup. + + """ + if self.directory_map: + for web_path, file_path in self.directory_map.items(): + web_path = '%s%s' % (cfg.server_dir, web_path) + _mount_static_directory(file_path, web_path)