diff options
Diffstat (limited to 'routers/api/packages/rubygems')
-rw-r--r-- | routers/api/packages/rubygems/rubygems.go | 451 |
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 +} |