summaryrefslogtreecommitdiffstats
path: root/routers/api/packages/conan
diff options
context:
space:
mode:
Diffstat (limited to 'routers/api/packages/conan')
-rw-r--r--routers/api/packages/conan/auth.go48
-rw-r--r--routers/api/packages/conan/conan.go807
-rw-r--r--routers/api/packages/conan/search.go163
3 files changed, 1018 insertions, 0 deletions
diff --git a/routers/api/packages/conan/auth.go b/routers/api/packages/conan/auth.go
new file mode 100644
index 0000000..e2e1901
--- /dev/null
+++ b/routers/api/packages/conan/auth.go
@@ -0,0 +1,48 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package conan
+
+import (
+ "net/http"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/auth"
+ "code.gitea.io/gitea/services/packages"
+)
+
+var _ auth.Method = &Auth{}
+
+type Auth struct{}
+
+func (a *Auth) Name() string {
+ return "conan"
+}
+
+// Verify extracts the user from the Bearer token
+func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
+ uid, scope, err := packages.ParseAuthorizationToken(req)
+ if err != nil {
+ log.Trace("ParseAuthorizationToken: %v", err)
+ return nil, err
+ }
+
+ if uid == 0 {
+ return nil, nil
+ }
+
+ // Propagate scope of the authorization token.
+ if scope != "" {
+ store.GetData()["IsApiToken"] = true
+ store.GetData()["ApiTokenScope"] = scope
+ }
+
+ u, err := user_model.GetUserByID(req.Context(), uid)
+ if err != nil {
+ log.Error("GetUserByID: %v", err)
+ return nil, err
+ }
+
+ return u, nil
+}
diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go
new file mode 100644
index 0000000..e07907a
--- /dev/null
+++ b/routers/api/packages/conan/conan.go
@@ -0,0 +1,807 @@
+// 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,
+ })
+}
diff --git a/routers/api/packages/conan/search.go b/routers/api/packages/conan/search.go
new file mode 100644
index 0000000..7370c70
--- /dev/null
+++ b/routers/api/packages/conan/search.go
@@ -0,0 +1,163 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package conan
+
+import (
+ "net/http"
+ "strings"
+
+ conan_model "code.gitea.io/gitea/models/packages/conan"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/json"
+ conan_module "code.gitea.io/gitea/modules/packages/conan"
+ "code.gitea.io/gitea/services/context"
+)
+
+// SearchResult contains the found recipe names
+type SearchResult struct {
+ Results []string `json:"results"`
+}
+
+// SearchRecipes searches all recipes matching the query
+func SearchRecipes(ctx *context.Context) {
+ q := ctx.FormTrim("q")
+
+ opts := parseQuery(ctx.Package.Owner, q)
+
+ results, err := conan_model.SearchRecipes(ctx, opts)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ jsonResponse(ctx, http.StatusOK, &SearchResult{
+ Results: results,
+ })
+}
+
+// parseQuery creates search options for the given query
+func parseQuery(owner *user_model.User, query string) *conan_model.RecipeSearchOptions {
+ opts := &conan_model.RecipeSearchOptions{
+ OwnerID: owner.ID,
+ }
+
+ if query != "" {
+ parts := strings.Split(strings.ReplaceAll(query, "@", "/"), "/")
+
+ opts.Name = parts[0]
+ if len(parts) > 1 && parts[1] != "*" {
+ opts.Version = parts[1]
+ }
+ if len(parts) > 2 && parts[2] != "*" {
+ opts.User = parts[2]
+ }
+ if len(parts) > 3 && parts[3] != "*" {
+ opts.Channel = parts[3]
+ }
+ }
+
+ return opts
+}
+
+// SearchPackagesV1 searches all packages of a recipe (Conan v1 endpoint)
+func SearchPackagesV1(ctx *context.Context) {
+ searchPackages(ctx, true)
+}
+
+// SearchPackagesV2 searches all packages of a recipe (Conan v2 endpoint)
+func SearchPackagesV2(ctx *context.Context) {
+ searchPackages(ctx, false)
+}
+
+func searchPackages(ctx *context.Context, searchAllRevisions bool) {
+ rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
+
+ if !searchAllRevisions && rref.Revision == "" {
+ lastRevision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref)
+ if err != nil {
+ if err == conan_model.ErrRecipeReferenceNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+ rref = rref.WithRevision(lastRevision.Value)
+ } else {
+ has, err := conan_model.RecipeExists(ctx, ctx.Package.Owner.ID, rref)
+ if err != nil {
+ if err == conan_model.ErrRecipeReferenceNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+ if !has {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
+ }
+
+ recipeRevisions := []*conan_model.PropertyValue{{Value: rref.Revision}}
+ if searchAllRevisions {
+ var err error
+ recipeRevisions, err = conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ }
+
+ result := make(map[string]*conan_module.Conaninfo)
+
+ for _, recipeRevision := range recipeRevisions {
+ currentRef := rref
+ if recipeRevision.Value != "" {
+ currentRef = rref.WithRevision(recipeRevision.Value)
+ }
+ packageReferences, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRef)
+ if err != nil {
+ if err == conan_model.ErrRecipeReferenceNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+ for _, packageReference := range packageReferences {
+ if _, ok := result[packageReference.Value]; ok {
+ continue
+ }
+ pref, _ := conan_module.NewPackageReference(currentRef, packageReference.Value, "")
+ lastPackageRevision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref)
+ if err != nil {
+ if err == conan_model.ErrPackageReferenceNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+ pref = pref.WithRevision(lastPackageRevision.Value)
+ infoRaw, err := conan_model.GetPackageInfo(ctx, ctx.Package.Owner.ID, pref)
+ if err != nil {
+ if err == conan_model.ErrPackageReferenceNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+ var info *conan_module.Conaninfo
+ if err := json.Unmarshal([]byte(infoRaw), &info); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ result[pref.Reference] = info
+ }
+ }
+
+ jsonResponse(ctx, http.StatusOK, result)
+}