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)
This commit is contained in:
parent
075c4c89ee
commit
461d8b53c2
@ -24,16 +24,26 @@ The following examples use `dnf`.
|
|||||||
|
|
||||||
## Configuring the package registry
|
## 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
|
```shell
|
||||||
dnf config-manager --add-repo https://gitea.example.com/api/packages/{owner}/rpm/{group}.repo
|
dnf config-manager --add-repo https://gitea.example.com/api/packages/{owner}/rpm/{group}.repo
|
||||||
```
|
```
|
||||||
|
|
||||||
| Placeholder | Description |
|
| Placeholder | Description |
|
||||||
| ----------- |----------------------------------------------------|
|
| ----------- | ----------- |
|
||||||
| `owner` | The owner of the package. |
|
| `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:
|
||||||
|
|
||||||
|
```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):
|
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
|
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
|
## Publish a package
|
||||||
|
|
||||||
@ -54,11 +64,17 @@ PUT https://gitea.example.com/api/packages/{owner}/rpm/{group}/upload
|
|||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
| --------- | ----------- |
|
| --------- | ----------- |
|
||||||
| `owner` | The owner of the package. |
|
| `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:
|
Example request using HTTP Basic authentication:
|
||||||
|
|
||||||
```shell
|
```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 \
|
curl --user your_username:your_password_or_token \
|
||||||
--upload-file path/to/file.rpm \
|
--upload-file path/to/file.rpm \
|
||||||
https://gitea.example.com/api/packages/testuser/rpm/centos/el7/upload
|
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}
|
DELETE https://gitea.example.com/api/packages/{owner}/rpm/{group}/package/{package_name}/{package_version}/{architecture}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|-------------------|----------------------------|
|
| ----------------- | ----------- |
|
||||||
| `owner` | The owner of the package. |
|
| `owner` | The owner of the package. |
|
||||||
| `group` | The package group . |
|
| `group` | Optional: The package group. |
|
||||||
| `package_name` | The package name. |
|
| `package_name` | The package name. |
|
||||||
| `package_version` | The package version. |
|
| `package_version` | The package version. |
|
||||||
| `architecture` | The package architecture. |
|
| `architecture` | The package architecture. |
|
||||||
|
|
||||||
Example request using HTTP Basic authentication:
|
Example request using HTTP Basic authentication:
|
||||||
|
|
||||||
```shell
|
```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 \
|
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
|
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) {
|
func TryInsertPackage(ctx context.Context, p *Package) (*Package, error) {
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
|
|
||||||
key := &Package{
|
existing := &Package{}
|
||||||
OwnerID: p.OwnerID,
|
|
||||||
Type: p.Type,
|
|
||||||
LowerName: p.LowerName,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if has {
|
if has {
|
||||||
return key, ErrDuplicatePackage
|
return existing, ErrDuplicatePackage
|
||||||
}
|
}
|
||||||
if _, err = e.Insert(p); err != nil {
|
if _, err = e.Insert(p); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -41,12 +41,20 @@ type PackageBlob struct {
|
|||||||
func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool, error) {
|
func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool, error) {
|
||||||
e := db.GetEngine(ctx)
|
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 {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
if has {
|
if has {
|
||||||
return pb, true, nil
|
return existing, true, nil
|
||||||
}
|
}
|
||||||
if _, err = e.Insert(pb); err != nil {
|
if _, err = e.Insert(pb); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
|
@ -46,18 +46,18 @@ type PackageFile struct {
|
|||||||
func TryInsertFile(ctx context.Context, pf *PackageFile) (*PackageFile, error) {
|
func TryInsertFile(ctx context.Context, pf *PackageFile) (*PackageFile, error) {
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
|
|
||||||
key := &PackageFile{
|
existing := &PackageFile{}
|
||||||
VersionID: pf.VersionID,
|
|
||||||
LowerName: pf.LowerName,
|
|
||||||
CompositeKey: pf.CompositeKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if has {
|
if has {
|
||||||
return pf, ErrDuplicatePackageFile
|
return existing, ErrDuplicatePackageFile
|
||||||
}
|
}
|
||||||
if _, err = e.Insert(pf); err != nil {
|
if _, err = e.Insert(pf); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -93,13 +93,13 @@ func GetFileForVersionByName(ctx context.Context, versionID int64, name, key str
|
|||||||
return nil, ErrPackageFileNotExist
|
return nil, ErrPackageFileNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
pf := &PackageFile{
|
pf := &PackageFile{}
|
||||||
VersionID: versionID,
|
|
||||||
LowerName: strings.ToLower(name),
|
|
||||||
CompositeKey: key,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -39,17 +39,17 @@ type PackageVersion struct {
|
|||||||
func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) {
|
func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) {
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
|
|
||||||
key := &PackageVersion{
|
existing := &PackageVersion{}
|
||||||
PackageID: pv.PackageID,
|
|
||||||
LowerVersion: pv.LowerVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
has, err := e.Get(key)
|
has, err := e.Where(builder.Eq{
|
||||||
|
"package_id": pv.PackageID,
|
||||||
|
"lower_version": pv.LowerVersion,
|
||||||
|
}).Get(existing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if has {
|
if has {
|
||||||
return key, ErrDuplicatePackageVersion
|
return existing, ErrDuplicatePackageVersion
|
||||||
}
|
}
|
||||||
if _, err = e.Insert(pv); err != nil {
|
if _, err = e.Insert(pv); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
23
models/packages/rpm/search.go
Normal file
23
models/packages/rpm/search.go
Normal file
@ -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 (
|
const (
|
||||||
PropertyMetadata = "rpm.metadata"
|
PropertyMetadata = "rpm.metadata"
|
||||||
|
PropertyGroup = "rpm.group"
|
||||||
|
PropertyArchitecture = "rpm.architecture"
|
||||||
|
|
||||||
SettingKeyPrivate = "rpm.key.private"
|
SettingKeyPrivate = "rpm.key.private"
|
||||||
SettingKeyPublic = "rpm.key.public"
|
SettingKeyPublic = "rpm.key.public"
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
@ -26,10 +25,6 @@ func (su *StringUtils) Contains(s, substr string) bool {
|
|||||||
return strings.Contains(s, substr)
|
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 {
|
func (su *StringUtils) Split(s, sep string) []string {
|
||||||
return strings.Split(s, sep)
|
return strings.Split(s, sep)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -45,3 +46,10 @@ func SliceSortedEqual[T comparable](s1, s2 []T) bool {
|
|||||||
func SliceRemoveAll[T comparable](slice []T, target T) []T {
|
func SliceRemoveAll[T comparable](slice []T, target T) []T {
|
||||||
return slices.DeleteFunc(slice, func(t T) bool { return t == target })
|
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.redhat = on RedHat based distributions
|
||||||
rpm.distros.suse = on SUSE based distributions
|
rpm.distros.suse = on SUSE based distributions
|
||||||
rpm.install = To install the package, run the following command:
|
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.install = To install the package using gem, run the following command:
|
||||||
rubygems.install2 = or add it to the Gemfile:
|
rubygems.install2 = or add it to the Gemfile:
|
||||||
rubygems.dependencies.runtime = Runtime Dependencies
|
rubygems.dependencies.runtime = Runtime Dependencies
|
||||||
|
@ -512,7 +512,77 @@ func CommonRoutes() *web.Route {
|
|||||||
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
|
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
|
||||||
r.Get("/simple/{id}", pypi.PackageMetadata)
|
r.Get("/simple/{id}", pypi.PackageMetadata)
|
||||||
}, reqPackageAccess(perm.AccessModeRead))
|
}, 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.Group("/rubygems", func() {
|
||||||
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
|
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
|
||||||
r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
|
r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
|
||||||
@ -577,82 +647,6 @@ func CommonRoutes() *web.Route {
|
|||||||
return r
|
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
|
// ContainerRoutes provides endpoints that implement the OCI API to serve containers
|
||||||
// These have to be mounted on `/v2/...` to comply with the OCI spec:
|
// These have to be mounted on `/v2/...` to comply with the OCI spec:
|
||||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
|
// 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
|
// https://dnf.readthedocs.io/en/latest/conf_ref.html
|
||||||
func GetRepositoryConfig(ctx *context.Context) {
|
func GetRepositoryConfig(ctx *context.Context) {
|
||||||
group := ctx.Params("group")
|
group := ctx.Params("group")
|
||||||
|
|
||||||
|
var groupParts []string
|
||||||
if group != "" {
|
if group != "" {
|
||||||
group = fmt.Sprintf("/%s", group)
|
groupParts = strings.Split(group, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("%sapi/packages/%s/rpm", setting.AppURL, ctx.Package.Owner.Name)
|
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, "/", " - ")+`
|
ctx.PlainText(http.StatusOK, `[gitea-`+strings.Join(append([]string{ctx.Package.Owner.LowerName}, groupParts...), "-")+`]
|
||||||
baseurl=`+url+group+`/
|
name=`+strings.Join(append([]string{ctx.Package.Owner.Name, setting.AppName}, groupParts...), " - ")+`
|
||||||
|
baseurl=`+strings.Join(append([]string{url}, groupParts...), "/")+`
|
||||||
enabled=1
|
enabled=1
|
||||||
gpgcheck=1
|
gpgcheck=1
|
||||||
gpgkey=`+url+`/repository.key`)
|
gpgkey=`+url+`/repository.key`)
|
||||||
@ -157,7 +161,7 @@ func UploadPackageFile(ctx *context.Context) {
|
|||||||
Owner: ctx.Package.Owner,
|
Owner: ctx.Package.Owner,
|
||||||
PackageType: packages_model.TypeRpm,
|
PackageType: packages_model.TypeRpm,
|
||||||
Name: pck.Name,
|
Name: pck.Name,
|
||||||
Version: strings.Trim(fmt.Sprintf("%s/%s", group, pck.Version), "/"),
|
Version: pck.Version,
|
||||||
},
|
},
|
||||||
Creator: ctx.Doer,
|
Creator: ctx.Doer,
|
||||||
Metadata: pck.VersionMetadata,
|
Metadata: pck.VersionMetadata,
|
||||||
@ -171,7 +175,9 @@ func UploadPackageFile(ctx *context.Context) {
|
|||||||
Data: buf,
|
Data: buf,
|
||||||
IsLead: true,
|
IsLead: true,
|
||||||
Properties: map[string]string{
|
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
|
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)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -196,20 +202,20 @@ func UploadPackageFile(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DownloadPackageFile(ctx *context.Context) {
|
func DownloadPackageFile(ctx *context.Context) {
|
||||||
group := ctx.Params("group")
|
|
||||||
name := ctx.Params("name")
|
name := ctx.Params("name")
|
||||||
version := ctx.Params("version")
|
version := ctx.Params("version")
|
||||||
|
|
||||||
s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
|
||||||
ctx,
|
ctx,
|
||||||
&packages_service.PackageInfo{
|
&packages_service.PackageInfo{
|
||||||
Owner: ctx.Package.Owner,
|
Owner: ctx.Package.Owner,
|
||||||
PackageType: packages_model.TypeRpm,
|
PackageType: packages_model.TypeRpm,
|
||||||
Name: name,
|
Name: name,
|
||||||
Version: strings.Trim(fmt.Sprintf("%s/%s", group, version), "/"),
|
Version: version,
|
||||||
},
|
},
|
||||||
&packages_service.PackageFileInfo{
|
&packages_service.PackageFileInfo{
|
||||||
Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.Params("architecture")),
|
Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.Params("architecture")),
|
||||||
CompositeKey: group,
|
CompositeKey: ctx.Params("group"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -229,6 +235,7 @@ func DeletePackageFile(webctx *context.Context) {
|
|||||||
name := webctx.Params("name")
|
name := webctx.Params("name")
|
||||||
version := webctx.Params("version")
|
version := webctx.Params("version")
|
||||||
architecture := webctx.Params("architecture")
|
architecture := webctx.Params("architecture")
|
||||||
|
|
||||||
var pd *packages_model.PackageDescriptor
|
var pd *packages_model.PackageDescriptor
|
||||||
|
|
||||||
err := db.WithTx(webctx, func(ctx stdctx.Context) error {
|
err := db.WithTx(webctx, func(ctx stdctx.Context) error {
|
||||||
@ -236,7 +243,7 @@ func DeletePackageFile(webctx *context.Context) {
|
|||||||
webctx.Package.Owner.ID,
|
webctx.Package.Owner.ID,
|
||||||
packages_model.TypeRpm,
|
packages_model.TypeRpm,
|
||||||
name,
|
name,
|
||||||
strings.Trim(fmt.Sprintf("%s/%s", group, version), "/"),
|
version,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -286,7 +293,7 @@ func DeletePackageFile(webctx *context.Context) {
|
|||||||
notify_service.PackageDelete(webctx, webctx.Doer, pd)
|
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)
|
apiError(webctx, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,6 @@ func CleanupExpiredData(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Flash.Success(ctx.Tr("packages.cleanup.success"))
|
ctx.Flash.Success(ctx.Tr("admin.packages.cleanup.success"))
|
||||||
ctx.Redirect(setting.AppSubURL + "/admin/packages")
|
ctx.Redirect(setting.AppSubURL + "/admin/packages")
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
|
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
|
||||||
debian_module "code.gitea.io/gitea/modules/packages/debian"
|
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/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
@ -195,9 +196,9 @@ func ViewPackageVersion(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["Branches"] = branches.Values()
|
ctx.Data["Branches"] = util.Sorted(branches.Values())
|
||||||
ctx.Data["Repositories"] = repositories.Values()
|
ctx.Data["Repositories"] = util.Sorted(repositories.Values())
|
||||||
ctx.Data["Architectures"] = architectures.Values()
|
ctx.Data["Architectures"] = util.Sorted(architectures.Values())
|
||||||
case packages_model.TypeDebian:
|
case packages_model.TypeDebian:
|
||||||
distributions := make(container.Set[string])
|
distributions := make(container.Set[string])
|
||||||
components := 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["Distributions"] = util.Sorted(distributions.Values())
|
||||||
ctx.Data["Components"] = components.Values()
|
ctx.Data["Components"] = util.Sorted(components.Values())
|
||||||
ctx.Data["Architectures"] = architectures.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 (
|
var (
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
cargo_service "code.gitea.io/gitea/services/packages/cargo"
|
cargo_service "code.gitea.io/gitea/services/packages/cargo"
|
||||||
container_service "code.gitea.io/gitea/services/packages/container"
|
container_service "code.gitea.io/gitea/services/packages/container"
|
||||||
debian_service "code.gitea.io/gitea/services/packages/debian"
|
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
|
// 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 {
|
if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
|
||||||
return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
|
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
|
return nil
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
packages_model "code.gitea.io/gitea/models/packages"
|
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"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
packages_module "code.gitea.io/gitea/modules/packages"
|
packages_module "code.gitea.io/gitea/modules/packages"
|
||||||
@ -96,6 +97,39 @@ func generateKeypair() (string, string, error) {
|
|||||||
return priv.String(), pub.String(), nil
|
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 {
|
type repoChecksum struct {
|
||||||
Value string `xml:",chardata"`
|
Value string `xml:",chardata"`
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
@ -126,7 +160,7 @@ type packageData struct {
|
|||||||
type packageCache = map[*packages_model.PackageFile]*packageData
|
type packageCache = map[*packages_model.PackageFile]*packageData
|
||||||
|
|
||||||
// BuildSpecificRepositoryFiles builds metadata files for the repository
|
// 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)
|
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -136,7 +170,7 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey strin
|
|||||||
OwnerID: ownerID,
|
OwnerID: ownerID,
|
||||||
PackageType: packages_model.TypeRpm,
|
PackageType: packages_model.TypeRpm,
|
||||||
Query: "%.rpm",
|
Query: "%.rpm",
|
||||||
CompositeKey: compositeKey,
|
CompositeKey: group,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -195,15 +229,15 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey strin
|
|||||||
cache[pf] = pd
|
cache[pf] = pd
|
||||||
}
|
}
|
||||||
|
|
||||||
primary, err := buildPrimary(ctx, pv, pfs, cache, compositeKey)
|
primary, err := buildPrimary(ctx, pv, pfs, cache, group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filelists, err := buildFilelists(ctx, pv, pfs, cache, compositeKey)
|
filelists, err := buildFilelists(ctx, pv, pfs, cache, group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
other, err := buildOther(ctx, pv, pfs, cache, compositeKey)
|
other, err := buildOther(ctx, pv, pfs, cache, group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -217,12 +251,12 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey strin
|
|||||||
filelists,
|
filelists,
|
||||||
other,
|
other,
|
||||||
},
|
},
|
||||||
compositeKey,
|
group,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml
|
// 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 {
|
type Repomd struct {
|
||||||
XMLName xml.Name `xml:"repomd"`
|
XMLName xml.Name `xml:"repomd"`
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
@ -278,7 +312,7 @@ func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID
|
|||||||
&packages_service.PackageFileCreationInfo{
|
&packages_service.PackageFileCreationInfo{
|
||||||
PackageFileInfo: packages_service.PackageFileInfo{
|
PackageFileInfo: packages_service.PackageFileInfo{
|
||||||
Filename: file.Name,
|
Filename: file.Name,
|
||||||
CompositeKey: compositeKey,
|
CompositeKey: group,
|
||||||
},
|
},
|
||||||
Creator: user_model.NewGhostUser(),
|
Creator: user_model.NewGhostUser(),
|
||||||
Data: file.Data,
|
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
|
// 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 {
|
type Version struct {
|
||||||
Epoch string `xml:"epoch,attr"`
|
Epoch string `xml:"epoch,attr"`
|
||||||
Version string `xml:"ver,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",
|
XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
|
||||||
PackageCount: len(pfs),
|
PackageCount: len(pfs),
|
||||||
Packages: packages,
|
Packages: packages,
|
||||||
}, compositeKey)
|
}, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml
|
// 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 {
|
type Version struct {
|
||||||
Epoch string `xml:"epoch,attr"`
|
Epoch string `xml:"epoch,attr"`
|
||||||
Version string `xml:"ver,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",
|
Xmlns: "http://linux.duke.edu/metadata/other",
|
||||||
PackageCount: len(pfs),
|
PackageCount: len(pfs),
|
||||||
Packages: packages,
|
Packages: packages,
|
||||||
},
|
}, group)
|
||||||
compositeKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml
|
// 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 {
|
type Version struct {
|
||||||
Epoch string `xml:"epoch,attr"`
|
Epoch string `xml:"epoch,attr"`
|
||||||
Version string `xml:"ver,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",
|
Xmlns: "http://linux.duke.edu/metadata/other",
|
||||||
PackageCount: len(pfs),
|
PackageCount: len(pfs),
|
||||||
Packages: packages,
|
Packages: packages,
|
||||||
}, compositeKey)
|
}, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writtenCounter counts all written bytes
|
// writtenCounter counts all written bytes
|
||||||
@ -549,8 +582,10 @@ func (wc *writtenCounter) Written() int64 {
|
|||||||
return wc.written
|
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()
|
content, _ := packages_module.NewHashedBuffer()
|
||||||
|
defer content.Close()
|
||||||
|
|
||||||
gzw := gzip.NewWriter(content)
|
gzw := gzip.NewWriter(content)
|
||||||
wc := &writtenCounter{}
|
wc := &writtenCounter{}
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
@ -574,7 +609,7 @@ func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion,
|
|||||||
&packages_service.PackageFileCreationInfo{
|
&packages_service.PackageFileCreationInfo{
|
||||||
PackageFileInfo: packages_service.PackageFileInfo{
|
PackageFileInfo: packages_service.PackageFileInfo{
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
CompositeKey: compositeKey,
|
CompositeKey: group,
|
||||||
},
|
},
|
||||||
Creator: user_model.NewGhostUser(),
|
Creator: user_model.NewGhostUser(),
|
||||||
Data: content,
|
Data: content,
|
||||||
|
@ -4,15 +4,21 @@
|
|||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rpm.registry"}}</label>
|
<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"}}
|
<div class="markup"><pre class="code-block"><code>{{- if gt (len .Groups) 1 -}}
|
||||||
{{$group_name:= StringUtils.ReplaceAllStringRegex .PackageDescriptor.Version.Version "(/[^/]+|[^/]*)\\z" "" -}}
|
# {{ctx.Locale.Tr "packages.rpm.repository.multiple_groups"}}
|
||||||
{{- if $group_name -}}
|
|
||||||
{{- $group_name = (print "/" $group_name) -}}
|
{{end -}}
|
||||||
{{- end -}}
|
# {{ctx.Locale.Tr "packages.rpm.distros.redhat"}}
|
||||||
dnf config-manager --add-repo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group_name}}.repo"></gitea-origin-url>
|
{{- 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"}}
|
# {{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>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rpm.install"}}</label>
|
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rpm.install"}}</label>
|
||||||
@ -30,6 +36,18 @@ zypper install {{$.PackageDescriptor.Package.Name}}</code></pre>
|
|||||||
</div>
|
</div>
|
||||||
</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}}
|
{{if or .PackageDescriptor.Metadata.Summary .PackageDescriptor.Metadata.Description}}
|
||||||
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
|
<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}}
|
{{if .PackageDescriptor.Metadata.Summary}}<div class="ui attached segment">{{.PackageDescriptor.Metadata.Summary}}</div>{{end}}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
@ -20,6 +21,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
|
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -73,346 +75,362 @@ Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5
|
|||||||
|
|
||||||
rootURL := fmt.Sprintf("/api/packages/%s/rpm", user.Name)
|
rootURL := fmt.Sprintf("/api/packages/%s/rpm", user.Name)
|
||||||
|
|
||||||
t.Run("RepositoryConfig", func(t *testing.T) {
|
for _, group := range []string{"", "el9", "el9/stable"} {
|
||||||
defer tests.PrintCurrentTest(t)()
|
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...), "/")
|
||||||
|
|
||||||
req := NewRequest(t, "GET", rootURL+"/el9/stable.repo")
|
t.Run("RepositoryConfig", func(t *testing.T) {
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
expected := fmt.Sprintf(`[gitea-%s-el9-stable]
|
req := NewRequest(t, "GET", groupURL+".repo")
|
||||||
name=%s - %s - el9 - stable
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
baseurl=%sapi/packages/%s/rpm/el9/stable/
|
|
||||||
|
expected := fmt.Sprintf(`[gitea-%s]
|
||||||
|
name=%s
|
||||||
|
baseurl=%s
|
||||||
enabled=1
|
enabled=1
|
||||||
gpgcheck=1
|
gpgcheck=1
|
||||||
gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppName, setting.AppURL, user.Name, setting.AppURL, user.Name)
|
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())
|
assert.Equal(t, expected, resp.Body.String())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("RepositoryKey", func(t *testing.T) {
|
t.Run("RepositoryKey", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
req := NewRequest(t, "GET", rootURL+"/repository.key")
|
req := NewRequest(t, "GET", rootURL+"/repository.key")
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
|
assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
|
||||||
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Upload", func(t *testing.T) {
|
t.Run("Upload", func(t *testing.T) {
|
||||||
url := rootURL + "/el9/stable/upload"
|
url := groupURL + "/upload"
|
||||||
|
|
||||||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
|
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
|
||||||
MakeRequest(t, req, http.StatusUnauthorized)
|
MakeRequest(t, req, http.StatusUnauthorized)
|
||||||
|
|
||||||
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
|
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
|
||||||
AddBasicAuth(user.Name)
|
AddBasicAuth(user.Name)
|
||||||
MakeRequest(t, req, http.StatusCreated)
|
MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRpm)
|
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRpm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, pvs, 1)
|
assert.Len(t, pvs, 1)
|
||||||
|
|
||||||
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
|
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Nil(t, pd.SemVer)
|
assert.Nil(t, pd.SemVer)
|
||||||
assert.IsType(t, &rpm_module.VersionMetadata{}, pd.Metadata)
|
assert.IsType(t, &rpm_module.VersionMetadata{}, pd.Metadata)
|
||||||
assert.Equal(t, packageName, pd.Package.Name)
|
assert.Equal(t, packageName, pd.Package.Name)
|
||||||
assert.Equal(t, fmt.Sprintf("el9/stable/%s", packageVersion), pd.Version.Version)
|
assert.Equal(t, packageVersion, pd.Version.Version)
|
||||||
|
|
||||||
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
|
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, pfs, 1)
|
assert.Len(t, pfs, 1)
|
||||||
assert.Equal(t, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture), pfs[0].Name)
|
assert.Equal(t, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture), pfs[0].Name)
|
||||||
assert.True(t, pfs[0].IsLead)
|
assert.True(t, pfs[0].IsLead)
|
||||||
|
|
||||||
pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
|
pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(len(content)), pb.Size)
|
assert.Equal(t, int64(len(content)), pb.Size)
|
||||||
|
|
||||||
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
|
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
|
||||||
AddBasicAuth(user.Name)
|
AddBasicAuth(user.Name)
|
||||||
MakeRequest(t, req, http.StatusConflict)
|
MakeRequest(t, req, http.StatusConflict)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Download", func(t *testing.T) {
|
t.Run("Download", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/el9/stable/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture))
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture))
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
assert.Equal(t, content, resp.Body.Bytes())
|
assert.Equal(t, content, resp.Body.Bytes())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Repository", func(t *testing.T) {
|
t.Run("Repository", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
url := rootURL + "/el9/stable/repodata"
|
url := groupURL + "/repodata"
|
||||||
|
|
||||||
req := NewRequest(t, "HEAD", url+"/dummy.xml")
|
req := NewRequest(t, "HEAD", url+"/dummy.xml")
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
req = NewRequest(t, "GET", url+"/dummy.xml")
|
req = NewRequest(t, "GET", url+"/dummy.xml")
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
t.Run("repomd.xml", func(t *testing.T) {
|
t.Run("repomd.xml", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
req = NewRequest(t, "HEAD", url+"/repomd.xml")
|
req = NewRequest(t, "HEAD", url+"/repomd.xml")
|
||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
req = NewRequest(t, "GET", url+"/repomd.xml")
|
req = NewRequest(t, "GET", url+"/repomd.xml")
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
type Repomd struct {
|
type Repomd struct {
|
||||||
XMLName xml.Name `xml:"repomd"`
|
XMLName xml.Name `xml:"repomd"`
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
XmlnsRpm string `xml:"xmlns:rpm,attr"`
|
XmlnsRpm string `xml:"xmlns:rpm,attr"`
|
||||||
Data []struct {
|
Data []struct {
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
Checksum struct {
|
Checksum struct {
|
||||||
Value string `xml:",chardata"`
|
Value string `xml:",chardata"`
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
} `xml:"checksum"`
|
} `xml:"checksum"`
|
||||||
OpenChecksum struct {
|
OpenChecksum struct {
|
||||||
Value string `xml:",chardata"`
|
Value string `xml:",chardata"`
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
} `xml:"open-checksum"`
|
} `xml:"open-checksum"`
|
||||||
Location struct {
|
Location struct {
|
||||||
Href string `xml:"href,attr"`
|
Href string `xml:"href,attr"`
|
||||||
} `xml:"location"`
|
} `xml:"location"`
|
||||||
Timestamp int64 `xml:"timestamp"`
|
Timestamp int64 `xml:"timestamp"`
|
||||||
Size int64 `xml:"size"`
|
Size int64 `xml:"size"`
|
||||||
OpenSize int64 `xml:"open-size"`
|
OpenSize int64 `xml:"open-size"`
|
||||||
} `xml:"data"`
|
} `xml:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var result Repomd
|
var result Repomd
|
||||||
decodeXML(t, resp, &result)
|
decodeXML(t, resp, &result)
|
||||||
|
|
||||||
assert.Len(t, result.Data, 3)
|
assert.Len(t, result.Data, 3)
|
||||||
for _, d := range result.Data {
|
for _, d := range result.Data {
|
||||||
assert.Equal(t, "sha256", d.Checksum.Type)
|
assert.Equal(t, "sha256", d.Checksum.Type)
|
||||||
assert.NotEmpty(t, d.Checksum.Value)
|
assert.NotEmpty(t, d.Checksum.Value)
|
||||||
assert.Equal(t, "sha256", d.OpenChecksum.Type)
|
assert.Equal(t, "sha256", d.OpenChecksum.Type)
|
||||||
assert.NotEmpty(t, d.OpenChecksum.Value)
|
assert.NotEmpty(t, d.OpenChecksum.Value)
|
||||||
assert.NotEqual(t, d.Checksum.Value, d.OpenChecksum.Value)
|
assert.NotEqual(t, d.Checksum.Value, d.OpenChecksum.Value)
|
||||||
assert.Greater(t, d.OpenSize, d.Size)
|
assert.Greater(t, d.OpenSize, d.Size)
|
||||||
|
|
||||||
switch d.Type {
|
switch d.Type {
|
||||||
case "primary":
|
case "primary":
|
||||||
assert.EqualValues(t, 722, d.Size)
|
assert.EqualValues(t, 722, d.Size)
|
||||||
assert.EqualValues(t, 1759, d.OpenSize)
|
assert.EqualValues(t, 1759, d.OpenSize)
|
||||||
assert.Equal(t, "repodata/primary.xml.gz", d.Location.Href)
|
assert.Equal(t, "repodata/primary.xml.gz", d.Location.Href)
|
||||||
case "filelists":
|
case "filelists":
|
||||||
assert.EqualValues(t, 257, d.Size)
|
assert.EqualValues(t, 257, d.Size)
|
||||||
assert.EqualValues(t, 326, d.OpenSize)
|
assert.EqualValues(t, 326, d.OpenSize)
|
||||||
assert.Equal(t, "repodata/filelists.xml.gz", d.Location.Href)
|
assert.Equal(t, "repodata/filelists.xml.gz", d.Location.Href)
|
||||||
case "other":
|
case "other":
|
||||||
assert.EqualValues(t, 306, d.Size)
|
assert.EqualValues(t, 306, d.Size)
|
||||||
assert.EqualValues(t, 394, d.OpenSize)
|
assert.EqualValues(t, 394, d.OpenSize)
|
||||||
assert.Equal(t, "repodata/other.xml.gz", d.Location.Href)
|
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("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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
}
|
||||||
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("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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user