mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Add support for npm unpublish (#20688)
				
					
				
			This commit is contained in:
		@@ -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
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user