feat(packages): expose package file properties in the package files API (#40066)

Package files carry type specific properties like `debian.distribution`,
`debian.component`, `rpm.group` or `alpine.branch` that decide where a
file lives in a registry, but the API never returned them. A client
listing `GET /packages/{owner}/{type}/{name}/{version}/files` could not
tell which distribution or component a file belongs to, or tell apart
same-named files in different ones. Tools copying packages between
instances, like https://gitea.com/gitea/importer, need them to re-upload
a file to the same place.

This adds a `properties` map to `PackageFile`, limited to the properties
that locate a file. Internal ones like the rpm, alpine, arch and conda
metadata or the Debian control file stay hidden, as they can be many MiB
per file.

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-29 18:10:36 +00:00
1 parent eb9267365d
commit b0b31bc593
18 files changed
+225 -2

No files matched your search

+2
View File
@@ -46,4 +46,6 @@ type PackageFile struct {
HashSHA256 string `json:"sha256"`
// The SHA512 hash of the package file
HashSHA512 string `json:"sha512"`
// The type specific properties locating the package file, e.g. the distribution and component of a Debian package
Properties map[string]string `json:"properties,omitempty"`
}
+1 -1
View File
@@ -227,7 +227,7 @@ func ListPackageFiles(ctx *context.APIContext) {
apiPackageFiles := make([]*api.PackageFile, 0, len(ctx.Package.Descriptor.Files))
for _, pfd := range ctx.Package.Descriptor.Files {
apiPackageFiles = append(apiPackageFiles, convert.ToPackageFile(pfd))
apiPackageFiles = append(apiPackageFiles, convert.ToPackageFile(ctx.Package.Descriptor.Package.Type, pfd))
}
ctx.JSON(http.StatusOK, apiPackageFiles)
+11 -1
View File
@@ -5,11 +5,13 @@ package convert
import (
"context"
"slices"
"gitea.dev/models/packages"
access_model "gitea.dev/models/perm/access"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
packages_service "gitea.dev/services/packages"
)
// ToPackage convert a packages.PackageDescriptor to api.Package
@@ -40,7 +42,14 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m
}
// ToPackageFile converts packages.PackageFileDescriptor to api.PackageFile
func ToPackageFile(pfd *packages.PackageFileDescriptor) *api.PackageFile {
func ToPackageFile(packageType packages.Type, pfd *packages.PackageFileDescriptor) *api.PackageFile {
properties := make(map[string]string)
publicPropertyNames := packages_service.GetSpecManager().Get(packageType).PublicPropertyNames()
for _, pp := range pfd.Properties {
if slices.Contains(publicPropertyNames, pp.Name) {
properties[pp.Name] = pp.Value
}
}
return &api.PackageFile{
ID: pfd.File.ID,
Size: pfd.Blob.Size,
@@ -49,5 +58,6 @@ func ToPackageFile(pfd *packages.PackageFileDescriptor) *api.PackageFile {
HashSHA1: pfd.Blob.HashSHA1,
HashSHA256: pfd.Blob.HashSHA256,
HashSHA512: pfd.Blob.HashSHA512,
Properties: properties,
}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package alpine
import (
alpine_module "gitea.dev/modules/packages/alpine"
packages_service "gitea.dev/services/packages"
)
type Specialization struct {
packages_service.SpecGetViewPackageVersionData
packages_service.SpecOnBeforeRemovePackageAll
packages_service.SpecOnBeforeRemovePackageVersion
packages_service.SpecRenderUsageManual
}
var _ packages_service.Specialization = (*Specialization)(nil)
func (s Specialization) PublicPropertyNames() []string {
return []string{alpine_module.PropertyBranch, alpine_module.PropertyRepository, alpine_module.PropertyArchitecture}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
arch_module "gitea.dev/modules/packages/arch"
packages_service "gitea.dev/services/packages"
)
type Specialization struct {
packages_service.SpecGetViewPackageVersionData
packages_service.SpecOnBeforeRemovePackageAll
packages_service.SpecOnBeforeRemovePackageVersion
packages_service.SpecRenderUsageManual
}
var _ packages_service.Specialization = (*Specialization)(nil)
func (s Specialization) PublicPropertyNames() []string {
return []string{arch_module.PropertyRepository, arch_module.PropertyArchitecture}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conan
import (
conan_module "gitea.dev/modules/packages/conan"
packages_service "gitea.dev/services/packages"
)
type Specialization struct {
packages_service.SpecGetViewPackageVersionData
packages_service.SpecOnBeforeRemovePackageAll
packages_service.SpecOnBeforeRemovePackageVersion
packages_service.SpecRenderUsageManual
}
var _ packages_service.Specialization = (*Specialization)(nil)
func (s Specialization) PublicPropertyNames() []string {
return []string{conan_module.PropertyRecipeUser, conan_module.PropertyRecipeChannel, conan_module.PropertyRecipeRevision, conan_module.PropertyPackageReference, conan_module.PropertyPackageRevision}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package conda
import (
conda_module "gitea.dev/modules/packages/conda"
packages_service "gitea.dev/services/packages"
)
type Specialization struct {
packages_service.SpecGetViewPackageVersionData
packages_service.SpecOnBeforeRemovePackageAll
packages_service.SpecOnBeforeRemovePackageVersion
packages_service.SpecRenderUsageManual
}
var _ packages_service.Specialization = (*Specialization)(nil)
func (s Specialization) PublicPropertyNames() []string {
return []string{conda_module.PropertySubdir}
}
+4
View File
@@ -85,3 +85,7 @@ func (s Specialization) RenderSetupManual(ctx context.Context, pkg *packages_mod
w.WriteHTML(`</div>`)
return w.HTMLString()
}
func (s Specialization) PublicPropertyNames() []string {
return []string{debian_module.PropertyDistribution, debian_module.PropertyComponent, debian_module.PropertyArchitecture}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package nuget
import (
nuget_module "gitea.dev/modules/packages/nuget"
packages_service "gitea.dev/services/packages"
)
type Specialization struct {
packages_service.SpecGetViewPackageVersionData
packages_service.SpecOnBeforeRemovePackageAll
packages_service.SpecOnBeforeRemovePackageVersion
packages_service.SpecRenderUsageManual
}
var _ packages_service.Specialization = (*Specialization)(nil)
func (s Specialization) PublicPropertyNames() []string {
return []string{nuget_module.PropertySymbolID}
}
+1
View File
@@ -35,6 +35,7 @@ var (
)
type Specialization interface {
PublicPropertyNames() []string // only properties locating the file, the others are internal and can be large
OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error
OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error
GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error)
+14
View File
@@ -6,14 +6,28 @@ package pkgspec
import (
packages_model "gitea.dev/models/packages"
packages_service "gitea.dev/services/packages"
"gitea.dev/services/packages/alpine"
"gitea.dev/services/packages/arch"
"gitea.dev/services/packages/conan"
"gitea.dev/services/packages/conda"
"gitea.dev/services/packages/debian"
"gitea.dev/services/packages/nuget"
"gitea.dev/services/packages/rpm"
"gitea.dev/services/packages/terraform"
"gitea.dev/services/packages/vagrant"
)
func InitManager() error {
mgr := packages_service.GetSpecManager()
mgr.Add(packages_model.TypeAlpine, &alpine.Specialization{})
mgr.Add(packages_model.TypeArch, &arch.Specialization{})
mgr.Add(packages_model.TypeConan, &conan.Specialization{})
mgr.Add(packages_model.TypeConda, &conda.Specialization{})
mgr.Add(packages_model.TypeDebian, &debian.Specialization{})
mgr.Add(packages_model.TypeNuGet, &nuget.Specialization{})
mgr.Add(packages_model.TypeRpm, &rpm.Specialization{})
mgr.Add(packages_model.TypeTerraformState, &terraform.Specialization{})
mgr.Add(packages_model.TypeVagrant, &vagrant.Specialization{})
// TODO: add more in the future, refactor the existing code to use this approach
return nil
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rpm
import (
rpm_module "gitea.dev/modules/packages/rpm"
packages_service "gitea.dev/services/packages"
)
type Specialization struct {
packages_service.SpecGetViewPackageVersionData
packages_service.SpecOnBeforeRemovePackageAll
packages_service.SpecOnBeforeRemovePackageVersion
packages_service.SpecRenderUsageManual
}
var _ packages_service.Specialization = (*Specialization)(nil)
func (s Specialization) PublicPropertyNames() []string {
return []string{rpm_module.PropertyGroup, rpm_module.PropertyArchitecture}
}
+7
View File
@@ -36,7 +36,14 @@ func (*SpecRenderUsageManual) RenderSetupManual(ctx context.Context, pkg *packag
return ""
}
type SpecPublicPropertyNames struct{}
func (*SpecPublicPropertyNames) PublicPropertyNames() []string {
return nil
}
type specDefault struct {
SpecPublicPropertyNames
SpecGetViewPackageVersionData
SpecOnBeforeRemovePackageAll
SpecOnBeforeRemovePackageVersion
+1
View File
@@ -15,6 +15,7 @@ import (
)
type Specialization struct {
packages_service.SpecPublicPropertyNames
packages_service.SpecRenderUsageManual
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package vagrant
import (
vagrant_module "gitea.dev/modules/packages/vagrant"
packages_service "gitea.dev/services/packages"
)
type Specialization struct {
packages_service.SpecGetViewPackageVersionData
packages_service.SpecOnBeforeRemovePackageAll
packages_service.SpecOnBeforeRemovePackageVersion
packages_service.SpecRenderUsageManual
}
var _ packages_service.Specialization = (*Specialization)(nil)
func (s Specialization) PublicPropertyNames() []string {
return []string{vagrant_module.PropertyProvider}
}
+8
View File
@@ -8506,6 +8506,14 @@
"type": "string",
"x-go-name": "Name"
},
"properties": {
"additionalProperties": {
"type": "string"
},
"description": "The type specific properties locating the package file, e.g. the distribution and component of a Debian package",
"type": "object",
"x-go-name": "Properties"
},
"sha1": {
"description": "The SHA1 hash of the package file",
"type": "string",
+8
View File
@@ -30991,6 +30991,14 @@
"type": "string",
"x-go-name": "Name"
},
"properties": {
"description": "The type specific properties locating the package file, e.g. the distribution and component of a Debian package",
"type": "object",
"additionalProperties": {
"type": "string"
},
"x-go-name": "Properties"
},
"sha1": {
"description": "The SHA1 hash of the package file",
"type": "string",
@@ -19,6 +19,7 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
debian_module "gitea.dev/modules/packages/debian"
api "gitea.dev/modules/structs"
packages_cleanup_service "gitea.dev/services/packages/cleanup"
"gitea.dev/tests"
@@ -116,6 +117,19 @@ func TestPackageDebian(t *testing.T) {
assert.Equal(t, packageName, pd.Package.Name)
assert.Equal(t, packageVersion, pd.Version.Version)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/debian/%s/%s/files", user.Name, packageName, packageVersion)).
AddBasicAuth(user.Name)
apiFiles := DecodeJSON(t, MakeRequest(t, req, http.StatusOK), []*api.PackageFile{})
var fileProperties []map[string]string
for _, apiFile := range apiFiles {
fileProperties = append(fileProperties, apiFile.Properties)
}
assert.Contains(t, fileProperties, map[string]string{
debian_module.PropertyDistribution: distribution,
debian_module.PropertyComponent: component,
debian_module.PropertyArchitecture: architecture,
})
pfs, err := packages.GetFilesByVersionID(t.Context(), pv.ID)
assert.NoError(t, err)
assert.NotEmpty(t, pfs)