···11+when:
22+ - event: ["push"]
33+ branch: ["main"]
44+ - event: ["pull_request"]
55+ branch: ["main"]
66+77+engine: "nixery"
88+99+dependencies:
1010+ nixpkgs:
1111+ - git
1212+ - go
1313+1414+steps:
1515+ - name: Run Tests
1616+ command: |
1717+ go test -cover ./...
-6
pkg/hold/handlers.go
···11-package hold
22-33-// This file previously contained legacy HTTP handlers that have been replaced by XRPC endpoints.
44-// The handlers (HandleProxyGet, HandleProxyPut, HandleMultipartPartUpload) are no longer needed
55-// as all blob operations now go through the XRPC com.atproto.repo.uploadBlob and
66-// com.atproto.sync.getBlob endpoints.
+9-62
pkg/hold/multipart.go
···55 "crypto/sha256"
66 "encoding/hex"
77 "fmt"
88- "io"
98 "log"
1010- "net/http"
119 "sync"
1210 "time"
1311···262260 return session.UploadID, Buffered, nil
263261}
264262265265-// GetPartUploadURL generates a URL for uploading a part
266266-// For S3Native: returns presigned URL
267267-// For Buffered: returns proxy endpoint
263263+// GetPartUploadURL generates a presigned URL for uploading a part
264264+// Only used for S3Native mode - Buffered mode is handled by blobstore adapter
268265func (s *HoldService) GetPartUploadURL(ctx context.Context, session *MultipartSession, partNumber int, did string) (string, error) {
269269- if session.Mode == S3Native {
270270- // Generate S3 presigned URL for this part
271271- url, err := s.getPartPresignedURL(ctx, session.Digest, session.S3UploadID, partNumber)
272272- if err != nil {
273273- return "", fmt.Errorf("failed to generate S3 part URL: %w", err)
274274- }
275275- return url, nil
266266+ if session.Mode != S3Native {
267267+ return "", fmt.Errorf("GetPartUploadURL only supports S3Native mode")
276268 }
277269278278- // Buffered mode: return proxy endpoint
279279- // url := fmt.Sprintf("%s/xrpc/com.atproto.repo.uploadBlob", s.config.Server.PublicURL)
280280-281281- url := fmt.Sprintf("%s/multipart-parts/%s/%d?did=%s",
282282- s.config.Server.PublicURL, session.UploadID, partNumber, did)
270270+ // Generate S3 presigned URL for this part
271271+ url, err := s.getPartPresignedURL(ctx, session.Digest, session.S3UploadID, partNumber)
272272+ if err != nil {
273273+ return "", fmt.Errorf("failed to generate S3 part URL: %w", err)
274274+ }
283275 return url, nil
284276}
285277···342334 return nil
343335}
344336345345-// HandleMultipartPartUpload handles uploading a part in buffered mode
346346-// This is a new endpoint: PUT /multipart-parts/{uploadID}/{partNumber}
347347-func (s *HoldService) HandleMultipartPartUpload(w http.ResponseWriter, r *http.Request, uploadID string, partNumber int, did string, manager *MultipartManager) {
348348- if r.Method != http.MethodPut {
349349- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
350350- return
351351- }
352352-353353- // Get session
354354- session, err := manager.GetSession(uploadID)
355355- if err != nil {
356356- http.Error(w, fmt.Sprintf("session not found: %v", err), http.StatusNotFound)
357357- return
358358- }
359359-360360- // Verify authorization
361361- if !s.isAuthorizedWrite(did) {
362362- if did == "" {
363363- http.Error(w, "unauthorized: authentication required", http.StatusUnauthorized)
364364- } else {
365365- http.Error(w, "forbidden: write access denied", http.StatusForbidden)
366366- }
367367- return
368368- }
369369-370370- // Verify session is in buffered mode
371371- if session.Mode != Buffered {
372372- http.Error(w, "session is not in buffered mode", http.StatusBadRequest)
373373- return
374374- }
375375-376376- // Read part data
377377- data, err := io.ReadAll(r.Body)
378378- if err != nil {
379379- http.Error(w, fmt.Sprintf("failed to read part data: %v", err), http.StatusInternalServerError)
380380- return
381381- }
382382-383383- // Store part and get ETag
384384- etag := session.StorePart(partNumber, data)
385385-386386- // Return ETag in response
387387- w.Header().Set("ETag", etag)
388388- w.WriteHeader(http.StatusOK)
389389-}
-88
pkg/hold/resolve.go
···11-package hold
22-33-import (
44- "context"
55- "fmt"
66- "sync"
77- "time"
88-99- "github.com/bluesky-social/indigo/atproto/identity"
1010- "github.com/bluesky-social/indigo/atproto/syntax"
1111-)
1212-1313-// handleCache provides caching for DID → handle resolution
1414-// This reduces latency for pattern matching authorization checks
1515-type handleCache struct {
1616- mu sync.RWMutex
1717- cache map[string]cacheEntry // did → handle
1818-}
1919-2020-type cacheEntry struct {
2121- handle string
2222- expiresAt time.Time
2323-}
2424-2525-const handleCacheTTL = 10 * time.Minute
2626-2727-var (
2828- // Global handle cache instance
2929- globalHandleCache = &handleCache{
3030- cache: make(map[string]cacheEntry),
3131- }
3232-)
3333-3434-// get retrieves a cached handle for a DID
3535-func (c *handleCache) get(did string) (string, bool) {
3636- c.mu.RLock()
3737- defer c.mu.RUnlock()
3838-3939- entry, ok := c.cache[did]
4040- if !ok || time.Now().After(entry.expiresAt) {
4141- return "", false
4242- }
4343- return entry.handle, true
4444-}
4545-4646-// set stores a handle in the cache
4747-func (c *handleCache) set(did, handle string) {
4848- c.mu.Lock()
4949- defer c.mu.Unlock()
5050-5151- c.cache[did] = cacheEntry{
5252- handle: handle,
5353- expiresAt: time.Now().Add(handleCacheTTL),
5454- }
5555-}
5656-5757-// resolveHandle resolves a DID to its current handle using ATProto identity resolution
5858-// Results are cached for 10 minutes to reduce latency
5959-func resolveHandle(did string) (string, error) {
6060- // Check cache first
6161- if handle, ok := globalHandleCache.get(did); ok {
6262- return handle, nil
6363- }
6464-6565- // Cache miss - resolve from network
6666- ctx := context.Background()
6767- directory := identity.DefaultDirectory()
6868-6969- didParsed, err := syntax.ParseDID(did)
7070- if err != nil {
7171- return "", fmt.Errorf("invalid DID: %w", err)
7272- }
7373-7474- ident, err := directory.LookupDID(ctx, didParsed)
7575- if err != nil {
7676- return "", fmt.Errorf("failed to resolve DID: %w", err)
7777- }
7878-7979- handle := ident.Handle.String()
8080- if handle == "" || handle == "handle.invalid" {
8181- return "", fmt.Errorf("no valid handle found for DID")
8282- }
8383-8484- // Cache the result
8585- globalHandleCache.set(did, handle)
8686-8787- return handle, nil
8888-}