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) }