aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/content/usage/packages/rpm.en-us.md49
-rw-r--r--models/packages/package.go14
-rw-r--r--models/packages/package_blob.go12
-rw-r--r--models/packages/package_file.go26
-rw-r--r--models/packages/package_version.go12
-rw-r--r--models/packages/rpm/search.go23
-rw-r--r--modules/packages/rpm/metadata.go5
-rw-r--r--modules/templates/util_string.go5
-rw-r--r--modules/util/slice.go8
-rw-r--r--options/locale/locale_en-US.ini3
-rw-r--r--routers/api/packages/api.go148
-rw-r--r--routers/api/packages/rpm/rpm.go31
-rw-r--r--routers/web/admin/packages.go2
-rw-r--r--routers/web/user/package.go30
-rw-r--r--services/packages/cleanup/cleanup.go5
-rw-r--r--services/packages/rpm/repository.go69
-rw-r--r--templates/package/content/rpm.tmpl32
-rw-r--r--tests/integration/api_packages_rpm_test.go688
18 files changed, 659 insertions, 503 deletions
diff --git a/docs/content/usage/packages/rpm.en-us.md b/docs/content/usage/packages/rpm.en-us.md
index 586e48d47f..1f93376b7b 100644
--- a/docs/content/usage/packages/rpm.en-us.md
+++ b/docs/content/usage/packages/rpm.en-us.md
@@ -24,16 +24,26 @@ The following examples use `dnf`.
## Configuring the package registry
-To register the RPM registry add the url to the list of known apt sources:
+To register the RPM registry add the url to the list of known sources:
```shell
dnf config-manager --add-repo https://gitea.example.com/api/packages/{owner}/rpm/{group}.repo
```
-| Placeholder | Description |
-| ----------- |----------------------------------------------------|
-| `owner` | The owner of the package. |
-| `group` | Everything, e.g. `el7`, `rocky/el9` , `test/fc38`.|
+| Placeholder | Description |
+| ----------- | ----------- |
+| `owner` | The owner of the package. |
+| `group` | Optional: Everything, e.g. empty, `el7`, `rocky/el9`, `test/fc38`. |
+
+Example:
+
+```shell
+# without a group
+dnf config-manager --add-repo https://gitea.example.com/api/packages/testuser/rpm.repo
+
+# with the group 'centos/el7'
+dnf config-manager --add-repo https://gitea.example.com/api/packages/testuser/rpm/centos/el7.repo
+```
If the registry is private, provide credentials in the url. You can use a password or a [personal access token](development/api-usage.md#authentication):
@@ -41,7 +51,7 @@ If the registry is private, provide credentials in the url. You can use a passwo
dnf config-manager --add-repo https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/rpm/{group}.repo
```
-You have to add the credentials to the urls in the `rpm.repo` file in `/etc/yum.repos.d` too.
+You have to add the credentials to the urls in the created `.repo` file in `/etc/yum.repos.d` too.
## Publish a package
@@ -54,11 +64,17 @@ PUT https://gitea.example.com/api/packages/{owner}/rpm/{group}/upload
| Parameter | Description |
| --------- | ----------- |
| `owner` | The owner of the package. |
-| `group` | Everything, e.g. `el7`, `rocky/el9` , `test/fc38`.|
+| `group` | Optional: Everything, e.g. empty, `el7`, `rocky/el9`, `test/fc38`. |
Example request using HTTP Basic authentication:
```shell
+# without a group
+curl --user your_username:your_password_or_token \
+ --upload-file path/to/file.rpm \
+ https://gitea.example.com/api/packages/testuser/rpm/upload
+
+# with the group 'centos/el7'
curl --user your_username:your_password_or_token \
--upload-file path/to/file.rpm \
https://gitea.example.com/api/packages/testuser/rpm/centos/el7/upload
@@ -83,17 +99,22 @@ To delete an RPM package perform a HTTP DELETE operation. This will delete the p
DELETE https://gitea.example.com/api/packages/{owner}/rpm/{group}/package/{package_name}/{package_version}/{architecture}
```
-| Parameter | Description |
-|-------------------|----------------------------|
-| `owner` | The owner of the package. |
-| `group` | The package group . |
-| `package_name` | The package name. |
-| `package_version` | The package version. |
-| `architecture` | The package architecture. |
+| Parameter | Description |
+| ----------------- | ----------- |
+| `owner` | The owner of the package. |
+| `group` | Optional: The package group. |
+| `package_name` | The package name. |
+| `package_version` | The package version. |
+| `architecture` | The package architecture. |
Example request using HTTP Basic authentication:
```shell
+# without a group
+curl --user your_username:your_token_or_password -X DELETE \
+ https://gitea.example.com/api/packages/testuser/rpm/package/test-package/1.0.0/x86_64
+
+# with the group 'centos/el7'
curl --user your_username:your_token_or_password -X DELETE \
https://gitea.example.com/api/packages/testuser/rpm/centos/el7/package/test-package/1.0.0/x86_64
```
diff --git a/models/packages/package.go b/models/packages/package.go
index 380a076f9d..65a2574150 100644
--- a/models/packages/package.go
+++ b/models/packages/package.go
@@ -191,18 +191,18 @@ type Package struct {
func TryInsertPackage(ctx context.Context, p *Package) (*Package, error) {
e := db.GetEngine(ctx)
- key := &Package{
- OwnerID: p.OwnerID,
- Type: p.Type,
- LowerName: p.LowerName,
- }
+ existing := &Package{}
- has, err := e.Get(key)
+ has, err := e.Where(builder.Eq{
+ "owner_id": p.OwnerID,
+ "type": p.Type,
+ "lower_name": p.LowerName,
+ }).Get(existing)
if err != nil {
return nil, err
}
if has {
- return key, ErrDuplicatePackage
+ return existing, ErrDuplicatePackage
}
if _, err = e.Insert(p); err != nil {
return nil, err
diff --git a/models/packages/package_blob.go b/models/packages/package_blob.go
index d1f470d520..d9c30b6533 100644
--- a/models/packages/package_blob.go
+++ b/models/packages/package_blob.go
@@ -41,12 +41,20 @@ type PackageBlob struct {
func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool, error) {
e := db.GetEngine(ctx)
- has, err := e.Get(pb)
+ existing := &PackageBlob{}
+
+ has, err := e.Where(builder.Eq{
+ "size": pb.Size,
+ "hash_md5": pb.HashMD5,
+ "hash_sha1": pb.HashSHA1,
+ "hash_sha256": pb.HashSHA256,
+ "hash_sha512": pb.HashSHA512,
+ }).Get(existing)
if err != nil {
return nil, false, err
}
if has {
- return pb, true, nil
+ return existing, true, nil
}
if _, err = e.Insert(pb); err != nil {
return nil, false, err
diff --git a/models/packages/package_file.go b/models/packages/package_file.go
index 1c2c9ac072..1bb6b57a34 100644
--- a/models/packages/package_file.go
+++ b/models/packages/package_file.go
@@ -46,18 +46,18 @@ type PackageFile struct {
func TryInsertFile(ctx context.Context, pf *PackageFile) (*PackageFile, error) {
e := db.GetEngine(ctx)
- key := &PackageFile{
- VersionID: pf.VersionID,
- LowerName: pf.LowerName,
- CompositeKey: pf.CompositeKey,
- }
+ existing := &PackageFile{}
- has, err := e.Get(key)
+ has, err := e.Where(builder.Eq{
+ "version_id": pf.VersionID,
+ "lower_name": pf.LowerName,
+ "composite_key": pf.CompositeKey,
+ }).Get(existing)
if err != nil {
return nil, err
}
if has {
- return pf, ErrDuplicatePackageFile
+ return existing, ErrDuplicatePackageFile
}
if _, err = e.Insert(pf); err != nil {
return nil, err
@@ -93,13 +93,13 @@ func GetFileForVersionByName(ctx context.Context, versionID int64, name, key str
return nil, ErrPackageFileNotExist
}
- pf := &PackageFile{
- VersionID: versionID,
- LowerName: strings.ToLower(name),
- CompositeKey: key,
- }
+ pf := &PackageFile{}
- has, err := db.GetEngine(ctx).Get(pf)
+ has, err := db.GetEngine(ctx).Where(builder.Eq{
+ "version_id": versionID,
+ "lower_name": strings.ToLower(name),
+ "composite_key": key,
+ }).Get(pf)
if err != nil {
return nil, err
}
diff --git a/models/packages/package_version.go b/models/packages/package_version.go
index 9999fc4dab..8fc475691b 100644
--- a/models/packages/package_version.go
+++ b/models/packages/package_version.go
@@ -39,17 +39,17 @@ type PackageVersion struct {
func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) {
e := db.GetEngine(ctx)
- key := &PackageVersion{
- PackageID: pv.PackageID,
- LowerVersion: pv.LowerVersion,
- }
+ existing := &PackageVersion{}
- has, err := e.Get(key)
+ has, err := e.Where(builder.Eq{
+ "package_id": pv.PackageID,
+ "lower_version": pv.LowerVersion,
+ }).Get(existing)
if err != nil {
return nil, err
}
if has {
- return key, ErrDuplicatePackageVersion
+ return existing, ErrDuplicatePackageVersion
}
if _, err = e.Insert(pv); err != nil {
return nil, err
diff --git a/models/packages/rpm/search.go b/models/packages/rpm/search.go
new file mode 100644
index 0000000000..e697421b49
--- /dev/null
+++ b/models/packages/rpm/search.go
@@ -0,0 +1,23 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package rpm
+
+import (
+ "context"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ rpm_module "code.gitea.io/gitea/modules/packages/rpm"
+)
+
+// GetGroups gets all available groups
+func GetGroups(ctx context.Context, ownerID int64) ([]string, error) {
+ return packages_model.GetDistinctPropertyValues(
+ ctx,
+ packages_model.TypeRpm,
+ ownerID,
+ packages_model.PropertyTypeFile,
+ rpm_module.PropertyGroup,
+ nil,
+ )
+}
diff --git a/modules/packages/rpm/metadata.go b/modules/packages/rpm/metadata.go
index 1ba4c73e8d..7fc47a53e6 100644
--- a/modules/packages/rpm/metadata.go
+++ b/modules/packages/rpm/metadata.go
@@ -15,7 +15,10 @@ import (
)
const (
- PropertyMetadata = "rpm.metadata"
+ PropertyMetadata = "rpm.metadata"
+ PropertyGroup = "rpm.group"
+ PropertyArchitecture = "rpm.architecture"
+
SettingKeyPrivate = "rpm.key.private"
SettingKeyPublic = "rpm.key.public"
diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go
index 613940ccdc..18a0d5cacc 100644
--- a/modules/templates/util_string.go
+++ b/modules/templates/util_string.go
@@ -4,7 +4,6 @@
package templates
import (
- "regexp"
"strings"
"code.gitea.io/gitea/modules/base"
@@ -26,10 +25,6 @@ func (su *StringUtils) Contains(s, substr string) bool {
return strings.Contains(s, substr)
}
-func (su *StringUtils) ReplaceAllStringRegex(s, regex, new string) string {
- return regexp.MustCompile(regex).ReplaceAllString(s, new)
-}
-
func (su *StringUtils) Split(s, sep string) []string {
return strings.Split(s, sep)
}
diff --git a/modules/util/slice.go b/modules/util/slice.go
index 6d63ab4a77..a7073fedee 100644
--- a/modules/util/slice.go
+++ b/modules/util/slice.go
@@ -4,6 +4,7 @@
package util
import (
+ "cmp"
"slices"
"strings"
)
@@ -45,3 +46,10 @@ func SliceSortedEqual[T comparable](s1, s2 []T) bool {
func SliceRemoveAll[T comparable](slice []T, target T) []T {
return slices.DeleteFunc(slice, func(t T) bool { return t == target })
}
+
+// Sorted returns the sorted slice
+// Note: The parameter is sorted inline.
+func Sorted[S ~[]E, E cmp.Ordered](values S) S {
+ slices.Sort(values)
+ return values
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 6f9ba8c884..e8345595aa 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3414,6 +3414,9 @@ rpm.registry = Setup this registry from the command line:
rpm.distros.redhat = on RedHat based distributions
rpm.distros.suse = on SUSE based distributions
rpm.install = To install the package, run the following command:
+rpm.repository = Repository Info
+rpm.repository.architectures = Architectures
+rpm.repository.multiple_groups = This package is available in multiple groups.
rubygems.install = To install the package using gem, run the following command:
rubygems.install2 = or add it to the Gemfile:
rubygems.dependencies.runtime = Runtime Dependencies
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index 9026387129..d990ebb56a 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -512,7 +512,77 @@ func CommonRoutes() *web.Route {
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
r.Get("/simple/{id}", pypi.PackageMetadata)
}, reqPackageAccess(perm.AccessModeRead))
- r.Group("/rpm", RpmRoutes(r), reqPackageAccess(perm.AccessModeRead))
+ r.Group("/rpm", func() {
+ r.Group("/repository.key", func() {
+ r.Head("", rpm.GetRepositoryKey)
+ r.Get("", rpm.GetRepositoryKey)
+ })
+
+ var (
+ repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`)
+ uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`)
+ filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`)
+ repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`)
+ )
+
+ r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) {
+ path := ctx.Params("*")
+ isHead := ctx.Req.Method == "HEAD"
+ isGetHead := ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET"
+ isPut := ctx.Req.Method == "PUT"
+ isDelete := ctx.Req.Method == "DELETE"
+
+ m := repoPattern.FindStringSubmatch(path)
+ if len(m) == 2 && isGetHead {
+ ctx.SetParams("group", strings.Trim(m[1], "/"))
+ rpm.GetRepositoryConfig(ctx)
+ return
+ }
+
+ m = repoFilePattern.FindStringSubmatch(path)
+ if len(m) == 3 && isGetHead {
+ ctx.SetParams("group", strings.Trim(m[1], "/"))
+ ctx.SetParams("filename", m[2])
+ if isHead {
+ rpm.CheckRepositoryFileExistence(ctx)
+ } else {
+ rpm.GetRepositoryFile(ctx)
+ }
+ return
+ }
+
+ m = uploadPattern.FindStringSubmatch(path)
+ if len(m) == 2 && isPut {
+ reqPackageAccess(perm.AccessModeWrite)(ctx)
+ if ctx.Written() {
+ return
+ }
+ ctx.SetParams("group", strings.Trim(m[1], "/"))
+ rpm.UploadPackageFile(ctx)
+ return
+ }
+
+ m = filePattern.FindStringSubmatch(path)
+ if len(m) == 6 && (isGetHead || isDelete) {
+ ctx.SetParams("group", strings.Trim(m[1], "/"))
+ ctx.SetParams("name", m[2])
+ ctx.SetParams("version", m[3])
+ ctx.SetParams("architecture", m[4])
+ if isGetHead {
+ rpm.DownloadPackageFile(ctx)
+ } else {
+ reqPackageAccess(perm.AccessModeWrite)(ctx)
+ if ctx.Written() {
+ return
+ }
+ rpm.DeletePackageFile(ctx)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNotFound)
+ })
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/rubygems", func() {
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
@@ -577,82 +647,6 @@ func CommonRoutes() *web.Route {
return r
}
-// Support for uploading rpm packages with arbitrary depth paths
-func RpmRoutes(r *web.Route) func() {
- var (
- groupRepoInfo = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)\.repo\z`)
- groupUpload = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)/upload\z`)
- groupRpm = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`)
- groupMetadata = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)/repodata/([^/]+)\z`)
- )
-
- return func() {
- r.Methods("HEAD,GET,POST,PUT,PATCH,DELETE", "*", func(ctx *context.Context) {
- path := ctx.Params("*")
- isHead := ctx.Req.Method == "HEAD"
- isGetHead := ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET"
- isPut := ctx.Req.Method == "PUT"
- isDelete := ctx.Req.Method == "DELETE"
-
- if path == "/repository.key" && isGetHead {
- rpm.GetRepositoryKey(ctx)
- return
- }
-
- // get repo
- m := groupRepoInfo.FindStringSubmatch(path)
- if len(m) == 2 && isGetHead {
- ctx.SetParams("group", strings.Trim(m[1], "/"))
- rpm.GetRepositoryConfig(ctx)
- return
- }
- // get meta
- m = groupMetadata.FindStringSubmatch(path)
- if len(m) == 3 && isGetHead {
- ctx.SetParams("group", strings.Trim(m[1], "/"))
- ctx.SetParams("filename", m[2])
- if isHead {
- rpm.CheckRepositoryFileExistence(ctx)
- } else {
- rpm.GetRepositoryFile(ctx)
- }
- return
- }
- // upload
- m = groupUpload.FindStringSubmatch(path)
- if len(m) == 2 && isPut {
- reqPackageAccess(perm.AccessModeWrite)(ctx)
- if ctx.Written() {
- return
- }
- ctx.SetParams("group", strings.Trim(m[1], "/"))
- rpm.UploadPackageFile(ctx)
- return
- }
- // rpm down/delete
- m = groupRpm.FindStringSubmatch(path)
- if len(m) == 6 {
- ctx.SetParams("group", strings.Trim(m[1], "/"))
- ctx.SetParams("name", m[2])
- ctx.SetParams("version", m[3])
- ctx.SetParams("architecture", m[4])
- if isGetHead {
- rpm.DownloadPackageFile(ctx)
- return
- } else if isDelete {
- reqPackageAccess(perm.AccessModeWrite)(ctx)
- if ctx.Written() {
- return
- }
- rpm.DeletePackageFile(ctx)
- }
- }
- // default
- ctx.Status(http.StatusNotFound)
- })
- }
-}
-
// ContainerRoutes provides endpoints that implement the OCI API to serve containers
// These have to be mounted on `/v2/...` to comply with the OCI spec:
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go
index 75d19e2b43..5d06680552 100644
--- a/routers/api/packages/rpm/rpm.go
+++ b/routers/api/packages/rpm/rpm.go
@@ -34,13 +34,17 @@ func apiError(ctx *context.Context, status int, obj any) {
// https://dnf.readthedocs.io/en/latest/conf_ref.html
func GetRepositoryConfig(ctx *context.Context) {
group := ctx.Params("group")
+
+ var groupParts []string
if group != "" {
- group = fmt.Sprintf("/%s", group)
+ groupParts = strings.Split(group, "/")
}
+
url := fmt.Sprintf("%sapi/packages/%s/rpm", setting.AppURL, ctx.Package.Owner.Name)
- ctx.PlainText(http.StatusOK, `[gitea-`+ctx.Package.Owner.LowerName+strings.ReplaceAll(group, "/", "-")+`]
-name=`+ctx.Package.Owner.Name+` - `+setting.AppName+strings.ReplaceAll(group, "/", " - ")+`
-baseurl=`+url+group+`/
+
+ ctx.PlainText(http.StatusOK, `[gitea-`+strings.Join(append([]string{ctx.Package.Owner.LowerName}, groupParts...), "-")+`]
+name=`+strings.Join(append([]string{ctx.Package.Owner.Name, setting.AppName}, groupParts...), " - ")+`
+baseurl=`+strings.Join(append([]string{url}, groupParts...), "/")+`
enabled=1
gpgcheck=1
gpgkey=`+url+`/repository.key`)
@@ -157,7 +161,7 @@ func UploadPackageFile(ctx *context.Context) {
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeRpm,
Name: pck.Name,
- Version: strings.Trim(fmt.Sprintf("%s/%s", group, pck.Version), "/"),
+ Version: pck.Version,
},
Creator: ctx.Doer,
Metadata: pck.VersionMetadata,
@@ -171,7 +175,9 @@ func UploadPackageFile(ctx *context.Context) {
Data: buf,
IsLead: true,
Properties: map[string]string{
- rpm_module.PropertyMetadata: string(fileMetadataRaw),
+ rpm_module.PropertyGroup: group,
+ rpm_module.PropertyArchitecture: pck.FileMetadata.Architecture,
+ rpm_module.PropertyMetadata: string(fileMetadataRaw),
},
},
)
@@ -187,7 +193,7 @@ func UploadPackageFile(ctx *context.Context) {
return
}
- if err := rpm_service.BuildRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil {
+ if err := rpm_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
@@ -196,20 +202,20 @@ func UploadPackageFile(ctx *context.Context) {
}
func DownloadPackageFile(ctx *context.Context) {
- group := ctx.Params("group")
name := ctx.Params("name")
version := ctx.Params("version")
+
s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeRpm,
Name: name,
- Version: strings.Trim(fmt.Sprintf("%s/%s", group, version), "/"),
+ Version: version,
},
&packages_service.PackageFileInfo{
Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.Params("architecture")),
- CompositeKey: group,
+ CompositeKey: ctx.Params("group"),
},
)
if err != nil {
@@ -229,6 +235,7 @@ func DeletePackageFile(webctx *context.Context) {
name := webctx.Params("name")
version := webctx.Params("version")
architecture := webctx.Params("architecture")
+
var pd *packages_model.PackageDescriptor
err := db.WithTx(webctx, func(ctx stdctx.Context) error {
@@ -236,7 +243,7 @@ func DeletePackageFile(webctx *context.Context) {
webctx.Package.Owner.ID,
packages_model.TypeRpm,
name,
- strings.Trim(fmt.Sprintf("%s/%s", group, version), "/"),
+ version,
)
if err != nil {
return err
@@ -286,7 +293,7 @@ func DeletePackageFile(webctx *context.Context) {
notify_service.PackageDelete(webctx, webctx.Doer, pd)
}
- if err := rpm_service.BuildRepositoryFiles(webctx, webctx.Package.Owner.ID, group); err != nil {
+ if err := rpm_service.BuildSpecificRepositoryFiles(webctx, webctx.Package.Owner.ID, group); err != nil {
apiError(webctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go
index f4e1a93a22..35ce215be4 100644
--- a/routers/web/admin/packages.go
+++ b/routers/web/admin/packages.go
@@ -108,6 +108,6 @@ func CleanupExpiredData(ctx *context.Context) {
return
}
- ctx.Flash.Success(ctx.Tr("packages.cleanup.success"))
+ ctx.Flash.Success(ctx.Tr("admin.packages.cleanup.success"))
ctx.Redirect(setting.AppSubURL + "/admin/packages")
}
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index d8da6a192e..708af3e43c 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/log"
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
debian_module "code.gitea.io/gitea/modules/packages/debian"
+ rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -195,9 +196,9 @@ func ViewPackageVersion(ctx *context.Context) {
}
}
- ctx.Data["Branches"] = branches.Values()
- ctx.Data["Repositories"] = repositories.Values()
- ctx.Data["Architectures"] = architectures.Values()
+ ctx.Data["Branches"] = util.Sorted(branches.Values())
+ ctx.Data["Repositories"] = util.Sorted(repositories.Values())
+ ctx.Data["Architectures"] = util.Sorted(architectures.Values())
case packages_model.TypeDebian:
distributions := make(container.Set[string])
components := make(container.Set[string])
@@ -216,9 +217,26 @@ func ViewPackageVersion(ctx *context.Context) {
}
}
- ctx.Data["Distributions"] = distributions.Values()
- ctx.Data["Components"] = components.Values()
- ctx.Data["Architectures"] = architectures.Values()
+ ctx.Data["Distributions"] = util.Sorted(distributions.Values())
+ ctx.Data["Components"] = util.Sorted(components.Values())
+ ctx.Data["Architectures"] = util.Sorted(architectures.Values())
+ case packages_model.TypeRpm:
+ groups := make(container.Set[string])
+ architectures := make(container.Set[string])
+
+ for _, f := range pd.Files {
+ for _, pp := range f.Properties {
+ switch pp.Name {
+ case rpm_module.PropertyGroup:
+ groups.Add(pp.Value)
+ case rpm_module.PropertyArchitecture:
+ architectures.Add(pp.Value)
+ }
+ }
+ }
+
+ ctx.Data["Groups"] = util.Sorted(groups.Values())
+ ctx.Data["Architectures"] = util.Sorted(architectures.Values())
}
var (
diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go
index 04ee6c4419..0ff8077bc9 100644
--- a/services/packages/cleanup/cleanup.go
+++ b/services/packages/cleanup/cleanup.go
@@ -19,6 +19,7 @@ import (
cargo_service "code.gitea.io/gitea/services/packages/cargo"
container_service "code.gitea.io/gitea/services/packages/container"
debian_service "code.gitea.io/gitea/services/packages/debian"
+ rpm_service "code.gitea.io/gitea/services/packages/rpm"
)
// Task method to execute cleanup rules and cleanup expired package data
@@ -127,6 +128,10 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
}
+ } else if pcr.Type == packages_model.TypeRpm {
+ if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
+ return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
+ }
}
}
return nil
diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go
index 7a49efde4f..c52c8a5dd9 100644
--- a/services/packages/rpm/repository.go
+++ b/services/packages/rpm/repository.go
@@ -18,6 +18,7 @@ import (
"time"
packages_model "code.gitea.io/gitea/models/packages"
+ rpm_model "code.gitea.io/gitea/models/packages/rpm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
@@ -96,6 +97,39 @@ func generateKeypair() (string, string, error) {
return priv.String(), pub.String(), nil
}
+// BuildAllRepositoryFiles (re)builds all repository files for every available group
+func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
+ pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
+ if err != nil {
+ return err
+ }
+
+ // 1. Delete all existing repository files
+ pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
+ if err != nil {
+ return err
+ }
+
+ for _, pf := range pfs {
+ if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
+ return err
+ }
+ }
+
+ // 2. (Re)Build repository files for existing packages
+ groups, err := rpm_model.GetGroups(ctx, ownerID)
+ if err != nil {
+ return err
+ }
+ for _, group := range groups {
+ if err := BuildSpecificRepositoryFiles(ctx, ownerID, group); err != nil {
+ return fmt.Errorf("failed to build repository files [%s]: %w", group, err)
+ }
+ }
+
+ return nil
+}
+
type repoChecksum struct {
Value string `xml:",chardata"`
Type string `xml:"type,attr"`
@@ -126,7 +160,7 @@ type packageData struct {
type packageCache = map[*packages_model.PackageFile]*packageData
// BuildSpecificRepositoryFiles builds metadata files for the repository
-func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey string) error {
+func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, group string) error {
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
if err != nil {
return err
@@ -136,7 +170,7 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey strin
OwnerID: ownerID,
PackageType: packages_model.TypeRpm,
Query: "%.rpm",
- CompositeKey: compositeKey,
+ CompositeKey: group,
})
if err != nil {
return err
@@ -195,15 +229,15 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey strin
cache[pf] = pd
}
- primary, err := buildPrimary(ctx, pv, pfs, cache, compositeKey)
+ primary, err := buildPrimary(ctx, pv, pfs, cache, group)
if err != nil {
return err
}
- filelists, err := buildFilelists(ctx, pv, pfs, cache, compositeKey)
+ filelists, err := buildFilelists(ctx, pv, pfs, cache, group)
if err != nil {
return err
}
- other, err := buildOther(ctx, pv, pfs, cache, compositeKey)
+ other, err := buildOther(ctx, pv, pfs, cache, group)
if err != nil {
return err
}
@@ -217,12 +251,12 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey strin
filelists,
other,
},
- compositeKey,
+ group,
)
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml
-func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData, compositeKey string) error {
+func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData, group string) error {
type Repomd struct {
XMLName xml.Name `xml:"repomd"`
Xmlns string `xml:"xmlns,attr"`
@@ -278,7 +312,7 @@ func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: file.Name,
- CompositeKey: compositeKey,
+ CompositeKey: group,
},
Creator: user_model.NewGhostUser(),
Data: file.Data,
@@ -295,7 +329,7 @@ func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#primary-xml
-func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, compositeKey string) (*repoData, error) {
+func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) {
type Version struct {
Epoch string `xml:"epoch,attr"`
Version string `xml:"ver,attr"`
@@ -434,11 +468,11 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
PackageCount: len(pfs),
Packages: packages,
- }, compositeKey)
+ }, group)
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml
-func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, compositeKey string) (*repoData, error) { //nolint:dupl
+func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl
type Version struct {
Epoch string `xml:"epoch,attr"`
Version string `xml:"ver,attr"`
@@ -481,12 +515,11 @@ func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs
Xmlns: "http://linux.duke.edu/metadata/other",
PackageCount: len(pfs),
Packages: packages,
- },
- compositeKey)
+ }, group)
}
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml
-func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, compositeKey string) (*repoData, error) { //nolint:dupl
+func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl
type Version struct {
Epoch string `xml:"epoch,attr"`
Version string `xml:"ver,attr"`
@@ -529,7 +562,7 @@ func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*p
Xmlns: "http://linux.duke.edu/metadata/other",
PackageCount: len(pfs),
Packages: packages,
- }, compositeKey)
+ }, group)
}
// writtenCounter counts all written bytes
@@ -549,8 +582,10 @@ func (wc *writtenCounter) Written() int64 {
return wc.written
}
-func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any, compositeKey string) (*repoData, error) {
+func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any, group string) (*repoData, error) {
content, _ := packages_module.NewHashedBuffer()
+ defer content.Close()
+
gzw := gzip.NewWriter(content)
wc := &writtenCounter{}
h := sha256.New()
@@ -574,7 +609,7 @@ func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion,
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: filename,
- CompositeKey: compositeKey,
+ CompositeKey: group,
},
Creator: user_model.NewGhostUser(),
Data: content,
diff --git a/templates/package/content/rpm.tmpl b/templates/package/content/rpm.tmpl
index 4fd54a3197..0f128fd3fb 100644
--- a/templates/package/content/rpm.tmpl
+++ b/templates/package/content/rpm.tmpl
@@ -4,15 +4,21 @@
<div class="ui form">
<div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rpm.registry"}}</label>
- <div class="markup"><pre class="code-block"><code># {{ctx.Locale.Tr "packages.rpm.distros.redhat"}}
-{{$group_name:= StringUtils.ReplaceAllStringRegex .PackageDescriptor.Version.Version "(/[^/]+|[^/]*)\\z" "" -}}
-{{- if $group_name -}}
-{{- $group_name = (print "/" $group_name) -}}
-{{- end -}}
-dnf config-manager --add-repo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group_name}}.repo"></gitea-origin-url>
+ <div class="markup"><pre class="code-block"><code>{{- if gt (len .Groups) 1 -}}
+# {{ctx.Locale.Tr "packages.rpm.repository.multiple_groups"}}
+
+{{end -}}
+# {{ctx.Locale.Tr "packages.rpm.distros.redhat"}}
+{{- range $group := .Groups}}
+ {{- if $group}}{{$group = print "/" $group}}{{end}}
+dnf config-manager --add-repo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url>
+{{- end}}
# {{ctx.Locale.Tr "packages.rpm.distros.suse"}}
-zypper addrepo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group_name}}.repo"></gitea-origin-url></code></pre></div>
+{{- range $group := .Groups}}
+ {{- if $group}}{{$group = print "/" $group}}{{end}}
+zypper addrepo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url>
+{{- end}}</code></pre></div>
</div>
<div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rpm.install"}}</label>
@@ -30,6 +36,18 @@ zypper install {{$.PackageDescriptor.Package.Name}}</code></pre>
</div>
</div>
+ <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.rpm.repository"}}</h4>
+ <div class="ui attached segment">
+ <table class="ui single line very basic table">
+ <tbody>
+ <tr>
+ <td class="collapsing"><h5>{{ctx.Locale.Tr "packages.rpm.repository.architectures"}}</h5></td>
+ <td>{{StringUtils.Join .Architectures ", "}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
{{if or .PackageDescriptor.Metadata.Summary .PackageDescriptor.Metadata.Description}}
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
{{if .PackageDescriptor.Metadata.Summary}}<div class="ui attached segment">{{.PackageDescriptor.Metadata.Summary}}</div>{{end}}
diff --git a/tests/integration/api_packages_rpm_test.go b/tests/integration/api_packages_rpm_test.go
index 822b0b040e..1dcec6099e 100644
--- a/tests/integration/api_packages_rpm_test.go
+++ b/tests/integration/api_packages_rpm_test.go
@@ -12,6 +12,7 @@ import (
"io"
"net/http"
"net/http/httptest"
+ "strings"
"testing"
"code.gitea.io/gitea/models/db"
@@ -20,6 +21,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -73,346 +75,362 @@ Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5
rootURL := fmt.Sprintf("/api/packages/%s/rpm", user.Name)
- t.Run("RepositoryConfig", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
+ for _, group := range []string{"", "el9", "el9/stable"} {
+ t.Run(fmt.Sprintf("[Group:%s]", group), func(t *testing.T) {
+ var groupParts []string
+ if group != "" {
+ groupParts = strings.Split(group, "/")
+ }
+ groupURL := strings.Join(append([]string{rootURL}, groupParts...), "/")
+
+ t.Run("RepositoryConfig", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
- req := NewRequest(t, "GET", rootURL+"/el9/stable.repo")
- resp := MakeRequest(t, req, http.StatusOK)
+ req := NewRequest(t, "GET", groupURL+".repo")
+ resp := MakeRequest(t, req, http.StatusOK)
- expected := fmt.Sprintf(`[gitea-%s-el9-stable]
-name=%s - %s - el9 - stable
-baseurl=%sapi/packages/%s/rpm/el9/stable/
+ expected := fmt.Sprintf(`[gitea-%s]
+name=%s
+baseurl=%s
enabled=1
gpgcheck=1
-gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppName, setting.AppURL, user.Name, setting.AppURL, user.Name)
-
- assert.Equal(t, expected, resp.Body.String())
- })
-
- t.Run("RepositoryKey", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
-
- req := NewRequest(t, "GET", rootURL+"/repository.key")
- resp := MakeRequest(t, req, http.StatusOK)
-
- assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
- assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
- })
-
- t.Run("Upload", func(t *testing.T) {
- url := rootURL + "/el9/stable/upload"
-
- req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
- MakeRequest(t, req, http.StatusUnauthorized)
-
- req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
- AddBasicAuth(user.Name)
- MakeRequest(t, req, http.StatusCreated)
-
- pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRpm)
- assert.NoError(t, err)
- assert.Len(t, pvs, 1)
-
- pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
- assert.NoError(t, err)
- assert.Nil(t, pd.SemVer)
- assert.IsType(t, &rpm_module.VersionMetadata{}, pd.Metadata)
- assert.Equal(t, packageName, pd.Package.Name)
- assert.Equal(t, fmt.Sprintf("el9/stable/%s", packageVersion), pd.Version.Version)
-
- pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
- assert.NoError(t, err)
- assert.Len(t, pfs, 1)
- assert.Equal(t, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture), pfs[0].Name)
- assert.True(t, pfs[0].IsLead)
-
- pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
- assert.NoError(t, err)
- assert.Equal(t, int64(len(content)), pb.Size)
-
- req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
- AddBasicAuth(user.Name)
- MakeRequest(t, req, http.StatusConflict)
- })
-
- t.Run("Download", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
-
- req := NewRequest(t, "GET", fmt.Sprintf("%s/el9/stable/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture))
- resp := MakeRequest(t, req, http.StatusOK)
-
- assert.Equal(t, content, resp.Body.Bytes())
- })
-
- t.Run("Repository", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
-
- url := rootURL + "/el9/stable/repodata"
-
- req := NewRequest(t, "HEAD", url+"/dummy.xml")
- MakeRequest(t, req, http.StatusNotFound)
-
- req = NewRequest(t, "GET", url+"/dummy.xml")
- MakeRequest(t, req, http.StatusNotFound)
-
- t.Run("repomd.xml", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
-
- req = NewRequest(t, "HEAD", url+"/repomd.xml")
- MakeRequest(t, req, http.StatusOK)
-
- req = NewRequest(t, "GET", url+"/repomd.xml")
- resp := MakeRequest(t, req, http.StatusOK)
-
- type Repomd struct {
- XMLName xml.Name `xml:"repomd"`
- Xmlns string `xml:"xmlns,attr"`
- XmlnsRpm string `xml:"xmlns:rpm,attr"`
- Data []struct {
- Type string `xml:"type,attr"`
- Checksum struct {
- Value string `xml:",chardata"`
- Type string `xml:"type,attr"`
- } `xml:"checksum"`
- OpenChecksum struct {
- Value string `xml:",chardata"`
- Type string `xml:"type,attr"`
- } `xml:"open-checksum"`
- Location struct {
- Href string `xml:"href,attr"`
- } `xml:"location"`
- Timestamp int64 `xml:"timestamp"`
- Size int64 `xml:"size"`
- OpenSize int64 `xml:"open-size"`
- } `xml:"data"`
- }
-
- var result Repomd
- decodeXML(t, resp, &result)
-
- assert.Len(t, result.Data, 3)
- for _, d := range result.Data {
- assert.Equal(t, "sha256", d.Checksum.Type)
- assert.NotEmpty(t, d.Checksum.Value)
- assert.Equal(t, "sha256", d.OpenChecksum.Type)
- assert.NotEmpty(t, d.OpenChecksum.Value)
- assert.NotEqual(t, d.Checksum.Value, d.OpenChecksum.Value)
- assert.Greater(t, d.OpenSize, d.Size)
-
- switch d.Type {
- case "primary":
- assert.EqualValues(t, 722, d.Size)
- assert.EqualValues(t, 1759, d.OpenSize)
- assert.Equal(t, "repodata/primary.xml.gz", d.Location.Href)
- case "filelists":
- assert.EqualValues(t, 257, d.Size)
- assert.EqualValues(t, 326, d.OpenSize)
- assert.Equal(t, "repodata/filelists.xml.gz", d.Location.Href)
- case "other":
- assert.EqualValues(t, 306, d.Size)
- assert.EqualValues(t, 394, d.OpenSize)
- assert.Equal(t, "repodata/other.xml.gz", d.Location.Href)
+gpgkey=%sapi/packages/%s/rpm/repository.key`,
+ strings.Join(append([]string{user.LowerName}, groupParts...), "-"),
+ strings.Join(append([]string{user.Name, setting.AppName}, groupParts...), " - "),
+ util.URLJoin(setting.AppURL, groupURL),
+ setting.AppURL,
+ user.Name,
+ )
+
+ assert.Equal(t, expected, resp.Body.String())
+ })
+
+ t.Run("RepositoryKey", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", rootURL+"/repository.key")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
+ assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ url := groupURL + "/upload"
+
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRpm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.Nil(t, pd.SemVer)
+ assert.IsType(t, &rpm_module.VersionMetadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture), pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+
+ req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusConflict)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+ })
+
+ t.Run("Repository", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ url := groupURL + "/repodata"
+
+ req := NewRequest(t, "HEAD", url+"/dummy.xml")
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", url+"/dummy.xml")
+ MakeRequest(t, req, http.StatusNotFound)
+
+ t.Run("repomd.xml", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req = NewRequest(t, "HEAD", url+"/repomd.xml")
+ MakeRequest(t, req, http.StatusOK)
+
+ req = NewRequest(t, "GET", url+"/repomd.xml")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type Repomd struct {
+ XMLName xml.Name `xml:"repomd"`
+ Xmlns string `xml:"xmlns,attr"`
+ XmlnsRpm string `xml:"xmlns:rpm,attr"`
+ Data []struct {
+ Type string `xml:"type,attr"`
+ Checksum struct {
+ Value string `xml:",chardata"`
+ Type string `xml:"type,attr"`
+ } `xml:"checksum"`
+ OpenChecksum struct {
+ Value string `xml:",chardata"`
+ Type string `xml:"type,attr"`
+ } `xml:"open-checksum"`
+ Location struct {
+ Href string `xml:"href,attr"`
+ } `xml:"location"`
+ Timestamp int64 `xml:"timestamp"`
+ Size int64 `xml:"size"`
+ OpenSize int64 `xml:"open-size"`
+ } `xml:"data"`
+ }
+
+ var result Repomd
+ decodeXML(t, resp, &result)
+
+ assert.Len(t, result.Data, 3)
+ for _, d := range result.Data {
+ assert.Equal(t, "sha256", d.Checksum.Type)
+ assert.NotEmpty(t, d.Checksum.Value)
+ assert.Equal(t, "sha256", d.OpenChecksum.Type)
+ assert.NotEmpty(t, d.OpenChecksum.Value)
+ assert.NotEqual(t, d.Checksum.Value, d.OpenChecksum.Value)
+ assert.Greater(t, d.OpenSize, d.Size)
+
+ switch d.Type {
+ case "primary":
+ assert.EqualValues(t, 722, d.Size)
+ assert.EqualValues(t, 1759, d.OpenSize)
+ assert.Equal(t, "repodata/primary.xml.gz", d.Location.Href)
+ case "filelists":
+ assert.EqualValues(t, 257, d.Size)
+ assert.EqualValues(t, 326, d.OpenSize)
+ assert.Equal(t, "repodata/filelists.xml.gz", d.Location.Href)
+ case "other":
+ assert.EqualValues(t, 306, d.Size)
+ assert.EqualValues(t, 394, d.OpenSize)
+ assert.Equal(t, "repodata/other.xml.gz", d.Location.Href)
+ }
+ }
+ })
+
+ t.Run("repomd.xml.asc", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req = NewRequest(t, "GET", url+"/repomd.xml.asc")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----")
+ })
+
+ decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
+ t.Helper()
+
+ zr, err := gzip.NewReader(resp.Body)
+ assert.NoError(t, err)
+
+ assert.NoError(t, xml.NewDecoder(zr).Decode(v))
}
- }
- })
-
- t.Run("repomd.xml.asc", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
-
- req = NewRequest(t, "GET", url+"/repomd.xml.asc")
- resp := MakeRequest(t, req, http.StatusOK)
- assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----")
+ t.Run("primary.xml.gz", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req = NewRequest(t, "GET", url+"/primary.xml.gz")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type EntryList struct {
+ Entries []*rpm_module.Entry `xml:"entry"`
+ }
+
+ type Metadata struct {
+ XMLName xml.Name `xml:"metadata"`
+ Xmlns string `xml:"xmlns,attr"`
+ XmlnsRpm string `xml:"xmlns:rpm,attr"`
+ PackageCount int `xml:"packages,attr"`
+ Packages []struct {
+ XMLName xml.Name `xml:"package"`
+ Type string `xml:"type,attr"`
+ Name string `xml:"name"`
+ Architecture string `xml:"arch"`
+ Version struct {
+ Epoch string `xml:"epoch,attr"`
+ Version string `xml:"ver,attr"`
+ Release string `xml:"rel,attr"`
+ } `xml:"version"`
+ Checksum struct {
+ Checksum string `xml:",chardata"`
+ Type string `xml:"type,attr"`
+ Pkgid string `xml:"pkgid,attr"`
+ } `xml:"checksum"`
+ Summary string `xml:"summary"`
+ Description string `xml:"description"`
+ Packager string `xml:"packager"`
+ URL string `xml:"url"`
+ Time struct {
+ File uint64 `xml:"file,attr"`
+ Build uint64 `xml:"build,attr"`
+ } `xml:"time"`
+ Size struct {
+ Package int64 `xml:"package,attr"`
+ Installed uint64 `xml:"installed,attr"`
+ Archive uint64 `xml:"archive,attr"`
+ } `xml:"size"`
+ Location struct {
+ Href string `xml:"href,attr"`
+ } `xml:"location"`
+ Format struct {
+ License string `xml:"license"`
+ Vendor string `xml:"vendor"`
+ Group string `xml:"group"`
+ Buildhost string `xml:"buildhost"`
+ Sourcerpm string `xml:"sourcerpm"`
+ Provides EntryList `xml:"provides"`
+ Requires EntryList `xml:"requires"`
+ Conflicts EntryList `xml:"conflicts"`
+ Obsoletes EntryList `xml:"obsoletes"`
+ Files []*rpm_module.File `xml:"file"`
+ } `xml:"format"`
+ } `xml:"package"`
+ }
+
+ var result Metadata
+ decodeGzipXML(t, resp, &result)
+
+ assert.EqualValues(t, 1, result.PackageCount)
+ assert.Len(t, result.Packages, 1)
+ p := result.Packages[0]
+ assert.Equal(t, "rpm", p.Type)
+ assert.Equal(t, packageName, p.Name)
+ assert.Equal(t, packageArchitecture, p.Architecture)
+ assert.Equal(t, "YES", p.Checksum.Pkgid)
+ assert.Equal(t, "sha256", p.Checksum.Type)
+ assert.Equal(t, "f1d5d2ffcbe4a7568e98b864f40d923ecca084e9b9bcd5977ed6521c46d3fa4c", p.Checksum.Checksum)
+ assert.Equal(t, "https://gitea.io", p.URL)
+ assert.EqualValues(t, len(content), p.Size.Package)
+ assert.EqualValues(t, 13, p.Size.Installed)
+ assert.EqualValues(t, 272, p.Size.Archive)
+ assert.Equal(t, fmt.Sprintf("package/%s/%s/%s/%s", packageName, packageVersion, packageArchitecture, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture)), p.Location.Href)
+ f := p.Format
+ assert.Equal(t, "MIT", f.License)
+ assert.Len(t, f.Provides.Entries, 2)
+ assert.Len(t, f.Requires.Entries, 7)
+ assert.Empty(t, f.Conflicts.Entries)
+ assert.Empty(t, f.Obsoletes.Entries)
+ assert.Len(t, f.Files, 1)
+ })
+
+ t.Run("filelists.xml.gz", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req = NewRequest(t, "GET", url+"/filelists.xml.gz")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type Filelists struct {
+ XMLName xml.Name `xml:"filelists"`
+ Xmlns string `xml:"xmlns,attr"`
+ PackageCount int `xml:"packages,attr"`
+ Packages []struct {
+ Pkgid string `xml:"pkgid,attr"`
+ Name string `xml:"name,attr"`
+ Architecture string `xml:"arch,attr"`
+ Version struct {
+ Epoch string `xml:"epoch,attr"`
+ Version string `xml:"ver,attr"`
+ Release string `xml:"rel,attr"`
+ } `xml:"version"`
+ Files []*rpm_module.File `xml:"file"`
+ } `xml:"package"`
+ }
+
+ var result Filelists
+ decodeGzipXML(t, resp, &result)
+
+ assert.EqualValues(t, 1, result.PackageCount)
+ assert.Len(t, result.Packages, 1)
+ p := result.Packages[0]
+ assert.NotEmpty(t, p.Pkgid)
+ assert.Equal(t, packageName, p.Name)
+ assert.Equal(t, packageArchitecture, p.Architecture)
+ assert.Len(t, p.Files, 1)
+ f := p.Files[0]
+ assert.Equal(t, "/usr/local/bin/hello", f.Path)
+ })
+
+ t.Run("other.xml.gz", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req = NewRequest(t, "GET", url+"/other.xml.gz")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type Other struct {
+ XMLName xml.Name `xml:"otherdata"`
+ Xmlns string `xml:"xmlns,attr"`
+ PackageCount int `xml:"packages,attr"`
+ Packages []struct {
+ Pkgid string `xml:"pkgid,attr"`
+ Name string `xml:"name,attr"`
+ Architecture string `xml:"arch,attr"`
+ Version struct {
+ Epoch string `xml:"epoch,attr"`
+ Version string `xml:"ver,attr"`
+ Release string `xml:"rel,attr"`
+ } `xml:"version"`
+ Changelogs []*rpm_module.Changelog `xml:"changelog"`
+ } `xml:"package"`
+ }
+
+ var result Other
+ decodeGzipXML(t, resp, &result)
+
+ assert.EqualValues(t, 1, result.PackageCount)
+ assert.Len(t, result.Packages, 1)
+ p := result.Packages[0]
+ assert.NotEmpty(t, p.Pkgid)
+ assert.Equal(t, packageName, p.Name)
+ assert.Equal(t, packageArchitecture, p.Architecture)
+ assert.Len(t, p.Changelogs, 1)
+ c := p.Changelogs[0]
+ assert.Equal(t, "KN4CK3R <dummy@gitea.io>", c.Author)
+ assert.EqualValues(t, 1678276800, c.Date)
+ assert.Equal(t, "- Changelog message.", c.Text)
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRpm)
+ assert.NoError(t, err)
+ assert.Empty(t, pvs)
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture)).
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
})
-
- decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
- t.Helper()
-
- zr, err := gzip.NewReader(resp.Body)
- assert.NoError(t, err)
-
- assert.NoError(t, xml.NewDecoder(zr).Decode(v))
- }
-
- t.Run("primary.xml.gz", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
-
- req = NewRequest(t, "GET", url+"/primary.xml.gz")
- resp := MakeRequest(t, req, http.StatusOK)
-
- type EntryList struct {
- Entries []*rpm_module.Entry `xml:"entry"`
- }
-
- type Metadata struct {
- XMLName xml.Name `xml:"metadata"`
- Xmlns string `xml:"xmlns,attr"`
- XmlnsRpm string `xml:"xmlns:rpm,attr"`
- PackageCount int `xml:"packages,attr"`
- Packages []struct {
- XMLName xml.Name `xml:"package"`
- Type string `xml:"type,attr"`
- Name string `xml:"name"`
- Architecture string `xml:"arch"`
- Version struct {
- Epoch string `xml:"epoch,attr"`
- Version string `xml:"ver,attr"`
- Release string `xml:"rel,attr"`
- } `xml:"version"`
- Checksum struct {
- Checksum string `xml:",chardata"`
- Type string `xml:"type,attr"`
- Pkgid string `xml:"pkgid,attr"`
- } `xml:"checksum"`
- Summary string `xml:"summary"`
- Description string `xml:"description"`
- Packager string `xml:"packager"`
- URL string `xml:"url"`
- Time struct {
- File uint64 `xml:"file,attr"`
- Build uint64 `xml:"build,attr"`
- } `xml:"time"`
- Size struct {
- Package int64 `xml:"package,attr"`
- Installed uint64 `xml:"installed,attr"`
- Archive uint64 `xml:"archive,attr"`
- } `xml:"size"`
- Location struct {
- Href string `xml:"href,attr"`
- } `xml:"location"`
- Format struct {
- License string `xml:"license"`
- Vendor string `xml:"vendor"`
- Group string `xml:"group"`
- Buildhost string `xml:"buildhost"`
- Sourcerpm string `xml:"sourcerpm"`
- Provides EntryList `xml:"provides"`
- Requires EntryList `xml:"requires"`
- Conflicts EntryList `xml:"conflicts"`
- Obsoletes EntryList `xml:"obsoletes"`
- Files []*rpm_module.File `xml:"file"`
- } `xml:"format"`
- } `xml:"package"`
- }
-
- var result Metadata
- decodeGzipXML(t, resp, &result)
-
- assert.EqualValues(t, 1, result.PackageCount)
- assert.Len(t, result.Packages, 1)
- p := result.Packages[0]
- assert.Equal(t, "rpm", p.Type)
- assert.Equal(t, packageName, p.Name)
- assert.Equal(t, packageArchitecture, p.Architecture)
- assert.Equal(t, "YES", p.Checksum.Pkgid)
- assert.Equal(t, "sha256", p.Checksum.Type)
- assert.Equal(t, "f1d5d2ffcbe4a7568e98b864f40d923ecca084e9b9bcd5977ed6521c46d3fa4c", p.Checksum.Checksum)
- assert.Equal(t, "https://gitea.io", p.URL)
- assert.EqualValues(t, len(content), p.Size.Package)
- assert.EqualValues(t, 13, p.Size.Installed)
- assert.EqualValues(t, 272, p.Size.Archive)
- assert.Equal(t, fmt.Sprintf("package/%s/%s/%s/%s", packageName, packageVersion, packageArchitecture, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture)), p.Location.Href)
- f := p.Format
- assert.Equal(t, "MIT", f.License)
- assert.Len(t, f.Provides.Entries, 2)
- assert.Len(t, f.Requires.Entries, 7)
- assert.Empty(t, f.Conflicts.Entries)
- assert.Empty(t, f.Obsoletes.Entries)
- assert.Len(t, f.Files, 1)
- })
-
- t.Run("filelists.xml.gz", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
-
- req = NewRequest(t, "GET", url+"/filelists.xml.gz")
- resp := MakeRequest(t, req, http.StatusOK)
-
- type Filelists struct {
- XMLName xml.Name `xml:"filelists"`
- Xmlns string `xml:"xmlns,attr"`
- PackageCount int `xml:"packages,attr"`
- Packages []struct {
- Pkgid string `xml:"pkgid,attr"`
- Name string `xml:"name,attr"`
- Architecture string `xml:"arch,attr"`
- Version struct {
- Epoch string `xml:"epoch,attr"`
- Version string `xml:"ver,attr"`
- Release string `xml:"rel,attr"`
- } `xml:"version"`
- Files []*rpm_module.File `xml:"file"`
- } `xml:"package"`
- }
-
- var result Filelists
- decodeGzipXML(t, resp, &result)
-
- assert.EqualValues(t, 1, result.PackageCount)
- assert.Len(t, result.Packages, 1)
- p := result.Packages[0]
- assert.NotEmpty(t, p.Pkgid)
- assert.Equal(t, packageName, p.Name)
- assert.Equal(t, packageArchitecture, p.Architecture)
- assert.Len(t, p.Files, 1)
- f := p.Files[0]
- assert.Equal(t, "/usr/local/bin/hello", f.Path)
- })
-
- t.Run("other.xml.gz", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
-
- req = NewRequest(t, "GET", url+"/other.xml.gz")
- resp := MakeRequest(t, req, http.StatusOK)
-
- type Other struct {
- XMLName xml.Name `xml:"otherdata"`
- Xmlns string `xml:"xmlns,attr"`
- PackageCount int `xml:"packages,attr"`
- Packages []struct {
- Pkgid string `xml:"pkgid,attr"`
- Name string `xml:"name,attr"`
- Architecture string `xml:"arch,attr"`
- Version struct {
- Epoch string `xml:"epoch,attr"`
- Version string `xml:"ver,attr"`
- Release string `xml:"rel,attr"`
- } `xml:"version"`
- Changelogs []*rpm_module.Changelog `xml:"changelog"`
- } `xml:"package"`
- }
-
- var result Other
- decodeGzipXML(t, resp, &result)
-
- assert.EqualValues(t, 1, result.PackageCount)
- assert.Len(t, result.Packages, 1)
- p := result.Packages[0]
- assert.NotEmpty(t, p.Pkgid)
- assert.Equal(t, packageName, p.Name)
- assert.Equal(t, packageArchitecture, p.Architecture)
- assert.Len(t, p.Changelogs, 1)
- c := p.Changelogs[0]
- assert.Equal(t, "KN4CK3R <dummy@gitea.io>", c.Author)
- assert.EqualValues(t, 1678276800, c.Date)
- assert.Equal(t, "- Changelog message.", c.Text)
- })
- })
-
- t.Run("Delete", func(t *testing.T) {
- defer tests.PrintCurrentTest(t)()
-
- req := NewRequest(t, "DELETE", fmt.Sprintf("%s/el9/stable/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture))
- MakeRequest(t, req, http.StatusUnauthorized)
-
- req = NewRequest(t, "DELETE", fmt.Sprintf("%s/el9/stable/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)).
- AddBasicAuth(user.Name)
- MakeRequest(t, req, http.StatusNoContent)
-
- pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRpm)
- assert.NoError(t, err)
- assert.Empty(t, pvs)
- req = NewRequest(t, "DELETE", fmt.Sprintf("%s/el9/stable/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)).
- AddBasicAuth(user.Name)
- MakeRequest(t, req, http.StatusNotFound)
- })
+ }
}