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  }