2026-01-20 21:52:44 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
condaUpstream = "https://conda.anaconda.org"
|
2026-03-18 10:59:29 +00:00
|
|
|
minCondaParts = 3 // name-version-build requires at least 3 hyphen-separated parts
|
2026-01-20 21:52:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// CondaHandler handles Conda/Anaconda registry protocol requests.
|
|
|
|
|
type CondaHandler struct {
|
|
|
|
|
proxy *Proxy
|
|
|
|
|
upstreamURL string
|
|
|
|
|
proxyURL string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewCondaHandler creates a new Conda protocol handler.
|
|
|
|
|
func NewCondaHandler(proxy *Proxy, proxyURL string) *CondaHandler {
|
|
|
|
|
return &CondaHandler{
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
upstreamURL: condaUpstream,
|
|
|
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Routes returns the HTTP handler for Conda requests.
|
|
|
|
|
func (h *CondaHandler) Routes() http.Handler {
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
|
|
|
|
|
// Channel index (repodata)
|
|
|
|
|
mux.HandleFunc("GET /{channel}/{arch}/repodata.json", h.proxyUpstream)
|
|
|
|
|
mux.HandleFunc("GET /{channel}/{arch}/repodata.json.bz2", h.proxyUpstream)
|
|
|
|
|
mux.HandleFunc("GET /{channel}/{arch}/current_repodata.json", h.proxyUpstream)
|
|
|
|
|
|
|
|
|
|
// Package downloads (cache these)
|
|
|
|
|
mux.HandleFunc("GET /{channel}/{arch}/{filename}", h.handleDownload)
|
|
|
|
|
|
|
|
|
|
return mux
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleDownload serves a package file, fetching and caching from upstream if needed.
|
|
|
|
|
func (h *CondaHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
channel := r.PathValue("channel")
|
|
|
|
|
arch := r.PathValue("arch")
|
|
|
|
|
filename := r.PathValue("filename")
|
|
|
|
|
|
|
|
|
|
// Only cache actual package files
|
|
|
|
|
if !h.isPackageFile(filename) {
|
|
|
|
|
h.proxyUpstream(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name, version := h.parseFilename(filename)
|
|
|
|
|
if name == "" {
|
|
|
|
|
h.proxyUpstream(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Include channel in package name
|
|
|
|
|
packageName := channel + "/" + name
|
|
|
|
|
|
|
|
|
|
h.proxy.Logger.Info("conda download request",
|
|
|
|
|
"channel", channel, "arch", arch, "name", name, "version", version)
|
|
|
|
|
|
|
|
|
|
upstreamURL := h.upstreamURL + r.URL.Path
|
|
|
|
|
|
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conda", packageName, version, filename, upstreamURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Error("failed to get artifact", "error", err)
|
|
|
|
|
http.Error(w, "failed to fetch package", http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ServeArtifact(w, result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isPackageFile returns true if the filename is a Conda package.
|
|
|
|
|
func (h *CondaHandler) isPackageFile(filename string) bool {
|
|
|
|
|
return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseFilename extracts name and version from a Conda package filename.
|
|
|
|
|
// Conda filenames are: {name}-{version}-{build}.{ext}
|
|
|
|
|
// e.g., "numpy-1.24.0-py311h64a7726_0.conda"
|
|
|
|
|
func (h *CondaHandler) parseFilename(filename string) (name, version string) {
|
|
|
|
|
// Remove extension
|
|
|
|
|
base := filename
|
|
|
|
|
for _, ext := range []string{".conda", ".tar.bz2"} {
|
|
|
|
|
if strings.HasSuffix(base, ext) {
|
|
|
|
|
base = strings.TrimSuffix(base, ext)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Split by hyphens, the format is name-version-build
|
|
|
|
|
// The name can contain hyphens, so we need to find version-build at the end
|
|
|
|
|
parts := strings.Split(base, "-")
|
2026-03-18 10:59:29 +00:00
|
|
|
if len(parts) < minCondaParts {
|
2026-01-20 21:52:44 +00:00
|
|
|
return "", ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build is the last part, version is second to last
|
|
|
|
|
// Everything before is the name
|
|
|
|
|
build := parts[len(parts)-1]
|
|
|
|
|
version = parts[len(parts)-2]
|
|
|
|
|
name = strings.Join(parts[:len(parts)-2], "-")
|
|
|
|
|
|
|
|
|
|
// Version should start with a digit
|
|
|
|
|
if len(version) == 0 || version[0] < '0' || version[0] > '9' {
|
|
|
|
|
return "", ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Include build in version for uniqueness
|
|
|
|
|
_ = build
|
|
|
|
|
|
|
|
|
|
return name, version
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// proxyUpstream forwards a request to Anaconda without caching.
|
|
|
|
|
func (h *CondaHandler) proxyUpstream(w http.ResponseWriter, r *http.Request) {
|
2026-03-18 10:59:29 +00:00
|
|
|
h.proxy.ProxyUpstream(w, r, h.upstreamURL+r.URL.Path, []string{"Accept-Encoding"})
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|