pkg-proxy/internal/server/mirror_api.go
Andrew Nesbitt d62c42b8d7
Add mirror command and API for selective package mirroring
Add a `proxy mirror` CLI command and `/api/mirror` API endpoints that
pre-populate the cache from various input sources: individual PURLs,
SBOM files (CycloneDX and SPDX), or full registry enumeration.

The mirror reuses the existing handler.Proxy.GetOrFetchArtifact()
pipeline so cached artifacts are identical to those fetched on demand.
A bounded worker pool controls download parallelism.

Metadata caching is opt-in via `cache_metadata: true` in config (or
PROXY_CACHE_METADATA=true). The mirror command always enables it. When
enabled, upstream metadata responses are stored for offline fallback
with ETag-based conditional revalidation.

New internal/mirror package with Source interface, PURLSource,
SBOMSource, RegistrySource, and async JobStore. New metadata_cache
database table for offline metadata serving.
2026-04-13 09:01:04 +01:00

70 lines
2 KiB
Go

package server
import (
"encoding/json"
"net/http"
"github.com/git-pkgs/proxy/internal/mirror"
"github.com/go-chi/chi/v5"
)
// MirrorAPIHandler handles mirror API requests.
type MirrorAPIHandler struct {
jobs *mirror.JobStore
}
// NewMirrorAPIHandler creates a new mirror API handler.
func NewMirrorAPIHandler(jobs *mirror.JobStore) *MirrorAPIHandler {
return &MirrorAPIHandler{jobs: jobs}
}
// HandleCreate starts a new mirror job.
func (h *MirrorAPIHandler) HandleCreate(w http.ResponseWriter, r *http.Request) {
var req mirror.JobRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
writeJSON(w, map[string]string{"error": "invalid request body"})
return
}
id, err := h.jobs.Create(req)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
writeJSON(w, map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
writeJSON(w, map[string]string{"id": id})
}
// HandleGet returns the status of a mirror job.
func (h *MirrorAPIHandler) HandleGet(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
job := h.jobs.Get(id)
if job == nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
writeJSON(w, map[string]string{"error": "job not found"})
return
}
w.Header().Set("Content-Type", "application/json")
writeJSON(w, job)
}
// HandleCancel cancels a running mirror job.
func (h *MirrorAPIHandler) HandleCancel(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if h.jobs.Cancel(id) {
w.Header().Set("Content-Type", "application/json")
writeJSON(w, map[string]string{"status": "canceled"})
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
writeJSON(w, map[string]string{"error": "job not found or not running"})
}
}