cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociserver/reader.go (about) 1 // Copyright 2018 Google LLC All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ociserver 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "net/http" 22 23 "cuelabs.dev/go/oci/ociregistry" 24 "cuelabs.dev/go/oci/ociregistry/internal/ocirequest" 25 ) 26 27 func (r *registry) handleBlobHead(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error { 28 desc, err := r.backend.ResolveBlob(ctx, rreq.Repo, ociregistry.Digest(rreq.Digest)) 29 if err != nil { 30 return err 31 } 32 resp.Header().Set("Content-Length", fmt.Sprint(desc.Size)) 33 resp.Header().Set("Docker-Content-Digest", string(desc.Digest)) 34 // TODO this is true in theory, but what if the backend doesn't support GetBlobRange ? 35 resp.Header().Set("Accept-Ranges", "bytes") 36 resp.WriteHeader(http.StatusOK) 37 return nil 38 } 39 40 func (r *registry) handleBlobGet(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error { 41 if r.opts.LocationsForDescriptor != nil { 42 // We need to find information on the blob before we can determine 43 // what to pass back, so resolve the blob first so we don't 44 // stimulate the backend to start sending the whole stream 45 // only to abandon it. 46 desc, err := r.backend.ResolveBlob(ctx, rreq.Repo, ociregistry.Digest(rreq.Digest)) 47 if err != nil { 48 // TODO this might not be the best response because ResolveBlob is 49 // often implemented with a HEAD request that can't return an error 50 // body. So it might be better to fall through to the usual GetBlob request, 51 // although that would mean that every error makes two calls :( 52 return err 53 } 54 locs, err := r.opts.LocationsForDescriptor(false, desc) 55 if err != nil { 56 return err 57 } 58 if len(locs) > 0 { 59 // TODO choose randomly from the set of locations? 60 // TODO make it possible to turn off this behaviour? 61 http.Redirect(resp, req, locs[0], http.StatusTemporaryRedirect) 62 return nil 63 } 64 } 65 ranges, err := parseRange(req.Header.Get("Range")) 66 if err != nil { 67 return withHTTPCode(http.StatusRequestedRangeNotSatisfiable, err) 68 } 69 switch len(ranges) { 70 case 0: 71 blob, err := r.backend.GetBlob(ctx, rreq.Repo, ociregistry.Digest(rreq.Digest)) 72 if err != nil { 73 return err 74 } 75 defer blob.Close() 76 desc := blob.Descriptor() 77 resp.Header().Set("Content-Type", desc.MediaType) 78 resp.Header().Set("Content-Length", fmt.Sprint(desc.Size)) 79 resp.Header().Set("Docker-Content-Digest", rreq.Digest) 80 resp.WriteHeader(http.StatusOK) 81 82 io.Copy(resp, blob) 83 return nil 84 case 1: 85 rng := ranges[0] 86 blob, err := r.backend.GetBlobRange(ctx, rreq.Repo, ociregistry.Digest(rreq.Digest), rng.start, rng.end) 87 if err != nil { 88 // TODO fall back to using GetBlob if err is ErrUnsupported? 89 return err 90 } 91 defer blob.Close() 92 desc := blob.Descriptor() 93 if rng.end == -1 || rng.end > desc.Size { 94 rng.end = desc.Size 95 } 96 if rng.start > desc.Size { 97 return withHTTPCode(http.StatusRequestedRangeNotSatisfiable, fmt.Errorf("range starts after end of blob")) 98 } 99 if rng.end < rng.start { 100 return withHTTPCode(http.StatusRequestedRangeNotSatisfiable, fmt.Errorf("range end is before start")) 101 } 102 resp.Header().Set("Content-Type", desc.MediaType) 103 resp.Header().Set("Content-Length", fmt.Sprint(rng.end-rng.start)) 104 resp.Header().Set("Docker-Content-Digest", rreq.Digest) 105 resp.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, desc.Size)) 106 resp.WriteHeader(http.StatusPartialContent) 107 108 io.Copy(resp, blob) 109 return nil 110 111 default: 112 return withHTTPCode(http.StatusRequestedRangeNotSatisfiable, fmt.Errorf("only a single range is supported")) 113 } 114 } 115 116 func (r *registry) handleManifestGet(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error { 117 // TODO we could do a redirect here too if we thought it was worthwhile. 118 var mr ociregistry.BlobReader 119 var err error 120 if rreq.Tag != "" { 121 mr, err = r.backend.GetTag(ctx, rreq.Repo, rreq.Tag) 122 } else { 123 mr, err = r.backend.GetManifest(ctx, rreq.Repo, ociregistry.Digest(rreq.Digest)) 124 } 125 if err != nil { 126 return err 127 } 128 desc := mr.Descriptor() 129 if !r.opts.OmitDigestFromTagGetResponse { 130 resp.Header().Set("Docker-Content-Digest", string(desc.Digest)) 131 } 132 resp.Header().Set("Content-Type", desc.MediaType) 133 resp.Header().Set("Content-Length", fmt.Sprint(desc.Size)) 134 resp.WriteHeader(http.StatusOK) 135 io.Copy(resp, mr) 136 return nil 137 } 138 139 func (r *registry) handleManifestHead(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error { 140 var desc ociregistry.Descriptor 141 var err error 142 if rreq.Tag != "" { 143 desc, err = r.backend.ResolveTag(ctx, rreq.Repo, rreq.Tag) 144 } else { 145 desc, err = r.backend.ResolveManifest(ctx, rreq.Repo, ociregistry.Digest(rreq.Digest)) 146 } 147 if err != nil { 148 return err 149 } 150 if !r.opts.OmitDigestFromTagGetResponse || rreq.Tag != "" { 151 // Note: when doing a HEAD of a tag, clients are entitled 152 // to expect that the digest header is set on the response 153 // even though the spec says it's only optional in this case. 154 // TODO raise an issue on the spec about this. 155 resp.Header().Set("Docker-Content-Digest", string(desc.Digest)) 156 } 157 resp.Header().Set("Content-Type", desc.MediaType) 158 resp.Header().Set("Content-Length", fmt.Sprint(desc.Size)) 159 resp.WriteHeader(http.StatusOK) 160 return nil 161 }