code.gitea.io/gitea@v1.21.7/routers/api/packages/rubygems/rubygems.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package rubygems
     5  
     6  import (
     7  	"compress/gzip"
     8  	"compress/zlib"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"strings"
    14  
    15  	packages_model "code.gitea.io/gitea/models/packages"
    16  	"code.gitea.io/gitea/modules/context"
    17  	packages_module "code.gitea.io/gitea/modules/packages"
    18  	rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
    19  	"code.gitea.io/gitea/modules/util"
    20  	"code.gitea.io/gitea/routers/api/packages/helper"
    21  	packages_service "code.gitea.io/gitea/services/packages"
    22  )
    23  
    24  func apiError(ctx *context.Context, status int, obj any) {
    25  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    26  		ctx.PlainText(status, message)
    27  	})
    28  }
    29  
    30  // EnumeratePackages serves the package list
    31  func EnumeratePackages(ctx *context.Context) {
    32  	packages, err := packages_model.GetVersionsByPackageType(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems)
    33  	if err != nil {
    34  		apiError(ctx, http.StatusInternalServerError, err)
    35  		return
    36  	}
    37  
    38  	enumeratePackages(ctx, "specs.4.8", packages)
    39  }
    40  
    41  // EnumeratePackagesLatest serves the list of the latest version of every package
    42  func EnumeratePackagesLatest(ctx *context.Context) {
    43  	pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
    44  		OwnerID:    ctx.Package.Owner.ID,
    45  		Type:       packages_model.TypeRubyGems,
    46  		IsInternal: util.OptionalBoolFalse,
    47  	})
    48  	if err != nil {
    49  		apiError(ctx, http.StatusInternalServerError, err)
    50  		return
    51  	}
    52  
    53  	enumeratePackages(ctx, "latest_specs.4.8", pvs)
    54  }
    55  
    56  // EnumeratePackagesPreRelease is not supported and serves an empty list
    57  func EnumeratePackagesPreRelease(ctx *context.Context) {
    58  	enumeratePackages(ctx, "prerelease_specs.4.8", []*packages_model.PackageVersion{})
    59  }
    60  
    61  func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_model.PackageVersion) {
    62  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
    63  	if err != nil {
    64  		apiError(ctx, http.StatusInternalServerError, err)
    65  		return
    66  	}
    67  
    68  	specs := make([]any, 0, len(pds))
    69  	for _, p := range pds {
    70  		specs = append(specs, []any{
    71  			p.Package.Name,
    72  			&rubygems_module.RubyUserMarshal{
    73  				Name:  "Gem::Version",
    74  				Value: []string{p.Version.Version},
    75  			},
    76  			p.Metadata.(*rubygems_module.Metadata).Platform,
    77  		})
    78  	}
    79  
    80  	ctx.SetServeHeaders(&context.ServeHeaderOptions{
    81  		Filename: filename + ".gz",
    82  	})
    83  
    84  	zw := gzip.NewWriter(ctx.Resp)
    85  	defer zw.Close()
    86  
    87  	zw.Name = filename
    88  
    89  	if err := rubygems_module.NewMarshalEncoder(zw).Encode(specs); err != nil {
    90  		ctx.ServerError("Download file failed", err)
    91  	}
    92  }
    93  
    94  // ServePackageSpecification serves the compressed Gemspec file of a package
    95  func ServePackageSpecification(ctx *context.Context) {
    96  	filename := ctx.Params("filename")
    97  
    98  	if !strings.HasSuffix(filename, ".gemspec.rz") {
    99  		apiError(ctx, http.StatusNotImplemented, nil)
   100  		return
   101  	}
   102  
   103  	pvs, err := getVersionsByFilename(ctx, filename[:len(filename)-10]+"gem")
   104  	if err != nil {
   105  		apiError(ctx, http.StatusInternalServerError, err)
   106  		return
   107  	}
   108  
   109  	if len(pvs) != 1 {
   110  		apiError(ctx, http.StatusNotFound, nil)
   111  		return
   112  	}
   113  
   114  	pd, err := packages_model.GetPackageDescriptor(ctx, pvs[0])
   115  	if err != nil {
   116  		apiError(ctx, http.StatusInternalServerError, err)
   117  		return
   118  	}
   119  
   120  	ctx.SetServeHeaders(&context.ServeHeaderOptions{
   121  		Filename: filename,
   122  	})
   123  
   124  	zw := zlib.NewWriter(ctx.Resp)
   125  	defer zw.Close()
   126  
   127  	metadata := pd.Metadata.(*rubygems_module.Metadata)
   128  
   129  	// create a Ruby Gem::Specification object
   130  	spec := &rubygems_module.RubyUserDef{
   131  		Name: "Gem::Specification",
   132  		Value: []any{
   133  			"3.2.3", // @rubygems_version
   134  			4,       // @specification_version,
   135  			pd.Package.Name,
   136  			&rubygems_module.RubyUserMarshal{
   137  				Name:  "Gem::Version",
   138  				Value: []string{pd.Version.Version},
   139  			},
   140  			nil,               // date
   141  			metadata.Summary,  // @summary
   142  			nil,               // @required_ruby_version
   143  			nil,               // @required_rubygems_version
   144  			metadata.Platform, // @original_platform
   145  			[]any{},           // @dependencies
   146  			nil,               // rubyforge_project
   147  			"",                // @email
   148  			metadata.Authors,
   149  			metadata.Description,
   150  			metadata.ProjectURL,
   151  			true,              // has_rdoc
   152  			metadata.Platform, // @new_platform
   153  			nil,
   154  			metadata.Licenses,
   155  		},
   156  	}
   157  
   158  	if err := rubygems_module.NewMarshalEncoder(zw).Encode(spec); err != nil {
   159  		ctx.ServerError("Download file failed", err)
   160  	}
   161  }
   162  
   163  // DownloadPackageFile serves the content of a package
   164  func DownloadPackageFile(ctx *context.Context) {
   165  	filename := ctx.Params("filename")
   166  
   167  	pvs, err := getVersionsByFilename(ctx, filename)
   168  	if err != nil {
   169  		apiError(ctx, http.StatusInternalServerError, err)
   170  		return
   171  	}
   172  
   173  	if len(pvs) != 1 {
   174  		apiError(ctx, http.StatusNotFound, nil)
   175  		return
   176  	}
   177  
   178  	s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
   179  		ctx,
   180  		pvs[0],
   181  		&packages_service.PackageFileInfo{
   182  			Filename: filename,
   183  		},
   184  	)
   185  	if err != nil {
   186  		if err == packages_model.ErrPackageFileNotExist {
   187  			apiError(ctx, http.StatusNotFound, err)
   188  			return
   189  		}
   190  		apiError(ctx, http.StatusInternalServerError, err)
   191  		return
   192  	}
   193  
   194  	helper.ServePackageFile(ctx, s, u, pf)
   195  }
   196  
   197  // UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
   198  func UploadPackageFile(ctx *context.Context) {
   199  	upload, close, err := ctx.UploadStream()
   200  	if err != nil {
   201  		apiError(ctx, http.StatusBadRequest, err)
   202  		return
   203  	}
   204  	if close {
   205  		defer upload.Close()
   206  	}
   207  
   208  	buf, err := packages_module.CreateHashedBufferFromReader(upload)
   209  	if err != nil {
   210  		apiError(ctx, http.StatusInternalServerError, err)
   211  		return
   212  	}
   213  	defer buf.Close()
   214  
   215  	rp, err := rubygems_module.ParsePackageMetaData(buf)
   216  	if err != nil {
   217  		if errors.Is(err, util.ErrInvalidArgument) {
   218  			apiError(ctx, http.StatusBadRequest, err)
   219  		} else {
   220  			apiError(ctx, http.StatusInternalServerError, err)
   221  		}
   222  		return
   223  	}
   224  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
   225  		apiError(ctx, http.StatusInternalServerError, err)
   226  		return
   227  	}
   228  
   229  	var filename string
   230  	if rp.Metadata.Platform == "" || rp.Metadata.Platform == "ruby" {
   231  		filename = strings.ToLower(fmt.Sprintf("%s-%s.gem", rp.Name, rp.Version))
   232  	} else {
   233  		filename = strings.ToLower(fmt.Sprintf("%s-%s-%s.gem", rp.Name, rp.Version, rp.Metadata.Platform))
   234  	}
   235  
   236  	_, _, err = packages_service.CreatePackageAndAddFile(
   237  		ctx,
   238  		&packages_service.PackageCreationInfo{
   239  			PackageInfo: packages_service.PackageInfo{
   240  				Owner:       ctx.Package.Owner,
   241  				PackageType: packages_model.TypeRubyGems,
   242  				Name:        rp.Name,
   243  				Version:     rp.Version,
   244  			},
   245  			SemverCompatible: true,
   246  			Creator:          ctx.Doer,
   247  			Metadata:         rp.Metadata,
   248  		},
   249  		&packages_service.PackageFileCreationInfo{
   250  			PackageFileInfo: packages_service.PackageFileInfo{
   251  				Filename: filename,
   252  			},
   253  			Creator: ctx.Doer,
   254  			Data:    buf,
   255  			IsLead:  true,
   256  		},
   257  	)
   258  	if err != nil {
   259  		switch err {
   260  		case packages_model.ErrDuplicatePackageVersion:
   261  			apiError(ctx, http.StatusBadRequest, err)
   262  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   263  			apiError(ctx, http.StatusForbidden, err)
   264  		default:
   265  			apiError(ctx, http.StatusInternalServerError, err)
   266  		}
   267  		return
   268  	}
   269  
   270  	ctx.Status(http.StatusCreated)
   271  }
   272  
   273  // DeletePackage deletes a package
   274  func DeletePackage(ctx *context.Context) {
   275  	// Go populates the form only for POST, PUT and PATCH requests
   276  	if err := ctx.Req.ParseMultipartForm(32 << 20); err != nil {
   277  		apiError(ctx, http.StatusInternalServerError, err)
   278  		return
   279  	}
   280  	packageName := ctx.FormString("gem_name")
   281  	packageVersion := ctx.FormString("version")
   282  
   283  	err := packages_service.RemovePackageVersionByNameAndVersion(
   284  		ctx,
   285  		ctx.Doer,
   286  		&packages_service.PackageInfo{
   287  			Owner:       ctx.Package.Owner,
   288  			PackageType: packages_model.TypeRubyGems,
   289  			Name:        packageName,
   290  			Version:     packageVersion,
   291  		},
   292  	)
   293  	if err != nil {
   294  		if err == packages_model.ErrPackageNotExist {
   295  			apiError(ctx, http.StatusNotFound, err)
   296  			return
   297  		}
   298  		apiError(ctx, http.StatusInternalServerError, err)
   299  	}
   300  }
   301  
   302  func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_model.PackageVersion, error) {
   303  	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
   304  		OwnerID:         ctx.Package.Owner.ID,
   305  		Type:            packages_model.TypeRubyGems,
   306  		HasFileWithName: filename,
   307  		IsInternal:      util.OptionalBoolFalse,
   308  	})
   309  	return pvs, err
   310  }