Skip to content

Commit c5068e8

Browse files
KN4CK3Rsilverwindwxiaoguang
authored and
Sysoev, Vladimir
committed
Allow multiple files in generic packages (go-gitea#20661)
* Allow multiple files in generic packages. * Add deletion of a single file. * Update docs. * Change version check. Co-authored-by: silverwind <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 6a83ccd commit c5068e8

File tree

4 files changed

+254
-76
lines changed

4 files changed

+254
-76
lines changed

docs/content/doc/packages/generic.en-us.md

+70-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ To authenticate to the Package Registry, you need to provide [custom HTTP header
2727
## Publish a package
2828

2929
To publish a generic package perform a HTTP PUT operation with the package content in the request body.
30-
You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first.
30+
You cannot publish a file with the same name twice to a package. You must delete the existing package version first.
3131

3232
```
3333
PUT https://gitea.example.com/api/packages/{owner}/generic/{package_name}/{package_version}/{file_name}
@@ -36,9 +36,9 @@ PUT https://gitea.example.com/api/packages/{owner}/generic/{package_name}/{packa
3636
| Parameter | Description |
3737
| ----------------- | ----------- |
3838
| `owner` | The owner of the package. |
39-
| `package_name` | The package name. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`). |
40-
| `package_version` | The package version, a non-empty string. |
41-
| `file_name` | The filename. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), or underscores (`_`). |
39+
| `package_name` | The package name. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), pluses (`+`), or underscores (`_`). |
40+
| `package_version` | The package version, a non-empty string without trailing or leading whitespaces. |
41+
| `file_name` | The filename. It can contain only lowercase letters (`a-z`), uppercase letter (`A-Z`), numbers (`0-9`), dots (`.`), hyphens (`-`), pluses (`+`), or underscores (`_`). |
4242

4343
Example request using HTTP Basic authentication:
4444

@@ -55,7 +55,8 @@ The server reponds with the following HTTP Status codes.
5555
| HTTP Status Code | Meaning |
5656
| ----------------- | ------- |
5757
| `201 Created` | The package has been published. |
58-
| `400 Bad Request` | The package name and/or version are invalid or a package with the same name and version already exist. |
58+
| `400 Bad Request` | The package name and/or version and/or file name are invalid. |
59+
| `409 Conflict` | A file with the same name exist already in the package. |
5960

6061
## Download a package
6162

@@ -80,3 +81,67 @@ Example request using HTTP Basic authentication:
8081
curl --user your_username:your_token_or_password \
8182
https://gitea.example.com/api/packages/testuser/generic/test_package/1.0.0/file.bin
8283
```
84+
85+
The server reponds with the following HTTP Status codes.
86+
87+
| HTTP Status Code | Meaning |
88+
| ----------------- | ------- |
89+
| `200 OK` | Success |
90+
| `404 Not Found` | The package or file was not found. |
91+
92+
## Delete a package
93+
94+
To delete a generic package perform a HTTP DELETE operation. This will delete all files of this version.
95+
96+
```
97+
DELETE https://gitea.example.com/api/packages/{owner}/generic/{package_name}/{package_version}
98+
```
99+
100+
| Parameter | Description |
101+
| ----------------- | ----------- |
102+
| `owner` | The owner of the package. |
103+
| `package_name` | The package name. |
104+
| `package_version` | The package version. |
105+
106+
Example request using HTTP Basic authentication:
107+
108+
```shell
109+
curl --user your_username:your_token_or_password -X DELETE \
110+
https://gitea.example.com/api/packages/testuser/generic/test_package/1.0.0
111+
```
112+
113+
The server reponds with the following HTTP Status codes.
114+
115+
| HTTP Status Code | Meaning |
116+
| ----------------- | ------- |
117+
| `204 No Content` | Success |
118+
| `404 Not Found` | The package was not found. |
119+
120+
## Delete a package file
121+
122+
To delete a file of a generic package perform a HTTP DELETE operation. This will delete the package version too if there is no file left.
123+
124+
```
125+
DELETE https://gitea.example.com/api/packages/{owner}/generic/{package_name}/{package_version}/{filename}
126+
```
127+
128+
| Parameter | Description |
129+
| ----------------- | ----------- |
130+
| `owner` | The owner of the package. |
131+
| `package_name` | The package name. |
132+
| `package_version` | The package version. |
133+
| `filename` | The filename. |
134+
135+
Example request using HTTP Basic authentication:
136+
137+
```shell
138+
curl --user your_username:your_token_or_password -X DELETE \
139+
https://gitea.example.com/api/packages/testuser/generic/test_package/1.0.0/file.bin
140+
```
141+
142+
The server reponds with the following HTTP Status codes.
143+
144+
| HTTP Status Code | Meaning |
145+
| ----------------- | ------- |
146+
| `204 No Content` | Success |
147+
| `404 Not Found` | The package or file was not found. |

integrations/api_packages_generic_test.go

+116-31
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@ func TestPackageGeneric(t *testing.T) {
2323
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
2424

2525
packageName := "te-st_pac.kage"
26-
packageVersion := "1.0.3"
26+
packageVersion := "1.0.3-te st"
2727
filename := "fi-le_na.me"
2828
content := []byte{1, 2, 3}
2929

30-
url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, filename)
30+
url := fmt.Sprintf("/api/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)
3131

3232
t.Run("Upload", func(t *testing.T) {
3333
defer PrintCurrentTest(t)()
3434

35-
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
35+
req := NewRequestWithBody(t, "PUT", url+"/"+filename, bytes.NewReader(content))
3636
AddBasicAuthHeader(req, user.Name)
3737
MakeRequest(t, req, http.StatusCreated)
3838

@@ -55,54 +55,139 @@ func TestPackageGeneric(t *testing.T) {
5555
pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
5656
assert.NoError(t, err)
5757
assert.Equal(t, int64(len(content)), pb.Size)
58-
})
5958

60-
t.Run("UploadExists", func(t *testing.T) {
61-
defer PrintCurrentTest(t)()
59+
t.Run("Exists", func(t *testing.T) {
60+
defer PrintCurrentTest(t)()
6261

63-
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
64-
AddBasicAuthHeader(req, user.Name)
65-
MakeRequest(t, req, http.StatusBadRequest)
62+
req := NewRequestWithBody(t, "PUT", url+"/"+filename, bytes.NewReader(content))
63+
AddBasicAuthHeader(req, user.Name)
64+
MakeRequest(t, req, http.StatusConflict)
65+
})
66+
67+
t.Run("Additional", func(t *testing.T) {
68+
defer PrintCurrentTest(t)()
69+
70+
req := NewRequestWithBody(t, "PUT", url+"/dummy.bin", bytes.NewReader(content))
71+
AddBasicAuthHeader(req, user.Name)
72+
MakeRequest(t, req, http.StatusCreated)
73+
74+
// Check deduplication
75+
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
76+
assert.NoError(t, err)
77+
assert.Len(t, pfs, 2)
78+
assert.Equal(t, pfs[0].BlobID, pfs[1].BlobID)
79+
})
80+
81+
t.Run("InvalidParameter", func(t *testing.T) {
82+
defer PrintCurrentTest(t)()
83+
84+
req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, "invalid+package name", packageVersion, filename), bytes.NewReader(content))
85+
AddBasicAuthHeader(req, user.Name)
86+
MakeRequest(t, req, http.StatusBadRequest)
87+
88+
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, "%20test ", filename), bytes.NewReader(content))
89+
AddBasicAuthHeader(req, user.Name)
90+
MakeRequest(t, req, http.StatusBadRequest)
91+
92+
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, "inval+id.na me"), bytes.NewReader(content))
93+
AddBasicAuthHeader(req, user.Name)
94+
MakeRequest(t, req, http.StatusBadRequest)
95+
})
6696
})
6797

6898
t.Run("Download", func(t *testing.T) {
6999
defer PrintCurrentTest(t)()
70100

71-
req := NewRequest(t, "GET", url)
101+
checkDownloadCount := func(count int64) {
102+
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
103+
assert.NoError(t, err)
104+
assert.Len(t, pvs, 1)
105+
assert.Equal(t, count, pvs[0].DownloadCount)
106+
}
107+
108+
checkDownloadCount(0)
109+
110+
req := NewRequest(t, "GET", url+"/"+filename)
72111
resp := MakeRequest(t, req, http.StatusOK)
73112

74113
assert.Equal(t, content, resp.Body.Bytes())
75114

76-
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
77-
assert.NoError(t, err)
78-
assert.Len(t, pvs, 1)
79-
assert.Equal(t, int64(1), pvs[0].DownloadCount)
115+
checkDownloadCount(1)
116+
117+
req = NewRequest(t, "GET", url+"/dummy.bin")
118+
MakeRequest(t, req, http.StatusOK)
119+
120+
checkDownloadCount(2)
121+
122+
t.Run("NotExists", func(t *testing.T) {
123+
defer PrintCurrentTest(t)()
124+
125+
req := NewRequest(t, "GET", url+"/not.found")
126+
MakeRequest(t, req, http.StatusNotFound)
127+
})
80128
})
81129

82130
t.Run("Delete", func(t *testing.T) {
83131
defer PrintCurrentTest(t)()
84132

85-
req := NewRequest(t, "DELETE", url)
86-
AddBasicAuthHeader(req, user.Name)
87-
MakeRequest(t, req, http.StatusOK)
133+
t.Run("File", func(t *testing.T) {
134+
defer PrintCurrentTest(t)()
88135

89-
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
90-
assert.NoError(t, err)
91-
assert.Empty(t, pvs)
92-
})
136+
req := NewRequest(t, "DELETE", url+"/"+filename)
137+
MakeRequest(t, req, http.StatusUnauthorized)
93138

94-
t.Run("DownloadNotExists", func(t *testing.T) {
95-
defer PrintCurrentTest(t)()
139+
req = NewRequest(t, "DELETE", url+"/"+filename)
140+
AddBasicAuthHeader(req, user.Name)
141+
MakeRequest(t, req, http.StatusNoContent)
96142

97-
req := NewRequest(t, "GET", url)
98-
MakeRequest(t, req, http.StatusNotFound)
99-
})
143+
req = NewRequest(t, "GET", url+"/"+filename)
144+
MakeRequest(t, req, http.StatusNotFound)
100145

101-
t.Run("DeleteNotExists", func(t *testing.T) {
102-
defer PrintCurrentTest(t)()
146+
req = NewRequest(t, "DELETE", url+"/"+filename)
147+
AddBasicAuthHeader(req, user.Name)
148+
MakeRequest(t, req, http.StatusNotFound)
103149

104-
req := NewRequest(t, "DELETE", url)
105-
AddBasicAuthHeader(req, user.Name)
106-
MakeRequest(t, req, http.StatusNotFound)
150+
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
151+
assert.NoError(t, err)
152+
assert.Len(t, pvs, 1)
153+
154+
t.Run("RemovesVersion", func(t *testing.T) {
155+
defer PrintCurrentTest(t)()
156+
157+
req = NewRequest(t, "DELETE", url+"/dummy.bin")
158+
AddBasicAuthHeader(req, user.Name)
159+
MakeRequest(t, req, http.StatusNoContent)
160+
161+
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
162+
assert.NoError(t, err)
163+
assert.Empty(t, pvs)
164+
})
165+
})
166+
167+
t.Run("Version", func(t *testing.T) {
168+
defer PrintCurrentTest(t)()
169+
170+
req := NewRequestWithBody(t, "PUT", url+"/"+filename, bytes.NewReader(content))
171+
AddBasicAuthHeader(req, user.Name)
172+
MakeRequest(t, req, http.StatusCreated)
173+
174+
req = NewRequest(t, "DELETE", url)
175+
MakeRequest(t, req, http.StatusUnauthorized)
176+
177+
req = NewRequest(t, "DELETE", url)
178+
AddBasicAuthHeader(req, user.Name)
179+
MakeRequest(t, req, http.StatusNoContent)
180+
181+
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
182+
assert.NoError(t, err)
183+
assert.Empty(t, pvs)
184+
185+
req = NewRequest(t, "GET", url+"/"+filename)
186+
MakeRequest(t, req, http.StatusNotFound)
187+
188+
req = NewRequest(t, "DELETE", url)
189+
AddBasicAuthHeader(req, user.Name)
190+
MakeRequest(t, req, http.StatusNotFound)
191+
})
107192
})
108193
}

routers/api/packages/api.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,15 @@ func Routes() *web.Route {
156156
})
157157
})
158158
r.Group("/generic", func() {
159-
r.Group("/{packagename}/{packageversion}/{filename}", func() {
160-
r.Get("", generic.DownloadPackageFile)
161-
r.Group("", func() {
162-
r.Put("", generic.UploadPackage)
163-
r.Delete("", generic.DeletePackage)
164-
}, reqPackageAccess(perm.AccessModeWrite))
159+
r.Group("/{packagename}/{packageversion}", func() {
160+
r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage)
161+
r.Group("/{filename}", func() {
162+
r.Get("", generic.DownloadPackageFile)
163+
r.Group("", func() {
164+
r.Put("", generic.UploadPackage)
165+
r.Delete("", generic.DeletePackageFile)
166+
}, reqPackageAccess(perm.AccessModeWrite))
167+
})
165168
})
166169
})
167170
r.Group("/helm", func() {

0 commit comments

Comments
 (0)