mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Add GET and DELETE endpoints for Docker blob uploads (#21367)
This PR adds support for https://docs.docker.com/registry/spec/api/#get-blob-upload https://docs.docker.com/registry/spec/api/#delete-blob-upload Both are not required by the OCI spec but some clients call these endpoints. Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		@@ -316,8 +316,10 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
			r.Group("/blobs/uploads", func() {
 | 
			
		||||
				r.Post("", container.InitiateUploadBlob)
 | 
			
		||||
				r.Group("/{uuid}", func() {
 | 
			
		||||
					r.Get("", container.GetUploadBlob)
 | 
			
		||||
					r.Patch("", container.UploadBlob)
 | 
			
		||||
					r.Put("", container.EndUploadBlob)
 | 
			
		||||
					r.Delete("", container.CancelUploadBlob)
 | 
			
		||||
				})
 | 
			
		||||
			}, reqPackageAccess(perm.AccessModeWrite))
 | 
			
		||||
			r.Group("/blobs/{digest}", func() {
 | 
			
		||||
@@ -377,7 +379,7 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			m := blobsUploadsPattern.FindStringSubmatch(path)
 | 
			
		||||
			if len(m) == 3 && (isPut || isPatch) {
 | 
			
		||||
			if len(m) == 3 && (isGet || isPut || isPatch || isDelete) {
 | 
			
		||||
				reqPackageAccess(perm.AccessModeWrite)(ctx)
 | 
			
		||||
				if ctx.Written() {
 | 
			
		||||
					return
 | 
			
		||||
@@ -391,10 +393,14 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
 | 
			
		||||
				ctx.SetParams("uuid", m[2])
 | 
			
		||||
 | 
			
		||||
				if isPatch {
 | 
			
		||||
				if isGet {
 | 
			
		||||
					container.GetUploadBlob(ctx)
 | 
			
		||||
				} else if isPatch {
 | 
			
		||||
					container.UploadBlob(ctx)
 | 
			
		||||
				} else {
 | 
			
		||||
				} else if isPut {
 | 
			
		||||
					container.EndUploadBlob(ctx)
 | 
			
		||||
				} else {
 | 
			
		||||
					container.CancelUploadBlob(ctx)
 | 
			
		||||
				}
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -248,6 +248,27 @@ func InitiateUploadBlob(ctx *context.Context) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://docs.docker.com/registry/spec/api/#get-blob-upload
 | 
			
		||||
func GetUploadBlob(ctx *context.Context) {
 | 
			
		||||
	uuid := ctx.Params("uuid")
 | 
			
		||||
 | 
			
		||||
	upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == packages_model.ErrPackageBlobUploadNotExist {
 | 
			
		||||
			apiErrorDefined(ctx, errBlobUploadUnknown)
 | 
			
		||||
		} else {
 | 
			
		||||
			apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
			
		||||
		Range:      fmt.Sprintf("0-%d", upload.BytesReceived),
 | 
			
		||||
		UploadUUID: upload.ID,
 | 
			
		||||
		Status:     http.StatusNoContent,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
 | 
			
		||||
func UploadBlob(ctx *context.Context) {
 | 
			
		||||
	image := ctx.Params("image")
 | 
			
		||||
@@ -354,6 +375,30 @@ func EndUploadBlob(ctx *context.Context) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://docs.docker.com/registry/spec/api/#delete-blob-upload
 | 
			
		||||
func CancelUploadBlob(ctx *context.Context) {
 | 
			
		||||
	uuid := ctx.Params("uuid")
 | 
			
		||||
 | 
			
		||||
	_, err := packages_model.GetBlobUploadByID(ctx, uuid)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == packages_model.ErrPackageBlobUploadNotExist {
 | 
			
		||||
			apiErrorDefined(ctx, errBlobUploadUnknown)
 | 
			
		||||
		} else {
 | 
			
		||||
			apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := container_service.RemoveBlobUploadByID(ctx, uuid); err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
			
		||||
		Status: http.StatusNoContent,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
 | 
			
		||||
	digest := ctx.Params("digest")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -205,18 +205,54 @@ func TestPackageContainer(t *testing.T) {
 | 
			
		||||
				assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid"))
 | 
			
		||||
				assert.Equal(t, contentRange, resp.Header().Get("Range"))
 | 
			
		||||
 | 
			
		||||
				uploadURL = resp.Header().Get("Location")
 | 
			
		||||
 | 
			
		||||
				req = NewRequest(t, "GET", setting.AppURL+uploadURL[1:])
 | 
			
		||||
				addTokenAuthHeader(req, userToken)
 | 
			
		||||
				resp = MakeRequest(t, req, http.StatusNoContent)
 | 
			
		||||
 | 
			
		||||
				assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid"))
 | 
			
		||||
				assert.Equal(t, fmt.Sprintf("0-%d", len(blobContent)), resp.Header().Get("Range"))
 | 
			
		||||
 | 
			
		||||
				pbu, err = packages_model.GetBlobUploadByID(db.DefaultContext, uuid)
 | 
			
		||||
				assert.NoError(t, err)
 | 
			
		||||
				assert.EqualValues(t, len(blobContent), pbu.BytesReceived)
 | 
			
		||||
 | 
			
		||||
				uploadURL = resp.Header().Get("Location")
 | 
			
		||||
 | 
			
		||||
				req = NewRequest(t, "PUT", fmt.Sprintf("%s?digest=%s", setting.AppURL+uploadURL[1:], blobDigest))
 | 
			
		||||
				addTokenAuthHeader(req, userToken)
 | 
			
		||||
				resp = MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
				assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location"))
 | 
			
		||||
				assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
 | 
			
		||||
 | 
			
		||||
				t.Run("Cancel", func(t *testing.T) {
 | 
			
		||||
					defer tests.PrintCurrentTest(t)()
 | 
			
		||||
 | 
			
		||||
					req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url))
 | 
			
		||||
					addTokenAuthHeader(req, userToken)
 | 
			
		||||
					resp := MakeRequest(t, req, http.StatusAccepted)
 | 
			
		||||
 | 
			
		||||
					uuid := resp.Header().Get("Docker-Upload-Uuid")
 | 
			
		||||
					assert.NotEmpty(t, uuid)
 | 
			
		||||
 | 
			
		||||
					uploadURL := resp.Header().Get("Location")
 | 
			
		||||
					assert.NotEmpty(t, uploadURL)
 | 
			
		||||
 | 
			
		||||
					req = NewRequest(t, "GET", setting.AppURL+uploadURL[1:])
 | 
			
		||||
					addTokenAuthHeader(req, userToken)
 | 
			
		||||
					resp = MakeRequest(t, req, http.StatusNoContent)
 | 
			
		||||
 | 
			
		||||
					assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid"))
 | 
			
		||||
					assert.Equal(t, "0-0", resp.Header().Get("Range"))
 | 
			
		||||
 | 
			
		||||
					req = NewRequest(t, "DELETE", setting.AppURL+uploadURL[1:])
 | 
			
		||||
					addTokenAuthHeader(req, userToken)
 | 
			
		||||
					MakeRequest(t, req, http.StatusNoContent)
 | 
			
		||||
 | 
			
		||||
					req = NewRequest(t, "GET", setting.AppURL+uploadURL[1:])
 | 
			
		||||
					addTokenAuthHeader(req, userToken)
 | 
			
		||||
					MakeRequest(t, req, http.StatusNotFound)
 | 
			
		||||
				})
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			for _, tag := range tags {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user