1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package ociserver
16
17 import (
18 "context"
19 "encoding/json"
20 "fmt"
21 "io"
22 "net/http"
23 "strconv"
24
25 "github.com/opencontainers/go-digest"
26 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
27
28 "cuelabs.dev/go/oci/ociregistry"
29 "cuelabs.dev/go/oci/ociregistry/internal/ocirequest"
30 )
31
32 func (r *registry) handleBlobUploadBlob(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error {
33 if r.opts.DisableSinglePostUpload {
34 return r.handleBlobStartUpload(ctx, resp, req, rreq)
35 }
36
37 mediaType := mediaTypeOctetStream
38
39 desc, err := r.backend.PushBlob(req.Context(), rreq.Repo, ociregistry.Descriptor{
40 MediaType: mediaType,
41 Size: req.ContentLength,
42 Digest: ociregistry.Digest(rreq.Digest),
43 }, req.Body)
44 if err != nil {
45 return err
46 }
47 if err := r.setLocationHeader(resp, false, desc, "/v2/"+rreq.Repo+"/blobs/"+string(desc.Digest)); err != nil {
48 return err
49 }
50 resp.WriteHeader(http.StatusCreated)
51 return nil
52 }
53
54 func (r *registry) handleBlobStartUpload(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error {
55
56
57 w, err := r.backend.PushBlobChunked(ctx, rreq.Repo, 0)
58 if err != nil {
59 return err
60 }
61 defer w.Close()
62
63 resp.Header().Set("Location", r.locationForUploadID(rreq.Repo, w.ID()))
64 resp.Header().Set("Range", "0-0")
65
66
67
68 resp.Header().Set("OCI-Chunk-Min-Length", strconv.Itoa(w.ChunkSize()))
69 resp.WriteHeader(http.StatusAccepted)
70 return nil
71 }
72
73 func (r *registry) handleBlobUploadInfo(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error {
74
75
76
77
78 w, err := r.backend.PushBlobChunkedResume(ctx, rreq.Repo, rreq.UploadID, -1, 0)
79 if err != nil {
80 return err
81 }
82 defer w.Close()
83 resp.Header().Set("Location", r.locationForUploadID(rreq.Repo, w.ID()))
84 resp.Header().Set("Range", ocirequest.RangeString(0, w.Size()))
85 resp.WriteHeader(http.StatusNoContent)
86 return nil
87 }
88
89 func (r *registry) handleBlobUploadChunk(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error {
90
91
92
93 start, end, err := chunkRange(req)
94 if err != nil {
95 return err
96 }
97
98 w, err := r.backend.PushBlobChunkedResume(ctx, rreq.Repo, rreq.UploadID, start, int(end-start))
99 if err != nil {
100 return err
101 }
102 if _, err := io.Copy(w, req.Body); err != nil {
103 w.Close()
104 return fmt.Errorf("cannot copy blob data: %w", err)
105 }
106 if err := w.Close(); err != nil {
107 return fmt.Errorf("cannot close BlobWriter: %w", err)
108 }
109 resp.Header().Set("Location", r.locationForUploadID(rreq.Repo, w.ID()))
110 resp.Header().Set("Range", ocirequest.RangeString(0, w.Size()))
111 resp.WriteHeader(http.StatusAccepted)
112 return nil
113 }
114
115 func (r *registry) handleBlobCompleteUpload(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error {
116
117
118
119
120
121
122
123
124
125
126
127
128
129 start, end, err := chunkRange(req)
130 if err != nil {
131 return err
132 }
133
134 w, err := r.backend.PushBlobChunkedResume(ctx, rreq.Repo, rreq.UploadID, start, int(end-start))
135 if err != nil {
136 return err
137 }
138 defer w.Close()
139
140 if _, err := io.Copy(w, req.Body); err != nil {
141 return fmt.Errorf("failed to copy data to %T: %v", w, err)
142 }
143 desc, err := w.Commit(ociregistry.Digest(rreq.Digest))
144 if err != nil {
145 return err
146 }
147 if err := r.setLocationHeader(resp, false, desc, "/v2/"+rreq.Repo+"/blobs/"+string(desc.Digest)); err != nil {
148 return err
149 }
150 resp.WriteHeader(http.StatusCreated)
151 return nil
152 }
153
154 func (r *registry) handleBlobMount(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error {
155 desc, err := r.backend.MountBlob(ctx, rreq.FromRepo, rreq.Repo, ociregistry.Digest(rreq.Digest))
156 if err != nil {
157 return err
158 }
159 if err := r.setLocationHeader(resp, true, desc, "/v2/"+rreq.Repo+"/blobs/"+rreq.Digest); err != nil {
160 return err
161 }
162 resp.WriteHeader(http.StatusCreated)
163 return nil
164 }
165
166 func (r *registry) handleManifestPut(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error {
167 mediaType := req.Header.Get("Content-Type")
168 if mediaType == "" {
169 mediaType = mediaTypeOctetStream
170 }
171
172
173 data, err := io.ReadAll(req.Body)
174 if err != nil {
175 return fmt.Errorf("cannot read content: %v", err)
176 }
177 dig := digest.FromBytes(data)
178 var tag string
179 if rreq.Tag != "" {
180 tag = rreq.Tag
181 } else {
182 if ociregistry.Digest(rreq.Digest) != dig {
183 return ociregistry.ErrDigestInvalid
184 }
185 }
186 subjectDesc, err := subjectFromManifest(req.Header.Get("Content-Type"), data)
187 if err != nil {
188 return fmt.Errorf("invalid manifest JSON: %v", err)
189 }
190 desc, err := r.backend.PushManifest(ctx, rreq.Repo, tag, data, mediaType)
191 if err != nil {
192 return err
193 }
194 if err := r.setLocationHeader(resp, false, desc, "/v2/"+rreq.Repo+"/manifests/"+string(desc.Digest)); err != nil {
195 return err
196 }
197 if subjectDesc != nil {
198 resp.Header().Set("OCI-Subject", string(subjectDesc.Digest))
199 }
200
201 resp.WriteHeader(http.StatusCreated)
202 return nil
203 }
204
205 func subjectFromManifest(contentType string, data []byte) (*ociregistry.Descriptor, error) {
206 switch contentType {
207 case ocispec.MediaTypeImageManifest,
208 ocispec.MediaTypeImageIndex:
209 break
210
211 default:
212 return nil, nil
213 }
214 var m struct {
215 Subject *ociregistry.Descriptor `json:"subject"`
216 }
217 if err := json.Unmarshal(data, &m); err != nil {
218 return nil, err
219 }
220 return m.Subject, nil
221 }
222
223 func (r *registry) locationForUploadID(repo string, uploadID string) string {
224 _, loc := (&ocirequest.Request{
225 Kind: ocirequest.ReqBlobUploadInfo,
226 Repo: repo,
227 UploadID: uploadID,
228 }).MustConstruct()
229 return loc
230 }
231
232 func chunkRange(req *http.Request) (start, end int64, _ error) {
233 var rangeOK bool
234 if s := req.Header.Get("Content-Range"); s != "" {
235 start, end, rangeOK = ocirequest.ParseRange(s)
236 if !rangeOK {
237 return 0, 0, badAPIUseError("we don't understand your Content-Range")
238 }
239 }
240
241 if rangeOK && req.ContentLength >= 0 {
242 rangeLength := end - start
243 if rangeLength != req.ContentLength {
244 return 0, 0, badAPIUseError("Content-Range implies a length of %d but Content-Length is %d", rangeLength, req.ContentLength)
245 }
246 }
247
248
249
250
251
252
253
254 if !rangeOK && req.ContentLength >= 0 {
255 end = req.ContentLength
256 }
257 return start, end, nil
258 }
259
View as plain text