@@ -23,6 +23,7 @@ registerClient <- function(client) {
23
23
24
24
25
25
.globals $ resourcePaths <- list ()
26
+ .globals $ resources <- list ()
26
27
27
28
.globals $ showcaseDefault <- 0
28
29
@@ -69,8 +70,62 @@ addResourcePath <- function(prefix, directoryPath) {
69
70
getShinyOption(" server" )$ setStaticPath(.list = stats :: setNames(normalizedPath , prefix ))
70
71
}
71
72
72
- # .globals$resourcePaths persists across runs of applications.
73
+ # .globals$resourcePaths and .globals$resources persist across runs of applications.
73
74
.globals $ resourcePaths [[prefix ]] <- staticPath(normalizedPath )
75
+ # This is necessary because resourcePaths is only for serving assets out of C++;
76
+ # to support subapps, we also need assets to be served out of R, because those
77
+ # URLs are rewritten by R code (i.e. routeHandler) before they can be matched to
78
+ # a resource path.
79
+ .globals $ resources [[prefix ]] <- list (
80
+ directoryPath = normalizedPath ,
81
+ func = staticHandler(normalizedPath )
82
+ )
83
+ }
84
+
85
+ # This function handles any GET request with two or more path elements where the
86
+ # first path element matches a prefix that was previously added using
87
+ # addResourcePath().
88
+ #
89
+ # For example, if `addResourcePath("foo", "~/bar")` was called, then a GET
90
+ # request for /foo/one/two.html would rewrite the PATH_INFO as /one/two.html and
91
+ # send it to the resource path function for "foo". As of this writing, that
92
+ # function will always be a staticHandler, which serves up a file if it exists
93
+ # and NULL if it does not.
94
+ #
95
+ # Since Shiny 1.3.x, assets registered via addResourcePath should mostly be
96
+ # served out of httpuv's native static file serving features. However, in the
97
+ # specific case of subapps, the R code path must be used, because subapps insert
98
+ # a giant random ID into the beginning of the URL that must be stripped off by
99
+ # an R route handler (see addSubApp()).
100
+ resourcePathHandler <- function (req ) {
101
+ if (! identical(req $ REQUEST_METHOD , ' GET' ))
102
+ return (NULL )
103
+
104
+ # e.g. "/foo/one/two.html"
105
+ path <- req $ PATH_INFO
106
+
107
+ match <- regexpr(' ^/([^/]+)/' , path , perl = TRUE )
108
+ if (match == - 1 )
109
+ return (NULL )
110
+ len <- attr(match , ' capture.length' )
111
+ # e.g. "foo"
112
+ prefix <- substr(path , 2 , 2 + len - 1 )
113
+
114
+ resInfo <- .globals $ resources [[prefix ]]
115
+ if (is.null(resInfo ))
116
+ return (NULL )
117
+
118
+ # e.g. "/one/two.html"
119
+ suffix <- substr(path , 2 + len , nchar(path ))
120
+
121
+ # Create a new request that's a clone of the current request, but adjust
122
+ # PATH_INFO and SCRIPT_NAME to reflect that we have already matched the first
123
+ # path element (e.g. "/foo"). See routeHandler() for more info.
124
+ subreq <- as.environment(as.list(req , all.names = TRUE ))
125
+ subreq $ PATH_INFO <- suffix
126
+ subreq $ SCRIPT_NAME <- paste(subreq $ SCRIPT_NAME , substr(path , 1 , 2 + len ), sep = ' ' )
127
+
128
+ return (resInfo $ func(subreq ))
74
129
}
75
130
76
131
# ' Define Server Functionality
@@ -158,6 +213,8 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
158
213
appvars <- new.env()
159
214
appvars $ server <- NULL
160
215
216
+ sys.www.root <- system.file(' www' , package = ' shiny' )
217
+
161
218
# This value, if non-NULL, must be present on all HTTP and WebSocket
162
219
# requests as the Shiny-Shared-Secret header or else access will be
163
220
# denied (403 response for HTTP, and instant close for websocket).
@@ -167,6 +224,8 @@ createAppHandlers <- function(httpHandlers, serverFuncSource) {
167
224
http = joinHandlers(c(
168
225
sessionHandler ,
169
226
httpHandlers ,
227
+ sys.www.root ,
228
+ resourcePathHandler ,
170
229
reactLogHandler
171
230
)),
172
231
ws = function (ws ) {
0 commit comments