Files
forge/routers/api/packages/conan/conan.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

836 lines
24 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
import (
std_ctx "context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
auth_model "github.com/hanzoai/git/models/auth"
"github.com/hanzoai/git/models/db"
packages_model "github.com/hanzoai/git/models/packages"
conan_model "github.com/hanzoai/git/models/packages/conan"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/container"
"github.com/hanzoai/git/modules/json"
"github.com/hanzoai/git/modules/log"
packages_module "github.com/hanzoai/git/modules/packages"
conan_module "github.com/hanzoai/git/modules/packages/conan"
"github.com/hanzoai/git/modules/setting"
"github.com/hanzoai/git/routers/api/packages/helper"
auth_service "github.com/hanzoai/git/services/auth"
"github.com/hanzoai/git/services/context"
notify_service "github.com/hanzoai/git/services/notify"
packages_service "github.com/hanzoai/git/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)
_ = json.NewEncoder(ctx.Resp).Encode(obj)
}
func apiError(ctx *context.Context, status int, obj any) {
message := helper.ProcessErrorForUser(ctx, status, obj)
jsonResponse(ctx, status, map[string]string{
"message": message,
})
}
func baseURL(ctx *context.Context) string {
return setting.PackageRegistryURL(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.PathParam("name"),
ctx.PathParam("version"),
ctx.PathParam("user"),
ctx.PathParam("channel"),
ctx.PathParam("recipe_revision"),
)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
ctx.Data[recipeReferenceKey] = rref
reference := ctx.PathParam("package_reference")
var pref *conan_module.PackageReference
if reference != "" {
pref, err = conan_module.NewPackageReference(
rref,
reference,
ctx.PathParam("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
}
packageScope := auth_service.GetAccessScope(ctx.Data)
if has, err := packageScope.HasAnyScope(
auth_model.AccessTokenScopeReadPackage,
auth_model.AccessTokenScopeWritePackage,
auth_model.AccessTokenScopeAll,
); !has {
if err != nil {
log.Error("Error checking access scope: %v", err)
}
apiError(ctx, http.StatusForbidden, nil)
return
}
token, err := packages_service.CreateAuthorizationToken(ctx.Doer, packageScope)
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)
return
}
packageScope := auth_service.GetAccessScope(ctx.Data)
if has, err := packageScope.HasAnyScope(
auth_model.AccessTokenScopeReadPackage,
auth_model.AccessTokenScopeWritePackage,
auth_model.AccessTokenScopeAll,
); !has {
if err != nil {
log.Error("Error checking access scope: %v", err)
}
ctx.Status(http.StatusForbidden)
return
}
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 errors.Is(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 errors.Is(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.PathParam("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()
if err := storeFile(ctx, ctx.Doer, rref, pref, fileKey, filename, buf); 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)
}
// storeFile is the ONE writer of Conan package files. The upload handler and the
// upstream cache both go through it, so a cached file lands in exactly the rows a
// publish would have produced and the cache can never hold something the registry
// would have refused on upload.
func storeFile(ctx *context.Context, creator *user_model.User, rref *conan_module.RecipeReference, pref *conan_module.PackageReference, fileKey, filename string, buf *packages_module.HashedBuffer) error {
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: creator,
}
pfci := &packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(filename),
CompositeKey: fileKey,
},
Creator: creator,
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 {
return err
}
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pci.Owner.ID, pci.PackageType, pci.Name, pci.Version)
if err != nil && err != packages_model.ErrPackageNotExist {
return err
}
if pv != nil {
raw, err := json.Marshal(metadata)
if err != nil {
return err
}
pv.MetadataJSON = string(raw)
if err := packages_model.UpdateVersion(ctx, pv); err != nil {
return err
}
} else {
pci.Metadata = metadata
}
} else {
info, err := conan_module.ParseConaninfo(buf)
if err != nil {
return err
}
raw, err := json.Marshal(info)
if err != nil {
return err
}
pfci.Properties[conan_module.PropertyPackageInfo] = string(raw)
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
return err
}
}
_, _, err := packages_service.CreatePackageOrAddFileToExisting(ctx, pci, pfci)
return err
}
// DownloadRecipeFile serves the content of the requested recipe file
func DownloadRecipeFile(ctx *context.Context) {
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
// cacheRecipeIfMissing: fetch this exact revision from the configured
// upstream once. With no upstream set this is a single string check.
cacheRecipeIfMissing(ctx, rref)
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.PathParam("filename")
if !fileFilter.Contains(filename) {
apiError(ctx, http.StatusBadRequest, nil)
return
}
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
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,
},
ctx.Req.Method,
)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(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 errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(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 errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(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 errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(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 errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(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 errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(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 errors.Is(err, conan_model.ErrRecipeReferenceNotExist) || errors.Is(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 errors.Is(err, conan_model.ErrRecipeReferenceNotExist) || errors.Is(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)
// cacheRecipeIfMissing: a client asks for the file list before the files, so
// this is where a cold cache fills.
cacheRecipeIfMissing(ctx, rref)
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 errors.Is(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,
})
}