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) }