From 6edc22cfcc613f3745f44443e4c697eddd7618b6 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sun, 12 Oct 2025 17:05:32 -0500 Subject: [PATCH] allow fetch tags from pds --- pkg/appview/storage/routing_repository.go | 6 +- pkg/atproto/client.go | 12 ++- pkg/atproto/tag_store.go | 125 ++++++++++++++++++++++ 3 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 pkg/atproto/tag_store.go diff --git a/pkg/appview/storage/routing_repository.go b/pkg/appview/storage/routing_repository.go index 6f2f9da..dc6291c 100644 --- a/pkg/appview/storage/routing_repository.go +++ b/pkg/appview/storage/routing_repository.go @@ -108,9 +108,7 @@ func (r *RoutingRepository) Blobs(ctx context.Context) distribution.BlobStore { } // Tags returns the tag service -// Tags will be handled by ATProto as well +// Tags are stored in ATProto as io.atcr.tag records func (r *RoutingRepository) Tags(ctx context.Context) distribution.TagService { - // For now, delegate to the base repository - // In a full implementation, this would also use ATProto - return r.Repository.Tags(ctx) + return atproto.NewTagStore(r.atprotoClient, r.repositoryName) } diff --git a/pkg/atproto/client.go b/pkg/atproto/client.go index 5f2c3a1..dec939c 100644 --- a/pkg/atproto/client.go +++ b/pkg/atproto/client.go @@ -135,7 +135,11 @@ func (c *Client) GetRecord(ctx context.Context, collection, rkey string) (*Recor return nil, err } - req.Header.Set("Authorization", "Bearer "+c.accessToken) + // Only set Authorization header if we have a token + // Empty Bearer tokens will be rejected by PDS + if c.accessToken != "" { + req.Header.Set("Authorization", "Bearer "+c.accessToken) + } resp, err := c.httpClient.Do(req) if err != nil { @@ -217,7 +221,11 @@ func (c *Client) ListRecords(ctx context.Context, collection string, limit int) return nil, err } - req.Header.Set("Authorization", "Bearer "+c.accessToken) + // Only set Authorization header if we have a token + // Empty Bearer tokens will be rejected by PDS + if c.accessToken != "" { + req.Header.Set("Authorization", "Bearer "+c.accessToken) + } resp, err := c.httpClient.Do(req) if err != nil { diff --git a/pkg/atproto/tag_store.go b/pkg/atproto/tag_store.go new file mode 100644 index 0000000..4160cfa --- /dev/null +++ b/pkg/atproto/tag_store.go @@ -0,0 +1,125 @@ +package atproto + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/distribution/distribution/v3" + "github.com/opencontainers/go-digest" +) + +// TagStore implements distribution.TagService +// It stores tags in ATProto as records +type TagStore struct { + client *Client + repository string +} + +// NewTagStore creates a new ATProto-backed tag store +func NewTagStore(client *Client, repository string) *TagStore { + return &TagStore{ + client: client, + repository: repository, + } +} + +// Get retrieves the descriptor for a tag +func (s *TagStore) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { + // Build record key + rkey := repositoryTagToRKey(s.repository, tag) + + // Fetch tag record from ATProto + record, err := s.client.GetRecord(ctx, TagCollection, rkey) + if err != nil { + return distribution.Descriptor{}, distribution.ErrTagUnknown{Tag: tag} + } + + var tagRecord TagRecord + if err := json.Unmarshal(record.Value, &tagRecord); err != nil { + return distribution.Descriptor{}, fmt.Errorf("failed to unmarshal tag record: %w", err) + } + + // Parse manifest digest + dgst, err := digest.Parse(tagRecord.ManifestDigest) + if err != nil { + return distribution.Descriptor{}, fmt.Errorf("invalid manifest digest in tag record: %w", err) + } + + // Return descriptor pointing to the manifest + return distribution.Descriptor{ + Digest: dgst, + MediaType: "application/vnd.oci.image.manifest.v1+json", + }, nil +} + +// Tag associates a tag with a descriptor (manifest digest) +func (s *TagStore) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { + // Create tag record + tagRecord := NewTagRecord(s.repository, tag, desc.Digest.String()) + + // Store in ATProto + rkey := repositoryTagToRKey(s.repository, tag) + _, err := s.client.PutRecord(ctx, TagCollection, rkey, tagRecord) + if err != nil { + return fmt.Errorf("failed to store tag in ATProto: %w", err) + } + + return nil +} + +// Untag removes a tag +func (s *TagStore) Untag(ctx context.Context, tag string) error { + rkey := repositoryTagToRKey(s.repository, tag) + return s.client.DeleteRecord(ctx, TagCollection, rkey) +} + +// All returns all tags for this repository +func (s *TagStore) All(ctx context.Context) ([]string, error) { + // List all records in the tag collection + records, err := s.client.ListRecords(ctx, TagCollection, 100) + if err != nil { + return nil, fmt.Errorf("failed to list tags: %w", err) + } + + var tags []string + for _, record := range records { + var tagRecord TagRecord + if err := json.Unmarshal(record.Value, &tagRecord); err != nil { + // Skip invalid records + continue + } + + // Only include tags for this repository + if tagRecord.Repository == s.repository { + tags = append(tags, tagRecord.Tag) + } + } + + return tags, nil +} + +// Lookup returns the set of tags for a given digest +func (s *TagStore) Lookup(ctx context.Context, desc distribution.Descriptor) ([]string, error) { + // List all records in the tag collection + records, err := s.client.ListRecords(ctx, TagCollection, 100) + if err != nil { + return nil, fmt.Errorf("failed to list tags: %w", err) + } + + var tags []string + for _, record := range records { + var tagRecord TagRecord + if err := json.Unmarshal(record.Value, &tagRecord); err != nil { + // Skip invalid records + continue + } + + // Only include tags for this repository that match the digest + if tagRecord.Repository == s.repository && tagRecord.ManifestDigest == desc.Digest.String() { + tags = append(tags, tagRecord.Tag) + } + } + + return tags, nil +}