1
1
package spaserve
2
2
3
3
import (
4
- "context"
5
4
"errors"
6
5
"io/fs"
7
6
"log/slog"
@@ -13,6 +12,14 @@ import (
13
12
"github.com/psanford/memfs"
14
13
)
15
14
15
+ type StaticFilesHandler struct {
16
+ opts staticFilesHandlerOpts
17
+ fileServer http.Handler
18
+ mfilesys * memfs.FS
19
+ logger * servespaLogger
20
+ muxErrHandler func (int , http.ResponseWriter , * http.Request )
21
+ }
22
+
16
23
type staticFilesHandlerOpts struct {
17
24
ns string
18
25
basePath string
@@ -92,16 +99,13 @@ func WithInjectWebEnv(env any, namespace string) staticFilesHandlerFunc {
92
99
// - ctx: the context
93
100
// - filesys: the file system to serve files from - this will be copied to a memfs
94
101
// - fn: optional functions to configure the handler (e.g. WithLogger, WithBasePath, WithMuxErrorHandler, WithInjectWebEnv)
95
- func StaticFilesHandler ( ctx context. Context , filesys fs.FS , fn ... staticFilesHandlerFunc ) (http.Handler , error ) {
102
+ func NewStaticFilesHandler ( filesys fs.FS , fn ... staticFilesHandlerFunc ) (http.Handler , error ) {
96
103
// process options
97
104
opts := defaultStaticFilesHandlerOpts
98
105
for _ , f := range fn {
99
106
opts = f (opts )
100
107
}
101
108
102
- logWithAttrs := newLoggerWithContext (ctx , opts .logger )
103
- muxErrHandler := newMuxErrorHandler (opts .muxErrHandler )
104
-
105
109
var (
106
110
mfilesys * memfs.FS
107
111
err error
@@ -118,89 +122,69 @@ func StaticFilesHandler(ctx context.Context, filesys fs.FS, fn ...staticFilesHan
118
122
119
123
// create file server
120
124
fileServer := http .FileServer (http .FS (mfilesys ))
125
+ logger := newLogger (opts .logger )
126
+
127
+ return & StaticFilesHandler {
128
+ opts : opts ,
129
+ mfilesys : mfilesys ,
130
+ fileServer : fileServer ,
131
+ logger : logger ,
132
+ muxErrHandler : newMuxErrorHandler (opts .muxErrHandler ),
133
+ }, nil
134
+ }
121
135
122
- // return handler
123
- return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
124
- // serve index.html for root path
125
- if r .URL .Path == "/" {
126
- fileServer .ServeHTTP (w , r )
127
- return
128
- }
136
+ func (h * StaticFilesHandler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
137
+ ctx := r .Context ()
129
138
130
- // warn if base path might be wrong
131
- if len (opts .basePath ) > 0 && r .URL .Path [:len (opts .basePath )] != opts .basePath {
132
- logWithAttrs (slog .LevelInfo , "WARNING: base path may not be set correctly" ,
133
- slog.Attr {Key : "reqPath" , Value : slog .StringValue (r .URL .Path )},
134
- slog.Attr {Key : "basePath" , Value : slog .StringValue (opts .basePath )},
135
- )
136
- }
139
+ // clean path for security and consistency
140
+ cleanedPath := path .Clean (r .URL .Path )
141
+ cleanedPath = strings .TrimPrefix (cleanedPath , h .opts .basePath )
142
+ cleanedPath = strings .TrimPrefix (cleanedPath , "/" )
143
+ cleanedPath = strings .TrimSuffix (cleanedPath , "/" )
137
144
138
- // clean path for security and consistency
139
- cleanedPath := path .Clean (r .URL .Path )
140
- cleanedPath = strings .TrimPrefix (cleanedPath , opts .basePath )
145
+ h .logger .logContext (ctx , slog .LevelDebug , "request" , slog.Attr {Key : "cleanedPath" , Value : slog .StringValue (cleanedPath )})
141
146
142
- // open file
143
- file , err := mfilesys .Open (cleanedPath )
144
- if file != nil {
145
- defer file .Close ()
146
- }
147
+ // reconstitute the path
148
+ r .URL .Path = "/" + cleanedPath
147
149
148
- // ensure leading slash
149
- r .URL .Path = cleanedPath
150
- if r .URL .Path [0 ] != '/' {
151
- r .URL .Path = "/" + r .URL .Path
152
- }
153
-
154
- // if index.html is requested, rewrite to avoid 301 redirect
155
- if r .URL .Path == "/index.html" {
156
- r .URL .Path = "/"
157
- }
150
+ // use root path for index.html
151
+ if r .URL .Path == "index.html" {
152
+ r .URL .Path = "/"
153
+ }
158
154
155
+ // handle non-root paths
156
+ if r .URL .Path != "/" {
157
+ // open file
158
+ file , err := h .mfilesys .Open (cleanedPath )
159
+ isErr := err != nil
159
160
isErrNotExist := errors .Is (err , os .ErrNotExist )
160
161
isFile := path .Ext (cleanedPath ) != ""
161
-
162
- // if err != nil {
163
- // fmt.Printf("error: %v\n", err)
164
- // fmt.Printf("request path: %s\n", r.URL.Path)
165
- // fmt.Printf("cleaned path: %s\n", cleanedPath)
166
- // fs.WalkDir(mfilesys, ".", func(path string, d fs.DirEntry, err error) error {
167
- // fmt.Printf("path: %s, d: %v, err: %v\n", path, d, err)
168
- // return nil
169
- // })
170
- // }
162
+ if file != nil {
163
+ file .Close ()
164
+ }
171
165
172
166
// return 500 for other errors
173
- if err != nil && ! isErrNotExist {
174
- logWithAttrs ( slog .LevelError , "could not open file" , slog.Attr {Key : "cleanedPath" , Value : slog .StringValue (cleanedPath )})
175
- muxErrHandler (http .StatusInternalServerError , w , r )
167
+ if isErr && ! isErrNotExist {
168
+ h . logger . logContext ( ctx , slog .LevelError , "could not open file" , slog.Attr {Key : "cleanedPath" , Value : slog .StringValue (cleanedPath )})
169
+ h . muxErrHandler (http .StatusInternalServerError , w , r )
176
170
return
177
171
}
178
172
179
173
// return 404 for actual static file requests that don't exist
180
- if err != nil && isErrNotExist && isFile {
181
- logWithAttrs ( slog .LevelError , "could not find file" , slog.Attr {Key : "cleanedPath" , Value : slog .StringValue (cleanedPath )})
182
- muxErrHandler (http .StatusNotFound , w , r )
174
+ if isErrNotExist && isFile {
175
+ h . logger . logContext ( ctx , slog .LevelDebug , "not found, static file" , slog.Attr {Key : "cleanedPath" , Value : slog .StringValue (cleanedPath )})
176
+ h . muxErrHandler (http .StatusNotFound , w , r )
183
177
return
184
178
}
185
179
186
180
// serve index.html and let SPA handle undefined routes
187
181
if isErrNotExist {
188
- logWithAttrs ( slog .LevelDebug , "not found, serve index" , slog.Attr {Key : "cleanedPath" , Value : slog .StringValue (cleanedPath )})
182
+ h . logger . logContext ( ctx , slog .LevelDebug , "not found, serve index" , slog.Attr {Key : "cleanedPath" , Value : slog .StringValue (cleanedPath )})
189
183
r .URL .Path = "/"
190
184
}
191
-
192
- fileServer .ServeHTTP (w , r )
193
- }), nil
194
- }
195
-
196
- // newLoggerWithContext creates a new logger function with the given context and logger.
197
- func newLoggerWithContext (ctx context.Context , logger * slog.Logger ) func (slog.Level , string , ... slog.Attr ) {
198
- return func (level slog.Level , msg string , attrs ... slog.Attr ) {
199
- if logger == nil {
200
- return
201
- }
202
- logger .LogAttrs (ctx , level , msg , attrs ... )
203
185
}
186
+
187
+ h .fileServer .ServeHTTP (w , r )
204
188
}
205
189
206
190
// newMuxErrorHandler creates a new error handler function with the given muxErrHandler.
0 commit comments