Files
new-api-custom/middleware/frontend_static.go
T

110 lines
2.9 KiB
Go

package middleware
import (
"bytes"
"compress/gzip"
"errors"
"io"
"mime"
"net/http"
"path"
"strconv"
"strings"
"sync"
ginGzip "github.com/gin-contrib/gzip"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
// precompressedFile is the gzip encoding of one frontend build file.
type precompressedFile struct {
once sync.Once
body []byte
contentType string
err error
}
// ServeFrontendFiles serves files from the embedded frontend build. The build
// never changes while the process runs, so each file is gzip-compressed once
// and the bytes are reused, instead of recompressing multi-megabyte chunks on
// every request. The cache holds at most one entry per build file. Clients
// without gzip, the image types the gzip middleware skips, directories, and
// the index.html redirect keep using http.FileServer.
func ServeFrontendFiles(frontendFS static.ServeFileSystem) gin.HandlerFunc {
fileServer := http.FileServer(frontendFS)
var cache sync.Map // URL path -> *precompressedFile
return func(c *gin.Context) {
urlPath := c.Request.URL.Path
if !frontendFS.Exists("/", urlPath) {
return
}
c.Abort()
if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") ||
ginGzip.DefaultExcludedExtentions.Contains(path.Ext(urlPath)) ||
strings.HasSuffix(urlPath, "/index.html") {
fileServer.ServeHTTP(c.Writer, c.Request)
return
}
value, ok := cache.Load(urlPath)
if !ok {
value, _ = cache.LoadOrStore(urlPath, &precompressedFile{})
}
file := value.(*precompressedFile)
file.once.Do(func() {
f, err := frontendFS.Open(urlPath)
if err != nil {
file.err = err
return
}
defer f.Close()
info, err := f.Stat()
if err != nil {
file.err = err
return
}
if info.IsDir() {
file.err = errors.New("directory")
return
}
raw, err := io.ReadAll(f)
if err != nil {
file.err = err
return
}
var compressed bytes.Buffer
writer := gzip.NewWriter(&compressed)
if _, err := writer.Write(raw); err != nil {
file.err = err
return
}
if err := writer.Close(); err != nil {
file.err = err
return
}
// Match http.FileServer: extension first, then content sniffing.
file.contentType = mime.TypeByExtension(path.Ext(urlPath))
if file.contentType == "" {
file.contentType = http.DetectContentType(raw)
}
file.body = bytes.Clone(compressed.Bytes())
})
if file.err != nil {
fileServer.ServeHTTP(c.Writer, c.Request)
return
}
// Range requests get the whole body; a server may ignore Range.
header := c.Writer.Header()
header.Set("Content-Encoding", "gzip")
header.Set("Vary", "Accept-Encoding")
header.Set("Content-Type", file.contentType)
header.Set("Content-Length", strconv.Itoa(len(file.body)))
c.Writer.WriteHeader(http.StatusOK)
if c.Request.Method != http.MethodHead {
_, _ = c.Writer.Write(file.body)
}
}
}