mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-26 18:12:17 +00:00
796ff26e0e
Hello there, Cargo Index over HTTP is now prefered over git for package updates: we should not force users who do not need the GIT repo to have the repo created/updated on each publish (it can still be created in the packages settings). The current behavior when publishing is to check if the repo exist and create it on the fly if not, then update it's content. Cargo HTTP Index does not rely on the repo itself so this will be useless for everyone not using the git protocol for cargo registry. This PR only disable the creation on the fly of the repo when publishing a crate. This is linked to #26844 (error 500 when trying to publish a crate if user is missing write access to the repo) because it's now optional. --------- Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
311 lines
8.3 KiB
Go
311 lines
8.3 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cargo
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
packages_model "code.gitea.io/gitea/models/packages"
|
|
"code.gitea.io/gitea/modules/context"
|
|
"code.gitea.io/gitea/modules/log"
|
|
packages_module "code.gitea.io/gitea/modules/packages"
|
|
cargo_module "code.gitea.io/gitea/modules/packages/cargo"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/util"
|
|
"code.gitea.io/gitea/routers/api/packages/helper"
|
|
"code.gitea.io/gitea/services/convert"
|
|
packages_service "code.gitea.io/gitea/services/packages"
|
|
cargo_service "code.gitea.io/gitea/services/packages/cargo"
|
|
)
|
|
|
|
// https://doc.rust-lang.org/cargo/reference/registries.html#web-api
|
|
type StatusResponse struct {
|
|
OK bool `json:"ok"`
|
|
Errors []StatusMessage `json:"errors,omitempty"`
|
|
}
|
|
|
|
type StatusMessage struct {
|
|
Message string `json:"detail"`
|
|
}
|
|
|
|
func apiError(ctx *context.Context, status int, obj any) {
|
|
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
|
ctx.JSON(status, StatusResponse{
|
|
OK: false,
|
|
Errors: []StatusMessage{
|
|
{
|
|
Message: message,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
// https://rust-lang.github.io/rfcs/2789-sparse-index.html
|
|
func RepositoryConfig(ctx *context.Context) {
|
|
ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
|
|
}
|
|
|
|
func EnumeratePackageVersions(ctx *context.Context) {
|
|
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.Params("package"))
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
} else {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
b, err := cargo_service.BuildPackageIndex(ctx, p)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if b == nil {
|
|
apiError(ctx, http.StatusNotFound, nil)
|
|
return
|
|
}
|
|
|
|
ctx.PlainTextBytes(http.StatusOK, b.Bytes())
|
|
}
|
|
|
|
type SearchResult struct {
|
|
Crates []*SearchResultCrate `json:"crates"`
|
|
Meta SearchResultMeta `json:"meta"`
|
|
}
|
|
|
|
type SearchResultCrate struct {
|
|
Name string `json:"name"`
|
|
LatestVersion string `json:"max_version"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type SearchResultMeta struct {
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
// https://doc.rust-lang.org/cargo/reference/registries.html#search
|
|
func SearchPackages(ctx *context.Context) {
|
|
page := ctx.FormInt("page")
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
perPage := ctx.FormInt("per_page")
|
|
paginator := db.ListOptions{
|
|
Page: page,
|
|
PageSize: convert.ToCorrectPageSize(perPage),
|
|
}
|
|
|
|
pvs, total, err := packages_model.SearchLatestVersions(
|
|
ctx,
|
|
&packages_model.PackageSearchOptions{
|
|
OwnerID: ctx.Package.Owner.ID,
|
|
Type: packages_model.TypeCargo,
|
|
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
|
IsInternal: util.OptionalBoolFalse,
|
|
Paginator: &paginator,
|
|
},
|
|
)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
crates := make([]*SearchResultCrate, 0, len(pvs))
|
|
for _, pd := range pds {
|
|
crates = append(crates, &SearchResultCrate{
|
|
Name: pd.Package.Name,
|
|
LatestVersion: pd.Version.Version,
|
|
Description: pd.Metadata.(*cargo_module.Metadata).Description,
|
|
})
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, SearchResult{
|
|
Crates: crates,
|
|
Meta: SearchResultMeta{
|
|
Total: total,
|
|
},
|
|
})
|
|
}
|
|
|
|
type Owners struct {
|
|
Users []OwnerUser `json:"users"`
|
|
}
|
|
|
|
type OwnerUser struct {
|
|
ID int64 `json:"id"`
|
|
Login string `json:"login"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// https://doc.rust-lang.org/cargo/reference/registries.html#owners-list
|
|
func ListOwners(ctx *context.Context) {
|
|
ctx.JSON(http.StatusOK, Owners{
|
|
Users: []OwnerUser{
|
|
{
|
|
ID: ctx.Package.Owner.ID,
|
|
Login: ctx.Package.Owner.Name,
|
|
Name: ctx.Package.Owner.DisplayName(),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// DownloadPackageFile serves the content of a package
|
|
func DownloadPackageFile(ctx *context.Context) {
|
|
s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
|
ctx,
|
|
&packages_service.PackageInfo{
|
|
Owner: ctx.Package.Owner,
|
|
PackageType: packages_model.TypeCargo,
|
|
Name: ctx.Params("package"),
|
|
Version: ctx.Params("version"),
|
|
},
|
|
&packages_service.PackageFileInfo{
|
|
Filename: strings.ToLower(fmt.Sprintf("%s-%s.crate", ctx.Params("package"), ctx.Params("version"))),
|
|
},
|
|
)
|
|
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)
|
|
}
|
|
|
|
// https://doc.rust-lang.org/cargo/reference/registries.html#publish
|
|
func UploadPackage(ctx *context.Context) {
|
|
defer ctx.Req.Body.Close()
|
|
|
|
cp, err := cargo_module.ParsePackage(ctx.Req.Body)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
buf, err := packages_module.CreateHashedBufferFromReader(cp.Content)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
defer buf.Close()
|
|
|
|
if buf.Size() != cp.ContentSize {
|
|
apiError(ctx, http.StatusBadRequest, "invalid content size")
|
|
return
|
|
}
|
|
|
|
pv, _, err := packages_service.CreatePackageAndAddFile(
|
|
ctx,
|
|
&packages_service.PackageCreationInfo{
|
|
PackageInfo: packages_service.PackageInfo{
|
|
Owner: ctx.Package.Owner,
|
|
PackageType: packages_model.TypeCargo,
|
|
Name: cp.Name,
|
|
Version: cp.Version,
|
|
},
|
|
SemverCompatible: true,
|
|
Creator: ctx.Doer,
|
|
Metadata: cp.Metadata,
|
|
VersionProperties: map[string]string{
|
|
cargo_module.PropertyYanked: strconv.FormatBool(false),
|
|
},
|
|
},
|
|
&packages_service.PackageFileCreationInfo{
|
|
PackageFileInfo: packages_service.PackageFileInfo{
|
|
Filename: strings.ToLower(fmt.Sprintf("%s-%s.crate", cp.Name, cp.Version)),
|
|
},
|
|
Creator: ctx.Doer,
|
|
Data: buf,
|
|
IsLead: true,
|
|
},
|
|
)
|
|
if err != nil {
|
|
switch err {
|
|
case packages_model.ErrDuplicatePackageVersion:
|
|
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
|
|
}
|
|
|
|
if err := cargo_service.UpdatePackageIndexIfExists(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
|
|
if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
|
log.Error("Rollback creation of package version: %v", err)
|
|
}
|
|
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, StatusResponse{OK: true})
|
|
}
|
|
|
|
// https://doc.rust-lang.org/cargo/reference/registries.html#yank
|
|
func YankPackage(ctx *context.Context) {
|
|
yankPackage(ctx, true)
|
|
}
|
|
|
|
// https://doc.rust-lang.org/cargo/reference/registries.html#unyank
|
|
func UnyankPackage(ctx *context.Context) {
|
|
yankPackage(ctx, false)
|
|
}
|
|
|
|
func yankPackage(ctx *context.Context, yank bool) {
|
|
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.Params("package"), ctx.Params("version"))
|
|
if err != nil {
|
|
if err == packages_model.ErrPackageNotExist {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, cargo_module.PropertyYanked)
|
|
if err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if len(pps) == 0 {
|
|
apiError(ctx, http.StatusInternalServerError, "Property not found")
|
|
return
|
|
}
|
|
|
|
pp := pps[0]
|
|
pp.Value = strconv.FormatBool(yank)
|
|
|
|
if err := packages_model.UpdateProperty(ctx, pp); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
if err := cargo_service.UpdatePackageIndexIfExists(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, StatusResponse{OK: true})
|
|
}
|