63 lines
2.5 KiB
Go
63 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)
|
||
|
|
}
|