ZDDC/zddc/internal/handler/static.go
2026-06-11 13:32:31 -05:00

98 lines
2.3 KiB
Go

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
}