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