8
8
"context"
9
9
"embed"
10
10
"fmt"
11
+ "html/template"
11
12
"io/fs"
12
13
"net/http"
13
14
"path/filepath"
@@ -69,10 +70,18 @@ func Handler(filesystem fs.FS, logger slog.Logger, templateFunc HTMLTemplateHand
69
70
}
70
71
71
72
func serveFiles (fileSystem fs.FS , logger slog.Logger ) (http.HandlerFunc , error ) {
73
+ // htmlFileToTemplate is a map of html files -> template
74
+ // We need to use templates in order to inject parameters from `HtmlState`
75
+ // (like CSRF token and CSP nonce)
76
+ htmlFileToTemplate := map [string ]* template.Template {}
72
77
73
- fileNameToBytes := map [string ][]byte {}
74
- var indexBytes []byte
75
- indexBytes = nil
78
+ // nonHtmlFileToTemplate is a map of files -> byte contents
79
+ // This is used for any non-HTML file
80
+ nonHtmlFileToTemplate := map [string ][]byte {}
81
+
82
+ // fallbackHtmlTemplate is used as the 'default' template if
83
+ // the path requested doesn't match anything on the file systme.
84
+ var fallbackHtmlTemplate * template.Template
76
85
77
86
files , err := fs .ReadDir (fileSystem , "." )
78
87
if err != nil {
@@ -93,19 +102,37 @@ func serveFiles(fileSystem fs.FS, logger slog.Logger) (http.HandlerFunc, error)
93
102
continue
94
103
}
95
104
96
- fileNameToBytes [normalizedName ] = fileBytes
97
- if normalizedName == "index.html" {
98
- indexBytes = fileBytes
105
+ isHtml := isHtmlFile (normalizedName )
106
+ if isHtml {
107
+ // For HTML files, we need to parse and store the template.
108
+ // If its index.html, we need to keep a reference to it as well.
109
+ template , err := template .New ("" ).Parse (string (fileBytes ))
110
+ if err != nil {
111
+ logger .Warn (context .Background (), "Unable to parse html template" , slog .F ("fileName" , normalizedName ))
112
+ continue
113
+ }
114
+
115
+ htmlFileToTemplate [normalizedName ] = template
116
+ // If this is the index page, use it as the fallback template
117
+ if strings .HasPrefix (normalizedName , "index." ) {
118
+ fallbackHtmlTemplate = template
119
+ }
120
+ } else {
121
+ // Non HTML files are easy - just cache the bytes
122
+ nonHtmlFileToTemplate [normalizedName ] = fileBytes
99
123
}
100
124
101
125
continue
102
126
}
103
127
128
+ // If we reached here, there was something on the file system (most likely a directory)
129
+ // that we were unable to handle in the current code - so log a warning.
104
130
logger .Warn (context .Background (), "Serving from nested directories is not implemented" , slog .F ("name" , name ))
105
131
}
106
132
107
- if indexBytes == nil {
108
- return nil , xerrors .Errorf ("No index.html available" )
133
+ // If we don't have a default template, then there's not much to do!
134
+ if fallbackHtmlTemplate == nil {
135
+ return nil , xerrors .Errorf ("No index.html found" )
109
136
}
110
137
111
138
serveFunc := func (writer http.ResponseWriter , request * http.Request ) {
@@ -116,29 +143,63 @@ func serveFiles(fileSystem fs.FS, logger slog.Logger) (http.HandlerFunc, error)
116
143
normalizedFileName = "index.html"
117
144
}
118
145
119
- isCacheable := ! strings .HasSuffix (normalizedFileName , ".html" ) && ! strings .HasSuffix (normalizedFileName , ".htm" )
120
-
121
- fileBytes , ok := fileNameToBytes [normalizedFileName ]
122
- if ! ok {
123
- logger .Warn (request .Context (), "Unable to find request file" , slog .F ("fileName" , normalizedFileName ))
124
- fileBytes = indexBytes
125
- isCacheable = false
126
- normalizedFileName = "index.html"
127
- }
128
-
129
- if isCacheable {
146
+ // First, let's look at our non-HTML files to see if this matches
147
+ fileBytes , ok := nonHtmlFileToTemplate [normalizedFileName ]
148
+ if ok {
130
149
// All our assets - JavaScript, CSS, images - should be cached.
131
150
// For cases like JavaScript, we rely on a cache-busting strategy whenever
132
151
// there is a new version (this is handled in our webpack config).
133
152
writer .Header ().Add ("Cache-Control" , "public, max-age=31536000, immutable" )
153
+ http .ServeContent (writer , request , normalizedFileName , time.Time {}, bytes .NewReader (fileBytes ))
154
+ return
134
155
}
135
156
136
- http .ServeContent (writer , request , normalizedFileName , time.Time {}, bytes .NewReader (fileBytes ))
157
+ var buf bytes.Buffer
158
+ // TODO: Fix this
159
+ templateData := HtmlState {
160
+ CSRFToken : "TODO" ,
161
+ CSPNonce : "TODO" ,
162
+ }
163
+
164
+ // Next, lets try and load from our HTML templates
165
+ template , ok := htmlFileToTemplate [normalizedFileName ]
166
+ if ok {
167
+ logger .Debug (context .Background (), "Applying template parameters" , slog .F ("fileName" , normalizedFileName ), slog .F ("templateData" , templateData ))
168
+ err := template .ExecuteTemplate (& buf , "" , templateData )
169
+
170
+ if err != nil {
171
+ logger .Error (request .Context (), "Error executing template" , slog .F ("templateData" , templateData ))
172
+ http .Error (writer , http .StatusText (http .StatusInternalServerError ), http .StatusInternalServerError )
173
+ return
174
+ }
175
+
176
+ http .ServeContent (writer , request , normalizedFileName , time.Time {}, bytes .NewReader (buf .Bytes ()))
177
+ return
178
+ }
179
+
180
+ // Finally... the path didn't match any file that we had cached.
181
+ // This is expected, because any nested path is going to hit this case.
182
+ // For that, we'll serve the fallback
183
+ logger .Debug (context .Background (), "Applying template parameters" , slog .F ("fileName" , normalizedFileName ), slog .F ("templateData" , templateData ))
184
+ err := fallbackHtmlTemplate .ExecuteTemplate (& buf , "" , templateData )
185
+
186
+ if err != nil {
187
+ logger .Error (request .Context (), "Error executing template" , slog .F ("templateData" , templateData ))
188
+ http .Error (writer , http .StatusText (http .StatusInternalServerError ), http .StatusInternalServerError )
189
+ return
190
+ }
191
+
192
+ http .ServeContent (writer , request , normalizedFileName , time.Time {}, bytes .NewReader (buf .Bytes ()))
193
+
137
194
}
138
195
139
196
return serveFunc , nil
140
197
}
141
198
199
+ func isHtmlFile (fileName string ) bool {
200
+ return strings .HasSuffix (fileName , ".html" ) || strings .HasSuffix (fileName , ".htm" )
201
+ }
202
+
142
203
type HtmlState struct {
143
204
CSPNonce string
144
205
CSRFToken string
0 commit comments