diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 6a40f876a50..b33df48df49 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -12,6 +12,7 @@ import ( "encoding/base64" "fmt" "io" + "net/url" "regexp" "strings" "sync" @@ -343,7 +344,9 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) { p.DistTags = append(p.DistTags, tag) } - p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version)) + filename := fmt.Sprintf("%s-%s.tgz", name, p.Version) + p.Filename = strings.ToLower(filename) + p.Metadata.GiteaTarballPath = util.PathEscapeSegments(p.Name) + "/-/" + url.PathEscape(filename) // canonical URL clients derive from name and version, file lookups ignore case attachment := upload.Attachments[meta.Name+"-"+meta.Version+".tgz"] // not the sigstore bundle of `npm publish --provenance` if attachment == nil && len(upload.Attachments) == 1 { diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go index 983ad032b37..1728f5d0224 100644 --- a/modules/packages/npm/creator_test.go +++ b/modules/packages/npm/creator_test.go @@ -23,7 +23,7 @@ func TestParsePackage(t *testing.T) { packageScope := "@scope" packageName := "test-package" packageFullName := packageScope + "/" + packageName - packageVersion := "1.0.1-pre" + packageVersion := "1.0.1-Pre" packageTag := "latest" packageAuthor := "KN4CK3R" packageBin := "gitea" @@ -287,7 +287,8 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, packageFullName, p.Name) assert.Equal(t, packageVersion, p.Version) assert.Equal(t, []string{packageTag}, p.DistTags) - assert.Equal(t, fmt.Sprintf("%s-%s.tgz", strings.Split(packageFullName, "/")[1], packageVersion), p.Filename) + assert.Equal(t, strings.ToLower(fmt.Sprintf("%s-%s.tgz", packageName, packageVersion)), p.Filename) + assert.Equal(t, fmt.Sprintf("%s/-/%s-%s.tgz", packageFullName, packageName, packageVersion), p.Metadata.GiteaTarballPath) b, _ = base64.StdEncoding.DecodeString(data) assert.Equal(t, b, p.Data) assert.Equal(t, packageName, p.Metadata.Name) diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index f0682d8934e..3e36a1151f2 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -34,4 +34,6 @@ type Metadata struct { Funding any `json:"funding,omitempty"` AcceptDependencies map[string]string `json:"accept_dependencies,omitempty"` Deprecated string `json:"deprecated,omitempty"` + + GiteaTarballPath string `json:"gitea_tarball_path,omitempty"` // Gitea-internal, fixed at upload because lockfiles pin it } diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 63244a14159..ba7326bf519 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -414,7 +414,7 @@ func CommonRoutes() *web.Router { g.UseUnescapedPath() g.MatchPath("DELETE", packageId+"/-///-rev/", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion) g.MatchPath("DELETE", packageId+"/-//-rev/", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion) - g.MatchPath("GET", packageId+"/-//", npm.DownloadPackageFileByName) // former tarball URL, still in lockfiles + g.MatchPath("GET", packageId+"/-//", npm.DownloadPackageFileByName) // 1.27 and 28.0 URL, still advertised for older uploads g.MatchPath("GET", packageId+"/-/", npm.DownloadPackageFileByName) g.MatchPath("DELETE", packageId+"/-rev/", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackage) g.MatchPath("PUT", packageId+"/-rev/", reqPackageAccess(perm.AccessModeWrite), npm.DeletePreview) diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index 7640529feba..5eba148ab23 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "net/url" "slices" "sort" "time" @@ -82,6 +83,11 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](pd) + tarballPath := metadata.GiteaTarballPath + if tarballPath == "" { + tarballPath = fmt.Sprintf("%s/-/%s/%s", url.QueryEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.Files[0].File.LowerName)) // 1.27 URL, pinned by older lockfiles + } + return &npm_module.PackageMetadataVersion{ ID: fmt.Sprintf("%s@%s", pd.Package.Name, pd.Version.Version), Name: pd.Package.Name, @@ -113,7 +119,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package Dist: npm_module.PackageDistribution{ Shasum: pd.Files[0].Blob.HashSHA1, Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes), - Tarball: fmt.Sprintf("%s/%s/-/%s", registryURL, pd.Package.Name, pd.Files[0].File.LowerName), // npmjs shape, which npm parses for allowScripts and yarn keeps registry-relative + Tarball: registryURL + "/" + tarballPath, }, } } diff --git a/routers/api/packages/npm/api_test.go b/routers/api/packages/npm/api_test.go index a6e4d176ad6..22fce040185 100644 --- a/routers/api/packages/npm/api_test.go +++ b/routers/api/packages/npm/api_test.go @@ -19,13 +19,13 @@ import ( func TestCreatePackageMetadataResponse(t *testing.T) { 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 { + descriptor := func(v string, publishedUnix int64, repo npm_module.Repository, tarballPath string) *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{Readme: v, Keywords: []string{"gitea"}, Repository: repo}, + Metadata: &npm_module.Metadata{Readme: v, Keywords: []string{"gitea"}, Repository: repo, GiteaTarballPath: tarballPath}, Files: []*packages_model.PackageFileDescriptor{{ File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"}, Blob: &packages_model.PackageBlob{}, @@ -34,9 +34,9 @@ func TestCreatePackageMetadataResponse(t *testing.T) { } result := createPackageMetadataResponse("https://gitea.dev/api/packages/alice/npm", []*packages_model.PackageDescriptor{ - descriptor("1.1.0", 1000, npm_module.Repository{}), - descriptor("2.0.0-rc.1", 1500, repository), - descriptor("1.0.0", 2000, repository), + descriptor("1.1.0", 1000, npm_module.Repository{}, "@scope/test/-/test-1.1.0.tgz"), + descriptor("2.0.0-rc.1", 1500, repository, ""), + descriptor("1.0.0", 2000, repository, ""), }) assert.Equal(t, map[string]time.Time{ @@ -54,9 +54,13 @@ func TestCreatePackageMetadataResponse(t *testing.T) { assert.Equal(t, []string{"gitea"}, result.Versions["1.0.0"].Keywords) assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Versions["1.0.0"].Maintainers) assert.Equal(t, - "https://gitea.dev/api/packages/alice/npm/@scope/test/-/test-1.0.0.tgz", + "https://gitea.dev/api/packages/alice/npm/%40scope%2Ftest/-/1.0.0/test-1.0.0.tgz", result.Versions["1.0.0"].Dist.Tarball, ) + assert.Equal(t, + "https://gitea.dev/api/packages/alice/npm/@scope/test/-/test-1.1.0.tgz", + result.Versions["1.1.0"].Dist.Tarball, + ) assert.Equal(t, repository, result.Versions["1.0.0"].Repository) withoutRepository, err := json.Marshal(result.Versions["1.1.0"])