mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-09 22:21:42 +09:00
enhance(packages): implement npm single-version API and add per-version repository (#39267)
Signed-off-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
@@ -66,7 +66,7 @@ type PackageMetadata struct {
|
||||
Time map[string]time.Time `json:"time,omitempty"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Repository Repository `json:"repository"`
|
||||
Repository Repository `json:"repository,omitzero"`
|
||||
Author User `json:"author"`
|
||||
ReadmeFilename string `json:"readmeFilename,omitempty"`
|
||||
Users map[string]bool `json:"users,omitempty"`
|
||||
@@ -104,7 +104,7 @@ type PackageMetadataVersion struct {
|
||||
Author User `json:"author"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
License License `json:"license,omitempty"`
|
||||
Repository Repository `json:"repository"`
|
||||
Repository Repository `json:"repository,omitzero"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
||||
BundleDependencies []string `json:"bundleDependencies,omitempty"`
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package npm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
@@ -13,7 +14,6 @@ import (
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
npm_module "gitea.dev/modules/packages/npm"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
|
||||
@@ -78,6 +78,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
|
||||
Maintainers: []npm_module.User{{Name: pd.Owner.Name}},
|
||||
Homepage: metadata.ProjectURL,
|
||||
License: metadata.License,
|
||||
Repository: metadata.Repository,
|
||||
Keywords: metadata.Keywords,
|
||||
Dependencies: metadata.Dependencies,
|
||||
BundleDependencies: metadata.BundleDependencies,
|
||||
@@ -104,7 +105,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
|
||||
}
|
||||
}
|
||||
|
||||
func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total int64) *npm_module.PackageSearch {
|
||||
func createPackageSearchResponse(ctx context.Context, pds []*packages_model.PackageDescriptor, total int64) *npm_module.PackageSearch {
|
||||
objects := make([]*npm_module.PackageSearchObject, 0, len(pds))
|
||||
for _, pd := range pds {
|
||||
metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](pd)
|
||||
@@ -126,7 +127,7 @@ func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total
|
||||
Maintainers: []npm_module.User{}, // npm cli needs this field
|
||||
Keywords: metadata.Keywords,
|
||||
Links: &npm_module.PackageSearchPackageLinks{
|
||||
Registry: setting.AppURL + "api/packages/" + pd.Owner.Name + "/npm",
|
||||
Registry: buildNpmRegistryURL(ctx, pd.Owner),
|
||||
Homepage: metadata.ProjectURL,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
npm_module "gitea.dev/modules/packages/npm"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
@@ -17,13 +18,14 @@ import (
|
||||
)
|
||||
|
||||
func TestCreatePackageMetadataResponse(t *testing.T) {
|
||||
descriptor := func(v string, publishedUnix int64) *packages_model.PackageDescriptor {
|
||||
repository := npm_module.Repository{Type: "git", URL: "https://gitea.dev/alice/test.git"}
|
||||
descriptor := func(v string, publishedUnix int64, repo npm_module.Repository) *packages_model.PackageDescriptor {
|
||||
return &packages_model.PackageDescriptor{
|
||||
Package: &packages_model.Package{Name: "@scope/test"},
|
||||
Owner: &user_model.User{Name: "alice"},
|
||||
Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)},
|
||||
SemVer: version.Must(version.NewVersion(v)),
|
||||
Metadata: &npm_module.Metadata{Keywords: []string{"gitea"}},
|
||||
Metadata: &npm_module.Metadata{Keywords: []string{"gitea"}, Repository: repo},
|
||||
Files: []*packages_model.PackageFileDescriptor{{
|
||||
File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"},
|
||||
Blob: &packages_model.PackageBlob{},
|
||||
@@ -32,8 +34,8 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
result := createPackageMetadataResponse("https://gitea.dev/api/packages/alice/npm", []*packages_model.PackageDescriptor{
|
||||
descriptor("1.1.0", 1000),
|
||||
descriptor("1.0.0", 2000),
|
||||
descriptor("1.1.0", 1000, npm_module.Repository{}),
|
||||
descriptor("1.0.0", 2000, repository),
|
||||
})
|
||||
|
||||
assert.Equal(t, map[string]time.Time{
|
||||
@@ -50,4 +52,15 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
|
||||
"https://gitea.dev/api/packages/alice/npm/@scope%2Ftest/-/1.0.0/test-1.0.0.tgz",
|
||||
result.Versions["1.0.0"].Dist.Tarball,
|
||||
)
|
||||
assert.Equal(t, repository, result.Versions["1.0.0"].Repository)
|
||||
|
||||
withoutRepository, err := json.Marshal(result.Versions["1.1.0"])
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, string(withoutRepository), `"repository"`)
|
||||
|
||||
raw, err := json.Marshal(result)
|
||||
assert.NoError(t, err)
|
||||
doc := map[string]any{}
|
||||
assert.NoError(t, json.Unmarshal(raw, &doc))
|
||||
assert.NotContains(t, doc, "repository")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
@@ -17,11 +18,12 @@ import (
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/optional"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
npm_module "gitea.dev/modules/packages/npm"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/routers/api/packages/helper"
|
||||
"gitea.dev/services/context"
|
||||
@@ -59,6 +61,10 @@ func packageNameFromParams(ctx *context.Context) string {
|
||||
return fullOrSub // id is the full package name, e.g.: "@angular/core" or "lodash"
|
||||
}
|
||||
|
||||
func buildNpmRegistryURL(ctx std_ctx.Context, owner *user_model.User) string {
|
||||
return httplib.GuessCurrentAppURL(ctx) + "api/packages/" + url.PathEscape(owner.Name) + "/npm"
|
||||
}
|
||||
|
||||
// PackageMetadata returns the metadata for a single package
|
||||
func PackageMetadata(ctx *context.Context) {
|
||||
packageName := packageNameFromParams(ctx)
|
||||
@@ -79,16 +85,42 @@ func PackageMetadata(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := createPackageMetadataResponse(
|
||||
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/npm",
|
||||
pds,
|
||||
)
|
||||
|
||||
resp := createPackageMetadataResponse(buildNpmRegistryURL(ctx, ctx.Package.Owner), pds)
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// PackageVersionMetadata returns the metadata for a single version or dist-tag
|
||||
func PackageVersionMetadata(ctx *context.Context) {
|
||||
ctx.HTTPError(http.StatusNotImplemented, "not implemented")
|
||||
versionOrTag := ctx.PathParam("version")
|
||||
|
||||
opts := &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNpm,
|
||||
Name: packages_model.SearchValue{ExactMatch: true, Value: packageNameFromParams(ctx)},
|
||||
IsInternal: optional.Some(false),
|
||||
}
|
||||
if _, err := version.NewVersion(versionOrTag); err == nil {
|
||||
opts.Version = packages_model.SearchValue{ExactMatch: true, Value: versionOrTag}
|
||||
} else { // a tag, since setPackageTag rejects version-like names
|
||||
opts.Properties = map[string]string{npm_module.TagProperty: versionOrTag}
|
||||
}
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, opts)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, "version not found: "+versionOrTag)
|
||||
return
|
||||
}
|
||||
|
||||
pd, err := packages_model.GetPackageDescriptor(ctx, pvs[0])
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, createPackageMetadataVersion(buildNpmRegistryURL(ctx, ctx.Package.Owner), pd))
|
||||
}
|
||||
|
||||
// DownloadPackageFile serves the content of a package
|
||||
@@ -527,10 +559,7 @@ func PackageSearch(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := createPackageSearchResponse(
|
||||
pds,
|
||||
total,
|
||||
)
|
||||
resp := createPackageSearchResponse(ctx, pds, total)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -182,9 +182,6 @@ func TestPackageNpm(t *testing.T) {
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/-/%s", root, filename)).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, b, resp.Body.Bytes())
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s", root, packageVersion)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotImplemented)
|
||||
}
|
||||
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNpm)
|
||||
assert.NoError(t, err)
|
||||
@@ -236,6 +233,24 @@ func TestPackageNpm(t *testing.T) {
|
||||
assert.Empty(t, pmv.Deprecated)
|
||||
})
|
||||
|
||||
t.Run("PackageVersionMetadata", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
for _, selector := range []string{packageVersion, packageTag} {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", root, selector)).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
pmv := DecodeJSON(t, resp, &npm.PackageMetadataVersion{})
|
||||
assert.Equal(t, fmt.Sprintf("%s@%s", packageName, packageVersion), pmv.ID)
|
||||
assert.Equal(t, npm.Repository{Type: repoType, URL: repoURL, Directory: repoDirectory}, pmv.Repository)
|
||||
}
|
||||
|
||||
for _, missing := range []string{"9.9.9", "no-such-tag"} {
|
||||
req := NewRequest(t, "GET", root+"/"+missing).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AddTag", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user