code.gitea.io/gitea@v1.22.3/routers/api/packages/npm/npm.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package npm
     5  
     6  import (
     7  	"bytes"
     8  	std_ctx "context"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"strings"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	packages_model "code.gitea.io/gitea/models/packages"
    17  	access_model "code.gitea.io/gitea/models/perm/access"
    18  	repo_model "code.gitea.io/gitea/models/repo"
    19  	"code.gitea.io/gitea/models/unit"
    20  	"code.gitea.io/gitea/modules/optional"
    21  	packages_module "code.gitea.io/gitea/modules/packages"
    22  	npm_module "code.gitea.io/gitea/modules/packages/npm"
    23  	"code.gitea.io/gitea/modules/setting"
    24  	"code.gitea.io/gitea/modules/util"
    25  	"code.gitea.io/gitea/routers/api/packages/helper"
    26  	"code.gitea.io/gitea/services/context"
    27  	packages_service "code.gitea.io/gitea/services/packages"
    28  
    29  	"github.com/hashicorp/go-version"
    30  )
    31  
    32  // errInvalidTagName indicates an invalid tag name
    33  var errInvalidTagName = errors.New("The tag name is invalid")
    34  
    35  func apiError(ctx *context.Context, status int, obj any) {
    36  	helper.LogAndProcessError(ctx, status, obj, func(message string) {
    37  		ctx.JSON(status, map[string]string{
    38  			"error": message,
    39  		})
    40  	})
    41  }
    42  
    43  // packageNameFromParams gets the package name from the url parameters
    44  // Variations: /name/, /@scope/name/, /@scope%2Fname/
    45  func packageNameFromParams(ctx *context.Context) string {
    46  	scope := ctx.Params("scope")
    47  	id := ctx.Params("id")
    48  	if scope != "" {
    49  		return fmt.Sprintf("@%s/%s", scope, id)
    50  	}
    51  	return id
    52  }
    53  
    54  // PackageMetadata returns the metadata for a single package
    55  func PackageMetadata(ctx *context.Context) {
    56  	packageName := packageNameFromParams(ctx)
    57  
    58  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
    59  	if err != nil {
    60  		apiError(ctx, http.StatusInternalServerError, err)
    61  		return
    62  	}
    63  	if len(pvs) == 0 {
    64  		apiError(ctx, http.StatusNotFound, err)
    65  		return
    66  	}
    67  
    68  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
    69  	if err != nil {
    70  		apiError(ctx, http.StatusInternalServerError, err)
    71  		return
    72  	}
    73  
    74  	resp := createPackageMetadataResponse(
    75  		setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/npm",
    76  		pds,
    77  	)
    78  
    79  	ctx.JSON(http.StatusOK, resp)
    80  }
    81  
    82  // DownloadPackageFile serves the content of a package
    83  func DownloadPackageFile(ctx *context.Context) {
    84  	packageName := packageNameFromParams(ctx)
    85  	packageVersion := ctx.Params("version")
    86  	filename := ctx.Params("filename")
    87  
    88  	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
    89  		ctx,
    90  		&packages_service.PackageInfo{
    91  			Owner:       ctx.Package.Owner,
    92  			PackageType: packages_model.TypeNpm,
    93  			Name:        packageName,
    94  			Version:     packageVersion,
    95  		},
    96  		&packages_service.PackageFileInfo{
    97  			Filename: filename,
    98  		},
    99  	)
   100  	if err != nil {
   101  		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
   102  			apiError(ctx, http.StatusNotFound, err)
   103  			return
   104  		}
   105  		apiError(ctx, http.StatusInternalServerError, err)
   106  		return
   107  	}
   108  
   109  	helper.ServePackageFile(ctx, s, u, pf)
   110  }
   111  
   112  // DownloadPackageFileByName finds the version and serves the contents of a package
   113  func DownloadPackageFileByName(ctx *context.Context) {
   114  	filename := ctx.Params("filename")
   115  
   116  	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
   117  		OwnerID: ctx.Package.Owner.ID,
   118  		Type:    packages_model.TypeNpm,
   119  		Name: packages_model.SearchValue{
   120  			ExactMatch: true,
   121  			Value:      packageNameFromParams(ctx),
   122  		},
   123  		HasFileWithName: filename,
   124  		IsInternal:      optional.Some(false),
   125  	})
   126  	if err != nil {
   127  		apiError(ctx, http.StatusInternalServerError, err)
   128  		return
   129  	}
   130  	if len(pvs) != 1 {
   131  		apiError(ctx, http.StatusNotFound, nil)
   132  		return
   133  	}
   134  
   135  	s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
   136  		ctx,
   137  		pvs[0],
   138  		&packages_service.PackageFileInfo{
   139  			Filename: filename,
   140  		},
   141  	)
   142  	if err != nil {
   143  		if err == packages_model.ErrPackageFileNotExist {
   144  			apiError(ctx, http.StatusNotFound, err)
   145  			return
   146  		}
   147  		apiError(ctx, http.StatusInternalServerError, err)
   148  		return
   149  	}
   150  
   151  	helper.ServePackageFile(ctx, s, u, pf)
   152  }
   153  
   154  // UploadPackage creates a new package
   155  func UploadPackage(ctx *context.Context) {
   156  	npmPackage, err := npm_module.ParsePackage(ctx.Req.Body)
   157  	if err != nil {
   158  		if errors.Is(err, util.ErrInvalidArgument) {
   159  			apiError(ctx, http.StatusBadRequest, err)
   160  		} else {
   161  			apiError(ctx, http.StatusInternalServerError, err)
   162  		}
   163  		return
   164  	}
   165  
   166  	repo, err := repo_model.GetRepositoryByURL(ctx, npmPackage.Metadata.Repository.URL)
   167  	if err == nil {
   168  		canWrite := repo.OwnerID == ctx.Doer.ID
   169  
   170  		if !canWrite {
   171  			perms, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
   172  			if err != nil {
   173  				apiError(ctx, http.StatusInternalServerError, err)
   174  				return
   175  			}
   176  
   177  			canWrite = perms.CanWrite(unit.TypePackages)
   178  		}
   179  
   180  		if !canWrite {
   181  			apiError(ctx, http.StatusForbidden, "no permission to upload this package")
   182  			return
   183  		}
   184  	}
   185  
   186  	buf, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(npmPackage.Data))
   187  	if err != nil {
   188  		apiError(ctx, http.StatusInternalServerError, err)
   189  		return
   190  	}
   191  	defer buf.Close()
   192  
   193  	pv, _, err := packages_service.CreatePackageAndAddFile(
   194  		ctx,
   195  		&packages_service.PackageCreationInfo{
   196  			PackageInfo: packages_service.PackageInfo{
   197  				Owner:       ctx.Package.Owner,
   198  				PackageType: packages_model.TypeNpm,
   199  				Name:        npmPackage.Name,
   200  				Version:     npmPackage.Version,
   201  			},
   202  			SemverCompatible: true,
   203  			Creator:          ctx.Doer,
   204  			Metadata:         npmPackage.Metadata,
   205  		},
   206  		&packages_service.PackageFileCreationInfo{
   207  			PackageFileInfo: packages_service.PackageFileInfo{
   208  				Filename: npmPackage.Filename,
   209  			},
   210  			Creator: ctx.Doer,
   211  			Data:    buf,
   212  			IsLead:  true,
   213  		},
   214  	)
   215  	if err != nil {
   216  		switch err {
   217  		case packages_model.ErrDuplicatePackageVersion:
   218  			apiError(ctx, http.StatusConflict, err)
   219  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   220  			apiError(ctx, http.StatusForbidden, err)
   221  		default:
   222  			apiError(ctx, http.StatusInternalServerError, err)
   223  		}
   224  		return
   225  	}
   226  
   227  	for _, tag := range npmPackage.DistTags {
   228  		if err := setPackageTag(ctx, tag, pv, false); err != nil {
   229  			if err == errInvalidTagName {
   230  				apiError(ctx, http.StatusBadRequest, err)
   231  				return
   232  			}
   233  			apiError(ctx, http.StatusInternalServerError, err)
   234  			return
   235  		}
   236  	}
   237  
   238  	if repo != nil {
   239  		if err := packages_model.SetRepositoryLink(ctx, pv.PackageID, repo.ID); err != nil {
   240  			apiError(ctx, http.StatusInternalServerError, err)
   241  			return
   242  		}
   243  	}
   244  
   245  	ctx.Status(http.StatusCreated)
   246  }
   247  
   248  // DeletePreview does nothing
   249  // The client tells the server what package version it knows about after deleting a version.
   250  func DeletePreview(ctx *context.Context) {
   251  	ctx.Status(http.StatusOK)
   252  }
   253  
   254  // DeletePackageVersion deletes the package version
   255  func DeletePackageVersion(ctx *context.Context) {
   256  	packageName := packageNameFromParams(ctx)
   257  	packageVersion := ctx.Params("version")
   258  
   259  	err := packages_service.RemovePackageVersionByNameAndVersion(
   260  		ctx,
   261  		ctx.Doer,
   262  		&packages_service.PackageInfo{
   263  			Owner:       ctx.Package.Owner,
   264  			PackageType: packages_model.TypeNpm,
   265  			Name:        packageName,
   266  			Version:     packageVersion,
   267  		},
   268  	)
   269  	if err != nil {
   270  		if err == packages_model.ErrPackageNotExist {
   271  			apiError(ctx, http.StatusNotFound, err)
   272  			return
   273  		}
   274  		apiError(ctx, http.StatusInternalServerError, err)
   275  		return
   276  	}
   277  
   278  	ctx.Status(http.StatusOK)
   279  }
   280  
   281  // DeletePackage deletes the package and all versions
   282  func DeletePackage(ctx *context.Context) {
   283  	packageName := packageNameFromParams(ctx)
   284  
   285  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
   286  	if err != nil {
   287  		apiError(ctx, http.StatusInternalServerError, err)
   288  		return
   289  	}
   290  
   291  	if len(pvs) == 0 {
   292  		apiError(ctx, http.StatusNotFound, err)
   293  		return
   294  	}
   295  
   296  	for _, pv := range pvs {
   297  		if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
   298  			apiError(ctx, http.StatusInternalServerError, err)
   299  			return
   300  		}
   301  	}
   302  
   303  	ctx.Status(http.StatusOK)
   304  }
   305  
   306  // ListPackageTags returns all tags for a package
   307  func ListPackageTags(ctx *context.Context) {
   308  	packageName := packageNameFromParams(ctx)
   309  
   310  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
   311  	if err != nil {
   312  		apiError(ctx, http.StatusInternalServerError, err)
   313  		return
   314  	}
   315  
   316  	tags := make(map[string]string)
   317  	for _, pv := range pvs {
   318  		pvps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, npm_module.TagProperty)
   319  		if err != nil {
   320  			apiError(ctx, http.StatusInternalServerError, err)
   321  			return
   322  		}
   323  		for _, pvp := range pvps {
   324  			tags[pvp.Value] = pv.Version
   325  		}
   326  	}
   327  
   328  	ctx.JSON(http.StatusOK, tags)
   329  }
   330  
   331  // AddPackageTag adds a tag to the package
   332  func AddPackageTag(ctx *context.Context) {
   333  	packageName := packageNameFromParams(ctx)
   334  
   335  	body, err := io.ReadAll(ctx.Req.Body)
   336  	if err != nil {
   337  		apiError(ctx, http.StatusInternalServerError, err)
   338  		return
   339  	}
   340  	version := strings.Trim(string(body), "\"") // is as "version" in the body
   341  
   342  	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName, version)
   343  	if err != nil {
   344  		if err == packages_model.ErrPackageNotExist {
   345  			apiError(ctx, http.StatusNotFound, err)
   346  			return
   347  		}
   348  		apiError(ctx, http.StatusInternalServerError, err)
   349  		return
   350  	}
   351  
   352  	if err := setPackageTag(ctx, ctx.Params("tag"), pv, false); err != nil {
   353  		if err == errInvalidTagName {
   354  			apiError(ctx, http.StatusBadRequest, err)
   355  			return
   356  		}
   357  		apiError(ctx, http.StatusInternalServerError, err)
   358  		return
   359  	}
   360  }
   361  
   362  // DeletePackageTag deletes a package tag
   363  func DeletePackageTag(ctx *context.Context) {
   364  	packageName := packageNameFromParams(ctx)
   365  
   366  	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
   367  	if err != nil {
   368  		apiError(ctx, http.StatusInternalServerError, err)
   369  		return
   370  	}
   371  
   372  	if len(pvs) != 0 {
   373  		if err := setPackageTag(ctx, ctx.Params("tag"), pvs[0], true); err != nil {
   374  			if err == errInvalidTagName {
   375  				apiError(ctx, http.StatusBadRequest, err)
   376  				return
   377  			}
   378  			apiError(ctx, http.StatusInternalServerError, err)
   379  			return
   380  		}
   381  	}
   382  }
   383  
   384  func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVersion, deleteOnly bool) error {
   385  	if tag == "" {
   386  		return errInvalidTagName
   387  	}
   388  	_, err := version.NewVersion(tag)
   389  	if err == nil {
   390  		return errInvalidTagName
   391  	}
   392  
   393  	return db.WithTx(ctx, func(ctx std_ctx.Context) error {
   394  		pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
   395  			PackageID: pv.PackageID,
   396  			Properties: map[string]string{
   397  				npm_module.TagProperty: tag,
   398  			},
   399  			IsInternal: optional.Some(false),
   400  		})
   401  		if err != nil {
   402  			return err
   403  		}
   404  
   405  		if len(pvs) == 1 {
   406  			pvps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pvs[0].ID, npm_module.TagProperty)
   407  			if err != nil {
   408  				return err
   409  			}
   410  
   411  			for _, pvp := range pvps {
   412  				if pvp.Value == tag {
   413  					if err := packages_model.DeletePropertyByID(ctx, pvp.ID); err != nil {
   414  						return err
   415  					}
   416  					break
   417  				}
   418  			}
   419  		}
   420  
   421  		if !deleteOnly {
   422  			_, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, npm_module.TagProperty, tag)
   423  			if err != nil {
   424  				return err
   425  			}
   426  		}
   427  		return nil
   428  	})
   429  }
   430  
   431  func PackageSearch(ctx *context.Context) {
   432  	pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
   433  		OwnerID:    ctx.Package.Owner.ID,
   434  		Type:       packages_model.TypeNpm,
   435  		IsInternal: optional.Some(false),
   436  		Name: packages_model.SearchValue{
   437  			ExactMatch: false,
   438  			Value:      ctx.FormTrim("text"),
   439  		},
   440  		Paginator: db.NewAbsoluteListOptions(
   441  			ctx.FormInt("from"),
   442  			ctx.FormInt("size"),
   443  		),
   444  	})
   445  	if err != nil {
   446  		apiError(ctx, http.StatusInternalServerError, err)
   447  		return
   448  	}
   449  
   450  	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
   451  	if err != nil {
   452  		apiError(ctx, http.StatusInternalServerError, err)
   453  		return
   454  	}
   455  
   456  	resp := createPackageSearchResponse(
   457  		pds,
   458  		total,
   459  	)
   460  
   461  	ctx.JSON(http.StatusOK, resp)
   462  }