From ba4d0b8ffbd78473273800f586ae8bde55cda6c5 Mon Sep 17 00:00:00 2001 From: Exploding Dragon Date: Fri, 12 Jan 2024 11:16:05 +0800 Subject: [PATCH] Support for grouping RPMs using paths (#26984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current rpm repository places all packages in the same repository, and different systems (el7,f34) may hit packages that do not belong to this distribution ( #25304 ) , which now supports grouping of rpm. ![图片](https://github.com/go-gitea/gitea/assets/33776693/d1e1d99f-7799-4b2b-a19b-cb2a5c692914) Fixes #25304 . Fixes #27056 . Refactor: [#25866](https://github.com/go-gitea/gitea/pull/25866) --- docs/content/usage/packages/rpm.en-us.md | 33 ++++---- docs/content/usage/packages/rpm.zh-cn.md | 25 +++--- modules/packages/rpm/metadata.go | 3 +- modules/templates/util_string.go | 5 ++ routers/api/packages/api.go | 90 ++++++++++++++++++---- routers/api/packages/rpm/rpm.go | 45 +++++++---- services/packages/rpm/repository.go | 47 +++++------ templates/package/content/rpm.tmpl | 16 ++-- tests/integration/api_packages_rpm_test.go | 29 ++++--- 9 files changed, 192 insertions(+), 101 deletions(-) diff --git a/docs/content/usage/packages/rpm.en-us.md b/docs/content/usage/packages/rpm.en-us.md index 5a4a31ee39d..586e48d47fe 100644 --- a/docs/content/usage/packages/rpm.en-us.md +++ b/docs/content/usage/packages/rpm.en-us.md @@ -27,17 +27,18 @@ The following examples use `dnf`. To register the RPM registry add the url to the list of known apt sources: ```shell -dnf config-manager --add-repo https://gitea.example.com/api/packages/{owner}/rpm.repo +dnf config-manager --add-repo https://gitea.example.com/api/packages/{owner}/rpm/{group}.repo ``` -| Placeholder | Description | -| ----------- | ----------- | -| `owner` | The owner of the package. | +| Placeholder | Description | +| ----------- |----------------------------------------------------| +| `owner` | The owner of the package. | +| `group` | Everything, e.g. `el7`, `rocky/el9` , `test/fc38`.| 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): ```shell -dnf config-manager --add-repo https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/rpm.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. @@ -47,19 +48,20 @@ You have to add the credentials to the urls in the `rpm.repo` file in `/etc/yum. To publish a RPM package (`*.rpm`), perform a HTTP PUT operation with the package content in the request body. ``` -PUT https://gitea.example.com/api/packages/{owner}/rpm/upload +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`.| Example request using HTTP Basic authentication: ```shell curl --user your_username:your_password_or_token \ --upload-file path/to/file.rpm \ - https://gitea.example.com/api/packages/testuser/rpm/upload + https://gitea.example.com/api/packages/testuser/rpm/centos/el7/upload ``` If you are using 2FA or OAuth use a [personal access token](development/api-usage.md#authentication) instead of the password. @@ -78,21 +80,22 @@ The server responds with the following HTTP Status codes. To delete an RPM package perform a HTTP DELETE operation. This will delete the package version too if there is no file left. ``` -DELETE https://gitea.example.com/api/packages/{owner}/rpm/{package_name}/{package_version}/{architecture} +DELETE https://gitea.example.com/api/packages/{owner}/rpm/{group}/package/{package_name}/{package_version}/{architecture} ``` -| Parameter | Description | -| ----------------- | ----------- | -| `owner` | The owner of the package. | -| `package_name` | The package name. | -| `package_version` | The package version. | -| `architecture` | The package architecture. | +| Parameter | Description | +|-------------------|----------------------------| +| `owner` | The owner of the package. | +| `group` | The package group . | +| `package_name` | The package name. | +| `package_version` | The package version. | +| `architecture` | The package architecture. | Example request using HTTP Basic authentication: ```shell curl --user your_username:your_token_or_password -X DELETE \ - https://gitea.example.com/api/packages/testuser/rpm/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 ``` The server responds with the following HTTP Status codes. diff --git a/docs/content/usage/packages/rpm.zh-cn.md b/docs/content/usage/packages/rpm.zh-cn.md index 3cc7dca8ff2..cbe74bfee23 100644 --- a/docs/content/usage/packages/rpm.zh-cn.md +++ b/docs/content/usage/packages/rpm.zh-cn.md @@ -27,17 +27,18 @@ menu: 要注册RPM注册表,请将 URL 添加到已知 `apt` 源列表中: ```shell -dnf config-manager --add-repo https://gitea.example.com/api/packages/{owner}/rpm.repo +dnf config-manager --add-repo https://gitea.example.com/api/packages/{owner}/rpm/{group}.repo ``` -| 占位符 | 描述 | -| ------- | -------------- | -| `owner` | 软件包的所有者 | +| 占位符 | 描述 | +| ------- |--------------------------------------| +| `owner` | 软件包的所有者 | +| `group` | 任何名称,例如 `centos/7`、`el-7`、`fc38` | 如果注册表是私有的,请在URL中提供凭据。您可以使用密码或[个人访问令牌](development/api-usage.md#通过-api-认证): ```shell -dnf config-manager --add-repo https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/rpm.repo +dnf config-manager --add-repo https://{username}:{your_password_or_token}@gitea.example.com/api/packages/{owner}/rpm/{group}.repo ``` 您还必须将凭据添加到 `/etc/yum.repos.d` 中的 `rpm.repo` 文件中的URL中。 @@ -47,19 +48,20 @@ dnf config-manager --add-repo https://{username}:{your_password_or_token}@gitea. 要发布RPM软件包(`*.rpm`),请执行带有软件包内容的 HTTP `PUT` 操作。 ``` -PUT https://gitea.example.com/api/packages/{owner}/rpm/upload +PUT https://gitea.example.com/api/packages/{owner}/rpm/{group}/upload ``` | 参数 | 描述 | -| ------- | -------------- | -| `owner` | 软件包的所有者 | +| ------- |--------------| +| `owner` | 软件包的所有者 | +| `group` | 软件包自定义分组名称 | 使用HTTP基本身份验证的示例请求: ```shell curl --user your_username:your_password_or_token \ --upload-file path/to/file.rpm \ - https://gitea.example.com/api/packages/testuser/rpm/upload + https://gitea.example.com/api/packages/testuser/rpm/centos/el7/version/upload ``` 如果您使用 2FA 或 OAuth,请使用[个人访问令牌](development/api-usage.md#通过-api-认证)替代密码。您无法将具有相同名称的文件两次发布到软件包中。您必须先删除现有的软件包版本。 @@ -77,12 +79,13 @@ curl --user your_username:your_password_or_token \ 要删除 RPM 软件包,请执行 HTTP `DELETE` 操作。如果没有文件剩余,这也将删除软件包版本。 ``` -DELETE https://gitea.example.com/api/packages/{owner}/rpm/{package_name}/{package_version}/{architecture} +DELETE https://gitea.example.com/api/packages/{owner}/rpm/{group}/package/{package_name}/{package_version}/{architecture} ``` | 参数 | 描述 | | ----------------- | -------------- | | `owner` | 软件包的所有者 | +| `group` | 软件包自定义分组 | | `package_name` | 软件包名称 | | `package_version` | 软件包版本 | | `architecture` | 软件包架构 | @@ -91,7 +94,7 @@ DELETE https://gitea.example.com/api/packages/{owner}/rpm/{package_name}/{packag ```shell curl --user your_username:your_token_or_password -X DELETE \ - https://gitea.example.com/api/packages/testuser/rpm/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 ``` 服务器将以以下HTTP状态码响应: diff --git a/modules/packages/rpm/metadata.go b/modules/packages/rpm/metadata.go index f019a8dde1a..1ba4c73e8d5 100644 --- a/modules/packages/rpm/metadata.go +++ b/modules/packages/rpm/metadata.go @@ -15,8 +15,7 @@ import ( ) const ( - PropertyMetadata = "rpm.metadata" - + PropertyMetadata = "rpm.metadata" SettingKeyPrivate = "rpm.key.private" SettingKeyPublic = "rpm.key.public" diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go index 18a0d5cacc9..613940ccdc4 100644 --- a/modules/templates/util_string.go +++ b/modules/templates/util_string.go @@ -4,6 +4,7 @@ package templates import ( + "regexp" "strings" "code.gitea.io/gitea/modules/base" @@ -25,6 +26,10 @@ func (su *StringUtils) Contains(s, substr string) bool { return strings.Contains(s, substr) } +func (su *StringUtils) ReplaceAllStringRegex(s, regex, new string) string { + return regexp.MustCompile(regex).ReplaceAllString(s, new) +} + func (su *StringUtils) Split(s, sep string) []string { return strings.Split(s, sep) } diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 76116d0751d..90263871297 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -512,19 +512,7 @@ func CommonRoutes() *web.Route { r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) r.Get("/simple/{id}", pypi.PackageMetadata) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/rpm", func() { - r.Get(".repo", rpm.GetRepositoryConfig) - r.Get("/repository.key", rpm.GetRepositoryKey) - r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile) - r.Group("/package/{name}/{version}/{architecture}", func() { - r.Get("", rpm.DownloadPackageFile) - r.Delete("", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile) - }) - r.Group("/repodata/{filename}", func() { - r.Head("", rpm.CheckRepositoryFileExistence) - r.Get("", rpm.GetRepositoryFile) - }) - }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/rpm", RpmRoutes(r), reqPackageAccess(perm.AccessModeRead)) r.Group("/rubygems", func() { r.Get("/specs.4.8.gz", rubygems.EnumeratePackages) r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) @@ -589,6 +577,82 @@ func CommonRoutes() *web.Route { return r } +// Support for uploading rpm packages with arbitrary depth paths +func RpmRoutes(r *web.Route) func() { + var ( + groupRepoInfo = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)\.repo\z`) + groupUpload = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)/upload\z`) + groupRpm = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`) + groupMetadata = regexp.MustCompile(`\A((?:/(?:[^/]+))*|)/repodata/([^/]+)\z`) + ) + + return func() { + r.Methods("HEAD,GET,POST,PUT,PATCH,DELETE", "*", func(ctx *context.Context) { + path := ctx.Params("*") + isHead := ctx.Req.Method == "HEAD" + isGetHead := ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET" + isPut := ctx.Req.Method == "PUT" + isDelete := ctx.Req.Method == "DELETE" + + if path == "/repository.key" && isGetHead { + rpm.GetRepositoryKey(ctx) + return + } + + // get repo + m := groupRepoInfo.FindStringSubmatch(path) + if len(m) == 2 && isGetHead { + ctx.SetParams("group", strings.Trim(m[1], "/")) + rpm.GetRepositoryConfig(ctx) + return + } + // get meta + m = groupMetadata.FindStringSubmatch(path) + if len(m) == 3 && isGetHead { + ctx.SetParams("group", strings.Trim(m[1], "/")) + ctx.SetParams("filename", m[2]) + if isHead { + rpm.CheckRepositoryFileExistence(ctx) + } else { + rpm.GetRepositoryFile(ctx) + } + return + } + // upload + m = groupUpload.FindStringSubmatch(path) + if len(m) == 2 && isPut { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + ctx.SetParams("group", strings.Trim(m[1], "/")) + rpm.UploadPackageFile(ctx) + return + } + // rpm down/delete + m = groupRpm.FindStringSubmatch(path) + if len(m) == 6 { + ctx.SetParams("group", strings.Trim(m[1], "/")) + ctx.SetParams("name", m[2]) + ctx.SetParams("version", m[3]) + ctx.SetParams("architecture", m[4]) + if isGetHead { + rpm.DownloadPackageFile(ctx) + return + } else if isDelete { + reqPackageAccess(perm.AccessModeWrite)(ctx) + if ctx.Written() { + return + } + rpm.DeletePackageFile(ctx) + } + } + // default + ctx.Status(http.StatusNotFound) + }) + } +} + // ContainerRoutes provides endpoints that implement the OCI API to serve containers // These have to be mounted on `/v2/...` to comply with the OCI spec: // https://github.com/opencontainers/distribution-spec/blob/main/spec.md diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index 2e161940b8c..75d19e2b436 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -33,11 +33,14 @@ 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") + if group != "" { + group = fmt.Sprintf("/%s", group) + } url := fmt.Sprintf("%sapi/packages/%s/rpm", setting.AppURL, ctx.Package.Owner.Name) - - ctx.PlainText(http.StatusOK, `[gitea-`+ctx.Package.Owner.LowerName+`] -name=`+ctx.Package.Owner.Name+` - `+setting.AppName+` -baseurl=`+url+` + 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+`/ enabled=1 gpgcheck=1 gpgkey=`+url+`/repository.key`) @@ -64,7 +67,7 @@ func CheckRepositoryFileExistence(ctx *context.Context) { return } - pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, ctx.Params("filename"), packages_model.EmptyFileKey) + pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, ctx.Params("filename"), ctx.Params("group")) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Status(http.StatusNotFound) @@ -93,7 +96,8 @@ func GetRepositoryFile(ctx *context.Context) { ctx, pv, &packages_service.PackageFileInfo{ - Filename: ctx.Params("filename"), + Filename: ctx.Params("filename"), + CompositeKey: ctx.Params("group"), }, ) if err != nil { @@ -145,7 +149,7 @@ func UploadPackageFile(ctx *context.Context) { apiError(ctx, http.StatusInternalServerError, err) return } - + group := ctx.Params("group") _, _, err = packages_service.CreatePackageOrAddFileToExisting( ctx, &packages_service.PackageCreationInfo{ @@ -153,14 +157,15 @@ func UploadPackageFile(ctx *context.Context) { Owner: ctx.Package.Owner, PackageType: packages_model.TypeRpm, Name: pck.Name, - Version: pck.Version, + Version: strings.Trim(fmt.Sprintf("%s/%s", group, pck.Version), "/"), }, Creator: ctx.Doer, Metadata: pck.VersionMetadata, }, &packages_service.PackageFileCreationInfo{ PackageFileInfo: packages_service.PackageFileInfo{ - Filename: fmt.Sprintf("%s-%s.%s.rpm", pck.Name, pck.Version, pck.FileMetadata.Architecture), + Filename: fmt.Sprintf("%s-%s.%s.rpm", pck.Name, pck.Version, pck.FileMetadata.Architecture), + CompositeKey: group, }, Creator: ctx.Doer, Data: buf, @@ -182,7 +187,7 @@ func UploadPackageFile(ctx *context.Context) { return } - if err := rpm_service.BuildRepositoryFiles(ctx, ctx.Package.Owner.ID); err != nil { + if err := rpm_service.BuildRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil { apiError(ctx, http.StatusInternalServerError, err) return } @@ -191,19 +196,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: version, + Version: strings.Trim(fmt.Sprintf("%s/%s", group, version), "/"), }, &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, }, ) if err != nil { @@ -219,14 +225,19 @@ func DownloadPackageFile(ctx *context.Context) { } func DeletePackageFile(webctx *context.Context) { + group := webctx.Params("group") 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 { - pv, err := packages_model.GetVersionByNameAndVersion(ctx, webctx.Package.Owner.ID, packages_model.TypeRpm, name, version) + pv, err := packages_model.GetVersionByNameAndVersion(ctx, + webctx.Package.Owner.ID, + packages_model.TypeRpm, + name, + strings.Trim(fmt.Sprintf("%s/%s", group, version), "/"), + ) if err != nil { return err } @@ -235,7 +246,7 @@ func DeletePackageFile(webctx *context.Context) { ctx, pv.ID, fmt.Sprintf("%s-%s.%s.rpm", name, version, architecture), - packages_model.EmptyFileKey, + group, ) if err != nil { return err @@ -275,7 +286,7 @@ func DeletePackageFile(webctx *context.Context) { notify_service.PackageDelete(webctx, webctx.Doer, pd) } - if err := rpm_service.BuildRepositoryFiles(webctx, webctx.Package.Owner.ID); err != nil { + if err := rpm_service.BuildRepositoryFiles(webctx, webctx.Package.Owner.ID, group); err != nil { apiError(webctx, http.StatusInternalServerError, err) return } diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go index 7e718d321fd..7a49efde4f6 100644 --- a/services/packages/rpm/repository.go +++ b/services/packages/rpm/repository.go @@ -125,17 +125,18 @@ type packageData struct { type packageCache = map[*packages_model.PackageFile]*packageData -// BuildRepositoryFiles builds metadata files for the repository -func BuildRepositoryFiles(ctx context.Context, ownerID int64) error { +// BuildSpecificRepositoryFiles builds metadata files for the repository +func BuildRepositoryFiles(ctx context.Context, ownerID int64, compositeKey string) error { pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) if err != nil { return err } pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ - OwnerID: ownerID, - PackageType: packages_model.TypeRpm, - Query: "%.rpm", + OwnerID: ownerID, + PackageType: packages_model.TypeRpm, + Query: "%.rpm", + CompositeKey: compositeKey, }) if err != nil { return err @@ -194,15 +195,15 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64) error { cache[pf] = pd } - primary, err := buildPrimary(ctx, pv, pfs, cache) + primary, err := buildPrimary(ctx, pv, pfs, cache, compositeKey) if err != nil { return err } - filelists, err := buildFilelists(ctx, pv, pfs, cache) + filelists, err := buildFilelists(ctx, pv, pfs, cache, compositeKey) if err != nil { return err } - other, err := buildOther(ctx, pv, pfs, cache) + other, err := buildOther(ctx, pv, pfs, cache, compositeKey) if err != nil { return err } @@ -216,11 +217,12 @@ func BuildRepositoryFiles(ctx context.Context, ownerID int64) error { filelists, other, }, + compositeKey, ) } // 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) error { +func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData, compositeKey string) error { type Repomd struct { XMLName xml.Name `xml:"repomd"` Xmlns string `xml:"xmlns,attr"` @@ -275,7 +277,8 @@ func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID pv, &packages_service.PackageFileCreationInfo{ PackageFileInfo: packages_service.PackageFileInfo{ - Filename: file.Name, + Filename: file.Name, + CompositeKey: compositeKey, }, Creator: user_model.NewGhostUser(), Data: file.Data, @@ -292,7 +295,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) (*repoData, error) { +func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, compositeKey string) (*repoData, error) { type Version struct { Epoch string `xml:"epoch,attr"` Version string `xml:"ver,attr"` @@ -372,7 +375,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] files = append(files, f) } } - + packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release) packages = append(packages, &Package{ Type: "rpm", Name: pd.Package.Name, @@ -401,7 +404,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] Archive: pd.FileMetadata.ArchiveSize, }, Location: Location{ - Href: fmt.Sprintf("package/%s/%s/%s", url.PathEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.FileMetadata.Architecture)), + Href: fmt.Sprintf("package/%s/%s/%s/%s", url.PathEscape(pd.Package.Name), url.PathEscape(packageVersion), url.PathEscape(pd.FileMetadata.Architecture), url.PathEscape(fmt.Sprintf("%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture))), }, Format: Format{ License: pd.VersionMetadata.License, @@ -431,11 +434,11 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] XmlnsRpm: "http://linux.duke.edu/metadata/rpm", PackageCount: len(pfs), Packages: packages, - }) + }, compositeKey) } // 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) (*repoData, error) { //nolint:dupl +func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, compositeKey string) (*repoData, error) { //nolint:dupl type Version struct { Epoch string `xml:"epoch,attr"` Version string `xml:"ver,attr"` @@ -478,11 +481,12 @@ func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs Xmlns: "http://linux.duke.edu/metadata/other", PackageCount: len(pfs), Packages: packages, - }) + }, + compositeKey) } // 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) (*repoData, error) { //nolint:dupl +func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, compositeKey string) (*repoData, error) { //nolint:dupl type Version struct { Epoch string `xml:"epoch,attr"` Version string `xml:"ver,attr"` @@ -525,7 +529,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) } // writtenCounter counts all written bytes @@ -545,10 +549,8 @@ func (wc *writtenCounter) Written() int64 { return wc.written } -func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any) (*repoData, error) { +func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any, compositeKey string) (*repoData, error) { content, _ := packages_module.NewHashedBuffer() - defer content.Close() - gzw := gzip.NewWriter(content) wc := &writtenCounter{} h := sha256.New() @@ -571,7 +573,8 @@ func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, pv, &packages_service.PackageFileCreationInfo{ PackageFileInfo: packages_service.PackageFileInfo{ - Filename: filename, + Filename: filename, + CompositeKey: compositeKey, }, Creator: user_model.NewGhostUser(), Data: content, diff --git a/templates/package/content/rpm.tmpl b/templates/package/content/rpm.tmpl index 3fd979567c0..4fd54a31977 100644 --- a/templates/package/content/rpm.tmpl +++ b/templates/package/content/rpm.tmpl @@ -4,19 +4,23 @@
-
# {{ctx.Locale.Tr "packages.rpm.distro.redhat"}}
-dnf config-manager --add-repo 
+				
# {{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 
 
-# {{ctx.Locale.Tr "packages.rpm.distro.suse"}}
-zypper addrepo 
+# {{ctx.Locale.Tr "packages.rpm.distros.suse"}} +zypper addrepo
-
# {{ctx.Locale.Tr "packages.rpm.distro.redhat"}}
+					
# {{ctx.Locale.Tr "packages.rpm.distros.redhat"}}
 dnf install {{$.PackageDescriptor.Package.Name}}
 
-# {{ctx.Locale.Tr "packages.rpm.distro.suse"}}
+# {{ctx.Locale.Tr "packages.rpm.distros.suse"}}
 zypper install {{$.PackageDescriptor.Package.Name}}
diff --git a/tests/integration/api_packages_rpm_test.go b/tests/integration/api_packages_rpm_test.go index caf6f86381e..822b0b040e7 100644 --- a/tests/integration/api_packages_rpm_test.go +++ b/tests/integration/api_packages_rpm_test.go @@ -76,12 +76,12 @@ Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5 t.Run("RepositoryConfig", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", rootURL+".repo") + req := NewRequest(t, "GET", rootURL+"/el9/stable.repo") resp := MakeRequest(t, req, http.StatusOK) - expected := fmt.Sprintf(`[gitea-%s] -name=%s - %s -baseurl=%sapi/packages/%s/rpm + expected := fmt.Sprintf(`[gitea-%s-el9-stable] +name=%s - %s - el9 - stable +baseurl=%sapi/packages/%s/rpm/el9/stable/ 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) @@ -100,7 +100,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppN }) t.Run("Upload", func(t *testing.T) { - url := rootURL + "/upload" + url := rootURL + "/el9/stable/upload" req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) MakeRequest(t, req, http.StatusUnauthorized) @@ -118,7 +118,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, packageVersion, pd.Version.Version) + assert.Equal(t, fmt.Sprintf("el9/stable/%s", packageVersion), pd.Version.Version) pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) assert.NoError(t, err) @@ -138,7 +138,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/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)) + req := NewRequest(t, "GET", fmt.Sprintf("%s/el9/stable/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)) resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, content, resp.Body.Bytes()) @@ -147,7 +147,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 + "/repodata" + url := rootURL + "/el9/stable/repodata" req := NewRequest(t, "HEAD", url+"/dummy.xml") MakeRequest(t, req, http.StatusNotFound) @@ -201,8 +201,8 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppN switch d.Type { case "primary": - assert.EqualValues(t, 718, d.Size) - assert.EqualValues(t, 1729, d.OpenSize) + assert.EqualValues(t, 722, d.Size) + assert.EqualValues(t, 1759, d.OpenSize) assert.Equal(t, "repodata/primary.xml.gz", d.Location.Href) case "filelists": assert.EqualValues(t, 257, d.Size) @@ -311,7 +311,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppN 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", packageName, packageVersion, packageArchitecture), p.Location.Href) + 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) @@ -401,18 +401,17 @@ 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/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)) + 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/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)). + 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/package/%s/%s/%s", rootURL, packageName, packageVersion, packageArchitecture)). + 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) })