From 5495b5d1264fde6443566880b0c3c2c84b3450e0 Mon Sep 17 00:00:00 2001 From: chethenry Date: Tue, 21 Apr 2026 04:52:28 -0600 Subject: [PATCH] When the requested arch rpm is missing fall back to noarch (#37236) This fixes: https://github.com/go-gitea/gitea/issues/37235 It uses the same changeset alpine packages got in: https://github.com/go-gitea/gitea/issues/26691 --------- Co-authored-by: Lunny Xiao Co-authored-by: wxiaoguang --- routers/api/packages/rpm/rpm.go | 51 ++++---- tests/integration/api_packages_rpm_test.go | 129 ++++++++++++++++++--- 2 files changed, 141 insertions(+), 39 deletions(-) diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index 51cedd2a9f..d4fdb0affa 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" "time" @@ -220,30 +221,38 @@ func UploadPackageFile(ctx *context.Context) { func DownloadPackageFile(ctx *context.Context) { name := ctx.PathParam("name") version := ctx.PathParam("version") + architecture := ctx.PathParam("architecture") + group := ctx.PathParam("group") - s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( - ctx, - &packages_service.PackageInfo{ - Owner: ctx.Package.Owner, - PackageType: packages_model.TypeRpm, - Name: name, - Version: version, - }, - &packages_service.PackageFileInfo{ - Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.PathParam("architecture")), - CompositeKey: ctx.PathParam("group"), - }, - ctx.Req.Method, - ) - if err != nil { - if errors.Is(err, util.ErrNotExist) { - apiError(ctx, http.StatusNotFound, err) - } else { - apiError(ctx, http.StatusInternalServerError, err) - } - return + openForDownload := func(filename string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { + return packages_service.OpenFileForDownloadByPackageNameAndVersion( + ctx, + &packages_service.PackageInfo{ + Owner: ctx.Package.Owner, + PackageType: packages_model.TypeRpm, + Name: name, + Version: version, + }, + &packages_service.PackageFileInfo{ + Filename: filename, + CompositeKey: group, + }, + ctx.Req.Method, + ) } + s, u, pf, err := openForDownload(fmt.Sprintf("%s-%s.%s.rpm", name, version, architecture)) + if errors.Is(err, util.ErrNotExist) && architecture != "noarch" { + s, u, pf, err = openForDownload(fmt.Sprintf("%s-%s.%s.rpm", name, version, "noarch")) + } + + if errors.Is(err, util.ErrNotExist) { + apiError(ctx, http.StatusNotFound, err) + return + } else if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } helper.ServePackageFile(ctx, s, u, pf) } diff --git a/tests/integration/api_packages_rpm_test.go b/tests/integration/api_packages_rpm_test.go index ecde21cc70..493964ebbc 100644 --- a/tests/integration/api_packages_rpm_test.go +++ b/tests/integration/api_packages_rpm_test.go @@ -32,14 +32,24 @@ import ( func TestPackageRpm(t *testing.T) { defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) packageName := "gitea-test" packageVersion := "1.0.2-1" packageArchitecture := "x86_64" - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - base64RpmPackageContent := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF + // To build an RPM package, it needs tools lik "cpio", it's not easy to do in Golang. + // So here we only use pre-built package contents. And to save space, the content is gzipped and base64 encoded. + rpmContentFromGzipBase64 := func(t testing.TB, s string) []byte { + b, err := base64.StdEncoding.DecodeString(s) + require.NoError(t, err) + zr, err := gzip.NewReader(bytes.NewReader(b)) + require.NoError(t, err) + content, err := io.ReadAll(zr) + require.NoError(t, err) + return content + } + packageRpmGzipBase64 := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF VNwk2zd2PdvZ9Sxnd3Z3NllNsmF3o6congVFsWFHRWwIImIXfRER0QcRfPBJEXvvBQvWSfZTT0VQ 8TF/MuU33zcz3+zOJGEe73lyuQBRBWKWRzDrEddjuVAkxLMc+lsFUOWfm5bvvReAalWECg/TsivU dyKa0U61aVnl6wj0Uxe4nc8F92hZiaYE8CO/P0r7/Quegr0c7M/AvoCaGZEIWNGUqMHrhhGROIUT @@ -67,14 +77,8 @@ Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5 9tMDyaXb7OAlk5acuPn57ss9mw6Wym0m1Fq2cej7tUt2LL4/b8enXU2fndk+fvv57ndnt55/cQob 7tpp/pEjDS7cGPZ6BY430+7danDq6f42Nw49b9F7zp6BiKpJb9s5P0AYN2+L159cnrur636rx+v1 7ae1K28QbMMcqI8CqwIrgwg9nTOp8Oj9q81plUY7ZuwXN8Vvs8wbAAA=` - rpmPackageContent, err := base64.StdEncoding.DecodeString(base64RpmPackageContent) - assert.NoError(t, err) - zr, err := gzip.NewReader(bytes.NewReader(rpmPackageContent)) - assert.NoError(t, err) - - content, err := io.ReadAll(zr) - assert.NoError(t, err) + packageRpmContent := rpmContentFromGzipBase64(t, packageRpmGzipBase64) decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) { t.Helper() @@ -130,10 +134,10 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, t.Run("Upload", func(t *testing.T) { url := groupURL + "/upload" - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)) + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)) MakeRequest(t, req, http.StatusUnauthorized) - req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)). + req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusCreated) @@ -156,9 +160,9 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, pb, err := packages.GetBlobByID(t.Context(), pfs[0].BlobID) assert.NoError(t, err) - assert.Equal(t, int64(len(content)), pb.Size) + assert.Equal(t, int64(len(packageRpmContent)), pb.Size) - req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)). + req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusConflict) }) @@ -169,12 +173,12 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, // download the package without the file name 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()) + assert.Equal(t, packageRpmContent, resp.Body.Bytes()) // download the package with a file name (it can be anything) req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s/any-file-name", groupURL, packageName, packageVersion, packageArchitecture)) resp = MakeRequest(t, req, http.StatusOK) - assert.Equal(t, content, resp.Body.Bytes()) + assert.Equal(t, packageRpmContent, resp.Body.Bytes()) }) t.Run("Repository", func(t *testing.T) { @@ -332,7 +336,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, 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, len(packageRpmContent), 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) @@ -744,6 +748,95 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, }) }) + t.Run("NoArch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + noarchPackageName := "gitea-noarch-test" + noarchPackageVersion := "1.0.0-1" + noarchPackageGzipBase64 := `H4sICKC05mkAA2dpdGVhLW5vYXJjaC10ZXN0LTEuMC4wLTEubm9hcmNoLnJwbQDtmLtrFEEcx+dy` + + `p0ZROYliFMUTLJJiz53HPgbBBwZR0Jgiglo5sztzLt6Lu0uIYmEhFhFUsBUJahd8gHZWKvgnpLES` + + `VIwYo43a6Dm7+zvfhbXsF+ZmP/N7zM5y03wXZt89yyOjbiXqKGHVG6IVnLQ6qt2xcNku2xZG/6wc` + + `WvL70qXbr3PwuAyh4gMz74TnW2YumqJVZl76vQPKrQEeTjn/2swFM6rAb9N61Ezr84sQPwfx9xA/` + + `b8Il6nEpWSgwVz7lrqOZ9oWjqKZSShIQT/k4cHWIAtcJHNsR1OGKSp8w5QcMM+4SiSV1OSY293wi` + + `TAMmmBauL1XoeJ7mXAqhafL6BXzzSd+N2eFP9x8/nHt25u7CBbN49t8/YKZMmTJlypQpU6ZMmTJl` + + `ypTpP1biiXS73Sso8TR+8U1KCOU+mHkXSnyN3HPICc3oh5yeTxL7Jn3A88Brgd8Ab0Q/fJTlZmwC` + + `XgC2gd+h1FfZDbwI9SPAHyB+EPgjxMeAP0O/ceAvED8B/BVYp1xYC1wDXg/nuww8CPvFvlHePG6A` + + `+D3gjcBd4KG0X24d1B9N63Nw3sKxND9XApaQPwQcAm8HVsAMWANz4CrwjpQHrsJ+8D0GnkIcvsfA` + + `C9j/OPBLyL8GPA/xmZj3oj/8OZT4cwij0WStFK+VmiI4JSoKjf8EZdMgevVgRjbqqg1/mEMHxtGR` + + `erupgkhHKkTVqD4xhdLuf27VswLL7VZQbjVrf3mZ9KVX9IZJqkZyaG+j1mypdluF+6KqGhU11R5G` + + `EItXRqKKKf6xNiZOVxsiSW7vF5NqrKV0NDWMqNmfWRixsptYkgx+iV1O/Mn+nldpHSYlq4KCZtRA` + + `lTNRE3E4lDWp6mGjZaUHTWomOtrykdCUKxxgLjTzfKG0J2wqFfeVFjyMzT1CfC255lRRn5HQDT2J` + + `mcDMdQRzeNqL0NBmhEimlTDpnoeV7xGPYSmpZ3vcljQIbYf6SmjJXEwoVT6xpc+k4wkS/7fSC97t` + + `xvcCFbdcTO92X57apoHNSquLs6s3b3s0uHXPpes77+yZnp5eaa7m3PxbJ3YYvwFme3wbyRUAAA==` + noarchRpmPackageContent := rpmContentFromGzipBase64(t, noarchPackageGzipBase64) + // Upload the noarch RPM + req := NewRequestWithBody(t, "PUT", groupURL+"/upload", bytes.NewReader(noarchRpmPackageContent)).AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + // The noarch package should appear in primary.xml when any arch is requested. + // At this point the group contains the existing x86_64 package + this noarch one. + req = NewRequest(t, "GET", groupURL+"/repodata/primary.xml.gz") + resp := MakeRequest(t, req, http.StatusOK) + + type PackageSummary struct { + Name string `xml:"name"` + Architecture string `xml:"arch"` + } + type PrimaryMetadata struct { + XMLName xml.Name `xml:"metadata"` + PackageCount int `xml:"packages,attr"` + Packages []PackageSummary `xml:"package"` + } + + var primary PrimaryMetadata + decodeGzipXML(t, resp, &primary) + + // Both the arch-specific package uploaded earlier and the noarch one must be present. + assert.Equal(t, 2, primary.PackageCount) + assert.Len(t, primary.Packages, 2) + + archNames := make([]string, 0, len(primary.Packages)) + for _, p := range primary.Packages { + archNames = append(archNames, p.Architecture) + } + assert.Contains(t, archNames, packageArchitecture) // x86_64 from the Upload subtest + assert.Contains(t, archNames, "noarch") + + // noarch package must be downloadable regardless of the arch path used. + for _, arch := range []string{"noarch", "x86_64", "aarch64", "my_arch"} { + req = NewRequest(t, "GET", fmt.Sprintf( + "%s/package/%s/%s/%s", + groupURL, noarchPackageName, noarchPackageVersion, arch, + )) + MakeRequest(t, req, http.StatusOK) + } + + // Clean up: delete via the canonical noarch path. + req = NewRequest(t, "DELETE", fmt.Sprintf( + "%s/package/%s/%s/noarch", + groupURL, noarchPackageName, noarchPackageVersion, + )).AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + + // After deletion, the noarch package must no longer be reachable via any arch. + for _, arch := range []string{"noarch", "x86_64", "aarch64"} { + req = NewRequest(t, "GET", fmt.Sprintf( + "%s/package/%s/%s/%s", + groupURL, noarchPackageName, noarchPackageVersion, arch, + )) + MakeRequest(t, req, http.StatusNotFound) + } + + // The x86_64 package from the Upload subtest must still be present. + req = NewRequest(t, "GET", groupURL+"/repodata/primary.xml.gz") + resp = MakeRequest(t, req, http.StatusOK) + + var primaryAfter PrimaryMetadata + decodeGzipXML(t, resp, &primaryAfter) + assert.Equal(t, 1, primaryAfter.PackageCount) + assert.Equal(t, packageArchitecture, primaryAfter.Packages[0].Architecture) + }) + t.Run("Delete", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -765,7 +858,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, t.Run("UploadSign", func(t *testing.T) { defer tests.PrintCurrentTest(t)() url := groupURL + "/upload?sign=true" - req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)). + req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusCreated)