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