2026-05-04 12:15:16 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2026-05-22 13:09:00 +02:00
|
|
|
"time"
|
2026-05-04 12:15:16 +02:00
|
|
|
|
2026-05-22 13:09:00 +02:00
|
|
|
"github.com/git-pkgs/proxy/internal/metrics"
|
2026-05-04 12:15:16 +02:00
|
|
|
"github.com/git-pkgs/proxy/internal/storage"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
gradleBuildCacheContentType = "application/vnd.gradle.build-cache-artifact.v2"
|
|
|
|
|
gradleBuildCacheStorageRoot = "_gradle/http-build-cache"
|
|
|
|
|
defaultGradleMaxUploadSize = 100 << 20
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var gradleBuildCacheKeyPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`)
|
|
|
|
|
|
|
|
|
|
// GradleBuildCacheHandler handles Gradle HttpBuildCache GET/HEAD/PUT requests.
|
|
|
|
|
//
|
|
|
|
|
// This handler accepts /{key} when mounted under a base URL.
|
|
|
|
|
type GradleBuildCacheHandler struct {
|
|
|
|
|
proxy *Proxy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewGradleBuildCacheHandler creates a Gradle HttpBuildCache handler.
|
|
|
|
|
func NewGradleBuildCacheHandler(proxy *Proxy) *GradleBuildCacheHandler {
|
|
|
|
|
return &GradleBuildCacheHandler{proxy: proxy}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Routes returns the HTTP handler for Gradle HttpBuildCache requests.
|
|
|
|
|
func (h *GradleBuildCacheHandler) Routes() http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
switch r.Method {
|
|
|
|
|
case http.MethodGet, http.MethodHead, http.MethodPut:
|
|
|
|
|
default:
|
|
|
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key, statusCode := h.parseCacheKey(r.URL.Path)
|
|
|
|
|
if statusCode != http.StatusOK {
|
|
|
|
|
if statusCode == http.StatusNotFound {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
http.Error(w, "invalid cache key", statusCode)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if r.Method == http.MethodPut {
|
|
|
|
|
if h.proxy.GradleReadOnly {
|
|
|
|
|
http.Error(w, "gradle build cache is read-only", http.StatusMethodNotAllowed)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.handlePut(w, r, key)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h.handleGetOrHead(w, r, key)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *GradleBuildCacheHandler) parseCacheKey(urlPath string) (string, int) {
|
|
|
|
|
keyPath := strings.TrimPrefix(urlPath, "/")
|
|
|
|
|
if keyPath == "" {
|
|
|
|
|
return "", http.StatusNotFound
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if containsPathTraversal(keyPath) {
|
|
|
|
|
return "", http.StatusBadRequest
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 10:25:17 +01:00
|
|
|
if strings.Contains(keyPath, "/") {
|
2026-05-04 12:15:16 +02:00
|
|
|
return "", http.StatusNotFound
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !gradleBuildCacheKeyPattern.MatchString(keyPath) {
|
|
|
|
|
return "", http.StatusBadRequest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return keyPath, http.StatusOK
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *GradleBuildCacheHandler) cacheStoragePath(key string) string {
|
|
|
|
|
return gradleBuildCacheStorageRoot + "/" + key
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *GradleBuildCacheHandler) handleGetOrHead(w http.ResponseWriter, r *http.Request, key string) {
|
|
|
|
|
storagePath := h.cacheStoragePath(key)
|
|
|
|
|
w.Header().Set("Content-Type", gradleBuildCacheContentType)
|
|
|
|
|
|
|
|
|
|
if r.Method == http.MethodHead {
|
2026-05-22 13:09:00 +02:00
|
|
|
existsStart := time.Now()
|
2026-05-04 12:15:16 +02:00
|
|
|
exists, err := h.proxy.Storage.Exists(r.Context(), storagePath)
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordStorageOperation("read", time.Since(existsStart))
|
2026-05-04 12:15:16 +02:00
|
|
|
if err != nil {
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordStorageError("read")
|
2026-05-04 12:15:16 +02:00
|
|
|
h.proxy.Logger.Error("failed to check gradle build cache entry", "key", key, "error", err)
|
|
|
|
|
http.Error(w, "failed to read cache entry", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !exists {
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordCacheMiss("gradle")
|
2026-05-04 12:15:16 +02:00
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordCacheHit("gradle")
|
2026-05-04 12:15:16 +02:00
|
|
|
|
2026-05-22 13:09:00 +02:00
|
|
|
sizeStart := time.Now()
|
|
|
|
|
size, err := h.proxy.Storage.Size(r.Context(), storagePath)
|
|
|
|
|
metrics.RecordStorageOperation("read", time.Since(sizeStart))
|
|
|
|
|
if err != nil {
|
|
|
|
|
metrics.RecordStorageError("read")
|
|
|
|
|
} else if size >= 0 {
|
2026-05-04 12:15:16 +02:00
|
|
|
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 13:09:00 +02:00
|
|
|
readStart := time.Now()
|
2026-05-04 12:15:16 +02:00
|
|
|
reader, err := h.proxy.Storage.Open(r.Context(), storagePath)
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordStorageOperation("read", time.Since(readStart))
|
2026-05-04 12:15:16 +02:00
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, storage.ErrNotFound) {
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordCacheMiss("gradle")
|
2026-05-04 12:15:16 +02:00
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordStorageError("read")
|
2026-05-04 12:15:16 +02:00
|
|
|
h.proxy.Logger.Error("failed to open gradle build cache entry", "key", key, "error", err)
|
|
|
|
|
http.Error(w, "failed to read cache entry", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = reader.Close() }()
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordCacheHit("gradle")
|
2026-05-04 12:15:16 +02:00
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
_, _ = io.Copy(w, reader)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *GradleBuildCacheHandler) handlePut(w http.ResponseWriter, r *http.Request, key string) {
|
|
|
|
|
storagePath := h.cacheStoragePath(key)
|
|
|
|
|
maxUploadSize := h.proxy.GradleMaxUploadSize
|
|
|
|
|
if maxUploadSize <= 0 {
|
|
|
|
|
maxUploadSize = defaultGradleMaxUploadSize
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
|
|
|
|
|
|
2026-05-22 13:09:00 +02:00
|
|
|
storeStart := time.Now()
|
2026-05-04 12:15:16 +02:00
|
|
|
_, hash, err := h.proxy.Storage.Store(r.Context(), storagePath, r.Body)
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordStorageOperation("write", time.Since(storeStart))
|
2026-05-04 12:15:16 +02:00
|
|
|
if err != nil {
|
|
|
|
|
var maxBytesErr *http.MaxBytesError
|
|
|
|
|
if errors.As(err, &maxBytesErr) {
|
|
|
|
|
http.Error(w, "cache entry too large", http.StatusRequestEntityTooLarge)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 13:09:00 +02:00
|
|
|
metrics.RecordStorageError("write")
|
2026-05-04 12:15:16 +02:00
|
|
|
h.proxy.Logger.Error("failed to store gradle build cache entry", "key", key, "error", err)
|
|
|
|
|
http.Error(w, "failed to write cache entry", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Length", "0")
|
|
|
|
|
w.Header().Set("ETag", `"`+hash+`"`)
|
|
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
|
}
|