mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-01 00:21:17 +00:00
5a871f6095
- The Conan and Container packages use a different type of authentication. It first authenticates via the regular way (api tokens or user:password, handled via `auth.Basic`) and then generates a JWT token that is used by the package software (such as Docker) to do the action they wanted to do. This JWT token didn't properly propagate the API scopes that the token was generated for, and thus could lead to a 'scope escalation' within the Conan and Container packages, read access to write access. - Store the API scope in the JWT token, so it can be propagated on subsequent calls that uses that JWT token. - Integration test added. - Resolves #5128
808 lines
23 KiB
Go
808 lines
23 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package conan
|
|
|
|
import (
|
|
std_ctx "context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/db"
|
|
packages_model "code.gitea.io/gitea/models/packages"
|
|
conan_model "code.gitea.io/gitea/models/packages/conan"
|
|
"code.gitea.io/gitea/modules/container"
|
|
"code.gitea.io/gitea/modules/json"
|
|
"code.gitea.io/gitea/modules/log"
|
|
packages_module "code.gitea.io/gitea/modules/packages"
|
|
conan_module "code.gitea.io/gitea/modules/packages/conan"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/routers/api/packages/helper"
|
|
"code.gitea.io/gitea/services/context"
|
|
notify_service "code.gitea.io/gitea/services/notify"
|
|
packages_service "code.gitea.io/gitea/services/packages"
|
|
)
|
|
|
|
const (
|
|
conanfileFile = "conanfile.py"
|
|
conaninfoFile = "conaninfo.txt"
|
|
|
|
recipeReferenceKey = "RecipeReference"
|
|
packageReferenceKey = "PackageReference"
|
|
)
|
|
|
|
var (
|
|
recipeFileList = container.SetOf(
|
|
conanfileFile,
|
|
"conanmanifest.txt",
|
|
"conan_sources.tgz",
|
|
"conan_export.tgz",
|
|
)
|
|
packageFileList = container.SetOf(
|
|
conaninfoFile,
|
|
"conanmanifest.txt",
|
|
"conan_package.tgz",
|
|
)
|
|
)
|
|
|
|
func jsonResponse(ctx *context.Context, status int, obj any) {
|
|
// https://github.com/conan-io/conan/issues/6613
|
|
ctx.Resp.Header().Set("Content-Type", "application/json")
|
|
ctx.Status(status)
|
|
if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
|
|
log.Error("JSON encode: %v", err)
|
|
}
|
|
}
|
|
|
|
func apiError(ctx *context.Context, status int, obj any) {
|
|
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
|
jsonResponse(ctx, status, map[string]string{
|
|
"message": message,
|
|
})
|
|
})
|
|
}
|
|
|
|
func baseURL(ctx *context.Context) string {
|
|
return setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/conan"
|
|
}
|
|
|
|
// ExtractPathParameters is a middleware to extract common parameters from path
|
|
func ExtractPathParameters(ctx *context.Context) {
|
|
rref, err := conan_module.NewRecipeReference(
|
|
ctx.Params("name"),
|
|
ctx.Params("version"),
|
|
ctx.Params("user"),
|
|
ctx.Params("channel"),
|
|
ctx.Params("recipe_revision"),
|
|
)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
ctx.Data[recipeReferenceKey] = rref
|
|
|
|
reference := ctx.Params("package_reference")
|
|
|
|
var pref *conan_module.PackageReference
|
|
if reference != "" {
|
|
pref, err = conan_module.NewPackageReference(
|
|
rref,
|
|
reference,
|
|
ctx.Params("package_revision"),
|
|
)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx.Data[packageReferenceKey] = pref
|
|
}
|
|
|
|
// Ping reports the server capabilities
|
|
func Ping(ctx *context.Context) {
|
|
ctx.RespHeader().Add("X-Conan-Server-Capabilities", "revisions") // complex_search,checksum_deploy,matrix_params
|
|
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
|
|
// Authenticate creates an authentication token for the user
|
|
func Authenticate(ctx *context.Context) {
|
|
if ctx.Doer == nil {
|
|
apiError(ctx, http.StatusBadRequest, nil)
|
|
return
|
|
}
|
|
|
|
// If there's an API scope, ensure it propagates.
|
|
scope, _ := ctx.Data.GetData()["ApiTokenScope"].(auth_model.AccessTokenScope)
|
|
|
|
token, err := packages_service.CreateAuthorizationToken(ctx.Doer, scope)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
ctx.PlainText(http.StatusOK, token)
|
|
}
|
|
|
|
// CheckCredentials tests if the provided authentication token is valid
|
|
func CheckCredentials(ctx *context.Context) {
|
|
if ctx.Doer == nil {
|
|
ctx.Status(http.StatusUnauthorized)
|
|
} else {
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// RecipeSnapshot displays the recipe files with their md5 hash
|
|
func RecipeSnapshot(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
serveSnapshot(ctx, rref.AsKey())
|
|
}
|
|
|
|
// RecipeSnapshot displays the package files with their md5 hash
|
|
func PackageSnapshot(ctx *context.Context) {
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
serveSnapshot(ctx, pref.AsKey())
|
|
}
|
|
|
|
func serveSnapshot(ctx *context.Context, fileKey string) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
|
|
if err != nil {
|
|
if err == packages_model.ErrPackageNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
|
VersionID: pv.ID,
|
|
CompositeKey: fileKey,
|
|
})
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if len(pfs) == 0 {
|
|
apiError(ctx, http.StatusNotFound, nil)
|
|
return
|
|
}
|
|
|
|
files := make(map[string]string)
|
|
for _, pf := range pfs {
|
|
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
files[pf.Name] = pb.HashMD5
|
|
}
|
|
|
|
jsonResponse(ctx, http.StatusOK, files)
|
|
}
|
|
|
|
// RecipeDownloadURLs displays the recipe files with their download url
|
|
func RecipeDownloadURLs(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
serveDownloadURLs(
|
|
ctx,
|
|
rref.AsKey(),
|
|
fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()),
|
|
)
|
|
}
|
|
|
|
// PackageDownloadURLs displays the package files with their download url
|
|
func PackageDownloadURLs(ctx *context.Context) {
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
serveDownloadURLs(
|
|
ctx,
|
|
pref.AsKey(),
|
|
fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()),
|
|
)
|
|
}
|
|
|
|
func serveDownloadURLs(ctx *context.Context, fileKey, downloadURL string) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
|
|
if err != nil {
|
|
if err == packages_model.ErrPackageNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
|
VersionID: pv.ID,
|
|
CompositeKey: fileKey,
|
|
})
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
if len(pfs) == 0 {
|
|
apiError(ctx, http.StatusNotFound, nil)
|
|
return
|
|
}
|
|
|
|
urls := make(map[string]string)
|
|
for _, pf := range pfs {
|
|
urls[pf.Name] = fmt.Sprintf("%s/%s", downloadURL, pf.Name)
|
|
}
|
|
|
|
jsonResponse(ctx, http.StatusOK, urls)
|
|
}
|
|
|
|
// RecipeUploadURLs displays the upload urls for the provided recipe files
|
|
func RecipeUploadURLs(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
serveUploadURLs(
|
|
ctx,
|
|
recipeFileList,
|
|
fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()),
|
|
)
|
|
}
|
|
|
|
// PackageUploadURLs displays the upload urls for the provided package files
|
|
func PackageUploadURLs(ctx *context.Context) {
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
serveUploadURLs(
|
|
ctx,
|
|
packageFileList,
|
|
fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()),
|
|
)
|
|
}
|
|
|
|
func serveUploadURLs(ctx *context.Context, fileFilter container.Set[string], uploadURL string) {
|
|
defer ctx.Req.Body.Close()
|
|
|
|
var files map[string]int64
|
|
if err := json.NewDecoder(ctx.Req.Body).Decode(&files); err != nil {
|
|
apiError(ctx, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
urls := make(map[string]string)
|
|
for file := range files {
|
|
if fileFilter.Contains(file) {
|
|
urls[file] = fmt.Sprintf("%s/%s", uploadURL, file)
|
|
}
|
|
}
|
|
|
|
jsonResponse(ctx, http.StatusOK, urls)
|
|
}
|
|
|
|
// UploadRecipeFile handles the upload of a recipe file
|
|
func UploadRecipeFile(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
uploadFile(ctx, recipeFileList, rref.AsKey())
|
|
}
|
|
|
|
// UploadPackageFile handles the upload of a package file
|
|
func UploadPackageFile(ctx *context.Context) {
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
uploadFile(ctx, packageFileList, pref.AsKey())
|
|
}
|
|
|
|
func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
filename := ctx.Params("filename")
|
|
if !fileFilter.Contains(filename) {
|
|
apiError(ctx, http.StatusBadRequest, nil)
|
|
return
|
|
}
|
|
|
|
upload, needToClose, err := ctx.UploadStream()
|
|
if err != nil {
|
|
apiError(ctx, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
if needToClose {
|
|
defer upload.Close()
|
|
}
|
|
|
|
buf, err := packages_module.CreateHashedBufferFromReader(upload)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
defer buf.Close()
|
|
|
|
isConanfileFile := filename == conanfileFile
|
|
isConaninfoFile := filename == conaninfoFile
|
|
|
|
pci := &packages_service.PackageCreationInfo{
|
|
PackageInfo: packages_service.PackageInfo{
|
|
Owner: ctx.Package.Owner,
|
|
PackageType: packages_model.TypeConan,
|
|
Name: rref.Name,
|
|
Version: rref.Version,
|
|
},
|
|
Creator: ctx.Doer,
|
|
}
|
|
pfci := &packages_service.PackageFileCreationInfo{
|
|
PackageFileInfo: packages_service.PackageFileInfo{
|
|
Filename: strings.ToLower(filename),
|
|
CompositeKey: fileKey,
|
|
},
|
|
Creator: ctx.Doer,
|
|
Data: buf,
|
|
IsLead: isConanfileFile,
|
|
Properties: map[string]string{
|
|
conan_module.PropertyRecipeUser: rref.User,
|
|
conan_module.PropertyRecipeChannel: rref.Channel,
|
|
conan_module.PropertyRecipeRevision: rref.RevisionOrDefault(),
|
|
},
|
|
OverwriteExisting: true,
|
|
}
|
|
|
|
if pref != nil {
|
|
pfci.Properties[conan_module.PropertyPackageReference] = pref.Reference
|
|
pfci.Properties[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault()
|
|
}
|
|
|
|
if isConanfileFile || isConaninfoFile {
|
|
if isConanfileFile {
|
|
metadata, err := conan_module.ParseConanfile(buf)
|
|
if err != nil {
|
|
log.Error("Error parsing package metadata: %v", err)
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pci.Owner.ID, pci.PackageType, pci.Name, pci.Version)
|
|
if err != nil && err != packages_model.ErrPackageNotExist {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if pv != nil {
|
|
raw, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
pv.MetadataJSON = string(raw)
|
|
if err := packages_model.UpdateVersion(ctx, pv); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
} else {
|
|
pci.Metadata = metadata
|
|
}
|
|
} else {
|
|
info, err := conan_module.ParseConaninfo(buf)
|
|
if err != nil {
|
|
log.Error("Error parsing conan info: %v", err)
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
raw, err := json.Marshal(info)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
pfci.Properties[conan_module.PropertyPackageInfo] = string(raw)
|
|
}
|
|
|
|
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
|
ctx,
|
|
pci,
|
|
pfci,
|
|
)
|
|
if err != nil {
|
|
switch err {
|
|
case packages_model.ErrDuplicatePackageFile:
|
|
apiError(ctx, http.StatusConflict, err)
|
|
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
|
apiError(ctx, http.StatusForbidden, err)
|
|
default:
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusCreated)
|
|
}
|
|
|
|
// DownloadRecipeFile serves the content of the requested recipe file
|
|
func DownloadRecipeFile(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
downloadFile(ctx, recipeFileList, rref.AsKey())
|
|
}
|
|
|
|
// DownloadPackageFile serves the content of the requested package file
|
|
func DownloadPackageFile(ctx *context.Context) {
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
downloadFile(ctx, packageFileList, pref.AsKey())
|
|
}
|
|
|
|
func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
filename := ctx.Params("filename")
|
|
if !fileFilter.Contains(filename) {
|
|
apiError(ctx, http.StatusBadRequest, nil)
|
|
return
|
|
}
|
|
|
|
s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
|
ctx,
|
|
&packages_service.PackageInfo{
|
|
Owner: ctx.Package.Owner,
|
|
PackageType: packages_model.TypeConan,
|
|
Name: rref.Name,
|
|
Version: rref.Version,
|
|
},
|
|
&packages_service.PackageFileInfo{
|
|
Filename: filename,
|
|
CompositeKey: fileKey,
|
|
},
|
|
)
|
|
if err != nil {
|
|
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
helper.ServePackageFile(ctx, s, u, pf)
|
|
}
|
|
|
|
// DeleteRecipeV1 deletes the requested recipe(s)
|
|
func DeleteRecipeV1(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
if err := deleteRecipeOrPackage(ctx, rref, true, nil, false); err != nil {
|
|
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
|
|
// DeleteRecipeV2 deletes the requested recipe(s) respecting its revisions
|
|
func DeleteRecipeV2(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
if err := deleteRecipeOrPackage(ctx, rref, rref.Revision == "", nil, false); err != nil {
|
|
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
|
|
// DeletePackageV1 deletes the requested package(s)
|
|
func DeletePackageV1(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
type PackageReferences struct {
|
|
References []string `json:"package_ids"`
|
|
}
|
|
|
|
var ids *PackageReferences
|
|
if err := json.NewDecoder(ctx.Req.Body).Decode(&ids); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
for _, revision := range revisions {
|
|
currentRref := rref.WithRevision(revision.Value)
|
|
|
|
var references []*conan_model.PropertyValue
|
|
if len(ids.References) == 0 {
|
|
if references, err = conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRref); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
} else {
|
|
for _, reference := range ids.References {
|
|
references = append(references, &conan_model.PropertyValue{Value: reference})
|
|
}
|
|
}
|
|
|
|
for _, reference := range references {
|
|
pref, _ := conan_module.NewPackageReference(currentRref, reference.Value, conan_module.DefaultRevision)
|
|
if err := deleteRecipeOrPackage(ctx, currentRref, true, pref, true); err != nil {
|
|
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
|
|
// DeletePackageV2 deletes the requested package(s) respecting its revisions
|
|
func DeletePackageV2(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
if pref != nil { // has package reference
|
|
if err := deleteRecipeOrPackage(ctx, rref, false, pref, pref.Revision == ""); err != nil {
|
|
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
} else {
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
return
|
|
}
|
|
|
|
references, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, rref)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if len(references) == 0 {
|
|
apiError(ctx, http.StatusNotFound, conan_model.ErrPackageReferenceNotExist)
|
|
return
|
|
}
|
|
|
|
for _, reference := range references {
|
|
pref, _ := conan_module.NewPackageReference(rref, reference.Value, conan_module.DefaultRevision)
|
|
|
|
if err := deleteRecipeOrPackage(ctx, rref, false, pref, true); err != nil {
|
|
if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx.Status(http.StatusOK)
|
|
}
|
|
|
|
func deleteRecipeOrPackage(apictx *context.Context, rref *conan_module.RecipeReference, ignoreRecipeRevision bool, pref *conan_module.PackageReference, ignorePackageRevision bool) error {
|
|
var pd *packages_model.PackageDescriptor
|
|
versionDeleted := false
|
|
|
|
err := db.WithTx(apictx, func(ctx std_ctx.Context) error {
|
|
pv, err := packages_model.GetVersionByNameAndVersion(ctx, apictx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pd, err = packages_model.GetPackageDescriptor(ctx, pv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filter := map[string]string{
|
|
conan_module.PropertyRecipeUser: rref.User,
|
|
conan_module.PropertyRecipeChannel: rref.Channel,
|
|
}
|
|
if !ignoreRecipeRevision {
|
|
filter[conan_module.PropertyRecipeRevision] = rref.RevisionOrDefault()
|
|
}
|
|
if pref != nil {
|
|
filter[conan_module.PropertyPackageReference] = pref.Reference
|
|
if !ignorePackageRevision {
|
|
filter[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault()
|
|
}
|
|
}
|
|
|
|
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
|
VersionID: pv.ID,
|
|
Properties: filter,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(pfs) == 0 {
|
|
return conan_model.ErrPackageReferenceNotExist
|
|
}
|
|
|
|
for _, pf := range pfs {
|
|
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
has, err := packages_model.HasVersionFileReferences(ctx, pv.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !has {
|
|
versionDeleted = true
|
|
|
|
return packages_service.DeletePackageVersionAndReferences(ctx, pv)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if versionDeleted {
|
|
notify_service.PackageDelete(apictx, apictx.Doer, pd)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListRecipeRevisions gets a list of all recipe revisions
|
|
func ListRecipeRevisions(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
listRevisions(ctx, revisions)
|
|
}
|
|
|
|
// ListPackageRevisions gets a list of all package revisions
|
|
func ListPackageRevisions(ctx *context.Context) {
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
revisions, err := conan_model.GetPackageRevisions(ctx, ctx.Package.Owner.ID, pref)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
listRevisions(ctx, revisions)
|
|
}
|
|
|
|
type revisionInfo struct {
|
|
Revision string `json:"revision"`
|
|
Time time.Time `json:"time"`
|
|
}
|
|
|
|
func listRevisions(ctx *context.Context, revisions []*conan_model.PropertyValue) {
|
|
if len(revisions) == 0 {
|
|
apiError(ctx, http.StatusNotFound, conan_model.ErrRecipeReferenceNotExist)
|
|
return
|
|
}
|
|
|
|
type RevisionList struct {
|
|
Revisions []*revisionInfo `json:"revisions"`
|
|
}
|
|
|
|
revs := make([]*revisionInfo, 0, len(revisions))
|
|
for _, rev := range revisions {
|
|
revs = append(revs, &revisionInfo{Revision: rev.Value, Time: rev.CreatedUnix.AsLocalTime()})
|
|
}
|
|
|
|
jsonResponse(ctx, http.StatusOK, &RevisionList{revs})
|
|
}
|
|
|
|
// LatestRecipeRevision gets the latest recipe revision
|
|
func LatestRecipeRevision(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
revision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref)
|
|
if err != nil {
|
|
if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()})
|
|
}
|
|
|
|
// LatestPackageRevision gets the latest package revision
|
|
func LatestPackageRevision(ctx *context.Context) {
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
revision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref)
|
|
if err != nil {
|
|
if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()})
|
|
}
|
|
|
|
// ListRecipeRevisionFiles gets a list of all recipe revision files
|
|
func ListRecipeRevisionFiles(ctx *context.Context) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
listRevisionFiles(ctx, rref.AsKey())
|
|
}
|
|
|
|
// ListPackageRevisionFiles gets a list of all package revision files
|
|
func ListPackageRevisionFiles(ctx *context.Context) {
|
|
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
|
|
|
|
listRevisionFiles(ctx, pref.AsKey())
|
|
}
|
|
|
|
func listRevisionFiles(ctx *context.Context, fileKey string) {
|
|
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
|
|
|
|
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
|
|
if err != nil {
|
|
if err == packages_model.ErrPackageNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
|
VersionID: pv.ID,
|
|
CompositeKey: fileKey,
|
|
})
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if len(pfs) == 0 {
|
|
apiError(ctx, http.StatusNotFound, nil)
|
|
return
|
|
}
|
|
|
|
files := make(map[string]any)
|
|
for _, pf := range pfs {
|
|
files[pf.Name] = nil
|
|
}
|
|
|
|
type FileList struct {
|
|
Files map[string]any `json:"files"`
|
|
}
|
|
|
|
jsonResponse(ctx, http.StatusOK, &FileList{
|
|
Files: files,
|
|
})
|
|
}
|