27
27
from ._connection import Connection , StarletteConnection
28
28
from ._error import ErrorMiddleware
29
29
from ._shinyenv import is_pyodide
30
- from ._utils import is_async_callable
30
+ from ._utils import guess_mime_type , is_async_callable
31
31
from .html_dependencies import jquery_deps , require_deps , shiny_deps
32
- from .http_staticfiles import StaticFiles
32
+ from .http_staticfiles import FileResponse , StaticFiles
33
33
from .session import Inputs , Outputs , Session , session_context
34
34
35
35
# Default values for App options.
@@ -54,7 +54,10 @@ class App:
54
54
A function which is called once for each session, ensuring that each app is
55
55
independent.
56
56
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.
58
61
debug
59
62
Whether to enable debug mode.
60
63
@@ -100,7 +103,7 @@ def __init__(
100
103
ui : Tag | TagList | Callable [[Request ], Tag | TagList ] | Path ,
101
104
server : Optional [Callable [[Inputs , Outputs , Session ], None ]],
102
105
* ,
103
- static_assets : Optional ["str" | "os.PathLike[str]" ] = None ,
106
+ static_assets : Optional ["str" | "os.PathLike[str]" | dict [ str , Path ] ] = None ,
104
107
debug : bool = False ,
105
108
) -> None :
106
109
if server is None :
@@ -119,15 +122,16 @@ def _server(inputs: Inputs, outputs: Outputs, session: Session):
119
122
self .sanitize_errors : bool = SANITIZE_ERRORS
120
123
self .sanitize_error_msg : str = SANITIZE_ERROR_MSG
121
124
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 )):
125
128
if not os .path .isabs (static_assets ):
126
129
raise ValueError (
127
130
f"static_assets must be an absolute path: { static_assets } "
128
131
)
132
+ static_assets = {"/" : Path (static_assets )}
129
133
130
- self ._static_assets : str | os . PathLike [str ] | None = static_assets
134
+ self ._static_assets : dict [str , Path ] = static_assets
131
135
132
136
self ._sessions : dict [str , Session ] = {}
133
137
@@ -136,13 +140,9 @@ def _server(inputs: Inputs, outputs: Outputs, session: Session):
136
140
self ._registered_dependencies : dict [str , HTMLDependency ] = {}
137
141
self ._dependency_handler = starlette .routing .Router ()
138
142
139
- if self . _static_assets is not None :
143
+ for mount_point , static_asset_path in self . _static_assets . items () :
140
144
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 )
146
146
)
147
147
148
148
starlette_app = self .init_starlette_app ()
@@ -414,3 +414,35 @@ def is_uifunc(x: Path | Tag | TagList | Callable[[Request], Tag | TagList]):
414
414
415
415
def html_dep_name (dep : HTMLDependency ) -> str :
416
416
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