package handler import ( "crypto/sha256" "encoding/hex" "fmt" "io" "mime" "net/http" "os" "path/filepath" "sync" "time" ) // etagCache caches (etag, modtime) keyed by absolute file path. // Key: path, Value: etagCacheEntry var etagCacheM sync.Map type etagCacheEntry struct { modTime time.Time etag string } // ServeFile serves a single file with ETag, conditional GET (If-None-Match, // If-Modified-Since), and Cache-Control headers. // fsPath is the absolute filesystem path to the file. func ServeFile(w http.ResponseWriter, r *http.Request, fsPath string) { f, err := os.Open(fsPath) if err != nil { if os.IsNotExist(err) { http.Error(w, "Not Found", http.StatusNotFound) } else { http.Error(w, "Internal Server Error", http.StatusInternalServerError) } return } defer f.Close() info, err := f.Stat() if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } if info.IsDir() { http.Error(w, "Not a file", http.StatusBadRequest) return } etag := computeETag(fsPath, info.ModTime(), f) // Set headers w.Header().Set("ETag", fmt.Sprintf(`"%s"`, etag)) w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate") // Conditional GET: If-None-Match if match := r.Header.Get("If-None-Match"); match != "" { if match == fmt.Sprintf(`"%s"`, etag) { w.WriteHeader(http.StatusNotModified) return } } // Content-Type ext := filepath.Ext(fsPath) ct := mime.TypeByExtension(ext) if ct == "" { ct = "application/octet-stream" } w.Header().Set("Content-Type", ct) // Seek back to start after ETag computation if _, err := f.Seek(0, io.SeekStart); err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } http.ServeContent(w, r, info.Name(), info.ModTime(), f) } // computeETag returns the SHA-256 ETag for a file, using a modtime-keyed cache. func computeETag(path string, modTime time.Time, f *os.File) string { if entry, ok := etagCacheM.Load(path); ok { cached := entry.(etagCacheEntry) if cached.modTime.Equal(modTime) { return cached.etag } } h := sha256.New() if _, err := f.Seek(0, io.SeekStart); err == nil { io.Copy(h, f) } etag := hex.EncodeToString(h.Sum(nil))[:32] // first 32 hex chars (128 bits) is sufficient etagCacheM.Store(path, etagCacheEntry{modTime: modTime, etag: etag}) return etag }