Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for npm unpublish #20688

Merged
merged 3 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/content/doc/packages/npm.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]
```

## Install a package

To install a package from the package registry, execute the following command:
Expand Down Expand Up @@ -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
```
106 changes: 81 additions & 25 deletions integrations/api_packages_npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 + `"
},
"versions": {
"` + version + `": {
"name": "` + packageName + `",
"version": "` + version + `",
"description": "` + packageDescription + `",
"author": {
"name": "` + packageAuthor + `"
},
"dist": {
"integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==",
"shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90"
}
}
},
"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))
Expand All @@ -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)

Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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)
})
})
}
18 changes: 16 additions & 2 deletions routers/api/packages/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,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)
Expand Down
57 changes: 57 additions & 0 deletions routers/api/packages/npm/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down