From ce3a24ef5f80413514e8de28ff47e77acccf0d2b Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 5 Dec 2023 16:24:57 +0800
Subject: [PATCH] Add `HEAD` support for rpm repo files (#28309) (#28360)

Backport #28309 by @KN4CK3R

Fixes https://codeberg.org/forgejo/forgejo/issues/1810

zypper uses HEAD requests to check file existence.

https://github.com/openSUSE/libzypp/blob/HEAD/zypp/RepoManager.cc#L2549

https://github.com/openSUSE/libzypp/blob/HEAD/zypp-curl/ng/network/private/downloaderstates/basicdownloader_p.cc#L116

@ExplodingDragon fyi

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
(cherry picked from commit 5105d2093c55925654ccfa6e2d3130de09f2272d)
---
 routers/api/packages/api.go                |  5 ++++-
 routers/api/packages/rpm/rpm.go            | 24 ++++++++++++++++++++++
 tests/integration/api_packages_rpm_test.go |  8 +++++++-
 3 files changed, 35 insertions(+), 2 deletions(-)

diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index 2ba35e2138..722ee3f87b 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -520,7 +520,10 @@ func CommonRoutes() *web.Route {
 				r.Get("", rpm.DownloadPackageFile)
 				r.Delete("", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
 			})
-			r.Get("/repodata/{filename}", rpm.GetRepositoryFile)
+			r.Group("/repodata/{filename}", func() {
+				r.Head("", rpm.CheckRepositoryFileExistence)
+				r.Get("", rpm.GetRepositoryFile)
+			})
 		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/rubygems", func() {
 			r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go
index f5d8b67e16..2e161940b8 100644
--- a/routers/api/packages/rpm/rpm.go
+++ b/routers/api/packages/rpm/rpm.go
@@ -57,6 +57,30 @@ func GetRepositoryKey(ctx *context.Context) {
 	})
 }
 
+func CheckRepositoryFileExistence(ctx *context.Context) {
+	pv, err := rpm_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, ctx.Params("filename"), packages_model.EmptyFileKey)
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Status(http.StatusNotFound)
+		} else {
+			apiError(ctx, http.StatusInternalServerError, err)
+		}
+		return
+	}
+
+	ctx.SetServeHeaders(&context.ServeHeaderOptions{
+		Filename:     pf.Name,
+		LastModified: pf.CreatedUnix.AsLocalTime(),
+	})
+	ctx.Status(http.StatusOK)
+}
+
 // Gets a pre-generated repository metadata file
 func GetRepositoryFile(ctx *context.Context) {
 	pv, err := rpm_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID)
diff --git a/tests/integration/api_packages_rpm_test.go b/tests/integration/api_packages_rpm_test.go
index fc4c4d1c4b..6d3b0688f2 100644
--- a/tests/integration/api_packages_rpm_test.go
+++ b/tests/integration/api_packages_rpm_test.go
@@ -149,12 +149,18 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, user.Name, user.Name, setting.AppN
 
 		url := rootURL + "/repodata"
 
-		req := NewRequest(t, "GET", url+"/dummy.xml")
+		req := NewRequest(t, "HEAD", url+"/dummy.xml")
+		MakeRequest(t, req, http.StatusNotFound)
+
+		req = NewRequest(t, "GET", url+"/dummy.xml")
 		MakeRequest(t, req, http.StatusNotFound)
 
 		t.Run("repomd.xml", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
 
+			req = NewRequest(t, "HEAD", url+"/repomd.xml")
+			MakeRequest(t, req, http.StatusOK)
+
 			req = NewRequest(t, "GET", url+"/repomd.xml")
 			resp := MakeRequest(t, req, http.StatusOK)