A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
77
fork

Configure Feed

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

remove unused files, add workflow for tests

+26 -156
+17
.tangled/workflows/tests.yml
··· 1 + when: 2 + - event: ["push"] 3 + branch: ["main"] 4 + - event: ["pull_request"] 5 + branch: ["main"] 6 + 7 + engine: "nixery" 8 + 9 + dependencies: 10 + nixpkgs: 11 + - git 12 + - go 13 + 14 + steps: 15 + - name: Run Tests 16 + command: | 17 + go test -cover ./...
-6
pkg/hold/handlers.go
··· 1 - package hold 2 - 3 - // This file previously contained legacy HTTP handlers that have been replaced by XRPC endpoints. 4 - // The handlers (HandleProxyGet, HandleProxyPut, HandleMultipartPartUpload) are no longer needed 5 - // as all blob operations now go through the XRPC com.atproto.repo.uploadBlob and 6 - // com.atproto.sync.getBlob endpoints.
+9 -62
pkg/hold/multipart.go
··· 5 5 "crypto/sha256" 6 6 "encoding/hex" 7 7 "fmt" 8 - "io" 9 8 "log" 10 - "net/http" 11 9 "sync" 12 10 "time" 13 11 ··· 262 260 return session.UploadID, Buffered, nil 263 261 } 264 262 265 - // GetPartUploadURL generates a URL for uploading a part 266 - // For S3Native: returns presigned URL 267 - // For Buffered: returns proxy endpoint 263 + // GetPartUploadURL generates a presigned URL for uploading a part 264 + // Only used for S3Native mode - Buffered mode is handled by blobstore adapter 268 265 func (s *HoldService) GetPartUploadURL(ctx context.Context, session *MultipartSession, partNumber int, did string) (string, error) { 269 - if session.Mode == S3Native { 270 - // Generate S3 presigned URL for this part 271 - url, err := s.getPartPresignedURL(ctx, session.Digest, session.S3UploadID, partNumber) 272 - if err != nil { 273 - return "", fmt.Errorf("failed to generate S3 part URL: %w", err) 274 - } 275 - return url, nil 266 + if session.Mode != S3Native { 267 + return "", fmt.Errorf("GetPartUploadURL only supports S3Native mode") 276 268 } 277 269 278 - // Buffered mode: return proxy endpoint 279 - // url := fmt.Sprintf("%s/xrpc/com.atproto.repo.uploadBlob", s.config.Server.PublicURL) 280 - 281 - url := fmt.Sprintf("%s/multipart-parts/%s/%d?did=%s", 282 - s.config.Server.PublicURL, session.UploadID, partNumber, did) 270 + // Generate S3 presigned URL for this part 271 + url, err := s.getPartPresignedURL(ctx, session.Digest, session.S3UploadID, partNumber) 272 + if err != nil { 273 + return "", fmt.Errorf("failed to generate S3 part URL: %w", err) 274 + } 283 275 return url, nil 284 276 } 285 277 ··· 342 334 return nil 343 335 } 344 336 345 - // HandleMultipartPartUpload handles uploading a part in buffered mode 346 - // This is a new endpoint: PUT /multipart-parts/{uploadID}/{partNumber} 347 - func (s *HoldService) HandleMultipartPartUpload(w http.ResponseWriter, r *http.Request, uploadID string, partNumber int, did string, manager *MultipartManager) { 348 - if r.Method != http.MethodPut { 349 - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) 350 - return 351 - } 352 - 353 - // Get session 354 - session, err := manager.GetSession(uploadID) 355 - if err != nil { 356 - http.Error(w, fmt.Sprintf("session not found: %v", err), http.StatusNotFound) 357 - return 358 - } 359 - 360 - // Verify authorization 361 - if !s.isAuthorizedWrite(did) { 362 - if did == "" { 363 - http.Error(w, "unauthorized: authentication required", http.StatusUnauthorized) 364 - } else { 365 - http.Error(w, "forbidden: write access denied", http.StatusForbidden) 366 - } 367 - return 368 - } 369 - 370 - // Verify session is in buffered mode 371 - if session.Mode != Buffered { 372 - http.Error(w, "session is not in buffered mode", http.StatusBadRequest) 373 - return 374 - } 375 - 376 - // Read part data 377 - data, err := io.ReadAll(r.Body) 378 - if err != nil { 379 - http.Error(w, fmt.Sprintf("failed to read part data: %v", err), http.StatusInternalServerError) 380 - return 381 - } 382 - 383 - // Store part and get ETag 384 - etag := session.StorePart(partNumber, data) 385 - 386 - // Return ETag in response 387 - w.Header().Set("ETag", etag) 388 - w.WriteHeader(http.StatusOK) 389 - }
-88
pkg/hold/resolve.go
··· 1 - package hold 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "sync" 7 - "time" 8 - 9 - "github.com/bluesky-social/indigo/atproto/identity" 10 - "github.com/bluesky-social/indigo/atproto/syntax" 11 - ) 12 - 13 - // handleCache provides caching for DID → handle resolution 14 - // This reduces latency for pattern matching authorization checks 15 - type handleCache struct { 16 - mu sync.RWMutex 17 - cache map[string]cacheEntry // did → handle 18 - } 19 - 20 - type cacheEntry struct { 21 - handle string 22 - expiresAt time.Time 23 - } 24 - 25 - const handleCacheTTL = 10 * time.Minute 26 - 27 - var ( 28 - // Global handle cache instance 29 - globalHandleCache = &handleCache{ 30 - cache: make(map[string]cacheEntry), 31 - } 32 - ) 33 - 34 - // get retrieves a cached handle for a DID 35 - func (c *handleCache) get(did string) (string, bool) { 36 - c.mu.RLock() 37 - defer c.mu.RUnlock() 38 - 39 - entry, ok := c.cache[did] 40 - if !ok || time.Now().After(entry.expiresAt) { 41 - return "", false 42 - } 43 - return entry.handle, true 44 - } 45 - 46 - // set stores a handle in the cache 47 - func (c *handleCache) set(did, handle string) { 48 - c.mu.Lock() 49 - defer c.mu.Unlock() 50 - 51 - c.cache[did] = cacheEntry{ 52 - handle: handle, 53 - expiresAt: time.Now().Add(handleCacheTTL), 54 - } 55 - } 56 - 57 - // resolveHandle resolves a DID to its current handle using ATProto identity resolution 58 - // Results are cached for 10 minutes to reduce latency 59 - func resolveHandle(did string) (string, error) { 60 - // Check cache first 61 - if handle, ok := globalHandleCache.get(did); ok { 62 - return handle, nil 63 - } 64 - 65 - // Cache miss - resolve from network 66 - ctx := context.Background() 67 - directory := identity.DefaultDirectory() 68 - 69 - didParsed, err := syntax.ParseDID(did) 70 - if err != nil { 71 - return "", fmt.Errorf("invalid DID: %w", err) 72 - } 73 - 74 - ident, err := directory.LookupDID(ctx, didParsed) 75 - if err != nil { 76 - return "", fmt.Errorf("failed to resolve DID: %w", err) 77 - } 78 - 79 - handle := ident.Handle.String() 80 - if handle == "" || handle == "handle.invalid" { 81 - return "", fmt.Errorf("no valid handle found for DID") 82 - } 83 - 84 - // Cache the result 85 - globalHandleCache.set(did, handle) 86 - 87 - return handle, nil 88 - }