Skip to content

Commit 051d642

Browse files
authored
registry: Implement Range requests for blobs (#1917)
This makes crane registry usable with registry explorer. Signed-off-by: Jon Johnson <[email protected]>
1 parent 0309184 commit 051d642

File tree

2 files changed

+93
-4
lines changed

2 files changed

+93
-4
lines changed

pkg/registry/blobs.go

+57-4
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ type redirectError struct {
9393
Code int
9494
}
9595

96+
type bytesCloser struct {
97+
*bytes.Reader
98+
}
99+
100+
func (r *bytesCloser) Close() error {
101+
return nil
102+
}
103+
96104
func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) }
97105

98106
// errNotFound represents an error locating the blob.
@@ -115,6 +123,7 @@ func (m *memHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error)
115123
}
116124
return int64(len(b)), nil
117125
}
126+
118127
func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser, error) {
119128
m.lock.Lock()
120129
defer m.lock.Unlock()
@@ -123,8 +132,9 @@ func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser,
123132
if !found {
124133
return nil, errNotFound
125134
}
126-
return io.NopCloser(bytes.NewReader(b)), nil
135+
return &bytesCloser{bytes.NewReader(b)}, nil
127136
}
137+
128138
func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadCloser) error {
129139
m.lock.Lock()
130140
defer m.lock.Unlock()
@@ -137,6 +147,7 @@ func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadClose
137147
m.m[h.String()] = all
138148
return nil
139149
}
150+
140151
func (m *memHandler) Delete(_ context.Context, _ string, h v1.Hash) error {
141152
m.lock.Lock()
142153
defer m.lock.Unlock()
@@ -177,6 +188,7 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError {
177188
service := elem[len(elem)-2]
178189
digest := req.URL.Query().Get("digest")
179190
contentRange := req.Header.Get("Content-Range")
191+
rangeHeader := req.Header.Get("Range")
180192

181193
repo := req.URL.Host + path.Join(elem[1:len(elem)-2]...)
182194

@@ -265,8 +277,10 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError {
265277

266278
return regErrInternal(err)
267279
}
280+
268281
defer rc.Close()
269282
r = rc
283+
270284
} else {
271285
tmp, err := b.blobHandler.Get(req.Context(), repo, h)
272286
if errors.Is(err, errNotFound) {
@@ -287,9 +301,48 @@ func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError {
287301
r = &buf
288302
}
289303

290-
resp.Header().Set("Content-Length", fmt.Sprint(size))
291-
resp.Header().Set("Docker-Content-Digest", h.String())
292-
resp.WriteHeader(http.StatusOK)
304+
if rangeHeader != "" {
305+
start, end := int64(0), int64(0)
306+
if _, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); err != nil {
307+
return &regError{
308+
Status: http.StatusRequestedRangeNotSatisfiable,
309+
Code: "BLOB_UNKNOWN",
310+
Message: "We don't understand your Range",
311+
}
312+
}
313+
314+
n := (end + 1) - start
315+
if ra, ok := r.(io.ReaderAt); ok {
316+
if end+1 > size {
317+
return &regError{
318+
Status: http.StatusRequestedRangeNotSatisfiable,
319+
Code: "BLOB_UNKNOWN",
320+
Message: fmt.Sprintf("range end %d > %d size", end+1, size),
321+
}
322+
}
323+
r = io.NewSectionReader(ra, start, n)
324+
} else {
325+
if _, err := io.CopyN(io.Discard, r, start); err != nil {
326+
return &regError{
327+
Status: http.StatusRequestedRangeNotSatisfiable,
328+
Code: "BLOB_UNKNOWN",
329+
Message: fmt.Sprintf("Failed to discard %d bytes", start),
330+
}
331+
}
332+
333+
r = io.LimitReader(r, n)
334+
}
335+
336+
resp.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))
337+
resp.Header().Set("Content-Length", fmt.Sprint(n))
338+
resp.Header().Set("Docker-Content-Digest", h.String())
339+
resp.WriteHeader(http.StatusPartialContent)
340+
} else {
341+
resp.Header().Set("Content-Length", fmt.Sprint(size))
342+
resp.Header().Set("Docker-Content-Digest", h.String())
343+
resp.WriteHeader(http.StatusOK)
344+
}
345+
293346
io.Copy(resp, r)
294347
return nil
295348

pkg/registry/registry_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,42 @@ func TestCalls(t *testing.T) {
137137
Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"},
138138
Want: "foo",
139139
},
140+
{
141+
Description: "GET blob range",
142+
Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
143+
Method: "GET",
144+
URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
145+
Code: http.StatusPartialContent,
146+
RequestHeader: map[string]string{
147+
"Range": "bytes=1-2",
148+
},
149+
Header: map[string]string{
150+
"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
151+
"Content-Length": "2",
152+
"Content-Range": "bytes 1-2/3",
153+
},
154+
Want: "oo",
155+
},
156+
{
157+
Description: "GET invalid range header",
158+
Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
159+
Method: "GET",
160+
URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
161+
RequestHeader: map[string]string{
162+
"Range": "nibbles=123-456",
163+
},
164+
Code: http.StatusRequestedRangeNotSatisfiable,
165+
},
166+
{
167+
Description: "GET bad blob range",
168+
Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},
169+
Method: "GET",
170+
URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
171+
RequestHeader: map[string]string{
172+
"Range": "bytes=1-3",
173+
},
174+
Code: http.StatusRequestedRangeNotSatisfiable,
175+
},
140176
{
141177
Description: "HEAD blob",
142178
Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"},

0 commit comments

Comments
 (0)