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