github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/handlers/blobupload.go (about) 1 package handlers 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "os" 8 9 "github.com/docker/distribution" 10 ctxu "github.com/docker/distribution/context" 11 "github.com/docker/distribution/digest" 12 "github.com/docker/distribution/reference" 13 "github.com/docker/distribution/registry/api/errcode" 14 "github.com/docker/distribution/registry/api/v2" 15 "github.com/docker/distribution/registry/storage" 16 "github.com/gorilla/handlers" 17 ) 18 19 // blobUploadDispatcher constructs and returns the blob upload handler for the 20 // given request context. 21 func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler { 22 buh := &blobUploadHandler{ 23 Context: ctx, 24 UUID: getUploadUUID(ctx), 25 } 26 27 handler := handlers.MethodHandler{ 28 "GET": http.HandlerFunc(buh.GetUploadStatus), 29 "HEAD": http.HandlerFunc(buh.GetUploadStatus), 30 } 31 32 if !ctx.readOnly { 33 handler["POST"] = http.HandlerFunc(buh.StartBlobUpload) 34 handler["PATCH"] = http.HandlerFunc(buh.PatchBlobData) 35 handler["PUT"] = http.HandlerFunc(buh.PutBlobUploadComplete) 36 handler["DELETE"] = http.HandlerFunc(buh.CancelBlobUpload) 37 } 38 39 if buh.UUID != "" { 40 state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state")) 41 if err != nil { 42 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 ctxu.GetLogger(ctx).Infof("error resolving upload: %v", err) 44 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) 45 }) 46 } 47 buh.State = state 48 49 if state.Name != ctx.Repository.Name().Name() { 50 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 51 ctxu.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, buh.Repository.Name()) 52 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) 53 }) 54 } 55 56 if state.UUID != buh.UUID { 57 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 58 ctxu.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, buh.UUID) 59 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) 60 }) 61 } 62 63 blobs := ctx.Repository.Blobs(buh) 64 upload, err := blobs.Resume(buh, buh.UUID) 65 if err != nil { 66 ctxu.GetLogger(ctx).Errorf("error resolving upload: %v", err) 67 if err == distribution.ErrBlobUploadUnknown { 68 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 69 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown.WithDetail(err)) 70 }) 71 } 72 73 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 74 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 75 }) 76 } 77 buh.Upload = upload 78 79 if state.Offset > 0 { 80 // Seek the blob upload to the correct spot if it's non-zero. 81 // These error conditions should be rare and demonstrate really 82 // problems. We basically cancel the upload and tell the client to 83 // start over. 84 if nn, err := upload.Seek(buh.State.Offset, os.SEEK_SET); err != nil { 85 defer upload.Close() 86 ctxu.GetLogger(ctx).Infof("error seeking blob upload: %v", err) 87 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 88 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) 89 upload.Cancel(buh) 90 }) 91 } else if nn != buh.State.Offset { 92 defer upload.Close() 93 ctxu.GetLogger(ctx).Infof("seek to wrong offest: %d != %d", nn, buh.State.Offset) 94 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 95 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) 96 upload.Cancel(buh) 97 }) 98 } 99 } 100 101 return closeResources(handler, buh.Upload) 102 } 103 104 return handler 105 } 106 107 // blobUploadHandler handles the http blob upload process. 108 type blobUploadHandler struct { 109 *Context 110 111 // UUID identifies the upload instance for the current request. Using UUID 112 // to key blob writers since this implementation uses UUIDs. 113 UUID string 114 115 Upload distribution.BlobWriter 116 117 State blobUploadState 118 } 119 120 // StartBlobUpload begins the blob upload process and allocates a server-side 121 // blob writer session, optionally mounting the blob from a separate repository. 122 func (buh *blobUploadHandler) StartBlobUpload(w http.ResponseWriter, r *http.Request) { 123 var options []distribution.BlobCreateOption 124 125 fromRepo := r.FormValue("from") 126 mountDigest := r.FormValue("mount") 127 128 if mountDigest != "" && fromRepo != "" { 129 opt, err := buh.createBlobMountOption(fromRepo, mountDigest) 130 if opt != nil && err == nil { 131 options = append(options, opt) 132 } 133 } 134 135 blobs := buh.Repository.Blobs(buh) 136 upload, err := blobs.Create(buh, options...) 137 138 if err != nil { 139 if ebm, ok := err.(distribution.ErrBlobMounted); ok { 140 if err := buh.writeBlobCreatedHeaders(w, ebm.Descriptor); err != nil { 141 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 142 } 143 } else if err == distribution.ErrUnsupported { 144 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported) 145 } else { 146 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 147 } 148 return 149 } 150 151 buh.Upload = upload 152 defer buh.Upload.Close() 153 154 if err := buh.blobUploadResponse(w, r, true); err != nil { 155 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 156 return 157 } 158 159 w.Header().Set("Docker-Upload-UUID", buh.Upload.ID()) 160 w.WriteHeader(http.StatusAccepted) 161 } 162 163 // GetUploadStatus returns the status of a given upload, identified by id. 164 func (buh *blobUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) { 165 if buh.Upload == nil { 166 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) 167 return 168 } 169 170 // TODO(dmcgowan): Set last argument to false in blobUploadResponse when 171 // resumable upload is supported. This will enable returning a non-zero 172 // range for clients to begin uploading at an offset. 173 if err := buh.blobUploadResponse(w, r, true); err != nil { 174 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 175 return 176 } 177 178 w.Header().Set("Docker-Upload-UUID", buh.UUID) 179 w.WriteHeader(http.StatusNoContent) 180 } 181 182 // PatchBlobData writes data to an upload. 183 func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Request) { 184 if buh.Upload == nil { 185 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) 186 return 187 } 188 189 ct := r.Header.Get("Content-Type") 190 if ct != "" && ct != "application/octet-stream" { 191 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(fmt.Errorf("Bad Content-Type"))) 192 // TODO(dmcgowan): encode error 193 return 194 } 195 196 // TODO(dmcgowan): support Content-Range header to seek and write range 197 198 if err := copyFullPayload(w, r, buh.Upload, buh, "blob PATCH", &buh.Errors); err != nil { 199 // copyFullPayload reports the error if necessary 200 return 201 } 202 203 if err := buh.blobUploadResponse(w, r, false); err != nil { 204 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 205 return 206 } 207 208 w.WriteHeader(http.StatusAccepted) 209 } 210 211 // PutBlobUploadComplete takes the final request of a blob upload. The 212 // request may include all the blob data or no blob data. Any data 213 // provided is received and verified. If successful, the blob is linked 214 // into the blob store and 201 Created is returned with the canonical 215 // url of the blob. 216 func (buh *blobUploadHandler) PutBlobUploadComplete(w http.ResponseWriter, r *http.Request) { 217 if buh.Upload == nil { 218 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) 219 return 220 } 221 222 dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters! 223 224 if dgstStr == "" { 225 // no digest? return error, but allow retry. 226 buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest missing")) 227 return 228 } 229 230 dgst, err := digest.ParseDigest(dgstStr) 231 if err != nil { 232 // no digest? return error, but allow retry. 233 buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest parsing failed")) 234 return 235 } 236 237 if err := copyFullPayload(w, r, buh.Upload, buh, "blob PUT", &buh.Errors); err != nil { 238 // copyFullPayload reports the error if necessary 239 return 240 } 241 242 desc, err := buh.Upload.Commit(buh, distribution.Descriptor{ 243 Digest: dgst, 244 245 // TODO(stevvooe): This isn't wildly important yet, but we should 246 // really set the length and mediatype. For now, we can let the 247 // backend take care of this. 248 }) 249 250 if err != nil { 251 switch err := err.(type) { 252 case distribution.ErrBlobInvalidDigest: 253 buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err)) 254 default: 255 switch err { 256 case distribution.ErrUnsupported: 257 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported) 258 case distribution.ErrBlobInvalidLength, distribution.ErrBlobDigestUnsupported: 259 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) 260 default: 261 ctxu.GetLogger(buh).Errorf("unknown error completing upload: %#v", err) 262 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 263 } 264 265 } 266 267 // Clean up the backend blob data if there was an error. 268 if err := buh.Upload.Cancel(buh); err != nil { 269 // If the cleanup fails, all we can do is observe and report. 270 ctxu.GetLogger(buh).Errorf("error canceling upload after error: %v", err) 271 } 272 273 return 274 } 275 if err := buh.writeBlobCreatedHeaders(w, desc); err != nil { 276 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 277 return 278 } 279 } 280 281 // CancelBlobUpload cancels an in-progress upload of a blob. 282 func (buh *blobUploadHandler) CancelBlobUpload(w http.ResponseWriter, r *http.Request) { 283 if buh.Upload == nil { 284 buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) 285 return 286 } 287 288 w.Header().Set("Docker-Upload-UUID", buh.UUID) 289 if err := buh.Upload.Cancel(buh); err != nil { 290 ctxu.GetLogger(buh).Errorf("error encountered canceling upload: %v", err) 291 buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) 292 } 293 294 w.WriteHeader(http.StatusNoContent) 295 } 296 297 // blobUploadResponse provides a standard request for uploading blobs and 298 // chunk responses. This sets the correct headers but the response status is 299 // left to the caller. The fresh argument is used to ensure that new blob 300 // uploads always start at a 0 offset. This allows disabling resumable push by 301 // always returning a 0 offset on check status. 302 func (buh *blobUploadHandler) blobUploadResponse(w http.ResponseWriter, r *http.Request, fresh bool) error { 303 304 var offset int64 305 if !fresh { 306 var err error 307 offset, err = buh.Upload.Seek(0, os.SEEK_CUR) 308 if err != nil { 309 ctxu.GetLogger(buh).Errorf("unable get current offset of blob upload: %v", err) 310 return err 311 } 312 } 313 314 // TODO(stevvooe): Need a better way to manage the upload state automatically. 315 buh.State.Name = buh.Repository.Name().Name() 316 buh.State.UUID = buh.Upload.ID() 317 buh.State.Offset = offset 318 buh.State.StartedAt = buh.Upload.StartedAt() 319 320 token, err := hmacKey(buh.Config.HTTP.Secret).packUploadState(buh.State) 321 if err != nil { 322 ctxu.GetLogger(buh).Infof("error building upload state token: %s", err) 323 return err 324 } 325 326 uploadURL, err := buh.urlBuilder.BuildBlobUploadChunkURL( 327 buh.Repository.Name(), buh.Upload.ID(), 328 url.Values{ 329 "_state": []string{token}, 330 }) 331 if err != nil { 332 ctxu.GetLogger(buh).Infof("error building upload url: %s", err) 333 return err 334 } 335 336 endRange := offset 337 if endRange > 0 { 338 endRange = endRange - 1 339 } 340 341 w.Header().Set("Docker-Upload-UUID", buh.UUID) 342 w.Header().Set("Location", uploadURL) 343 w.Header().Set("Content-Length", "0") 344 w.Header().Set("Range", fmt.Sprintf("0-%d", endRange)) 345 346 return nil 347 } 348 349 // mountBlob attempts to mount a blob from another repository by its digest. If 350 // successful, the blob is linked into the blob store and 201 Created is 351 // returned with the canonical url of the blob. 352 func (buh *blobUploadHandler) createBlobMountOption(fromRepo, mountDigest string) (distribution.BlobCreateOption, error) { 353 dgst, err := digest.ParseDigest(mountDigest) 354 if err != nil { 355 return nil, err 356 } 357 358 ref, err := reference.ParseNamed(fromRepo) 359 if err != nil { 360 return nil, err 361 } 362 363 canonical, err := reference.WithDigest(ref, dgst) 364 if err != nil { 365 return nil, err 366 } 367 368 return storage.WithMountFrom(canonical), nil 369 } 370 371 // writeBlobCreatedHeaders writes the standard headers describing a newly 372 // created blob. A 201 Created is written as well as the canonical URL and 373 // blob digest. 374 func (buh *blobUploadHandler) writeBlobCreatedHeaders(w http.ResponseWriter, desc distribution.Descriptor) error { 375 ref, err := reference.WithDigest(buh.Repository.Name(), desc.Digest) 376 if err != nil { 377 return err 378 } 379 blobURL, err := buh.urlBuilder.BuildBlobURL(ref) 380 if err != nil { 381 return err 382 } 383 384 w.Header().Set("Location", blobURL) 385 w.Header().Set("Content-Length", "0") 386 w.Header().Set("Docker-Content-Digest", desc.Digest.String()) 387 w.WriteHeader(http.StatusCreated) 388 return nil 389 }