Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-13 17:49:15 -05:00
pkg-proxy/internal/handler/cran.go

178 lines
5.5 KiB
Go
Raw Permalink Normal View History

2026-01-20 21:52:44 +00:00
package handler
import (
"net/http"
"strings"
"github.com/git-pkgs/proxy/internal/retention"
2026-01-20 21:52:44 +00:00
)
const (
cranUpstream = "https://cloud.r-project.org"
)
// Source and binary packages are stored under pkg:cran/{name}, the name from
// the filename with its case kept, whether they came from src/contrib, its
// Archive or a binary tree. purl keeps cran names as they are, so package
// rules use the default key mapping.
func init() {
retention.Default.Register(retention.Spec{Key: "cran", CanonicalPackage: retention.DefaultCanonical("cran")})
}
2026-01-20 21:52:44 +00:00
// CRANHandler handles CRAN (R) registry protocol requests.
type CRANHandler struct {
proxy *Proxy
upstreamURL string
proxyURL string
}
// NewCRANHandler creates a new CRAN protocol handler.
func NewCRANHandler(proxy *Proxy, proxyURL string) *CRANHandler {
return &CRANHandler{
proxy: proxy,
upstreamURL: cranUpstream,
proxyURL: strings.TrimSuffix(proxyURL, "/"),
}
}
// NewCRANHandlerWithUpstream creates a CRAN handler with a custom upstream.
func NewCRANHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *CRANHandler {
h := NewCRANHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, cranUpstream)
return h
}
2026-01-20 21:52:44 +00:00
// Routes returns the HTTP handler for CRAN requests.
func (h *CRANHandler) Routes() http.Handler {
mux := http.NewServeMux()
// Package indexes
mux.HandleFunc("GET /src/contrib/PACKAGES", h.proxyCached)
mux.HandleFunc("GET /src/contrib/PACKAGES.gz", h.proxyCached)
mux.HandleFunc("GET /src/contrib/PACKAGES.rds", h.proxyCached)
2026-01-20 21:52:44 +00:00
// Binary package indexes
mux.HandleFunc("GET /bin/{platform}/contrib/{rversion}/PACKAGES", h.proxyCached)
mux.HandleFunc("GET /bin/{platform}/contrib/{rversion}/PACKAGES.gz", h.proxyCached)
mux.HandleFunc("GET /bin/{platform}/contrib/{rversion}/PACKAGES.rds", h.proxyCached)
2026-01-20 21:52:44 +00:00
// Source package downloads
mux.HandleFunc("GET /src/contrib/{filename}", h.handleSourceDownload)
mux.HandleFunc("GET /src/contrib/Archive/{name}/{filename}", h.handleSourceDownload)
// Binary package downloads
mux.HandleFunc("GET /bin/{platform}/contrib/{rversion}/{filename}", h.handleBinaryDownload)
return mux
}
// handleSourceDownload serves a source package, fetching and caching from upstream.
func (h *CRANHandler) handleSourceDownload(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
archiveName := r.PathValue("name") // empty for current packages
if !strings.HasSuffix(filename, ".tar.gz") {
h.proxyUpstream(w, r)
return
}
name, version := h.parseSourceFilename(filename)
if name == "" {
h.proxyUpstream(w, r)
return
}
h.proxy.Logger.Info("cran source download",
"name", name, "version", version, "archive", archiveName)
upstreamURL := h.upstreamURL + r.URL.Path
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, version, filename, upstreamURL)
if err != nil {
h.proxy.serveArtifactError(w, err, "failed to fetch package")
2026-01-20 21:52:44 +00:00
return
}
Support HTTP byte-range requests for local file cached artifact downloads (#404) * feat(storage): expose efficient seek for local files Open file-backed artifacts with os.Open so local readers support efficient seeking. Leave cloud storage readers unchanged. Keep the legacy sidecar mapping test focused on fileblob's reader: Blob.Open now reads local files directly and no longer consults the sidecar. Continue checking sidecar cleanup and post-store reads. * feat(handler): preserve efficient seek through integrity checks Keep the cache result's single Reader field and preserve io.Seeker when the underlying storage reader supports efficient seeking. Sequential reads continue through whole-object integrity verification. A successful seek switches subsequent reads to the source for range responses, where full-object verification is not possible. Test both modes and verify the source is closed once. * feat(handler): add request-aware byte range responses Add a request-aware artifact serving helper for bounded, open-ended, and suffix byte ranges, including If-Range handling, 206 responses, and 416 responses for valid unsatisfiable ranges. Ignore malformed and multi-range requests by serving the full artifact. Advertise byte-range support only for seekable readers, preserving HEAD, redirect, and non-seekable behavior. Clarify that net/http recovers ErrAbortHandler per request and keeps the server running. * feat(handler): enable byte ranges across artifact downloads Forward each artifact request to the shared range-aware response helper, including cached generic release assets and OCI blobs. This lets the handler honor ranges only when its reader supports efficient seeking, without changing direct-storage redirect behavior. Add warm-cache endpoint tests that assert partial response headers and bytes without an upstream refetch. Document supported range behavior, unsupported reader fallback, and the integrity limitation of partial reads.
2026-10-04 09:48:41 -07:00
ServeArtifactRequest(w, r, result)
2026-01-20 21:52:44 +00:00
}
// handleBinaryDownload serves a binary package, fetching and caching from upstream.
func (h *CRANHandler) handleBinaryDownload(w http.ResponseWriter, r *http.Request) {
platform := r.PathValue("platform")
rversion := r.PathValue("rversion")
filename := r.PathValue("filename")
if !h.isBinaryPackage(filename) {
h.proxyUpstream(w, r)
return
}
name, version := h.parseBinaryFilename(filename)
if name == "" {
h.proxyUpstream(w, r)
return
}
// Include platform and R version in stored version
storageVersion := version + "_" + platform + "_" + rversion
h.proxy.Logger.Info("cran binary download",
"name", name, "version", version, "platform", platform, "rversion", rversion)
upstreamURL := h.upstreamURL + r.URL.Path
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, storageVersion, filename, upstreamURL)
if err != nil {
h.proxy.serveArtifactError(w, err, "failed to fetch package")
2026-01-20 21:52:44 +00:00
return
}
Support HTTP byte-range requests for local file cached artifact downloads (#404) * feat(storage): expose efficient seek for local files Open file-backed artifacts with os.Open so local readers support efficient seeking. Leave cloud storage readers unchanged. Keep the legacy sidecar mapping test focused on fileblob's reader: Blob.Open now reads local files directly and no longer consults the sidecar. Continue checking sidecar cleanup and post-store reads. * feat(handler): preserve efficient seek through integrity checks Keep the cache result's single Reader field and preserve io.Seeker when the underlying storage reader supports efficient seeking. Sequential reads continue through whole-object integrity verification. A successful seek switches subsequent reads to the source for range responses, where full-object verification is not possible. Test both modes and verify the source is closed once. * feat(handler): add request-aware byte range responses Add a request-aware artifact serving helper for bounded, open-ended, and suffix byte ranges, including If-Range handling, 206 responses, and 416 responses for valid unsatisfiable ranges. Ignore malformed and multi-range requests by serving the full artifact. Advertise byte-range support only for seekable readers, preserving HEAD, redirect, and non-seekable behavior. Clarify that net/http recovers ErrAbortHandler per request and keeps the server running. * feat(handler): enable byte ranges across artifact downloads Forward each artifact request to the shared range-aware response helper, including cached generic release assets and OCI blobs. This lets the handler honor ranges only when its reader supports efficient seeking, without changing direct-storage redirect behavior. Add warm-cache endpoint tests that assert partial response headers and bytes without an upstream refetch. Document supported range behavior, unsupported reader fallback, and the integrity limitation of partial reads.
2026-10-04 09:48:41 -07:00
ServeArtifactRequest(w, r, result)
2026-01-20 21:52:44 +00:00
}
// parseSourceFilename extracts name and version from a CRAN source filename.
// Format: {name}_{version}.tar.gz
func (h *CRANHandler) parseSourceFilename(filename string) (name, version string) {
base := strings.TrimSuffix(filename, ".tar.gz")
idx := strings.LastIndex(base, "_")
if idx < 0 {
return "", ""
}
return base[:idx], base[idx+1:]
}
// parseBinaryFilename extracts name and version from a CRAN binary filename.
// Windows: {name}_{version}.zip
// macOS: {name}_{version}.tgz
func (h *CRANHandler) parseBinaryFilename(filename string) (name, version string) {
base := filename
for _, ext := range []string{".zip", ".tgz"} {
if strings.HasSuffix(base, ext) {
base = strings.TrimSuffix(base, ext)
break
}
}
idx := strings.LastIndex(base, "_")
if idx < 0 {
return "", ""
}
return base[:idx], base[idx+1:]
}
// isBinaryPackage returns true if the filename is a CRAN binary package.
func (h *CRANHandler) isBinaryPackage(filename string) bool {
return strings.HasSuffix(filename, ".zip") || strings.HasSuffix(filename, ".tgz")
}
// proxyCached forwards a metadata request with caching.
func (h *CRANHandler) proxyCached(w http.ResponseWriter, r *http.Request) {
cacheKey := strings.TrimPrefix(r.URL.Path, "/")
cacheKey = strings.ReplaceAll(cacheKey, "/", "_")
h.proxy.ProxyCached(w, r, h.upstreamURL+r.URL.Path, "cran", cacheKey, "*/*")
}
2026-01-20 21:52:44 +00:00
// proxyUpstream forwards a request to CRAN without caching.
func (h *CRANHandler) proxyUpstream(w http.ResponseWriter, r *http.Request) {
h.proxy.ProxyUpstream(w, r, h.upstreamURL+r.URL.Path, []string{headerAcceptEncoding})
2026-01-20 21:52:44 +00:00
}