1package filesystem
2
3import (
4 "crypto/sha256"
5 "encoding/json"
6 "fmt"
7 "os"
8 "path/filepath"
9 "time"
10
11 "github.com/bnema/zerowrap"
12
13 "github.com/bnema/gordon/pkg/validation"
14)
15
16// ManifestStorage implements the ManifestStorage interface using the local filesystem.
17type ManifestStorage struct {
18 rootDir string
19 log zerowrap.Logger
20}
21
22// NewManifestStorage creates a new filesystem manifest storage instance.
23func NewManifestStorage(rootDir string, log zerowrap.Logger) (*ManifestStorage, error) {
24 // Create directory structure if it doesn't exist
25 reposDir := filepath.Join(rootDir, "repositories")
26 if err := os.MkdirAll(reposDir, 0750); err != nil {
27 return nil, fmt.Errorf("failed to create repositories directory: %w", err)
28 }
29
30 log.Info().
31 Str(zerowrap.FieldLayer, "adapter").
32 Str(zerowrap.FieldAdapter, "filesystem").
33 Str("root_dir", rootDir).
34 Msg("manifest storage initialized")
35
36 return &ManifestStorage{
37 rootDir: rootDir,
38 log: log,
39 }, nil
40}
41
42// GetManifest retrieves a manifest by name and reference.
43// Returns the manifest data and content type.
44func (s *ManifestStorage) GetManifest(name, reference string) ([]byte, string, error) {
45 manifestPath, err := s.getManifestPath(name, reference)
46 if err != nil {
47 return nil, "", fmt.Errorf("invalid path: %w", err)
48 }
49
50 data, err := os.ReadFile(manifestPath)
51 if err != nil {
52 if os.IsNotExist(err) {
53 return nil, "", fmt.Errorf("manifest not found: %s/%s", name, reference)
54 }
55 return nil, "", fmt.Errorf("failed to read manifest: %w", err)
56 }
57
58 contentType, err := s.getManifestContentType(name, reference)
59 if err != nil {
60 // Fallback for manifests stored before content type tracking
61 s.log.Warn().
62 Str(zerowrap.FieldLayer, "adapter").
63 Str(zerowrap.FieldAdapter, "filesystem").
64 Err(err).
65 Str("name", name).
66 Str("reference", reference).
67 Msg("could not get manifest content type, falling back to default")
68 contentType = "application/vnd.docker.distribution.manifest.v2+json"
69 }
70
71 return data, contentType, nil
72}
73
74// PutManifest stores a manifest.
75func (s *ManifestStorage) PutManifest(name, reference, contentType string, data []byte) error {
76 manifestPath, err := s.getManifestPath(name, reference)
77 if err != nil {
78 return fmt.Errorf("invalid path: %w", err)
79 }
80
81 // Create directory if it doesn't exist
82 if err := os.MkdirAll(filepath.Dir(manifestPath), 0750); err != nil {
83 return fmt.Errorf("failed to create manifest directory: %w", err)
84 }
85
86 // Write manifest file
87 if err := os.WriteFile(manifestPath, data, 0600); err != nil {
88 return fmt.Errorf("failed to write manifest: %w", err)
89 }
90
91 // Store content type
92 if err := s.putManifestContentType(name, reference, contentType); err != nil {
93 // Don't fail the whole operation, but log a warning
94 s.log.Warn().
95 Str(zerowrap.FieldLayer, "adapter").
96 Str(zerowrap.FieldAdapter, "filesystem").
97 Err(err).
98 Str("name", name).
99 Str("reference", reference).
100 Msg("failed to store manifest content type")
101 }
102
103 // Update tags list
104 if err := s.updateTagsList(name, reference); err != nil {
105 s.log.Warn().
106 Str(zerowrap.FieldLayer, "adapter").
107 Str(zerowrap.FieldAdapter, "filesystem").
108 Err(err).
109 Str("name", name).
110 Str("reference", reference).
111 Msg("failed to update tags list")
112 }
113
114 // Mirror tag-addressed manifests under their content digest so
115 // digest-addressed pulls (the pinned deploy path) resolve.
116 if !validation.IsDigest(reference) {
117 digest := fmt.Sprintf("sha256:%x", sha256.Sum256(data))
118 if digest != reference {
119 if err := s.putManifestByDigest(name, digest, contentType, data); err != nil {
120 s.log.Warn().
121 Str(zerowrap.FieldLayer, "adapter").
122 Str(zerowrap.FieldAdapter, "filesystem").
123 Err(err).
124 Str("name", name).
125 Str("digest", digest).
126 Msg("failed to mirror manifest by digest")
127 }
128 }
129 }
130
131 s.log.Info().
132 Str(zerowrap.FieldLayer, "adapter").
133 Str(zerowrap.FieldAdapter, "filesystem").
134 Str("name", name).
135 Str("reference", reference).
136 Msg("manifest stored")
137
138 return nil
139}
140
141// DeleteManifest removes a manifest.
142func (s *ManifestStorage) DeleteManifest(name, reference string) error {
143 manifestPath, err := s.getManifestPath(name, reference)
144 if err != nil {
145 return fmt.Errorf("invalid path: %w", err)
146 }
147
148 if err := os.Remove(manifestPath); err != nil {
149 if os.IsNotExist(err) {
150 return fmt.Errorf("manifest not found: %s/%s", name, reference)
151 }
152 return fmt.Errorf("failed to delete manifest: %w", err)
153 }
154
155 // Delete content type file
156 if err := s.deleteManifestContentType(name, reference); err != nil {
157 s.log.Warn().
158 Str(zerowrap.FieldLayer, "adapter").
159 Str(zerowrap.FieldAdapter, "filesystem").
160 Err(err).
161 Str("name", name).
162 Str("reference", reference).
163 Msg("failed to delete manifest content type")
164 }
165
166 // Remove from tags list
167 if err := s.removeFromTagsList(name, reference); err != nil {
168 s.log.Warn().
169 Str(zerowrap.FieldLayer, "adapter").
170 Str(zerowrap.FieldAdapter, "filesystem").
171 Err(err).
172 Str("name", name).
173 Str("reference", reference).
174 Msg("failed to update tags list")
175 }
176
177 s.log.Info().
178 Str(zerowrap.FieldLayer, "adapter").
179 Str(zerowrap.FieldAdapter, "filesystem").
180 Str("name", name).
181 Str("reference", reference).
182 Msg("manifest deleted")
183
184 return nil
185}
186
187// ListTags returns all tags for a repository.
188func (s *ManifestStorage) ListTags(name string) ([]string, error) {
189 tagsPath, err := s.getTagsPath(name)
190 if err != nil {
191 return nil, fmt.Errorf("invalid path: %w", err)
192 }
193
194 data, err := os.ReadFile(tagsPath)
195 if err != nil {
196 if os.IsNotExist(err) {
197 return []string{}, nil // No tags file means no tags
198 }
199 return nil, fmt.Errorf("failed to read tags file: %w", err)
200 }
201
202 var tags []string
203 if err := json.Unmarshal(data, &tags); err != nil {
204 return nil, fmt.Errorf("failed to parse tags file: %w", err)
205 }
206
207 return tags, nil
208}
209
210// ListRepositories returns all repository names.
211func (s *ManifestStorage) ListRepositories() ([]string, error) {
212 reposDir := filepath.Join(s.rootDir, "repositories")
213
214 var repositories []string
215 err := filepath.Walk(reposDir, func(path string, info os.FileInfo, err error) error {
216 if err != nil {
217 return err
218 }
219
220 if info.IsDir() && path != reposDir {
221 // Get relative path from repositories directory
222 relPath, err := filepath.Rel(reposDir, path)
223 if err != nil {
224 return err
225 }
226
227 // Check if this directory contains manifests
228 manifestsDir := filepath.Join(path, "manifests")
229 if _, err := os.Stat(manifestsDir); err == nil {
230 repositories = append(repositories, relPath)
231 }
232 }
233
234 return nil
235 })
236
237 if err != nil {
238 return nil, fmt.Errorf("failed to list repositories: %w", err)
239 }
240
241 return repositories, nil
242}
243
244// GetManifestModTime returns the manifest modification time.
245func (s *ManifestStorage) GetManifestModTime(name, reference string) (time.Time, error) {
246 manifestPath, err := s.getManifestPath(name, reference)
247 if err != nil {
248 return time.Time{}, fmt.Errorf("invalid path: %w", err)
249 }
250
251 info, err := os.Stat(manifestPath)
252 if err != nil {
253 if os.IsNotExist(err) {
254 return time.Time{}, fmt.Errorf("manifest not found: %s/%s", name, reference)
255 }
256 return time.Time{}, fmt.Errorf("failed to stat manifest: %w", err)
257 }
258
259 return info.ModTime(), nil
260}
261
262// Helper methods for path generation with security validation
263
264func (s *ManifestStorage) getManifestPath(name, reference string) (string, error) {
265 // Validate name to prevent path traversal (defense in depth)
266 if _, err := validation.ValidatePath(name); err != nil {
267 return "", fmt.Errorf("invalid repository name: %w", err)
268 }
269 if _, err := validation.ValidatePath(reference); err != nil {
270 return "", fmt.Errorf("invalid reference: %w", err)
271 }
272
273 path := filepath.Join(s.rootDir, "repositories", name, "manifests", reference)
274
275 // Verify the path stays within root directory
276 if err := validation.ValidatePathWithinRoot(s.rootDir, path); err != nil {
277 return "", fmt.Errorf("path validation failed: %w", err)
278 }
279
280 return path, nil
281}
282
283func (s *ManifestStorage) getManifestContentTypePath(name, reference string) (string, error) {
284 manifestPath, err := s.getManifestPath(name, reference)
285 if err != nil {
286 return "", err
287 }
288 return manifestPath + ".contenttype", nil
289}
290
291func (s *ManifestStorage) getTagsPath(name string) (string, error) {
292 // Validate name to prevent path traversal
293 if _, err := validation.ValidatePath(name); err != nil {
294 return "", fmt.Errorf("invalid repository name: %w", err)
295 }
296
297 path := filepath.Join(s.rootDir, "repositories", name, "tags.json")
298
299 // Verify the path stays within root directory
300 if err := validation.ValidatePathWithinRoot(s.rootDir, path); err != nil {
301 return "", fmt.Errorf("path validation failed: %w", err)
302 }
303
304 return path, nil
305}
306
307// Content type helpers
308
309func (s *ManifestStorage) getManifestContentType(name, reference string) (string, error) {
310 contentTypePath, err := s.getManifestContentTypePath(name, reference)
311 if err != nil {
312 return "", err
313 }
314 data, err := os.ReadFile(contentTypePath)
315 if err != nil {
316 return "", err
317 }
318 return string(data), nil
319}
320
321func (s *ManifestStorage) putManifestContentType(name, reference, contentType string) error {
322 contentTypePath, err := s.getManifestContentTypePath(name, reference)
323 if err != nil {
324 return err
325 }
326 return os.WriteFile(contentTypePath, []byte(contentType), 0600)
327}
328
329// putManifestByDigest mirrors one manifest under its content digest so
330// digest-addressed pulls resolve.
331func (s *ManifestStorage) putManifestByDigest(name, digest, contentType string, data []byte) error {
332 manifestPath, err := s.getManifestPath(name, digest)
333 if err != nil {
334 return err
335 }
336 if err := os.MkdirAll(filepath.Dir(manifestPath), 0750); err != nil {
337 return fmt.Errorf("failed to create manifest directory: %w", err)
338 }
339 if err := os.WriteFile(manifestPath, data, 0600); err != nil {
340 return fmt.Errorf("failed to write digest manifest: %w", err)
341 }
342 return s.putManifestContentType(name, digest, contentType)
343}
344
345func (s *ManifestStorage) deleteManifestContentType(name, reference string) error {
346 contentTypePath, err := s.getManifestContentTypePath(name, reference)
347 if err != nil {
348 return err
349 }
350 err = os.Remove(contentTypePath)
351 if err != nil && !os.IsNotExist(err) {
352 return err
353 }
354 return nil
355}
356
357// Tags management helpers
358
359// filterOutDigests removes digest entries from a tag list.
360func filterOutDigests(tags []string) []string {
361 filtered := make([]string, 0, len(tags))
362 for _, tag := range tags {
363 if !validation.IsDigest(tag) {
364 filtered = append(filtered, tag)
365 }
366 }
367 return filtered
368}
369
370func (s *ManifestStorage) updateTagsList(name, reference string) error {
371 if validation.IsDigest(reference) {
372 return nil
373 }
374
375 tags, err := s.ListTags(name)
376 if err != nil {
377 return err
378 }
379
380 tags = filterOutDigests(tags)
381
382 // Add tag if not already present
383 for _, tag := range tags {
384 if tag == reference {
385 return nil // Already exists
386 }
387 }
388
389 tags = append(tags, reference)
390 return s.saveTagsList(name, tags)
391}
392
393func (s *ManifestStorage) removeFromTagsList(name, reference string) error {
394 tags, err := s.ListTags(name)
395 if err != nil {
396 return err
397 }
398
399 tags = filterOutDigests(tags)
400
401 // Remove tag if present
402 var newTags []string
403 for _, tag := range tags {
404 if tag != reference {
405 newTags = append(newTags, tag)
406 }
407 }
408
409 return s.saveTagsList(name, newTags)
410}
411
412func (s *ManifestStorage) saveTagsList(name string, tags []string) error {
413 tagsPath, err := s.getTagsPath(name)
414 if err != nil {
415 return fmt.Errorf("invalid path: %w", err)
416 }
417
418 // Create directory if it doesn't exist
419 if err := os.MkdirAll(filepath.Dir(tagsPath), 0750); err != nil {
420 return fmt.Errorf("failed to create tags directory: %w", err)
421 }
422
423 data, err := json.Marshal(tags)
424 if err != nil {
425 return fmt.Errorf("failed to marshal tags: %w", err)
426 }
427
428 if err := os.WriteFile(tagsPath, data, 0600); err != nil {
429 return fmt.Errorf("failed to write tags file: %w", err)
430 }
431
432 return nil
433}