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}