mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
The case-variant lookup filtered on lower(name), which no index covers, so a legacy row's first lookup scanned every metadata_cache row in its ecosystem. Reclaim already skips a queued path a row still points at, so each row now queues just its own legacy blob once it has moved off it. On case-insensitive storage a case variant still at its legacy path can lose its blob that way, and is then fetched again.
134 lines
4.4 KiB
Go
134 lines
4.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
)
|
|
|
|
// metadataStoragePath builds the storage path for cached metadata. Cache
|
|
// keys are case-sensitive, but on a case-insensitive filesystem (APFS, NTFS)
|
|
// keys that differ only in case, such as Go's github.com/Sirupsen/logrus and
|
|
// github.com/sirupsen/logrus, would share one file. Keys may also hold names
|
|
// Windows reserves or end in a dot. So the path carries a hash of the exact
|
|
// key. Keys that already are lowercase SHA-256 hex digests are used as they
|
|
// are, so their existing entries stay valid.
|
|
func metadataStoragePath(ecosystem, cacheKey string) string {
|
|
if !isSHA256Hex(cacheKey) {
|
|
digest := sha256.Sum256([]byte(cacheKey))
|
|
cacheKey = hex.EncodeToString(digest[:])
|
|
}
|
|
return legacyMetadataStoragePath(ecosystem, cacheKey)
|
|
}
|
|
|
|
// legacyMetadataStoragePath is where metadata was stored before paths were
|
|
// hashed: under the cache key itself.
|
|
func legacyMetadataStoragePath(ecosystem, cacheKey string) string {
|
|
return "_metadata/" + ecosystem + "/" + cacheKey + "/metadata"
|
|
}
|
|
|
|
func isSHA256Hex(s string) bool {
|
|
if len(s) != 2*sha256.Size {
|
|
return false
|
|
}
|
|
for i := range len(s) {
|
|
c := s[i]
|
|
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// migrateLegacyMetadataEntry handles a cache row whose blob is still at its
|
|
// legacy, unhashed path. On a case-insensitive filesystem keys differing only
|
|
// in case shared that blob, so it is served only if it matches the row's digest.
|
|
func (p *Proxy) migrateLegacyMetadataEntry(ctx context.Context, ecosystem, cacheKey string, entry *database.MetadataCacheEntry) *database.MetadataCacheEntry {
|
|
if entry == nil || p.Storage == nil {
|
|
return entry
|
|
}
|
|
legacy := legacyMetadataStoragePath(ecosystem, cacheKey)
|
|
hashed := metadataStoragePath(ecosystem, cacheKey)
|
|
if entry.StoragePath != legacy || legacy == hashed {
|
|
return entry
|
|
}
|
|
var matches bool
|
|
if entry.ContentDigest.Valid {
|
|
digest, err := p.storedDigest(ctx, legacy)
|
|
if err != nil {
|
|
p.Logger.Warn("failed to read legacy metadata blob", "ecosystem", ecosystem, "key", cacheKey, "error", err)
|
|
return nil
|
|
}
|
|
matches = digest == entry.ContentDigest.String
|
|
}
|
|
if !matches {
|
|
if err := p.DB.ForgetMetadataDocument(ecosystem, cacheKey, legacy, hashed); err != nil {
|
|
p.Logger.Warn("failed to drop legacy metadata row", "ecosystem", ecosystem, "key", cacheKey, "error", err)
|
|
return nil
|
|
}
|
|
p.queueLegacyMetadataBlob(ecosystem, cacheKey, legacy)
|
|
return nil
|
|
}
|
|
if !p.moveLegacyMetadataBlob(ctx, entry, legacy, hashed) {
|
|
return entry
|
|
}
|
|
p.queueLegacyMetadataBlob(ecosystem, cacheKey, legacy)
|
|
moved := *entry
|
|
moved.StoragePath = hashed
|
|
return &moved
|
|
}
|
|
|
|
// storedDigest hashes the blob at path as cacheMetadataBlob records it.
|
|
func (p *Proxy) storedDigest(ctx context.Context, path string) (string, error) {
|
|
r, err := p.Storage.Open(ctx, path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() { _ = r.Close() }()
|
|
h := sha256.New()
|
|
if _, err := io.Copy(h, r); err != nil {
|
|
return "", err
|
|
}
|
|
return "sha256:" + hex.EncodeToString(h.Sum(nil)), nil
|
|
}
|
|
|
|
// moveLegacyMetadataBlob copies entry's verified blob from legacy to hashed
|
|
// and points the row there, reporting whether it did.
|
|
func (p *Proxy) moveLegacyMetadataBlob(ctx context.Context, entry *database.MetadataCacheEntry, legacy, hashed string) bool {
|
|
logFailure := func(err error) {
|
|
p.Logger.Warn("failed to move legacy metadata blob", "ecosystem", entry.Ecosystem, "key", entry.Name, "error", err)
|
|
}
|
|
r, err := p.Storage.Open(ctx, legacy)
|
|
if err != nil {
|
|
logFailure(err)
|
|
return false
|
|
}
|
|
defer func() { _ = r.Close() }()
|
|
_, hash, err := p.Storage.Store(ctx, hashed, r)
|
|
if err != nil {
|
|
logFailure(err)
|
|
return false
|
|
}
|
|
if "sha256:"+hash != entry.ContentDigest.String {
|
|
logFailure(fmt.Errorf("blob changed while being copied"))
|
|
return false
|
|
}
|
|
moved, err := p.DB.MoveMetadataCache(entry.Ecosystem, entry.Name, legacy, hashed, entry.ContentDigest.String)
|
|
if err != nil {
|
|
logFailure(err)
|
|
return false
|
|
}
|
|
return moved
|
|
}
|
|
|
|
// queueLegacyMetadataBlob queues a row's legacy blob for deletion once the row
|
|
// no longer reads it. Reclaim skips it while another row still points there.
|
|
func (p *Proxy) queueLegacyMetadataBlob(ecosystem, cacheKey, legacy string) {
|
|
if err := p.DB.QueuePendingDelete(legacy); err != nil {
|
|
p.Logger.Warn("failed to queue legacy metadata blob for deletion", "ecosystem", ecosystem, "key", cacheKey, "error", err)
|
|
}
|
|
}
|