github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/asynqmon/static.go (about)

     1  package asynqmon
     2  
     3  import (
     4  	"embed"
     5  	"errors"
     6  	"html/template"
     7  	"io/fs"
     8  	"net/http"
     9  	"path/filepath"
    10  	"strings"
    11  )
    12  
    13  // uiAssetsHandler is a http.Handler.
    14  // The path to the static file directory and
    15  // the path to the index file within that static directory are used to
    16  // serve the SPA.
    17  type uiAssetsHandler struct {
    18  	rootPath       string
    19  	contents       embed.FS
    20  	staticDirPath  string
    21  	indexFileName  string
    22  	prometheusAddr string
    23  	readOnly       bool
    24  }
    25  
    26  // ServeHTTP inspects the URL path to locate a file within the static dir
    27  // on the SPA handler.
    28  // If path '/' is requested, it will serve the index file, otherwise it will
    29  // serve the file specified by the URL path.
    30  func (h *uiAssetsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    31  	// Get the absolute path to prevent directory traversal.
    32  	path, err := filepath.Abs(r.URL.Path)
    33  	if err != nil {
    34  		http.Error(w, err.Error(), http.StatusBadRequest)
    35  		return
    36  	}
    37  
    38  	// Get the path relative to the root path.
    39  	if !strings.HasPrefix(path, h.rootPath) {
    40  		http.Error(w, "unexpected path prefix", http.StatusBadRequest)
    41  		return
    42  	}
    43  	path = strings.TrimPrefix(path, h.rootPath)
    44  
    45  	if code, err := h.serveFile(w, path); err != nil {
    46  		http.Error(w, err.Error(), code)
    47  		return
    48  	}
    49  }
    50  
    51  func (h *uiAssetsHandler) indexFilePath() string {
    52  	return filepath.Join(h.staticDirPath, h.indexFileName)
    53  }
    54  
    55  func (h *uiAssetsHandler) renderIndexFile(w http.ResponseWriter) error {
    56  	// Note: Replace the default delimiter ("{{") with a custom one
    57  	// since webpack escapes the '{' character when it compiles the index.html file.
    58  	// See the "homepage" field in package.json.
    59  	tmpl, err := template.New(h.indexFileName).Delims("/[[", "]]").ParseFS(h.contents, h.indexFilePath())
    60  	if err != nil {
    61  		return err
    62  	}
    63  	data := struct {
    64  		RootPath       string
    65  		PrometheusAddr string
    66  		ReadOnly       bool
    67  	}{
    68  		RootPath:       h.rootPath,
    69  		PrometheusAddr: h.prometheusAddr,
    70  		ReadOnly:       h.readOnly,
    71  	}
    72  	return tmpl.Execute(w, data)
    73  }
    74  
    75  // serveFile writes file requested at path and returns http status code and error if any.
    76  // If requested path is root, it serves the index file.
    77  // Otherwise, it looks for file requiested in the static content filesystem
    78  // and serves if a file is found.
    79  // If a requested file is not found in the filesystem, it serves the index file to
    80  // make sure when user refreshes the page in SPA things still work.
    81  func (h *uiAssetsHandler) serveFile(w http.ResponseWriter, path string) (code int, err error) {
    82  	if path == "/" || path == "" {
    83  		if err := h.renderIndexFile(w); err != nil {
    84  			return http.StatusInternalServerError, err
    85  		}
    86  		return http.StatusOK, nil
    87  	}
    88  	path = filepath.Join(h.staticDirPath, path)
    89  	bytes, err := h.contents.ReadFile(path)
    90  	if err != nil {
    91  		// If path is error (e.g. file not exist, path is a directory), serve index file.
    92  		var pathErr *fs.PathError
    93  		if errors.As(err, &pathErr) {
    94  			if err := h.renderIndexFile(w); err != nil {
    95  				return http.StatusInternalServerError, err
    96  			}
    97  			return http.StatusOK, nil
    98  		}
    99  		return http.StatusInternalServerError, err
   100  	}
   101  	// Setting the MIME type for .js files manually to application/javascript as
   102  	// http.DetectContentType is using https://mimesniff.spec.whatwg.org/ which
   103  	// will not recognize application/javascript for security reasons.
   104  	if strings.HasSuffix(path, ".js") {
   105  		w.Header().Add("Content-Type", "application/javascript; charset=utf-8")
   106  	} else {
   107  		w.Header().Add("Content-Type", http.DetectContentType(bytes))
   108  	}
   109  
   110  	if _, err := w.Write(bytes); err != nil {
   111  		return http.StatusInternalServerError, err
   112  	}
   113  	return http.StatusOK, nil
   114  }