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.

allow fetch tags from pds

+137 -6
+2 -4
pkg/appview/storage/routing_repository.go
··· 108 108 } 109 109 110 110 // Tags returns the tag service 111 - // Tags will be handled by ATProto as well 111 + // Tags are stored in ATProto as io.atcr.tag records 112 112 func (r *RoutingRepository) Tags(ctx context.Context) distribution.TagService { 113 - // For now, delegate to the base repository 114 - // In a full implementation, this would also use ATProto 115 - return r.Repository.Tags(ctx) 113 + return atproto.NewTagStore(r.atprotoClient, r.repositoryName) 116 114 }
+10 -2
pkg/atproto/client.go
··· 135 135 return nil, err 136 136 } 137 137 138 - req.Header.Set("Authorization", "Bearer "+c.accessToken) 138 + // Only set Authorization header if we have a token 139 + // Empty Bearer tokens will be rejected by PDS 140 + if c.accessToken != "" { 141 + req.Header.Set("Authorization", "Bearer "+c.accessToken) 142 + } 139 143 140 144 resp, err := c.httpClient.Do(req) 141 145 if err != nil { ··· 217 221 return nil, err 218 222 } 219 223 220 - req.Header.Set("Authorization", "Bearer "+c.accessToken) 224 + // Only set Authorization header if we have a token 225 + // Empty Bearer tokens will be rejected by PDS 226 + if c.accessToken != "" { 227 + req.Header.Set("Authorization", "Bearer "+c.accessToken) 228 + } 221 229 222 230 resp, err := c.httpClient.Do(req) 223 231 if err != nil {
+125
pkg/atproto/tag_store.go
··· 1 + package atproto 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + 8 + "github.com/distribution/distribution/v3" 9 + "github.com/opencontainers/go-digest" 10 + ) 11 + 12 + // TagStore implements distribution.TagService 13 + // It stores tags in ATProto as records 14 + type TagStore struct { 15 + client *Client 16 + repository string 17 + } 18 + 19 + // NewTagStore creates a new ATProto-backed tag store 20 + func NewTagStore(client *Client, repository string) *TagStore { 21 + return &TagStore{ 22 + client: client, 23 + repository: repository, 24 + } 25 + } 26 + 27 + // Get retrieves the descriptor for a tag 28 + func (s *TagStore) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { 29 + // Build record key 30 + rkey := repositoryTagToRKey(s.repository, tag) 31 + 32 + // Fetch tag record from ATProto 33 + record, err := s.client.GetRecord(ctx, TagCollection, rkey) 34 + if err != nil { 35 + return distribution.Descriptor{}, distribution.ErrTagUnknown{Tag: tag} 36 + } 37 + 38 + var tagRecord TagRecord 39 + if err := json.Unmarshal(record.Value, &tagRecord); err != nil { 40 + return distribution.Descriptor{}, fmt.Errorf("failed to unmarshal tag record: %w", err) 41 + } 42 + 43 + // Parse manifest digest 44 + dgst, err := digest.Parse(tagRecord.ManifestDigest) 45 + if err != nil { 46 + return distribution.Descriptor{}, fmt.Errorf("invalid manifest digest in tag record: %w", err) 47 + } 48 + 49 + // Return descriptor pointing to the manifest 50 + return distribution.Descriptor{ 51 + Digest: dgst, 52 + MediaType: "application/vnd.oci.image.manifest.v1+json", 53 + }, nil 54 + } 55 + 56 + // Tag associates a tag with a descriptor (manifest digest) 57 + func (s *TagStore) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { 58 + // Create tag record 59 + tagRecord := NewTagRecord(s.repository, tag, desc.Digest.String()) 60 + 61 + // Store in ATProto 62 + rkey := repositoryTagToRKey(s.repository, tag) 63 + _, err := s.client.PutRecord(ctx, TagCollection, rkey, tagRecord) 64 + if err != nil { 65 + return fmt.Errorf("failed to store tag in ATProto: %w", err) 66 + } 67 + 68 + return nil 69 + } 70 + 71 + // Untag removes a tag 72 + func (s *TagStore) Untag(ctx context.Context, tag string) error { 73 + rkey := repositoryTagToRKey(s.repository, tag) 74 + return s.client.DeleteRecord(ctx, TagCollection, rkey) 75 + } 76 + 77 + // All returns all tags for this repository 78 + func (s *TagStore) All(ctx context.Context) ([]string, error) { 79 + // List all records in the tag collection 80 + records, err := s.client.ListRecords(ctx, TagCollection, 100) 81 + if err != nil { 82 + return nil, fmt.Errorf("failed to list tags: %w", err) 83 + } 84 + 85 + var tags []string 86 + for _, record := range records { 87 + var tagRecord TagRecord 88 + if err := json.Unmarshal(record.Value, &tagRecord); err != nil { 89 + // Skip invalid records 90 + continue 91 + } 92 + 93 + // Only include tags for this repository 94 + if tagRecord.Repository == s.repository { 95 + tags = append(tags, tagRecord.Tag) 96 + } 97 + } 98 + 99 + return tags, nil 100 + } 101 + 102 + // Lookup returns the set of tags for a given digest 103 + func (s *TagStore) Lookup(ctx context.Context, desc distribution.Descriptor) ([]string, error) { 104 + // List all records in the tag collection 105 + records, err := s.client.ListRecords(ctx, TagCollection, 100) 106 + if err != nil { 107 + return nil, fmt.Errorf("failed to list tags: %w", err) 108 + } 109 + 110 + var tags []string 111 + for _, record := range records { 112 + var tagRecord TagRecord 113 + if err := json.Unmarshal(record.Value, &tagRecord); err != nil { 114 + // Skip invalid records 115 + continue 116 + } 117 + 118 + // Only include tags for this repository that match the digest 119 + if tagRecord.Repository == s.repository && tagRecord.ManifestDigest == desc.Digest.String() { 120 + tags = append(tags, tagRecord.Tag) 121 + } 122 + } 123 + 124 + return tags, nil 125 + }