mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-28 12:07:00 +00:00
116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
package hold
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/s3"
|
|
)
|
|
|
|
// blobPath converts a digest (e.g., "sha256:abc123...") or temp path to a storage path
|
|
// Distribution stores blobs as: /docker/registry/v2/blobs/{algorithm}/{xx}/{hash}/data
|
|
// where xx is the first 2 characters of the hash for directory sharding
|
|
// NOTE: Path must start with / for filesystem driver
|
|
func blobPath(digest string) string {
|
|
// Handle temp paths (start with uploads/temp-)
|
|
if strings.HasPrefix(digest, "uploads/temp-") {
|
|
return fmt.Sprintf("/docker/registry/v2/%s/data", digest)
|
|
}
|
|
|
|
// Split digest into algorithm and hash
|
|
parts := strings.SplitN(digest, ":", 2)
|
|
if len(parts) != 2 {
|
|
// Fallback for malformed digest
|
|
return fmt.Sprintf("/docker/registry/v2/blobs/%s/data", digest)
|
|
}
|
|
|
|
algorithm := parts[0]
|
|
hash := parts[1]
|
|
|
|
// Use first 2 characters for sharding
|
|
if len(hash) < 2 {
|
|
return fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/data", algorithm, hash)
|
|
}
|
|
|
|
return fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/%s/data", algorithm, hash[:2], hash)
|
|
}
|
|
|
|
// getPresignedURL generates a presigned URL for GET, HEAD, or PUT operations
|
|
func (s *HoldService) getPresignedURL(ctx context.Context, operation PresignedURLOperation, digest string, did string) (string, error) {
|
|
path := blobPath(digest)
|
|
|
|
// Check blob exists for GET/HEAD operations (not for PUT since blob doesn't exist yet)
|
|
if operation == OperationGet || operation == OperationHead {
|
|
if _, err := s.driver.Stat(ctx, path); err != nil {
|
|
return "", fmt.Errorf("blob not found: %w", err)
|
|
}
|
|
}
|
|
|
|
// Check if presigned URLs are disabled
|
|
if s.config.Server.DisablePresignedURLs {
|
|
log.Printf("Presigned URLs disabled, using proxy URL")
|
|
return s.getProxyURL(digest, did), nil
|
|
}
|
|
|
|
// Generate presigned URL if S3 client is available
|
|
if s.s3Client != nil {
|
|
// Build S3 key from blob path
|
|
s3Key := strings.TrimPrefix(path, "/")
|
|
if s.s3PathPrefix != "" {
|
|
s3Key = s.s3PathPrefix + "/" + s3Key
|
|
}
|
|
|
|
// Create appropriate S3 request based on operation
|
|
var req interface {
|
|
Presign(time.Duration) (string, error)
|
|
}
|
|
switch operation {
|
|
case OperationGet:
|
|
// Note: Don't use ResponseContentType - not supported by all S3-compatible services
|
|
req, _ = s.s3Client.GetObjectRequest(&s3.GetObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(s3Key),
|
|
})
|
|
|
|
case OperationHead:
|
|
req, _ = s.s3Client.HeadObjectRequest(&s3.HeadObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(s3Key),
|
|
})
|
|
|
|
case OperationPut:
|
|
req, _ = s.s3Client.PutObjectRequest(&s3.PutObjectInput{
|
|
Bucket: aws.String(s.bucket),
|
|
Key: aws.String(s3Key),
|
|
ContentType: aws.String("application/octet-stream"),
|
|
})
|
|
|
|
default:
|
|
return "", fmt.Errorf("unsupported operation: %s", operation)
|
|
}
|
|
|
|
// Generate presigned URL with 15 minute expiry
|
|
url, err := req.Presign(15 * time.Minute)
|
|
if err != nil {
|
|
log.Printf("[getPresignedURL] Presign FAILED for %s: %v", operation, err)
|
|
log.Printf(" Falling back to proxy URL")
|
|
return s.getProxyURL(digest, did), nil
|
|
}
|
|
|
|
return url, nil
|
|
}
|
|
|
|
// Fallback: return proxy URL through this service
|
|
return s.getProxyURL(digest, did), nil
|
|
}
|
|
|
|
// getProxyURL returns a proxy URL for blob operations (fallback when presigned URLs unavailable)
|
|
func (s *HoldService) getProxyURL(digest, did string) string {
|
|
// All operations use the same proxy endpoint
|
|
return fmt.Sprintf("%s/blobs/%s?did=%s", s.config.Server.PublicURL, digest, did)
|
|
}
|