first with full handle over rpm
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
7948368573
commit
274ea454dd
50 changed files with 4309 additions and 0 deletions
212
internal/clone/rpm/metadata_gen.go
Normal file
212
internal/clone/rpm/metadata_gen.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
package rpm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const PrimarySourceFile = ".primary-source"
|
||||
const localPrimaryHref = "repodata/primary.xml.gz"
|
||||
|
||||
// filterablePackage captures location for filtering and inner XML for faithful re-emission.
|
||||
type filterablePackage struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Location struct {
|
||||
Href string `xml:"href,attr"`
|
||||
} `xml:"http://linux.duke.edu/metadata/common location"`
|
||||
Inner string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type filterableMetadata struct {
|
||||
XMLName xml.Name `xml:"http://linux.duke.edu/metadata/common metadata"`
|
||||
Packages []filterablePackage `xml:"http://linux.duke.edu/metadata/common package"`
|
||||
}
|
||||
|
||||
// RegenerateMetadata filters the upstream primary.xml.gz to only include packages
|
||||
// present on disk, writes a new primary.xml.gz, and updates repomd.xml.
|
||||
func RegenerateMetadata(localDir string) error {
|
||||
sourcePath, err := upstreamPrimaryPath(localDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gzData, err := os.ReadFile(sourcePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read upstream primary: %w", err)
|
||||
}
|
||||
gz, err := gzip.NewReader(bytes.NewReader(gzData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("open gzip: %w", err)
|
||||
}
|
||||
var meta filterableMetadata
|
||||
if err := xml.NewDecoder(gz).Decode(&meta); err != nil {
|
||||
gz.Close()
|
||||
return fmt.Errorf("parse primary.xml: %w", err)
|
||||
}
|
||||
gz.Close()
|
||||
|
||||
var local []filterablePackage
|
||||
for _, pkg := range meta.Packages {
|
||||
dest := filepath.Join(localDir, filepath.FromSlash(pkg.Location.Href))
|
||||
if _, statErr := os.Stat(dest); statErr == nil {
|
||||
local = append(local, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
var xmlBuf bytes.Buffer
|
||||
xmlBuf.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
|
||||
fmt.Fprintf(&xmlBuf,
|
||||
`<metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="%d">`,
|
||||
len(local))
|
||||
for _, pkg := range local {
|
||||
fmt.Fprintf(&xmlBuf, `<package type="%s">%s</package>`, pkg.Type, pkg.Inner)
|
||||
}
|
||||
xmlBuf.WriteString(`</metadata>`)
|
||||
xmlBytes := xmlBuf.Bytes()
|
||||
|
||||
var gzBuf bytes.Buffer
|
||||
gzw := gzip.NewWriter(&gzBuf)
|
||||
if _, err := gzw.Write(xmlBytes); err != nil {
|
||||
return fmt.Errorf("gzip write: %w", err)
|
||||
}
|
||||
if err := gzw.Close(); err != nil {
|
||||
return fmt.Errorf("gzip close: %w", err)
|
||||
}
|
||||
gzBytes := gzBuf.Bytes()
|
||||
|
||||
openSum := sha256.Sum256(xmlBytes)
|
||||
gzSum := sha256.Sum256(gzBytes)
|
||||
|
||||
outPath := filepath.Join(localDir, localPrimaryHref)
|
||||
if err := atomicWrite(outPath, gzBytes); err != nil {
|
||||
return fmt.Errorf("write primary.xml.gz: %w", err)
|
||||
}
|
||||
|
||||
return updateRepoMD(localDir, gzSum[:], openSum[:], int64(len(gzBytes)), int64(len(xmlBytes)))
|
||||
}
|
||||
|
||||
// upstreamPrimaryPath returns the full path to the original upstream primary.xml.gz,
|
||||
// persisting it in .primary-source so subsequent calls always filter from the full list.
|
||||
func upstreamPrimaryPath(localDir string) (string, error) {
|
||||
markerPath := filepath.Join(localDir, "repodata", PrimarySourceFile)
|
||||
|
||||
if data, err := os.ReadFile(markerPath); err == nil {
|
||||
href := strings.TrimSpace(string(data))
|
||||
full := filepath.Join(localDir, filepath.FromSlash(href))
|
||||
if _, err := os.Stat(full); err == nil {
|
||||
return full, nil
|
||||
}
|
||||
}
|
||||
|
||||
repomdData, err := os.ReadFile(filepath.Join(localDir, "repodata", "repomd.xml"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read repomd.xml: %w", err)
|
||||
}
|
||||
var repomd RepoMD
|
||||
if err := xml.Unmarshal(repomdData, &repomd); err != nil {
|
||||
return "", fmt.Errorf("parse repomd.xml: %w", err)
|
||||
}
|
||||
for _, entry := range repomd.Data {
|
||||
if entry.Type == "primary" && entry.Location.Href != localPrimaryHref {
|
||||
_ = os.WriteFile(markerPath, []byte(entry.Location.Href), 0o644)
|
||||
return filepath.Join(localDir, filepath.FromSlash(entry.Location.Href)), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("upstream primary source not found — repo must be cloned first")
|
||||
}
|
||||
|
||||
func updateRepoMD(localDir string, gzSum, openSum []byte, gzSize, openSize int64) error {
|
||||
repomdPath := filepath.Join(localDir, "repodata", "repomd.xml")
|
||||
repomdData, err := os.ReadFile(repomdPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read repomd.xml: %w", err)
|
||||
}
|
||||
var repomd RepoMD
|
||||
if err := xml.Unmarshal(repomdData, &repomd); err != nil {
|
||||
return fmt.Errorf("parse repomd.xml: %w", err)
|
||||
}
|
||||
for i, entry := range repomd.Data {
|
||||
if entry.Type == "primary" {
|
||||
repomd.Data[i].Location.Href = localPrimaryHref
|
||||
repomd.Data[i].Checksum = RepoMDChecksum{Type: "sha256", Value: hex.EncodeToString(gzSum)}
|
||||
repomd.Data[i].OpenChecksum = RepoMDChecksum{Type: "sha256", Value: hex.EncodeToString(openSum)}
|
||||
repomd.Data[i].Size = gzSize
|
||||
repomd.Data[i].OpenSize = openSize
|
||||
break
|
||||
}
|
||||
}
|
||||
newXML, err := xml.MarshalIndent(repomd, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal repomd.xml: %w", err)
|
||||
}
|
||||
return atomicWrite(repomdPath, append([]byte(xml.Header), newXML...))
|
||||
}
|
||||
|
||||
// InitEmptyRepo creates the directory structure and empty RPM metadata for a new repo.
|
||||
// It is idempotent: if repomd.xml already exists it is left untouched.
|
||||
func InitEmptyRepo(localDir string) error {
|
||||
for _, d := range []string{
|
||||
filepath.Join(localDir, "repodata"),
|
||||
filepath.Join(localDir, "Packages"),
|
||||
} {
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
repomdPath := filepath.Join(localDir, "repodata", "repomd.xml")
|
||||
if _, err := os.Stat(repomdPath); err == nil {
|
||||
return nil // already initialised
|
||||
}
|
||||
|
||||
emptyXML := []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
`<metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="0"></metadata>`)
|
||||
|
||||
var gzBuf bytes.Buffer
|
||||
gzw := gzip.NewWriter(&gzBuf)
|
||||
_, _ = gzw.Write(emptyXML)
|
||||
_ = gzw.Close()
|
||||
gzBytes := gzBuf.Bytes()
|
||||
|
||||
openSum := sha256.Sum256(emptyXML)
|
||||
gzSum := sha256.Sum256(gzBytes)
|
||||
|
||||
if err := atomicWrite(filepath.Join(localDir, localPrimaryHref), gzBytes); err != nil {
|
||||
return fmt.Errorf("write empty primary.xml.gz: %w", err)
|
||||
}
|
||||
|
||||
repomd := RepoMD{
|
||||
Data: []RepoMDEntry{{
|
||||
Type: "primary",
|
||||
Location: RepoMDLocation{Href: localPrimaryHref},
|
||||
Checksum: RepoMDChecksum{Type: "sha256", Value: hex.EncodeToString(gzSum[:])},
|
||||
OpenChecksum: RepoMDChecksum{Type: "sha256", Value: hex.EncodeToString(openSum[:])},
|
||||
Size: int64(len(gzBytes)),
|
||||
OpenSize: int64(len(emptyXML)),
|
||||
}},
|
||||
}
|
||||
newXML, err := xml.MarshalIndent(repomd, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return atomicWrite(repomdPath, append([]byte(xml.Header), newXML...))
|
||||
}
|
||||
|
||||
func atomicWrite(path string, data []byte) error {
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue