Skip to content

Commit 5df9fd3

Browse files
lunnyZettat123
andauthored
Add API to support link package to repository and unlink it (#33481)
Fix #21062 --------- Co-authored-by: Zettat123 <[email protected]>
1 parent 50a5d6b commit 5df9fd3

File tree

6 files changed

+337
-15
lines changed

6 files changed

+337
-15
lines changed

models/packages/package.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ func SetRepositoryLink(ctx context.Context, packageID, repoID int64) error {
228228
return err
229229
}
230230

231+
func UnlinkRepository(ctx context.Context, packageID int64) error {
232+
_, err := db.GetEngine(ctx).ID(packageID).Cols("repo_id").Update(&Package{RepoID: 0})
233+
return err
234+
}
235+
231236
// UnlinkRepositoryFromAllPackages unlinks every package from the repository
232237
func UnlinkRepositoryFromAllPackages(ctx context.Context, repoID int64) error {
233238
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Cols("repo_id").Update(&Package{})

routers/api/v1/api.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,13 +1537,19 @@ func Routes() *web.Router {
15371537

15381538
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
15391539
m.Group("/packages/{username}", func() {
1540-
m.Group("/{type}/{name}/{version}", func() {
1541-
m.Get("", reqToken(), packages.GetPackage)
1542-
m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
1543-
m.Get("/files", reqToken(), packages.ListPackageFiles)
1540+
m.Group("/{type}/{name}", func() {
1541+
m.Group("/{version}", func() {
1542+
m.Get("", packages.GetPackage)
1543+
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
1544+
m.Get("/files", packages.ListPackageFiles)
1545+
})
1546+
1547+
m.Post("/-/link/{repo_name}", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage)
1548+
m.Post("/-/unlink", reqPackageAccess(perm.AccessModeWrite), packages.UnlinkPackage)
15441549
})
1545-
m.Get("/", reqToken(), packages.ListPackages)
1546-
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
1550+
1551+
m.Get("/", packages.ListPackages)
1552+
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
15471553

15481554
// Organizations
15491555
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)

routers/api/v1/packages/package.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
package packages
55

66
import (
7+
"errors"
78
"net/http"
89

910
"code.gitea.io/gitea/models/packages"
11+
repo_model "code.gitea.io/gitea/models/repo"
1012
"code.gitea.io/gitea/modules/optional"
1113
api "code.gitea.io/gitea/modules/structs"
14+
"code.gitea.io/gitea/modules/util"
1215
"code.gitea.io/gitea/routers/api/v1/utils"
1316
"code.gitea.io/gitea/services/context"
1417
"code.gitea.io/gitea/services/convert"
@@ -213,3 +216,122 @@ func ListPackageFiles(ctx *context.APIContext) {
213216

214217
ctx.JSON(http.StatusOK, apiPackageFiles)
215218
}
219+
220+
// LinkPackage sets a repository link for a package
221+
func LinkPackage(ctx *context.APIContext) {
222+
// swagger:operation POST /packages/{owner}/{type}/{name}/-/link/{repo_name} package linkPackage
223+
// ---
224+
// summary: Link a package to a repository
225+
// parameters:
226+
// - name: owner
227+
// in: path
228+
// description: owner of the package
229+
// type: string
230+
// required: true
231+
// - name: type
232+
// in: path
233+
// description: type of the package
234+
// type: string
235+
// required: true
236+
// - name: name
237+
// in: path
238+
// description: name of the package
239+
// type: string
240+
// required: true
241+
// - name: repo_name
242+
// in: path
243+
// description: name of the repository to link.
244+
// type: string
245+
// required: true
246+
// responses:
247+
// "201":
248+
// "$ref": "#/responses/empty"
249+
// "404":
250+
// "$ref": "#/responses/notFound"
251+
252+
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
253+
if err != nil {
254+
if errors.Is(err, util.ErrNotExist) {
255+
ctx.Error(http.StatusNotFound, "GetPackageByName", err)
256+
} else {
257+
ctx.Error(http.StatusInternalServerError, "GetPackageByName", err)
258+
}
259+
return
260+
}
261+
262+
repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name"))
263+
if err != nil {
264+
if errors.Is(err, util.ErrNotExist) {
265+
ctx.Error(http.StatusNotFound, "GetRepositoryByName", err)
266+
} else {
267+
ctx.Error(http.StatusInternalServerError, "GetRepositoryByName", err)
268+
}
269+
return
270+
}
271+
272+
err = packages_service.LinkToRepository(ctx, pkg, repo, ctx.Doer)
273+
if err != nil {
274+
switch {
275+
case errors.Is(err, util.ErrInvalidArgument):
276+
ctx.Error(http.StatusBadRequest, "LinkToRepository", err)
277+
case errors.Is(err, util.ErrPermissionDenied):
278+
ctx.Error(http.StatusForbidden, "LinkToRepository", err)
279+
default:
280+
ctx.Error(http.StatusInternalServerError, "LinkToRepository", err)
281+
}
282+
return
283+
}
284+
ctx.Status(http.StatusCreated)
285+
}
286+
287+
// UnlinkPackage sets a repository link for a package
288+
func UnlinkPackage(ctx *context.APIContext) {
289+
// swagger:operation POST /packages/{owner}/{type}/{name}/-/unlink package unlinkPackage
290+
// ---
291+
// summary: Unlink a package from a repository
292+
// parameters:
293+
// - name: owner
294+
// in: path
295+
// description: owner of the package
296+
// type: string
297+
// required: true
298+
// - name: type
299+
// in: path
300+
// description: type of the package
301+
// type: string
302+
// required: true
303+
// - name: name
304+
// in: path
305+
// description: name of the package
306+
// type: string
307+
// required: true
308+
// responses:
309+
// "201":
310+
// "$ref": "#/responses/empty"
311+
// "404":
312+
// "$ref": "#/responses/notFound"
313+
314+
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
315+
if err != nil {
316+
if errors.Is(err, util.ErrNotExist) {
317+
ctx.Error(http.StatusNotFound, "GetPackageByName", err)
318+
} else {
319+
ctx.Error(http.StatusInternalServerError, "GetPackageByName", err)
320+
}
321+
return
322+
}
323+
324+
err = packages_service.UnlinkFromRepository(ctx, pkg, ctx.Doer)
325+
if err != nil {
326+
switch {
327+
case errors.Is(err, util.ErrPermissionDenied):
328+
ctx.Error(http.StatusForbidden, "UnlinkFromRepository", err)
329+
case errors.Is(err, util.ErrInvalidArgument):
330+
ctx.Error(http.StatusBadRequest, "UnlinkFromRepository", err)
331+
default:
332+
ctx.Error(http.StatusInternalServerError, "UnlinkFromRepository", err)
333+
}
334+
return
335+
}
336+
ctx.Status(http.StatusNoContent)
337+
}

services/packages/package_update.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package packages
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
org_model "code.gitea.io/gitea/models/organization"
11+
packages_model "code.gitea.io/gitea/models/packages"
12+
access_model "code.gitea.io/gitea/models/perm/access"
13+
repo_model "code.gitea.io/gitea/models/repo"
14+
"code.gitea.io/gitea/models/unit"
15+
user_model "code.gitea.io/gitea/models/user"
16+
"code.gitea.io/gitea/modules/util"
17+
)
18+
19+
func LinkToRepository(ctx context.Context, pkg *packages_model.Package, repo *repo_model.Repository, doer *user_model.User) error {
20+
if pkg.OwnerID != repo.OwnerID {
21+
return util.ErrPermissionDenied
22+
}
23+
if pkg.RepoID > 0 {
24+
return util.ErrInvalidArgument
25+
}
26+
27+
perms, err := access_model.GetUserRepoPermission(ctx, repo, doer)
28+
if err != nil {
29+
return fmt.Errorf("error getting permissions for user %d on repository %d: %w", doer.ID, repo.ID, err)
30+
}
31+
if !perms.CanWrite(unit.TypePackages) {
32+
return util.ErrPermissionDenied
33+
}
34+
35+
if err := packages_model.SetRepositoryLink(ctx, pkg.ID, repo.ID); err != nil {
36+
return fmt.Errorf("error while linking package '%v' to repo '%v' : %w", pkg.Name, repo.FullName(), err)
37+
}
38+
return nil
39+
}
40+
41+
func UnlinkFromRepository(ctx context.Context, pkg *packages_model.Package, doer *user_model.User) error {
42+
if pkg.RepoID == 0 {
43+
return util.ErrInvalidArgument
44+
}
45+
46+
repo, err := repo_model.GetRepositoryByID(ctx, pkg.RepoID)
47+
if err != nil {
48+
return fmt.Errorf("error getting repository %d: %w", pkg.RepoID, err)
49+
}
50+
51+
perms, err := access_model.GetUserRepoPermission(ctx, repo, doer)
52+
if err != nil {
53+
return fmt.Errorf("error getting permissions for user %d on repository %d: %w", doer.ID, repo.ID, err)
54+
}
55+
if !perms.CanWrite(unit.TypePackages) {
56+
return util.ErrPermissionDenied
57+
}
58+
59+
user, err := user_model.GetUserByID(ctx, pkg.OwnerID)
60+
if err != nil {
61+
return err
62+
}
63+
if !doer.IsAdmin {
64+
if !user.IsOrganization() {
65+
if doer.ID != pkg.OwnerID {
66+
return fmt.Errorf("no permission to unlink package '%v' from its repository, or packages are disabled", pkg.Name)
67+
}
68+
} else {
69+
isOrgAdmin, err := org_model.OrgFromUser(user).IsOrgAdmin(ctx, doer.ID)
70+
if err != nil {
71+
return err
72+
} else if !isOrgAdmin {
73+
return fmt.Errorf("no permission to unlink package '%v' from its repository, or packages are disabled", pkg.Name)
74+
}
75+
}
76+
}
77+
return packages_model.UnlinkRepository(ctx, pkg.ID)
78+
}

templates/swagger/v1_json.tmpl

Lines changed: 87 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)