A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
80
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 {