summaryrefslogtreecommitdiffstats
path: root/routers/api/packages/rubygems
diff options
context:
space:
mode:
Diffstat (limited to 'routers/api/packages/rubygems')
-rw-r--r--routers/api/packages/rubygems/rubygems.go451
1 files changed, 451 insertions, 0 deletions
diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go
new file mode 100644
index 0000000..dfefe2c
--- /dev/null
+++ b/routers/api/packages/rubygems/rubygems.go
@@ -0,0 +1,451 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package rubygems
+
+import (
+ "compress/gzip"
+ "compress/zlib"
+ "crypto/md5"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/modules/optional"
+ packages_module "code.gitea.io/gitea/modules/packages"
+ rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
+ packages_service "code.gitea.io/gitea/services/packages"
+)
+
+const (
+ Sep = "---\n"
+)
+
+func apiError(ctx *context.Context, status int, obj any) {
+ helper.LogAndProcessError(ctx, status, obj, func(message string) {
+ ctx.PlainText(status, message)
+ })
+}
+
+// EnumeratePackages serves the package list
+func EnumeratePackages(ctx *context.Context) {
+ packages, err := packages_model.GetVersionsByPackageType(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ enumeratePackages(ctx, "specs.4.8", packages)
+}
+
+// EnumeratePackagesLatest serves the list of the latest version of every package
+func EnumeratePackagesLatest(ctx *context.Context) {
+ pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeRubyGems,
+ IsInternal: optional.Some(false),
+ })
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ enumeratePackages(ctx, "latest_specs.4.8", pvs)
+}
+
+// EnumeratePackagesPreRelease is not supported and serves an empty list
+func EnumeratePackagesPreRelease(ctx *context.Context) {
+ enumeratePackages(ctx, "prerelease_specs.4.8", []*packages_model.PackageVersion{})
+}
+
+func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_model.PackageVersion) {
+ pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ specs := make([]any, 0, len(pds))
+ for _, p := range pds {
+ specs = append(specs, []any{
+ p.Package.Name,
+ &rubygems_module.RubyUserMarshal{
+ Name: "Gem::Version",
+ Value: []string{p.Version.Version},
+ },
+ p.Metadata.(*rubygems_module.Metadata).Platform,
+ })
+ }
+
+ ctx.SetServeHeaders(&context.ServeHeaderOptions{
+ Filename: filename + ".gz",
+ })
+
+ zw := gzip.NewWriter(ctx.Resp)
+ defer zw.Close()
+
+ zw.Name = filename
+
+ if err := rubygems_module.NewMarshalEncoder(zw).Encode(specs); err != nil {
+ ctx.ServerError("Download file failed", err)
+ }
+}
+
+// Serves info file for rubygems.org compatible /info/{gem} file.
+// See also https://guides.rubygems.org/rubygems-org-compact-index-api/.
+func ServePackageInfo(ctx *context.Context) {
+ packageName := ctx.Params("package")
+ versions, err := packages_model.GetVersionsByPackageName(
+ ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems, packageName)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ if len(versions) == 0 {
+ apiError(ctx, http.StatusNotFound, fmt.Sprintf("Could not find package %s", packageName))
+ }
+
+ result, err := buildInfoFileForPackage(ctx, versions)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.PlainText(http.StatusOK, *result)
+}
+
+// ServeVersionsFile creates rubygems.org compatible /versions file.
+// See also https://guides.rubygems.org/rubygems-org-compact-index-api/.
+func ServeVersionsFile(ctx *context.Context) {
+ packages, err := packages_model.GetPackagesByType(
+ ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ result := new(strings.Builder)
+ result.WriteString(Sep)
+ for _, pack := range packages {
+ versions, err := packages_model.GetVersionsByPackageName(
+ ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems, pack.Name)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ if len(versions) == 0 {
+ // No versions left for this package, we should continue.
+ continue
+ }
+
+ fmt.Fprintf(result, "%s ", pack.Name)
+ for i, v := range versions {
+ result.WriteString(v.Version)
+ if i != len(versions)-1 {
+ result.WriteString(",")
+ }
+ }
+
+ info, err := buildInfoFileForPackage(ctx, versions)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+
+ checksum := md5.Sum([]byte(*info))
+ fmt.Fprintf(result, " %x\n", checksum)
+ }
+ ctx.PlainText(http.StatusOK, result.String())
+}
+
+// ServePackageSpecification serves the compressed Gemspec file of a package
+func ServePackageSpecification(ctx *context.Context) {
+ filename := ctx.Params("filename")
+
+ if !strings.HasSuffix(filename, ".gemspec.rz") {
+ apiError(ctx, http.StatusNotImplemented, nil)
+ return
+ }
+
+ pvs, err := getVersionsByFilename(ctx, filename[:len(filename)-10]+"gem")
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(pvs) != 1 {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
+
+ pd, err := packages_model.GetPackageDescriptor(ctx, pvs[0])
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.SetServeHeaders(&context.ServeHeaderOptions{
+ Filename: filename,
+ })
+
+ zw := zlib.NewWriter(ctx.Resp)
+ defer zw.Close()
+
+ metadata := pd.Metadata.(*rubygems_module.Metadata)
+
+ // create a Ruby Gem::Specification object
+ spec := &rubygems_module.RubyUserDef{
+ Name: "Gem::Specification",
+ Value: []any{
+ "3.2.3", // @rubygems_version
+ 4, // @specification_version,
+ pd.Package.Name,
+ &rubygems_module.RubyUserMarshal{
+ Name: "Gem::Version",
+ Value: []string{pd.Version.Version},
+ },
+ nil, // date
+ metadata.Summary, // @summary
+ nil, // @required_ruby_version
+ nil, // @required_rubygems_version
+ metadata.Platform, // @original_platform
+ []any{}, // @dependencies
+ nil, // rubyforge_project
+ "", // @email
+ metadata.Authors,
+ metadata.Description,
+ metadata.ProjectURL,
+ true, // has_rdoc
+ metadata.Platform, // @new_platform
+ nil,
+ metadata.Licenses,
+ },
+ }
+
+ if err := rubygems_module.NewMarshalEncoder(zw).Encode(spec); err != nil {
+ ctx.ServerError("Download file failed", err)
+ }
+}
+
+// DownloadPackageFile serves the content of a package
+func DownloadPackageFile(ctx *context.Context) {
+ filename := ctx.Params("filename")
+
+ pvs, err := getVersionsByFilename(ctx, filename)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(pvs) != 1 {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
+
+ s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
+ ctx,
+ pvs[0],
+ &packages_service.PackageFileInfo{
+ Filename: filename,
+ },
+ )
+ if err != nil {
+ if err == packages_model.ErrPackageFileNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ helper.ServePackageFile(ctx, s, u, pf)
+}
+
+// UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
+func UploadPackageFile(ctx *context.Context) {
+ upload, needToClose, err := ctx.UploadStream()
+ if err != nil {
+ apiError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ if needToClose {
+ defer upload.Close()
+ }
+
+ buf, err := packages_module.CreateHashedBufferFromReader(upload)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer buf.Close()
+
+ rp, err := rubygems_module.ParsePackageMetaData(buf)
+ 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
+ }
+
+ filename := getFullFilename(rp.Name, rp.Version, rp.Metadata.Platform)
+
+ _, _, err = packages_service.CreatePackageAndAddFile(
+ ctx,
+ &packages_service.PackageCreationInfo{
+ PackageInfo: packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeRubyGems,
+ Name: rp.Name,
+ Version: rp.Version,
+ },
+ SemverCompatible: true,
+ Creator: ctx.Doer,
+ Metadata: rp.Metadata,
+ },
+ &packages_service.PackageFileCreationInfo{
+ PackageFileInfo: packages_service.PackageFileInfo{
+ Filename: filename,
+ },
+ 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)
+}
+
+// DeletePackage deletes a package
+func DeletePackage(ctx *context.Context) {
+ // Go populates the form only for POST, PUT and PATCH requests
+ if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ packageName := ctx.FormString("gem_name")
+ packageVersion := ctx.FormString("version")
+
+ err := packages_service.RemovePackageVersionByNameAndVersion(
+ ctx,
+ ctx.Doer,
+ &packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeRubyGems,
+ Name: packageName,
+ Version: packageVersion,
+ },
+ )
+ if err != nil {
+ if err == packages_model.ErrPackageNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+}
+
+func writeRequirements(reqs []rubygems_module.VersionRequirement, result *strings.Builder) {
+ if len(reqs) == 0 {
+ reqs = []rubygems_module.VersionRequirement{{Restriction: ">=", Version: "0"}}
+ }
+ for i, req := range reqs {
+ if i != 0 {
+ result.WriteString("&")
+ }
+ result.WriteString(req.Restriction)
+ result.WriteString(" ")
+ result.WriteString(req.Version)
+ }
+}
+
+func buildRequirementStringFromVersion(ctx *context.Context, version *packages_model.PackageVersion) (string, error) {
+ pd, err := packages_model.GetPackageDescriptor(ctx, version)
+ if err != nil {
+ return "", err
+ }
+ metadata := pd.Metadata.(*rubygems_module.Metadata)
+ dependencyRequirements := new(strings.Builder)
+ for i, dep := range metadata.RuntimeDependencies {
+ if i != 0 {
+ dependencyRequirements.WriteString(",")
+ }
+
+ dependencyRequirements.WriteString(dep.Name)
+ dependencyRequirements.WriteString(":")
+ reqs := dep.Version
+ writeRequirements(reqs, dependencyRequirements)
+ }
+ fullname := getFullFilename(pd.Package.Name, version.Version, metadata.Platform)
+ file, err := packages_model.GetFileForVersionByName(ctx, version.ID, fullname, "")
+ if err != nil {
+ return "", err
+ }
+ blob, err := packages_model.GetBlobByID(ctx, file.BlobID)
+ if err != nil {
+ return "", err
+ }
+ additionalRequirements := new(strings.Builder)
+ fmt.Fprintf(additionalRequirements, "checksum:%s", blob.HashSHA256)
+ if len(metadata.RequiredRubyVersion) != 0 {
+ additionalRequirements.WriteString(",ruby:")
+ writeRequirements(metadata.RequiredRubyVersion, additionalRequirements)
+ }
+ if len(metadata.RequiredRubygemsVersion) != 0 {
+ additionalRequirements.WriteString(",rubygems:")
+ writeRequirements(metadata.RequiredRubygemsVersion, additionalRequirements)
+ }
+ return fmt.Sprintf("%s %s|%s", version.Version, dependencyRequirements, additionalRequirements), nil
+}
+
+func buildInfoFileForPackage(ctx *context.Context, versions []*packages_model.PackageVersion) (*string, error) {
+ result := "---\n"
+ for _, v := range versions {
+ str, err := buildRequirementStringFromVersion(ctx, v)
+ if err != nil {
+ return nil, err
+ }
+ result += str
+ result += "\n"
+ }
+ return &result, nil
+}
+
+func getFullFilename(gemName, version, platform string) string {
+ return strings.ToLower(getFullName(gemName, version, platform)) + ".gem"
+}
+
+func getFullName(gemName, version, platform string) string {
+ if platform == "" || platform == "ruby" {
+ return fmt.Sprintf("%s-%s", gemName, version)
+ }
+ return fmt.Sprintf("%s-%s-%s", gemName, version, platform)
+}
+
+func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_model.PackageVersion, error) {
+ pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeRubyGems,
+ HasFileWithName: filename,
+ IsInternal: optional.Some(false),
+ })
+ return pvs, err
+}