Files
YMhut-box-C-/server/unified-management/internal/web/public_snapshot.go
T

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
}