2.0.1 dcf7d6b

storage.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close