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)tags/v1.22.0-rc0
@@ -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 | |||
``` |
@@ -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 |
@@ -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 |
@@ -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 | |||
} |
@@ -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 |
@@ -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, | |||
) | |||
} |
@@ -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" | |||
@@ -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) | |||
} |
@@ -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 | |||
} |
@@ -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 |
@@ -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 |
@@ -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 | |||
} |
@@ -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") | |||
} |
@@ -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 ( |
@@ -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 |
@@ -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, |
@@ -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}} |
@@ -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) | |||
}) | |||
} | |||
} |