mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	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: 
This commit is contained in:
		| @@ -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) |  | ||||||
| 	}) |  | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user