forked from hailey.at/cocoon
An atproto PDS written in Go

half finished import repo (#14)

authored by hailey.at and committed by GitHub 997dc83e fe52bbfb

+44 -7
blockstore/blockstore.go
··· 16 db *gorm.DB 17 did string 18 readonly bool 19 - inserts []blocks.Block 20 } 21 22 func New(did string, db *gorm.DB) *SqliteBlockstore { ··· 24 did: did, 25 db: db, 26 readonly: false, 27 - inserts: []blocks.Block{}, 28 } 29 } 30 ··· 33 did: did, 34 db: db, 35 readonly: true, 36 - inserts: []blocks.Block{}, 37 } 38 } 39 40 func (bs *SqliteBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) { 41 var block models.Block 42 if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", bs.did, cid.Bytes()).Scan(&block).Error; err != nil { 43 return nil, err 44 } ··· 52 } 53 54 func (bs *SqliteBlockstore) Put(ctx context.Context, block blocks.Block) error { 55 - bs.inserts = append(bs.inserts, block) 56 57 if bs.readonly { 58 return nil ··· 87 panic("not implemented") 88 } 89 90 - func (bs *SqliteBlockstore) PutMany(context.Context, []blocks.Block) error { 91 - panic("not implemented") 92 } 93 94 func (bs *SqliteBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) { ··· 121 return nil 122 } 123 124 - func (bs *SqliteBlockstore) GetLog() []blocks.Block { 125 return bs.inserts 126 }
··· 16 db *gorm.DB 17 did string 18 readonly bool 19 + inserts map[cid.Cid]blocks.Block 20 } 21 22 func New(did string, db *gorm.DB) *SqliteBlockstore { ··· 24 did: did, 25 db: db, 26 readonly: false, 27 + inserts: map[cid.Cid]blocks.Block{}, 28 } 29 } 30 ··· 33 did: did, 34 db: db, 35 readonly: true, 36 + inserts: map[cid.Cid]blocks.Block{}, 37 } 38 } 39 40 func (bs *SqliteBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) { 41 var block models.Block 42 + 43 + maybeBlock, ok := bs.inserts[cid] 44 + if ok { 45 + return maybeBlock, nil 46 + } 47 + 48 if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", bs.did, cid.Bytes()).Scan(&block).Error; err != nil { 49 return nil, err 50 } ··· 58 } 59 60 func (bs *SqliteBlockstore) Put(ctx context.Context, block blocks.Block) error { 61 + bs.inserts[block.Cid()] = block 62 63 if bs.readonly { 64 return nil ··· 93 panic("not implemented") 94 } 95 96 + func (bs *SqliteBlockstore) PutMany(ctx context.Context, blocks []blocks.Block) error { 97 + tx := bs.db.Begin() 98 + 99 + for _, block := range blocks { 100 + bs.inserts[block.Cid()] = block 101 + 102 + if bs.readonly { 103 + continue 104 + } 105 + 106 + b := models.Block{ 107 + Did: bs.did, 108 + Cid: block.Cid().Bytes(), 109 + Rev: syntax.NewTIDNow(0).String(), // TODO: WARN, this is bad. don't do this 110 + Value: block.RawData(), 111 + } 112 + 113 + if err := tx.Clauses(clause.OnConflict{ 114 + Columns: []clause.Column{{Name: "did"}, {Name: "cid"}}, 115 + UpdateAll: true, 116 + }).Create(&b).Error; err != nil { 117 + tx.Rollback() 118 + return err 119 + } 120 + } 121 + 122 + if bs.readonly { 123 + return nil 124 + } 125 + 126 + tx.Commit() 127 + 128 + return nil 129 } 130 131 func (bs *SqliteBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) { ··· 158 return nil 159 } 160 161 + func (bs *SqliteBlockstore) GetLog() map[cid.Cid]blocks.Block { 162 return bs.inserts 163 }
+116
server/handle_import_repo.go
···
··· 1 + package server 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "io" 7 + "slices" 8 + "strings" 9 + 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/bluesky-social/indigo/repo" 12 + "github.com/haileyok/cocoon/blockstore" 13 + "github.com/haileyok/cocoon/internal/helpers" 14 + "github.com/haileyok/cocoon/models" 15 + blocks "github.com/ipfs/go-block-format" 16 + "github.com/ipfs/go-cid" 17 + "github.com/ipld/go-car" 18 + "github.com/labstack/echo/v4" 19 + ) 20 + 21 + func (s *Server) handleRepoImportRepo(e echo.Context) error { 22 + urepo := e.Get("repo").(*models.RepoActor) 23 + 24 + b, err := io.ReadAll(e.Request().Body) 25 + if err != nil { 26 + s.logger.Error("could not read bytes in import request", "error", err) 27 + return helpers.ServerError(e, nil) 28 + } 29 + 30 + bs := blockstore.New(urepo.Repo.Did, s.db) 31 + 32 + cs, err := car.NewCarReader(bytes.NewReader(b)) 33 + if err != nil { 34 + s.logger.Error("could not read car in import request", "error", err) 35 + return helpers.ServerError(e, nil) 36 + } 37 + 38 + orderedBlocks := []blocks.Block{} 39 + currBlock, err := cs.Next() 40 + if err != nil { 41 + s.logger.Error("could not get first block from car", "error", err) 42 + return helpers.ServerError(e, nil) 43 + } 44 + currBlockCt := 1 45 + 46 + for currBlock != nil { 47 + s.logger.Info("someone is importing their repo", "block", currBlockCt) 48 + orderedBlocks = append(orderedBlocks, currBlock) 49 + next, _ := cs.Next() 50 + currBlock = next 51 + currBlockCt++ 52 + } 53 + 54 + slices.Reverse(orderedBlocks) 55 + 56 + if err := bs.PutMany(context.TODO(), orderedBlocks); err != nil { 57 + s.logger.Error("could not insert blocks", "error", err) 58 + return helpers.ServerError(e, nil) 59 + } 60 + 61 + r, err := repo.OpenRepo(context.TODO(), bs, cs.Header.Roots[0]) 62 + if err != nil { 63 + s.logger.Error("could not open repo", "error", err) 64 + return helpers.ServerError(e, nil) 65 + } 66 + 67 + tx := s.db.Begin() 68 + 69 + clock := syntax.NewTIDClock(0) 70 + 71 + if err := r.ForEach(context.TODO(), "", func(key string, cid cid.Cid) error { 72 + pts := strings.Split(key, "/") 73 + nsid := pts[0] 74 + rkey := pts[1] 75 + cidStr := cid.String() 76 + b, err := bs.Get(context.TODO(), cid) 77 + if err != nil { 78 + s.logger.Error("record bytes don't exist in blockstore", "error", err) 79 + return helpers.ServerError(e, nil) 80 + } 81 + 82 + rec := models.Record{ 83 + Did: urepo.Repo.Did, 84 + CreatedAt: clock.Next().String(), 85 + Nsid: nsid, 86 + Rkey: rkey, 87 + Cid: cidStr, 88 + Value: b.RawData(), 89 + } 90 + 91 + if err := tx.Create(rec).Error; err != nil { 92 + return err 93 + } 94 + 95 + return nil 96 + }); err != nil { 97 + tx.Rollback() 98 + s.logger.Error("record bytes don't exist in blockstore", "error", err) 99 + return helpers.ServerError(e, nil) 100 + } 101 + 102 + tx.Commit() 103 + 104 + root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 105 + if err != nil { 106 + s.logger.Error("error committing", "error", err) 107 + return helpers.ServerError(e, nil) 108 + } 109 + 110 + if err := bs.UpdateRepo(context.TODO(), root, rev); err != nil { 111 + s.logger.Error("error updating repo after commit", "error", err) 112 + return helpers.ServerError(e, nil) 113 + } 114 + 115 + return nil 116 + }
+62 -42
server/handle_server_create_account.go
··· 10 "github.com/Azure/go-autorest/autorest/to" 11 "github.com/bluesky-social/indigo/api/atproto" 12 "github.com/bluesky-social/indigo/atproto/crypto" 13 "github.com/bluesky-social/indigo/events" 14 "github.com/bluesky-social/indigo/repo" 15 "github.com/bluesky-social/indigo/util" ··· 38 39 func (s *Server) handleCreateAccount(e echo.Context) error { 40 var request ComAtprotoServerCreateAccountRequest 41 42 if err := e.Bind(&request); err != nil { 43 s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err) ··· 109 110 // TODO: unsupported domains 111 112 - // TODO: did stuff 113 - 114 k, err := crypto.GeneratePrivateKeyK256() 115 if err != nil { 116 s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err) 117 return helpers.ServerError(e, nil) 118 } 119 120 - did, op, err := s.plcClient.CreateDID(k, "", request.Handle) 121 - if err != nil { 122 - s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err) 123 - return helpers.ServerError(e, nil) 124 - } 125 126 - if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil { 127 - s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err) 128 - return helpers.ServerError(e, nil) 129 } 130 131 hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10) ··· 135 } 136 137 urepo := models.Repo{ 138 - Did: did, 139 CreatedAt: time.Now(), 140 Email: request.Email, 141 EmailVerificationCode: to.StringPtr(fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6))), ··· 144 } 145 146 actor := models.Actor{ 147 - Did: did, 148 Handle: request.Handle, 149 } 150 ··· 153 return helpers.ServerError(e, nil) 154 } 155 156 - bs := blockstore.New(did, s.db) 157 - r := repo.NewRepo(context.TODO(), did, bs) 158 - 159 - root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 160 - if err != nil { 161 - s.logger.Error("error committing", "error", err) 162 return helpers.ServerError(e, nil) 163 } 164 165 - if err := bs.UpdateRepo(context.TODO(), root, rev); err != nil { 166 - s.logger.Error("error updating repo after commit", "error", err) 167 - return helpers.ServerError(e, nil) 168 - } 169 170 - s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 171 - RepoHandle: &atproto.SyncSubscribeRepos_Handle{ 172 - Did: urepo.Did, 173 - Handle: request.Handle, 174 - Seq: time.Now().UnixMicro(), // TODO: no 175 - Time: time.Now().Format(util.ISO8601), 176 - }, 177 - }) 178 179 - s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 180 - RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ 181 - Did: urepo.Did, 182 - Handle: to.StringPtr(request.Handle), 183 - Seq: time.Now().UnixMicro(), // TODO: no 184 - Time: time.Now().Format(util.ISO8601), 185 - }, 186 - }) 187 188 - if err := s.db.Create(&actor).Error; err != nil { 189 - s.logger.Error("error inserting new actor", "error", err) 190 - return helpers.ServerError(e, nil) 191 } 192 193 if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", request.InviteCode).Scan(&ic).Error; err != nil { ··· 214 AccessJwt: sess.AccessToken, 215 RefreshJwt: sess.RefreshToken, 216 Handle: request.Handle, 217 - Did: did, 218 }) 219 }
··· 10 "github.com/Azure/go-autorest/autorest/to" 11 "github.com/bluesky-social/indigo/api/atproto" 12 "github.com/bluesky-social/indigo/atproto/crypto" 13 + "github.com/bluesky-social/indigo/atproto/syntax" 14 "github.com/bluesky-social/indigo/events" 15 "github.com/bluesky-social/indigo/repo" 16 "github.com/bluesky-social/indigo/util" ··· 39 40 func (s *Server) handleCreateAccount(e echo.Context) error { 41 var request ComAtprotoServerCreateAccountRequest 42 + 43 + var signupDid string 44 + customDidHeader := e.Request().Header.Get("authorization") 45 + if customDidHeader != "" { 46 + pts := strings.Split(customDidHeader, " ") 47 + if len(pts) != 2 { 48 + return helpers.InputError(e, to.StringPtr("InvalidDid")) 49 + } 50 + 51 + _, err := syntax.ParseDID(pts[1]) 52 + if err != nil { 53 + return helpers.InputError(e, to.StringPtr("InvalidDid")) 54 + } 55 + 56 + signupDid = pts[1] 57 + } 58 59 if err := e.Bind(&request); err != nil { 60 s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err) ··· 126 127 // TODO: unsupported domains 128 129 k, err := crypto.GeneratePrivateKeyK256() 130 if err != nil { 131 s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err) 132 return helpers.ServerError(e, nil) 133 } 134 135 + if signupDid == "" { 136 + did, op, err := s.plcClient.CreateDID(k, "", request.Handle) 137 + if err != nil { 138 + s.logger.Error("error creating operation", "endpoint", "com.atproto.server.createAccount", "error", err) 139 + return helpers.ServerError(e, nil) 140 + } 141 142 + if err := s.plcClient.SendOperation(e.Request().Context(), did, op); err != nil { 143 + s.logger.Error("error sending plc op", "endpoint", "com.atproto.server.createAccount", "error", err) 144 + return helpers.ServerError(e, nil) 145 + } 146 + signupDid = did 147 } 148 149 hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10) ··· 153 } 154 155 urepo := models.Repo{ 156 + Did: signupDid, 157 CreatedAt: time.Now(), 158 Email: request.Email, 159 EmailVerificationCode: to.StringPtr(fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6))), ··· 162 } 163 164 actor := models.Actor{ 165 + Did: signupDid, 166 Handle: request.Handle, 167 } 168 ··· 171 return helpers.ServerError(e, nil) 172 } 173 174 + if err := s.db.Create(&actor).Error; err != nil { 175 + s.logger.Error("error inserting new actor", "error", err) 176 return helpers.ServerError(e, nil) 177 } 178 179 + if customDidHeader == "" { 180 + bs := blockstore.New(signupDid, s.db) 181 + r := repo.NewRepo(context.TODO(), signupDid, bs) 182 183 + root, rev, err := r.Commit(context.TODO(), urepo.SignFor) 184 + if err != nil { 185 + s.logger.Error("error committing", "error", err) 186 + return helpers.ServerError(e, nil) 187 + } 188 189 + if err := bs.UpdateRepo(context.TODO(), root, rev); err != nil { 190 + s.logger.Error("error updating repo after commit", "error", err) 191 + return helpers.ServerError(e, nil) 192 + } 193 194 + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 195 + RepoHandle: &atproto.SyncSubscribeRepos_Handle{ 196 + Did: urepo.Did, 197 + Handle: request.Handle, 198 + Seq: time.Now().UnixMicro(), // TODO: no 199 + Time: time.Now().Format(util.ISO8601), 200 + }, 201 + }) 202 + 203 + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ 204 + RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ 205 + Did: urepo.Did, 206 + Handle: to.StringPtr(request.Handle), 207 + Seq: time.Now().UnixMicro(), // TODO: no 208 + Time: time.Now().Format(util.ISO8601), 209 + }, 210 + }) 211 } 212 213 if err := s.db.Raw("UPDATE invite_codes SET remaining_use_count = remaining_use_count - 1 WHERE code = ?", request.InviteCode).Scan(&ic).Error; err != nil { ··· 234 AccessJwt: sess.AccessToken, 235 RefreshJwt: sess.RefreshToken, 236 Handle: request.Handle, 237 + Did: signupDid, 238 }) 239 }
+7 -2
server/server.go
··· 121 if err := next(e); err != nil { 122 e.Error(err) 123 } 124 - 125 return nil 126 } 127 } ··· 293 httpd := &http.Server{ 294 Addr: args.Addr, 295 Handler: e, 296 } 297 298 db, err := gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) ··· 417 s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleSessionMiddleware) 418 s.echo.POST("/xrpc/com.atproto.repo.applyWrites", s.handleApplyWrites, s.handleSessionMiddleware) 419 s.echo.POST("/xrpc/com.atproto.repo.uploadBlob", s.handleRepoUploadBlob, s.handleSessionMiddleware) 420 421 // stupid silly endpoints 422 s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleSessionMiddleware) ··· 425 // are there any routes that we should be allowing without auth? i dont think so but idk 426 s.echo.GET("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 427 s.echo.POST("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 428 - 429 // admin routes 430 s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware) 431 s.echo.POST("/xrpc/com.atproto.server.createInviteCodes", s.handleCreateInviteCodes, s.handleAdminMiddleware)
··· 121 if err := next(e); err != nil { 122 e.Error(err) 123 } 124 + 125 return nil 126 } 127 } ··· 293 httpd := &http.Server{ 294 Addr: args.Addr, 295 Handler: e, 296 + // shitty defaults but okay for now, needed for import repo 297 + ReadTimeout: 5 * time.Minute, 298 + WriteTimeout: 5 * time.Minute, 299 + IdleTimeout: 5 * time.Minute, 300 } 301 302 db, err := gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) ··· 421 s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleSessionMiddleware) 422 s.echo.POST("/xrpc/com.atproto.repo.applyWrites", s.handleApplyWrites, s.handleSessionMiddleware) 423 s.echo.POST("/xrpc/com.atproto.repo.uploadBlob", s.handleRepoUploadBlob, s.handleSessionMiddleware) 424 + s.echo.POST("/xrpc/com.atproto.repo.importRepo", s.handleRepoImportRepo, s.handleSessionMiddleware) 425 426 // stupid silly endpoints 427 s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleSessionMiddleware) ··· 430 // are there any routes that we should be allowing without auth? i dont think so but idk 431 s.echo.GET("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 432 s.echo.POST("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 433 + 434 // admin routes 435 s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware) 436 s.echo.POST("/xrpc/com.atproto.server.createInviteCodes", s.handleCreateInviteCodes, s.handleAdminMiddleware)