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