aboutsummaryrefslogtreecommitdiffstats
path: root/routers/api
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2023-05-14 17:38:40 +0200
committerGitHub <noreply@github.com>2023-05-14 23:38:40 +0800
commit5968c63a11c94b0fdde0485af194bebb2ea1b8e7 (patch)
tree8ff60c459712755a7346c4f5ed2e31e464af6cbe /routers/api
parent53a00017bbd89fddab11b323fc39872c44286d96 (diff)
downloadgitea-5968c63a11c94b0fdde0485af194bebb2ea1b8e7.tar.gz
gitea-5968c63a11c94b0fdde0485af194bebb2ea1b8e7.zip
Add Go package registry (#24687)
Fixes #7608 This PR adds a Go package registry usable with the Go proxy protocol. ![grafik](https://github.com/go-gitea/gitea/assets/1666336/328feb5c-3df2-4f9d-8eae-fe3126d14c37)
Diffstat (limited to 'routers/api')
-rw-r--r--routers/api/packages/api.go59
-rw-r--r--routers/api/packages/goproxy/goproxy.go226
-rw-r--r--routers/api/v1/packages/package.go2
3 files changed, 286 insertions, 1 deletions
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index 355387332e..aaceb8a92b 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/routers/api/packages/container"
"code.gitea.io/gitea/routers/api/packages/debian"
"code.gitea.io/gitea/routers/api/packages/generic"
+ "code.gitea.io/gitea/routers/api/packages/goproxy"
"code.gitea.io/gitea/routers/api/packages/helm"
"code.gitea.io/gitea/routers/api/packages/maven"
"code.gitea.io/gitea/routers/api/packages/npm"
@@ -312,6 +313,64 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
}, reqPackageAccess(perm.AccessModeWrite))
})
}, reqPackageAccess(perm.AccessModeRead))
+ r.Group("/go", func() {
+ r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage)
+ r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) {
+ ctx.Status(http.StatusNotFound)
+ })
+
+ // Manual mapping of routes because the package name contains slashes which chi does not support
+ // https://go.dev/ref/mod#goproxy-protocol
+ r.Get("/*", func(ctx *context.Context) {
+ path := ctx.Params("*")
+
+ if strings.HasSuffix(path, "/@latest") {
+ ctx.SetParams("name", path[:len(path)-len("/@latest")])
+ ctx.SetParams("version", "latest")
+
+ goproxy.PackageVersionMetadata(ctx)
+ return
+ }
+
+ parts := strings.SplitN(path, "/@v/", 2)
+ if len(parts) != 2 {
+ ctx.Status(http.StatusNotFound)
+ return
+ }
+
+ ctx.SetParams("name", parts[0])
+
+ // <package/name>/@v/list
+ if parts[1] == "list" {
+ goproxy.EnumeratePackageVersions(ctx)
+ return
+ }
+
+ // <package/name>/@v/<version>.zip
+ if strings.HasSuffix(parts[1], ".zip") {
+ ctx.SetParams("version", parts[1][:len(parts[1])-len(".zip")])
+
+ goproxy.DownloadPackageFile(ctx)
+ return
+ }
+ // <package/name>/@v/<version>.info
+ if strings.HasSuffix(parts[1], ".info") {
+ ctx.SetParams("version", parts[1][:len(parts[1])-len(".info")])
+
+ goproxy.PackageVersionMetadata(ctx)
+ return
+ }
+ // <package/name>/@v/<version>.mod
+ if strings.HasSuffix(parts[1], ".mod") {
+ ctx.SetParams("version", parts[1][:len(parts[1])-len(".mod")])
+
+ goproxy.PackageVersionGoModContent(ctx)
+ return
+ }
+
+ ctx.Status(http.StatusNotFound)
+ })
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/generic", func() {
r.Group("/{packagename}/{packageversion}", func() {
r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage)
diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go
new file mode 100644
index 0000000000..d0bc9c1e98
--- /dev/null
+++ b/routers/api/packages/goproxy/goproxy.go
@@ -0,0 +1,226 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package goproxy
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "sort"
+ "time"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/modules/context"
+ packages_module "code.gitea.io/gitea/modules/packages"
+ goproxy_module "code.gitea.io/gitea/modules/packages/goproxy"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/packages/helper"
+ packages_service "code.gitea.io/gitea/services/packages"
+)
+
+func apiError(ctx *context.Context, status int, obj interface{}) {
+ helper.LogAndProcessError(ctx, status, obj, func(message string) {
+ ctx.PlainText(status, message)
+ })
+}
+
+func EnumeratePackageVersions(ctx *context.Context) {
+ pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeGo, ctx.Params("name"))
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if len(pvs) == 0 {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+
+ sort.Slice(pvs, func(i, j int) bool {
+ return pvs[i].CreatedUnix < pvs[j].CreatedUnix
+ })
+
+ ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
+
+ for _, pv := range pvs {
+ fmt.Fprintln(ctx.Resp, pv.Version)
+ }
+}
+
+func PackageVersionMetadata(ctx *context.Context) {
+ pv, err := resolvePackage(ctx, ctx.Package.Owner.ID, ctx.Params("name"), ctx.Params("version"))
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, struct {
+ Version string `json:"Version"`
+ Time time.Time `json:"Time"`
+ }{
+ Version: pv.Version,
+ Time: pv.CreatedUnix.AsLocalTime(),
+ })
+}
+
+func PackageVersionGoModContent(ctx *context.Context) {
+ pv, err := resolvePackage(ctx, ctx.Package.Owner.ID, ctx.Params("name"), ctx.Params("version"))
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, goproxy_module.PropertyGoMod)
+ if err != nil || len(pps) != 1 {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.PlainText(http.StatusOK, pps[0].Value)
+}
+
+func DownloadPackageFile(ctx *context.Context) {
+ pv, err := resolvePackage(ctx, ctx.Package.Owner.ID, ctx.Params("name"), ctx.Params("version"))
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
+ if err != nil || len(pfs) != 1 {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ s, _, err := packages_service.GetPackageFileStream(ctx, pfs[0])
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ apiError(ctx, http.StatusNotFound, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+ defer s.Close()
+
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pfs[0].Name,
+ LastModified: pfs[0].CreatedUnix.AsLocalTime(),
+ })
+}
+
+func resolvePackage(ctx *context.Context, ownerID int64, name, version string) (*packages_model.PackageVersion, error) {
+ var pv *packages_model.PackageVersion
+
+ if version == "latest" {
+ pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: ownerID,
+ Type: packages_model.TypeGo,
+ Name: packages_model.SearchValue{
+ Value: name,
+ ExactMatch: true,
+ },
+ IsInternal: util.OptionalBoolFalse,
+ Sort: packages_model.SortCreatedDesc,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if len(pvs) != 1 {
+ return nil, packages_model.ErrPackageNotExist
+ }
+
+ pv = pvs[0]
+ } else {
+ var err error
+ pv, err = packages_model.GetVersionByNameAndVersion(ctx, ownerID, packages_model.TypeGo, name, version)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return pv, nil
+}
+
+func UploadPackage(ctx *context.Context) {
+ upload, close, err := ctx.UploadStream()
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if close {
+ defer upload.Close()
+ }
+
+ buf, err := packages_module.CreateHashedBufferFromReader(upload)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer buf.Close()
+
+ pck, err := goproxy_module.ParsePackage(buf, buf.Size())
+ if err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ apiError(ctx, http.StatusBadRequest, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ if _, err := buf.Seek(0, io.SeekStart); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ _, _, err = packages_service.CreatePackageAndAddFile(
+ &packages_service.PackageCreationInfo{
+ PackageInfo: packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeGo,
+ Name: pck.Name,
+ Version: pck.Version,
+ },
+ Creator: ctx.Doer,
+ VersionProperties: map[string]string{
+ goproxy_module.PropertyGoMod: pck.GoMod,
+ },
+ },
+ &packages_service.PackageFileCreationInfo{
+ PackageFileInfo: packages_service.PackageFileInfo{
+ Filename: fmt.Sprintf("%v.zip", pck.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
+ }
+
+ ctx.Status(http.StatusCreated)
+}
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index d7277247fc..0c9a134281 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
// in: query
// description: package type filter
// type: string
- // enum: [alpine, cargo, chef, composer, conan, conda, container, debian, generic, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant]
+ // enum: [alpine, cargo, chef, composer, conan, conda, container, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, vagrant]
// - name: q
// in: query
// description: name filter