feat: add local RPM repository with on-demand repodata
ci/woodpecker/pr/pre-commit Pipeline failed
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

Upload RPMs to local repos. Metadata is parsed async after upload
using cavaliergopher/rpm and stored in rpm_metadata table. Repodata
(repomd.xml, primary.xml.gz, filelists.xml.gz, other.xml.gz) is
generated on-demand from the DB — nothing stored in S3.

- RPM provider implements LocalUploader (validates .rpm extension,
  stores under Packages/)
- RPM provider implements PostUploadHook (async goroutine parses RPM
  headers, extracts name/version/arch/deps/etc into rpm_metadata)
- RPM provider implements LocalIndexer (serves repodata/* paths by
  querying rpm_metadata and generating XML on the fly)
- New provider interfaces: PostUploadHook, BlobReader, MetadataStore,
  RPMMetadataReader
- New rpm_metadata table with JSONB columns for requires/provides/
  files/changelogs

Tested e2e: upload cowsay RPM → repodata generated → dnf install
from local repo
This commit is contained in:
2026-06-23 23:08:59 +10:00
parent 3a6721c2a7
commit bb172276ba
7 changed files with 627 additions and 2 deletions
+15 -2
View File
@@ -1,6 +1,7 @@
package v2
import (
"context"
"errors"
"fmt"
"io"
@@ -58,14 +59,14 @@ func (h *LocalHandler) upload(w http.ResponseWriter, r *http.Request) {
prov, _ := provider.Get(remote.PackageType)
if uploader, ok := prov.(provider.LocalUploader); ok {
h.uploadValidated(w, r, remote, filePath, uploader)
h.uploadValidated(w, r, remote, filePath, prov, uploader)
return
}
h.uploadGeneric(w, r, remote, filePath)
}
func (h *LocalHandler) uploadValidated(w http.ResponseWriter, r *http.Request, remote *models.Remote, filePath string, uploader provider.LocalUploader) {
func (h *LocalHandler) uploadValidated(w http.ResponseWriter, r *http.Request, remote *models.Remote, filePath string, prov provider.Provider, uploader provider.LocalUploader) {
storagePath, contentType, err := uploader.ValidateUpload(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -102,6 +103,10 @@ func (h *LocalHandler) uploadValidated(w http.ResponseWriter, r *http.Request, r
return
}
if hook, ok := prov.(provider.PostUploadHook); ok {
go hook.AfterUpload(context.Background(), remote.Name, storagePath, result.ContentHash, h, h.db)
}
writeJSON(w, http.StatusCreated, uploader.UploadResponse(storagePath, result.ContentHash, result.SizeBytes))
}
@@ -190,3 +195,11 @@ func (h *LocalHandler) remove(w http.ResponseWriter, r *http.Request) {
func (h *LocalHandler) DB() *database.DB {
return h.db
}
func (h *LocalHandler) Download(ctx context.Context, key string) (io.ReadCloser, int64, error) {
reader, info, err := h.store.Download(ctx, key)
if err != nil {
return nil, 0, err
}
return reader, info.Size, nil
}