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]>
492 lines
13 KiB
Go
492 lines
13 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package packages
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/hanzoai/git/models/packages"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
"github.com/hanzoai/git/modules/optional"
|
|
api "github.com/hanzoai/git/modules/structs"
|
|
"github.com/hanzoai/git/modules/util"
|
|
"github.com/hanzoai/git/routers/api/v1/utils"
|
|
"github.com/hanzoai/git/services/context"
|
|
"github.com/hanzoai/git/services/convert"
|
|
packages_service "github.com/hanzoai/git/services/packages"
|
|
)
|
|
|
|
// ListPackages gets all packages of an owner
|
|
func ListPackages(ctx *context.APIContext) {
|
|
// swagger:operation GET /owners/{owner}/packages package listPackages
|
|
// ---
|
|
// summary: Gets all packages of an owner
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the packages
|
|
// type: string
|
|
// required: true
|
|
// - name: page
|
|
// in: query
|
|
// description: page number of results to return (1-based)
|
|
// type: integer
|
|
// - name: limit
|
|
// in: query
|
|
// description: page size of results
|
|
// type: integer
|
|
// - name: type
|
|
// in: query
|
|
// description: package type filter
|
|
// type: string
|
|
// enum: [alpine, cargo, chef, composer, conan, conda, container, cran, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, terraform, vagrant]
|
|
// - name: q
|
|
// in: query
|
|
// description: name filter
|
|
// type: string
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/PackageList"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
listOptions := utils.GetListOptions(ctx)
|
|
|
|
apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{
|
|
OwnerID: ctx.Package.Owner.ID,
|
|
Type: packages.Type(ctx.FormTrim("type")),
|
|
Name: packages.SearchValue{Value: ctx.FormTrim("q")},
|
|
IsInternal: optional.Some(false),
|
|
Paginator: &listOptions,
|
|
})
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.SetLinkHeader(count, listOptions.PageSize)
|
|
ctx.SetTotalCountHeader(count)
|
|
ctx.JSON(http.StatusOK, apiPackages)
|
|
}
|
|
|
|
// GetPackage gets a package
|
|
func GetPackage(ctx *context.APIContext) {
|
|
// swagger:operation GET /owners/{owner}/packages/{type}/{name}/{version} package getPackage
|
|
// ---
|
|
// summary: Gets a package
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: type
|
|
// in: path
|
|
// description: type of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: name of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: version
|
|
// in: path
|
|
// description: version of the package
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/Package"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
apiPackage, err := convert.ToPackage(ctx, ctx.Package.Descriptor, ctx.Doer)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, apiPackage)
|
|
}
|
|
|
|
// DeletePackage deletes a package
|
|
func DeletePackage(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /owners/{owner}/packages/{type}/{name} package deletePackage
|
|
// ---
|
|
// summary: Delete a package
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: type
|
|
// in: path
|
|
// description: type of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: name of the package
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
err := packages_service.RemovePackage(ctx, ctx.Doer, ctx.Package.Descriptor.Package)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// DeletePackageVersion deletes a package version
|
|
func DeletePackageVersion(ctx *context.APIContext) {
|
|
// swagger:operation DELETE /owners/{owner}/packages/{type}/{name}/{version} package deletePackageVersion
|
|
// ---
|
|
// summary: Delete a package version
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: type
|
|
// in: path
|
|
// description: type of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: name of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: version
|
|
// in: path
|
|
// description: version of the package
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "204":
|
|
// "$ref": "#/responses/empty"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
err := packages_service.RemovePackageVersion(ctx, ctx.Doer, ctx.Package.Descriptor.Version)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// ListPackageFiles gets all files of a package
|
|
func ListPackageFiles(ctx *context.APIContext) {
|
|
// swagger:operation GET /owners/{owner}/packages/{type}/{name}/{version}/files package listPackageFiles
|
|
// ---
|
|
// summary: Gets all files of a package
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: type
|
|
// in: path
|
|
// description: type of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: name of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: version
|
|
// in: path
|
|
// description: version of the package
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/PackageFileList"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
apiPackageFiles := make([]*api.PackageFile, 0, len(ctx.Package.Descriptor.Files))
|
|
for _, pfd := range ctx.Package.Descriptor.Files {
|
|
apiPackageFiles = append(apiPackageFiles, convert.ToPackageFile(pfd))
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, apiPackageFiles)
|
|
}
|
|
|
|
// ListPackageVersions gets all versions of a package
|
|
func ListPackageVersions(ctx *context.APIContext) {
|
|
// swagger:operation GET /owners/{owner}/packages/{type}/{name} package listPackageVersions
|
|
// ---
|
|
// summary: Gets all versions of a package
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: type
|
|
// in: path
|
|
// description: type of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: name of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: page
|
|
// in: query
|
|
// description: page number of results to return (1-based)
|
|
// type: integer
|
|
// - name: limit
|
|
// in: query
|
|
// description: page size of results
|
|
// type: integer
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/PackageList"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
listOptions := utils.GetListOptions(ctx)
|
|
|
|
apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{
|
|
OwnerID: ctx.Package.Owner.ID,
|
|
Type: packages.Type(ctx.PathParam("type")),
|
|
Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true},
|
|
IsInternal: optional.Some(false),
|
|
Paginator: &listOptions,
|
|
})
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.SetLinkHeader(count, listOptions.PageSize)
|
|
ctx.SetTotalCountHeader(count)
|
|
ctx.JSON(http.StatusOK, apiPackages)
|
|
}
|
|
|
|
// GetLatestPackageVersion gets the latest version of a package
|
|
func GetLatestPackageVersion(ctx *context.APIContext) {
|
|
// swagger:operation GET /owners/{owner}/packages/{type}/{name}/-/latest package getLatestPackageVersion
|
|
// ---
|
|
// summary: Gets the latest version of a package
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: type
|
|
// in: path
|
|
// description: type of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: name of the package
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "200":
|
|
// "$ref": "#/responses/Package"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
pvs, _, err := packages.SearchLatestVersions(ctx, &packages.PackageSearchOptions{
|
|
OwnerID: ctx.Package.Owner.ID,
|
|
Type: packages.Type(ctx.PathParam("type")),
|
|
Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true},
|
|
IsInternal: optional.Some(false),
|
|
})
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
if len(pvs) == 0 {
|
|
ctx.APIErrorNotFound()
|
|
return
|
|
}
|
|
|
|
pd, err := packages.GetPackageDescriptor(ctx, pvs[0])
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
|
|
if err != nil {
|
|
ctx.APIErrorInternal(err)
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, apiPackage)
|
|
}
|
|
|
|
// LinkPackage sets a repository link for a package
|
|
func LinkPackage(ctx *context.APIContext) {
|
|
// swagger:operation POST /owners/{owner}/packages/{type}/{name}/-/link/{repo_name} package linkPackage
|
|
// ---
|
|
// summary: Link a package to a repository
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: type
|
|
// in: path
|
|
// description: type of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: name of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: repo_name
|
|
// in: path
|
|
// description: name of the repository to link.
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "201":
|
|
// "$ref": "#/responses/empty"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
ctx.APIError(http.StatusNotFound, err.Error())
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name"))
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
ctx.APIError(http.StatusNotFound, err.Error())
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
err = packages_service.LinkToRepository(ctx, pkg, repo, ctx.Doer)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, util.ErrInvalidArgument):
|
|
ctx.APIError(http.StatusBadRequest, err.Error())
|
|
case errors.Is(err, util.ErrPermissionDenied):
|
|
ctx.APIError(http.StatusForbidden, err.Error())
|
|
default:
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Status(http.StatusCreated)
|
|
}
|
|
|
|
// UnlinkPackage sets a repository link for a package
|
|
func UnlinkPackage(ctx *context.APIContext) {
|
|
// swagger:operation POST /owners/{owner}/packages/{type}/{name}/-/unlink package unlinkPackage
|
|
// ---
|
|
// summary: Unlink a package from a repository
|
|
// parameters:
|
|
// - name: owner
|
|
// in: path
|
|
// description: owner of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: type
|
|
// in: path
|
|
// description: type of the package
|
|
// type: string
|
|
// required: true
|
|
// - name: name
|
|
// in: path
|
|
// description: name of the package
|
|
// type: string
|
|
// required: true
|
|
// responses:
|
|
// "201":
|
|
// "$ref": "#/responses/empty"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
ctx.APIError(http.StatusNotFound, err.Error())
|
|
} else {
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
err = packages_service.UnlinkFromRepository(ctx, pkg, ctx.Doer)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, util.ErrPermissionDenied):
|
|
ctx.APIError(http.StatusForbidden, err.Error())
|
|
case errors.Is(err, util.ErrInvalidArgument):
|
|
ctx.APIError(http.StatusBadRequest, err.Error())
|
|
default:
|
|
ctx.APIErrorInternal(err)
|
|
}
|
|
return
|
|
}
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func searchPackages(ctx *context.APIContext, opts *packages.PackageSearchOptions) ([]*api.Package, int64, error) {
|
|
pvs, count, err := packages.SearchVersions(ctx, opts)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
pds, err := packages.GetPackageDescriptors(ctx, pvs)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
apiPackages := make([]*api.Package, 0, len(pds))
|
|
for _, pd := range pds {
|
|
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
apiPackages = append(apiPackages, apiPackage)
|
|
}
|
|
|
|
return apiPackages, count, nil
|
|
}
|