2727from ._connection import Connection , StarletteConnection
2828from ._error import ErrorMiddleware
2929from ._shinyenv import is_pyodide
30- from ._utils import is_async_callable
30+ from ._utils import guess_mime_type , is_async_callable
3131from .html_dependencies import jquery_deps , require_deps , shiny_deps
32- from .http_staticfiles import StaticFiles
32+ from .http_staticfiles import FileResponse , StaticFiles
3333from .session import Inputs , Outputs , Session , session_context
3434
3535# Default values for App options.
@@ -54,7 +54,10 @@ class App:
5454 A function which is called once for each session, ensuring that each app is
5555 independent.
5656 static_assets
57- An absolute directory containing static files to be served by the app.
57+ Static files to be served by the app. If this is a string or Path object, it
58+ must be a directory, and it will be mounted at `/`. If this is a dictionary,
59+ each key is a mount point and each value is a file or directory to be served at
60+ that mount point.
5861 debug
5962 Whether to enable debug mode.
6063
@@ -100,7 +103,7 @@ def __init__(
100103 ui : Tag | TagList | Callable [[Request ], Tag | TagList ] | Path ,
101104 server : Optional [Callable [[Inputs , Outputs , Session ], None ]],
102105 * ,
103- static_assets : Optional ["str" | "os.PathLike[str]" ] = None ,
106+ static_assets : Optional ["str" | "os.PathLike[str]" | dict [ str , Path ] ] = None ,
104107 debug : bool = False ,
105108 ) -> None :
106109 if server is None :
@@ -119,15 +122,16 @@ def _server(inputs: Inputs, outputs: Outputs, session: Session):
119122 self .sanitize_errors : bool = SANITIZE_ERRORS
120123 self .sanitize_error_msg : str = SANITIZE_ERROR_MSG
121124
122- if static_assets is not None :
123- if not os . path . isdir ( static_assets ):
124- raise ValueError ( f" static_assets must be a directory: { static_assets } " )
125+ if static_assets is None :
126+ static_assets = {}
127+ if isinstance ( static_assets , ( str , os . PathLike )):
125128 if not os .path .isabs (static_assets ):
126129 raise ValueError (
127130 f"static_assets must be an absolute path: { static_assets } "
128131 )
132+ static_assets = {"/" : Path (static_assets )}
129133
130- self ._static_assets : str | os . PathLike [str ] | None = static_assets
134+ self ._static_assets : dict [str , Path ] = static_assets
131135
132136 self ._sessions : dict [str , Session ] = {}
133137
@@ -136,13 +140,9 @@ def _server(inputs: Inputs, outputs: Outputs, session: Session):
136140 self ._registered_dependencies : dict [str , HTMLDependency ] = {}
137141 self ._dependency_handler = starlette .routing .Router ()
138142
139- if self . _static_assets is not None :
143+ for mount_point , static_asset_path in self . _static_assets . items () :
140144 self ._dependency_handler .routes .append (
141- starlette .routing .Mount (
142- "/" ,
143- StaticFiles (directory = self ._static_assets ),
144- name = "shiny-app-static-assets-directory" ,
145- )
145+ create_static_asset_route (mount_point , static_asset_path )
146146 )
147147
148148 starlette_app = self .init_starlette_app ()
@@ -414,3 +414,35 @@ def is_uifunc(x: Path | Tag | TagList | Callable[[Request], Tag | TagList]):
414414
415415def html_dep_name (dep : HTMLDependency ) -> str :
416416 return dep .name + "-" + str (dep .version )
417+
418+
419+ def create_static_asset_route (
420+ mount_point : str , static_asset_path : Path
421+ ) -> starlette .routing .BaseRoute :
422+ """
423+ Create a Starlette route for serving static assets.
424+
425+ Parameters
426+ ----------
427+ mount_point
428+ The mount point where the static assets will be served.
429+ static_asset_path
430+ The path on disk to the static assets.
431+ """
432+ if static_asset_path .is_dir ():
433+ return starlette .routing .Mount (
434+ mount_point ,
435+ StaticFiles (directory = static_asset_path ),
436+ name = "shiny-app-static-assets-" + mount_point ,
437+ )
438+ else :
439+ mime_type = guess_mime_type (static_asset_path , strict = False )
440+
441+ def file_response_handler (req : Request ) -> FileResponse :
442+ return FileResponse (static_asset_path , media_type = mime_type )
443+
444+ return starlette .routing .Route (
445+ mount_point ,
446+ file_response_handler ,
447+ name = "shiny-app-static-assets-" + mount_point ,
448+ )
0 commit comments