93 lines
2.4 KiB
Go
93 lines
2.4 KiB
Go
package web
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type publicSnapshot struct {
|
|
data []byte
|
|
etag string
|
|
generatedAt time.Time
|
|
lastModified time.Time
|
|
}
|
|
|
|
type publicSnapshotEntry struct {
|
|
snapshot publicSnapshot
|
|
expiresAt time.Time
|
|
}
|
|
|
|
type publicSnapshotService struct {
|
|
mu sync.Mutex
|
|
ttl time.Duration
|
|
entries map[string]publicSnapshotEntry
|
|
}
|
|
|
|
func newPublicSnapshotService(ttl time.Duration) *publicSnapshotService {
|
|
return &publicSnapshotService{ttl: ttl, entries: map[string]publicSnapshotEntry{}}
|
|
}
|
|
|
|
func (s *publicSnapshotService) Get(key string, build func(generatedAt time.Time) any) publicSnapshot {
|
|
now := time.Now().UTC()
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if entry, ok := s.entries[key]; ok && now.Before(entry.expiresAt) {
|
|
return entry.snapshot
|
|
}
|
|
|
|
payload := build(now)
|
|
data, err := json.Marshal(payload)
|
|
if err != nil {
|
|
data = []byte(`{"ok":false,"error":"SNAPSHOT_FAILED"}`)
|
|
}
|
|
hash := sha256.Sum256(data)
|
|
snapshot := publicSnapshot{
|
|
data: append(data, '\n'),
|
|
etag: `"` + hex.EncodeToString(hash[:]) + `"`,
|
|
generatedAt: now,
|
|
lastModified: now.Truncate(time.Second),
|
|
}
|
|
s.entries[key] = publicSnapshotEntry{snapshot: snapshot, expiresAt: now.Add(s.ttl)}
|
|
return snapshot
|
|
}
|
|
|
|
func (s *publicSnapshotService) Invalidate() {
|
|
s.mu.Lock()
|
|
s.entries = map[string]publicSnapshotEntry{}
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func writePublicSnapshot(w http.ResponseWriter, req *http.Request, snapshot publicSnapshot) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("ETag", snapshot.etag)
|
|
w.Header().Set("Last-Modified", snapshot.lastModified.Format(http.TimeFormat))
|
|
w.Header().Set("Cache-Control", "public, max-age=60, stale-while-revalidate=300")
|
|
if etagMatches(req.Header.Get("If-None-Match"), snapshot.etag) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
if modifiedSince := req.Header.Get("If-Modified-Since"); modifiedSince != "" {
|
|
if parsed, err := http.ParseTime(modifiedSince); err == nil && !snapshot.lastModified.After(parsed) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(snapshot.data)
|
|
}
|
|
|
|
func etagMatches(header, etag string) bool {
|
|
for _, candidate := range strings.Split(header, ",") {
|
|
candidate = strings.TrimSpace(candidate)
|
|
if candidate == "*" || candidate == etag || strings.TrimPrefix(candidate, "W/") == etag {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|