Files
artifactapi/internal/virtual/pypi_merger.go
T
unkinben 79bf0de110
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
feat: virtual PyPI repos can merge local + remote members
The virtual engine now detects local members (repo_type=local) and
generates their package index in-memory instead of trying to fetch
from a non-existent upstream.

- MemberIndex gains RepoType field so mergers use correct URL prefix
  (/api/v1/local/ vs /api/v1/remote/)
- Virtual engine accepts a LocalIndexGenerator interface for producing
  local PyPI indexes
- LocalHandler implements GeneratePyPIPackageHTML for reuse by both
  the direct serving path and the virtual merger
- Includes local PyPI upload support (cherry-picked from benvin/local-pypi)

Tested e2e: local wheel upload + virtual merge + uv pip install from
both direct local and virtual URLs
2026-06-23 22:13:53 +10:00

95 lines
1.8 KiB
Go

package virtual
import (
"fmt"
"sort"
"strings"
"git.unkin.net/unkin/artifactapi/pkg/models"
)
func init() {
RegisterMerger(models.PackagePyPI, &PyPIMerger{})
}
type PyPIMerger struct{}
func (m *PyPIMerger) MergeIndexes(members []MemberIndex, proxyBaseURL string) ([]byte, error) {
links := map[string]string{}
for _, member := range members {
body := string(member.Body)
for _, line := range strings.Split(body, "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "<a ") {
continue
}
href := extractHref(line)
text := extractLinkText(line)
if text == "" {
continue
}
if _, exists := links[text]; exists {
continue
}
if proxyBaseURL != "" && href != "" {
routePrefix := "remote"
if member.RepoType == "local" {
routePrefix = "local"
}
href = fmt.Sprintf("%s/api/v1/%s/%s/%s",
strings.TrimRight(proxyBaseURL, "/"),
routePrefix,
member.RemoteName,
strings.TrimLeft(href, "/"))
}
links[text] = href
}
}
keys := make([]string, 0, len(links))
for k := range links {
keys = append(keys, k)
}
sort.Strings(keys)
var sb strings.Builder
sb.WriteString("<!DOCTYPE html>\n<html><body>\n")
for _, name := range keys {
sb.WriteString(fmt.Sprintf(" <a href=\"%s\">%s</a>\n", links[name], name))
}
sb.WriteString("</body></html>\n")
return []byte(sb.String()), nil
}
func extractHref(tag string) string {
idx := strings.Index(tag, `href="`)
if idx == -1 {
return ""
}
rest := tag[idx+6:]
end := strings.Index(rest, `"`)
if end == -1 {
return rest
}
return rest[:end]
}
func extractLinkText(tag string) string {
start := strings.Index(tag, ">")
if start == -1 {
return ""
}
rest := tag[start+1:]
end := strings.Index(rest, "</a>")
if end == -1 {
return strings.TrimSpace(rest)
}
return strings.TrimSpace(rest[:end])
}