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

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package composer
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	packages_model "code.gitea.io/gitea/models/packages"
    17  	"code.gitea.io/gitea/modules/context"
    18  	packages_module "code.gitea.io/gitea/modules/packages"
    19  	composer_module "code.gitea.io/gitea/modules/packages/composer"
    20  	"code.gitea.io/gitea/modules/setting"
    21  	"code.gitea.io/gitea/modules/util"
    22  	"code.gitea.io/gitea/routers/api/packages/helper"
    23  	"code.gitea.io/gitea/services/convert"
    24  	packages_service "code.gitea.io/gitea/services/packages"
    25  
    26  	"github.com/hashicorp/go-version"
    27  )
    28  
    29  func apiError(ctx *context.Context, status int, obj any) {
    30  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    31  		type Error struct {
    32  			Status  int    `json:"status"`
    33  			Message string `json:"message"`
    34  		}
    35  		ctx.JSON(status, struct {
    36  			Errors []Error `json:"errors"`
    37  		}{
    38  			Errors: []Error{
    39  				{Status: status, Message: message},
    40  			},
    41  		})
    42  	})
    43  }
    44  
    45  // ServiceIndex displays registry endpoints
    46  func ServiceIndex(ctx *context.Context) {
    47  	resp := createServiceIndexResponse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/composer")
    48  
    49  	ctx.JSON(http.StatusOK, resp)
    50  }
    51  
    52  // SearchPackages searches packages, only "q" is supported
    53  // https://packagist.org/apidoc#search-packages
    54  func SearchPackages(ctx *context.Context) {
    55  	page := ctx.FormInt("page")
    56  	if page < 1 {
    57  		page = 1
    58  	}
    59  	perPage := ctx.FormInt("per_page")
    60  	paginator := db.ListOptions{
    61  		Page:     page,
    62  		PageSize: convert.ToCorrectPageSize(perPage),
    63  	}
    64  
    65  	opts := &packages_model.PackageSearchOptions{
    66  		OwnerID:    ctx.Package.Owner.ID,
    67  		Type:       packages_model.TypeComposer,
    68  		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
    69  		IsInternal: util.OptionalBoolFalse,
    70  		Paginator:  &paginator,
    71  	}
    72  	if ctx.FormTrim("type") != "" {
    73  		opts.Properties = map[string]string{
    74  			composer_module.TypeProperty: ctx.FormTrim("type"),
    75  		}
    76  	}
    77  
    78  	pvs, total, err := packages_model.SearchLatestVersions(ctx, opts)
    79  	if err != nil {
    80  		apiError(ctx, http.StatusInternalServerError, err)
    81  		return
    82  	}
    83  
    84  	nextLink := ""
    85  	if len(pvs) == paginator.PageSize {
    86  		u, err := url.Parse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/composer/search.json")
    87  		if err != nil {
    88  			apiError(ctx, http.StatusInternalServerError, err)
    89  			return
    90  		}
    91  		q := u.Query()
    92  		q.Set("q", ctx.FormTrim("q"))
    93  		q.Set("type", ctx.FormTrim("type"))
    94  		q.Set("page", strconv.Itoa(page+1))
    95  		if perPage != 0 {
    96  			q.Set("per_page", strconv.Itoa(perPage))
    97  		}
    98  		u.RawQuery = q.Encode()
    99  
   100  		nextLink = u.String()
   101  	}
   102  
   103  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   104  	if err != nil {
   105  		apiError(ctx, http.StatusInternalServerError, err)
   106  		return
   107  	}
   108  
   109  	resp := createSearchResultResponse(total, pds, nextLink)
   110  
   111  	ctx.JSON(http.StatusOK, resp)
   112  }
   113  
   114  // EnumeratePackages lists all package names
   115  // https://packagist.org/apidoc#list-packages
   116  func EnumeratePackages(ctx *context.Context) {
   117  	ps, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeComposer)
   118  	if err != nil {
   119  		apiError(ctx, http.StatusInternalServerError, err)
   120  		return
   121  	}
   122  
   123  	names := make([]string, 0, len(ps))
   124  	for _, p := range ps {
   125  		names = append(names, p.Name)
   126  	}
   127  
   128  	ctx.JSON(http.StatusOK, map[string][]string{
   129  		"packageNames": names,
   130  	})
   131  }
   132  
   133  // PackageMetadata returns the metadata for a single package
   134  // https://packagist.org/apidoc#get-package-data
   135  func PackageMetadata(ctx *context.Context) {
   136  	vendorName := ctx.Params("vendorname")
   137  	projectName := ctx.Params("projectname")
   138  
   139  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeComposer, vendorName+"/"+projectName)
   140  	if err != nil {
   141  		apiError(ctx, http.StatusInternalServerError, err)
   142  		return
   143  	}
   144  	if len(pvs) == 0 {
   145  		apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist)
   146  		return
   147  	}
   148  
   149  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   150  	if err != nil {
   151  		apiError(ctx, http.StatusInternalServerError, err)
   152  		return
   153  	}
   154  
   155  	resp := createPackageMetadataResponse(
   156  		setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer",
   157  		pds,
   158  	)
   159  
   160  	ctx.JSON(http.StatusOK, resp)
   161  }
   162  
   163  // DownloadPackageFile serves the content of a package
   164  func DownloadPackageFile(ctx *context.Context) {
   165  	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
   166  		ctx,
   167  		&packages_service.PackageInfo{
   168  			Owner:       ctx.Package.Owner,
   169  			PackageType: packages_model.TypeComposer,
   170  			Name:        ctx.Params("package"),
   171  			Version:     ctx.Params("version"),
   172  		},
   173  		&packages_service.PackageFileInfo{
   174  			Filename: ctx.Params("filename"),
   175  		},
   176  	)
   177  	if err != nil {
   178  		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
   179  			apiError(ctx, http.StatusNotFound, err)
   180  			return
   181  		}
   182  		apiError(ctx, http.StatusInternalServerError, err)
   183  		return
   184  	}
   185  
   186  	helper.ServePackageFile(ctx, s, u, pf)
   187  }
   188  
   189  // UploadPackage creates a new package
   190  func UploadPackage(ctx *context.Context) {
   191  	buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
   192  	if err != nil {
   193  		apiError(ctx, http.StatusInternalServerError, err)
   194  		return
   195  	}
   196  	defer buf.Close()
   197  
   198  	cp, err := composer_module.ParsePackage(buf, buf.Size())
   199  	if err != nil {
   200  		if errors.Is(err, util.ErrInvalidArgument) {
   201  			apiError(ctx, http.StatusBadRequest, err)
   202  		} else {
   203  			apiError(ctx, http.StatusInternalServerError, err)
   204  		}
   205  		return
   206  	}
   207  
   208  	if _, err := buf.Seek(0, io.SeekStart); err != nil {
   209  		apiError(ctx, http.StatusInternalServerError, err)
   210  		return
   211  	}
   212  
   213  	if cp.Version == "" {
   214  		v, err := version.NewVersion(ctx.FormTrim("version"))
   215  		if err != nil {
   216  			apiError(ctx, http.StatusBadRequest, composer_module.ErrInvalidVersion)
   217  			return
   218  		}
   219  		cp.Version = v.String()
   220  	}
   221  
   222  	_, _, err = packages_service.CreatePackageAndAddFile(
   223  		ctx,
   224  		&packages_service.PackageCreationInfo{
   225  			PackageInfo: packages_service.PackageInfo{
   226  				Owner:       ctx.Package.Owner,
   227  				PackageType: packages_model.TypeComposer,
   228  				Name:        cp.Name,
   229  				Version:     cp.Version,
   230  			},
   231  			SemverCompatible: true,
   232  			Creator:          ctx.Doer,
   233  			Metadata:         cp.Metadata,
   234  			VersionProperties: map[string]string{
   235  				composer_module.TypeProperty: cp.Type,
   236  			},
   237  		},
   238  		&packages_service.PackageFileCreationInfo{
   239  			PackageFileInfo: packages_service.PackageFileInfo{
   240  				Filename: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)),
   241  			},
   242  			Creator: ctx.Doer,
   243  			Data:    buf,
   244  			IsLead:  true,
   245  		},
   246  	)
   247  	if err != nil {
   248  		switch err {
   249  		case packages_model.ErrDuplicatePackageVersion:
   250  			apiError(ctx, http.StatusBadRequest, err)
   251  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   252  			apiError(ctx, http.StatusForbidden, err)
   253  		default:
   254  			apiError(ctx, http.StatusInternalServerError, err)
   255  		}
   256  		return
   257  	}
   258  
   259  	ctx.Status(http.StatusCreated)
   260  }