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