mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 05:18:25 +00:00 
			
		
		
		
	Fix #28121 I did some tests and found that the `missing signature key` error is caused by an incorrect `Content-Type` header. Gitea correctly sets the `Content-Type` header when serving files.348d1d0f32/routers/api/packages/container/container.go (L712-L717)However, when `SERVE_DIRECT` is enabled, the `Content-Type` header may be set to an incorrect value by the storage service. To fix this issue, we can use query parameters to override response header values. https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html <img width="600px" src="https://github.com/user-attachments/assets/f2ff90f0-f1df-46f9-9680-b8120222c555" /> In this PR, I introduced a new parameter to the `URL` method to support additional parameters. ``` URL(path, name string, reqParams url.Values) (*url.URL, error) ``` --- Most S3-like services support specifying the content type when storing objects. However, Gitea always use `application/octet-stream`. Therefore, I believe we also need to improve the `Save` method to support storing objects with the correct content type.b7fb20e73e/modules/storage/minio.go (L214-L221)
		
			
				
	
	
		
			802 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			802 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package container
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	auth_model "code.gitea.io/gitea/models/auth"
 | 
						|
	packages_model "code.gitea.io/gitea/models/packages"
 | 
						|
	container_model "code.gitea.io/gitea/models/packages/container"
 | 
						|
	user_model "code.gitea.io/gitea/models/user"
 | 
						|
	"code.gitea.io/gitea/modules/httplib"
 | 
						|
	"code.gitea.io/gitea/modules/json"
 | 
						|
	"code.gitea.io/gitea/modules/log"
 | 
						|
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
						|
	container_module "code.gitea.io/gitea/modules/packages/container"
 | 
						|
	"code.gitea.io/gitea/modules/setting"
 | 
						|
	"code.gitea.io/gitea/modules/util"
 | 
						|
	"code.gitea.io/gitea/routers/api/packages/helper"
 | 
						|
	auth_service "code.gitea.io/gitea/services/auth"
 | 
						|
	"code.gitea.io/gitea/services/context"
 | 
						|
	packages_service "code.gitea.io/gitea/services/packages"
 | 
						|
	container_service "code.gitea.io/gitea/services/packages/container"
 | 
						|
 | 
						|
	digest "github.com/opencontainers/go-digest"
 | 
						|
)
 | 
						|
 | 
						|
// maximum size of a container manifest
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
 | 
						|
const maxManifestSize = 10 * 1024 * 1024
 | 
						|
 | 
						|
var (
 | 
						|
	imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
 | 
						|
	referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
 | 
						|
)
 | 
						|
 | 
						|
type containerHeaders struct {
 | 
						|
	Status        int
 | 
						|
	ContentDigest string
 | 
						|
	UploadUUID    string
 | 
						|
	Range         string
 | 
						|
	Location      string
 | 
						|
	ContentType   string
 | 
						|
	ContentLength int64
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
 | 
						|
func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
 | 
						|
	if h.Location != "" {
 | 
						|
		resp.Header().Set("Location", h.Location)
 | 
						|
	}
 | 
						|
	if h.Range != "" {
 | 
						|
		resp.Header().Set("Range", h.Range)
 | 
						|
	}
 | 
						|
	if h.ContentType != "" {
 | 
						|
		resp.Header().Set("Content-Type", h.ContentType)
 | 
						|
	}
 | 
						|
	if h.ContentLength != 0 {
 | 
						|
		resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
 | 
						|
	}
 | 
						|
	if h.UploadUUID != "" {
 | 
						|
		resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
 | 
						|
	}
 | 
						|
	if h.ContentDigest != "" {
 | 
						|
		resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
 | 
						|
		resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
 | 
						|
	}
 | 
						|
	resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
 | 
						|
	resp.WriteHeader(h.Status)
 | 
						|
}
 | 
						|
 | 
						|
func jsonResponse(ctx *context.Context, status int, obj any) {
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		Status:      status,
 | 
						|
		ContentType: "application/json",
 | 
						|
	})
 | 
						|
	if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
 | 
						|
		log.Error("JSON encode: %v", err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func apiError(ctx *context.Context, status int, err error) {
 | 
						|
	helper.LogAndProcessError(ctx, status, err, func(message string) {
 | 
						|
		setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
			Status: status,
 | 
						|
		})
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
 | 
						|
func apiErrorDefined(ctx *context.Context, err *namedError) {
 | 
						|
	type ContainerError struct {
 | 
						|
		Code    string `json:"code"`
 | 
						|
		Message string `json:"message"`
 | 
						|
	}
 | 
						|
 | 
						|
	type ContainerErrors struct {
 | 
						|
		Errors []ContainerError `json:"errors"`
 | 
						|
	}
 | 
						|
 | 
						|
	jsonResponse(ctx, err.StatusCode, ContainerErrors{
 | 
						|
		Errors: []ContainerError{
 | 
						|
			{
 | 
						|
				Code:    err.Code,
 | 
						|
				Message: err.Message,
 | 
						|
			},
 | 
						|
		},
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func apiUnauthorizedError(ctx *context.Context) {
 | 
						|
	// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed
 | 
						|
	realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token"
 | 
						|
	ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
 | 
						|
	apiErrorDefined(ctx, errUnauthorized)
 | 
						|
}
 | 
						|
 | 
						|
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
 | 
						|
func ReqContainerAccess(ctx *context.Context) {
 | 
						|
	if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
 | 
						|
		apiUnauthorizedError(ctx)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// VerifyImageName is a middleware which checks if the image name is allowed
 | 
						|
func VerifyImageName(ctx *context.Context) {
 | 
						|
	if !imageNamePattern.MatchString(ctx.PathParam("image")) {
 | 
						|
		apiErrorDefined(ctx, errNameInvalid)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// DetermineSupport is used to test if the registry supports OCI
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
 | 
						|
func DetermineSupport(ctx *context.Context) {
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		Status: http.StatusOK,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// Authenticate creates a token for the current user
 | 
						|
// If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
 | 
						|
func Authenticate(ctx *context.Context) {
 | 
						|
	u := ctx.Doer
 | 
						|
	packageScope := auth_service.GetAccessScope(ctx.Data)
 | 
						|
	if u == nil {
 | 
						|
		if setting.Service.RequireSignInView {
 | 
						|
			apiUnauthorizedError(ctx)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		u = user_model.NewGhostUser()
 | 
						|
	} else {
 | 
						|
		if has, err := packageScope.HasAnyScope(
 | 
						|
			auth_model.AccessTokenScopeReadPackage,
 | 
						|
			auth_model.AccessTokenScopeWritePackage,
 | 
						|
			auth_model.AccessTokenScopeAll,
 | 
						|
		); !has {
 | 
						|
			if err != nil {
 | 
						|
				log.Error("Error checking access scope: %v", err)
 | 
						|
			}
 | 
						|
			apiUnauthorizedError(ctx)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	token, err := packages_service.CreateAuthorizationToken(u, packageScope)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.JSON(http.StatusOK, map[string]string{
 | 
						|
		"token": token,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://distribution.github.io/distribution/spec/auth/oauth/
 | 
						|
func AuthenticateNotImplemented(ctx *context.Context) {
 | 
						|
	// This optional endpoint can be used to authenticate a client.
 | 
						|
	// It must implement the specification described in:
 | 
						|
	// https://datatracker.ietf.org/doc/html/rfc6749
 | 
						|
	// https://distribution.github.io/distribution/spec/auth/oauth/
 | 
						|
	// Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed.
 | 
						|
 | 
						|
	ctx.Status(http.StatusNotFound)
 | 
						|
}
 | 
						|
 | 
						|
// https://docs.docker.com/registry/spec/api/#listing-repositories
 | 
						|
func GetRepositoryList(ctx *context.Context) {
 | 
						|
	n := ctx.FormInt("n")
 | 
						|
	if n <= 0 || n > 100 {
 | 
						|
		n = 100
 | 
						|
	}
 | 
						|
	last := ctx.FormTrim("last")
 | 
						|
 | 
						|
	repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	type RepositoryList struct {
 | 
						|
		Repositories []string `json:"repositories"`
 | 
						|
	}
 | 
						|
 | 
						|
	if len(repositories) == n {
 | 
						|
		v := url.Values{}
 | 
						|
		if n > 0 {
 | 
						|
			v.Add("n", strconv.Itoa(n))
 | 
						|
		}
 | 
						|
		v.Add("last", repositories[len(repositories)-1])
 | 
						|
 | 
						|
		ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode()))
 | 
						|
	}
 | 
						|
 | 
						|
	jsonResponse(ctx, http.StatusOK, RepositoryList{
 | 
						|
		Repositories: repositories,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
 | 
						|
func InitiateUploadBlob(ctx *context.Context) {
 | 
						|
	image := ctx.PathParam("image")
 | 
						|
 | 
						|
	mount := ctx.FormTrim("mount")
 | 
						|
	from := ctx.FormTrim("from")
 | 
						|
	if mount != "" {
 | 
						|
		blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
 | 
						|
			Repository: from,
 | 
						|
			Digest:     mount,
 | 
						|
		})
 | 
						|
		if blob != nil {
 | 
						|
			accessible, err := packages_model.IsBlobAccessibleForUser(ctx, blob.Blob.ID, ctx.Doer)
 | 
						|
			if err != nil {
 | 
						|
				apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			if accessible {
 | 
						|
				if err := mountBlob(ctx, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil {
 | 
						|
					apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
					return
 | 
						|
				}
 | 
						|
 | 
						|
				setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
					Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
 | 
						|
					ContentDigest: mount,
 | 
						|
					Status:        http.StatusCreated,
 | 
						|
				})
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	digest := ctx.FormTrim("digest")
 | 
						|
	if digest != "" {
 | 
						|
		buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
 | 
						|
		if err != nil {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		defer buf.Close()
 | 
						|
 | 
						|
		if digest != digestFromHashSummer(buf) {
 | 
						|
			apiErrorDefined(ctx, errDigestInvalid)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if _, err := saveAsPackageBlob(ctx,
 | 
						|
			buf,
 | 
						|
			&packages_service.PackageCreationInfo{
 | 
						|
				PackageInfo: packages_service.PackageInfo{
 | 
						|
					Owner: ctx.Package.Owner,
 | 
						|
					Name:  image,
 | 
						|
				},
 | 
						|
				Creator: ctx.Doer,
 | 
						|
			},
 | 
						|
		); err != nil {
 | 
						|
			switch err {
 | 
						|
			case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
 | 
						|
				apiError(ctx, http.StatusForbidden, err)
 | 
						|
			default:
 | 
						|
				apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			}
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
			Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
 | 
						|
			ContentDigest: digest,
 | 
						|
			Status:        http.StatusCreated,
 | 
						|
		})
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	upload, err := packages_model.CreateBlobUpload(ctx)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		Location:   fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
 | 
						|
		Range:      "0-0",
 | 
						|
		UploadUUID: upload.ID,
 | 
						|
		Status:     http.StatusAccepted,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://docs.docker.com/registry/spec/api/#get-blob-upload
 | 
						|
func GetUploadBlob(ctx *context.Context) {
 | 
						|
	uuid := ctx.PathParam("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.PathParam("image")
 | 
						|
 | 
						|
	uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"))
 | 
						|
	if err != nil {
 | 
						|
		if err == packages_model.ErrPackageBlobUploadNotExist {
 | 
						|
			apiErrorDefined(ctx, errBlobUploadUnknown)
 | 
						|
		} else {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
	defer uploader.Close()
 | 
						|
 | 
						|
	contentRange := ctx.Req.Header.Get("Content-Range")
 | 
						|
	if contentRange != "" {
 | 
						|
		start, end := 0, 0
 | 
						|
		if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
 | 
						|
			apiErrorDefined(ctx, errBlobUploadInvalid)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if int64(start) != uploader.Size() {
 | 
						|
			apiErrorDefined(ctx, errBlobUploadInvalid.WithStatusCode(http.StatusRequestedRangeNotSatisfiable))
 | 
						|
			return
 | 
						|
		}
 | 
						|
	} else if uploader.Size() != 0 {
 | 
						|
		apiErrorDefined(ctx, errBlobUploadInvalid.WithMessage("Stream uploads after first write are not allowed"))
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		Location:   fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID),
 | 
						|
		Range:      fmt.Sprintf("0-%d", uploader.Size()-1),
 | 
						|
		UploadUUID: uploader.ID,
 | 
						|
		Status:     http.StatusAccepted,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
 | 
						|
func EndUploadBlob(ctx *context.Context) {
 | 
						|
	image := ctx.PathParam("image")
 | 
						|
 | 
						|
	digest := ctx.FormTrim("digest")
 | 
						|
	if digest == "" {
 | 
						|
		apiErrorDefined(ctx, errDigestInvalid)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"))
 | 
						|
	if err != nil {
 | 
						|
		if err == packages_model.ErrPackageBlobUploadNotExist {
 | 
						|
			apiErrorDefined(ctx, errBlobUploadUnknown)
 | 
						|
		} else {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
	doClose := true
 | 
						|
	defer func() {
 | 
						|
		if doClose {
 | 
						|
			uploader.Close()
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	if ctx.Req.Body != nil {
 | 
						|
		if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if digest != digestFromHashSummer(uploader) {
 | 
						|
		apiErrorDefined(ctx, errDigestInvalid)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if _, err := saveAsPackageBlob(ctx,
 | 
						|
		uploader,
 | 
						|
		&packages_service.PackageCreationInfo{
 | 
						|
			PackageInfo: packages_service.PackageInfo{
 | 
						|
				Owner: ctx.Package.Owner,
 | 
						|
				Name:  image,
 | 
						|
			},
 | 
						|
			Creator: ctx.Doer,
 | 
						|
		},
 | 
						|
	); err != nil {
 | 
						|
		switch err {
 | 
						|
		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
 | 
						|
			apiError(ctx, http.StatusForbidden, err)
 | 
						|
		default:
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err := uploader.Close(); err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	doClose = false
 | 
						|
 | 
						|
	if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
 | 
						|
		ContentDigest: digest,
 | 
						|
		Status:        http.StatusCreated,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://docs.docker.com/registry/spec/api/#delete-blob-upload
 | 
						|
func CancelUploadBlob(ctx *context.Context) {
 | 
						|
	uuid := ctx.PathParam("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) {
 | 
						|
	d := ctx.PathParam("digest")
 | 
						|
 | 
						|
	if digest.Digest(d).Validate() != nil {
 | 
						|
		return nil, container_model.ErrContainerBlobNotExist
 | 
						|
	}
 | 
						|
 | 
						|
	return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
 | 
						|
		OwnerID: ctx.Package.Owner.ID,
 | 
						|
		Image:   ctx.PathParam("image"),
 | 
						|
		Digest:  d,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
 | 
						|
func HeadBlob(ctx *context.Context) {
 | 
						|
	blob, err := getBlobFromContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		if err == container_model.ErrContainerBlobNotExist {
 | 
						|
			apiErrorDefined(ctx, errBlobUnknown)
 | 
						|
		} else {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
 | 
						|
		ContentLength: blob.Blob.Size,
 | 
						|
		Status:        http.StatusOK,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-blobs
 | 
						|
func GetBlob(ctx *context.Context) {
 | 
						|
	blob, err := getBlobFromContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		if err == container_model.ErrContainerBlobNotExist {
 | 
						|
			apiErrorDefined(ctx, errBlobUnknown)
 | 
						|
		} else {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	serveBlob(ctx, blob)
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
 | 
						|
func DeleteBlob(ctx *context.Context) {
 | 
						|
	d := ctx.PathParam("digest")
 | 
						|
 | 
						|
	if digest.Digest(d).Validate() != nil {
 | 
						|
		apiErrorDefined(ctx, errBlobUnknown)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err := deleteBlob(ctx, ctx.Package.Owner.ID, ctx.PathParam("image"), d); err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		Status: http.StatusAccepted,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
 | 
						|
func UploadManifest(ctx *context.Context) {
 | 
						|
	reference := ctx.PathParam("reference")
 | 
						|
 | 
						|
	mci := &manifestCreationInfo{
 | 
						|
		MediaType: ctx.Req.Header.Get("Content-Type"),
 | 
						|
		Owner:     ctx.Package.Owner,
 | 
						|
		Creator:   ctx.Doer,
 | 
						|
		Image:     ctx.PathParam("image"),
 | 
						|
		Reference: reference,
 | 
						|
		IsTagged:  digest.Digest(reference).Validate() != nil,
 | 
						|
	}
 | 
						|
 | 
						|
	if mci.IsTagged && !referencePattern.MatchString(reference) {
 | 
						|
		apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid"))
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	maxSize := maxManifestSize + 1
 | 
						|
	buf, err := packages_module.CreateHashedBufferFromReaderWithSize(&io.LimitedReader{R: ctx.Req.Body, N: int64(maxSize)}, maxSize)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	defer buf.Close()
 | 
						|
 | 
						|
	if buf.Size() > maxManifestSize {
 | 
						|
		apiErrorDefined(ctx, errManifestInvalid.WithMessage("Manifest exceeds maximum size").WithStatusCode(http.StatusRequestEntityTooLarge))
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	digest, err := processManifest(ctx, mci, buf)
 | 
						|
	if err != nil {
 | 
						|
		var namedError *namedError
 | 
						|
		if errors.As(err, &namedError) {
 | 
						|
			apiErrorDefined(ctx, namedError)
 | 
						|
		} else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
 | 
						|
			apiErrorDefined(ctx, errBlobUnknown)
 | 
						|
		} else {
 | 
						|
			switch err {
 | 
						|
			case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
 | 
						|
				apiError(ctx, http.StatusForbidden, err)
 | 
						|
			default:
 | 
						|
				apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		Location:      fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference),
 | 
						|
		ContentDigest: digest,
 | 
						|
		Status:        http.StatusCreated,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) {
 | 
						|
	reference := ctx.PathParam("reference")
 | 
						|
 | 
						|
	opts := &container_model.BlobSearchOptions{
 | 
						|
		OwnerID:    ctx.Package.Owner.ID,
 | 
						|
		Image:      ctx.PathParam("image"),
 | 
						|
		IsManifest: true,
 | 
						|
	}
 | 
						|
 | 
						|
	if digest.Digest(reference).Validate() == nil {
 | 
						|
		opts.Digest = reference
 | 
						|
	} else if referencePattern.MatchString(reference) {
 | 
						|
		opts.Tag = reference
 | 
						|
	} else {
 | 
						|
		return nil, container_model.ErrContainerBlobNotExist
 | 
						|
	}
 | 
						|
 | 
						|
	return opts, nil
 | 
						|
}
 | 
						|
 | 
						|
func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
 | 
						|
	opts, err := getBlobSearchOptionsFromContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return workaroundGetContainerBlob(ctx, opts)
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
 | 
						|
func HeadManifest(ctx *context.Context) {
 | 
						|
	manifest, err := getManifestFromContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		if err == container_model.ErrContainerBlobNotExist {
 | 
						|
			apiErrorDefined(ctx, errManifestUnknown)
 | 
						|
		} else {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
 | 
						|
		ContentType:   manifest.Properties.GetByName(container_module.PropertyMediaType),
 | 
						|
		ContentLength: manifest.Blob.Size,
 | 
						|
		Status:        http.StatusOK,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
 | 
						|
func GetManifest(ctx *context.Context) {
 | 
						|
	manifest, err := getManifestFromContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		if err == container_model.ErrContainerBlobNotExist {
 | 
						|
			apiErrorDefined(ctx, errManifestUnknown)
 | 
						|
		} else {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	serveBlob(ctx, manifest)
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests
 | 
						|
func DeleteManifest(ctx *context.Context) {
 | 
						|
	opts, err := getBlobSearchOptionsFromContext(ctx)
 | 
						|
	if err != nil {
 | 
						|
		apiErrorDefined(ctx, errManifestUnknown)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	pvs, err := container_model.GetManifestVersions(ctx, opts)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if len(pvs) == 0 {
 | 
						|
		apiErrorDefined(ctx, errManifestUnknown)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	for _, pv := range pvs {
 | 
						|
		if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	setResponseHeaders(ctx.Resp, &containerHeaders{
 | 
						|
		Status: http.StatusAccepted,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
 | 
						|
	serveDirectReqParams := make(url.Values)
 | 
						|
	serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
 | 
						|
	s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	headers := &containerHeaders{
 | 
						|
		ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
 | 
						|
		ContentType:   pfd.Properties.GetByName(container_module.PropertyMediaType),
 | 
						|
		ContentLength: pfd.Blob.Size,
 | 
						|
		Status:        http.StatusOK,
 | 
						|
	}
 | 
						|
 | 
						|
	if u != nil {
 | 
						|
		headers.Status = http.StatusTemporaryRedirect
 | 
						|
		headers.Location = u.String()
 | 
						|
 | 
						|
		setResponseHeaders(ctx.Resp, headers)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	defer s.Close()
 | 
						|
 | 
						|
	setResponseHeaders(ctx.Resp, headers)
 | 
						|
	if _, err := io.Copy(ctx.Resp, s); err != nil {
 | 
						|
		log.Error("Error whilst copying content to response: %v", err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
 | 
						|
func GetTagList(ctx *context.Context) {
 | 
						|
	image := ctx.PathParam("image")
 | 
						|
 | 
						|
	if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
 | 
						|
		if err == packages_model.ErrPackageNotExist {
 | 
						|
			apiErrorDefined(ctx, errNameUnknown)
 | 
						|
		} else {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	n := -1
 | 
						|
	if ctx.FormTrim("n") != "" {
 | 
						|
		n = ctx.FormInt("n")
 | 
						|
	}
 | 
						|
	last := ctx.FormTrim("last")
 | 
						|
 | 
						|
	tags, err := container_model.GetImageTags(ctx, ctx.Package.Owner.ID, image, n, last)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	type TagList struct {
 | 
						|
		Name string   `json:"name"`
 | 
						|
		Tags []string `json:"tags"`
 | 
						|
	}
 | 
						|
 | 
						|
	if len(tags) > 0 {
 | 
						|
		v := url.Values{}
 | 
						|
		if n > 0 {
 | 
						|
			v.Add("n", strconv.Itoa(n))
 | 
						|
		}
 | 
						|
		v.Add("last", tags[len(tags)-1])
 | 
						|
 | 
						|
		ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/%s/%s/tags/list?%s>; rel="next"`, ctx.Package.Owner.LowerName, image, v.Encode()))
 | 
						|
	}
 | 
						|
 | 
						|
	jsonResponse(ctx, http.StatusOK, TagList{
 | 
						|
		Name: strings.ToLower(ctx.Package.Owner.LowerName + "/" + image),
 | 
						|
		Tags: tags,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// FIXME: Workaround to be removed in v1.20
 | 
						|
// https://github.com/go-gitea/gitea/issues/19586
 | 
						|
func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) {
 | 
						|
	blob, err := container_model.GetContainerBlob(ctx, opts)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(blob.Blob.HashSHA256))
 | 
						|
	if err != nil {
 | 
						|
		if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
 | 
						|
			log.Debug("Package registry inconsistent: blob %s does not exist on file system", blob.Blob.HashSHA256)
 | 
						|
			return nil, container_model.ErrContainerBlobNotExist
 | 
						|
		}
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return blob, nil
 | 
						|
}
 |