mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-05 18:31:03 +02:00
1b200dc3da
Resolves https://github.com/go-gitea/gitea/issues/37124 This PR adds support for RPM Errata (security advisories, bugfixes, and enhancements) to Gitea's built-in RPM registry. --------- Signed-off-by: Rohan Guliani <rohansguliani@google.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
737 lines
20 KiB
Go
737 lines
20 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package rpm
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
packages_model "code.gitea.io/gitea/models/packages"
|
|
rpm_model "code.gitea.io/gitea/models/packages/rpm"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/json"
|
|
packages_module "code.gitea.io/gitea/modules/packages"
|
|
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
|
|
"code.gitea.io/gitea/modules/util"
|
|
packages_service "code.gitea.io/gitea/services/packages"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
|
)
|
|
|
|
// GetOrCreateRepositoryVersion gets or creates the internal repository package
|
|
// The RPM registry needs multiple metadata files which are stored in this package.
|
|
func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) {
|
|
return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeRpm, rpm_module.RepositoryPackage, rpm_module.RepositoryVersion)
|
|
}
|
|
|
|
// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files
|
|
func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
|
|
priv, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPrivate)
|
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
|
return "", "", err
|
|
}
|
|
|
|
pub, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPublic)
|
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
|
return "", "", err
|
|
}
|
|
|
|
if priv == "" || pub == "" {
|
|
priv, pub, err = generateKeypair()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
if err := user_model.SetUserSetting(ctx, ownerID, rpm_module.SettingKeyPrivate, priv); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
if err := user_model.SetUserSetting(ctx, ownerID, rpm_module.SettingKeyPublic, pub); err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
return priv, pub, nil
|
|
}
|
|
|
|
func generateKeypair() (string, string, error) {
|
|
e, err := openpgp.NewEntity("", "RPM Registry", "", nil)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
var priv strings.Builder
|
|
var pub strings.Builder
|
|
|
|
w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if err := e.SerializePrivate(w, nil); err != nil {
|
|
return "", "", err
|
|
}
|
|
w.Close()
|
|
|
|
w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if err := e.Serialize(w); err != nil {
|
|
return "", "", err
|
|
}
|
|
w.Close()
|
|
|
|
return priv.String(), pub.String(), nil
|
|
}
|
|
|
|
// BuildAllRepositoryFiles (re)builds all repository files for every available group
|
|
func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
|
|
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 1. Delete all existing repository files
|
|
pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, pf := range pfs {
|
|
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 2. (Re)Build repository files for existing packages
|
|
groups, err := rpm_model.GetGroups(ctx, ownerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, group := range groups {
|
|
if err := BuildSpecificRepositoryFiles(ctx, ownerID, group); err != nil {
|
|
return fmt.Errorf("failed to build repository files [%s]: %w", group, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type repoChecksum struct {
|
|
Value string `xml:",chardata"`
|
|
Type string `xml:"type,attr"`
|
|
}
|
|
|
|
type repoLocation struct {
|
|
Href string `xml:"href,attr"`
|
|
}
|
|
|
|
type repoData struct {
|
|
Type string `xml:"type,attr"`
|
|
Checksum repoChecksum `xml:"checksum"`
|
|
OpenChecksum repoChecksum `xml:"open-checksum"`
|
|
Location repoLocation `xml:"location"`
|
|
Timestamp int64 `xml:"timestamp"`
|
|
Size int64 `xml:"size"`
|
|
OpenSize int64 `xml:"open-size"`
|
|
}
|
|
|
|
type packageData struct {
|
|
Package *packages_model.Package
|
|
Version *packages_model.PackageVersion
|
|
Blob *packages_model.PackageBlob
|
|
VersionMetadata *rpm_module.VersionMetadata
|
|
FileMetadata *rpm_module.FileMetadata
|
|
}
|
|
|
|
type packageCache = map[*packages_model.PackageFile]*packageData
|
|
|
|
// BuildSpecificRepositoryFiles builds metadata files for the repository
|
|
func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, group 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",
|
|
CompositeKey: group,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete the repository files if there are no packages
|
|
if len(pfs) == 0 {
|
|
pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, pf := range pfs {
|
|
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Cache data needed for all repository files
|
|
cache := make(packageCache)
|
|
for _, pf := range pfs {
|
|
pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p, err := packages_model.GetPackageByID(ctx, pv.PackageID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, rpm_module.PropertyMetadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pd := &packageData{
|
|
Package: p,
|
|
Version: pv,
|
|
Blob: pb,
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil {
|
|
return err
|
|
}
|
|
if len(pps) > 0 {
|
|
if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
cache[pf] = pd
|
|
}
|
|
|
|
primary, err := buildPrimary(ctx, pv, pfs, cache, group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filelists, err := buildFilelists(ctx, pv, pfs, cache, group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
other, err := buildOther(ctx, pv, pfs, cache, group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data := []*repoData{primary, filelists, other}
|
|
|
|
updates := collectUpdateInfoUpdates(pfs, cache)
|
|
if len(updates) > 0 {
|
|
updateInfo, err := buildUpdateInfo(ctx, pv, updates, group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data = append(data, updateInfo)
|
|
}
|
|
|
|
return buildRepomd(
|
|
ctx,
|
|
pv,
|
|
ownerID,
|
|
data,
|
|
group,
|
|
)
|
|
}
|
|
|
|
// 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, group string) error {
|
|
type Repomd struct {
|
|
XMLName xml.Name `xml:"repomd"`
|
|
Xmlns string `xml:"xmlns,attr"`
|
|
XmlnsRpm string `xml:"xmlns:rpm,attr"`
|
|
Data []*repoData `xml:"data"`
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteString(xml.Header)
|
|
if err := xml.NewEncoder(&buf).Encode(&Repomd{
|
|
Xmlns: "http://linux.duke.edu/metadata/repo",
|
|
XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
|
|
Data: data,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
block, err := armor.Decode(strings.NewReader(priv))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
repomdAscContent, _ := packages_module.NewHashedBuffer()
|
|
defer repomdAscContent.Close()
|
|
|
|
if err := openpgp.ArmoredDetachSign(repomdAscContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
repomdContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
|
|
defer repomdContent.Close()
|
|
|
|
for _, file := range []struct {
|
|
Name string
|
|
Data packages_module.HashedSizeReader
|
|
}{
|
|
{"repomd.xml", repomdContent},
|
|
{"repomd.xml.asc", repomdAscContent},
|
|
} {
|
|
_, err = packages_service.AddFileToPackageVersionInternal(
|
|
ctx,
|
|
pv,
|
|
&packages_service.PackageFileCreationInfo{
|
|
PackageFileInfo: packages_service.PackageFileInfo{
|
|
Filename: file.Name,
|
|
CompositeKey: group,
|
|
},
|
|
Creator: user_model.NewGhostUser(),
|
|
Data: file.Data,
|
|
IsLead: false,
|
|
OverwriteExisting: true,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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, group string) (*repoData, error) {
|
|
type Version struct {
|
|
Epoch string `xml:"epoch,attr"`
|
|
Version string `xml:"ver,attr"`
|
|
Release string `xml:"rel,attr"`
|
|
}
|
|
|
|
type Checksum struct {
|
|
Checksum string `xml:",chardata"`
|
|
Type string `xml:"type,attr"`
|
|
Pkgid string `xml:"pkgid,attr"`
|
|
}
|
|
|
|
type Times struct {
|
|
File uint64 `xml:"file,attr"`
|
|
Build uint64 `xml:"build,attr"`
|
|
}
|
|
|
|
type Sizes struct {
|
|
Package int64 `xml:"package,attr"`
|
|
Installed uint64 `xml:"installed,attr"`
|
|
Archive uint64 `xml:"archive,attr"`
|
|
}
|
|
|
|
type Location struct {
|
|
Href string `xml:"href,attr"`
|
|
}
|
|
|
|
type EntryList struct {
|
|
Entries []*rpm_module.Entry `xml:"rpm:entry"`
|
|
}
|
|
|
|
type Format struct {
|
|
License string `xml:"rpm:license"`
|
|
Vendor string `xml:"rpm:vendor"`
|
|
Group string `xml:"rpm:group"`
|
|
Buildhost string `xml:"rpm:buildhost"`
|
|
Sourcerpm string `xml:"rpm:sourcerpm"`
|
|
Provides EntryList `xml:"rpm:provides"`
|
|
Requires EntryList `xml:"rpm:requires"`
|
|
Conflicts EntryList `xml:"rpm:conflicts"`
|
|
Obsoletes EntryList `xml:"rpm:obsoletes"`
|
|
Files []*rpm_module.File `xml:"file"`
|
|
}
|
|
|
|
type Package struct {
|
|
XMLName xml.Name `xml:"package"`
|
|
Type string `xml:"type,attr"`
|
|
Name string `xml:"name"`
|
|
Architecture string `xml:"arch"`
|
|
Version Version `xml:"version"`
|
|
Checksum Checksum `xml:"checksum"`
|
|
Summary string `xml:"summary"`
|
|
Description string `xml:"description"`
|
|
Packager string `xml:"packager"`
|
|
URL string `xml:"url"`
|
|
Time Times `xml:"time"`
|
|
Size Sizes `xml:"size"`
|
|
Location Location `xml:"location"`
|
|
Format Format `xml:"format"`
|
|
}
|
|
|
|
type Metadata struct {
|
|
XMLName xml.Name `xml:"metadata"`
|
|
Xmlns string `xml:"xmlns,attr"`
|
|
XmlnsRpm string `xml:"xmlns:rpm,attr"`
|
|
PackageCount int `xml:"packages,attr"`
|
|
Packages []*Package `xml:"package"`
|
|
}
|
|
|
|
packages := make([]*Package, 0, len(pfs))
|
|
for _, pf := range pfs {
|
|
pd := c[pf]
|
|
|
|
files := make([]*rpm_module.File, 0, 3)
|
|
for _, f := range pd.FileMetadata.Files {
|
|
if f.IsExecutable {
|
|
files = append(files, f)
|
|
}
|
|
}
|
|
packages = append(packages, &Package{
|
|
Type: "rpm",
|
|
Name: pd.Package.Name,
|
|
Architecture: pd.FileMetadata.Architecture,
|
|
Version: Version{
|
|
Epoch: pd.FileMetadata.Epoch,
|
|
Version: pd.FileMetadata.Version,
|
|
Release: pd.FileMetadata.Release,
|
|
},
|
|
Checksum: Checksum{
|
|
Type: "sha256",
|
|
Checksum: pd.Blob.HashSHA256,
|
|
Pkgid: "YES",
|
|
},
|
|
Summary: pd.VersionMetadata.Summary,
|
|
Description: pd.VersionMetadata.Description,
|
|
Packager: pd.FileMetadata.Packager,
|
|
URL: pd.VersionMetadata.ProjectURL,
|
|
Time: Times{
|
|
File: pd.FileMetadata.FileTime,
|
|
Build: pd.FileMetadata.BuildTime,
|
|
},
|
|
Size: Sizes{
|
|
Package: pd.Blob.Size,
|
|
Installed: pd.FileMetadata.InstalledSize,
|
|
Archive: pd.FileMetadata.ArchiveSize,
|
|
},
|
|
Location: Location{
|
|
Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture),
|
|
},
|
|
Format: Format{
|
|
License: pd.VersionMetadata.License,
|
|
Vendor: pd.FileMetadata.Vendor,
|
|
Group: pd.FileMetadata.Group,
|
|
Buildhost: pd.FileMetadata.BuildHost,
|
|
Sourcerpm: pd.FileMetadata.SourceRpm,
|
|
Provides: EntryList{
|
|
Entries: pd.FileMetadata.Provides,
|
|
},
|
|
Requires: EntryList{
|
|
Entries: pd.FileMetadata.Requires,
|
|
},
|
|
Conflicts: EntryList{
|
|
Entries: pd.FileMetadata.Conflicts,
|
|
},
|
|
Obsoletes: EntryList{
|
|
Entries: pd.FileMetadata.Obsoletes,
|
|
},
|
|
Files: files,
|
|
},
|
|
})
|
|
}
|
|
|
|
return addDataAsFileToRepo(ctx, pv, "primary", &Metadata{
|
|
Xmlns: "http://linux.duke.edu/metadata/common",
|
|
XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
|
|
PackageCount: len(pfs),
|
|
Packages: packages,
|
|
}, group)
|
|
}
|
|
|
|
// 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, group string) (*repoData, error) { //nolint:dupl // duplicates with buildOther
|
|
type Version struct {
|
|
Epoch string `xml:"epoch,attr"`
|
|
Version string `xml:"ver,attr"`
|
|
Release string `xml:"rel,attr"`
|
|
}
|
|
|
|
type Package struct {
|
|
Pkgid string `xml:"pkgid,attr"`
|
|
Name string `xml:"name,attr"`
|
|
Architecture string `xml:"arch,attr"`
|
|
Version Version `xml:"version"`
|
|
Files []*rpm_module.File `xml:"file"`
|
|
}
|
|
|
|
type Filelists struct {
|
|
XMLName xml.Name `xml:"filelists"`
|
|
Xmlns string `xml:"xmlns,attr"`
|
|
PackageCount int `xml:"packages,attr"`
|
|
Packages []*Package `xml:"package"`
|
|
}
|
|
|
|
packages := make([]*Package, 0, len(pfs))
|
|
for _, pf := range pfs {
|
|
pd := c[pf]
|
|
|
|
packages = append(packages, &Package{
|
|
Pkgid: pd.Blob.HashSHA256,
|
|
Name: pd.Package.Name,
|
|
Architecture: pd.FileMetadata.Architecture,
|
|
Version: Version{
|
|
Epoch: pd.FileMetadata.Epoch,
|
|
Version: pd.FileMetadata.Version,
|
|
Release: pd.FileMetadata.Release,
|
|
},
|
|
Files: pd.FileMetadata.Files,
|
|
})
|
|
}
|
|
|
|
return addDataAsFileToRepo(ctx, pv, "filelists", &Filelists{
|
|
Xmlns: "http://linux.duke.edu/metadata/other",
|
|
PackageCount: len(pfs),
|
|
Packages: packages,
|
|
}, group)
|
|
}
|
|
|
|
// 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, group string) (*repoData, error) { //nolint:dupl // duplicates with buildFilelists
|
|
type Version struct {
|
|
Epoch string `xml:"epoch,attr"`
|
|
Version string `xml:"ver,attr"`
|
|
Release string `xml:"rel,attr"`
|
|
}
|
|
|
|
type Package struct {
|
|
Pkgid string `xml:"pkgid,attr"`
|
|
Name string `xml:"name,attr"`
|
|
Architecture string `xml:"arch,attr"`
|
|
Version Version `xml:"version"`
|
|
Changelogs []*rpm_module.Changelog `xml:"changelog"`
|
|
}
|
|
|
|
type Otherdata struct {
|
|
XMLName xml.Name `xml:"otherdata"`
|
|
Xmlns string `xml:"xmlns,attr"`
|
|
PackageCount int `xml:"packages,attr"`
|
|
Packages []*Package `xml:"package"`
|
|
}
|
|
|
|
packages := make([]*Package, 0, len(pfs))
|
|
for _, pf := range pfs {
|
|
pd := c[pf]
|
|
|
|
packages = append(packages, &Package{
|
|
Pkgid: pd.Blob.HashSHA256,
|
|
Name: pd.Package.Name,
|
|
Architecture: pd.FileMetadata.Architecture,
|
|
Version: Version{
|
|
Epoch: pd.FileMetadata.Epoch,
|
|
Version: pd.FileMetadata.Version,
|
|
Release: pd.FileMetadata.Release,
|
|
},
|
|
Changelogs: pd.FileMetadata.Changelogs,
|
|
})
|
|
}
|
|
|
|
return addDataAsFileToRepo(ctx, pv, "other", &Otherdata{
|
|
Xmlns: "http://linux.duke.edu/metadata/other",
|
|
PackageCount: len(pfs),
|
|
Packages: packages,
|
|
}, group)
|
|
}
|
|
|
|
func collectUpdateInfoUpdates(pfs []*packages_model.PackageFile, c packageCache) (updates []*rpm_module.Update) {
|
|
seenVersions := make(map[int64]bool)
|
|
for _, pf := range pfs {
|
|
pd := c[pf]
|
|
if pd.Version != nil && !seenVersions[pd.Version.ID] && pd.VersionMetadata.Updates != nil {
|
|
updates = append(updates, pd.VersionMetadata.Updates...)
|
|
seenVersions[pd.Version.ID] = true
|
|
}
|
|
}
|
|
return updates
|
|
}
|
|
|
|
// buildUpdateInfo builds the updateinfo.xml file
|
|
func buildUpdateInfo(ctx context.Context, pv *packages_model.PackageVersion, updates []*rpm_module.Update, group string) (*repoData, error) {
|
|
// Group updates by ID to merge package lists
|
|
type updateKey struct {
|
|
ID string
|
|
}
|
|
updateMap := make(map[updateKey]*rpm_module.Update)
|
|
|
|
for _, u := range updates {
|
|
key := updateKey{ID: u.ID}
|
|
if existing, ok := updateMap[key]; ok {
|
|
for _, newColl := range u.PkgList {
|
|
collFound := false
|
|
for j, existingColl := range existing.PkgList {
|
|
if existingColl.Short == newColl.Short {
|
|
for _, newPkg := range newColl.Packages {
|
|
pkgFound := false
|
|
for _, existingPkg := range existingColl.Packages {
|
|
if existingPkg.Name == newPkg.Name &&
|
|
existingPkg.Version == newPkg.Version &&
|
|
existingPkg.Release == newPkg.Release &&
|
|
existingPkg.Arch == newPkg.Arch {
|
|
pkgFound = true
|
|
break
|
|
}
|
|
}
|
|
if !pkgFound {
|
|
existing.PkgList[j].Packages = append(existing.PkgList[j].Packages, newPkg)
|
|
}
|
|
}
|
|
collFound = true
|
|
break
|
|
}
|
|
}
|
|
if !collFound {
|
|
collCopy := *newColl
|
|
collCopy.Packages = append([]*rpm_module.UpdatePackage(nil), newColl.Packages...)
|
|
existing.PkgList = append(existing.PkgList, &collCopy)
|
|
}
|
|
}
|
|
} else {
|
|
// Create a shallow copy so we don't mutate the original cached pointer
|
|
uCopy := *u
|
|
// Deep copy PkgList and Collections to avoid mutating cache
|
|
// Note: References is shallow-copied, but safe as long as it remains immutable
|
|
uCopy.PkgList = make([]*rpm_module.Collection, len(u.PkgList))
|
|
for i, coll := range u.PkgList {
|
|
collCopy := *coll
|
|
collCopy.Packages = append([]*rpm_module.UpdatePackage(nil), coll.Packages...)
|
|
uCopy.PkgList[i] = &collCopy
|
|
}
|
|
updateMap[key] = &uCopy
|
|
}
|
|
}
|
|
|
|
var mergedUpdates []*rpm_module.Update
|
|
for _, u := range updateMap {
|
|
mergedUpdates = append(mergedUpdates, u)
|
|
}
|
|
slices.SortFunc(mergedUpdates, func(a, b *rpm_module.Update) int {
|
|
return strings.Compare(a.ID, b.ID)
|
|
})
|
|
|
|
type updateInfo struct {
|
|
XMLName xml.Name `xml:"updates"`
|
|
Xmlns string `xml:"xmlns,attr"`
|
|
Updates []*rpm_module.Update `xml:"update"`
|
|
}
|
|
|
|
return addDataAsFileToRepo(ctx, pv, "updateinfo", &updateInfo{
|
|
Xmlns: "http://linux.duke.edu/metadata/updateinfo",
|
|
Updates: mergedUpdates,
|
|
}, group)
|
|
}
|
|
|
|
// writtenCounter counts all written bytes
|
|
type writtenCounter struct {
|
|
written int64
|
|
}
|
|
|
|
func (wc *writtenCounter) Write(buf []byte) (int, error) {
|
|
n := len(buf)
|
|
|
|
wc.written += int64(n)
|
|
|
|
return n, nil
|
|
}
|
|
|
|
func (wc *writtenCounter) Written() int64 {
|
|
return wc.written
|
|
}
|
|
|
|
func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any, group string) (*repoData, error) {
|
|
content, _ := packages_module.NewHashedBuffer()
|
|
defer content.Close()
|
|
|
|
gzw := gzip.NewWriter(content)
|
|
wc := &writtenCounter{}
|
|
h := sha256.New()
|
|
|
|
w := io.MultiWriter(gzw, wc, h)
|
|
_, _ = w.Write([]byte(xml.Header))
|
|
|
|
if err := xml.NewEncoder(w).Encode(obj); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := gzw.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filename := filetype + ".xml.gz"
|
|
|
|
_, err := packages_service.AddFileToPackageVersionInternal(
|
|
ctx,
|
|
pv,
|
|
&packages_service.PackageFileCreationInfo{
|
|
PackageFileInfo: packages_service.PackageFileInfo{
|
|
Filename: filename,
|
|
CompositeKey: group,
|
|
},
|
|
Creator: user_model.NewGhostUser(),
|
|
Data: content,
|
|
IsLead: false,
|
|
OverwriteExisting: true,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, _, hashSHA256, _ := content.Sums()
|
|
|
|
return &repoData{
|
|
Type: filetype,
|
|
Checksum: repoChecksum{
|
|
Type: "sha256",
|
|
Value: hex.EncodeToString(hashSHA256),
|
|
},
|
|
OpenChecksum: repoChecksum{
|
|
Type: "sha256",
|
|
Value: hex.EncodeToString(h.Sum(nil)),
|
|
},
|
|
Location: repoLocation{
|
|
Href: "repodata/" + filename,
|
|
},
|
|
Timestamp: time.Now().Unix(),
|
|
Size: content.Size(),
|
|
OpenSize: wc.Written(),
|
|
}, nil
|
|
}
|