From 841cbebd82500198956c2038e582a38e6cc254d3 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 19 Jan 2024 12:37:10 +0100 Subject: [PATCH] Fix some RPM registry flaws (#28782) Related #26984 (https://github.com/go-gitea/gitea/pull/26984#issuecomment-1889588912) Fix admin cleanup message. Fix models `Get` not respecting default values. Rebuild RPM repository files after cleanup. Do not add RPM group to package version name. Force stable sorting of Alpine/Debian/RPM repository data. Fix missing deferred `Close`. Add tests for multiple RPM groups. Removed non-cached `ReplaceAllStringRegex`. If there are multiple groups available, it's stated in the package installation screen: ![grafik](https://github.com/go-gitea/gitea/assets/1666336/8f132760-882c-4ab8-9678-77e47dfc4415) --- docs/content/usage/packages/rpm.en-us.md | 49 +- models/packages/package.go | 14 +- models/packages/package_blob.go | 12 +- models/packages/package_file.go | 26 +- models/packages/package_version.go | 12 +- models/packages/rpm/search.go | 23 + modules/packages/rpm/metadata.go | 5 +- modules/templates/util_string.go | 5 - modules/util/slice.go | 8 + options/locale/locale_en-US.ini | 3 + routers/api/packages/api.go | 148 +++-- routers/api/packages/rpm/rpm.go | 31 +- routers/web/admin/packages.go | 2 +- routers/web/user/package.go | 30 +- services/packages/cleanup/cleanup.go | 5 + services/packages/rpm/repository.go | 69 ++- templates/package/content/rpm.tmpl | 32 +- tests/integration/api_packages_rpm_test.go | 688 +++++++++++---------- 18 files changed, 659 insertions(+), 503 deletions(-) create mode 100644 models/packages/rpm/search.go diff --git a/docs/content/usage/packages/rpm.en-us.md b/docs/content/usage/packages/rpm.en-us.md index 586e48d47fe3..1f93376b7b19 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 380a076f9dfe..65a25741509e 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 d1f470d5205a..d9c30b653337 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 1c2c9ac072f4..1bb6b57a34e8 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 9999fc4dab7b..8fc475691bd0 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 000000000000..e697421b4941 --- /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 1ba4c73e8d5c..7fc47a53e69a 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 613940ccdc48..18a0d5cacc97 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 6d63ab4a7771..a7073fedee8b 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 8454a9fce7ff..941e064b4f8b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3424,6 +3424,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 902638712978..d990ebb56a75 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 75d19e2b436b..5d06680552cb 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 f4e1a93a22f1..35ce215be481 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 d8da6a192e55..708af3e43c98 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 04ee6c441976..0ff8077bc907 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 7a49efde4f65..c52c8a5dd98c 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 4fd54a319773..0f128fd3fb56 100644 --- a/templates/package/content/rpm.tmpl +++ b/templates/package/content/rpm.tmpl @@ -4,15 +4,21 @@
-
# {{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 
+				
{{- 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 
+{{- end}}
 
 # {{ctx.Locale.Tr "packages.rpm.distros.suse"}}
-zypper addrepo 
+{{- range $group := .Groups}} + {{- if $group}}{{$group = print "/" $group}}{{end}} +zypper addrepo +{{- end}}
@@ -30,6 +36,18 @@ zypper install {{$.PackageDescriptor.Package.Name}}
+

{{ctx.Locale.Tr "packages.rpm.repository"}}

+
+ + + + + + + +
{{ctx.Locale.Tr "packages.rpm.repository.architectures"}}
{{StringUtils.Join .Architectures ", "}}
+
+ {{if or .PackageDescriptor.Metadata.Summary .PackageDescriptor.Metadata.Description}}

{{ctx.Locale.Tr "packages.about"}}

{{if .PackageDescriptor.Metadata.Summary}}
{{.PackageDescriptor.Metadata.Summary}}
{{end}} diff --git a/tests/integration/api_packages_rpm_test.go b/tests/integration/api_packages_rpm_test.go index 822b0b040e78..1dcec6099e32 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 ", 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 ", 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) - }) + } }