Add support for npm unpublish
(#20688)
This commit is contained in:
parent
cc6927b2d8
commit
fba20550f9
4 changed files with 175 additions and 27 deletions
|
@ -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.
|
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
|
## Install a package
|
||||||
|
|
||||||
To install a package from the package registry, execute the following command:
|
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 install
|
||||||
npm ci
|
npm ci
|
||||||
npm publish
|
npm publish
|
||||||
|
npm unpublish
|
||||||
npm dist-tag
|
npm dist-tag
|
||||||
npm view
|
npm view
|
||||||
```
|
```
|
||||||
|
|
|
@ -36,17 +36,19 @@ func TestPackageNpm(t *testing.T) {
|
||||||
packageDescription := "Test Description"
|
packageDescription := "Test Description"
|
||||||
|
|
||||||
data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
|
data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
|
||||||
upload := `{
|
|
||||||
|
buildUpload := func(version string) string {
|
||||||
|
return `{
|
||||||
"_id": "` + packageName + `",
|
"_id": "` + packageName + `",
|
||||||
"name": "` + packageName + `",
|
"name": "` + packageName + `",
|
||||||
"description": "` + packageDescription + `",
|
"description": "` + packageDescription + `",
|
||||||
"dist-tags": {
|
"dist-tags": {
|
||||||
"` + packageTag + `": "` + packageVersion + `"
|
"` + packageTag + `": "` + version + `"
|
||||||
},
|
},
|
||||||
"versions": {
|
"versions": {
|
||||||
"` + packageVersion + `": {
|
"` + version + `": {
|
||||||
"name": "` + packageName + `",
|
"name": "` + packageName + `",
|
||||||
"version": "` + packageVersion + `",
|
"version": "` + version + `",
|
||||||
"description": "` + packageDescription + `",
|
"description": "` + packageDescription + `",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "` + packageAuthor + `"
|
"name": "` + packageAuthor + `"
|
||||||
|
@ -58,11 +60,12 @@ func TestPackageNpm(t *testing.T) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"_attachments": {
|
"_attachments": {
|
||||||
"` + packageName + `-` + packageVersion + `.tgz": {
|
"` + packageName + `-` + version + `.tgz": {
|
||||||
"data": "` + data + `"
|
"data": "` + data + `"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
root := fmt.Sprintf("/api/packages/%s/npm/%s", user.Name, url.QueryEscape(packageName))
|
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))
|
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) {
|
t.Run("Upload", func(t *testing.T) {
|
||||||
defer PrintCurrentTest(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)
|
req = addTokenAuthHeader(req, token)
|
||||||
MakeRequest(t, req, http.StatusCreated)
|
MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
|
@ -103,7 +106,7 @@ func TestPackageNpm(t *testing.T) {
|
||||||
t.Run("UploadExists", func(t *testing.T) {
|
t.Run("UploadExists", func(t *testing.T) {
|
||||||
defer PrintCurrentTest(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)
|
req = addTokenAuthHeader(req, token)
|
||||||
MakeRequest(t, req, http.StatusBadRequest)
|
MakeRequest(t, req, http.StatusBadRequest)
|
||||||
})
|
})
|
||||||
|
@ -219,4 +222,57 @@ func TestPackageNpm(t *testing.T) {
|
||||||
test(t, http.StatusOK, "dummy")
|
test(t, http.StatusOK, "dummy")
|
||||||
test(t, http.StatusOK, packageTag2)
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,12 +198,26 @@ func Routes() *web.Route {
|
||||||
r.Group("/@{scope}/{id}", func() {
|
r.Group("/@{scope}/{id}", func() {
|
||||||
r.Get("", npm.PackageMetadata)
|
r.Get("", npm.PackageMetadata)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
|
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.Group("/{id}", func() {
|
||||||
r.Get("", npm.PackageMetadata)
|
r.Get("", npm.PackageMetadata)
|
||||||
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
|
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.Group("/-/package/@{scope}/{id}/dist-tags", func() {
|
||||||
r.Get("", npm.ListPackageTags)
|
r.Get("", npm.ListPackageTags)
|
||||||
|
|
|
@ -164,6 +164,63 @@ func UploadPackage(ctx *context.Context) {
|
||||||
ctx.Status(http.StatusCreated)
|
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
|
// ListPackageTags returns all tags for a package
|
||||||
func ListPackageTags(ctx *context.Context) {
|
func ListPackageTags(ctx *context.Context) {
|
||||||
packageName := packageNameFromParams(ctx)
|
packageName := packageNameFromParams(ctx)
|
||||||
|
|
Reference in a new issue