Serve both UI assets and REST API from handler

This commit is contained in:
Ken Hibino
2021-10-10 06:33:38 -07:00
parent b20cf02f3b
commit ccdd6cea01
35 changed files with 620 additions and 234 deletions

View File

@@ -2,10 +2,8 @@ package main
import (
"crypto/tls"
"embed"
"flag"
"fmt"
"github.com/gorilla/mux"
"log"
"net/http"
"strings"
@@ -41,7 +39,9 @@ func init() {
flag.StringVar(&flagRedisClusterNodes, "redis-cluster-nodes", "", "comma separated list of host:port addresses of cluster nodes")
}
func getRedisOptionsFromFlags() (*redis.UniversalOptions, error) {
// TODO: Write test and refactor this code.
// IDEA: https://eli.thegreenplace.net/2020/testing-flag-parsing-in-go-programs/
func getRedisOptionsFromFlags() (asynq.RedisConnOpt, error) {
var opts redis.UniversalOptions
if flagRedisClusterNodes != "" {
@@ -74,59 +74,40 @@ func getRedisOptionsFromFlags() (*redis.UniversalOptions, error) {
opts.TLSConfig.InsecureSkipVerify = true
}
return &opts, nil
if flagRedisClusterNodes != "" {
return asynq.RedisClusterClientOpt{
Addrs: opts.Addrs,
Password: opts.Password,
TLSConfig: opts.TLSConfig,
}, nil
}
return asynq.RedisClientOpt{
Addr: opts.Addrs[0],
DB: opts.DB,
Password: opts.Password,
TLSConfig: opts.TLSConfig,
}, nil
}
//go:embed ui-assets/*
var staticContents embed.FS
func main() {
flag.Parse()
opts, err := getRedisOptionsFromFlags()
redisConnOpt, err := getRedisOptionsFromFlags()
if err != nil {
log.Fatal(err)
}
useRedisCluster := flagRedisClusterNodes != ""
var redisConnOpt asynq.RedisConnOpt
if useRedisCluster {
redisConnOpt = asynq.RedisClusterClientOpt{
Addrs: opts.Addrs,
Password: opts.Password,
TLSConfig: opts.TLSConfig,
}
} else {
redisConnOpt = asynq.RedisClientOpt{
Addr: opts.Addrs[0],
DB: opts.DB,
Password: opts.Password,
TLSConfig: opts.TLSConfig,
}
}
h := asynqmon.New(asynqmon.Options{
RedisConnOpt: redisConnOpt,
})
defer h.Close()
r := mux.NewRouter()
r.PathPrefix("/api").Handler(h)
r.PathPrefix("/").Handler(&staticContentHandler{
contents: staticContents,
staticDirPath: "ui-assets",
indexFileName: "index.html",
})
r.Use(loggingMiddleware)
c := cors.New(cors.Options{
AllowedMethods: []string{"GET", "POST", "DELETE"},
})
srv := &http.Server{
Handler: c.Handler(r),
Handler: c.Handler(h),
Addr: fmt.Sprintf(":%d", flagPort),
WriteTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second,

View File

@@ -1,58 +0,0 @@
package main
import (
"embed"
"errors"
"io/fs"
"net/http"
"path/filepath"
)
// staticFileServer implements the http.Handler interface, so we can use it
// to respond to HTTP requests. The path to the static directory and
// path to the index file within that static directory are used to
// serve the SPA in the given static directory.
type staticContentHandler struct {
contents embed.FS
staticDirPath string
indexFileName string
}
// ServeHTTP inspects the URL path to locate a file within the static dir
// on the SPA handler.
// If path '/' is requested, it will serve the index file, otherwise it will
// serve the file specified by the URL path.
func (h *staticContentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get the absolute path to prevent directory traversal.
path, err := filepath.Abs(r.URL.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if path == "/" {
path = h.indexFilePath()
} else {
path = filepath.Join(h.staticDirPath, path)
}
bytes, err := h.contents.ReadFile(path)
// If path is error (e.g. file not exist, path is a directory), serve index file.
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
bytes, err = h.contents.ReadFile(h.indexFilePath())
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := w.Write(bytes); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (h *staticContentHandler) indexFilePath() string {
return filepath.Join(h.staticDirPath, h.indexFileName)
}