Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions routers/api/packages/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ func CommonRoutes() *web.Router {
})
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/generic", func() {
r.Get("/{packagename}/list", generic.EnumeratePackageVersions)
r.Group("/{packagename}/{packageversion}", func() {
r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage)
r.Group("/{filename}", func() {
Expand Down
60 changes: 60 additions & 0 deletions routers/api/packages/generic/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

packages_model "code.gitea.io/gitea/models/packages"
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
Expand All @@ -22,11 +23,70 @@ var (
filenameRegex = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`)
)

// PackageFileInfo represents information about an existing package file
// swagger:model
Comment on lines +26 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"/api/packages/%s/generic/test-package/list" doesn't belong to v1 api, so swagger won't work, and there won't be generated document for the "package internal API".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, I realized this during implementation but left those comments in there because I wasn't sure what the intention was for documentation.

type PackageFileInfo struct {
// Name of package file
Name string `json:"name"`
// swagger:strfmt date-time
// Date when package file was created/uploaded
CreatedUnix timeutil.TimeStamp `json:"created"`
}

// PackageInfo represents information about an existing package file
// swagger:model
type PackageInfo struct {
/// Version linked to package information
Version string `json:"version"`
/// Download count for files within version
DownloadCount int64 `json:"downloads"`
/// Files uploaded for package version
Files []PackageFileInfo `json:"files"`
}

func apiError(ctx *context.Context, status int, obj any) {
message := helper.ProcessErrorForUser(ctx, status, obj)
ctx.PlainText(status, message)
}

// EnumeratePackageVersions lists upload versions and their associated files
func EnumeratePackageVersions(ctx *context.Context) {
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeGeneric, ctx.PathParam("packagename"))
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pvs) == 0 {
apiError(ctx, http.StatusNotFound, err)
return
}

var info []PackageInfo
for _, pv := range pvs {
Copy link
Contributor

@wxiaoguang wxiaoguang Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems strange to list all versions with all files together. IIRC most APIs are designed to "list the versions, and then list the files of a version".


What if there are hundreds of versions, and each version has hundreds of files?


And it's unclear about how to "for example automatically downloading and deploying the latest compiled build of repos within your organization."

It needs a definition for "what is 'latest'". By id, or by time, or by some "tag"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see an organization wanting to map "latest" by any of the three which is why all of the data is returned at once whilst remaining as minimal as possible.

packageFiles, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}

var files []PackageFileInfo
for _, file := range packageFiles {
files = append(files, PackageFileInfo{
Name: file.Name,
CreatedUnix: file.CreatedUnix,
})
}

info = append(info, PackageInfo{
Version: pv.Version,
DownloadCount: pv.DownloadCount,
Files: files,
})
}

ctx.JSON(http.StatusOK, info)
}

// DownloadPackageFile serves the specific generic package.
func DownloadPackageFile(ctx *context.Context) {
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
Expand Down