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.

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 - }