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 16 db *gorm.DB 17 17 did string 18 18 readonly bool 19 - inserts []blocks.Block 19 + inserts map[cid.Cid]blocks.Block 20 20 } 21 21 22 22 func New(did string, db *gorm.DB) *SqliteBlockstore { ··· 24 24 did: did, 25 25 db: db, 26 26 readonly: false, 27 - inserts: []blocks.Block{}, 27 + inserts: map[cid.Cid]blocks.Block{}, 28 28 } 29 29 } 30 30 ··· 33 33 did: did, 34 34 db: db, 35 35 readonly: true, 36 - inserts: []blocks.Block{}, 36 + inserts: map[cid.Cid]blocks.Block{}, 37 37 } 38 38 } 39 39 40 40 func (bs *SqliteBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) { 41 41 var block models.Block 42 + 43 + maybeBlock, ok := bs.inserts[cid] 44 + if ok { 45 + return maybeBlock, nil 46 + } 47 + 42 48 if err := bs.db.Raw("SELECT * FROM blocks WHERE did = ? AND cid = ?", bs.did, cid.Bytes()).Scan(&block).Error; err != nil { 43 49 return nil, err 44 50 } ··· 52 58 } 53 59 54 60 func (bs *SqliteBlockstore) Put(ctx context.Context, block blocks.Block) error { 55 - bs.inserts = append(bs.inserts, block) 61 + bs.inserts[block.Cid()] = block 56 62 57 63 if bs.readonly { 58 64 return nil ··· 87 93 panic("not implemented") 88 94 } 89 95 90 - func (bs *SqliteBlockstore) PutMany(context.Context, []blocks.Block) error { 91 - panic("not implemented") 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 92 129 } 93 130 94 131 func (bs *SqliteBlockstore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) { ··· 121 158 return nil 122 159 } 123 160 124 - func (bs *SqliteBlockstore) GetLog() []blocks.Block { 161 + func (bs *SqliteBlockstore) GetLog() map[cid.Cid]blocks.Block { 125 162 return bs.inserts 126 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 10 "github.com/Azure/go-autorest/autorest/to" 11 11 "github.com/bluesky-social/indigo/api/atproto" 12 12 "github.com/bluesky-social/indigo/atproto/crypto" 13 + "github.com/bluesky-social/indigo/atproto/syntax" 13 14 "github.com/bluesky-social/indigo/events" 14 15 "github.com/bluesky-social/indigo/repo" 15 16 "github.com/bluesky-social/indigo/util" ··· 38 39 39 40 func (s *Server) handleCreateAccount(e echo.Context) error { 40 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 + } 41 58 42 59 if err := e.Bind(&request); err != nil { 43 60 s.logger.Error("error receiving request", "endpoint", "com.atproto.server.createAccount", "error", err) ··· 109 126 110 127 // TODO: unsupported domains 111 128 112 - // TODO: did stuff 113 - 114 129 k, err := crypto.GeneratePrivateKeyK256() 115 130 if err != nil { 116 131 s.logger.Error("error creating signing key", "endpoint", "com.atproto.server.createAccount", "error", err) 117 132 return helpers.ServerError(e, nil) 118 133 } 119 134 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 - } 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 + } 125 141 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) 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 129 147 } 130 148 131 149 hashed, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10) ··· 135 153 } 136 154 137 155 urepo := models.Repo{ 138 - Did: did, 156 + Did: signupDid, 139 157 CreatedAt: time.Now(), 140 158 Email: request.Email, 141 159 EmailVerificationCode: to.StringPtr(fmt.Sprintf("%s-%s", helpers.RandomVarchar(6), helpers.RandomVarchar(6))), ··· 144 162 } 145 163 146 164 actor := models.Actor{ 147 - Did: did, 165 + Did: signupDid, 148 166 Handle: request.Handle, 149 167 } 150 168 ··· 153 171 return helpers.ServerError(e, nil) 154 172 } 155 173 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) 174 + if err := s.db.Create(&actor).Error; err != nil { 175 + s.logger.Error("error inserting new actor", "error", err) 162 176 return helpers.ServerError(e, nil) 163 177 } 164 178 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 - } 179 + if customDidHeader == "" { 180 + bs := blockstore.New(signupDid, s.db) 181 + r := repo.NewRepo(context.TODO(), signupDid, bs) 169 182 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 - }) 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 + } 178 188 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 - }) 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 + } 187 193 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) 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 + }) 191 211 } 192 212 193 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 { ··· 214 234 AccessJwt: sess.AccessToken, 215 235 RefreshJwt: sess.RefreshToken, 216 236 Handle: request.Handle, 217 - Did: did, 237 + Did: signupDid, 218 238 }) 219 239 }
+7 -2
server/server.go
··· 121 121 if err := next(e); err != nil { 122 122 e.Error(err) 123 123 } 124 - 124 + 125 125 return nil 126 126 } 127 127 } ··· 293 293 httpd := &http.Server{ 294 294 Addr: args.Addr, 295 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, 296 300 } 297 301 298 302 db, err := gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) ··· 417 421 s.echo.POST("/xrpc/com.atproto.repo.deleteRecord", s.handleDeleteRecord, s.handleSessionMiddleware) 418 422 s.echo.POST("/xrpc/com.atproto.repo.applyWrites", s.handleApplyWrites, s.handleSessionMiddleware) 419 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) 420 425 421 426 // stupid silly endpoints 422 427 s.echo.GET("/xrpc/app.bsky.actor.getPreferences", s.handleActorGetPreferences, s.handleSessionMiddleware) ··· 425 430 // are there any routes that we should be allowing without auth? i dont think so but idk 426 431 s.echo.GET("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 427 432 s.echo.POST("/xrpc/*", s.handleProxy, s.handleSessionMiddleware) 428 - 433 + 429 434 // admin routes 430 435 s.echo.POST("/xrpc/com.atproto.server.createInviteCode", s.handleCreateInviteCode, s.handleAdminMiddleware) 431 436 s.echo.POST("/xrpc/com.atproto.server.createInviteCodes", s.handleCreateInviteCodes, s.handleAdminMiddleware)