Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

297 lines
8.1 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package cargo
import (
"errors"
"net/http"
"strconv"
"github.com/hanzoai/git/models/db"
packages_model "github.com/hanzoai/git/models/packages"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/modules/optional"
packages_module "github.com/hanzoai/git/modules/packages"
cargo_module "github.com/hanzoai/git/modules/packages/cargo"
"github.com/hanzoai/git/modules/setting"
"github.com/hanzoai/git/modules/structs"
"github.com/hanzoai/git/modules/util"
"github.com/hanzoai/git/routers/api/packages/helper"
"github.com/hanzoai/git/services/context"
"github.com/hanzoai/git/services/convert"
packages_service "github.com/hanzoai/git/services/packages"
cargo_service "github.com/hanzoai/git/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) {
message := helper.ProcessErrorForUser(ctx, status, obj)
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.RequireSignInViewStrict || 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.PathParam("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 := max(ctx.FormInt("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: optional.Some(false),
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) {
// openOrCache: serve what we have, else fetch it once from the configured
// upstream and serve that. With no upstream set this is exactly the local open.
s, u, pf, err := openOrCache(ctx, ctx.PathParam("package"), ctx.PathParam("version"))
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(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: crateFilename(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.PathParam("package"), ctx.PathParam("version"))
if err != nil {
if errors.Is(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})
}