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}