1package filesystem
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8
9 "github.com/bnema/zerowrap"
10
11 "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
306func (s *ManifestStorage) updateTagsList(name, reference string) error {
307 tags, err := s.ListTags(name)
308 if err != nil {
309 return err
310 }
311
312 // Add tag if not already present
313 for _, tag := range tags {
314 if tag == reference {
315 return nil // Already exists
316 }
317 }
318
319 tags = append(tags, reference)
320 return s.saveTagsList(name, tags)
321}
322
323func (s *ManifestStorage) removeFromTagsList(name, reference string) error {
324 tags, err := s.ListTags(name)
325 if err != nil {
326 return err
327 }
328
329 // Remove tag if present
330 var newTags []string
331 for _, tag := range tags {
332 if tag != reference {
333 newTags = append(newTags, tag)
334 }
335 }
336
337 return s.saveTagsList(name, newTags)
338}
339
340func (s *ManifestStorage) saveTagsList(name string, tags []string) error {
341 tagsPath, err := s.getTagsPath(name)
342 if err != nil {
343 return fmt.Errorf("invalid path: %w", err)
344 }
345
346 // Create directory if it doesn't exist
347 if err := os.MkdirAll(filepath.Dir(tagsPath), 0750); err != nil {
348 return fmt.Errorf("failed to create tags directory: %w", err)
349 }
350
351 data, err := json.Marshal(tags)
352 if err != nil {
353 return fmt.Errorf("failed to marshal tags: %w", err)
354 }
355
356 if err := os.WriteFile(tagsPath, data, 0600); err != nil {
357 return fmt.Errorf("failed to write tags file: %w", err)
358 }
359
360 return nil
361}