forgejo/routers/api/packages/conan/conan.go
Gusted 5a871f6095
[SEC] Ensure propagation of API scopes for Conan and Container authentication
- 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
2024-08-28 10:33:32 +02:00

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,
})
}