From fba20550f917a808846256d279ac3b4f9e302936 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Tue, 9 Aug 2022 09:23:43 +0200 Subject: [PATCH] Add support for `npm unpublish` (#20688) --- docs/content/doc/packages/npm.en-us.md | 21 +++++ integrations/api_packages_npm_test.go | 106 +++++++++++++++++++------ routers/api/packages/api.go | 18 ++++- routers/api/packages/npm/npm.go | 57 +++++++++++++ 4 files changed, 175 insertions(+), 27 deletions(-) diff --git a/docs/content/doc/packages/npm.en-us.md b/docs/content/doc/packages/npm.en-us.md index 9ab4ac900c..122f306ee5 100644 --- a/docs/content/doc/packages/npm.en-us.md +++ b/docs/content/doc/packages/npm.en-us.md @@ -67,6 +67,26 @@ npm publish You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first. +## Unpublish a package + +Delete a package by running the following command: + +```shell +npm unpublish {package_name}[@{package_version}] +``` + +| Parameter | Description | +| ----------------- | ----------- | +| `package_name` | The package name. | +| `package_version` | The package version. | + +For example: + +```shell +npm unpublish @test/test_package +npm unpublish @test/test_package@1.0.0 +``` + ## Install a package To install a package from the package registry, execute the following command: @@ -113,6 +133,7 @@ The tag name must not be a valid version. All tag names which are parsable as a npm install npm ci npm publish +npm unpublish npm dist-tag npm view ``` diff --git a/integrations/api_packages_npm_test.go b/integrations/api_packages_npm_test.go index ad88ac5da6..23f13866bf 100644 --- a/integrations/api_packages_npm_test.go +++ b/integrations/api_packages_npm_test.go @@ -36,33 +36,36 @@ func TestPackageNpm(t *testing.T) { packageDescription := "Test Description" data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA" - upload := `{ - "_id": "` + packageName + `", - "name": "` + packageName + `", - "description": "` + packageDescription + `", - "dist-tags": { - "` + packageTag + `": "` + packageVersion + `" - }, - "versions": { - "` + packageVersion + `": { + + buildUpload := func(version string) string { + return `{ + "_id": "` + packageName + `", "name": "` + packageName + `", - "version": "` + packageVersion + `", "description": "` + packageDescription + `", - "author": { - "name": "` + packageAuthor + `" + "dist-tags": { + "` + packageTag + `": "` + version + `" }, - "dist": { - "integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==", - "shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90" + "versions": { + "` + version + `": { + "name": "` + packageName + `", + "version": "` + version + `", + "description": "` + packageDescription + `", + "author": { + "name": "` + packageAuthor + `" + }, + "dist": { + "integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==", + "shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90" + } + } + }, + "_attachments": { + "` + packageName + `-` + version + `.tgz": { + "data": "` + data + `" + } } - } - }, - "_attachments": { - "` + packageName + `-` + packageVersion + `.tgz": { - "data": "` + data + `" - } - } - }` + }` + } root := fmt.Sprintf("/api/packages/%s/npm/%s", user.Name, url.QueryEscape(packageName)) tagsRoot := fmt.Sprintf("/api/packages/%s/npm/-/package/%s/dist-tags", user.Name, url.QueryEscape(packageName)) @@ -71,7 +74,7 @@ func TestPackageNpm(t *testing.T) { t.Run("Upload", func(t *testing.T) { defer PrintCurrentTest(t)() - req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload)) + req := NewRequestWithBody(t, "PUT", root, strings.NewReader(buildUpload(packageVersion))) req = addTokenAuthHeader(req, token) MakeRequest(t, req, http.StatusCreated) @@ -103,7 +106,7 @@ func TestPackageNpm(t *testing.T) { t.Run("UploadExists", func(t *testing.T) { defer PrintCurrentTest(t)() - req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload)) + req := NewRequestWithBody(t, "PUT", root, strings.NewReader(buildUpload(packageVersion))) req = addTokenAuthHeader(req, token) MakeRequest(t, req, http.StatusBadRequest) }) @@ -219,4 +222,57 @@ func TestPackageNpm(t *testing.T) { test(t, http.StatusOK, "dummy") test(t, http.StatusOK, packageTag2) }) + + t.Run("Delete", func(t *testing.T) { + defer PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", root, strings.NewReader(buildUpload(packageVersion+"-dummy"))) + req = addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "PUT", root+"/-rev/dummy") + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "PUT", root+"/-rev/dummy") + req = addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusOK) + + t.Run("Version", func(t *testing.T) { + defer PrintCurrentTest(t)() + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm) + assert.NoError(t, err) + assert.Len(t, pvs, 2) + + req := NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename)) + req = addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusOK) + + pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + }) + + t.Run("Full", func(t *testing.T) { + defer PrintCurrentTest(t)() + + pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm) + assert.NoError(t, err) + assert.Len(t, pvs, 1) + + req := NewRequest(t, "DELETE", root+"/-rev/dummy") + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", root+"/-rev/dummy") + req = addTokenAuthHeader(req, token) + MakeRequest(t, req, http.StatusOK) + + pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm) + assert.NoError(t, err) + assert.Len(t, pvs, 0) + }) + }) } diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 84bdce30fa..39ba41cdfb 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -198,12 +198,26 @@ func Routes() *web.Route { r.Group("/@{scope}/{id}", func() { r.Get("", npm.PackageMetadata) r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage) - r.Get("/-/{version}/{filename}", npm.DownloadPackageFile) + r.Group("/-/{version}/{filename}", func() { + r.Get("", npm.DownloadPackageFile) + r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion) + }) + r.Group("/-rev/{revision}", func() { + r.Delete("", npm.DeletePackage) + r.Put("", npm.DeletePreview) + }, reqPackageAccess(perm.AccessModeWrite)) }) r.Group("/{id}", func() { r.Get("", npm.PackageMetadata) r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage) - r.Get("/-/{version}/{filename}", npm.DownloadPackageFile) + r.Group("/-/{version}/{filename}", func() { + r.Get("", npm.DownloadPackageFile) + r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion) + }) + r.Group("/-rev/{revision}", func() { + r.Delete("", npm.DeletePackage) + r.Put("", npm.DeletePreview) + }, reqPackageAccess(perm.AccessModeWrite)) }) r.Group("/-/package/@{scope}/{id}/dist-tags", func() { r.Get("", npm.ListPackageTags) diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 152edc681a..d5ba70f964 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -164,6 +164,63 @@ func UploadPackage(ctx *context.Context) { 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.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.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)