web_server: Introduce component to handle special static file dirs

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Veiko Aasa <veiko17@disroot.org>
This commit is contained in:
Sunil Mohan Adapa 2020-03-24 14:41:09 -07:00 committed by Veiko Aasa
parent a0f6b0ea4a
commit 7a0ea38fb1
No known key found for this signature in database
GPG Key ID: 478539CAE680674E
4 changed files with 126 additions and 1 deletions

View File

@ -14,6 +14,7 @@ Components
frontpage
domain
letsencrypt
staticfiles
Base Classes
^^^^^^^^^^^^

View File

@ -0,0 +1,7 @@
.. SPDX-License-Identifier: CC-BY-SA-4.0
StaticFiles
^^^^^^^^^^^
.. autoclass:: plinth.web_server.StaticFiles
:members:

View File

@ -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)

View File

@ -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 <app>/static directory will be automatically served on
/static/<app>/ 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)