Enables and tests the usage of Authorized Integrations to access the package registries. Specific testing includes: - Container registry -- automated testing and manual testing - Generic registry, w/ detailed authorization tests -- automated testing - Conan registry -- automated testing (uses an "authenticate" endpoint that required updates) - npm registry -- manual testing with a Forgejo Action publishing packages For the container & conan registeries, where the client uses an authentication endpoint to request a temporary access token, the expiry of the temporary access token is restricted to the expiry of the authorized integration's JWT for the authorized integration in order to prevent an escalation of privileges. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [ ] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12310 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
812 lines
23 KiB
Go
812 lines
23 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package conan
|
|
|
|
import (
|
|
std_ctx "context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"forgejo.org/models/db"
|
|
packages_model "forgejo.org/models/packages"
|
|
conan_model "forgejo.org/models/packages/conan"
|
|
"forgejo.org/modules/container"
|
|
"forgejo.org/modules/json"
|
|
"forgejo.org/modules/log"
|
|
packages_module "forgejo.org/modules/packages"
|
|
conan_module "forgejo.org/modules/packages/conan"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/routers/api/packages/helper"
|
|
"forgejo.org/services/context"
|
|
notify_service "forgejo.org/services/notify"
|
|
packages_service "forgejo.org/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.Authentication.Scope().ValueOrZeroValue()
|
|
exp := ctx.Authentication.ExpiresAt()
|
|
|
|
token, err := packages_service.CreateAuthorizationToken(ctx.Doer, scope, exp)
|
|
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 {
|
|
if context.IsFormError(err) {
|
|
apiError(ctx, http.StatusBadRequest, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, 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,
|
|
})
|
|
}
|