Add local cargo registry type
- serve sparse index and config.json for local cargo repos from a cargo_index table - add Web API publish, yank and unyank under the local repo api root - drop index lines when the crate file is deleted
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/database"
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider/cargo"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
// This file implements the cargo registry Web API (publish, yank, unyank) for
|
||||
// local cargo repositories. config.json points api at /api/v1/local/<repo>, so
|
||||
// cargo appends its own /api/v1/crates/... below that.
|
||||
|
||||
const maxCratePublish = 64 << 20
|
||||
|
||||
func (h *ProxyHandler) localCargoRemote(r *http.Request) (*models.Remote, bool) {
|
||||
remote, err := h.db.GetRemote(r.Context(), chi.URLParam(r, "localName"))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return remote, remote.RepoType == models.RepoTypeLocal && remote.PackageType == models.PackageCargo
|
||||
}
|
||||
|
||||
func cargoError(w http.ResponseWriter, status int, format string, args ...any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"errors": []map[string]string{{"detail": fmt.Sprintf(format, args...)}},
|
||||
})
|
||||
}
|
||||
|
||||
func cargoOK(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func (h *ProxyHandler) cargoPublish(w http.ResponseWriter, r *http.Request) {
|
||||
remote, ok := h.localCargoRemote(r)
|
||||
if !ok {
|
||||
cargoError(w, http.StatusNotFound, "no local cargo registry %q", chi.URLParam(r, "localName"))
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, maxCratePublish+1))
|
||||
if err != nil {
|
||||
cargoError(w, http.StatusBadRequest, "read body: %v", err)
|
||||
return
|
||||
}
|
||||
if len(body) > maxCratePublish {
|
||||
cargoError(w, http.StatusRequestEntityTooLarge, "publish body exceeds %d bytes", maxCratePublish)
|
||||
return
|
||||
}
|
||||
meta, crate, err := cargo.ParsePublish(body)
|
||||
if err != nil {
|
||||
cargoError(w, http.StatusBadRequest, "%v", err)
|
||||
return
|
||||
}
|
||||
line, err := cargo.NewIndexLine(meta, crate)
|
||||
if err != nil {
|
||||
cargoError(w, http.StatusBadRequest, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
existing, err := h.db.ListCargoIndexEntries(ctx, remote.Name, meta.Name)
|
||||
if err != nil {
|
||||
cargoError(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
for _, e := range existing {
|
||||
if e.Name != meta.Name {
|
||||
cargoError(w, http.StatusBadRequest, "crate %q already exists as %q", meta.Name, e.Name)
|
||||
return
|
||||
}
|
||||
if e.Version == meta.Vers {
|
||||
cargoError(w, http.StatusConflict, "crate version `%s@%s` already exists", meta.Name, meta.Vers)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lineJSON, err := json.Marshal(line)
|
||||
if err != nil {
|
||||
cargoError(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
path := cargo.CratePath(meta.Name, meta.Vers)
|
||||
result, err := h.cas.Store(ctx, bytes.NewReader(crate), "application/gzip")
|
||||
if err != nil {
|
||||
cargoError(w, http.StatusInternalServerError, "store failed: %v", err)
|
||||
return
|
||||
}
|
||||
if err := h.db.UpsertBlob(ctx, result.ContentHash, result.S3Key, result.SizeBytes, "application/gzip"); err != nil {
|
||||
cargoError(w, http.StatusInternalServerError, "record blob: %v", err)
|
||||
return
|
||||
}
|
||||
if err := h.db.CreateLocalFile(ctx, remote.Name, path, result.ContentHash); err != nil {
|
||||
if errors.Is(err, database.ErrAlreadyExists) {
|
||||
cargoError(w, http.StatusConflict, "crate version `%s@%s` already exists", meta.Name, meta.Vers)
|
||||
return
|
||||
}
|
||||
cargoError(w, http.StatusInternalServerError, "record file: %v", err)
|
||||
return
|
||||
}
|
||||
entry := &provider.CargoIndexEntry{RepoName: remote.Name, FilePath: path, Name: meta.Name, Version: meta.Vers, Line: string(lineJSON)}
|
||||
if err := h.db.InsertCargoIndexEntry(ctx, entry); err != nil {
|
||||
// Drop the file so a retry is not rejected as a duplicate.
|
||||
_ = h.db.DeleteLocalFile(ctx, remote.Name, path)
|
||||
cargoError(w, http.StatusInternalServerError, "record index: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("cargo: published", "repo", remote.Name, "crate", meta.Name, "version", meta.Vers)
|
||||
cargoOK(w, map[string]any{"warnings": map[string][]string{"invalid_categories": {}, "invalid_badges": {}, "other": {}}})
|
||||
}
|
||||
|
||||
func (h *ProxyHandler) cargoYank(yanked bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
remote, ok := h.localCargoRemote(r)
|
||||
if !ok {
|
||||
cargoError(w, http.StatusNotFound, "no local cargo registry %q", chi.URLParam(r, "localName"))
|
||||
return
|
||||
}
|
||||
name, version := chi.URLParam(r, "crate"), chi.URLParam(r, "version")
|
||||
found, err := h.db.SetCargoYanked(r.Context(), remote.Name, name, version, yanked)
|
||||
if err != nil {
|
||||
cargoError(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
cargoError(w, http.StatusNotFound, "crate version `%s@%s` not found", name, version)
|
||||
return
|
||||
}
|
||||
slog.Info("cargo: yank updated", "repo", remote.Name, "crate", name, "version", version, "yanked", yanked)
|
||||
cargoOK(w, map[string]bool{"ok": true})
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,9 @@ func (h *ProxyHandler) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/remote/{remoteName}/*", h.handleProxy)
|
||||
r.Get("/local/{localName}/*", h.handleLocal)
|
||||
r.Put("/local/{localName}/api/v1/crates/new", h.cargoPublish)
|
||||
r.Delete("/local/{localName}/api/v1/crates/{crate}/{version}/yank", h.cargoYank(true))
|
||||
r.Put("/local/{localName}/api/v1/crates/{crate}/{version}/unyank", h.cargoYank(false))
|
||||
r.Get("/virtual/{virtualName}/*", h.handleVirtual)
|
||||
return r
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user