mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Implement fastAPI prefix handling
Admins just need to set `galaxy_url_prefix`, the rest should just work.
This commit is contained in:
@@ -1885,6 +1885,17 @@
|
||||
:Type: str
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
``galaxy_url_prefix``
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:Description:
|
||||
URL prefix for Galaxy application. If Galaxy should be served
|
||||
under a prefix set this to the desired prefix value.
|
||||
:Default: ``/``
|
||||
:Type: str
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
``galaxy_infrastructure_url``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1946,7 +1957,7 @@
|
||||
|
||||
:Description:
|
||||
The brand image source.
|
||||
:Default: ``/static/favicon.png``
|
||||
:Default: ``static/favicon.png``
|
||||
:Type: str
|
||||
|
||||
|
||||
|
||||
@@ -993,6 +993,10 @@ galaxy:
|
||||
# user_preferences_extra_conf.yml
|
||||
#default_locale: auto
|
||||
|
||||
# URL prefix for Galaxy application. If Galaxy should be served under
|
||||
# a prefix set this to the desired prefix value.
|
||||
#galaxy_url_prefix: /
|
||||
|
||||
# URL (with schema http/https) of the Galaxy instance as accessible
|
||||
# within your local network - if specified used as a default by pulsar
|
||||
# file staging and Jupyter Docker container for communicating back
|
||||
@@ -1020,7 +1024,7 @@ galaxy:
|
||||
#logo_url: /
|
||||
|
||||
# The brand image source.
|
||||
#logo_src: /static/favicon.png
|
||||
#logo_src: static/favicon.png
|
||||
|
||||
# The custom brand image source.
|
||||
#logo_src_secondary: null
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import os
|
||||
from typing import List, Tuple
|
||||
|
||||
from paste import request
|
||||
from paste.fileapp import FileApp
|
||||
from paste.httpheaders import ETAG
|
||||
from paste.urlparser import StaticURLParser
|
||||
|
||||
|
||||
class CacheableStaticURLParser(StaticURLParser):
|
||||
|
||||
def __init__(self, directory, cache_seconds=None, directory_per_host=None):
|
||||
StaticURLParser.__init__(self, directory)
|
||||
self.cache_seconds = cache_seconds
|
||||
self.directory_per_host = directory_per_host
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
path_info = environ.get('PATH_INFO', '')
|
||||
if not path_info:
|
||||
# See if this is a static file hackishly mapped.
|
||||
if os.path.exists(self.directory) and os.path.isfile(self.directory):
|
||||
app = FileApp(self.directory)
|
||||
if self.cache_seconds:
|
||||
app.cache_control(max_age=int(self.cache_seconds))
|
||||
return app(environ, start_response)
|
||||
return self.add_slash(environ, start_response)
|
||||
if path_info == '/':
|
||||
# @@: This should obviously be configurable
|
||||
filename = 'index.html'
|
||||
else:
|
||||
filename = request.path_info_pop(environ)
|
||||
|
||||
directory = self.directory
|
||||
host = environ.get('HTTP_HOST')
|
||||
if self.directory_per_host and host:
|
||||
for host_key, host_val in self.directory_per_host.items():
|
||||
if host_key in host:
|
||||
directory = host_val
|
||||
break
|
||||
|
||||
full = os.path.join(directory, filename)
|
||||
if not os.path.exists(full):
|
||||
return self.not_found(environ, start_response)
|
||||
if os.path.isdir(full):
|
||||
# @@: Cache?
|
||||
return self.__class__(full)(environ, start_response)
|
||||
if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/':
|
||||
return self.error_extra_path(environ, start_response)
|
||||
if_none_match = environ.get('HTTP_IF_NONE_MATCH')
|
||||
if if_none_match:
|
||||
mytime = os.stat(full).st_mtime
|
||||
if str(mytime) == if_none_match:
|
||||
headers: List[Tuple[str, str]] = []
|
||||
ETAG.update(headers, mytime)
|
||||
start_response('304 Not Modified', headers)
|
||||
return [''] # empty body
|
||||
app = FileApp(full)
|
||||
if self.cache_seconds:
|
||||
app.cache_control(max_age=int(self.cache_seconds))
|
||||
return app(environ, start_response)
|
||||
|
||||
|
||||
def make_static(global_conf, document_root, cache_seconds=None):
|
||||
return CacheableStaticURLParser(document_root, cache_seconds)
|
||||
@@ -976,36 +976,3 @@ def build_native_uwsgi_app(paste_factory, config_section):
|
||||
app_kwds = get_app_kwds(config_section)
|
||||
uwsgi_app = paste_factory({}, load_app_kwds=app_kwds)
|
||||
return uwsgi_app
|
||||
|
||||
|
||||
def build_url_map(app, global_conf, local_conf):
|
||||
from paste.urlmap import URLMap
|
||||
from galaxy.web.framework.middleware.static import CacheableStaticURLParser as Static
|
||||
urlmap = URLMap()
|
||||
# Merge the global and local configurations
|
||||
conf = global_conf.copy()
|
||||
conf.update(local_conf)
|
||||
# Get cache time in seconds
|
||||
cache_time = conf.get("static_cache_time", None)
|
||||
if cache_time is not None:
|
||||
cache_time = int(cache_time)
|
||||
# Send to dynamic app by default
|
||||
urlmap["/"] = app
|
||||
|
||||
def get_static_from_config(option_name, default_path):
|
||||
config_val = conf.get(option_name, default_url_path(default_path))
|
||||
per_host_config_option = f"{option_name}_by_host"
|
||||
per_host_config = conf.get(per_host_config_option)
|
||||
return Static(config_val, cache_time, directory_per_host=per_host_config)
|
||||
|
||||
# Define static mappings from config
|
||||
urlmap["/static"] = get_static_from_config("static_dir", "static/")
|
||||
urlmap["/images"] = get_static_from_config("static_images_dir", "static/images")
|
||||
urlmap["/static/scripts"] = get_static_from_config("static_scripts_dir", "static/scripts/")
|
||||
urlmap["/static/welcome.html"] = get_static_from_config("static_welcome_html", "static/welcome.html")
|
||||
urlmap["/favicon.ico"] = get_static_from_config("static_favicon_dir", "static/favicon.ico")
|
||||
urlmap["/robots.txt"] = get_static_from_config("static_robots_txt", "static/robots.txt")
|
||||
|
||||
if 'static_local_dir' in conf:
|
||||
urlmap["/static_local"] = Static(conf["static_local_dir"], cache_time)
|
||||
return urlmap, cache_time
|
||||
|
||||
@@ -207,10 +207,6 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs):
|
||||
# Wrap the webapp in some useful middleware
|
||||
if kwargs.get('middleware', True):
|
||||
webapp = wrap_in_middleware(webapp, global_conf, app.application_stack, **kwargs)
|
||||
if asbool(kwargs.get('static_enabled', True)):
|
||||
webapp = wrap_if_allowed(webapp, app.application_stack, wrap_in_static,
|
||||
args=(global_conf,),
|
||||
kwargs=dict(plugin_frameworks=[app.visualizations_registry], **kwargs))
|
||||
app.application_stack.register_postfork_function(postfork_setup)
|
||||
|
||||
for th in threading.enumerate():
|
||||
@@ -1399,8 +1395,3 @@ def wrap_in_middleware(app, global_conf, application_stack, **local_conf):
|
||||
from galaxy.web.framework.middleware.sqldebug import SQLDebugMiddleware
|
||||
app = wrap_if_allowed(app, stack, SQLDebugMiddleware, args=(webapp, {}))
|
||||
return app
|
||||
|
||||
|
||||
def wrap_in_static(app, global_conf, plugin_frameworks=None, **local_conf):
|
||||
urlmap, cache_time = galaxy.webapps.base.webapp.build_url_map(app, global_conf, local_conf)
|
||||
return urlmap
|
||||
|
||||
@@ -1361,6 +1361,14 @@ mapping:
|
||||
Users can override this settings in their user preferences if the localization
|
||||
settings are enabled in user_preferences_extra_conf.yml
|
||||
|
||||
galaxy_url_prefix:
|
||||
type: str
|
||||
default: '/'
|
||||
required: false
|
||||
desc: |
|
||||
URL prefix for Galaxy application. If Galaxy should be served under a prefix set this to
|
||||
the desired prefix value.
|
||||
|
||||
galaxy_infrastructure_url:
|
||||
type: str
|
||||
default: http://localhost:8080
|
||||
@@ -1409,7 +1417,7 @@ mapping:
|
||||
|
||||
logo_src:
|
||||
type: str
|
||||
default: /static/favicon.png
|
||||
default: static/favicon.png
|
||||
required: false
|
||||
per_host: true
|
||||
desc: |
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import os
|
||||
import stat
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import anyio
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.wsgi import WSGIMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.datastructures import Headers
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import (
|
||||
FileResponse,
|
||||
RedirectResponse,
|
||||
Response,
|
||||
)
|
||||
from starlette.staticfiles import PathLike
|
||||
from starlette.types import Scope
|
||||
|
||||
from galaxy.webapps.base.api import (
|
||||
add_exception_handler,
|
||||
@@ -16,6 +26,8 @@ from galaxy.webapps.base.api import (
|
||||
)
|
||||
from galaxy.webapps.base.webapp import config_allows_origin
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from galaxy.config import Configuration
|
||||
|
||||
# https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-tags
|
||||
api_tags_metadata = [
|
||||
@@ -58,6 +70,90 @@ api_tags_metadata = [
|
||||
]
|
||||
|
||||
|
||||
def get_static_from_config(conf, option_name, default_path):
|
||||
config_val = conf.get(option_name, default_path)
|
||||
per_host_config_option = f"{option_name}_by_host"
|
||||
per_host_config = conf.get(per_host_config_option)
|
||||
return (config_val, per_host_config or {})
|
||||
|
||||
|
||||
class GalaxyStaticFiles(StaticFiles):
|
||||
def __init__(
|
||||
self, *, config: "Configuration", html: bool = False, check_dir: bool = True
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.directory, self.directory_per_host = get_static_from_config(
|
||||
config, "static_dir", "static/"
|
||||
)
|
||||
super().__init__(
|
||||
directory=self.directory, packages=None, html=html, check_dir=check_dir
|
||||
)
|
||||
|
||||
def get_directories(
|
||||
self, directory: PathLike = None, packages: typing.List[str] = None
|
||||
) -> typing.List[PathLike]:
|
||||
pass
|
||||
|
||||
async def check_config(self) -> None:
|
||||
"""
|
||||
Perform a one-off configuration check that GalaxyStaticFiles is actually
|
||||
pointed at directories, so that we can raise loud errors rather than
|
||||
just returning 404 responses.
|
||||
"""
|
||||
for directory in (self.directory, *self.directory_per_host.values()):
|
||||
try:
|
||||
stat_result = await anyio.to_thread.run_sync(os.stat, directory)
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError(
|
||||
f"StaticFiles directory '{directory}' does not exist."
|
||||
)
|
||||
if not (
|
||||
stat.S_ISDIR(stat_result.st_mode) or stat.S_ISLNK(stat_result.st_mode)
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"StaticFiles path '{directory}' is not a directory."
|
||||
)
|
||||
|
||||
async def get_response(self, path: str, scope: Scope) -> Response:
|
||||
"""
|
||||
Returns an HTTP response, given the incoming path, method and request headers.
|
||||
"""
|
||||
if scope["method"] not in ("GET", "HEAD"):
|
||||
raise HTTPException(status_code=405)
|
||||
|
||||
headers = Headers(scope=scope)
|
||||
|
||||
try:
|
||||
full_path, stat_result = await anyio.to_thread.run_sync(
|
||||
self.lookup_path, path, headers.get("host")
|
||||
)
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=401)
|
||||
except OSError:
|
||||
raise
|
||||
|
||||
if stat_result and stat.S_ISREG(stat_result.st_mode):
|
||||
# We have a static file to serve.
|
||||
return self.file_response(full_path, stat_result, scope)
|
||||
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
def lookup_path(
|
||||
self, path: str, host: typing.Optional[str] = None
|
||||
) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
|
||||
directory = self.directory_per_host.get(host, self.directory)
|
||||
full_path = os.path.realpath(os.path.join(directory, path))
|
||||
directory = os.path.realpath(directory)
|
||||
if os.path.commonprefix([full_path, directory]) != directory:
|
||||
# Don't allow misbehaving clients to break out of the static files
|
||||
# directory.
|
||||
return "", None
|
||||
try:
|
||||
return full_path, os.stat(full_path)
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
return "", None
|
||||
|
||||
|
||||
class GalaxyCORSMiddleware(CORSMiddleware):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
@@ -129,6 +225,16 @@ def initialize_fast_app(gx_wsgi_webapp, gx_app):
|
||||
add_galaxy_middleware(app, gx_app)
|
||||
add_request_id_middleware(app)
|
||||
include_all_package_routers(app, 'galaxy.webapps.galaxy.api')
|
||||
app.mount("/static", GalaxyStaticFiles(config=gx_app.config), name="static")
|
||||
|
||||
@app.get("/favicon.ico")
|
||||
async def favicon():
|
||||
return RedirectResponse(url="static/favicon.ico")
|
||||
|
||||
wsgi_handler = WSGIMiddleware(gx_wsgi_webapp)
|
||||
app.mount('/', wsgi_handler)
|
||||
if gx_app.config.galaxy_url_prefix != '/':
|
||||
parent_app = FastAPI()
|
||||
parent_app.mount(gx_app.config.galaxy_url_prefix, app=app)
|
||||
return parent_app
|
||||
return app
|
||||
|
||||
@@ -68,10 +68,6 @@ def app_factory(global_conf, load_app_kwds=None, **kwargs):
|
||||
# Wrap the webapp in some useful middleware
|
||||
if kwargs.get('middleware', True):
|
||||
webapp = wrap_in_middleware(webapp, global_conf, app.application_stack, **kwargs)
|
||||
if asbool(kwargs.get('static_enabled', True)):
|
||||
webapp = wrap_if_allowed(webapp, app.application_stack, wrap_in_static,
|
||||
args=(global_conf,),
|
||||
kwargs=kwargs)
|
||||
return webapp
|
||||
|
||||
|
||||
@@ -121,10 +117,5 @@ def wrap_in_middleware(app, global_conf, application_stack, **local_conf):
|
||||
return app
|
||||
|
||||
|
||||
def wrap_in_static(app, global_conf, **local_conf):
|
||||
urlmap, _ = galaxy.webapps.base.webapp.build_url_map(app, global_conf, local_conf)
|
||||
return urlmap
|
||||
|
||||
|
||||
def uwsgi_app():
|
||||
return galaxy.webapps.base.webapp.build_native_uwsgi_app(app_factory, "reports")
|
||||
|
||||
@@ -190,10 +190,6 @@ def app_factory(global_conf, load_app_kwds=None, **kwargs):
|
||||
# Wrap the webapp in some useful middleware
|
||||
if kwargs.get('middleware', True):
|
||||
webapp = wrap_in_middleware(webapp, global_conf, app.application_stack, **kwargs)
|
||||
if asbool(kwargs.get('static_enabled', True)):
|
||||
webapp = wrap_if_allowed(webapp, app.application_stack, wrap_in_static,
|
||||
args=(global_conf,),
|
||||
kwargs=kwargs)
|
||||
return webapp
|
||||
|
||||
|
||||
@@ -263,11 +259,6 @@ def wrap_in_middleware(app, global_conf, application_stack, **local_conf):
|
||||
return app
|
||||
|
||||
|
||||
def wrap_in_static(app, global_conf, **local_conf):
|
||||
urlmap, _ = galaxy.webapps.base.webapp.build_url_map(app, global_conf, local_conf)
|
||||
return urlmap
|
||||
|
||||
|
||||
def _map_redirects(mapper):
|
||||
"""
|
||||
Add redirect to the Routes mapper and forward the received query string.
|
||||
|
||||
Reference in New Issue
Block a user