diff --git a/go.mod b/go.mod index eae1700..36a2252 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 github.com/ipfs/go-cid v0.4.1 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 github.com/klauspost/compress v1.18.0 github.com/mattn/go-sqlite3 v1.14.32 github.com/opencontainers/go-digest v1.0.0 @@ -68,7 +69,6 @@ require ( github.com/ipfs/go-merkledag v0.11.0 // indirect github.com/ipfs/go-metrics-interface v0.0.1 // indirect github.com/ipfs/go-verifcid v0.0.3 // indirect - github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 // indirect github.com/ipld/go-codec-dagpb v1.6.0 // indirect github.com/ipld/go-ipld-prime v0.21.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/pkg/hold/pds/xrpc.go b/pkg/hold/pds/xrpc.go index 6540ae0..d441720 100644 --- a/pkg/hold/pds/xrpc.go +++ b/pkg/hold/pds/xrpc.go @@ -1,10 +1,15 @@ package pds import ( + "bytes" "encoding/json" "fmt" "net/http" "strings" + + "github.com/ipfs/go-cid" + "github.com/ipld/go-car" + carutil "github.com/ipld/go-car/util" ) // XRPC handler for ATProto endpoints @@ -63,6 +68,7 @@ func (h *XRPCHandler) RegisterHandlers(mux *http.ServeMux) { // Sync endpoints mux.HandleFunc("/xrpc/com.atproto.sync.listRepos", corsMiddleware(h.HandleListRepos)) + mux.HandleFunc("/xrpc/com.atproto.sync.getRecord", corsMiddleware(h.HandleSyncGetRecord)) // Blob endpoints (wrap existing presigned URL logic) mux.HandleFunc("/xrpc/com.atproto.repo.uploadBlob", corsMiddleware(h.HandleUploadBlob)) @@ -240,6 +246,75 @@ func (h *XRPCHandler) HandleListRecords(w http.ResponseWriter, r *http.Request) json.NewEncoder(w).Encode(response) } +// HandleSyncGetRecord returns a single record as a CAR file for sync +func (h *XRPCHandler) HandleSyncGetRecord(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + did := r.URL.Query().Get("did") + collection := r.URL.Query().Get("collection") + rkey := r.URL.Query().Get("rkey") + + if did == "" || collection == "" || rkey == "" { + http.Error(w, "missing required parameters", http.StatusBadRequest) + return + } + + if did != h.pds.DID() { + http.Error(w, "invalid did", http.StatusBadRequest) + return + } + + // Only support crew collection for now + if collection != CrewCollection { + http.Error(w, "collection not found", http.StatusNotFound) + return + } + + // Get the record CID and raw bytes + recordCID, _, err := h.pds.GetCrewMember(r.Context(), rkey) + if err != nil { + http.Error(w, fmt.Sprintf("failed to get record: %v", err), http.StatusNotFound) + return + } + + // Get the raw block from the blockstore + blk, err := h.pds.repo.Blockstore().Get(r.Context(), recordCID) + if err != nil { + http.Error(w, fmt.Sprintf("failed to get record block: %v", err), http.StatusInternalServerError) + return + } + + // Write CAR file with the single record + w.Header().Set("Content-Type", "application/vnd.ipld.car") + + // Create a buffer to write the CAR data + var buf bytes.Buffer + + // Create CAR header with the record CID as root + header := &car.CarHeader{ + Roots: []cid.Cid{recordCID}, + Version: 1, + } + + // Write the CAR header + if err := car.WriteHeader(header, &buf); err != nil { + http.Error(w, fmt.Sprintf("failed to write CAR header: %v", err), http.StatusInternalServerError) + return + } + + // Write the block using LdWrite + if err := carutil.LdWrite(&buf, recordCID.Bytes(), blk.RawData()); err != nil { + http.Error(w, fmt.Sprintf("failed to write block to CAR: %v", err), http.StatusInternalServerError) + return + } + + // Write the CAR data to the response + w.Write(buf.Bytes()) +} + // HandleUploadBlob wraps existing presigned upload URL logic func (h *XRPCHandler) HandleUploadBlob(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost {