Skip to content
Snippets Groups Projects
spa_fileserver.go 4.51 KiB
Newer Older
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)
	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

Christophe de Vienne's avatar
Christophe de Vienne committed
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
}