-
Christophe de Vienne authoredChristophe de Vienne authored
spa_fileserver.go 4.51 KiB
package orusapi
import (
"mime"
"net/http"
"os"
"path"
"path/filepath"
"sort"
"strings"
)
func NewSPAFileServer(root http.FileSystem, version string) http.Handler {
return &SPAFileHandler{root, version}
}
type SPAFileHandler struct {
root http.FileSystem
etag string
}
var encodingWeight = map[string]int{
"deflate": 1,
"gzip": 2,
"br": 3,
}
func acceptEncoding(r *http.Request) []string {
s := r.Header.Get("Accept-Encoding")
splitted := strings.Split(s, ",")
for i := range splitted {
splitted[i] = strings.Trim(splitted[i], " ")
}
sort.Slice(splitted, func(i, j int) bool {
return encodingWeight[splitted[i]] > encodingWeight[splitted[j]]
})
return splitted
}
func encoding(encoding string) (string, string) {
switch encoding {
case "br":
return "br", ".br"
case "gzip":
return "gzip", ".gz"
case "deflate":
return "", ""
default:
return "", ""
}
}
func (h *SPAFileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
upath := r.URL.Path
if !strings.HasPrefix(upath, "/") {
upath = "/" + upath
r.URL.Path = upath
}
_, debugMode := r.URL.Query()["debug"]
h.serveFile(w, r, path.Clean(upath), true, debugMode)
}
// localRedirect gives a Moved Permanently response.
// It does not convert relative paths to absolute paths like Redirect does.
func (h *SPAFileHandler) localRedirect(w http.ResponseWriter, r *http.Request, newPath string) {
if q := r.URL.RawQuery; q != "" {
newPath += "?" + q
}
w.Header().Set("Location", newPath)
w.WriteHeader(http.StatusMovedPermanently)
}
func (h *SPAFileHandler) open(name string, acceptEncoding []string) (http.File, string, error) {
for _, enc := range acceptEncoding {
contentEncoding, ext := encoding(enc)
f, err := h.root.Open(name + ext)
if err == nil {
return f, contentEncoding, nil
}
if !os.IsNotExist(err) {
return nil, "", err
}
}
return nil, "", os.ErrNotExist
}
func (h *SPAFileHandler) serveFile(w http.ResponseWriter, r *http.Request, name string, redirect bool, debugMode bool) {
if h.etag != "" {
ifNoneMatch := r.Header.Get("If-None-Match")
if ifNoneMatch == h.etag {
w.WriteHeader(http.StatusNotModified)
return
}
}
indexPage := "/index.html"
if debugMode {
indexPage = "/index.debug.html"
}
// redirect .../index.html to .../
// can't use Redirect() because that would make the path absolute,
// which would be a problem running under StripPrefix
if strings.HasSuffix(r.URL.Path, indexPage) {
h.localRedirect(w, r, "./")
return
}
f, contentEncoding, err := h.open(name, acceptEncoding(r))
if os.IsNotExist(err) {
f, contentEncoding, err = h.open(indexPage, acceptEncoding(r))
if err != nil {
name = indexPage
}
}
if err != nil {
msg, code := toHTTPError(err)
http.Error(w, msg, code)
return
}
defer f.Close()
d, err := f.Stat()
if err != nil {
msg, code := toHTTPError(err)
http.Error(w, msg, code)
return
}
if redirect {
// redirect to canonical path: / at end of directory url
// r.URL.Path always begins with /
url := r.URL.Path
if d.IsDir() {
if url[len(url)-1] != '/' {
h.localRedirect(w, r, path.Base(url)+"/")
return
}
} else {
if url[len(url)-1] == '/' {
h.localRedirect(w, r, "../"+path.Base(url))
return
}
}
}
if d.IsDir() {
url := r.URL.Path
// redirect if the directory name doesn't end in a slash
if url == "" || url[len(url)-1] != '/' {
h.localRedirect(w, r, path.Base(url)+"/")
return
}
// use contents of index.html for directory, if present
index := strings.TrimSuffix(name, "/") + indexPage
ff, ffContentEncoding, err := h.open(index, acceptEncoding(r))
if err == nil {
defer ff.Close()
dd, err := ff.Stat()
if err == nil {
name = index
d = dd
f = ff
contentEncoding = ffContentEncoding
}
}
}
// Still a directory? (we didn't find an index.html file)
if d.IsDir() {
http.Error(w, "Not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(name)))
if contentEncoding != "" {
w.Header().Set("Content-Encoding", contentEncoding)
}
w.Header().Set("ETag", h.etag)
w.Header().Set("Cache-Control", "no-cache")
http.ServeContent(w, r, d.Name(), d.ModTime(), f)
}
// The following code is copied from the golang 'http' package
func toHTTPError(err error) (string, int) {
if os.IsNotExist(err) {
return "404 page not found", http.StatusNotFound
}
if os.IsPermission(err) {
return "403 Forbidden", http.StatusForbidden
}
// Default:
return "500 Internal Server Error", http.StatusInternalServerError
}