A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix tag and manifest deletion

+68 -45
+9 -11
deploy/.env.prod.template
··· 49 49 AWS_ACCESS_KEY_ID= 50 50 AWS_SECRET_ACCESS_KEY= 51 51 52 - # S3 Region 52 + # S3 Region (for distribution S3 driver) 53 53 # UpCloud regions: us-chi1, us-nyc1, de-fra1, uk-lon1, sg-sin1, etc. 54 54 # Default: us-chi1 55 - AWS_REGION=us-chi1 55 + S3_REGION=us-chi1 56 56 57 57 # S3 Bucket Name 58 58 # Create this bucket in UpCloud Object Storage 59 59 # Example: atcr-blobs 60 60 S3_BUCKET=atcr 61 61 62 - # S3 Endpoint (for custom domain or UpCloud endpoint) 63 - # If using custom domain (blobs.atcr.io): 64 - # S3_ENDPOINT=https://blobs.atcr.io 65 - # If using UpCloud default endpoint: 66 - # S3_ENDPOINT=https://s3.us-chi1.upcloudobjects.com 62 + # S3 Endpoint 63 + # Get this from UpCloud Console → Storage → Object Storage → Your bucket → "S3 endpoint" 64 + # Format: https://[bucket-id].upcloudobjects.com 65 + # Example: https://6vmss.upcloudobjects.com 67 66 # 68 - # IMPORTANT: If using custom domain, create CNAME: 69 - # blobs.atcr.io → [bucket].us-chi1.upcloudobjects.com 70 - # (with Cloudflare proxy DISABLED - gray cloud) 71 - S3_ENDPOINT=https://blobs.atcr.io 67 + # NOTE: Use the bucket-specific endpoint, NOT a custom domain 68 + # Custom domains break presigned URL generation 69 + S3_ENDPOINT=https://6vmss.upcloudobjects.com 72 70 73 71 # S3 Region Endpoint (alternative to S3_ENDPOINT) 74 72 # Use this if your S3 driver requires region-specific endpoint format
+24 -3
pkg/appview/db/queries.go
··· 535 535 } 536 536 537 537 // DeleteManifest deletes a manifest and its associated layers 538 + // If repository is empty, deletes all manifests matching did and digest 538 539 func DeleteManifest(db *sql.DB, did, repository, digest string) error { 539 - _, err := db.Exec(` 540 - DELETE FROM manifests WHERE did = ? AND repository = ? AND digest = ? 541 - `, did, repository, digest) 540 + var err error 541 + if repository == "" { 542 + // Delete by DID + digest only (used when repository is unknown, e.g., Jetstream DELETE events) 543 + _, err = db.Exec(`DELETE FROM manifests WHERE did = ? AND digest = ?`, did, digest) 544 + } else { 545 + // Delete specific manifest 546 + _, err = db.Exec(`DELETE FROM manifests WHERE did = ? AND repository = ? AND digest = ?`, did, repository, digest) 547 + } 542 548 return err 543 549 } 544 550 ··· 1015 1021 } 1016 1022 1017 1023 return stars, rows.Err() 1024 + } 1025 + 1026 + // CleanupOrphanedTags removes tags whose manifest digest no longer exists 1027 + // This handles cases where manifests were deleted but tags pointing to them remain 1028 + func CleanupOrphanedTags(db *sql.DB, did string) error { 1029 + _, err := db.Exec(` 1030 + DELETE FROM tags 1031 + WHERE did = ? 1032 + AND NOT EXISTS ( 1033 + SELECT 1 FROM manifests 1034 + WHERE manifests.did = tags.did 1035 + AND manifests.digest = tags.digest 1036 + ) 1037 + `, did) 1038 + return err 1018 1039 } 1019 1040 1020 1041 // DeleteStarsNotInList deletes stars from the database that are not in the provided list
+7
pkg/appview/jetstream/backfill.go
··· 198 198 fmt.Printf("WARNING: Failed to reconcile deletions for %s: %v\n", did, err) 199 199 } 200 200 201 + // After processing manifests, clean up orphaned tags (tags pointing to non-existent manifests) 202 + if collection == atproto.ManifestCollection { 203 + if err := db.CleanupOrphanedTags(b.db, did); err != nil { 204 + fmt.Printf("WARNING: Failed to cleanup orphaned tags for %s: %v\n", did, err) 205 + } 206 + } 207 + 201 208 return recordCount, nil 202 209 } 203 210
+8 -31
pkg/appview/jetstream/worker.go
··· 6 6 "encoding/json" 7 7 "fmt" 8 8 "net/url" 9 - "strings" 10 9 "sync" 11 10 "time" 12 11 ··· 418 417 } 419 418 420 419 if commit.Operation == "delete" { 421 - // Delete manifest 422 - repo := extractRepoFromRKey(commit.RKey) 420 + // Delete manifest - rkey is just the digest, repository is not encoded 423 421 digest := commit.RKey 424 - return db.DeleteManifest(w.db, commit.DID, repo, digest) 422 + if err := db.DeleteManifest(w.db, commit.DID, "", digest); err != nil { 423 + return err 424 + } 425 + // Clean up any orphaned tags pointing to this manifest 426 + return db.CleanupOrphanedTags(w.db, commit.DID) 425 427 } 426 428 427 429 // Parse manifest record ··· 497 499 } 498 500 499 501 if commit.Operation == "delete" { 500 - // Delete tag 501 - parts := strings.Split(commit.RKey, "/") 502 - if len(parts) < 2 { 503 - return fmt.Errorf("invalid tag rkey: %s", commit.RKey) 504 - } 505 - repo := strings.Join(parts[:len(parts)-1], "/") 506 - tag := parts[len(parts)-1] 502 + // Delete tag - decode rkey back to repository and tag 503 + repo, tag := atproto.RKeyToRepositoryTag(commit.RKey) 507 504 return db.DeleteTag(w.db, commit.DID, repo, tag) 508 505 } 509 506 ··· 606 603 Status string `json:"status,omitempty"` 607 604 } 608 605 609 - // Helper functions 610 - 611 - func extractRepoFromRKey(rkey string) string { 612 - // RKey format: <digest> or <repo>/<digest> 613 - // For manifest, it's just the digest 614 - parts := strings.Split(rkey, "/") 615 - if len(parts) > 1 { 616 - return parts[0] 617 - } 618 - return "" 619 - } 620 - 621 - func calculateManifestSize(manifest *atproto.ManifestRecord) int64 { 622 - var total int64 623 - total += manifest.Config.Size 624 - for _, layer := range manifest.Layers { 625 - total += layer.Size 626 - } 627 - return total 628 - }
+20
pkg/atproto/manifest_store.go
··· 207 207 return key 208 208 } 209 209 210 + // RKeyToRepositoryTag converts an ATProto record key back to repository and tag 211 + // This is the inverse of repositoryTagToRKey 212 + // Note: If the tag contains underscores, this will split on the LAST underscore 213 + func RKeyToRepositoryTag(rkey string) (repository, tag string) { 214 + // Find the last underscore to split repository and tag 215 + lastUnderscore := strings.LastIndex(rkey, "_") 216 + if lastUnderscore == -1 { 217 + // No underscore found - treat entire string as tag with empty repository 218 + return "", rkey 219 + } 220 + 221 + repository = rkey[:lastUnderscore] 222 + tag = rkey[lastUnderscore+1:] 223 + 224 + // Convert dashes back to slashes in repository 225 + repository = strings.ReplaceAll(repository, "-", "/") 226 + 227 + return repository, tag 228 + } 229 + 210 230 // GetLastFetchedHoldEndpoint returns the hold endpoint from the most recently fetched manifest 211 231 // This is used by the routing repository to cache the hold for blob requests 212 232 func (s *ManifestStore) GetLastFetchedHoldEndpoint() string {