Some checks failed
secrets-guard / encrypted (push) Successful in 24s
shell-lint / shellcheck (push) Successful in 26s
Deploy frontend to LAN dev server / build (push) Successful in 2m11s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 2m20s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 2m28s
Deploy backend to LAN dev server / build (push) Successful in 2m45s
PR build (required check) / changes (pull_request) Successful in 16s
secrets-guard / encrypted (pull_request) Successful in 16s
shell-lint / shellcheck (pull_request) Successful in 15s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
Deploy frontend to LAN dev server / deploy (push) Successful in 39s
PR build (required check) / build-backend (pull_request) Successful in 1m21s
PR build (required check) / gate (pull_request) Successful in 3s
Deploy backend to LAN dev server / deploy (push) Failing after 2m31s
62 lines
2.5 KiB
Go
62 lines
2.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// staticCacheControl mirrors app.py's _STATIC_CACHE_CONTROL: static assets
|
|
// aren't content-hashed in their filenames, so this has to stay short — it's
|
|
// a revalidation window, not a long-lived immutable cache. Still saves a full
|
|
// body re-transfer (the ETag/Last-Modified pair otherwise forces a
|
|
// conditional GET on every navigation) for anything fetched again within it.
|
|
// Stamped on every static file response, including 304s.
|
|
const staticCacheControl = "public, max-age=300"
|
|
|
|
// serveStatic is the catch-all under BASE — the Go analogue of app.py's
|
|
// _CachedStaticFiles mount, matching Starlette's StaticFiles semantics rather
|
|
// than http.FileServer's (which would 301 */index.html to the directory and
|
|
// render directory listings; Starlette 404s both —
|
|
// tests/unit/test_pages.py::test_old_static_index_is_gone pins the index.html
|
|
// case, keeping it from lingering as indexable duplicate content behind the
|
|
// server-rendered homepage).
|
|
func (s *Server) serveStatic(w http.ResponseWriter, r *http.Request) {
|
|
rel := strings.TrimPrefix(r.URL.Path, s.base)
|
|
// Trailing-slash paths never name a file; Starlette 404'd them (the
|
|
// mount swallowed everything under BASE, so redirect_slashes never ran
|
|
// for them).
|
|
if strings.HasSuffix(rel, "/") {
|
|
writePlain(w, http.StatusNotFound, "Not Found")
|
|
return
|
|
}
|
|
// Rooted Clean cannot escape the static dir ("/../x" cleans to "/x");
|
|
// the mux has already cleaned the request path, this is belt and braces
|
|
// for handlers invoked directly (the content package's lazy IndexNow
|
|
// route delegates here without re-cleaning).
|
|
rel = path.Clean("/" + strings.TrimPrefix(rel, "/"))
|
|
full := filepath.Join(s.staticDir, filepath.FromSlash(strings.TrimPrefix(rel, "/")))
|
|
|
|
info, err := os.Stat(full)
|
|
if err != nil || info.IsDir() {
|
|
writePlain(w, http.StatusNotFound, "Not Found")
|
|
return
|
|
}
|
|
f, err := os.Open(full)
|
|
if err != nil {
|
|
writePlain(w, http.StatusNotFound, "Not Found")
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
w.Header().Set("Cache-Control", staticCacheControl)
|
|
// Starlette's StaticFiles stamped a stat-derived ETag (mtime+size hash);
|
|
// same recipe here so If-None-Match revalidation keeps costing an empty
|
|
// 304 instead of a body re-transfer. Headers set before ServeContent
|
|
// survive on its 304/206 paths, Cache-Control included.
|
|
w.Header().Set("ETag", fmt.Sprintf("\"%x-%x\"", info.ModTime().UnixNano(), info.Size()))
|
|
http.ServeContent(w, r, info.Name(), info.ModTime(), f)
|
|
}
|