-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
file_server.go
185 lines (157 loc) · 4.92 KB
/
file_server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package rest
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)
// FS provides http.FileServer handler to serve static files from a http.FileSystem,
// prevents directory listing by default and supports spa-friendly mode (off by default) returning /index.html on 404.
// - public defines base path of the url, i.e. for http://example.com/static/* it should be /static
// - local for the local path to the root of the served directory
// - notFound is the reader for the custom 404 html, can be nil for default
type FS struct {
public, root string
notFound io.Reader
isSpa bool
enableListing bool
handler http.HandlerFunc
}
// NewFileServer creates file server with optional spa mode and optional direcroty listing (disabled by default)
func NewFileServer(public, local string, options ...FsOpt) (*FS, error) {
res := FS{
public: public,
notFound: nil,
isSpa: false,
enableListing: false,
}
root, err := filepath.Abs(local)
if err != nil {
return nil, fmt.Errorf("can't get absolute path for %s: %w", local, err)
}
res.root = root
if _, err = os.Stat(root); os.IsNotExist(err) {
return nil, fmt.Errorf("local path %s doesn't exist: %w", root, err)
}
for _, opt := range options {
err = opt(&res)
if err != nil {
return nil, err
}
}
cfs := customFS{
fs: http.Dir(root),
spa: res.isSpa,
listing: res.enableListing,
}
f := http.StripPrefix(public, http.FileServer(cfs))
res.handler = func(w http.ResponseWriter, r *http.Request) { f.ServeHTTP(w, r) }
if !res.enableListing {
h, err := custom404Handler(f, res.notFound)
if err != nil {
return nil, err
}
res.handler = func(w http.ResponseWriter, r *http.Request) { h.ServeHTTP(w, r) }
}
return &res, nil
}
// FileServer is a shortcut for making FS with listing disabled and the custom noFound reader (can be nil).
// Deprecated: the method is for back-compatibility only and user should use the universal NewFileServer instead
func FileServer(public, local string, notFound io.Reader) (http.Handler, error) {
return NewFileServer(public, local, FsOptCustom404(notFound))
}
// FileServerSPA is a shortcut for making FS with SPA-friendly handling of 404, listing disabled and the custom noFound reader (can be nil).
// Deprecated: the method is for back-compatibility only and user should use the universal NewFileServer instead
func FileServerSPA(public, local string, notFound io.Reader) (http.Handler, error) {
return NewFileServer(public, local, FsOptCustom404(notFound), FsOptSPA)
}
// ServeHTTP makes FileServer compatible with http.Handler interface
func (fs *FS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fs.handler(w, r)
}
// FsOpt defines functional option type
type FsOpt func(fs *FS) error
// FsOptSPA turns on SPA mode returning "/index.html" on not-found
func FsOptSPA(fs *FS) error {
fs.isSpa = true
return nil
}
// FsOptListing turns on directory listing
func FsOptListing(fs *FS) error {
fs.enableListing = true
return nil
}
// FsOptCustom404 sets custom 404 reader
func FsOptCustom404(fr io.Reader) FsOpt {
return func(fs *FS) error {
fs.notFound = fr
return nil
}
}
// customFS wraps http.FileSystem with spa and no-listing optional support
type customFS struct {
fs http.FileSystem
spa bool
listing bool
}
// Open file on FS, for directory enforce index.html and fail on a missing index
func (cfs customFS) Open(name string) (http.File, error) {
f, err := cfs.fs.Open(name)
if err != nil {
if cfs.spa {
return cfs.fs.Open("/index.html")
}
return nil, err
}
finfo, err := f.Stat()
if err != nil {
return nil, err
}
if finfo.IsDir() {
index := strings.TrimSuffix(name, "/") + "/index.html"
if _, err := cfs.fs.Open(index); err == nil { // index.html will be served if found
return f, nil
}
// no index.html in directory
if !cfs.listing { // listing disabled
if _, err := cfs.fs.Open(index); err != nil {
return nil, err
}
}
}
return f, nil
}
// respWriter404 intercept Write to provide custom 404 response
type respWriter404 struct {
http.ResponseWriter
status int
msg []byte
}
func (w *respWriter404) WriteHeader(status int) {
w.status = status
if status == http.StatusNotFound {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}
w.ResponseWriter.WriteHeader(status)
}
func (w *respWriter404) Write(p []byte) (n int, err error) {
if w.status != http.StatusNotFound || w.msg == nil {
return w.ResponseWriter.Write(p)
}
_, err = w.ResponseWriter.Write(w.msg)
return len(p), err
}
func custom404Handler(next http.Handler, notFound io.Reader) (http.Handler, error) {
if notFound == nil {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) }), nil
}
body, err := io.ReadAll(notFound)
if err != nil {
return nil, err
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(&respWriter404{ResponseWriter: w, msg: body}, r)
}), nil
}