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 }