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