Skip to content

Commit 381ba48

Browse files
KN4CK3RSysoev, Vladimir
authored and
Sysoev, Vladimir
committed
Add support for npm unpublish (go-gitea#20688)
1 parent c5068e8 commit 381ba48

File tree

4 files changed

+175
-27
lines changed

4 files changed

+175
-27
lines changed

Diff for: docs/content/doc/packages/npm.en-us.md

+21
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,26 @@ npm publish
6767

6868
You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first.
6969

70+
## Unpublish a package
71+
72+
Delete a package by running the following command:
73+
74+
```shell
75+
npm unpublish {package_name}[@{package_version}]
76+
```
77+
78+
| Parameter | Description |
79+
| ----------------- | ----------- |
80+
| `package_name` | The package name. |
81+
| `package_version` | The package version. |
82+
83+
For example:
84+
85+
```shell
86+
npm unpublish @test/test_package
87+
npm unpublish @test/[email protected]
88+
```
89+
7090
## Install a package
7191

7292
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
113133
npm install
114134
npm ci
115135
npm publish
136+
npm unpublish
116137
npm dist-tag
117138
npm view
118139
```

Diff for: integrations/api_packages_npm_test.go

+81-25
Original file line numberDiff line numberDiff line change
@@ -36,33 +36,36 @@ func TestPackageNpm(t *testing.T) {
3636
packageDescription := "Test Description"
3737

3838
data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
39-
upload := `{
40-
"_id": "` + packageName + `",
41-
"name": "` + packageName + `",
42-
"description": "` + packageDescription + `",
43-
"dist-tags": {
44-
"` + packageTag + `": "` + packageVersion + `"
45-
},
46-
"versions": {
47-
"` + packageVersion + `": {
39+
40+
buildUpload := func(version string) string {
41+
return `{
42+
"_id": "` + packageName + `",
4843
"name": "` + packageName + `",
49-
"version": "` + packageVersion + `",
5044
"description": "` + packageDescription + `",
51-
"author": {
52-
"name": "` + packageAuthor + `"
45+
"dist-tags": {
46+
"` + packageTag + `": "` + version + `"
47+
},
48+
"versions": {
49+
"` + version + `": {
50+
"name": "` + packageName + `",
51+
"version": "` + version + `",
52+
"description": "` + packageDescription + `",
53+
"author": {
54+
"name": "` + packageAuthor + `"
55+
},
56+
"dist": {
57+
"integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==",
58+
"shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90"
59+
}
60+
}
5361
},
54-
"dist": {
55-
"integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==",
56-
"shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90"
62+
"_attachments": {
63+
"` + packageName + `-` + version + `.tgz": {
64+
"data": "` + data + `"
65+
}
5766
}
58-
}
59-
},
60-
"_attachments": {
61-
"` + packageName + `-` + packageVersion + `.tgz": {
62-
"data": "` + data + `"
63-
}
64-
}
65-
}`
67+
}`
68+
}
6669

6770
root := fmt.Sprintf("/api/packages/%s/npm/%s", user.Name, url.QueryEscape(packageName))
6871
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) {
7174
t.Run("Upload", func(t *testing.T) {
7275
defer PrintCurrentTest(t)()
7376

74-
req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload))
77+
req := NewRequestWithBody(t, "PUT", root, strings.NewReader(buildUpload(packageVersion)))
7578
req = addTokenAuthHeader(req, token)
7679
MakeRequest(t, req, http.StatusCreated)
7780

@@ -103,7 +106,7 @@ func TestPackageNpm(t *testing.T) {
103106
t.Run("UploadExists", func(t *testing.T) {
104107
defer PrintCurrentTest(t)()
105108

106-
req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload))
109+
req := NewRequestWithBody(t, "PUT", root, strings.NewReader(buildUpload(packageVersion)))
107110
req = addTokenAuthHeader(req, token)
108111
MakeRequest(t, req, http.StatusBadRequest)
109112
})
@@ -219,4 +222,57 @@ func TestPackageNpm(t *testing.T) {
219222
test(t, http.StatusOK, "dummy")
220223
test(t, http.StatusOK, packageTag2)
221224
})
225+
226+
t.Run("Delete", func(t *testing.T) {
227+
defer PrintCurrentTest(t)()
228+
229+
req := NewRequestWithBody(t, "PUT", root, strings.NewReader(buildUpload(packageVersion+"-dummy")))
230+
req = addTokenAuthHeader(req, token)
231+
MakeRequest(t, req, http.StatusCreated)
232+
233+
req = NewRequest(t, "PUT", root+"/-rev/dummy")
234+
MakeRequest(t, req, http.StatusUnauthorized)
235+
236+
req = NewRequest(t, "PUT", root+"/-rev/dummy")
237+
req = addTokenAuthHeader(req, token)
238+
MakeRequest(t, req, http.StatusOK)
239+
240+
t.Run("Version", func(t *testing.T) {
241+
defer PrintCurrentTest(t)()
242+
243+
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
244+
assert.NoError(t, err)
245+
assert.Len(t, pvs, 2)
246+
247+
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename))
248+
MakeRequest(t, req, http.StatusUnauthorized)
249+
250+
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/-/%s/%s/-rev/dummy", root, packageVersion, filename))
251+
req = addTokenAuthHeader(req, token)
252+
MakeRequest(t, req, http.StatusOK)
253+
254+
pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
255+
assert.NoError(t, err)
256+
assert.Len(t, pvs, 1)
257+
})
258+
259+
t.Run("Full", func(t *testing.T) {
260+
defer PrintCurrentTest(t)()
261+
262+
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
263+
assert.NoError(t, err)
264+
assert.Len(t, pvs, 1)
265+
266+
req := NewRequest(t, "DELETE", root+"/-rev/dummy")
267+
MakeRequest(t, req, http.StatusUnauthorized)
268+
269+
req = NewRequest(t, "DELETE", root+"/-rev/dummy")
270+
req = addTokenAuthHeader(req, token)
271+
MakeRequest(t, req, http.StatusOK)
272+
273+
pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
274+
assert.NoError(t, err)
275+
assert.Len(t, pvs, 0)
276+
})
277+
})
222278
}

Diff for: routers/api/packages/api.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,26 @@ func Routes() *web.Route {
198198
r.Group("/@{scope}/{id}", func() {
199199
r.Get("", npm.PackageMetadata)
200200
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
201-
r.Get("/-/{version}/{filename}", npm.DownloadPackageFile)
201+
r.Group("/-/{version}/{filename}", func() {
202+
r.Get("", npm.DownloadPackageFile)
203+
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
204+
})
205+
r.Group("/-rev/{revision}", func() {
206+
r.Delete("", npm.DeletePackage)
207+
r.Put("", npm.DeletePreview)
208+
}, reqPackageAccess(perm.AccessModeWrite))
202209
})
203210
r.Group("/{id}", func() {
204211
r.Get("", npm.PackageMetadata)
205212
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
206-
r.Get("/-/{version}/{filename}", npm.DownloadPackageFile)
213+
r.Group("/-/{version}/{filename}", func() {
214+
r.Get("", npm.DownloadPackageFile)
215+
r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
216+
})
217+
r.Group("/-rev/{revision}", func() {
218+
r.Delete("", npm.DeletePackage)
219+
r.Put("", npm.DeletePreview)
220+
}, reqPackageAccess(perm.AccessModeWrite))
207221
})
208222
r.Group("/-/package/@{scope}/{id}/dist-tags", func() {
209223
r.Get("", npm.ListPackageTags)

Diff for: routers/api/packages/npm/npm.go

+57
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,63 @@ func UploadPackage(ctx *context.Context) {
164164
ctx.Status(http.StatusCreated)
165165
}
166166

167+
// DeletePreview does nothing
168+
// The client tells the server what package version it knows about after deleting a version.
169+
func DeletePreview(ctx *context.Context) {
170+
ctx.Status(http.StatusOK)
171+
}
172+
173+
// DeletePackageVersion deletes the package version
174+
func DeletePackageVersion(ctx *context.Context) {
175+
packageName := packageNameFromParams(ctx)
176+
packageVersion := ctx.Params("version")
177+
178+
err := packages_service.RemovePackageVersionByNameAndVersion(
179+
ctx.Doer,
180+
&packages_service.PackageInfo{
181+
Owner: ctx.Package.Owner,
182+
PackageType: packages_model.TypeNpm,
183+
Name: packageName,
184+
Version: packageVersion,
185+
},
186+
)
187+
if err != nil {
188+
if err == packages_model.ErrPackageNotExist {
189+
apiError(ctx, http.StatusNotFound, err)
190+
return
191+
}
192+
apiError(ctx, http.StatusInternalServerError, err)
193+
return
194+
}
195+
196+
ctx.Status(http.StatusOK)
197+
}
198+
199+
// DeletePackage deletes the package and all versions
200+
func DeletePackage(ctx *context.Context) {
201+
packageName := packageNameFromParams(ctx)
202+
203+
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
204+
if err != nil {
205+
apiError(ctx, http.StatusInternalServerError, err)
206+
return
207+
}
208+
209+
if len(pvs) == 0 {
210+
apiError(ctx, http.StatusNotFound, err)
211+
return
212+
}
213+
214+
for _, pv := range pvs {
215+
if err := packages_service.RemovePackageVersion(ctx.Doer, pv); err != nil {
216+
apiError(ctx, http.StatusInternalServerError, err)
217+
return
218+
}
219+
}
220+
221+
ctx.Status(http.StatusOK)
222+
}
223+
167224
// ListPackageTags returns all tags for a package
168225
func ListPackageTags(ctx *context.Context) {
169226
packageName := packageNameFromParams(ctx)

0 commit comments

Comments
 (0)