Files
zampler/internal/server/server.go
T
aneurinandClaude Sonnet 5 1964fda05e
PR Checks / checks (pull_request) Successful in 1m40s
Build / build-and-push (push) Successful in 1m16s
Add configurable Cache-Control headers for statics, files, and API
Send our own Cache-Control headers instead of relying entirely on
front-proxy configuration, so browsers cache appropriately even when
Zampler is reached directly. --static-max-age, --file-max-age, and
--api-max-age (all cobra Duration flags, 0 disables caching) cover the
three response categories independently; --no-immutable turns off the
immutable directive on served files, which is on by default since a
file's content never changes for a given ID. The HTML shell always
gets no-cache regardless of --static-max-age, since it isn't
content-hashed like the rest of the Vite build and must keep
revalidating so a new deploy's asset references are picked up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 11:48:53 +01:00

91 lines
2.6 KiB
Go

package server
import (
"embed"
"io/fs"
"net/http"
"strings"
"code.aneur.in/zampler/zampler/internal/database"
"code.aneur.in/zampler/zampler/internal/dto"
"code.aneur.in/zampler/zampler/internal/rest"
"code.aneur.in/zampler/zampler/internal/server/middlewares"
"github.com/gorilla/mux"
"github.com/rs/zerolog"
)
//go:embed static
var staticFiles embed.FS
// htmlCacheControl is fixed rather than driven by --static-max-age: it
// covers index.html, which is not content-hashed (unlike the Vite build's
// /assets/* files) and must always be revalidated so a deploy's new,
// hashed asset references are picked up promptly.
const htmlCacheControl = "no-cache"
type Server struct {
url string
allowIndexing bool
db *database.DB
log zerolog.Logger
static http.Handler
staticFiles fs.FS
fileCache *middlewares.CacheControl
}
func (srv *Server) ConfigureRouter(r *mux.Router) {
r.Path("/config.json").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.GetConfig)
r.Path("/robots.txt").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.GetRobots)
r.Path("/file/{id}").Methods(http.MethodGet, http.MethodOptions).Handler(srv.fileCache.Middleware(http.HandlerFunc(srv.ReadFile)))
}
func (srv *Server) ConfigureStaticAssets(r *mux.Router, maxAge int) {
staticFiles, _ := fs.Sub(staticFiles, "static")
srv.staticFiles = staticFiles
srv.static = http.FileServerFS(staticFiles)
assetHandler := middlewares.NewCacheControl(maxAge, false).Middleware(srv.static)
r.Methods(http.MethodGet, http.MethodOptions).Handler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if isHTMLPath(req.URL.Path) {
w.Header().Set("Cache-Control", htmlCacheControl)
srv.static.ServeHTTP(w, req)
return
}
assetHandler.ServeHTTP(w, req)
}))
}
// isHTMLPath reports whether p resolves to an HTML document rather than a
// hashed build asset: the root (served as index.html by http.FileServerFS)
// or anything ending in .html.
func isHTMLPath(p string) bool {
return p == "/" || p == "" || strings.HasSuffix(p, ".html")
}
func (srv *Server) GetConfig(w http.ResponseWriter, req *http.Request) {
rest.WriteResponseJSON(w, http.StatusOK, dto.Config{
ServerURL: strings.TrimRight(srv.url, "/"),
})
}
func NewServer(db *database.DB, log zerolog.Logger, url string, allowIndexing bool, fileMaxAge int, fileImmutable bool) *Server {
srv := &Server{
url: url,
allowIndexing: allowIndexing,
db: db,
log: log.With().Str("namespace", "server.Server").Logger(),
fileCache: middlewares.NewCacheControl(fileMaxAge, fileImmutable),
}
return srv
}