code.gitea.io/gitea@v1.21.7/routers/api/packages/container/container.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package container
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  
    17  	packages_model "code.gitea.io/gitea/models/packages"
    18  	container_model "code.gitea.io/gitea/models/packages/container"
    19  	user_model "code.gitea.io/gitea/models/user"
    20  	"code.gitea.io/gitea/modules/context"
    21  	"code.gitea.io/gitea/modules/json"
    22  	"code.gitea.io/gitea/modules/log"
    23  	packages_module "code.gitea.io/gitea/modules/packages"
    24  	container_module "code.gitea.io/gitea/modules/packages/container"
    25  	"code.gitea.io/gitea/modules/setting"
    26  	"code.gitea.io/gitea/modules/util"
    27  	"code.gitea.io/gitea/routers/api/packages/helper"
    28  	packages_service "code.gitea.io/gitea/services/packages"
    29  	container_service "code.gitea.io/gitea/services/packages/container"
    30  
    31  	digest "github.com/opencontainers/go-digest"
    32  )
    33  
    34  // maximum size of a container manifest
    35  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
    36  const maxManifestSize = 10 * 1024 * 1024
    37  
    38  var (
    39  	imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
    40  	referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
    41  )
    42  
    43  type containerHeaders struct {
    44  	Status        int
    45  	ContentDigest string
    46  	UploadUUID    string
    47  	Range         string
    48  	Location      string
    49  	ContentType   string
    50  	ContentLength int64
    51  }
    52  
    53  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
    54  func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
    55  	if h.Location != "" {
    56  		resp.Header().Set("Location", h.Location)
    57  	}
    58  	if h.Range != "" {
    59  		resp.Header().Set("Range", h.Range)
    60  	}
    61  	if h.ContentType != "" {
    62  		resp.Header().Set("Content-Type", h.ContentType)
    63  	}
    64  	if h.ContentLength != 0 {
    65  		resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
    66  	}
    67  	if h.UploadUUID != "" {
    68  		resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
    69  	}
    70  	if h.ContentDigest != "" {
    71  		resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
    72  		resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
    73  	}
    74  	resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
    75  	resp.WriteHeader(h.Status)
    76  }
    77  
    78  func jsonResponse(ctx *context.Context, status int, obj any) {
    79  	setResponseHeaders(ctx.Resp, &containerHeaders{
    80  		Status:      status,
    81  		ContentType: "application/json",
    82  	})
    83  	if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
    84  		log.Error("JSON encode: %v", err)
    85  	}
    86  }
    87  
    88  func apiError(ctx *context.Context, status int, err error) {
    89  	helper.LogAndProcessError(ctx, status, err, func(message string) {
    90  		setResponseHeaders(ctx.Resp, &containerHeaders{
    91  			Status: status,
    92  		})
    93  	})
    94  }
    95  
    96  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
    97  func apiErrorDefined(ctx *context.Context, err *namedError) {
    98  	type ContainerError struct {
    99  		Code    string `json:"code"`
   100  		Message string `json:"message"`
   101  	}
   102  
   103  	type ContainerErrors struct {
   104  		Errors []ContainerError `json:"errors"`
   105  	}
   106  
   107  	jsonResponse(ctx, err.StatusCode, ContainerErrors{
   108  		Errors: []ContainerError{
   109  			{
   110  				Code:    err.Code,
   111  				Message: err.Message,
   112  			},
   113  		},
   114  	})
   115  }
   116  
   117  func apiUnauthorizedError(ctx *context.Context) {
   118  	ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
   119  	apiErrorDefined(ctx, errUnauthorized)
   120  }
   121  
   122  // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
   123  func ReqContainerAccess(ctx *context.Context) {
   124  	if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
   125  		apiUnauthorizedError(ctx)
   126  	}
   127  }
   128  
   129  // VerifyImageName is a middleware which checks if the image name is allowed
   130  func VerifyImageName(ctx *context.Context) {
   131  	if !imageNamePattern.MatchString(ctx.Params("image")) {
   132  		apiErrorDefined(ctx, errNameInvalid)
   133  	}
   134  }
   135  
   136  // DetermineSupport is used to test if the registry supports OCI
   137  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
   138  func DetermineSupport(ctx *context.Context) {
   139  	setResponseHeaders(ctx.Resp, &containerHeaders{
   140  		Status: http.StatusOK,
   141  	})
   142  }
   143  
   144  // Authenticate creates a token for the current user
   145  // If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
   146  func Authenticate(ctx *context.Context) {
   147  	u := ctx.Doer
   148  	if u == nil {
   149  		if setting.Service.RequireSignInView {
   150  			apiUnauthorizedError(ctx)
   151  			return
   152  		}
   153  
   154  		u = user_model.NewGhostUser()
   155  	}
   156  
   157  	token, err := packages_service.CreateAuthorizationToken(u)
   158  	if err != nil {
   159  		apiError(ctx, http.StatusInternalServerError, err)
   160  		return
   161  	}
   162  
   163  	ctx.JSON(http.StatusOK, map[string]string{
   164  		"token": token,
   165  	})
   166  }
   167  
   168  // https://distribution.github.io/distribution/spec/auth/oauth/
   169  func AuthenticateNotImplemented(ctx *context.Context) {
   170  	// This optional endpoint can be used to authenticate a client.
   171  	// It must implement the specification described in:
   172  	// https://datatracker.ietf.org/doc/html/rfc6749
   173  	// https://distribution.github.io/distribution/spec/auth/oauth/
   174  	// Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed.
   175  
   176  	ctx.Status(http.StatusNotFound)
   177  }
   178  
   179  // https://docs.docker.com/registry/spec/api/#listing-repositories
   180  func GetRepositoryList(ctx *context.Context) {
   181  	n := ctx.FormInt("n")
   182  	if n <= 0 || n > 100 {
   183  		n = 100
   184  	}
   185  	last := ctx.FormTrim("last")
   186  
   187  	repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last)
   188  	if err != nil {
   189  		apiError(ctx, http.StatusInternalServerError, err)
   190  		return
   191  	}
   192  
   193  	type RepositoryList struct {
   194  		Repositories []string `json:"repositories"`
   195  	}
   196  
   197  	if len(repositories) == n {
   198  		v := url.Values{}
   199  		if n > 0 {
   200  			v.Add("n", strconv.Itoa(n))
   201  		}
   202  		v.Add("last", repositories[len(repositories)-1])
   203  
   204  		ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode()))
   205  	}
   206  
   207  	jsonResponse(ctx, http.StatusOK, RepositoryList{
   208  		Repositories: repositories,
   209  	})
   210  }
   211  
   212  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
   213  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
   214  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
   215  func InitiateUploadBlob(ctx *context.Context) {
   216  	image := ctx.Params("image")
   217  
   218  	mount := ctx.FormTrim("mount")
   219  	from := ctx.FormTrim("from")
   220  	if mount != "" {
   221  		blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
   222  			Repository: from,
   223  			Digest:     mount,
   224  		})
   225  		if blob != nil {
   226  			accessible, err := packages_model.IsBlobAccessibleForUser(ctx, blob.Blob.ID, ctx.Doer)
   227  			if err != nil {
   228  				apiError(ctx, http.StatusInternalServerError, err)
   229  				return
   230  			}
   231  
   232  			if accessible {
   233  				if err := mountBlob(ctx, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil {
   234  					apiError(ctx, http.StatusInternalServerError, err)
   235  					return
   236  				}
   237  
   238  				setResponseHeaders(ctx.Resp, &containerHeaders{
   239  					Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
   240  					ContentDigest: mount,
   241  					Status:        http.StatusCreated,
   242  				})
   243  				return
   244  			}
   245  		}
   246  	}
   247  
   248  	digest := ctx.FormTrim("digest")
   249  	if digest != "" {
   250  		buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
   251  		if err != nil {
   252  			apiError(ctx, http.StatusInternalServerError, err)
   253  			return
   254  		}
   255  		defer buf.Close()
   256  
   257  		if digest != digestFromHashSummer(buf) {
   258  			apiErrorDefined(ctx, errDigestInvalid)
   259  			return
   260  		}
   261  
   262  		if _, err := saveAsPackageBlob(ctx,
   263  			buf,
   264  			&packages_service.PackageCreationInfo{
   265  				PackageInfo: packages_service.PackageInfo{
   266  					Owner: ctx.Package.Owner,
   267  					Name:  image,
   268  				},
   269  				Creator: ctx.Doer,
   270  			},
   271  		); err != nil {
   272  			switch err {
   273  			case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   274  				apiError(ctx, http.StatusForbidden, err)
   275  			default:
   276  				apiError(ctx, http.StatusInternalServerError, err)
   277  			}
   278  			return
   279  		}
   280  
   281  		setResponseHeaders(ctx.Resp, &containerHeaders{
   282  			Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
   283  			ContentDigest: digest,
   284  			Status:        http.StatusCreated,
   285  		})
   286  		return
   287  	}
   288  
   289  	upload, err := packages_model.CreateBlobUpload(ctx)
   290  	if err != nil {
   291  		apiError(ctx, http.StatusInternalServerError, err)
   292  		return
   293  	}
   294  
   295  	setResponseHeaders(ctx.Resp, &containerHeaders{
   296  		Location:   fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
   297  		Range:      "0-0",
   298  		UploadUUID: upload.ID,
   299  		Status:     http.StatusAccepted,
   300  	})
   301  }
   302  
   303  // https://docs.docker.com/registry/spec/api/#get-blob-upload
   304  func GetUploadBlob(ctx *context.Context) {
   305  	uuid := ctx.Params("uuid")
   306  
   307  	upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
   308  	if err != nil {
   309  		if err == packages_model.ErrPackageBlobUploadNotExist {
   310  			apiErrorDefined(ctx, errBlobUploadUnknown)
   311  		} else {
   312  			apiError(ctx, http.StatusInternalServerError, err)
   313  		}
   314  		return
   315  	}
   316  
   317  	setResponseHeaders(ctx.Resp, &containerHeaders{
   318  		Range:      fmt.Sprintf("0-%d", upload.BytesReceived),
   319  		UploadUUID: upload.ID,
   320  		Status:     http.StatusNoContent,
   321  	})
   322  }
   323  
   324  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
   325  func UploadBlob(ctx *context.Context) {
   326  	image := ctx.Params("image")
   327  
   328  	uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
   329  	if err != nil {
   330  		if err == packages_model.ErrPackageBlobUploadNotExist {
   331  			apiErrorDefined(ctx, errBlobUploadUnknown)
   332  		} else {
   333  			apiError(ctx, http.StatusInternalServerError, err)
   334  		}
   335  		return
   336  	}
   337  	defer uploader.Close()
   338  
   339  	contentRange := ctx.Req.Header.Get("Content-Range")
   340  	if contentRange != "" {
   341  		start, end := 0, 0
   342  		if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
   343  			apiErrorDefined(ctx, errBlobUploadInvalid)
   344  			return
   345  		}
   346  
   347  		if int64(start) != uploader.Size() {
   348  			apiErrorDefined(ctx, errBlobUploadInvalid.WithStatusCode(http.StatusRequestedRangeNotSatisfiable))
   349  			return
   350  		}
   351  	} else if uploader.Size() != 0 {
   352  		apiErrorDefined(ctx, errBlobUploadInvalid.WithMessage("Stream uploads after first write are not allowed"))
   353  		return
   354  	}
   355  
   356  	if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
   357  		apiError(ctx, http.StatusInternalServerError, err)
   358  		return
   359  	}
   360  
   361  	setResponseHeaders(ctx.Resp, &containerHeaders{
   362  		Location:   fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID),
   363  		Range:      fmt.Sprintf("0-%d", uploader.Size()-1),
   364  		UploadUUID: uploader.ID,
   365  		Status:     http.StatusAccepted,
   366  	})
   367  }
   368  
   369  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
   370  func EndUploadBlob(ctx *context.Context) {
   371  	image := ctx.Params("image")
   372  
   373  	digest := ctx.FormTrim("digest")
   374  	if digest == "" {
   375  		apiErrorDefined(ctx, errDigestInvalid)
   376  		return
   377  	}
   378  
   379  	uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
   380  	if err != nil {
   381  		if err == packages_model.ErrPackageBlobUploadNotExist {
   382  			apiErrorDefined(ctx, errBlobUploadUnknown)
   383  		} else {
   384  			apiError(ctx, http.StatusInternalServerError, err)
   385  		}
   386  		return
   387  	}
   388  	close := true
   389  	defer func() {
   390  		if close {
   391  			uploader.Close()
   392  		}
   393  	}()
   394  
   395  	if ctx.Req.Body != nil {
   396  		if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
   397  			apiError(ctx, http.StatusInternalServerError, err)
   398  			return
   399  		}
   400  	}
   401  
   402  	if digest != digestFromHashSummer(uploader) {
   403  		apiErrorDefined(ctx, errDigestInvalid)
   404  		return
   405  	}
   406  
   407  	if _, err := saveAsPackageBlob(ctx,
   408  		uploader,
   409  		&packages_service.PackageCreationInfo{
   410  			PackageInfo: packages_service.PackageInfo{
   411  				Owner: ctx.Package.Owner,
   412  				Name:  image,
   413  			},
   414  			Creator: ctx.Doer,
   415  		},
   416  	); err != nil {
   417  		switch err {
   418  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   419  			apiError(ctx, http.StatusForbidden, err)
   420  		default:
   421  			apiError(ctx, http.StatusInternalServerError, err)
   422  		}
   423  		return
   424  	}
   425  
   426  	if err := uploader.Close(); err != nil {
   427  		apiError(ctx, http.StatusInternalServerError, err)
   428  		return
   429  	}
   430  	close = false
   431  
   432  	if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil {
   433  		apiError(ctx, http.StatusInternalServerError, err)
   434  		return
   435  	}
   436  
   437  	setResponseHeaders(ctx.Resp, &containerHeaders{
   438  		Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
   439  		ContentDigest: digest,
   440  		Status:        http.StatusCreated,
   441  	})
   442  }
   443  
   444  // https://docs.docker.com/registry/spec/api/#delete-blob-upload
   445  func CancelUploadBlob(ctx *context.Context) {
   446  	uuid := ctx.Params("uuid")
   447  
   448  	_, err := packages_model.GetBlobUploadByID(ctx, uuid)
   449  	if err != nil {
   450  		if err == packages_model.ErrPackageBlobUploadNotExist {
   451  			apiErrorDefined(ctx, errBlobUploadUnknown)
   452  		} else {
   453  			apiError(ctx, http.StatusInternalServerError, err)
   454  		}
   455  		return
   456  	}
   457  
   458  	if err := container_service.RemoveBlobUploadByID(ctx, uuid); err != nil {
   459  		apiError(ctx, http.StatusInternalServerError, err)
   460  		return
   461  	}
   462  
   463  	setResponseHeaders(ctx.Resp, &containerHeaders{
   464  		Status: http.StatusNoContent,
   465  	})
   466  }
   467  
   468  func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
   469  	d := ctx.Params("digest")
   470  
   471  	if digest.Digest(d).Validate() != nil {
   472  		return nil, container_model.ErrContainerBlobNotExist
   473  	}
   474  
   475  	return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
   476  		OwnerID: ctx.Package.Owner.ID,
   477  		Image:   ctx.Params("image"),
   478  		Digest:  d,
   479  	})
   480  }
   481  
   482  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
   483  func HeadBlob(ctx *context.Context) {
   484  	blob, err := getBlobFromContext(ctx)
   485  	if err != nil {
   486  		if err == container_model.ErrContainerBlobNotExist {
   487  			apiErrorDefined(ctx, errBlobUnknown)
   488  		} else {
   489  			apiError(ctx, http.StatusInternalServerError, err)
   490  		}
   491  		return
   492  	}
   493  
   494  	setResponseHeaders(ctx.Resp, &containerHeaders{
   495  		ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
   496  		ContentLength: blob.Blob.Size,
   497  		Status:        http.StatusOK,
   498  	})
   499  }
   500  
   501  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-blobs
   502  func GetBlob(ctx *context.Context) {
   503  	blob, err := getBlobFromContext(ctx)
   504  	if err != nil {
   505  		if err == container_model.ErrContainerBlobNotExist {
   506  			apiErrorDefined(ctx, errBlobUnknown)
   507  		} else {
   508  			apiError(ctx, http.StatusInternalServerError, err)
   509  		}
   510  		return
   511  	}
   512  
   513  	serveBlob(ctx, blob)
   514  }
   515  
   516  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
   517  func DeleteBlob(ctx *context.Context) {
   518  	d := ctx.Params("digest")
   519  
   520  	if digest.Digest(d).Validate() != nil {
   521  		apiErrorDefined(ctx, errBlobUnknown)
   522  		return
   523  	}
   524  
   525  	if err := deleteBlob(ctx, ctx.Package.Owner.ID, ctx.Params("image"), d); err != nil {
   526  		apiError(ctx, http.StatusInternalServerError, err)
   527  		return
   528  	}
   529  
   530  	setResponseHeaders(ctx.Resp, &containerHeaders{
   531  		Status: http.StatusAccepted,
   532  	})
   533  }
   534  
   535  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
   536  func UploadManifest(ctx *context.Context) {
   537  	reference := ctx.Params("reference")
   538  
   539  	mci := &manifestCreationInfo{
   540  		MediaType: ctx.Req.Header.Get("Content-Type"),
   541  		Owner:     ctx.Package.Owner,
   542  		Creator:   ctx.Doer,
   543  		Image:     ctx.Params("image"),
   544  		Reference: reference,
   545  		IsTagged:  digest.Digest(reference).Validate() != nil,
   546  	}
   547  
   548  	if mci.IsTagged && !referencePattern.MatchString(reference) {
   549  		apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid"))
   550  		return
   551  	}
   552  
   553  	maxSize := maxManifestSize + 1
   554  	buf, err := packages_module.CreateHashedBufferFromReaderWithSize(&io.LimitedReader{R: ctx.Req.Body, N: int64(maxSize)}, maxSize)
   555  	if err != nil {
   556  		apiError(ctx, http.StatusInternalServerError, err)
   557  		return
   558  	}
   559  	defer buf.Close()
   560  
   561  	if buf.Size() > maxManifestSize {
   562  		apiErrorDefined(ctx, errManifestInvalid.WithMessage("Manifest exceeds maximum size").WithStatusCode(http.StatusRequestEntityTooLarge))
   563  		return
   564  	}
   565  
   566  	digest, err := processManifest(ctx, mci, buf)
   567  	if err != nil {
   568  		var namedError *namedError
   569  		if errors.As(err, &namedError) {
   570  			apiErrorDefined(ctx, namedError)
   571  		} else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
   572  			apiErrorDefined(ctx, errBlobUnknown)
   573  		} else {
   574  			switch err {
   575  			case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   576  				apiError(ctx, http.StatusForbidden, err)
   577  			default:
   578  				apiError(ctx, http.StatusInternalServerError, err)
   579  			}
   580  		}
   581  		return
   582  	}
   583  
   584  	setResponseHeaders(ctx.Resp, &containerHeaders{
   585  		Location:      fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference),
   586  		ContentDigest: digest,
   587  		Status:        http.StatusCreated,
   588  	})
   589  }
   590  
   591  func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) {
   592  	reference := ctx.Params("reference")
   593  
   594  	opts := &container_model.BlobSearchOptions{
   595  		OwnerID:    ctx.Package.Owner.ID,
   596  		Image:      ctx.Params("image"),
   597  		IsManifest: true,
   598  	}
   599  
   600  	if digest.Digest(reference).Validate() == nil {
   601  		opts.Digest = reference
   602  	} else if referencePattern.MatchString(reference) {
   603  		opts.Tag = reference
   604  	} else {
   605  		return nil, container_model.ErrContainerBlobNotExist
   606  	}
   607  
   608  	return opts, nil
   609  }
   610  
   611  func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
   612  	opts, err := getBlobSearchOptionsFromContext(ctx)
   613  	if err != nil {
   614  		return nil, err
   615  	}
   616  
   617  	return workaroundGetContainerBlob(ctx, opts)
   618  }
   619  
   620  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
   621  func HeadManifest(ctx *context.Context) {
   622  	manifest, err := getManifestFromContext(ctx)
   623  	if err != nil {
   624  		if err == container_model.ErrContainerBlobNotExist {
   625  			apiErrorDefined(ctx, errManifestUnknown)
   626  		} else {
   627  			apiError(ctx, http.StatusInternalServerError, err)
   628  		}
   629  		return
   630  	}
   631  
   632  	setResponseHeaders(ctx.Resp, &containerHeaders{
   633  		ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
   634  		ContentType:   manifest.Properties.GetByName(container_module.PropertyMediaType),
   635  		ContentLength: manifest.Blob.Size,
   636  		Status:        http.StatusOK,
   637  	})
   638  }
   639  
   640  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
   641  func GetManifest(ctx *context.Context) {
   642  	manifest, err := getManifestFromContext(ctx)
   643  	if err != nil {
   644  		if err == container_model.ErrContainerBlobNotExist {
   645  			apiErrorDefined(ctx, errManifestUnknown)
   646  		} else {
   647  			apiError(ctx, http.StatusInternalServerError, err)
   648  		}
   649  		return
   650  	}
   651  
   652  	serveBlob(ctx, manifest)
   653  }
   654  
   655  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags
   656  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests
   657  func DeleteManifest(ctx *context.Context) {
   658  	opts, err := getBlobSearchOptionsFromContext(ctx)
   659  	if err != nil {
   660  		apiErrorDefined(ctx, errManifestUnknown)
   661  		return
   662  	}
   663  
   664  	pvs, err := container_model.GetManifestVersions(ctx, opts)
   665  	if err != nil {
   666  		apiError(ctx, http.StatusInternalServerError, err)
   667  		return
   668  	}
   669  
   670  	if len(pvs) == 0 {
   671  		apiErrorDefined(ctx, errManifestUnknown)
   672  		return
   673  	}
   674  
   675  	for _, pv := range pvs {
   676  		if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
   677  			apiError(ctx, http.StatusInternalServerError, err)
   678  			return
   679  		}
   680  	}
   681  
   682  	setResponseHeaders(ctx.Resp, &containerHeaders{
   683  		Status: http.StatusAccepted,
   684  	})
   685  }
   686  
   687  func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
   688  	s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob)
   689  	if err != nil {
   690  		apiError(ctx, http.StatusInternalServerError, err)
   691  		return
   692  	}
   693  
   694  	headers := &containerHeaders{
   695  		ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
   696  		ContentType:   pfd.Properties.GetByName(container_module.PropertyMediaType),
   697  		ContentLength: pfd.Blob.Size,
   698  		Status:        http.StatusOK,
   699  	}
   700  
   701  	if u != nil {
   702  		headers.Status = http.StatusTemporaryRedirect
   703  		headers.Location = u.String()
   704  
   705  		setResponseHeaders(ctx.Resp, headers)
   706  		return
   707  	}
   708  
   709  	defer s.Close()
   710  
   711  	setResponseHeaders(ctx.Resp, headers)
   712  	if _, err := io.Copy(ctx.Resp, s); err != nil {
   713  		log.Error("Error whilst copying content to response: %v", err)
   714  	}
   715  }
   716  
   717  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
   718  func GetTagList(ctx *context.Context) {
   719  	image := ctx.Params("image")
   720  
   721  	if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
   722  		if err == packages_model.ErrPackageNotExist {
   723  			apiErrorDefined(ctx, errNameUnknown)
   724  		} else {
   725  			apiError(ctx, http.StatusInternalServerError, err)
   726  		}
   727  		return
   728  	}
   729  
   730  	n := -1
   731  	if ctx.FormTrim("n") != "" {
   732  		n = ctx.FormInt("n")
   733  	}
   734  	last := ctx.FormTrim("last")
   735  
   736  	tags, err := container_model.GetImageTags(ctx, ctx.Package.Owner.ID, image, n, last)
   737  	if err != nil {
   738  		apiError(ctx, http.StatusInternalServerError, err)
   739  		return
   740  	}
   741  
   742  	type TagList struct {
   743  		Name string   `json:"name"`
   744  		Tags []string `json:"tags"`
   745  	}
   746  
   747  	if len(tags) > 0 {
   748  		v := url.Values{}
   749  		if n > 0 {
   750  			v.Add("n", strconv.Itoa(n))
   751  		}
   752  		v.Add("last", tags[len(tags)-1])
   753  
   754  		ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/%s/%s/tags/list?%s>; rel="next"`, ctx.Package.Owner.LowerName, image, v.Encode()))
   755  	}
   756  
   757  	jsonResponse(ctx, http.StatusOK, TagList{
   758  		Name: strings.ToLower(ctx.Package.Owner.LowerName + "/" + image),
   759  		Tags: tags,
   760  	})
   761  }
   762  
   763  // FIXME: Workaround to be removed in v1.20
   764  // https://github.com/go-gitea/gitea/issues/19586
   765  func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) {
   766  	blob, err := container_model.GetContainerBlob(ctx, opts)
   767  	if err != nil {
   768  		return nil, err
   769  	}
   770  
   771  	err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(blob.Blob.HashSHA256))
   772  	if err != nil {
   773  		if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
   774  			log.Debug("Package registry inconsistent: blob %s does not exist on file system", blob.Blob.HashSHA256)
   775  			return nil, container_model.ErrContainerBlobNotExist
   776  		}
   777  		return nil, err
   778  	}
   779  
   780  	return blob, nil
   781  }