code.gitea.io/gitea@v1.21.7/routers/api/packages/conan/conan.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package conan
     5  
     6  import (
     7  	std_ctx "context"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"strings"
    12  	"time"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	packages_model "code.gitea.io/gitea/models/packages"
    16  	conan_model "code.gitea.io/gitea/models/packages/conan"
    17  	"code.gitea.io/gitea/modules/container"
    18  	"code.gitea.io/gitea/modules/context"
    19  	"code.gitea.io/gitea/modules/json"
    20  	"code.gitea.io/gitea/modules/log"
    21  	packages_module "code.gitea.io/gitea/modules/packages"
    22  	conan_module "code.gitea.io/gitea/modules/packages/conan"
    23  	"code.gitea.io/gitea/modules/setting"
    24  	"code.gitea.io/gitea/routers/api/packages/helper"
    25  	notify_service "code.gitea.io/gitea/services/notify"
    26  	packages_service "code.gitea.io/gitea/services/packages"
    27  )
    28  
    29  const (
    30  	conanfileFile = "conanfile.py"
    31  	conaninfoFile = "conaninfo.txt"
    32  
    33  	recipeReferenceKey  = "RecipeReference"
    34  	packageReferenceKey = "PackageReference"
    35  )
    36  
    37  var (
    38  	recipeFileList = container.SetOf(
    39  		conanfileFile,
    40  		"conanmanifest.txt",
    41  		"conan_sources.tgz",
    42  		"conan_export.tgz",
    43  	)
    44  	packageFileList = container.SetOf(
    45  		conaninfoFile,
    46  		"conanmanifest.txt",
    47  		"conan_package.tgz",
    48  	)
    49  )
    50  
    51  func jsonResponse(ctx *context.Context, status int, obj any) {
    52  	// https://github.com/conan-io/conan/issues/6613
    53  	ctx.Resp.Header().Set("Content-Type", "application/json")
    54  	ctx.Status(status)
    55  	if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
    56  		log.Error("JSON encode: %v", err)
    57  	}
    58  }
    59  
    60  func apiError(ctx *context.Context, status int, obj any) {
    61  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    62  		jsonResponse(ctx, status, map[string]string{
    63  			"message": message,
    64  		})
    65  	})
    66  }
    67  
    68  func baseURL(ctx *context.Context) string {
    69  	return setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/conan"
    70  }
    71  
    72  // ExtractPathParameters is a middleware to extract common parameters from path
    73  func ExtractPathParameters(ctx *context.Context) {
    74  	rref, err := conan_module.NewRecipeReference(
    75  		ctx.Params("name"),
    76  		ctx.Params("version"),
    77  		ctx.Params("user"),
    78  		ctx.Params("channel"),
    79  		ctx.Params("recipe_revision"),
    80  	)
    81  	if err != nil {
    82  		apiError(ctx, http.StatusBadRequest, err)
    83  		return
    84  	}
    85  
    86  	ctx.Data[recipeReferenceKey] = rref
    87  
    88  	reference := ctx.Params("package_reference")
    89  
    90  	var pref *conan_module.PackageReference
    91  	if reference != "" {
    92  		pref, err = conan_module.NewPackageReference(
    93  			rref,
    94  			reference,
    95  			ctx.Params("package_revision"),
    96  		)
    97  		if err != nil {
    98  			apiError(ctx, http.StatusBadRequest, err)
    99  			return
   100  		}
   101  	}
   102  
   103  	ctx.Data[packageReferenceKey] = pref
   104  }
   105  
   106  // Ping reports the server capabilities
   107  func Ping(ctx *context.Context) {
   108  	ctx.RespHeader().Add("X-Conan-Server-Capabilities", "revisions") // complex_search,checksum_deploy,matrix_params
   109  
   110  	ctx.Status(http.StatusOK)
   111  }
   112  
   113  // Authenticate creates an authentication token for the user
   114  func Authenticate(ctx *context.Context) {
   115  	if ctx.Doer == nil {
   116  		apiError(ctx, http.StatusBadRequest, nil)
   117  		return
   118  	}
   119  
   120  	token, err := packages_service.CreateAuthorizationToken(ctx.Doer)
   121  	if err != nil {
   122  		apiError(ctx, http.StatusInternalServerError, err)
   123  		return
   124  	}
   125  
   126  	ctx.PlainText(http.StatusOK, token)
   127  }
   128  
   129  // CheckCredentials tests if the provided authentication token is valid
   130  func CheckCredentials(ctx *context.Context) {
   131  	if ctx.Doer == nil {
   132  		ctx.Status(http.StatusUnauthorized)
   133  	} else {
   134  		ctx.Status(http.StatusOK)
   135  	}
   136  }
   137  
   138  // RecipeSnapshot displays the recipe files with their md5 hash
   139  func RecipeSnapshot(ctx *context.Context) {
   140  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   141  
   142  	serveSnapshot(ctx, rref.AsKey())
   143  }
   144  
   145  // RecipeSnapshot displays the package files with their md5 hash
   146  func PackageSnapshot(ctx *context.Context) {
   147  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   148  
   149  	serveSnapshot(ctx, pref.AsKey())
   150  }
   151  
   152  func serveSnapshot(ctx *context.Context, fileKey string) {
   153  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   154  
   155  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
   156  	if err != nil {
   157  		if err == packages_model.ErrPackageNotExist {
   158  			apiError(ctx, http.StatusNotFound, err)
   159  		} else {
   160  			apiError(ctx, http.StatusInternalServerError, err)
   161  		}
   162  		return
   163  	}
   164  
   165  	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   166  		VersionID:    pv.ID,
   167  		CompositeKey: fileKey,
   168  	})
   169  	if err != nil {
   170  		apiError(ctx, http.StatusInternalServerError, err)
   171  		return
   172  	}
   173  	if len(pfs) == 0 {
   174  		apiError(ctx, http.StatusNotFound, nil)
   175  		return
   176  	}
   177  
   178  	files := make(map[string]string)
   179  	for _, pf := range pfs {
   180  		pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
   181  		if err != nil {
   182  			apiError(ctx, http.StatusInternalServerError, err)
   183  			return
   184  		}
   185  		files[pf.Name] = pb.HashMD5
   186  	}
   187  
   188  	jsonResponse(ctx, http.StatusOK, files)
   189  }
   190  
   191  // RecipeDownloadURLs displays the recipe files with their download url
   192  func RecipeDownloadURLs(ctx *context.Context) {
   193  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   194  
   195  	serveDownloadURLs(
   196  		ctx,
   197  		rref.AsKey(),
   198  		fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()),
   199  	)
   200  }
   201  
   202  // PackageDownloadURLs displays the package files with their download url
   203  func PackageDownloadURLs(ctx *context.Context) {
   204  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   205  
   206  	serveDownloadURLs(
   207  		ctx,
   208  		pref.AsKey(),
   209  		fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()),
   210  	)
   211  }
   212  
   213  func serveDownloadURLs(ctx *context.Context, fileKey, downloadURL string) {
   214  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   215  
   216  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
   217  	if err != nil {
   218  		if err == packages_model.ErrPackageNotExist {
   219  			apiError(ctx, http.StatusNotFound, err)
   220  		} else {
   221  			apiError(ctx, http.StatusInternalServerError, err)
   222  		}
   223  		return
   224  	}
   225  
   226  	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   227  		VersionID:    pv.ID,
   228  		CompositeKey: fileKey,
   229  	})
   230  	if err != nil {
   231  		apiError(ctx, http.StatusInternalServerError, err)
   232  		return
   233  	}
   234  
   235  	if len(pfs) == 0 {
   236  		apiError(ctx, http.StatusNotFound, nil)
   237  		return
   238  	}
   239  
   240  	urls := make(map[string]string)
   241  	for _, pf := range pfs {
   242  		urls[pf.Name] = fmt.Sprintf("%s/%s", downloadURL, pf.Name)
   243  	}
   244  
   245  	jsonResponse(ctx, http.StatusOK, urls)
   246  }
   247  
   248  // RecipeUploadURLs displays the upload urls for the provided recipe files
   249  func RecipeUploadURLs(ctx *context.Context) {
   250  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   251  
   252  	serveUploadURLs(
   253  		ctx,
   254  		recipeFileList,
   255  		fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/recipe", rref.LinkName()),
   256  	)
   257  }
   258  
   259  // PackageUploadURLs displays the upload urls for the provided package files
   260  func PackageUploadURLs(ctx *context.Context) {
   261  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   262  
   263  	serveUploadURLs(
   264  		ctx,
   265  		packageFileList,
   266  		fmt.Sprintf(baseURL(ctx)+"/v1/files/%s/package/%s", pref.Recipe.LinkName(), pref.LinkName()),
   267  	)
   268  }
   269  
   270  func serveUploadURLs(ctx *context.Context, fileFilter container.Set[string], uploadURL string) {
   271  	defer ctx.Req.Body.Close()
   272  
   273  	var files map[string]int64
   274  	if err := json.NewDecoder(ctx.Req.Body).Decode(&files); err != nil {
   275  		apiError(ctx, http.StatusBadRequest, err)
   276  		return
   277  	}
   278  
   279  	urls := make(map[string]string)
   280  	for file := range files {
   281  		if fileFilter.Contains(file) {
   282  			urls[file] = fmt.Sprintf("%s/%s", uploadURL, file)
   283  		}
   284  	}
   285  
   286  	jsonResponse(ctx, http.StatusOK, urls)
   287  }
   288  
   289  // UploadRecipeFile handles the upload of a recipe file
   290  func UploadRecipeFile(ctx *context.Context) {
   291  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   292  
   293  	uploadFile(ctx, recipeFileList, rref.AsKey())
   294  }
   295  
   296  // UploadPackageFile handles the upload of a package file
   297  func UploadPackageFile(ctx *context.Context) {
   298  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   299  
   300  	uploadFile(ctx, packageFileList, pref.AsKey())
   301  }
   302  
   303  func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) {
   304  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   305  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   306  
   307  	filename := ctx.Params("filename")
   308  	if !fileFilter.Contains(filename) {
   309  		apiError(ctx, http.StatusBadRequest, nil)
   310  		return
   311  	}
   312  
   313  	upload, close, err := ctx.UploadStream()
   314  	if err != nil {
   315  		apiError(ctx, http.StatusBadRequest, err)
   316  		return
   317  	}
   318  	if close {
   319  		defer upload.Close()
   320  	}
   321  
   322  	buf, err := packages_module.CreateHashedBufferFromReader(upload)
   323  	if err != nil {
   324  		apiError(ctx, http.StatusInternalServerError, err)
   325  		return
   326  	}
   327  	defer buf.Close()
   328  
   329  	isConanfileFile := filename == conanfileFile
   330  	isConaninfoFile := filename == conaninfoFile
   331  
   332  	pci := &packages_service.PackageCreationInfo{
   333  		PackageInfo: packages_service.PackageInfo{
   334  			Owner:       ctx.Package.Owner,
   335  			PackageType: packages_model.TypeConan,
   336  			Name:        rref.Name,
   337  			Version:     rref.Version,
   338  		},
   339  		Creator: ctx.Doer,
   340  	}
   341  	pfci := &packages_service.PackageFileCreationInfo{
   342  		PackageFileInfo: packages_service.PackageFileInfo{
   343  			Filename:     strings.ToLower(filename),
   344  			CompositeKey: fileKey,
   345  		},
   346  		Creator: ctx.Doer,
   347  		Data:    buf,
   348  		IsLead:  isConanfileFile,
   349  		Properties: map[string]string{
   350  			conan_module.PropertyRecipeUser:     rref.User,
   351  			conan_module.PropertyRecipeChannel:  rref.Channel,
   352  			conan_module.PropertyRecipeRevision: rref.RevisionOrDefault(),
   353  		},
   354  		OverwriteExisting: true,
   355  	}
   356  
   357  	if pref != nil {
   358  		pfci.Properties[conan_module.PropertyPackageReference] = pref.Reference
   359  		pfci.Properties[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault()
   360  	}
   361  
   362  	if isConanfileFile || isConaninfoFile {
   363  		if isConanfileFile {
   364  			metadata, err := conan_module.ParseConanfile(buf)
   365  			if err != nil {
   366  				log.Error("Error parsing package metadata: %v", err)
   367  				apiError(ctx, http.StatusInternalServerError, err)
   368  				return
   369  			}
   370  			pv, err := packages_model.GetVersionByNameAndVersion(ctx, pci.Owner.ID, pci.PackageType, pci.Name, pci.Version)
   371  			if err != nil && err != packages_model.ErrPackageNotExist {
   372  				apiError(ctx, http.StatusInternalServerError, err)
   373  				return
   374  			}
   375  			if pv != nil {
   376  				raw, err := json.Marshal(metadata)
   377  				if err != nil {
   378  					apiError(ctx, http.StatusInternalServerError, err)
   379  					return
   380  				}
   381  				pv.MetadataJSON = string(raw)
   382  				if err := packages_model.UpdateVersion(ctx, pv); err != nil {
   383  					apiError(ctx, http.StatusInternalServerError, err)
   384  					return
   385  				}
   386  			} else {
   387  				pci.Metadata = metadata
   388  			}
   389  		} else {
   390  			info, err := conan_module.ParseConaninfo(buf)
   391  			if err != nil {
   392  				log.Error("Error parsing conan info: %v", err)
   393  				apiError(ctx, http.StatusInternalServerError, err)
   394  				return
   395  			}
   396  			raw, err := json.Marshal(info)
   397  			if err != nil {
   398  				apiError(ctx, http.StatusInternalServerError, err)
   399  				return
   400  			}
   401  			pfci.Properties[conan_module.PropertyPackageInfo] = string(raw)
   402  		}
   403  
   404  		if _, err := buf.Seek(0, io.SeekStart); err != nil {
   405  			apiError(ctx, http.StatusInternalServerError, err)
   406  			return
   407  		}
   408  	}
   409  
   410  	_, _, err = packages_service.CreatePackageOrAddFileToExisting(
   411  		ctx,
   412  		pci,
   413  		pfci,
   414  	)
   415  	if err != nil {
   416  		switch err {
   417  		case packages_model.ErrDuplicatePackageFile:
   418  			apiError(ctx, http.StatusBadRequest, err)
   419  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   420  			apiError(ctx, http.StatusForbidden, err)
   421  		default:
   422  			apiError(ctx, http.StatusInternalServerError, err)
   423  		}
   424  		return
   425  	}
   426  
   427  	ctx.Status(http.StatusCreated)
   428  }
   429  
   430  // DownloadRecipeFile serves the content of the requested recipe file
   431  func DownloadRecipeFile(ctx *context.Context) {
   432  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   433  
   434  	downloadFile(ctx, recipeFileList, rref.AsKey())
   435  }
   436  
   437  // DownloadPackageFile serves the content of the requested package file
   438  func DownloadPackageFile(ctx *context.Context) {
   439  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   440  
   441  	downloadFile(ctx, packageFileList, pref.AsKey())
   442  }
   443  
   444  func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) {
   445  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   446  
   447  	filename := ctx.Params("filename")
   448  	if !fileFilter.Contains(filename) {
   449  		apiError(ctx, http.StatusBadRequest, nil)
   450  		return
   451  	}
   452  
   453  	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
   454  		ctx,
   455  		&packages_service.PackageInfo{
   456  			Owner:       ctx.Package.Owner,
   457  			PackageType: packages_model.TypeConan,
   458  			Name:        rref.Name,
   459  			Version:     rref.Version,
   460  		},
   461  		&packages_service.PackageFileInfo{
   462  			Filename:     filename,
   463  			CompositeKey: fileKey,
   464  		},
   465  	)
   466  	if err != nil {
   467  		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
   468  			apiError(ctx, http.StatusNotFound, err)
   469  			return
   470  		}
   471  		apiError(ctx, http.StatusInternalServerError, err)
   472  		return
   473  	}
   474  
   475  	helper.ServePackageFile(ctx, s, u, pf)
   476  }
   477  
   478  // DeleteRecipeV1 deletes the requested recipe(s)
   479  func DeleteRecipeV1(ctx *context.Context) {
   480  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   481  
   482  	if err := deleteRecipeOrPackage(ctx, rref, true, nil, false); err != nil {
   483  		if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
   484  			apiError(ctx, http.StatusNotFound, err)
   485  		} else {
   486  			apiError(ctx, http.StatusInternalServerError, err)
   487  		}
   488  		return
   489  	}
   490  	ctx.Status(http.StatusOK)
   491  }
   492  
   493  // DeleteRecipeV2 deletes the requested recipe(s) respecting its revisions
   494  func DeleteRecipeV2(ctx *context.Context) {
   495  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   496  
   497  	if err := deleteRecipeOrPackage(ctx, rref, rref.Revision == "", nil, false); err != nil {
   498  		if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
   499  			apiError(ctx, http.StatusNotFound, err)
   500  		} else {
   501  			apiError(ctx, http.StatusInternalServerError, err)
   502  		}
   503  		return
   504  	}
   505  	ctx.Status(http.StatusOK)
   506  }
   507  
   508  // DeletePackageV1 deletes the requested package(s)
   509  func DeletePackageV1(ctx *context.Context) {
   510  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   511  
   512  	type PackageReferences struct {
   513  		References []string `json:"package_ids"`
   514  	}
   515  
   516  	var ids *PackageReferences
   517  	if err := json.NewDecoder(ctx.Req.Body).Decode(&ids); err != nil {
   518  		apiError(ctx, http.StatusInternalServerError, err)
   519  		return
   520  	}
   521  
   522  	revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
   523  	if err != nil {
   524  		apiError(ctx, http.StatusInternalServerError, err)
   525  		return
   526  	}
   527  	for _, revision := range revisions {
   528  		currentRref := rref.WithRevision(revision.Value)
   529  
   530  		var references []*conan_model.PropertyValue
   531  		if len(ids.References) == 0 {
   532  			if references, err = conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRref); err != nil {
   533  				apiError(ctx, http.StatusInternalServerError, err)
   534  				return
   535  			}
   536  		} else {
   537  			for _, reference := range ids.References {
   538  				references = append(references, &conan_model.PropertyValue{Value: reference})
   539  			}
   540  		}
   541  
   542  		for _, reference := range references {
   543  			pref, _ := conan_module.NewPackageReference(currentRref, reference.Value, conan_module.DefaultRevision)
   544  			if err := deleteRecipeOrPackage(ctx, currentRref, true, pref, true); err != nil {
   545  				if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
   546  					apiError(ctx, http.StatusNotFound, err)
   547  				} else {
   548  					apiError(ctx, http.StatusInternalServerError, err)
   549  				}
   550  				return
   551  			}
   552  		}
   553  	}
   554  	ctx.Status(http.StatusOK)
   555  }
   556  
   557  // DeletePackageV2 deletes the requested package(s) respecting its revisions
   558  func DeletePackageV2(ctx *context.Context) {
   559  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   560  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   561  
   562  	if pref != nil { // has package reference
   563  		if err := deleteRecipeOrPackage(ctx, rref, false, pref, pref.Revision == ""); err != nil {
   564  			if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
   565  				apiError(ctx, http.StatusNotFound, err)
   566  			} else {
   567  				apiError(ctx, http.StatusInternalServerError, err)
   568  			}
   569  		} else {
   570  			ctx.Status(http.StatusOK)
   571  		}
   572  		return
   573  	}
   574  
   575  	references, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, rref)
   576  	if err != nil {
   577  		apiError(ctx, http.StatusInternalServerError, err)
   578  		return
   579  	}
   580  	if len(references) == 0 {
   581  		apiError(ctx, http.StatusNotFound, conan_model.ErrPackageReferenceNotExist)
   582  		return
   583  	}
   584  
   585  	for _, reference := range references {
   586  		pref, _ := conan_module.NewPackageReference(rref, reference.Value, conan_module.DefaultRevision)
   587  
   588  		if err := deleteRecipeOrPackage(ctx, rref, false, pref, true); err != nil {
   589  			if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist {
   590  				apiError(ctx, http.StatusNotFound, err)
   591  			} else {
   592  				apiError(ctx, http.StatusInternalServerError, err)
   593  			}
   594  			return
   595  		}
   596  	}
   597  
   598  	ctx.Status(http.StatusOK)
   599  }
   600  
   601  func deleteRecipeOrPackage(apictx *context.Context, rref *conan_module.RecipeReference, ignoreRecipeRevision bool, pref *conan_module.PackageReference, ignorePackageRevision bool) error {
   602  	var pd *packages_model.PackageDescriptor
   603  	versionDeleted := false
   604  
   605  	err := db.WithTx(apictx, func(ctx std_ctx.Context) error {
   606  		pv, err := packages_model.GetVersionByNameAndVersion(ctx, apictx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
   607  		if err != nil {
   608  			return err
   609  		}
   610  
   611  		pd, err = packages_model.GetPackageDescriptor(ctx, pv)
   612  		if err != nil {
   613  			return err
   614  		}
   615  
   616  		filter := map[string]string{
   617  			conan_module.PropertyRecipeUser:    rref.User,
   618  			conan_module.PropertyRecipeChannel: rref.Channel,
   619  		}
   620  		if !ignoreRecipeRevision {
   621  			filter[conan_module.PropertyRecipeRevision] = rref.RevisionOrDefault()
   622  		}
   623  		if pref != nil {
   624  			filter[conan_module.PropertyPackageReference] = pref.Reference
   625  			if !ignorePackageRevision {
   626  				filter[conan_module.PropertyPackageRevision] = pref.RevisionOrDefault()
   627  			}
   628  		}
   629  
   630  		pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   631  			VersionID:  pv.ID,
   632  			Properties: filter,
   633  		})
   634  		if err != nil {
   635  			return err
   636  		}
   637  		if len(pfs) == 0 {
   638  			return conan_model.ErrPackageReferenceNotExist
   639  		}
   640  
   641  		for _, pf := range pfs {
   642  			if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
   643  				return err
   644  			}
   645  		}
   646  		has, err := packages_model.HasVersionFileReferences(ctx, pv.ID)
   647  		if err != nil {
   648  			return err
   649  		}
   650  		if !has {
   651  			versionDeleted = true
   652  
   653  			return packages_service.DeletePackageVersionAndReferences(ctx, pv)
   654  		}
   655  		return nil
   656  	})
   657  	if err != nil {
   658  		return err
   659  	}
   660  
   661  	if versionDeleted {
   662  		notify_service.PackageDelete(apictx, apictx.Doer, pd)
   663  	}
   664  
   665  	return nil
   666  }
   667  
   668  // ListRecipeRevisions gets a list of all recipe revisions
   669  func ListRecipeRevisions(ctx *context.Context) {
   670  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   671  
   672  	revisions, err := conan_model.GetRecipeRevisions(ctx, ctx.Package.Owner.ID, rref)
   673  	if err != nil {
   674  		apiError(ctx, http.StatusInternalServerError, err)
   675  		return
   676  	}
   677  
   678  	listRevisions(ctx, revisions)
   679  }
   680  
   681  // ListPackageRevisions gets a list of all package revisions
   682  func ListPackageRevisions(ctx *context.Context) {
   683  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   684  
   685  	revisions, err := conan_model.GetPackageRevisions(ctx, ctx.Package.Owner.ID, pref)
   686  	if err != nil {
   687  		apiError(ctx, http.StatusInternalServerError, err)
   688  		return
   689  	}
   690  
   691  	listRevisions(ctx, revisions)
   692  }
   693  
   694  type revisionInfo struct {
   695  	Revision string    `json:"revision"`
   696  	Time     time.Time `json:"time"`
   697  }
   698  
   699  func listRevisions(ctx *context.Context, revisions []*conan_model.PropertyValue) {
   700  	if len(revisions) == 0 {
   701  		apiError(ctx, http.StatusNotFound, conan_model.ErrRecipeReferenceNotExist)
   702  		return
   703  	}
   704  
   705  	type RevisionList struct {
   706  		Revisions []*revisionInfo `json:"revisions"`
   707  	}
   708  
   709  	revs := make([]*revisionInfo, 0, len(revisions))
   710  	for _, rev := range revisions {
   711  		revs = append(revs, &revisionInfo{Revision: rev.Value, Time: rev.CreatedUnix.AsLocalTime()})
   712  	}
   713  
   714  	jsonResponse(ctx, http.StatusOK, &RevisionList{revs})
   715  }
   716  
   717  // LatestRecipeRevision gets the latest recipe revision
   718  func LatestRecipeRevision(ctx *context.Context) {
   719  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   720  
   721  	revision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref)
   722  	if err != nil {
   723  		if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist {
   724  			apiError(ctx, http.StatusNotFound, err)
   725  		} else {
   726  			apiError(ctx, http.StatusInternalServerError, err)
   727  		}
   728  		return
   729  	}
   730  
   731  	jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()})
   732  }
   733  
   734  // LatestPackageRevision gets the latest package revision
   735  func LatestPackageRevision(ctx *context.Context) {
   736  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   737  
   738  	revision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref)
   739  	if err != nil {
   740  		if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist {
   741  			apiError(ctx, http.StatusNotFound, err)
   742  		} else {
   743  			apiError(ctx, http.StatusInternalServerError, err)
   744  		}
   745  		return
   746  	}
   747  
   748  	jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()})
   749  }
   750  
   751  // ListRecipeRevisionFiles gets a list of all recipe revision files
   752  func ListRecipeRevisionFiles(ctx *context.Context) {
   753  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   754  
   755  	listRevisionFiles(ctx, rref.AsKey())
   756  }
   757  
   758  // ListPackageRevisionFiles gets a list of all package revision files
   759  func ListPackageRevisionFiles(ctx *context.Context) {
   760  	pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
   761  
   762  	listRevisionFiles(ctx, pref.AsKey())
   763  }
   764  
   765  func listRevisionFiles(ctx *context.Context, fileKey string) {
   766  	rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
   767  
   768  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version)
   769  	if err != nil {
   770  		if err == packages_model.ErrPackageNotExist {
   771  			apiError(ctx, http.StatusNotFound, err)
   772  		} else {
   773  			apiError(ctx, http.StatusInternalServerError, err)
   774  		}
   775  		return
   776  	}
   777  
   778  	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   779  		VersionID:    pv.ID,
   780  		CompositeKey: fileKey,
   781  	})
   782  	if err != nil {
   783  		apiError(ctx, http.StatusInternalServerError, err)
   784  		return
   785  	}
   786  	if len(pfs) == 0 {
   787  		apiError(ctx, http.StatusNotFound, nil)
   788  		return
   789  	}
   790  
   791  	files := make(map[string]any)
   792  	for _, pf := range pfs {
   793  		files[pf.Name] = nil
   794  	}
   795  
   796  	type FileList struct {
   797  		Files map[string]any `json:"files"`
   798  	}
   799  
   800  	jsonResponse(ctx, http.StatusOK, &FileList{
   801  		Files: files,
   802  	})
   803  }