v2.31.0-dev9 062dcae

blob_test.go RawHistoryFolder

1package filesystem
2
3import (
4	"bytes"
5	"crypto/sha256"
6	"encoding/hex"
7	"errors"
8	"io"
9	"os"
10	"path/filepath"
11	"sync"
12	"testing"
13
14	"github.com/bnema/zerowrap"
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/require"
17
18	"github.com/bnema/gordon/internal/domain"
19)
20
21const (
22	testDigest1       = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
23	testDigest2       = "sha256:b3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d5"
24	testDigestSHA512  = "sha512:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
25	testDigestMissing = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
26)
27
28func testLogger() zerowrap.Logger {
29	return zerowrap.Default()
30}
31
32type readFailer struct {
33	read bool
34}
35
36func (r *readFailer) Read(_ []byte) (int, error) {
37	r.read = true
38	return 0, io.ErrUnexpectedEOF
39}
40
41func TestNewBlobStorage(t *testing.T) {
42	tmpDir := t.TempDir()
43	log := testLogger()
44
45	storage, err := NewBlobStorage(tmpDir, log)
46
47	require.NoError(t, err)
48	assert.NotNil(t, storage)
49
50	// Verify directories were created
51	assert.DirExists(t, filepath.Join(tmpDir, "blobs"))
52	assert.DirExists(t, filepath.Join(tmpDir, "uploads"))
53}
54
55func TestNewBlobStorage_InvalidPath(t *testing.T) {
56	log := testLogger()
57
58	// Try to create in a path that can't be created (no permissions)
59	_, err := NewBlobStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
60
61	assert.Error(t, err)
62}
63
64func TestBlobStorage_PutAndGetBlob(t *testing.T) {
65	tmpDir := t.TempDir()
66	log := testLogger()
67
68	storage, err := NewBlobStorage(tmpDir, log)
69	require.NoError(t, err)
70
71	blobData := []byte("test blob content")
72
73	// Put blob
74	err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
75	require.NoError(t, err)
76
77	// Get blob
78	reader, err := storage.GetBlob(testDigest1)
79	require.NoError(t, err)
80	defer reader.Close()
81
82	data, err := io.ReadAll(reader)
83	require.NoError(t, err)
84
85	assert.Equal(t, blobData, data)
86}
87
88func TestBlobStorage_GetBlob_NotFound(t *testing.T) {
89	tmpDir := t.TempDir()
90	log := testLogger()
91
92	storage, err := NewBlobStorage(tmpDir, log)
93	require.NoError(t, err)
94
95	reader, err := storage.GetBlob(testDigestMissing)
96
97	assert.Error(t, err)
98	assert.Nil(t, reader)
99	assert.Contains(t, err.Error(), "blob not found")
100}
101
102func TestBlobStorage_GetBlobPath(t *testing.T) {
103	tmpDir := t.TempDir()
104	log := testLogger()
105
106	storage, err := NewBlobStorage(tmpDir, log)
107	require.NoError(t, err)
108
109	blobData := []byte("test blob content")
110
111	// Put blob first
112	err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
113	require.NoError(t, err)
114
115	// Get blob path
116	path, err := storage.GetBlobPath(testDigest1)
117	require.NoError(t, err)
118	assert.NotEmpty(t, path)
119
120	// Verify file exists at path
121	_, err = os.Stat(path)
122	assert.NoError(t, err)
123}
124
125func TestBlobStorage_GetBlobPath_NotFound(t *testing.T) {
126	tmpDir := t.TempDir()
127	log := testLogger()
128
129	storage, err := NewBlobStorage(tmpDir, log)
130	require.NoError(t, err)
131
132	path, err := storage.GetBlobPath(testDigestMissing)
133
134	assert.Error(t, err)
135	assert.Empty(t, path)
136	assert.Contains(t, err.Error(), "blob not found")
137}
138
139func TestBlobStorage_PutBlob_SizeMismatch(t *testing.T) {
140	tmpDir := t.TempDir()
141	log := testLogger()
142
143	storage, err := NewBlobStorage(tmpDir, log)
144	require.NoError(t, err)
145
146	blobData := []byte("test blob content")
147
148	// Put blob with wrong size
149	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), 999)
150
151	assert.Error(t, err)
152	assert.Contains(t, err.Error(), "size mismatch")
153}
154
155func TestBlobStorage_PutBlob_ZeroSize(t *testing.T) {
156	tmpDir := t.TempDir()
157	log := testLogger()
158
159	storage, err := NewBlobStorage(tmpDir, log)
160	require.NoError(t, err)
161
162	blobData := []byte("test blob content")
163
164	// Put blob with size=0 (should skip size check)
165	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), 0)
166
167	assert.NoError(t, err)
168
169	// Verify blob was stored
170	assert.True(t, storage.BlobExists(testDigest2))
171}
172
173func TestBlobStorage_DeleteBlob(t *testing.T) {
174	tmpDir := t.TempDir()
175	log := testLogger()
176
177	storage, err := NewBlobStorage(tmpDir, log)
178	require.NoError(t, err)
179
180	blobData := []byte("test blob content")
181
182	// Put blob
183	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
184	require.NoError(t, err)
185
186	// Verify it exists
187	assert.True(t, storage.BlobExists(testDigest2))
188
189	// Delete blob
190	size, err := storage.DeleteBlob(testDigest2)
191	require.NoError(t, err)
192	assert.Equal(t, int64(len(blobData)), size)
193
194	// Verify it's gone
195	assert.False(t, storage.BlobExists(testDigest2))
196}
197
198func TestBlobStorage_DeleteBlob_NotFound(t *testing.T) {
199	tmpDir := t.TempDir()
200	log := testLogger()
201
202	storage, err := NewBlobStorage(tmpDir, log)
203	require.NoError(t, err)
204
205	size, err := storage.DeleteBlob(testDigestMissing)
206
207	assert.Error(t, err)
208	assert.Equal(t, int64(0), size)
209	assert.Contains(t, err.Error(), "blob not found")
210}
211
212func TestBlobStorage_BlobExists(t *testing.T) {
213	tmpDir := t.TempDir()
214	log := testLogger()
215
216	storage, err := NewBlobStorage(tmpDir, log)
217	require.NoError(t, err)
218
219	// Should not exist initially
220	assert.False(t, storage.BlobExists(testDigest2))
221
222	// Put blob
223	blobData := []byte("test blob content")
224	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
225	require.NoError(t, err)
226
227	// Should exist now
228	assert.True(t, storage.BlobExists(testDigest2))
229}
230
231func TestBlobStorage_ListBlobs(t *testing.T) {
232	tmpDir := t.TempDir()
233	log := testLogger()
234
235	storage, err := NewBlobStorage(tmpDir, log)
236	require.NoError(t, err)
237
238	blobData := []byte("test blob content")
239
240	err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
241	require.NoError(t, err)
242
243	err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
244	require.NoError(t, err)
245
246	digests, err := storage.ListBlobs()
247	require.NoError(t, err)
248
249	assert.ElementsMatch(t, []string{testDigest1, testDigest2}, digests)
250}
251
252func TestBlobStorage_ListBlobs_Empty(t *testing.T) {
253	tmpDir := t.TempDir()
254	log := testLogger()
255
256	storage, err := NewBlobStorage(tmpDir, log)
257	require.NoError(t, err)
258
259	digests, err := storage.ListBlobs()
260	require.NoError(t, err)
261	assert.Empty(t, digests)
262}
263
264func TestBlobStorage_StartBlobUpload(t *testing.T) {
265	tmpDir := t.TempDir()
266	log := testLogger()
267
268	storage, err := NewBlobStorage(tmpDir, log)
269	require.NoError(t, err)
270
271	uuid, err := storage.StartBlobUpload("myapp")
272
273	require.NoError(t, err)
274	assert.NotEmpty(t, uuid)
275	assert.Len(t, uuid, 36) // standard UUID format
276
277	// Verify upload file was created
278	uploadPath := filepath.Join(tmpDir, "uploads", uuid)
279	_, err = os.Stat(uploadPath)
280	assert.NoError(t, err)
281}
282
283func TestBlobStorage_AppendBlobChunk(t *testing.T) {
284	tmpDir := t.TempDir()
285	log := testLogger()
286
287	storage, err := NewBlobStorage(tmpDir, log)
288	require.NoError(t, err)
289
290	uuid, err := storage.StartBlobUpload("myapp")
291	require.NoError(t, err)
292
293	// Append first chunk
294	chunk1 := []byte("first chunk")
295	size1, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk1), -1, 0)
296	require.NoError(t, err)
297	assert.Equal(t, int64(len(chunk1)), size1)
298
299	// Append second chunk
300	chunk2 := []byte(" second chunk")
301	size2, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk2), -1, 0)
302	require.NoError(t, err)
303	assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2)
304}
305
306func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) {
307	tmpDir := t.TempDir()
308	log := testLogger()
309
310	storage, err := NewBlobStorage(tmpDir, log)
311	require.NoError(t, err)
312
313	_, err = storage.AppendBlobChunk("myapp", "00000000-0000-4000-a000-000000000000", bytes.NewReader([]byte("data")), -1, 100)
314
315	assert.Error(t, err)
316	assert.Contains(t, err.Error(), "upload not found")
317}
318
319func TestBlobStorage_AppendBlobChunk_MaxBlobSize(t *testing.T) {
320	tmpDir := t.TempDir()
321	log := testLogger()
322
323	storage, err := NewBlobStorage(tmpDir, log)
324	require.NoError(t, err)
325
326	uuid, err := storage.StartBlobUpload("myapp")
327	require.NoError(t, err)
328
329	size, err := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("12345")), -1, 10)
330	require.NoError(t, err)
331	assert.Equal(t, int64(5), size)
332
333	size, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("67890")), -1, 10)
334	require.NoError(t, err)
335	assert.Equal(t, int64(10), size)
336
337	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("x")), -1, 10)
338	require.Error(t, err)
339	assert.True(t, errors.Is(err, domain.ErrBlobSizeExceeded))
340
341	uploadPath := filepath.Join(tmpDir, "uploads", uuid)
342	info, statErr := os.Stat(uploadPath)
343	require.NoError(t, statErr)
344	assert.Equal(t, int64(10), info.Size())
345}
346
347func TestBlobStorage_AppendBlobChunk_MaxBlobSizeContentLengthTooLargeDoesNotRead(t *testing.T) {
348	tmpDir := t.TempDir()
349	log := testLogger()
350
351	storage, err := NewBlobStorage(tmpDir, log)
352	require.NoError(t, err)
353
354	uuid, err := storage.StartBlobUpload("myapp")
355	require.NoError(t, err)
356
357	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("12345")), 5, 10)
358	require.NoError(t, err)
359
360	reader := &readFailer{}
361	_, err = storage.AppendBlobChunk("myapp", uuid, reader, 6, 10)
362	require.Error(t, err)
363	assert.ErrorIs(t, err, domain.ErrBlobSizeExceeded)
364	assert.False(t, reader.read)
365}
366
367func TestBlobStorage_AppendBlobChunk_MaxBlobSizeOverflowMidReadTruncates(t *testing.T) {
368	tmpDir := t.TempDir()
369	log := testLogger()
370
371	storage, err := NewBlobStorage(tmpDir, log)
372	require.NoError(t, err)
373
374	uuid, err := storage.StartBlobUpload("myapp")
375	require.NoError(t, err)
376
377	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("12345")), -1, 10)
378	require.NoError(t, err)
379
380	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("6789012345")), -1, 10)
381	require.Error(t, err)
382	assert.True(t, errors.Is(err, domain.ErrBlobSizeExceeded))
383
384	uploadPath := filepath.Join(tmpDir, "uploads", uuid)
385	info, statErr := os.Stat(uploadPath)
386	require.NoError(t, statErr)
387	assert.Equal(t, int64(5), info.Size())
388}
389
390func TestBlobStorage_GetBlobUpload(t *testing.T) {
391	tmpDir := t.TempDir()
392	log := testLogger()
393
394	storage, err := NewBlobStorage(tmpDir, log)
395	require.NoError(t, err)
396
397	uuid, err := storage.StartBlobUpload("myapp")
398	require.NoError(t, err)
399
400	writer, err := storage.GetBlobUpload(uuid)
401	require.NoError(t, err)
402	defer writer.Close()
403
404	// Write some data
405	_, err = writer.Write([]byte("test data"))
406	assert.NoError(t, err)
407}
408
409func TestBlobStorage_GetBlobUpload_NotFound(t *testing.T) {
410	tmpDir := t.TempDir()
411	log := testLogger()
412
413	storage, err := NewBlobStorage(tmpDir, log)
414	require.NoError(t, err)
415
416	writer, err := storage.GetBlobUpload("00000000-0000-4000-a000-000000000000")
417
418	assert.Error(t, err)
419	assert.Nil(t, writer)
420	assert.Contains(t, err.Error(), "upload not found")
421}
422
423func TestBlobStorage_FinishBlobUpload(t *testing.T) {
424	tmpDir := t.TempDir()
425	log := testLogger()
426
427	storage, err := NewBlobStorage(tmpDir, log)
428	require.NoError(t, err)
429
430	// Start upload
431	uuid, err := storage.StartBlobUpload("myapp")
432	require.NoError(t, err)
433
434	// Append data - content that hashes to testDigest1
435	blobData := []byte("complete blob content")
436	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0)
437	require.NoError(t, err)
438
439	// Finish upload - use testDigest1 (validation will pass, but hash won't match)
440	// This tests the flow, actual digest verification happens in verifyUploadDigest
441	err = storage.FinishBlobUpload(uuid, testDigest1)
442	// This will fail because content doesn't match digest
443	assert.Error(t, err)
444	assert.Contains(t, err.Error(), "digest")
445}
446
447func TestBlobStorage_CancelBlobUpload(t *testing.T) {
448	tmpDir := t.TempDir()
449	log := testLogger()
450
451	storage, err := NewBlobStorage(tmpDir, log)
452	require.NoError(t, err)
453
454	// Start upload
455	uuid, err := storage.StartBlobUpload("myapp")
456	require.NoError(t, err)
457
458	// Verify upload file exists
459	uploadPath := filepath.Join(tmpDir, "uploads", uuid)
460	_, err = os.Stat(uploadPath)
461	require.NoError(t, err)
462
463	// Cancel upload
464	err = storage.CancelBlobUpload(uuid)
465	require.NoError(t, err)
466
467	// Verify upload file is gone
468	_, err = os.Stat(uploadPath)
469	assert.True(t, os.IsNotExist(err))
470}
471
472func TestBlobStorage_CancelBlobUpload_NotFound(t *testing.T) {
473	tmpDir := t.TempDir()
474	log := testLogger()
475
476	storage, err := NewBlobStorage(tmpDir, log)
477	require.NoError(t, err)
478
479	err = storage.CancelBlobUpload("00000000-0000-4000-a000-000000000000")
480
481	assert.Error(t, err)
482	assert.Contains(t, err.Error(), "upload not found")
483}
484
485func TestBlobStorage_BlobPathStructure(t *testing.T) {
486	tmpDir := t.TempDir()
487	log := testLogger()
488
489	storage, err := NewBlobStorage(tmpDir, log)
490	require.NoError(t, err)
491
492	// Test that blobs are stored with proper directory structure
493	blobData := []byte("test blob content")
494
495	err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
496	require.NoError(t, err)
497
498	// Check the path structure: blobs/sha256/a3/a3ed95caeb02...
499	// First 2 chars of hash after sha256: are "a3"
500	expectedPath := filepath.Join(tmpDir, "blobs", "sha256", "a3", "a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
501	_, err = os.Stat(expectedPath)
502	assert.NoError(t, err)
503}
504
505func TestBlobStorage_DigestFormats(t *testing.T) {
506	tmpDir := t.TempDir()
507	log := testLogger()
508
509	storage, err := NewBlobStorage(tmpDir, log)
510	require.NoError(t, err)
511
512	tests := []struct {
513		name   string
514		digest string
515	}{
516		{"sha256 standard", testDigest1},
517		{"sha512", testDigestSHA512},
518	}
519
520	for _, tt := range tests {
521		t.Run(tt.name, func(t *testing.T) {
522			blobData := []byte("test content for " + tt.digest)
523
524			err := storage.PutBlob(tt.digest, bytes.NewReader(blobData), int64(len(blobData)))
525			require.NoError(t, err)
526
527			assert.True(t, storage.BlobExists(tt.digest))
528
529			reader, err := storage.GetBlob(tt.digest)
530			require.NoError(t, err)
531			defer reader.Close()
532
533			data, err := io.ReadAll(reader)
534			require.NoError(t, err)
535			assert.Equal(t, blobData, data)
536		})
537	}
538}
539
540func TestBlobStorage_CompleteUploadFlow(t *testing.T) {
541	tmpDir := t.TempDir()
542	log := testLogger()
543
544	storage, err := NewBlobStorage(tmpDir, log)
545	require.NoError(t, err)
546
547	// Simulate a complete upload flow
548	repoName := "myorg/myapp"
549
550	// 1. Start upload
551	uuid, err := storage.StartBlobUpload(repoName)
552	require.NoError(t, err)
553
554	// 2. Send multiple chunks
555	chunks := [][]byte{
556		[]byte("first chunk of data"),
557		[]byte(" - middle chunk"),
558		[]byte(" - final chunk"),
559	}
560
561	var totalSize int64
562	for _, chunk := range chunks {
563		size, err := storage.AppendBlobChunk(repoName, uuid, bytes.NewReader(chunk), -1, 0)
564		require.NoError(t, err)
565		totalSize = size
566	}
567
568	expectedTotal := int64(len(chunks[0]) + len(chunks[1]) + len(chunks[2]))
569	assert.Equal(t, expectedTotal, totalSize)
570
571	// 3. Cancel upload (since we can't easily compute the real digest)
572	err = storage.CancelBlobUpload(uuid)
573	require.NoError(t, err)
574}
575
576func TestBlobStorage_ConcurrentAppendAndFinalize(t *testing.T) {
577	tmpDir := t.TempDir()
578	log := testLogger()
579
580	storage, err := NewBlobStorage(tmpDir, log)
581	require.NoError(t, err)
582
583	uuid, err := storage.StartBlobUpload("myapp")
584	require.NoError(t, err)
585
586	initialData := []byte("initial-")
587	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(initialData), -1, 0)
588	require.NoError(t, err)
589
590	appendChunk := []byte("chunk")
591	appenders := 8
592	var wg sync.WaitGroup
593	appendReady := make(chan struct{})
594	finalizeErrs := make(chan error, 1)
595	appendErrs := make(chan error, appenders)
596
597	for range appenders {
598		wg.Add(1)
599		go func() {
600			defer wg.Done()
601			<-appendReady
602			_, appendErr := storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(appendChunk), -1, 0)
603			appendErrs <- appendErr
604		}()
605	}
606
607	close(appendReady)
608	fullyAppended := appendRepeated(initialData, appendChunk, appenders)
609
610	wg.Add(1)
611	go func() {
612		defer wg.Done()
613		finalizeErrs <- storage.FinishBlobUpload(uuid, digestForData(fullyAppended))
614	}()
615
616	wg.Wait()
617	close(appendErrs)
618
619	finalizeErr := <-finalizeErrs
620	if finalizeErr == nil {
621		for appendErr := range appendErrs {
622			require.NoError(t, appendErr)
623		}
624
625		reader, err := storage.GetBlob(digestForData(fullyAppended))
626		require.NoError(t, err)
627		defer reader.Close()
628
629		blobData, err := io.ReadAll(reader)
630		require.NoError(t, err)
631		assert.Equal(t, fullyAppended, blobData)
632	} else {
633		assert.Contains(t, finalizeErr.Error(), "digest verification failed")
634
635		appendCount := 0
636		for appendErr := range appendErrs {
637			require.NoError(t, appendErr)
638			appendCount++
639		}
640
641		require.Equal(t, appenders, appendCount)
642
643		retryDigest := digestForData(fullyAppended)
644		err = storage.FinishBlobUpload(uuid, retryDigest)
645		require.NoError(t, err)
646
647		reader, err := storage.GetBlob(retryDigest)
648		require.NoError(t, err)
649		defer reader.Close()
650
651		blobData, err := io.ReadAll(reader)
652		require.NoError(t, err)
653		assert.Equal(t, fullyAppended, blobData)
654	}
655
656	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("after")), -1, 0)
657	assert.Error(t, err)
658	assert.Contains(t, err.Error(), "upload not found")
659}
660
661func TestBlobStorage_AppendAfterFinalize(t *testing.T) {
662	tmpDir := t.TempDir()
663	log := testLogger()
664
665	storage, err := NewBlobStorage(tmpDir, log)
666	require.NoError(t, err)
667
668	uuid, err := storage.StartBlobUpload("myapp")
669	require.NoError(t, err)
670
671	blobData := []byte("finalized content")
672	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0)
673	require.NoError(t, err)
674
675	err = storage.FinishBlobUpload(uuid, digestForData(blobData))
676	require.NoError(t, err)
677
678	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader([]byte("more")), -1, 0)
679	assert.Error(t, err)
680	assert.Contains(t, err.Error(), "upload not found")
681}
682
683func TestBlobStorage_FailedFinalizeRetryable(t *testing.T) {
684	tmpDir := t.TempDir()
685	log := testLogger()
686
687	storage, err := NewBlobStorage(tmpDir, log)
688	require.NoError(t, err)
689
690	uuid, err := storage.StartBlobUpload("myapp")
691	require.NoError(t, err)
692
693	blobData := []byte("retryable finalize")
694	_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(blobData), -1, 0)
695	require.NoError(t, err)
696
697	err = storage.FinishBlobUpload(uuid, testDigest1)
698	assert.Error(t, err)
699	assert.Contains(t, err.Error(), "digest verification failed")
700
701	err = storage.FinishBlobUpload(uuid, digestForData(blobData))
702	require.NoError(t, err)
703}
704
705func TestBlobStorage_SequentialFlow(t *testing.T) {
706	tmpDir := t.TempDir()
707	log := testLogger()
708
709	storage, err := NewBlobStorage(tmpDir, log)
710	require.NoError(t, err)
711
712	uuid, err := storage.StartBlobUpload("myapp")
713	require.NoError(t, err)
714
715	chunks := [][]byte{
716		[]byte("one-"),
717		[]byte("two-"),
718		[]byte("three"),
719	}
720
721	var blobData []byte
722	for _, chunk := range chunks {
723		blobData = append(blobData, chunk...)
724		_, err = storage.AppendBlobChunk("myapp", uuid, bytes.NewReader(chunk), -1, 0)
725		require.NoError(t, err)
726	}
727
728	digest := digestForData(blobData)
729	err = storage.FinishBlobUpload(uuid, digest)
730	require.NoError(t, err)
731
732	reader, err := storage.GetBlob(digest)
733	require.NoError(t, err)
734	defer reader.Close()
735
736	storedData, err := io.ReadAll(reader)
737	require.NoError(t, err)
738	assert.Equal(t, blobData, storedData)
739}
740
741func digestForData(data []byte) string {
742	sum := sha256.Sum256(data)
743	return "sha256:" + hex.EncodeToString(sum[:])
744}
745
746func appendRepeated(initialData, chunk []byte, repetitions int) []byte {
747	data := append([]byte{}, initialData...)
748	for range repetitions {
749		data = append(data, chunk...)
750	}
751	return data
752}

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