cmd/frontend/main.go (140 lines of code) (raw):
package main
import (
"embed"
"errors"
"io/fs"
"log"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/zeebo/xxh3"
)
//go:embed all:resources
var assetFs embed.FS
func main() {
err := run()
if err != nil {
log.Fatal(err)
}
}
type assetInfo struct {
data []byte
dataBr []byte
eTag string
contentLength string
contentLengthBr string
contentType string
}
func run() error {
pathToAsset := map[string]*assetInfo{}
err := fs.WalkDir(assetFs, "resources", func(path string, _ fs.DirEntry, err error) error {
if err != nil {
return err
}
if !strings.ContainsRune(path, '.') || strings.HasPrefix(path, "/.") {
// is a directory
return nil
}
data, err := assetFs.ReadFile(path)
if err != nil {
return err
}
if path == "resources" {
return nil
}
key := strings.TrimPrefix(path, "resources")
isBr := strings.HasSuffix(key, ".br")
if isBr {
key = strings.TrimSuffix(key, ".br")
}
info := pathToAsset[key]
if info == nil {
info = &assetInfo{}
pathToAsset[key] = info
}
lengthAsString := strconv.Itoa(len(data))
if isBr {
info.dataBr = data
info.contentLengthBr = lengthAsString
} else {
info.data = data
info.contentLength = lengthAsString
// no mime type on distroless image
switch {
case strings.HasSuffix(key, ".woff"):
info.contentType = "font/woff"
case strings.HasSuffix(key, ".woff2"):
info.contentType = "font/woff2"
case strings.HasSuffix(key, ".svg"):
info.contentType = "image/svg+xml"
case strings.HasSuffix(key, ".js"):
info.contentType = "text/javascript"
case strings.HasSuffix(key, ".json"):
info.contentType = "application/json"
case strings.HasSuffix(key, ".html"):
info.contentType = "text/html"
case strings.HasSuffix(key, ".css"):
info.contentType = "text/css"
case strings.HasSuffix(key, ".wasm"):
info.contentType = "application/wasm"
case strings.HasSuffix(key, ".dictionary"):
info.contentType = "application/octet-stream"
case strings.HasSuffix(key, ".ttf"):
info.contentType = "font/ttf"
default:
return errors.New("cannot determinate content-type by file extension: " + filepath.Ext(path))
}
// assets are immutable
if !strings.HasPrefix(key, "/assets/") {
hash := xxh3.Hash128(data)
info.eTag = strconv.FormatUint(hash.Hi, 36) + "-" + strconv.FormatUint(hash.Lo, 36)
}
}
return nil
})
if err != nil {
return err
}
// no need to keep it anymore
assetFs = embed.FS{}
indexHtml := pathToAsset["/index.html"]
pathToAsset["/"] = indexHtml
http.Handle("/index.html", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
newPath := "./"
q := request.URL.RawQuery
if q != "" {
newPath += "?" + q
}
writer.Header().Set("Location", newPath)
writer.WriteHeader(http.StatusMovedPermanently)
}))
http.Handle("/", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
header := writer.Header()
path := request.URL.Path
asset := pathToAsset[path]
if asset == nil {
if strings.ContainsRune(path, '.') {
http.NotFound(writer, request)
return
}
// vue router HTML5 mode (https://next.router.vuejs.org/guide/essentials/history-mode.html#html5-mode)
asset = indexHtml
}
header.Set("Vary", "Accept-Encoding")
header.Set("Content-Type", asset.contentType)
// https://medium.com/adobetech/an-http-caching-strategy-for-static-assets-configuring-the-server-1192452ce06a
if asset.eTag == "" {
header.Set("Cache-Control", "public,max-age=31536000,immutable")
} else {
header.Set("Cache-Control", "no-cache")
if request.Header.Get("If-None-Match") == asset.eTag {
writer.WriteHeader(http.StatusNotModified)
return
}
header.Set("ETag", asset.eTag)
}
if len(asset.dataBr) != 0 && strings.Contains(request.Header.Get("Accept-Encoding"), "br") {
header.Set("Content-Length", asset.contentLengthBr)
header.Set("Content-Encoding", "br")
_, _ = writer.Write(asset.dataBr)
} else {
header.Set("Content-Length", asset.contentLength)
_, _ = writer.Write(asset.data)
}
}))
return http.ListenAndServe(":8080", nil)
}