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 }