mirror of
https://github.com/go-gitea/gitea
synced 2025-01-19 14:14:26 +00:00
34dfc25b83
close #33086 * Add a special value for "SSH_USER" setting: `(DOER_USERNAME)` * Improve parseRepositoryURL and add tests (now it doesn't have hard dependency on some setting values) Many changes are just adding "ctx" and "doer" argument to functions. By the way, improve app.example.ini, remove all `%(key)s` syntax, it only makes messy and no user really cares about it. Document: https://gitea.com/gitea/docs/pulls/138
463 lines
12 KiB
Go
463 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/optional"
|
|
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"
|
|
"code.gitea.io/gitea/services/context"
|
|
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.PathParam("scope")
|
|
id := ctx.PathParam("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.PathParam("version")
|
|
filename := ctx.PathParam("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 errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(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.PathParam("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: optional.Some(false),
|
|
})
|
|
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 errors.Is(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.GetRepositoryByURLRelax(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.PathParam("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 errors.Is(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 errors.Is(err, packages_model.ErrPackageNotExist) {
|
|
apiError(ctx, http.StatusNotFound, err)
|
|
return
|
|
}
|
|
apiError(ctx, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
if err := setPackageTag(ctx, ctx.PathParam("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.PathParam("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: optional.Some(false),
|
|
})
|
|
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: optional.Some(false),
|
|
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)
|
|
}
|