Files
reproxy/vendor/github.com/go-pkgz/rest/file_server.go
2022-01-06 01:48:10 -06:00

186 lines
4.9 KiB
Go

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
}