diff --git a/internal/middleware/etag_cache.go b/internal/middleware/etag_cache.go new file mode 100644 index 0000000..d19cfbe --- /dev/null +++ b/internal/middleware/etag_cache.go @@ -0,0 +1,66 @@ +package middleware + +import ( + "fmt" + "hash/crc64" + "log" + "net/http" + "strings" + "time" +) + +type ETag struct { + Name string + Value string + Expiration time.Duration + HashTime time.Duration +} + +var etags = make([]*ETag, 0) + +func (etag *ETag) Header() string { + return fmt.Sprintf("\"pb-%s-%s\"", etag.Name, etag.Value) +} + +func (etag *ETag) CacheControlHeader() string { + return fmt.Sprintf("max-age=%d", int(etag.Expiration.Seconds())) +} + +func StaticEtagMiddleware(etag ETag, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cc := etag.CacheControlHeader() + w.Header().Set("ETag", etag.Header()) + w.Header().Set("Cache-Control", cc) + + log.Printf("Returned etag with Cache-Control '%s'", cc) + + if match := r.Header.Get("If-None-Match"); match != "" { + if strings.Contains(match, etag.Value) { + log.Print("ETag cache hit") + w.WriteHeader(http.StatusNotModified) + return + } + } + next.ServeHTTP(w, r) + }) +} + +// GenerateETagFromBuffer calculates etag value from one or more buffers (e.g. embedded files) +func GenerateETagFromBuffer(name string, expiration time.Duration, buffers ...[]byte) (*ETag, error) { + start := time.Now() + hash := crc64.New(crc64.MakeTable(crc64.ECMA)) + for _, buffer := range buffers { + _, err := hash.Write(buffer) + if err != nil { + return nil, fmt.Errorf("unable to generate etag from buffer: %w", err) + } + } + etag := &ETag{ + Name: name, + Expiration: expiration, + Value: fmt.Sprintf("%x", hash.Sum64()), + HashTime: time.Since(start), + } + etags = append(etags, etag) + return etag, nil +}