A community based topic aggregation platform built on atproto
1package repository 2 3import ( 4 "bytes" 5 "context" 6 "fmt" 7 "strings" 8 "time" 9 10 "Coves/internal/atproto/carstore" 11 "github.com/ipfs/go-cid" 12) 13 14// Service implements the RepositoryService interface using Indigo's carstore 15type Service struct { 16 repo RepositoryRepository 17 repoStore *carstore.RepoStore 18 signingKeys map[string]interface{} // DID -> signing key 19} 20 21// NewService creates a new repository service using carstore 22func NewService(repo RepositoryRepository, repoStore *carstore.RepoStore) *Service { 23 return &Service{ 24 repo: repo, 25 repoStore: repoStore, 26 signingKeys: make(map[string]interface{}), 27 } 28} 29 30// SetSigningKey sets the signing key for a DID 31func (s *Service) SetSigningKey(did string, signingKey interface{}) { 32 s.signingKeys[did] = signingKey 33} 34 35// CreateRepository creates a new repository 36func (s *Service) CreateRepository(did string) (*Repository, error) { 37 // Check if repository already exists 38 existing, err := s.repo.GetByDID(did) 39 if err != nil { 40 return nil, fmt.Errorf("checking existing repository: %w", err) 41 } 42 if existing != nil { 43 return nil, fmt.Errorf("repository already exists for DID: %s", did) 44 } 45 46 // For now, just create the user mapping without importing CAR data 47 // The actual repository data will be created when records are added 48 ctx := context.Background() 49 50 // Ensure user mapping exists 51 _, err = s.repoStore.GetOrCreateUID(ctx, did) 52 if err != nil { 53 return nil, fmt.Errorf("creating user mapping: %w", err) 54 } 55 56 // Use placeholder CID for the empty repository 57 58 // Create repository record 59 repository := &Repository{ 60 DID: did, 61 HeadCID: PlaceholderCID, 62 Revision: "rev-0", 63 RecordCount: 0, 64 StorageSize: 0, 65 CreatedAt: time.Now(), 66 UpdatedAt: time.Now(), 67 } 68 69 // Save to database 70 if err := s.repo.Create(repository); err != nil { 71 return nil, fmt.Errorf("saving repository: %w", err) 72 } 73 74 return repository, nil 75} 76 77// GetRepository retrieves a repository by DID 78func (s *Service) GetRepository(did string) (*Repository, error) { 79 repo, err := s.repo.GetByDID(did) 80 if err != nil { 81 return nil, fmt.Errorf("getting repository: %w", err) 82 } 83 if repo == nil { 84 return nil, fmt.Errorf("repository not found for DID: %s", did) 85 } 86 87 // Update head CID from carstore 88 headCID, err := s.repoStore.GetRepoHead(context.Background(), did) 89 if err == nil && headCID.Defined() { 90 repo.HeadCID = headCID 91 } 92 93 return repo, nil 94} 95 96// DeleteRepository deletes a repository 97func (s *Service) DeleteRepository(did string) error { 98 // Delete from carstore 99 if err := s.repoStore.DeleteRepo(context.Background(), did); err != nil { 100 return fmt.Errorf("deleting repo from carstore: %w", err) 101 } 102 103 // Delete from database 104 if err := s.repo.Delete(did); err != nil { 105 return fmt.Errorf("deleting repository from database: %w", err) 106 } 107 108 return nil 109} 110 111// ExportRepository exports a repository as a CAR file 112func (s *Service) ExportRepository(did string) ([]byte, error) { 113 // First check if repository exists in our database 114 repo, err := s.repo.GetByDID(did) 115 if err != nil { 116 return nil, fmt.Errorf("getting repository: %w", err) 117 } 118 if repo == nil { 119 return nil, fmt.Errorf("repository not found for DID: %s", did) 120 } 121 122 // Try to read from carstore 123 carData, err := s.repoStore.ReadRepo(context.Background(), did, "") 124 if err != nil { 125 // If no data in carstore yet, return empty CAR 126 // This happens when a repo is created but no records added yet 127 // Check for the specific error pattern from Indigo's carstore 128 errMsg := err.Error() 129 if strings.Contains(errMsg, "no data found for user") || 130 strings.Contains(errMsg, "user not found") { 131 return []byte{}, nil 132 } 133 return nil, fmt.Errorf("exporting repository: %w", err) 134 } 135 136 return carData, nil 137} 138 139// ImportRepository imports a repository from a CAR file 140func (s *Service) ImportRepository(did string, carData []byte) error { 141 ctx := context.Background() 142 143 // If empty CAR data, just create user mapping 144 if len(carData) == 0 { 145 _, err := s.repoStore.GetOrCreateUID(ctx, did) 146 if err != nil { 147 return fmt.Errorf("creating user mapping: %w", err) 148 } 149 150 // Use placeholder CID for empty repository 151 headCID := PlaceholderCID 152 153 // Create repository record 154 repo := &Repository{ 155 DID: did, 156 HeadCID: headCID, 157 Revision: "imported-empty", 158 RecordCount: 0, 159 StorageSize: 0, 160 CreatedAt: time.Now(), 161 UpdatedAt: time.Now(), 162 } 163 if err := s.repo.Create(repo); err != nil { 164 return fmt.Errorf("creating repository: %w", err) 165 } 166 return nil 167 } 168 169 // Import non-empty CAR into carstore 170 headCID, err := s.repoStore.ImportRepo(ctx, did, bytes.NewReader(carData)) 171 if err != nil { 172 return fmt.Errorf("importing repository: %w", err) 173 } 174 175 // Create or update repository record 176 repo, err := s.repo.GetByDID(did) 177 if err != nil { 178 return fmt.Errorf("getting repository: %w", err) 179 } 180 181 if repo == nil { 182 // Create new repository 183 repo = &Repository{ 184 DID: did, 185 HeadCID: headCID, 186 Revision: "imported", 187 RecordCount: 0, // TODO: Count records in CAR 188 StorageSize: int64(len(carData)), 189 CreatedAt: time.Now(), 190 UpdatedAt: time.Now(), 191 } 192 if err := s.repo.Create(repo); err != nil { 193 return fmt.Errorf("creating repository: %w", err) 194 } 195 } else { 196 // Update existing repository 197 repo.HeadCID = headCID 198 repo.UpdatedAt = time.Now() 199 if err := s.repo.Update(repo); err != nil { 200 return fmt.Errorf("updating repository: %w", err) 201 } 202 } 203 204 return nil 205} 206 207// CompactRepository runs garbage collection on a repository 208func (s *Service) CompactRepository(did string) error { 209 return s.repoStore.CompactRepo(context.Background(), did) 210} 211 212// Note: Record-level operations would require more complex implementation 213// to work with the carstore. For now, these are placeholder implementations 214// that would need to be expanded to properly handle record CRUD operations 215// by reading the CAR, modifying the repo structure, and writing back. 216 217func (s *Service) CreateRecord(input CreateRecordInput) (*Record, error) { 218 return nil, fmt.Errorf("record operations not yet implemented for carstore") 219} 220 221func (s *Service) GetRecord(input GetRecordInput) (*Record, error) { 222 return nil, fmt.Errorf("record operations not yet implemented for carstore") 223} 224 225func (s *Service) UpdateRecord(input UpdateRecordInput) (*Record, error) { 226 return nil, fmt.Errorf("record operations not yet implemented for carstore") 227} 228 229func (s *Service) DeleteRecord(input DeleteRecordInput) error { 230 return fmt.Errorf("record operations not yet implemented for carstore") 231} 232 233func (s *Service) ListRecords(did string, collection string, limit int, cursor string) ([]*Record, string, error) { 234 return nil, "", fmt.Errorf("record operations not yet implemented for carstore") 235} 236 237func (s *Service) GetCommit(did string, commitCID cid.Cid) (*Commit, error) { 238 return nil, fmt.Errorf("commit operations not yet implemented for carstore") 239} 240 241func (s *Service) ListCommits(did string, limit int, cursor string) ([]*Commit, string, error) { 242 return nil, "", fmt.Errorf("commit operations not yet implemented for carstore") 243}