···33import (
44 "bytes"
55 "context"
66+ "encoding/base64"
67 "encoding/json"
78 "fmt"
89 "io"
···343344 return nil, fmt.Errorf("failed to read blob data: %w", err)
344345 }
345346347347+ // Check if PDS returned JSON-wrapped blob (Bluesky implementation)
348348+ // PDS may wrap blobs as JSON-encoded base64 strings
349349+ // Detection: Check if content starts with a quote (indicating JSON string)
350350+ if len(data) > 0 && data[0] == '"' {
351351+ // Blob is JSON-encoded - decode it
352352+ var base64Str string
353353+ if err := json.Unmarshal(data, &base64Str); err != nil {
354354+ return nil, fmt.Errorf("failed to unmarshal JSON-wrapped blob: %w", err)
355355+ }
356356+357357+ // Base64-decode the blob content
358358+ decoded, err := base64.StdEncoding.DecodeString(base64Str)
359359+ if err != nil {
360360+ return nil, fmt.Errorf("failed to base64-decode blob: %w", err)
361361+ }
362362+363363+ return decoded, nil
364364+ }
365365+366366+ // Raw blob response (expected ATProto behavior)
346367 return data, nil
347368}
348369
+47-49
pkg/hold/handlers.go
···1212 "atcr.io/pkg/atproto"
1313)
14141515-// HandlePresignedURL returns an HTTP handler for presigned URL requests (GET, HEAD, or PUT)
1616-// This consolidates the three separate handlers into a single parameterized implementation
1717-func (s *HoldService) HandlePresignedURL(operation PresignedURLOperation) http.HandlerFunc {
1818- return func(w http.ResponseWriter, r *http.Request) {
1919- if r.Method != http.MethodPost {
2020- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
2121- return
2222- }
1515+// HandlePresignedURL handles presigned URL requests (GET, HEAD, or PUT)
1616+// Operation type is specified in the request body
1717+func (s *HoldService) HandlePresignedURL(w http.ResponseWriter, r *http.Request) {
1818+ if r.Method != http.MethodPost {
1919+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
2020+ return
2121+ }
23222424- var req PresignedURLRequest
2525- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
2626- http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
2727- return
2828- }
2323+ var req PresignedURLRequest
2424+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
2525+ http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
2626+ return
2727+ }
29283030- // Validate DID authorization based on operation type
3131- var authorized bool
3232- switch operation {
3333- case OperationGet, OperationHead:
3434- authorized = s.isAuthorizedRead(req.DID)
3535- case OperationPut:
3636- authorized = s.isAuthorizedWrite(req.DID)
3737- default:
3838- http.Error(w, "unsupported operation", http.StatusBadRequest)
3939- return
4040- }
2929+ // Validate DID authorization based on operation type
3030+ var authorized bool
3131+ switch req.Operation {
3232+ case OperationGet, OperationHead:
3333+ authorized = s.isAuthorizedRead(req.DID)
3434+ case OperationPut:
3535+ authorized = s.isAuthorizedWrite(req.DID)
3636+ default:
3737+ http.Error(w, "unsupported operation", http.StatusBadRequest)
3838+ return
3939+ }
41404242- if !authorized {
4343- log.Printf("[HandlePresignedURL:%s] Authorization FAILED", operation)
4444- if req.DID == "" {
4545- http.Error(w, "unauthorized: authentication required", http.StatusUnauthorized)
4646- } else {
4747- http.Error(w, "forbidden: access denied", http.StatusForbidden)
4848- }
4949- return
4141+ if !authorized {
4242+ log.Printf("[HandlePresignedURL:%s] Authorization FAILED", req.Operation)
4343+ if req.DID == "" {
4444+ http.Error(w, "unauthorized: authentication required", http.StatusUnauthorized)
4545+ } else {
4646+ http.Error(w, "forbidden: access denied", http.StatusForbidden)
5047 }
5151-5252- // Generate presigned URL (15 minute expiry)
5353- ctx := context.Background()
5454- expiry := time.Now().Add(15 * time.Minute)
4848+ return
4949+ }
55505656- url, err := s.getPresignedURL(ctx, operation, req.Digest, req.DID)
5757- if err != nil {
5858- log.Printf("[HandlePresignedURL:%s] getPresignedURL failed: %v", operation, err)
5959- http.Error(w, fmt.Sprintf("failed to generate URL: %v", err), http.StatusInternalServerError)
6060- return
6161- }
5151+ // Generate presigned URL (15 minute expiry)
5252+ ctx := context.Background()
5353+ expiry := time.Now().Add(15 * time.Minute)
62546363- log.Printf("[HandlePresignedURL:%s] Returning URL to client", operation)
5555+ url, err := s.getPresignedURL(ctx, req.Operation, req.Digest, req.DID)
5656+ if err != nil {
5757+ log.Printf("[HandlePresignedURL:%s] getPresignedURL failed: %v", req.Operation, err)
5858+ http.Error(w, fmt.Sprintf("failed to generate URL: %v", err), http.StatusInternalServerError)
5959+ return
6060+ }
64616565- resp := PresignedURLResponse{
6666- URL: url,
6767- ExpiresAt: expiry,
6868- }
6262+ log.Printf("[HandlePresignedURL:%s] Returning URL to client", req.Operation)
69637070- w.Header().Set("Content-Type", "application/json")
7171- json.NewEncoder(w).Encode(resp)
6464+ resp := PresignedURLResponse{
6565+ URL: url,
6666+ ExpiresAt: expiry,
7267 }
6868+6969+ w.Header().Set("Content-Type", "application/json")
7070+ json.NewEncoder(w).Encode(resp)
7371}
74727573// HandleProxyGet proxies a blob download through the service
+4-3
pkg/hold/types.go
···15151616// PresignedURLRequest represents a request for a presigned URL (GET, HEAD, or PUT)
1717type PresignedURLRequest struct {
1818- DID string `json:"did"`
1919- Digest string `json:"digest"`
2020- Size int64 `json:"size,omitempty"` // Only required for PUT operations
1818+ Operation PresignedURLOperation `json:"operation"`
1919+ DID string `json:"did"`
2020+ Digest string `json:"digest"`
2121+ Size int64 `json:"size,omitempty"` // Only required for PUT operations
2122}
22232324// PresignedURLResponse contains the presigned URL
+22-79
pkg/storage/proxy_blob_store.go
···270270 return writer, nil
271271}
272272273273-// getDownloadURL requests a presigned download URL from the storage service
274274-func (p *ProxyBlobStore) getDownloadURL(ctx context.Context, dgst digest.Digest) (string, error) {
273273+// getPresignedURL requests a presigned URL from the storage service for any operation
274274+func (p *ProxyBlobStore) getPresignedURL(ctx context.Context, operation, dgst string, size int64) (string, error) {
275275 reqBody := map[string]any{
276276- "did": p.did,
277277- "digest": dgst.String(),
276276+ "operation": operation,
277277+ "did": p.did,
278278+ "digest": dgst,
279279+ }
280280+281281+ // Only include size for PUT operations
282282+ if size > 0 {
283283+ reqBody["size"] = size
278284 }
279285280286 body, err := json.Marshal(reqBody)
···282288 return "", err
283289 }
284290285285- url := fmt.Sprintf("%s/get-presigned-url", p.storageEndpoint)
291291+ url := fmt.Sprintf("%s/presigned-url", p.storageEndpoint)
286292 req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
287293 if err != nil {
288294 return "", err
···296302 defer resp.Body.Close()
297303298304 if resp.StatusCode != http.StatusOK {
299299- return "", fmt.Errorf("failed to get download URL: status %d", resp.StatusCode)
305305+ return "", fmt.Errorf("failed to get presigned URL: status %d", resp.StatusCode)
300306 }
301307302308 var result struct {
···309315 return result.URL, nil
310316}
311317318318+// getDownloadURL requests a presigned download URL from the storage service
319319+func (p *ProxyBlobStore) getDownloadURL(ctx context.Context, dgst digest.Digest) (string, error) {
320320+ return p.getPresignedURL(ctx, "GET", dgst.String(), 0)
321321+}
322322+312323// getHeadURL requests a presigned HEAD URL from the storage service
313324func (p *ProxyBlobStore) getHeadURL(ctx context.Context, dgst digest.Digest) (string, error) {
314314- reqBody := map[string]any{
315315- "did": p.did,
316316- "digest": dgst.String(),
317317- }
318318-319319- body, err := json.Marshal(reqBody)
320320- if err != nil {
321321- return "", err
322322- }
323323-324324- url := fmt.Sprintf("%s/head-presigned-url", p.storageEndpoint)
325325- req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
326326- if err != nil {
327327- return "", err
328328- }
329329- req.Header.Set("Content-Type", "application/json")
330330-331331- resp, err := p.httpClient.Do(req)
332332- if err != nil {
333333- return "", err
334334- }
335335- defer resp.Body.Close()
336336-337337- if resp.StatusCode != http.StatusOK {
338338- return "", fmt.Errorf("failed to get HEAD URL: status %d", resp.StatusCode)
339339- }
340340-341341- var result struct {
342342- URL string `json:"url"`
343343- }
344344- if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
345345- return "", err
346346- }
347347-348348- return result.URL, nil
325325+ return p.getPresignedURL(ctx, "HEAD", dgst.String(), 0)
349326}
350327351328// getUploadURL requests a presigned upload URL from the storage service
352329func (p *ProxyBlobStore) getUploadURL(ctx context.Context, dgst digest.Digest, size int64) (string, error) {
353330 fmt.Printf("DEBUG [proxy_blob_store/getUploadURL]: storageEndpoint=%s, digest=%s\n", p.storageEndpoint, dgst)
354354-355355- reqBody := map[string]any{
356356- "did": p.did,
357357- "digest": dgst.String(),
358358- "size": size,
359359- }
360360-361361- body, err := json.Marshal(reqBody)
362362- if err != nil {
363363- return "", err
364364- }
365365-366366- url := fmt.Sprintf("%s/put-presigned-url", p.storageEndpoint)
367367- fmt.Printf("DEBUG [proxy_blob_store/getUploadURL]: Calling %s\n", url)
368368- req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
369369- if err != nil {
370370- return "", err
331331+ url, err := p.getPresignedURL(ctx, "PUT", dgst.String(), size)
332332+ if err == nil {
333333+ fmt.Printf("DEBUG [proxy_blob_store/getUploadURL]: Got presigned URL=%s\n", url)
371334 }
372372- req.Header.Set("Content-Type", "application/json")
373373-374374- resp, err := p.httpClient.Do(req)
375375- if err != nil {
376376- return "", err
377377- }
378378- defer resp.Body.Close()
379379-380380- if resp.StatusCode != http.StatusOK {
381381- return "", fmt.Errorf("failed to get upload URL: status %d", resp.StatusCode)
382382- }
383383-384384- var result struct {
385385- URL string `json:"url"`
386386- }
387387- if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
388388- return "", err
389389- }
390390-391391- fmt.Printf("DEBUG [proxy_blob_store/getUploadURL]: Got presigned URL=%s\n", result.URL)
392392- return result.URL, nil
335335+ return url, err
393336}
394337395338// startMultipartUpload initiates a multipart upload via hold service