mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
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]>
297 lines
8.1 KiB
Go
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})
|
|
}
|