2026-10-25 21:48:59 +01:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"errors"
|
|
|
|
|
"net/url"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// swiftDocument is a release list or manifest as the upstream sent it.
|
|
|
|
|
type swiftDocument struct {
|
|
|
|
|
body []byte
|
|
|
|
|
contentType string
|
|
|
|
|
// link is the upstream Link header, its values joined, with the
|
|
|
|
|
// upstream's URLs.
|
|
|
|
|
link string
|
|
|
|
|
fetchedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fetchDocument returns the document at upstreamURL, from the metadata cache
|
|
|
|
|
// under cacheKey while it is fresh or when the upstream fails, and otherwise
|
2026-10-26 16:43:42 +01:00
|
|
|
// from upstream, storing it. The upstream Link header is cached with it,
|
|
|
|
|
// since it carries the pagination of release lists and the version-specific
|
|
|
|
|
// manifests a client picks from.
|
2026-10-25 21:48:59 +01:00
|
|
|
func (h *SwiftHandler) fetchDocument(ctx context.Context, cacheKey, upstreamURL, accept string) (*swiftDocument, error) {
|
|
|
|
|
cached := h.loadDocument(ctx, cacheKey)
|
|
|
|
|
if cached != nil && h.proxy.MetadataTTL > 0 && time.Since(cached.fetchedAt) < h.proxy.MetadataTTL {
|
|
|
|
|
return cached, nil
|
|
|
|
|
}
|
|
|
|
|
body, contentType, headers, err := h.fetchMetadataWithHeaders(ctx, upstreamURL, accept)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if cached == nil || errors.Is(err, ErrUpstreamNotFound) {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
h.proxy.Logger.Warn("upstream Swift metadata fetch failed, serving cached copy",
|
|
|
|
|
"url", upstreamURL, "error", err)
|
|
|
|
|
return cached, nil
|
|
|
|
|
}
|
|
|
|
|
doc := &swiftDocument{
|
|
|
|
|
body: body,
|
|
|
|
|
contentType: contentType,
|
|
|
|
|
link: strings.Join(headers.Values("Link"), ", "),
|
|
|
|
|
fetchedAt: time.Now(),
|
|
|
|
|
}
|
|
|
|
|
h.storeDocument(ctx, cacheKey, doc)
|
|
|
|
|
return doc, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *SwiftHandler) documentsCached() bool {
|
|
|
|
|
return h.proxy.CacheMetadata && h.proxy.DB != nil && h.proxy.Storage != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// loadDocument returns the cached document under cacheKey, or nil.
|
|
|
|
|
func (h *SwiftHandler) loadDocument(ctx context.Context, cacheKey string) *swiftDocument {
|
|
|
|
|
if !h.documentsCached() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-10-26 16:43:42 +01:00
|
|
|
entry := h.proxy.storedMetadataEntry("swift", cacheKey, true)
|
|
|
|
|
if entry == nil {
|
2026-10-25 21:48:59 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-10-26 16:43:42 +01:00
|
|
|
reader, err := h.proxy.openMetadataEntry(ctx, "swift", cacheKey, entry)
|
2026-10-25 21:48:59 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = reader.Close() }()
|
|
|
|
|
body, err := h.proxy.ReadMetadata(reader)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
h.proxy.recordMetadataRead(entry)
|
|
|
|
|
return &swiftDocument{
|
|
|
|
|
body: body,
|
|
|
|
|
contentType: entry.ContentType.String,
|
|
|
|
|
link: entry.Link.String,
|
|
|
|
|
fetchedAt: entry.FetchedAt.Time,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// storeDocument caches doc under cacheKey. A failure is logged; the
|
|
|
|
|
// document is still served.
|
|
|
|
|
func (h *SwiftHandler) storeDocument(ctx context.Context, cacheKey string, doc *swiftDocument) {
|
|
|
|
|
if !h.documentsCached() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
storagePath := metadataStoragePath("swift", cacheKey)
|
2026-10-26 16:43:42 +01:00
|
|
|
size, hash, err := h.proxy.Storage.Store(ctx, storagePath, bytes.NewReader(doc.body))
|
2026-10-25 21:48:59 +01:00
|
|
|
if err == nil {
|
|
|
|
|
err = h.proxy.DB.UpsertMetadataCache(&database.MetadataCacheEntry{
|
2026-10-26 16:43:42 +01:00
|
|
|
Ecosystem: "swift",
|
|
|
|
|
Name: cacheKey,
|
|
|
|
|
StoragePath: storagePath,
|
|
|
|
|
Link: sql.NullString{String: doc.link, Valid: doc.link != ""},
|
|
|
|
|
ContentType: sql.NullString{String: doc.contentType, Valid: doc.contentType != ""},
|
|
|
|
|
ContentDigest: sql.NullString{String: "sha256:" + hash, Valid: hash != ""},
|
|
|
|
|
Size: sql.NullInt64{Int64: size, Valid: true},
|
|
|
|
|
FetchedAt: sql.NullTime{Time: doc.fetchedAt, Valid: true},
|
2026-10-25 21:48:59 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Warn("failed to cache Swift metadata", "key", cacheKey, "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func swiftReleasesCacheKey(scope, name, rawQuery string) string {
|
|
|
|
|
return swiftMetadataCacheKey("releases", scope, name, rawQuery)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// swiftManifestCacheKey keys a manifest by its swift-version as a tools
|
|
|
|
|
// version, so the 5.8 of a manifest's alternate link and the 5.8.0 a client
|
|
|
|
|
// requests for it share a key.
|
|
|
|
|
func swiftManifestCacheKey(scope, name, version, rawQuery string) string {
|
|
|
|
|
if query, err := url.ParseQuery(rawQuery); err == nil {
|
|
|
|
|
if tools := query.Get("swift-version"); tools != "" {
|
|
|
|
|
for strings.Count(tools, ".") > 0 && strings.HasSuffix(tools, ".0") {
|
|
|
|
|
tools = strings.TrimSuffix(tools, ".0")
|
|
|
|
|
}
|
|
|
|
|
query.Set("swift-version", tools)
|
|
|
|
|
}
|
|
|
|
|
rawQuery = query.Encode()
|
|
|
|
|
}
|
|
|
|
|
return swiftMetadataCacheKey("manifest", scope, name, version, rawQuery)
|
|
|
|
|
}
|