This commit is contained in:
KN4CK3R 2025-04-13 05:04:22 -04:00 committed by GitHub
commit 97c2f47d79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 334 additions and 80 deletions

View File

@ -23,8 +23,8 @@ type Package struct {
// PackageFile represents a package file
type PackageFile struct {
ID int64 `json:"id"`
Size int64
ID int64 `json:"id"`
Size int64 `json:"size"`
Name string `json:"name"`
HashMD5 string `json:"md5"`
HashSHA1 string `json:"sha1"`

View File

@ -1544,14 +1544,19 @@ func Routes() *web.Router {
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
m.Group("/packages/{username}", func() {
m.Group("/{type}/{name}", func() {
m.Get("/", packages.ListPackageVersions)
m.Group("/{version}", func() {
m.Get("", packages.GetPackage)
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
m.Get("/files", packages.ListPackageFiles)
})
m.Post("/-/link/{repo_name}", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage)
m.Post("/-/unlink", reqPackageAccess(perm.AccessModeWrite), packages.UnlinkPackage)
m.Group("/-", func() {
m.Get("/latest", packages.GetLatestPackageVersion)
m.Post("/link/{repo_name}", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage)
m.Post("/unlink", reqPackageAccess(perm.AccessModeWrite), packages.UnlinkPackage)
})
})
m.Get("/", packages.ListPackages)

View File

@ -56,13 +56,10 @@ func ListPackages(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
packageType := ctx.FormTrim("type")
query := ctx.FormTrim("q")
pvs, count, err := packages.SearchVersions(ctx, &packages.PackageSearchOptions{
apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(packageType),
Name: packages.SearchValue{Value: query},
Type: packages.Type(ctx.FormTrim("type")),
Name: packages.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: optional.Some(false),
Paginator: &listOptions,
})
@ -71,22 +68,6 @@ func ListPackages(ctx *context.APIContext) {
return
}
pds, err := packages.GetPackageDescriptors(ctx, pvs)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiPackages := make([]*api.Package, 0, len(pds))
for _, pd := range pds {
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiPackages = append(apiPackages, apiPackage)
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiPackages)
@ -217,6 +198,121 @@ func ListPackageFiles(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiPackageFiles)
}
// ListPackageVersions gets all versions of a package
func ListPackageVersions(ctx *context.APIContext) {
// swagger:operation GET /packages/{owner}/{type}/{name} package listPackageVersions
// ---
// summary: Gets all versions of a package
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/PackageList"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(ctx.PathParam("type")),
Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true},
IsInternal: optional.Some(false),
Paginator: &listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiPackages)
}
// GetLatestPackageVersion gets the latest version of a package
func GetLatestPackageVersion(ctx *context.APIContext) {
// swagger:operation GET /packages/{owner}/{type}/{name}/-/latest package getLatestPackageVersion
// ---
// summary: Gets the latest version of a package
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Package"
// "404":
// "$ref": "#/responses/notFound"
pvs, _, err := packages.SearchLatestVersions(ctx, &packages.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(ctx.PathParam("type")),
Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true},
IsInternal: optional.Some(false),
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
if len(pvs) == 0 {
ctx.APIError(http.StatusNotFound, err)
return
}
pd, err := packages.GetPackageDescriptor(ctx, pvs[0])
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiPackage)
}
// LinkPackage sets a repository link for a package
func LinkPackage(ctx *context.APIContext) {
// swagger:operation POST /packages/{owner}/{type}/{name}/-/link/{repo_name} package linkPackage
@ -335,3 +431,26 @@ func UnlinkPackage(ctx *context.APIContext) {
}
ctx.Status(http.StatusNoContent)
}
func searchPackages(ctx *context.APIContext, opts *packages.PackageSearchOptions) ([]*api.Package, int64, error) {
pvs, count, err := packages.SearchVersions(ctx, opts)
if err != nil {
return nil, 0, err
}
pds, err := packages.GetPackageDescriptors(ctx, pvs)
if err != nil {
return nil, 0, err
}
apiPackages := make([]*api.Package, 0, len(pds))
for _, pd := range pds {
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
if err != nil {
return nil, 0, err
}
apiPackages = append(apiPackages, apiPackage)
}
return apiPackages, count, nil
}

View File

@ -3339,6 +3339,104 @@
}
}
},
"/packages/{owner}/{type}/{name}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"package"
],
"summary": "Gets all versions of a package",
"operationId": "listPackageVersions",
"parameters": [
{
"type": "string",
"description": "owner of the package",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "type of the package",
"name": "type",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the package",
"name": "name",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/PackageList"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/packages/{owner}/{type}/{name}/-/latest": {
"get": {
"produces": [
"application/json"
],
"tags": [
"package"
],
"summary": "Gets the latest version of a package",
"operationId": "getLatestPackageVersion",
"parameters": [
{
"type": "string",
"description": "owner of the package",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "type of the package",
"name": "type",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the package",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/Package"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/packages/{owner}/{type}/{name}/-/link/{repo_name}": {
"post": {
"tags": [
@ -24386,10 +24484,6 @@
"description": "PackageFile represents a package file",
"type": "object",
"properties": {
"Size": {
"type": "integer",
"format": "int64"
},
"id": {
"type": "integer",
"format": "int64",
@ -24414,6 +24508,11 @@
"sha512": {
"type": "string",
"x-go-name": "HashSHA512"
},
"size": {
"type": "integer",
"format": "int64",
"x-go-name": "Size"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"

View File

@ -83,70 +83,101 @@ func TestPackageAPI(t *testing.T) {
assert.Equal(t, packageVersion, p.Version)
assert.NotNil(t, p.Creator)
assert.Equal(t, user.Name, p.Creator.UserName)
})
t.Run("RepositoryLink", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("ListPackageVersions", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
_, err := packages_model.GetPackageByName(db.DefaultContext, user.ID, packages_model.TypeGeneric, packageName)
assert.NoError(t, err)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)).
AddTokenAuth(tokenReadPackage)
resp := MakeRequest(t, req, http.StatusOK)
// no repository link
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
AddTokenAuth(tokenReadPackage)
resp := MakeRequest(t, req, http.StatusOK)
var apiPackages []*api.Package
DecodeJSON(t, resp, &apiPackages)
var ap1 *api.Package
DecodeJSON(t, resp, &ap1)
assert.Nil(t, ap1.Repository)
assert.Len(t, apiPackages, 1)
assert.Equal(t, string(packages_model.TypeGeneric), apiPackages[0].Type)
assert.Equal(t, packageName, apiPackages[0].Name)
assert.Equal(t, packageVersion, apiPackages[0].Version)
})
// create a repository
newRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
Name: "repo4",
})
assert.NoError(t, err)
t.Run("LatestPackageVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// link to public repository
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/link/%s", user.Name, packageName, newRepo.Name)).AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusCreated)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/latest", user.Name, packageName)).
AddTokenAuth(tokenReadPackage)
resp := MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
AddTokenAuth(tokenReadPackage)
resp = MakeRequest(t, req, http.StatusOK)
var apiPackage *api.Package
DecodeJSON(t, resp, &apiPackage)
var ap2 *api.Package
DecodeJSON(t, resp, &ap2)
assert.NotNil(t, ap2.Repository)
assert.Equal(t, newRepo.ID, ap2.Repository.ID)
assert.Equal(t, string(packages_model.TypeGeneric), apiPackage.Type)
assert.Equal(t, packageName, apiPackage.Name)
assert.Equal(t, packageVersion, apiPackage.Version)
})
// link to repository without write access, should fail
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/link/%s", user.Name, packageName, "repo3")).AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusNotFound)
t.Run("RepositoryLink", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// remove link
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/unlink", user.Name, packageName)).AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusNoContent)
_, err := packages_model.GetPackageByName(db.DefaultContext, user.ID, packages_model.TypeGeneric, packageName)
assert.NoError(t, err)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
AddTokenAuth(tokenReadPackage)
resp = MakeRequest(t, req, http.StatusOK)
// no repository link
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
AddTokenAuth(tokenReadPackage)
resp := MakeRequest(t, req, http.StatusOK)
var ap3 *api.Package
DecodeJSON(t, resp, &ap3)
assert.Nil(t, ap3.Repository)
var ap1 *api.Package
DecodeJSON(t, resp, &ap1)
assert.Nil(t, ap1.Repository)
// force link to a repository the currently logged-in user doesn't have access to
privateRepoID := int64(6)
assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, privateRepoID))
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).AddTokenAuth(tokenReadPackage)
resp = MakeRequest(t, req, http.StatusOK)
var ap4 *api.Package
DecodeJSON(t, resp, &ap4)
assert.Nil(t, ap4.Repository)
assert.NoError(t, packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, privateRepoID))
// create a repository
newRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
Name: "repo4",
})
assert.NoError(t, err)
// link to public repository
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/link/%s", user.Name, packageName, newRepo.Name)).AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusCreated)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
AddTokenAuth(tokenReadPackage)
resp = MakeRequest(t, req, http.StatusOK)
var ap2 *api.Package
DecodeJSON(t, resp, &ap2)
assert.NotNil(t, ap2.Repository)
assert.Equal(t, newRepo.ID, ap2.Repository.ID)
// link to repository without write access, should fail
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/link/%s", user.Name, packageName, "repo3")).AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusNotFound)
// remove link
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/unlink", user.Name, packageName)).AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
AddTokenAuth(tokenReadPackage)
resp = MakeRequest(t, req, http.StatusOK)
var ap3 *api.Package
DecodeJSON(t, resp, &ap3)
assert.Nil(t, ap3.Repository)
// force link to a repository the currently logged-in user doesn't have access to
privateRepoID := int64(6)
assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, ap1.ID, privateRepoID))
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).AddTokenAuth(tokenReadPackage)
resp = MakeRequest(t, req, http.StatusOK)
var ap4 *api.Package
DecodeJSON(t, resp, &ap4)
assert.Nil(t, ap4.Repository)
assert.NoError(t, packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, privateRepoID))
})
t.Run("ListPackageFiles", func(t *testing.T) {