Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ComponentResourceHandler to server #3284

Merged
merged 13 commits into from
Mar 31, 2022
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ include panel/models/vtk/*.ts
include panel/_templates/*.css
include panel/_templates/*.js
include panel/_templates/*.html
include panel/tests/assets/*.css
include panel/tests/assets/*.js
include panel/tests/test_data/*.png
include panel/tests/pane/assets/*.mp3
include panel/tests/pane/assets/*.mp4
Expand Down
5 changes: 2 additions & 3 deletions panel/command/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,8 @@ def customize_kwargs(self, args, server_kwargs):
# Handle tranquilized functions in the supplied functions
kwargs['extra_patterns'] = patterns = kwargs.get('extra_patterns', [])

if args.static_dirs:
static_dirs = parse_vars(args.static_dirs)
patterns += get_static_routes(static_dirs)
static_dirs = parse_vars(args.static_dirs) if args.static_dirs else {}
patterns += get_static_routes(static_dirs)

files = []
for f in args.files:
Expand Down
56 changes: 33 additions & 23 deletions panel/io/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
import copy
import glob
import importlib
import json
import os

Expand All @@ -25,7 +26,7 @@
from jinja2.environment import Environment
from jinja2.loaders import FileSystemLoader

from ..util import url_path
from ..util import isurl, url_path
from .state import state


Expand Down Expand Up @@ -83,6 +84,19 @@ def set_resource_mode(mode):
RESOURCE_MODE = old_mode
_settings.resources.set_value(old_resources)

def resolve_custom_path(obj, path):
"""
Attempts to resolve a path relative to some component.
"""
if not path:
return
elif path.startswith(os.path.sep):
return os.path.isfile(path)
try:
mod = importlib.import_module(obj.__module__)
return (Path(mod.__file__).parent / path).is_file()
except Exception:
return None

def loading_css():
from ..config import config
Expand All @@ -96,7 +110,6 @@ def loading_css():
}}
"""


def bundled_files(model, file_type='javascript'):
bdir = os.path.join(PANEL_DIR, 'dist', 'bundled', model.__name__.lower())
name = model.__name__.lower()
Expand All @@ -111,7 +124,6 @@ def bundled_files(model, file_type='javascript'):
files.append(url)
return files


def bundle_resources(roots, resources):
from ..config import panel_extension as ext
global RESOURCE_MODE
Expand Down Expand Up @@ -177,6 +189,20 @@ def from_bokeh(cls, bkr):
root_dir=bkr.root_dir, **kwargs
)

def extra_resources(self, resources, resource_type):
from ..reactive import ReactiveHTML
custom_path = "components"
if state.rel_path:
custom_path = f"{state.rel_path}/{custom_path}"
for model in param.concrete_descendents(ReactiveHTML).values():
if not (getattr(model, resource_type, None) and model._loaded()):
continue
for resource in getattr(model, resource_type, []):
if not isurl(resource) and not resource.startswith('static/extensions'):
resource = f'{custom_path}/{model.__module__}/{model.__name__}/{resource_type}/{resource}'
if resource not in resources:
resources.append(resource)

@property
def css_raw(self):
from ..config import config
Expand All @@ -203,15 +229,9 @@ def css_raw(self):
@property
def js_files(self):
from ..config import config
from ..reactive import ReactiveHTML

files = super(Resources, self).js_files

for model in param.concrete_descendents(ReactiveHTML).values():
if getattr(model, '__javascript__', None) and model._loaded():
for jsfile in model.__javascript__:
if jsfile not in files:
files.append(jsfile)
self.extra_resources(files, '__javascript__')

js_files = []
for js_file in files:
Expand Down Expand Up @@ -244,27 +264,16 @@ def js_files(self):
@property
def js_modules(self):
from ..config import config
from ..reactive import ReactiveHTML
modules = list(config.js_modules.values())
for model in param.concrete_descendents(ReactiveHTML).values():
if hasattr(model, '__javascript_modules__') and model._loaded():
for jsmodule in model.__javascript_modules__:
if jsmodule not in modules:
modules.append(jsmodule)
self.extra_resources(modules, '__javascript_modules__')
return modules

@property
def css_files(self):
from ..config import config
from ..reactive import ReactiveHTML

files = super(Resources, self).css_files

for model in param.concrete_descendents(ReactiveHTML).values():
if getattr(model, '__css__', None) and model._loaded():
for css_file in model.__css__:
if css_file not in files:
files.append(css_file)
self.extra_resources(files, '__css__')

for cssf in config.css_files:
if os.path.isfile(cssf) or cssf in files:
Expand All @@ -277,6 +286,7 @@ def css_files(self):
dist_dir = LOCAL_DIST
else:
dist_dir = CDN_DIST

for cssf in glob.glob(str(DIST_DIR / 'css' / '*.css')):
if self.mode == 'inline':
break
Expand Down
88 changes: 87 additions & 1 deletion panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import datetime as dt
import gc
import html
import importlib
import logging
import os
import pathlib
Expand Down Expand Up @@ -41,7 +42,7 @@

# Tornado imports
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, StaticFileHandler, authenticated
from tornado.web import HTTPError, RequestHandler, StaticFileHandler, authenticated
from tornado.wsgi import WSGIContainer

# Internal imports
Expand Down Expand Up @@ -292,6 +293,88 @@ async def get(self, *args, **kwargs):

per_app_patterns[3] = (r'/autoload.js', AutoloadJsHandler)


class ComponentResourceHandler(StaticFileHandler):
"""
A handler that serves local resources relative to a Python module.
The handler resolves a specific Panel component by module reference
and name, then resolves an attribute on that component to check
if it contains the requested resource path.

/<endpoint>/<module>/<class>/<attribute>/<path>
"""

_resource_attrs = [
'__css__', '__javascript__', '__js_module__', '_resources',
'_css', '_js', 'base_css', 'css'
]

def initialize(self, path=None, default_filename=None):
self.root = path
self.default_filename = default_filename

def parse_url_path(self, path):
"""
Resolves the resource the URL pattern refers to.
"""
parts = path.split('/')
if len(parts) < 4:
raise HTTPError(400, 'Malformed URL')
module, cls, rtype, *subpath = parts
try:
module = importlib.import_module(module)
except ModuleNotFoundError:
raise HTTPError(404, 'Module not found')
try:
component = getattr(module, cls)
except AttributeError:
raise HTTPError(404, 'Component not found')

# May only access resources listed in specific attributes
if rtype not in self._resource_attrs:
raise HTTPError(403, 'Requested resource type not valid.')

try:
resources = getattr(component, rtype)
except AttributeError:
raise HTTPError(404, 'Resource type not found')

# Handle template resources
if rtype == '_resources':
rtype = subpath[0]
subpath = subpath[1:]
if rtype in resources:
resources = resources[rtype]
else:
raise HTTPError(404, 'Resource type not found')

if isinstance(resources, dict):
resources = list(resources.values())

if subpath[0] == '':
subpath = tuple('/')+subpath[1:]
path = '/'.join(subpath)

# Important: May only access resources explicitly listed on the component
# Otherwise this potentially exposes all files to the web
if path not in resources and f'./{path}' not in resources:
raise HTTPError(403, 'Requested resource was not listed.')

if not path.startswith('/'):
path = pathlib.Path(module.__file__).parent / path
return path

def get_absolute_path(self, root, path):
return path

def validate_absolute_path(self, root, absolute_path):
if not os.path.exists(absolute_path):
raise HTTPError(404)
if not os.path.isfile(absolute_path):
raise HTTPError(403, "%s is not a file", self.path)
return absolute_path


def modify_document(self, doc):
from bokeh.io.doc import set_curdoc as bk_set_curdoc
from ..config import config
Expand Down Expand Up @@ -505,6 +588,9 @@ def get_static_routes(static_dirs):
patterns.append(
(r"%s/(.*)" % slug, StaticFileHandler, {"path": path})
)
patterns.append((
'/components/(.*)', ComponentResourceHandler, {}
))
return patterns


Expand Down
Loading