mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 11:41:32 +01: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:
		
							parent
							
								
									075c4c89ee
								
							
						
					
					
						commit
						461d8b53c2
					
				| @ -24,16 +24,26 @@ The following examples use `dnf`. | ||||
| 
 | ||||
| ## Configuring the package registry | ||||
| 
 | ||||
| To register the RPM registry add the url to the list of known apt sources: | ||||
| To register the RPM registry add the url to the list of known sources: | ||||
| 
 | ||||
| ```shell | ||||
| dnf config-manager --add-repo https://gitea.example.com/api/packages/{owner}/rpm/{group}.repo | ||||
| ``` | ||||
| 
 | ||||
| | Placeholder | Description | | ||||
| | ----------- |----------------------------------------------------| | ||||
| | ----------- | ----------- | | ||||
| | `owner`     | The owner of the package. | | ||||
| | `group`     |  Everything, e.g. `el7`, `rocky/el9` , `test/fc38`.| | ||||
| | `group`     | Optional: Everything, e.g. empty, `el7`, `rocky/el9`, `test/fc38`. | | ||||
| 
 | ||||
| Example: | ||||
| 
 | ||||
| ```shell | ||||
| # without a group | ||||
| dnf config-manager --add-repo https://gitea.example.com/api/packages/testuser/rpm.repo | ||||
| 
 | ||||
| # with the group 'centos/el7' | ||||
| dnf config-manager --add-repo https://gitea.example.com/api/packages/testuser/rpm/centos/el7.repo | ||||
| ``` | ||||
| 
 | ||||
| If the registry is private, provide credentials in the url. You can use a password or a [personal access token](development/api-usage.md#authentication): | ||||
| 
 | ||||
| @ -41,7 +51,7 @@ If the registry is private, provide credentials in the url. You can use a passwo | ||||
| dnf config-manager --add-repo https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/rpm/{group}.repo | ||||
| ``` | ||||
| 
 | ||||
| You have to add the credentials to the urls in the `rpm.repo` file in `/etc/yum.repos.d` too. | ||||
| You have to add the credentials to the urls in the created `.repo` file in `/etc/yum.repos.d` too. | ||||
| 
 | ||||
| ## Publish a package | ||||
| 
 | ||||
| @ -54,11 +64,17 @@ PUT https://gitea.example.com/api/packages/{owner}/rpm/{group}/upload | ||||
| | Parameter | Description | | ||||
| | --------- | ----------- | | ||||
| | `owner`   | The owner of the package. | | ||||
| | `group`   |  Everything, e.g. `el7`, `rocky/el9` , `test/fc38`.| | ||||
| | `group`   | Optional: Everything, e.g. empty, `el7`, `rocky/el9`, `test/fc38`. | | ||||
| 
 | ||||
| Example request using HTTP Basic authentication: | ||||
| 
 | ||||
| ```shell | ||||
| # without a group | ||||
| curl --user your_username:your_password_or_token \ | ||||
|      --upload-file path/to/file.rpm \ | ||||
|      https://gitea.example.com/api/packages/testuser/rpm/upload | ||||
| 
 | ||||
| # with the group 'centos/el7' | ||||
| curl --user your_username:your_password_or_token \ | ||||
|      --upload-file path/to/file.rpm \ | ||||
|      https://gitea.example.com/api/packages/testuser/rpm/centos/el7/upload | ||||
| @ -84,9 +100,9 @@ DELETE https://gitea.example.com/api/packages/{owner}/rpm/{group}/package/{packa | ||||
| ``` | ||||
| 
 | ||||
| | Parameter         | Description | | ||||
| |-------------------|----------------------------| | ||||
| | ----------------- | ----------- | | ||||
| | `owner`           | The owner of the package. | | ||||
| | `group`           | The package group       .  | | ||||
| | `group`           | Optional: The package group. | | ||||
| | `package_name`    | The package name. | | ||||
| | `package_version` | The package version. | | ||||
| | `architecture`    | The package architecture. | | ||||
| @ -94,6 +110,11 @@ DELETE https://gitea.example.com/api/packages/{owner}/rpm/{group}/package/{packa | ||||
| Example request using HTTP Basic authentication: | ||||
| 
 | ||||
| ```shell | ||||
| # without a group | ||||
| curl --user your_username:your_token_or_password -X DELETE \ | ||||
|      https://gitea.example.com/api/packages/testuser/rpm/package/test-package/1.0.0/x86_64 | ||||
| 
 | ||||
| # with the group 'centos/el7' | ||||
| curl --user your_username:your_token_or_password -X DELETE \ | ||||
|      https://gitea.example.com/api/packages/testuser/rpm/centos/el7/package/test-package/1.0.0/x86_64 | ||||
| ``` | ||||
|  | ||||
| @ -191,18 +191,18 @@ type Package struct { | ||||
| func TryInsertPackage(ctx context.Context, p *Package) (*Package, error) { | ||||
| 	e := db.GetEngine(ctx) | ||||
| 
 | ||||
| 	key := &Package{ | ||||
| 		OwnerID:   p.OwnerID, | ||||
| 		Type:      p.Type, | ||||
| 		LowerName: p.LowerName, | ||||
| 	} | ||||
| 	existing := &Package{} | ||||
| 
 | ||||
| 	has, err := e.Get(key) | ||||
| 	has, err := e.Where(builder.Eq{ | ||||
| 		"owner_id":   p.OwnerID, | ||||
| 		"type":       p.Type, | ||||
| 		"lower_name": p.LowerName, | ||||
| 	}).Get(existing) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if has { | ||||
| 		return key, ErrDuplicatePackage | ||||
| 		return existing, ErrDuplicatePackage | ||||
| 	} | ||||
| 	if _, err = e.Insert(p); err != nil { | ||||
| 		return nil, err | ||||
|  | ||||
| @ -41,12 +41,20 @@ type PackageBlob struct { | ||||
| func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool, error) { | ||||
| 	e := db.GetEngine(ctx) | ||||
| 
 | ||||
| 	has, err := e.Get(pb) | ||||
| 	existing := &PackageBlob{} | ||||
| 
 | ||||
| 	has, err := e.Where(builder.Eq{ | ||||
| 		"size":        pb.Size, | ||||
| 		"hash_md5":    pb.HashMD5, | ||||
| 		"hash_sha1":   pb.HashSHA1, | ||||
| 		"hash_sha256": pb.HashSHA256, | ||||
| 		"hash_sha512": pb.HashSHA512, | ||||
| 	}).Get(existing) | ||||
| 	if err != nil { | ||||
| 		return nil, false, err | ||||
| 	} | ||||
| 	if has { | ||||
| 		return pb, true, nil | ||||
| 		return existing, true, nil | ||||
| 	} | ||||
| 	if _, err = e.Insert(pb); err != nil { | ||||
| 		return nil, false, err | ||||
|  | ||||
| @ -46,18 +46,18 @@ type PackageFile struct { | ||||
| func TryInsertFile(ctx context.Context, pf *PackageFile) (*PackageFile, error) { | ||||
| 	e := db.GetEngine(ctx) | ||||
| 
 | ||||
| 	key := &PackageFile{ | ||||
| 		VersionID:    pf.VersionID, | ||||
| 		LowerName:    pf.LowerName, | ||||
| 		CompositeKey: pf.CompositeKey, | ||||
| 	} | ||||
| 	existing := &PackageFile{} | ||||
| 
 | ||||
| 	has, err := e.Get(key) | ||||
| 	has, err := e.Where(builder.Eq{ | ||||
| 		"version_id":    pf.VersionID, | ||||
| 		"lower_name":    pf.LowerName, | ||||
| 		"composite_key": pf.CompositeKey, | ||||
| 	}).Get(existing) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if has { | ||||
| 		return pf, ErrDuplicatePackageFile | ||||
| 		return existing, ErrDuplicatePackageFile | ||||
| 	} | ||||
| 	if _, err = e.Insert(pf); err != nil { | ||||
| 		return nil, err | ||||
| @ -93,13 +93,13 @@ func GetFileForVersionByName(ctx context.Context, versionID int64, name, key str | ||||
| 		return nil, ErrPackageFileNotExist | ||||
| 	} | ||||
| 
 | ||||
| 	pf := &PackageFile{ | ||||
| 		VersionID:    versionID, | ||||
| 		LowerName:    strings.ToLower(name), | ||||
| 		CompositeKey: key, | ||||
| 	} | ||||
| 	pf := &PackageFile{} | ||||
| 
 | ||||
| 	has, err := db.GetEngine(ctx).Get(pf) | ||||
| 	has, err := db.GetEngine(ctx).Where(builder.Eq{ | ||||
| 		"version_id":    versionID, | ||||
| 		"lower_name":    strings.ToLower(name), | ||||
| 		"composite_key": key, | ||||
| 	}).Get(pf) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @ -39,17 +39,17 @@ type PackageVersion struct { | ||||
| func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) { | ||||
| 	e := db.GetEngine(ctx) | ||||
| 
 | ||||
| 	key := &PackageVersion{ | ||||
| 		PackageID:    pv.PackageID, | ||||
| 		LowerVersion: pv.LowerVersion, | ||||
| 	} | ||||
| 	existing := &PackageVersion{} | ||||
| 
 | ||||
| 	has, err := e.Get(key) | ||||
| 	has, err := e.Where(builder.Eq{ | ||||
| 		"package_id":    pv.PackageID, | ||||
| 		"lower_version": pv.LowerVersion, | ||||
| 	}).Get(existing) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if has { | ||||
| 		return key, ErrDuplicatePackageVersion | ||||
| 		return existing, ErrDuplicatePackageVersion | ||||
| 	} | ||||
| 	if _, err = e.Insert(pv); err != nil { | ||||
| 		return nil, err | ||||
|  | ||||
							
								
								
									
										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, | ||||
| 	) | ||||
| } | ||||
| @ -16,6 +16,9 @@ import ( | ||||
| 
 | ||||
| const ( | ||||
| 	PropertyMetadata     = "rpm.metadata" | ||||
| 	PropertyGroup        = "rpm.group" | ||||
| 	PropertyArchitecture = "rpm.architecture" | ||||
| 
 | ||||
| 	SettingKeyPrivate = "rpm.key.private" | ||||
| 	SettingKeyPublic  = "rpm.key.public" | ||||
| 
 | ||||
|  | ||||
| @ -4,7 +4,6 @@ | ||||
| package templates | ||||
| 
 | ||||
| import ( | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| @ -26,10 +25,6 @@ func (su *StringUtils) Contains(s, substr string) bool { | ||||
| 	return strings.Contains(s, substr) | ||||
| } | ||||
| 
 | ||||
| func (su *StringUtils) ReplaceAllStringRegex(s, regex, new string) string { | ||||
| 	return regexp.MustCompile(regex).ReplaceAllString(s, new) | ||||
| } | ||||
| 
 | ||||
| func (su *StringUtils) Split(s, sep string) []string { | ||||
| 	return strings.Split(s, sep) | ||||
| } | ||||
|  | ||||
| @ -4,6 +4,7 @@ | ||||
| package util | ||||
| 
 | ||||
| import ( | ||||
| 	"cmp" | ||||
| 	"slices" | ||||
| 	"strings" | ||||
| ) | ||||
| @ -45,3 +46,10 @@ func SliceSortedEqual[T comparable](s1, s2 []T) bool { | ||||
| func SliceRemoveAll[T comparable](slice []T, target T) []T { | ||||
| 	return slices.DeleteFunc(slice, func(t T) bool { return t == target }) | ||||
| } | ||||
| 
 | ||||
| // Sorted returns the sorted slice | ||||
| // Note: The parameter is sorted inline. | ||||
| func Sorted[S ~[]E, E cmp.Ordered](values S) S { | ||||
| 	slices.Sort(values) | ||||
| 	return values | ||||
| } | ||||
|  | ||||
| @ -3414,6 +3414,9 @@ rpm.registry = Setup this registry from the command line: | ||||
| rpm.distros.redhat = on RedHat based distributions | ||||
| rpm.distros.suse = on SUSE based distributions | ||||
| rpm.install = To install the package, run the following command: | ||||
| rpm.repository = Repository Info | ||||
| rpm.repository.architectures = Architectures | ||||
| rpm.repository.multiple_groups = This package is available in multiple groups. | ||||
| rubygems.install = To install the package using gem, run the following command: | ||||
| rubygems.install2 = or add it to the Gemfile: | ||||
| rubygems.dependencies.runtime = Runtime Dependencies | ||||
|  | ||||
| @ -512,7 +512,77 @@ func CommonRoutes() *web.Route { | ||||
| 			r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) | ||||
| 			r.Get("/simple/{id}", pypi.PackageMetadata) | ||||
| 		}, reqPackageAccess(perm.AccessModeRead)) | ||||
| 		r.Group("/rpm", RpmRoutes(r), reqPackageAccess(perm.AccessModeRead)) | ||||
| 		r.Group("/rpm", func() { | ||||
| 			r.Group("/repository.key", func() { | ||||
| 				r.Head("", rpm.GetRepositoryKey) | ||||
| 				r.Get("", rpm.GetRepositoryKey) | ||||
| 			}) | ||||
| 
 | ||||
| 			var ( | ||||
| 				repoPattern     = regexp.MustCompile(`\A(.*?)\.repo\z`) | ||||
| 				uploadPattern   = regexp.MustCompile(`\A(.*?)/upload\z`) | ||||
| 				filePattern     = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`) | ||||
| 				repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`) | ||||
| 			) | ||||
| 
 | ||||
| 			r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { | ||||
| 				path := ctx.Params("*") | ||||
| 				isHead := ctx.Req.Method == "HEAD" | ||||
| 				isGetHead := ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET" | ||||
| 				isPut := ctx.Req.Method == "PUT" | ||||
| 				isDelete := ctx.Req.Method == "DELETE" | ||||
| 
 | ||||
| 				m := repoPattern.FindStringSubmatch(path) | ||||
| 				if len(m) == 2 && isGetHead { | ||||
| 					ctx.SetParams("group", strings.Trim(m[1], "/")) | ||||
| 					rpm.GetRepositoryConfig(ctx) | ||||
| 					return | ||||
| 				} | ||||
| 
 | ||||
| 				m = repoFilePattern.FindStringSubmatch(path) | ||||
| 				if len(m) == 3 && isGetHead { | ||||
| 					ctx.SetParams("group", strings.Trim(m[1], "/")) | ||||
| 					ctx.SetParams("filename", m[2]) | ||||
| 					if isHead { | ||||
| 						rpm.CheckRepositoryFileExistence(ctx) | ||||
| 					} else { | ||||
| 						rpm.GetRepositoryFile(ctx) | ||||
| 					} | ||||
| 					return | ||||
| 				} | ||||
| 
 | ||||
| 				m = uploadPattern.FindStringSubmatch(path) | ||||
| 				if len(m) == 2 && isPut { | ||||
| 					reqPackageAccess(perm.AccessModeWrite)(ctx) | ||||
| 					if ctx.Written() { | ||||
| 						return | ||||
| 					} | ||||
| 					ctx.SetParams("group", strings.Trim(m[1], "/")) | ||||
| 					rpm.UploadPackageFile(ctx) | ||||
| 					return | ||||
| 				} | ||||
| 
 | ||||
| 				m = filePattern.FindStringSubmatch(path) | ||||
| 				if len(m) == 6 && (isGetHead || isDelete) { | ||||
| 					ctx.SetParams("group", strings.Trim(m[1], "/")) | ||||
| 					ctx.SetParams("name", m[2]) | ||||
| 					ctx.SetParams("version", m[3]) | ||||
| 					ctx.SetParams("architecture", m[4]) | ||||
| 					if isGetHead { | ||||
| 						rpm.DownloadPackageFile(ctx) | ||||
| 					} else { | ||||
| 						reqPackageAccess(perm.AccessModeWrite)(ctx) | ||||
| 						if ctx.Written() { | ||||
| 							return | ||||
| 						} | ||||
| 						rpm.DeletePackageFile(ctx) | ||||
| 					} | ||||
| 					return | ||||
| 				} | ||||
| 
 | ||||
| 				ctx.Status(http.StatusNotFound) | ||||
| 			}) | ||||
| 		}, reqPackageAccess(perm.AccessModeRead)) | ||||
| 		r.Group("/rubygems", func() { | ||||
| 			r.Get("/specs.4.8.gz", rubygems.EnumeratePackages) | ||||
| 			r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) | ||||
| @ -577,82 +647,6 @@ func CommonRoutes() *web.Route { | ||||
| 	return r | ||||
| } | ||||
| 
 | ||||
| // Support for uploading rpm packages with arbitrary depth paths | ||||
| func RpmRoutes(r *web.Route) func() { | ||||
| 	var ( | ||||
| 		groupRepoInfo = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)\.repo\z`) | ||||
| 		groupUpload   = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)/upload\z`) | ||||
| 		groupRpm      = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`) | ||||
| 		groupMetadata = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)/repodata/([^/]+)\z`) | ||||
| 	) | ||||
| 
 | ||||
| 	return func() { | ||||
| 		r.Methods("HEAD,GET,POST,PUT,PATCH,DELETE", "*", func(ctx *context.Context) { | ||||
| 			path := ctx.Params("*") | ||||
| 			isHead := ctx.Req.Method == "HEAD" | ||||
| 			isGetHead := ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET" | ||||
| 			isPut := ctx.Req.Method == "PUT" | ||||
| 			isDelete := ctx.Req.Method == "DELETE" | ||||
| 
 | ||||
| 			if path == "/repository.key" && isGetHead { | ||||
| 				rpm.GetRepositoryKey(ctx) | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			// get repo | ||||
| 			m := groupRepoInfo.FindStringSubmatch(path) | ||||
| 			if len(m) == 2 && isGetHead { | ||||
| 				ctx.SetParams("group", strings.Trim(m[1], "/")) | ||||
| 				rpm.GetRepositoryConfig(ctx) | ||||
| 				return | ||||
| 			} | ||||
| 			// get meta | ||||
| 			m = groupMetadata.FindStringSubmatch(path) | ||||
| 			if len(m) == 3 && isGetHead { | ||||
| 				ctx.SetParams("group", strings.Trim(m[1], "/")) | ||||
| 				ctx.SetParams("filename", m[2]) | ||||
| 				if isHead { | ||||
| 					rpm.CheckRepositoryFileExistence(ctx) | ||||
| 				} else { | ||||
| 					rpm.GetRepositoryFile(ctx) | ||||
| 				} | ||||
| 				return | ||||
| 			} | ||||
| 			// upload | ||||
| 			m = groupUpload.FindStringSubmatch(path) | ||||
| 			if len(m) == 2 && isPut { | ||||
| 				reqPackageAccess(perm.AccessModeWrite)(ctx) | ||||
| 				if ctx.Written() { | ||||
| 					return | ||||
| 				} | ||||
| 				ctx.SetParams("group", strings.Trim(m[1], "/")) | ||||
| 				rpm.UploadPackageFile(ctx) | ||||
| 				return | ||||
| 			} | ||||
| 			// rpm down/delete | ||||
| 			m = groupRpm.FindStringSubmatch(path) | ||||
| 			if len(m) == 6 { | ||||
| 				ctx.SetParams("group", strings.Trim(m[1], "/")) | ||||
| 				ctx.SetParams("name", m[2]) | ||||
| 				ctx.SetParams("version", m[3]) | ||||
| 				ctx.SetParams("architecture", m[4]) | ||||
| 				if isGetHead { | ||||
| 					rpm.DownloadPackageFile(ctx) | ||||
| 					return | ||||
| 				} else if isDelete { | ||||
| 					reqPackageAccess(perm.AccessModeWrite)(ctx) | ||||
| 					if ctx.Written() { | ||||
| 						return | ||||
| 					} | ||||
| 					rpm.DeletePackageFile(ctx) | ||||
| 				} | ||||
| 			} | ||||
| 			// default | ||||
| 			ctx.Status(http.StatusNotFound) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ContainerRoutes provides endpoints that implement the OCI API to serve containers | ||||
| // These have to be mounted on `/v2/...` to comply with the OCI spec: | ||||
| // https://github.com/opencontainers/distribution-spec/blob/main/spec.md | ||||
|  | ||||
| @ -34,13 +34,17 @@ func apiError(ctx *context.Context, status int, obj any) { | ||||
| // https://dnf.readthedocs.io/en/latest/conf_ref.html | ||||
| func GetRepositoryConfig(ctx *context.Context) { | ||||
| 	group := ctx.Params("group") | ||||
| 
 | ||||
| 	var groupParts []string | ||||
| 	if group != "" { | ||||
| 		group = fmt.Sprintf("/%s", group) | ||||
| 		groupParts = strings.Split(group, "/") | ||||
| 	} | ||||
| 
 | ||||
| 	url := fmt.Sprintf("%sapi/packages/%s/rpm", setting.AppURL, ctx.Package.Owner.Name) | ||||
| 	ctx.PlainText(http.StatusOK, `[gitea-`+ctx.Package.Owner.LowerName+strings.ReplaceAll(group, "/", "-")+`] | ||||
| name=`+ctx.Package.Owner.Name+` - `+setting.AppName+strings.ReplaceAll(group, "/", " - ")+` | ||||
| baseurl=`+url+group+`/ | ||||
| 
 | ||||
| 	ctx.PlainText(http.StatusOK, `[gitea-`+strings.Join(append([]string{ctx.Package.Owner.LowerName}, groupParts...), "-")+`] | ||||
| name=`+strings.Join(append([]string{ctx.Package.Owner.Name, setting.AppName}, groupParts...), " - ")+` | ||||
| baseurl=`+strings.Join(append([]string{url}, groupParts...), "/")+` | ||||
| enabled=1 | ||||
| gpgcheck=1 | ||||
| gpgkey=`+url+`/repository.key`) | ||||
| @ -157,7 +161,7 @@ func UploadPackageFile(ctx *context.Context) { | ||||
| 				Owner:       ctx.Package.Owner, | ||||
| 				PackageType: packages_model.TypeRpm, | ||||
| 				Name:        pck.Name, | ||||
| 				Version:     strings.Trim(fmt.Sprintf("%s/%s", group, pck.Version), "/"), | ||||
| 				Version:     pck.Version, | ||||
| 			}, | ||||
| 			Creator:  ctx.Doer, | ||||
| 			Metadata: pck.VersionMetadata, | ||||
| @ -171,6 +175,8 @@ func UploadPackageFile(ctx *context.Context) { | ||||
| 			Data:    buf, | ||||
| 			IsLead:  true, | ||||
| 			Properties: map[string]string{ | ||||
| 				rpm_module.PropertyGroup:        group, | ||||
| 				rpm_module.PropertyArchitecture: pck.FileMetadata.Architecture, | ||||
| 				rpm_module.PropertyMetadata:     string(fileMetadataRaw), | ||||
| 			}, | ||||
| 		}, | ||||
| @ -187,7 +193,7 @@ func UploadPackageFile(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := rpm_service.BuildRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil { | ||||
| 	if err := rpm_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil { | ||||
| 		apiError(ctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
| @ -196,20 +202,20 @@ func UploadPackageFile(ctx *context.Context) { | ||||
| } | ||||
| 
 | ||||
| func DownloadPackageFile(ctx *context.Context) { | ||||
| 	group := ctx.Params("group") | ||||
| 	name := ctx.Params("name") | ||||
| 	version := ctx.Params("version") | ||||
| 
 | ||||
| 	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( | ||||
| 		ctx, | ||||
| 		&packages_service.PackageInfo{ | ||||
| 			Owner:       ctx.Package.Owner, | ||||
| 			PackageType: packages_model.TypeRpm, | ||||
| 			Name:        name, | ||||
| 			Version:     strings.Trim(fmt.Sprintf("%s/%s", group, version), "/"), | ||||
| 			Version:     version, | ||||
| 		}, | ||||
| 		&packages_service.PackageFileInfo{ | ||||
| 			Filename:     fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.Params("architecture")), | ||||
| 			CompositeKey: group, | ||||
| 			CompositeKey: ctx.Params("group"), | ||||
| 		}, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| @ -229,6 +235,7 @@ func DeletePackageFile(webctx *context.Context) { | ||||
| 	name := webctx.Params("name") | ||||
| 	version := webctx.Params("version") | ||||
| 	architecture := webctx.Params("architecture") | ||||
| 
 | ||||
| 	var pd *packages_model.PackageDescriptor | ||||
| 
 | ||||
| 	err := db.WithTx(webctx, func(ctx stdctx.Context) error { | ||||
| @ -236,7 +243,7 @@ func DeletePackageFile(webctx *context.Context) { | ||||
| 			webctx.Package.Owner.ID, | ||||
| 			packages_model.TypeRpm, | ||||
| 			name, | ||||
| 			strings.Trim(fmt.Sprintf("%s/%s", group, version), "/"), | ||||
| 			version, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| @ -286,7 +293,7 @@ func DeletePackageFile(webctx *context.Context) { | ||||
| 		notify_service.PackageDelete(webctx, webctx.Doer, pd) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := rpm_service.BuildRepositoryFiles(webctx, webctx.Package.Owner.ID, group); err != nil { | ||||
| 	if err := rpm_service.BuildSpecificRepositoryFiles(webctx, webctx.Package.Owner.ID, group); err != nil { | ||||
| 		apiError(webctx, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @ -108,6 +108,6 @@ func CleanupExpiredData(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Flash.Success(ctx.Tr("packages.cleanup.success")) | ||||
| 	ctx.Flash.Success(ctx.Tr("admin.packages.cleanup.success")) | ||||
| 	ctx.Redirect(setting.AppSubURL + "/admin/packages") | ||||
| } | ||||
|  | ||||
| @ -19,6 +19,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	alpine_module "code.gitea.io/gitea/modules/packages/alpine" | ||||
| 	debian_module "code.gitea.io/gitea/modules/packages/debian" | ||||
| 	rpm_module "code.gitea.io/gitea/modules/packages/rpm" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| @ -195,9 +196,9 @@ func ViewPackageVersion(ctx *context.Context) { | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		ctx.Data["Branches"] = branches.Values() | ||||
| 		ctx.Data["Repositories"] = repositories.Values() | ||||
| 		ctx.Data["Architectures"] = architectures.Values() | ||||
| 		ctx.Data["Branches"] = util.Sorted(branches.Values()) | ||||
| 		ctx.Data["Repositories"] = util.Sorted(repositories.Values()) | ||||
| 		ctx.Data["Architectures"] = util.Sorted(architectures.Values()) | ||||
| 	case packages_model.TypeDebian: | ||||
| 		distributions := make(container.Set[string]) | ||||
| 		components := make(container.Set[string]) | ||||
| @ -216,9 +217,26 @@ func ViewPackageVersion(ctx *context.Context) { | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		ctx.Data["Distributions"] = distributions.Values() | ||||
| 		ctx.Data["Components"] = components.Values() | ||||
| 		ctx.Data["Architectures"] = architectures.Values() | ||||
| 		ctx.Data["Distributions"] = util.Sorted(distributions.Values()) | ||||
| 		ctx.Data["Components"] = util.Sorted(components.Values()) | ||||
| 		ctx.Data["Architectures"] = util.Sorted(architectures.Values()) | ||||
| 	case packages_model.TypeRpm: | ||||
| 		groups := make(container.Set[string]) | ||||
| 		architectures := make(container.Set[string]) | ||||
| 
 | ||||
| 		for _, f := range pd.Files { | ||||
| 			for _, pp := range f.Properties { | ||||
| 				switch pp.Name { | ||||
| 				case rpm_module.PropertyGroup: | ||||
| 					groups.Add(pp.Value) | ||||
| 				case rpm_module.PropertyArchitecture: | ||||
| 					architectures.Add(pp.Value) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		ctx.Data["Groups"] = util.Sorted(groups.Values()) | ||||
| 		ctx.Data["Architectures"] = util.Sorted(architectures.Values()) | ||||
| 	} | ||||
| 
 | ||||
| 	var ( | ||||
|  | ||||
| @ -19,6 +19,7 @@ import ( | ||||
| 	cargo_service "code.gitea.io/gitea/services/packages/cargo" | ||||
| 	container_service "code.gitea.io/gitea/services/packages/container" | ||||
| 	debian_service "code.gitea.io/gitea/services/packages/debian" | ||||
| 	rpm_service "code.gitea.io/gitea/services/packages/rpm" | ||||
| ) | ||||
| 
 | ||||
| // Task method to execute cleanup rules and cleanup expired package data | ||||
| @ -127,6 +128,10 @@ func ExecuteCleanupRules(outerCtx context.Context) error { | ||||
| 				if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { | ||||
| 					return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err) | ||||
| 				} | ||||
| 			} else if pcr.Type == packages_model.TypeRpm { | ||||
| 				if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { | ||||
| 					return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return nil | ||||
|  | ||||
| @ -18,6 +18,7 @@ import ( | ||||
| 	"time" | ||||
| 
 | ||||
| 	packages_model "code.gitea.io/gitea/models/packages" | ||||
| 	rpm_model "code.gitea.io/gitea/models/packages/rpm" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	packages_module "code.gitea.io/gitea/modules/packages" | ||||
| @ -96,6 +97,39 @@ func generateKeypair() (string, string, error) { | ||||
| 	return priv.String(), pub.String(), nil | ||||
| } | ||||
| 
 | ||||
| // BuildAllRepositoryFiles (re)builds all repository files for every available group | ||||
| func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error { | ||||
| 	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// 1. Delete all existing repository files | ||||
| 	pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	for _, pf := range pfs { | ||||
| 		if err := packages_service.DeletePackageFile(ctx, pf); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// 2. (Re)Build repository files for existing packages | ||||
| 	groups, err := rpm_model.GetGroups(ctx, ownerID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, group := range groups { | ||||
| 		if err := BuildSpecificRepositoryFiles(ctx, ownerID, group); err != nil { | ||||
| 			return fmt.Errorf("failed to build repository files [%s]: %w", group, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type repoChecksum struct { | ||||
| 	Value string `xml:",chardata"` | ||||
| 	Type  string `xml:"type,attr"` | ||||
| @ -126,7 +160,7 @@ type packageData struct { | ||||
| type packageCache = map[*packages_model.PackageFile]*packageData | ||||
| 
 | ||||
| // BuildSpecificRepositoryFiles builds metadata files for the repository | ||||
| func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey string) error { | ||||
| func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, group string) error { | ||||
| 	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @ -136,7 +170,7 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey strin | ||||
| 		OwnerID:      ownerID, | ||||
| 		PackageType:  packages_model.TypeRpm, | ||||
| 		Query:        "%.rpm", | ||||
| 		CompositeKey: compositeKey, | ||||
| 		CompositeKey: group, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @ -195,15 +229,15 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey strin | ||||
| 		cache[pf] = pd | ||||
| 	} | ||||
| 
 | ||||
| 	primary, err := buildPrimary(ctx, pv, pfs, cache, compositeKey) | ||||
| 	primary, err := buildPrimary(ctx, pv, pfs, cache, group) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	filelists, err := buildFilelists(ctx, pv, pfs, cache, compositeKey) | ||||
| 	filelists, err := buildFilelists(ctx, pv, pfs, cache, group) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	other, err := buildOther(ctx, pv, pfs, cache, compositeKey) | ||||
| 	other, err := buildOther(ctx, pv, pfs, cache, group) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @ -217,12 +251,12 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey strin | ||||
| 			filelists, | ||||
| 			other, | ||||
| 		}, | ||||
| 		compositeKey, | ||||
| 		group, | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml | ||||
| func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData, compositeKey string) error { | ||||
| func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData, group string) error { | ||||
| 	type Repomd struct { | ||||
| 		XMLName  xml.Name    `xml:"repomd"` | ||||
| 		Xmlns    string      `xml:"xmlns,attr"` | ||||
| @ -278,7 +312,7 @@ func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID | ||||
| 			&packages_service.PackageFileCreationInfo{ | ||||
| 				PackageFileInfo: packages_service.PackageFileInfo{ | ||||
| 					Filename:     file.Name, | ||||
| 					CompositeKey: compositeKey, | ||||
| 					CompositeKey: group, | ||||
| 				}, | ||||
| 				Creator:           user_model.NewGhostUser(), | ||||
| 				Data:              file.Data, | ||||
| @ -295,7 +329,7 @@ func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID | ||||
| } | ||||
| 
 | ||||
| // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#primary-xml | ||||
| func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, compositeKey string) (*repoData, error) { | ||||
| func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { | ||||
| 	type Version struct { | ||||
| 		Epoch   string `xml:"epoch,attr"` | ||||
| 		Version string `xml:"ver,attr"` | ||||
| @ -434,11 +468,11 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] | ||||
| 		XmlnsRpm:     "http://linux.duke.edu/metadata/rpm", | ||||
| 		PackageCount: len(pfs), | ||||
| 		Packages:     packages, | ||||
| 	}, compositeKey) | ||||
| 	}, group) | ||||
| } | ||||
| 
 | ||||
| // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml | ||||
| func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, compositeKey string) (*repoData, error) { //nolint:dupl | ||||
| func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl | ||||
| 	type Version struct { | ||||
| 		Epoch   string `xml:"epoch,attr"` | ||||
| 		Version string `xml:"ver,attr"` | ||||
| @ -481,12 +515,11 @@ func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs | ||||
| 		Xmlns:        "http://linux.duke.edu/metadata/other", | ||||
| 		PackageCount: len(pfs), | ||||
| 		Packages:     packages, | ||||
| 	}, | ||||
| 		compositeKey) | ||||
| 	}, group) | ||||
| } | ||||
| 
 | ||||
| // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml | ||||
| func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, compositeKey string) (*repoData, error) { //nolint:dupl | ||||
| func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl | ||||
| 	type Version struct { | ||||
| 		Epoch   string `xml:"epoch,attr"` | ||||
| 		Version string `xml:"ver,attr"` | ||||
| @ -529,7 +562,7 @@ func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*p | ||||
| 		Xmlns:        "http://linux.duke.edu/metadata/other", | ||||
| 		PackageCount: len(pfs), | ||||
| 		Packages:     packages, | ||||
| 	}, compositeKey) | ||||
| 	}, group) | ||||
| } | ||||
| 
 | ||||
| // writtenCounter counts all written bytes | ||||
| @ -549,8 +582,10 @@ func (wc *writtenCounter) Written() int64 { | ||||
| 	return wc.written | ||||
| } | ||||
| 
 | ||||
| func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any, compositeKey string) (*repoData, error) { | ||||
| func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any, group string) (*repoData, error) { | ||||
| 	content, _ := packages_module.NewHashedBuffer() | ||||
| 	defer content.Close() | ||||
| 
 | ||||
| 	gzw := gzip.NewWriter(content) | ||||
| 	wc := &writtenCounter{} | ||||
| 	h := sha256.New() | ||||
| @ -574,7 +609,7 @@ func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, | ||||
| 		&packages_service.PackageFileCreationInfo{ | ||||
| 			PackageFileInfo: packages_service.PackageFileInfo{ | ||||
| 				Filename:     filename, | ||||
| 				CompositeKey: compositeKey, | ||||
| 				CompositeKey: group, | ||||
| 			}, | ||||
| 			Creator:           user_model.NewGhostUser(), | ||||
| 			Data:              content, | ||||
|  | ||||
| @ -4,15 +4,21 @@ | ||||
| 		<div class="ui form"> | ||||
| 			<div class="field"> | ||||
| 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rpm.registry"}}</label> | ||||
| 				<div class="markup"><pre class="code-block"><code># {{ctx.Locale.Tr "packages.rpm.distros.redhat"}} | ||||
| {{$group_name:= StringUtils.ReplaceAllStringRegex .PackageDescriptor.Version.Version "(/[^/]+|[^/]*)\\z" "" -}} | ||||
| {{- if $group_name -}} | ||||
| {{- $group_name = (print "/" $group_name) -}} | ||||
| {{- end -}} | ||||
| dnf config-manager --add-repo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group_name}}.repo"></gitea-origin-url> | ||||
| 				<div class="markup"><pre class="code-block"><code>{{- if gt (len .Groups) 1 -}} | ||||
| # {{ctx.Locale.Tr "packages.rpm.repository.multiple_groups"}} | ||||
| 
 | ||||
| {{end -}} | ||||
| # {{ctx.Locale.Tr "packages.rpm.distros.redhat"}} | ||||
| {{- range $group := .Groups}} | ||||
| 	{{- if $group}}{{$group = print "/" $group}}{{end}} | ||||
| dnf config-manager --add-repo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url> | ||||
| {{- end}} | ||||
| 
 | ||||
| # {{ctx.Locale.Tr "packages.rpm.distros.suse"}} | ||||
| zypper addrepo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group_name}}.repo"></gitea-origin-url></code></pre></div> | ||||
| {{- range $group := .Groups}} | ||||
| 	{{- if $group}}{{$group = print "/" $group}}{{end}} | ||||
| zypper addrepo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url> | ||||
| {{- end}}</code></pre></div> | ||||
| 			</div> | ||||
| 			<div class="field"> | ||||
| 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rpm.install"}}</label> | ||||
| @ -30,6 +36,18 @@ zypper install {{$.PackageDescriptor.Package.Name}}</code></pre> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.rpm.repository"}}</h4> | ||||
| 	<div class="ui attached segment"> | ||||
| 		<table class="ui single line very basic table"> | ||||
| 			<tbody> | ||||
| 				<tr> | ||||
| 					<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.rpm.repository.architectures"}}</h5></td> | ||||
| 					<td>{{StringUtils.Join .Architectures ", "}}</td> | ||||
| 				</tr> | ||||
| 			</tbody> | ||||
| 		</table> | ||||
| 	</div> | ||||
| 
 | ||||
| 	{{if or .PackageDescriptor.Metadata.Summary .PackageDescriptor.Metadata.Description}} | ||||
| 		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4> | ||||
| 		{{if .PackageDescriptor.Metadata.Summary}}<div class="ui attached segment">{{.PackageDescriptor.Metadata.Summary}}</div>{{end}} | ||||
|  | ||||
| @ -12,6 +12,7 @@ import ( | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| @ -20,6 +21,7 @@ import ( | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	rpm_module "code.gitea.io/gitea/modules/packages/rpm" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/tests" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| @ -73,18 +75,32 @@ Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5 | ||||
| 
 | ||||
| 	rootURL := fmt.Sprintf("/api/packages/%s/rpm", user.Name) | ||||
| 
 | ||||
| 	for _, group := range []string{"", "el9", "el9/stable"} { | ||||
| 		t.Run(fmt.Sprintf("[Group:%s]", group), func(t *testing.T) { | ||||
| 			var groupParts []string | ||||
| 			if group != "" { | ||||
| 				groupParts = strings.Split(group, "/") | ||||
| 			} | ||||
| 			groupURL := strings.Join(append([]string{rootURL}, groupParts...), "/") | ||||
| 
 | ||||
| 			t.Run("RepositoryConfig", func(t *testing.T) { | ||||
| 				defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 		req := NewRequest(t, "GET", rootURL+"/el9/stable.repo") | ||||
| 				req := NewRequest(t, "GET", groupURL+".repo") | ||||
| 				resp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 		expected := fmt.Sprintf(`[gitea-%s-el9-stable] | ||||
| name=%s - %s - el9 - stable | ||||
| baseurl=%sapi/packages/%s/rpm/el9/stable/ | ||||
| 				expected := fmt.Sprintf(`[gitea-%s] | ||||
| name=%s | ||||
| baseurl=%s | ||||
| enabled=1 | ||||
| gpgcheck=1 | ||||
| gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppName, setting.AppURL, user.Name, setting.AppURL, user.Name) | ||||
| 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()) | ||||
| 			}) | ||||
| @ -100,7 +116,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppN | ||||
| 			}) | ||||
| 
 | ||||
| 			t.Run("Upload", func(t *testing.T) { | ||||
| 		url := rootURL + "/el9/stable/upload" | ||||
| 				url := groupURL + "/upload" | ||||
| 
 | ||||
| 				req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) | ||||
| 				MakeRequest(t, req, http.StatusUnauthorized) | ||||
| @ -118,7 +134,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppN | ||||
| 				assert.Nil(t, pd.SemVer) | ||||
| 				assert.IsType(t, &rpm_module.VersionMetadata{}, pd.Metadata) | ||||
| 				assert.Equal(t, packageName, pd.Package.Name) | ||||
| 		assert.Equal(t, fmt.Sprintf("el9/stable/%s", packageVersion), pd.Version.Version) | ||||
| 				assert.Equal(t, packageVersion, pd.Version.Version) | ||||
| 
 | ||||
| 				pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) | ||||
| 				assert.NoError(t, err) | ||||
| @ -138,7 +154,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppN | ||||
| 			t.Run("Download", func(t *testing.T) { | ||||
| 				defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("%s/el9/stable/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)) | ||||
| 				req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture)) | ||||
| 				resp := MakeRequest(t, req, http.StatusOK) | ||||
| 
 | ||||
| 				assert.Equal(t, content, resp.Body.Bytes()) | ||||
| @ -147,7 +163,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppN | ||||
| 			t.Run("Repository", func(t *testing.T) { | ||||
| 				defer tests.PrintCurrentTest(t)() | ||||
| 
 | ||||
| 		url := rootURL + "/el9/stable/repodata" | ||||
| 				url := groupURL + "/repodata" | ||||
| 
 | ||||
| 				req := NewRequest(t, "HEAD", url+"/dummy.xml") | ||||
| 				MakeRequest(t, req, http.StatusNotFound) | ||||
| @ -401,18 +417,20 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppN | ||||
| 			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)) | ||||
| 				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/el9/stable/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)). | ||||
| 				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/el9/stable/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)). | ||||
| 				req = NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture)). | ||||
| 					AddBasicAuth(user.Name) | ||||
| 				MakeRequest(t, req, http.StatusNotFound) | ||||
| 			}) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user