Files
actions-go/go-cache-plugin-src/lib/revproxy/cache.go
Eric Liu 6e45f50874
Some checks failed
Go CI with S3 Caching / build-and-test (push) Failing after 4s
feat(cache): add Cloudflare SigV4 S3 signature compatibility fix and compile locally
2026-05-19 21:28:45 -07:00

160 lines
4.2 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package revproxy
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/creachadair/atomicfile"
"github.com/creachadair/scheddle"
"github.com/creachadair/taskgroup"
)
// cacheLoadLocal reads cached headers and body from the local cache.
func (s *Server) cacheLoadLocal(hash string) ([]byte, http.Header, error) {
data, err := os.ReadFile(s.makePath(hash))
if err != nil {
return nil, nil, err
}
return parseCacheObject(data)
}
// cacheStoreLocal writes the contents of body to the local cache.
//
// The file format is a plain-text section at the top recording a subset of the
// response headers, followed by "\n\n", followed by the response body.
func (s *Server) cacheStoreLocal(hash string, hdr http.Header, body []byte) error {
path := s.makePath(hash)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
return atomicfile.Tx(s.makePath(hash), 0644, func(f io.Writer) error {
return writeCacheObject(f, hdr, body)
})
}
// cacheLoadS3 reads cached headers and body from the remote S3 cache.
func (s *Server) cacheLoadS3(ctx context.Context, hash string) ([]byte, http.Header, error) {
data, err := s.S3Client.GetData(ctx, s.makeKey(hash))
if err != nil {
return nil, nil, err
}
return parseCacheObject(data)
}
// cacheStoreS3 returns a task that writes the contents of body to the remote
// S3 cache.
func (s *Server) cacheStoreS3(hash string, hdr http.Header, body []byte) taskgroup.Task {
var buf bytes.Buffer
writeCacheObject(&buf, hdr, body)
nb := buf.Len()
return func() error {
sctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
if err := s.S3Client.Put(sctx, s.makeKey(hash), &buf); err != nil {
s.logf("[s3] put %q failed: %v", hash, err)
s.rspPushError.Add(1)
} else {
s.rspPush.Add(1)
s.rspPushBytes.Add(int64(nb))
}
return nil
}
}
// cacheLoadMemory reads cached headers and body from the memory cache.
func (s *Server) cacheLoadMemory(hash string) ([]byte, http.Header, error) {
e, ok := s.mcache.Get(hash)
if !ok {
return nil, nil, fs.ErrNotExist
}
return e.body, e.header, nil
}
// cacheStoreMemory writes the contents of body to the memory cache.
func (s *Server) cacheStoreMemory(hash string, maxAge time.Duration, hdr http.Header, body []byte) {
s.mcache.Put(hash, memCacheEntry{
header: trimCacheHeader(hdr),
body: body,
})
s.expire.After(maxAge, scheddle.Run(func() {
s.mcache.Remove(hash)
}))
}
var keepHeader = []string{
"Cache-Control", "Content-Type", "Date", "Etag",
}
func trimCacheHeader(h http.Header) http.Header {
out := make(http.Header)
for _, name := range keepHeader {
if v := h.Get(name); v != "" {
out.Set(name, v)
}
}
return out
}
// parseCacheDbject parses cached object data to extract the body and headers.
func parseCacheObject(data []byte) ([]byte, http.Header, error) {
hdr, rest, ok := bytes.Cut(data, []byte("\n\n"))
if !ok {
return nil, nil, errors.New("invalid cache object: missing header")
}
h := make(http.Header)
for _, line := range strings.Split(string(hdr), "\n") {
name, value, ok := strings.Cut(line, ": ")
if ok {
h.Add(name, value)
}
}
return rest, h, nil
}
// writeCacheObject writes the specified response data into a cache object at w.
func writeCacheObject(w io.Writer, h http.Header, body []byte) error {
hprintf(w, h, "Content-Type", "application/octet-stream")
hprintf(w, h, "Date", "")
hprintf(w, h, "Etag", "")
fmt.Fprint(w, "\n")
_, err := w.Write(body)
return err
}
func hprintf(w io.Writer, h http.Header, name, fallback string) {
if v := h.Get(name); v != "" {
fmt.Fprintf(w, "%s: %s\n", name, v)
} else if fallback != "" {
fmt.Fprintf(w, "%s: %s\n", name, fallback)
}
}
// setXCacheInfo adds cache-specific headers to h.
func setXCacheInfo(h http.Header, result, hash string) {
h.Set("X-Cache", result)
if hash != "" {
h.Set("X-Cache-Id", hash[:12])
}
}
// memCacheEntry is the format of entries in the memory cache.
type memCacheEntry struct {
header http.Header
body []byte
}
func entrySize(e memCacheEntry) int64 { return int64(len(e.body)) }