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