1package registry
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "os"
8 "path/filepath"
9 "strings"
10 "time"
11
12 "github.com/rs/zerolog/log"
13)
14
15// Storage interface defines the contract for registry storage backends
16type Storage interface {
17 // Manifest operations
18 GetManifest(name, reference string) ([]byte, error)
19 PutManifest(name, reference string, data []byte) error
20 DeleteManifest(name, reference string) error
21
22 // Blob operations
23 GetBlob(digest string) (io.ReadCloser, error)
24 PutBlob(digest string, data io.Reader, size int64) error
25 DeleteBlob(digest string) error
26 BlobExists(digest string) bool
27
28 // Upload operations
29 StartBlobUpload(name string) (string, error) // returns upload UUID
30 GetBlobUpload(uuid string) (io.WriteCloser, error)
31 FinishBlobUpload(uuid, digest string) error
32 CancelBlobUpload(uuid string) error
33
34 // Tag operations
35 ListTags(name string) ([]string, error)
36
37 // Repository operations
38 ListRepositories() ([]string, error)
39}
40
41// FilesystemStorage implements Storage using the local filesystem
42type FilesystemStorage struct {
43 rootDir string
44}
45
46// NewFilesystemStorage creates a new filesystem storage instance
47func NewFilesystemStorage(rootDir string) (*FilesystemStorage, error) {
48 // Create directory structure if it doesn't exist
49 dirs := []string{
50 filepath.Join(rootDir, "repositories"),
51 filepath.Join(rootDir, "blobs"),
52 filepath.Join(rootDir, "uploads"),
53 }
54
55 for _, dir := range dirs {
56 if err := os.MkdirAll(dir, 0755); err != nil {
57 return nil, fmt.Errorf("failed to create directory %s: %w", dir, err)
58 }
59 }
60
61 log.Info().Str("root_dir", rootDir).Msg("Filesystem storage initialized")
62
63 return &FilesystemStorage{
64 rootDir: rootDir,
65 }, nil
66}
67
68// Manifest operations
69
70func (fs *FilesystemStorage) GetManifest(name, reference string) ([]byte, error) {
71 manifestPath := fs.getManifestPath(name, reference)
72
73 data, err := os.ReadFile(manifestPath)
74 if err != nil {
75 if os.IsNotExist(err) {
76 return nil, fmt.Errorf("manifest not found: %s/%s", name, reference)
77 }
78 return nil, fmt.Errorf("failed to read manifest: %w", err)
79 }
80
81 return data, nil
82}
83
84func (fs *FilesystemStorage) PutManifest(name, reference string, data []byte) error {
85 manifestPath := fs.getManifestPath(name, reference)
86
87 // Create directory if it doesn't exist
88 if err := os.MkdirAll(filepath.Dir(manifestPath), 0755); err != nil {
89 return fmt.Errorf("failed to create manifest directory: %w", err)
90 }
91
92 // Write manifest file
93 if err := os.WriteFile(manifestPath, data, 0644); err != nil {
94 return fmt.Errorf("failed to write manifest: %w", err)
95 }
96
97 // Update tags list
98 if err := fs.updateTagsList(name, reference); err != nil {
99 log.Warn().Err(err).Str("name", name).Str("reference", reference).Msg("Failed to update tags list")
100 }
101
102 log.Info().Str("name", name).Str("reference", reference).Msg("Manifest stored")
103 return nil
104}
105
106func (fs *FilesystemStorage) DeleteManifest(name, reference string) error {
107 manifestPath := fs.getManifestPath(name, reference)
108
109 if err := os.Remove(manifestPath); err != nil {
110 if os.IsNotExist(err) {
111 return fmt.Errorf("manifest not found: %s/%s", name, reference)
112 }
113 return fmt.Errorf("failed to delete manifest: %w", err)
114 }
115
116 // Remove from tags list
117 if err := fs.removeFromTagsList(name, reference); err != nil {
118 log.Warn().Err(err).Str("name", name).Str("reference", reference).Msg("Failed to update tags list")
119 }
120
121 log.Info().Str("name", name).Str("reference", reference).Msg("Manifest deleted")
122 return nil
123}
124
125// Blob operations
126
127func (fs *FilesystemStorage) GetBlob(digest string) (io.ReadCloser, error) {
128 blobPath := fs.getBlobPath(digest)
129
130 file, err := os.Open(blobPath)
131 if err != nil {
132 if os.IsNotExist(err) {
133 return nil, fmt.Errorf("blob not found: %s", digest)
134 }
135 return nil, fmt.Errorf("failed to open blob: %w", err)
136 }
137
138 return file, nil
139}
140
141func (fs *FilesystemStorage) PutBlob(digest string, data io.Reader, size int64) error {
142 blobPath := fs.getBlobPath(digest)
143
144 // Create directory if it doesn't exist
145 if err := os.MkdirAll(filepath.Dir(blobPath), 0755); err != nil {
146 return fmt.Errorf("failed to create blob directory: %w", err)
147 }
148
149 // Create temporary file first
150 tmpPath := blobPath + ".tmp"
151 file, err := os.Create(tmpPath)
152 if err != nil {
153 return fmt.Errorf("failed to create temporary blob file: %w", err)
154 }
155 defer file.Close()
156
157 // Copy data to file
158 written, err := io.Copy(file, data)
159 if err != nil {
160 os.Remove(tmpPath)
161 return fmt.Errorf("failed to write blob data: %w", err)
162 }
163
164 // Verify size if provided
165 if size > 0 && written != size {
166 os.Remove(tmpPath)
167 return fmt.Errorf("blob size mismatch: expected %d, got %d", size, written)
168 }
169
170 // Move to final location
171 if err := os.Rename(tmpPath, blobPath); err != nil {
172 os.Remove(tmpPath)
173 return fmt.Errorf("failed to move blob to final location: %w", err)
174 }
175
176 log.Info().Str("digest", digest).Int64("size", written).Msg("Blob stored")
177 return nil
178}
179
180func (fs *FilesystemStorage) DeleteBlob(digest string) error {
181 blobPath := fs.getBlobPath(digest)
182
183 if err := os.Remove(blobPath); err != nil {
184 if os.IsNotExist(err) {
185 return fmt.Errorf("blob not found: %s", digest)
186 }
187 return fmt.Errorf("failed to delete blob: %w", err)
188 }
189
190 log.Info().Str("digest", digest).Msg("Blob deleted")
191 return nil
192}
193
194func (fs *FilesystemStorage) BlobExists(digest string) bool {
195 blobPath := fs.getBlobPath(digest)
196 _, err := os.Stat(blobPath)
197 return err == nil
198}
199
200// Upload operations
201
202func (fs *FilesystemStorage) StartBlobUpload(name string) (string, error) {
203 // Generate UUID-like upload ID using timestamp
204 uuid := fmt.Sprintf("%d-%s", time.Now().UnixNano(), strings.ReplaceAll(name, "/", "_"))
205
206 uploadPath := fs.getUploadPath(uuid)
207
208 // Create directory if it doesn't exist
209 if err := os.MkdirAll(filepath.Dir(uploadPath), 0755); err != nil {
210 return "", fmt.Errorf("failed to create upload directory: %w", err)
211 }
212
213 // Create empty upload file
214 file, err := os.Create(uploadPath)
215 if err != nil {
216 return "", fmt.Errorf("failed to create upload file: %w", err)
217 }
218 file.Close()
219
220 log.Info().Str("uuid", uuid).Str("name", name).Msg("Blob upload started")
221 return uuid, nil
222}
223
224func (fs *FilesystemStorage) GetBlobUpload(uuid string) (io.WriteCloser, error) {
225 uploadPath := fs.getUploadPath(uuid)
226
227 file, err := os.OpenFile(uploadPath, os.O_WRONLY|os.O_APPEND, 0644)
228 if err != nil {
229 if os.IsNotExist(err) {
230 return nil, fmt.Errorf("upload not found: %s", uuid)
231 }
232 return nil, fmt.Errorf("failed to open upload file: %w", err)
233 }
234
235 return file, nil
236}
237
238func (fs *FilesystemStorage) FinishBlobUpload(uuid, digest string) error {
239 uploadPath := fs.getUploadPath(uuid)
240 blobPath := fs.getBlobPath(digest)
241
242 // Create blob directory if it doesn't exist
243 if err := os.MkdirAll(filepath.Dir(blobPath), 0755); err != nil {
244 return fmt.Errorf("failed to create blob directory: %w", err)
245 }
246
247 // Move upload file to blob location
248 if err := os.Rename(uploadPath, blobPath); err != nil {
249 return fmt.Errorf("failed to move upload to blob location: %w", err)
250 }
251
252 log.Info().Str("uuid", uuid).Str("digest", digest).Msg("Blob upload finished")
253 return nil
254}
255
256func (fs *FilesystemStorage) CancelBlobUpload(uuid string) error {
257 uploadPath := fs.getUploadPath(uuid)
258
259 if err := os.Remove(uploadPath); err != nil {
260 if os.IsNotExist(err) {
261 return fmt.Errorf("upload not found: %s", uuid)
262 }
263 return fmt.Errorf("failed to cancel upload: %w", err)
264 }
265
266 log.Info().Str("uuid", uuid).Msg("Blob upload cancelled")
267 return nil
268}
269
270// Tag operations
271
272func (fs *FilesystemStorage) ListTags(name string) ([]string, error) {
273 tagsPath := fs.getTagsPath(name)
274
275 data, err := os.ReadFile(tagsPath)
276 if err != nil {
277 if os.IsNotExist(err) {
278 return []string{}, nil // No tags file means no tags
279 }
280 return nil, fmt.Errorf("failed to read tags file: %w", err)
281 }
282
283 var tags []string
284 if err := json.Unmarshal(data, &tags); err != nil {
285 return nil, fmt.Errorf("failed to parse tags file: %w", err)
286 }
287
288 return tags, nil
289}
290
291// Repository operations
292
293func (fs *FilesystemStorage) ListRepositories() ([]string, error) {
294 reposDir := filepath.Join(fs.rootDir, "repositories")
295
296 var repositories []string
297 err := filepath.Walk(reposDir, func(path string, info os.FileInfo, err error) error {
298 if err != nil {
299 return err
300 }
301
302 if info.IsDir() && path != reposDir {
303 // Get relative path from repositories directory
304 relPath, err := filepath.Rel(reposDir, path)
305 if err != nil {
306 return err
307 }
308
309 // Check if this directory contains manifests
310 manifestsDir := filepath.Join(path, "manifests")
311 if _, err := os.Stat(manifestsDir); err == nil {
312 repositories = append(repositories, relPath)
313 }
314 }
315
316 return nil
317 })
318
319 if err != nil {
320 return nil, fmt.Errorf("failed to list repositories: %w", err)
321 }
322
323 return repositories, nil
324}
325
326// Helper methods for path generation
327
328func (fs *FilesystemStorage) getManifestPath(name, reference string) string {
329 return filepath.Join(fs.rootDir, "repositories", name, "manifests", reference)
330}
331
332func (fs *FilesystemStorage) getBlobPath(digest string) string {
333 // Split digest into directory structure (e.g., sha256:abc123... -> sha256/ab/abc123...)
334 parts := strings.SplitN(digest, ":", 2)
335 if len(parts) != 2 {
336 // Fallback if digest format is unexpected
337 return filepath.Join(fs.rootDir, "blobs", digest)
338 }
339
340 algorithm := parts[0]
341 hash := parts[1]
342
343 // Create two-level directory structure for better performance
344 if len(hash) >= 2 {
345 return filepath.Join(fs.rootDir, "blobs", algorithm, hash[:2], hash)
346 }
347
348 return filepath.Join(fs.rootDir, "blobs", algorithm, hash)
349}
350
351func (fs *FilesystemStorage) getUploadPath(uuid string) string {
352 return filepath.Join(fs.rootDir, "uploads", uuid)
353}
354
355func (fs *FilesystemStorage) getTagsPath(name string) string {
356 return filepath.Join(fs.rootDir, "repositories", name, "tags.json")
357}
358
359// Helper methods for tags management
360
361func (fs *FilesystemStorage) updateTagsList(name, reference string) error {
362 tags, err := fs.ListTags(name)
363 if err != nil {
364 return err
365 }
366
367 // Add tag if not already present
368 for _, tag := range tags {
369 if tag == reference {
370 return nil // Already exists
371 }
372 }
373
374 tags = append(tags, reference)
375 return fs.saveTagsList(name, tags)
376}
377
378func (fs *FilesystemStorage) removeFromTagsList(name, reference string) error {
379 tags, err := fs.ListTags(name)
380 if err != nil {
381 return err
382 }
383
384 // Remove tag if present
385 var newTags []string
386 for _, tag := range tags {
387 if tag != reference {
388 newTags = append(newTags, tag)
389 }
390 }
391
392 return fs.saveTagsList(name, newTags)
393}
394
395func (fs *FilesystemStorage) saveTagsList(name string, tags []string) error {
396 tagsPath := fs.getTagsPath(name)
397
398 // Create directory if it doesn't exist
399 if err := os.MkdirAll(filepath.Dir(tagsPath), 0755); err != nil {
400 return fmt.Errorf("failed to create tags directory: %w", err)
401 }
402
403 data, err := json.Marshal(tags)
404 if err != nil {
405 return fmt.Errorf("failed to marshal tags: %w", err)
406 }
407
408 if err := os.WriteFile(tagsPath, data, 0644); err != nil {
409 return fmt.Errorf("failed to write tags file: %w", err)
410 }
411
412 return nil
413}