mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	fix #27470 (hope there is nothing missing 😢 ) --------- Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
		
			
				
	
	
		
			462 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			462 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package npm
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	std_ctx "context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"code.gitea.io/gitea/models/db"
 | 
						|
	packages_model "code.gitea.io/gitea/models/packages"
 | 
						|
	access_model "code.gitea.io/gitea/models/perm/access"
 | 
						|
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						|
	"code.gitea.io/gitea/models/unit"
 | 
						|
	"code.gitea.io/gitea/modules/context"
 | 
						|
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
						|
	npm_module "code.gitea.io/gitea/modules/packages/npm"
 | 
						|
	"code.gitea.io/gitea/modules/setting"
 | 
						|
	"code.gitea.io/gitea/modules/util"
 | 
						|
	"code.gitea.io/gitea/routers/api/packages/helper"
 | 
						|
	packages_service "code.gitea.io/gitea/services/packages"
 | 
						|
 | 
						|
	"github.com/hashicorp/go-version"
 | 
						|
)
 | 
						|
 | 
						|
// errInvalidTagName indicates an invalid tag name
 | 
						|
var errInvalidTagName = errors.New("The tag name is invalid")
 | 
						|
 | 
						|
func apiError(ctx *context.Context, status int, obj any) {
 | 
						|
	helper.LogAndProcessError(ctx, status, obj, func(message string) {
 | 
						|
		ctx.JSON(status, map[string]string{
 | 
						|
			"error": message,
 | 
						|
		})
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// packageNameFromParams gets the package name from the url parameters
 | 
						|
// Variations: /name/, /@scope/name/, /@scope%2Fname/
 | 
						|
func packageNameFromParams(ctx *context.Context) string {
 | 
						|
	scope := ctx.Params("scope")
 | 
						|
	id := ctx.Params("id")
 | 
						|
	if scope != "" {
 | 
						|
		return fmt.Sprintf("@%s/%s", scope, id)
 | 
						|
	}
 | 
						|
	return id
 | 
						|
}
 | 
						|
 | 
						|
// PackageMetadata returns the metadata for a single package
 | 
						|
func PackageMetadata(ctx *context.Context) {
 | 
						|
	packageName := packageNameFromParams(ctx)
 | 
						|
 | 
						|
	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if len(pvs) == 0 {
 | 
						|
		apiError(ctx, http.StatusNotFound, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	resp := createPackageMetadataResponse(
 | 
						|
		setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/npm",
 | 
						|
		pds,
 | 
						|
	)
 | 
						|
 | 
						|
	ctx.JSON(http.StatusOK, resp)
 | 
						|
}
 | 
						|
 | 
						|
// DownloadPackageFile serves the content of a package
 | 
						|
func DownloadPackageFile(ctx *context.Context) {
 | 
						|
	packageName := packageNameFromParams(ctx)
 | 
						|
	packageVersion := ctx.Params("version")
 | 
						|
	filename := ctx.Params("filename")
 | 
						|
 | 
						|
	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
 | 
						|
		ctx,
 | 
						|
		&packages_service.PackageInfo{
 | 
						|
			Owner:       ctx.Package.Owner,
 | 
						|
			PackageType: packages_model.TypeNpm,
 | 
						|
			Name:        packageName,
 | 
						|
			Version:     packageVersion,
 | 
						|
		},
 | 
						|
		&packages_service.PackageFileInfo{
 | 
						|
			Filename: filename,
 | 
						|
		},
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
 | 
						|
			apiError(ctx, http.StatusNotFound, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	helper.ServePackageFile(ctx, s, u, pf)
 | 
						|
}
 | 
						|
 | 
						|
// DownloadPackageFileByName finds the version and serves the contents of a package
 | 
						|
func DownloadPackageFileByName(ctx *context.Context) {
 | 
						|
	filename := ctx.Params("filename")
 | 
						|
 | 
						|
	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 | 
						|
		OwnerID: ctx.Package.Owner.ID,
 | 
						|
		Type:    packages_model.TypeNpm,
 | 
						|
		Name: packages_model.SearchValue{
 | 
						|
			ExactMatch: true,
 | 
						|
			Value:      packageNameFromParams(ctx),
 | 
						|
		},
 | 
						|
		HasFileWithName: filename,
 | 
						|
		IsInternal:      util.OptionalBoolFalse,
 | 
						|
	})
 | 
						|
	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)
 | 
						|
}
 | 
						|
 | 
						|
// UploadPackage creates a new package
 | 
						|
func UploadPackage(ctx *context.Context) {
 | 
						|
	npmPackage, err := npm_module.ParsePackage(ctx.Req.Body)
 | 
						|
	if err != nil {
 | 
						|
		if errors.Is(err, util.ErrInvalidArgument) {
 | 
						|
			apiError(ctx, http.StatusBadRequest, err)
 | 
						|
		} else {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	repo, err := repo_model.GetRepositoryByURL(ctx, npmPackage.Metadata.Repository.URL)
 | 
						|
	if err == nil {
 | 
						|
		canWrite := repo.OwnerID == ctx.Doer.ID
 | 
						|
 | 
						|
		if !canWrite {
 | 
						|
			perms, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
 | 
						|
			if err != nil {
 | 
						|
				apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			canWrite = perms.CanWrite(unit.TypePackages)
 | 
						|
		}
 | 
						|
 | 
						|
		if !canWrite {
 | 
						|
			apiError(ctx, http.StatusForbidden, "no permission to upload this package")
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	buf, err := packages_module.CreateHashedBufferFromReader(bytes.NewReader(npmPackage.Data))
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	defer buf.Close()
 | 
						|
 | 
						|
	pv, _, err := packages_service.CreatePackageAndAddFile(
 | 
						|
		ctx,
 | 
						|
		&packages_service.PackageCreationInfo{
 | 
						|
			PackageInfo: packages_service.PackageInfo{
 | 
						|
				Owner:       ctx.Package.Owner,
 | 
						|
				PackageType: packages_model.TypeNpm,
 | 
						|
				Name:        npmPackage.Name,
 | 
						|
				Version:     npmPackage.Version,
 | 
						|
			},
 | 
						|
			SemverCompatible: true,
 | 
						|
			Creator:          ctx.Doer,
 | 
						|
			Metadata:         npmPackage.Metadata,
 | 
						|
		},
 | 
						|
		&packages_service.PackageFileCreationInfo{
 | 
						|
			PackageFileInfo: packages_service.PackageFileInfo{
 | 
						|
				Filename: npmPackage.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
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tag := range npmPackage.DistTags {
 | 
						|
		if err := setPackageTag(ctx, tag, pv, false); err != nil {
 | 
						|
			if err == errInvalidTagName {
 | 
						|
				apiError(ctx, http.StatusBadRequest, err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if repo != nil {
 | 
						|
		if err := packages_model.SetRepositoryLink(ctx, pv.PackageID, repo.ID); err != nil {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.Status(http.StatusCreated)
 | 
						|
}
 | 
						|
 | 
						|
// DeletePreview does nothing
 | 
						|
// The client tells the server what package version it knows about after deleting a version.
 | 
						|
func DeletePreview(ctx *context.Context) {
 | 
						|
	ctx.Status(http.StatusOK)
 | 
						|
}
 | 
						|
 | 
						|
// DeletePackageVersion deletes the package version
 | 
						|
func DeletePackageVersion(ctx *context.Context) {
 | 
						|
	packageName := packageNameFromParams(ctx)
 | 
						|
	packageVersion := ctx.Params("version")
 | 
						|
 | 
						|
	err := packages_service.RemovePackageVersionByNameAndVersion(
 | 
						|
		ctx,
 | 
						|
		ctx.Doer,
 | 
						|
		&packages_service.PackageInfo{
 | 
						|
			Owner:       ctx.Package.Owner,
 | 
						|
			PackageType: packages_model.TypeNpm,
 | 
						|
			Name:        packageName,
 | 
						|
			Version:     packageVersion,
 | 
						|
		},
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		if err == packages_model.ErrPackageNotExist {
 | 
						|
			apiError(ctx, http.StatusNotFound, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.Status(http.StatusOK)
 | 
						|
}
 | 
						|
 | 
						|
// DeletePackage deletes the package and all versions
 | 
						|
func DeletePackage(ctx *context.Context) {
 | 
						|
	packageName := packageNameFromParams(ctx)
 | 
						|
 | 
						|
	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if len(pvs) == 0 {
 | 
						|
		apiError(ctx, http.StatusNotFound, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	for _, pv := range pvs {
 | 
						|
		if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.Status(http.StatusOK)
 | 
						|
}
 | 
						|
 | 
						|
// ListPackageTags returns all tags for a package
 | 
						|
func ListPackageTags(ctx *context.Context) {
 | 
						|
	packageName := packageNameFromParams(ctx)
 | 
						|
 | 
						|
	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	tags := make(map[string]string)
 | 
						|
	for _, pv := range pvs {
 | 
						|
		pvps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, npm_module.TagProperty)
 | 
						|
		if err != nil {
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		for _, pvp := range pvps {
 | 
						|
			tags[pvp.Value] = pv.Version
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	ctx.JSON(http.StatusOK, tags)
 | 
						|
}
 | 
						|
 | 
						|
// AddPackageTag adds a tag to the package
 | 
						|
func AddPackageTag(ctx *context.Context) {
 | 
						|
	packageName := packageNameFromParams(ctx)
 | 
						|
 | 
						|
	body, err := io.ReadAll(ctx.Req.Body)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	version := strings.Trim(string(body), "\"") // is as "version" in the body
 | 
						|
 | 
						|
	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName, version)
 | 
						|
	if err != nil {
 | 
						|
		if err == packages_model.ErrPackageNotExist {
 | 
						|
			apiError(ctx, http.StatusNotFound, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err := setPackageTag(ctx, ctx.Params("tag"), pv, false); err != nil {
 | 
						|
		if err == errInvalidTagName {
 | 
						|
			apiError(ctx, http.StatusBadRequest, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// DeletePackageTag deletes a package tag
 | 
						|
func DeletePackageTag(ctx *context.Context) {
 | 
						|
	packageName := packageNameFromParams(ctx)
 | 
						|
 | 
						|
	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if len(pvs) != 0 {
 | 
						|
		if err := setPackageTag(ctx, ctx.Params("tag"), pvs[0], true); err != nil {
 | 
						|
			if err == errInvalidTagName {
 | 
						|
				apiError(ctx, http.StatusBadRequest, err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVersion, deleteOnly bool) error {
 | 
						|
	if tag == "" {
 | 
						|
		return errInvalidTagName
 | 
						|
	}
 | 
						|
	_, err := version.NewVersion(tag)
 | 
						|
	if err == nil {
 | 
						|
		return errInvalidTagName
 | 
						|
	}
 | 
						|
 | 
						|
	return db.WithTx(ctx, func(ctx std_ctx.Context) error {
 | 
						|
		pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 | 
						|
			PackageID: pv.PackageID,
 | 
						|
			Properties: map[string]string{
 | 
						|
				npm_module.TagProperty: tag,
 | 
						|
			},
 | 
						|
			IsInternal: util.OptionalBoolFalse,
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		if len(pvs) == 1 {
 | 
						|
			pvps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pvs[0].ID, npm_module.TagProperty)
 | 
						|
			if err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
 | 
						|
			for _, pvp := range pvps {
 | 
						|
				if pvp.Value == tag {
 | 
						|
					if err := packages_model.DeletePropertyByID(ctx, pvp.ID); err != nil {
 | 
						|
						return err
 | 
						|
					}
 | 
						|
					break
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if !deleteOnly {
 | 
						|
			_, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, npm_module.TagProperty, tag)
 | 
						|
			if err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func PackageSearch(ctx *context.Context) {
 | 
						|
	pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
 | 
						|
		OwnerID:    ctx.Package.Owner.ID,
 | 
						|
		Type:       packages_model.TypeNpm,
 | 
						|
		IsInternal: util.OptionalBoolFalse,
 | 
						|
		Name: packages_model.SearchValue{
 | 
						|
			ExactMatch: false,
 | 
						|
			Value:      ctx.FormTrim("text"),
 | 
						|
		},
 | 
						|
		Paginator: db.NewAbsoluteListOptions(
 | 
						|
			ctx.FormInt("from"),
 | 
						|
			ctx.FormInt("size"),
 | 
						|
		),
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
 | 
						|
	if err != nil {
 | 
						|
		apiError(ctx, http.StatusInternalServerError, err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	resp := createPackageSearchResponse(
 | 
						|
		pds,
 | 
						|
		total,
 | 
						|
	)
 | 
						|
 | 
						|
	ctx.JSON(http.StatusOK, resp)
 | 
						|
}
 |