602 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			602 lines
		
	
	
		
			16 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"
 | 
						|
	"net/url"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	packages_model "code.gitea.io/gitea/models/packages"
 | 
						|
	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/setting"
 | 
						|
	"code.gitea.io/gitea/modules/util"
 | 
						|
	packages_service "code.gitea.io/gitea/services/packages"
 | 
						|
 | 
						|
	"github.com/keybase/go-crypto/openpgp"
 | 
						|
	"github.com/keybase/go-crypto/openpgp/armor"
 | 
						|
	"github.com/keybase/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(ownerID int64) (*packages_model.PackageVersion, error) {
 | 
						|
	return packages_service.GetOrCreateInternalPackageVersion(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(ownerID int64) (string, string, error) {
 | 
						|
	priv, err := user_model.GetSetting(ownerID, rpm_module.SettingKeyPrivate)
 | 
						|
	if err != nil && !errors.Is(err, util.ErrNotExist) {
 | 
						|
		return "", "", err
 | 
						|
	}
 | 
						|
 | 
						|
	pub, err := user_model.GetSetting(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(ownerID, rpm_module.SettingKeyPrivate, priv); err != nil {
 | 
						|
			return "", "", err
 | 
						|
		}
 | 
						|
 | 
						|
		if err := user_model.SetUserSetting(ownerID, rpm_module.SettingKeyPublic, pub); err != nil {
 | 
						|
			return "", "", err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return priv, pub, nil
 | 
						|
}
 | 
						|
 | 
						|
func generateKeypair() (string, string, error) {
 | 
						|
	e, err := openpgp.NewEntity(setting.AppName, "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
 | 
						|
}
 | 
						|
 | 
						|
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 BuildRepositoryFiles(ctx context.Context, ownerID int64) error {
 | 
						|
	pv, err := GetOrCreateRepositoryVersion(ownerID)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
 | 
						|
		OwnerID:     ownerID,
 | 
						|
		PackageType: packages_model.TypeRpm,
 | 
						|
		Query:       "%.rpm",
 | 
						|
	})
 | 
						|
	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_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
			if err := packages_model.DeleteFileByID(ctx, pf.ID); 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(pv, pfs, cache)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	filelists, err := buildFilelists(pv, pfs, cache)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	other, err := buildOther(pv, pfs, cache)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return buildRepomd(
 | 
						|
		pv,
 | 
						|
		ownerID,
 | 
						|
		[]*repoData{
 | 
						|
			primary,
 | 
						|
			filelists,
 | 
						|
			other,
 | 
						|
		},
 | 
						|
	)
 | 
						|
}
 | 
						|
 | 
						|
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml
 | 
						|
func buildRepomd(pv *packages_model.PackageVersion, ownerID int64, data []*repoData) 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.Write([]byte(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(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()
 | 
						|
	if err := openpgp.ArmoredDetachSign(repomdAscContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	repomdContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
 | 
						|
 | 
						|
	for _, file := range []struct {
 | 
						|
		Name string
 | 
						|
		Data packages_module.HashedSizeReader
 | 
						|
	}{
 | 
						|
		{"repomd.xml", repomdContent},
 | 
						|
		{"repomd.xml.asc", repomdAscContent},
 | 
						|
	} {
 | 
						|
		_, err = packages_service.AddFileToPackageVersionInternal(
 | 
						|
			pv,
 | 
						|
			&packages_service.PackageFileCreationInfo{
 | 
						|
				PackageFileInfo: packages_service.PackageFileInfo{
 | 
						|
					Filename: file.Name,
 | 
						|
				},
 | 
						|
				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(pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*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", url.PathEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(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(pv, "primary", &Metadata{
 | 
						|
		Xmlns:        "http://linux.duke.edu/metadata/common",
 | 
						|
		XmlnsRpm:     "http://linux.duke.edu/metadata/rpm",
 | 
						|
		PackageCount: len(pfs),
 | 
						|
		Packages:     packages,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml
 | 
						|
func buildFilelists(pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) { //nolint:dupl
 | 
						|
	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(pv, "filelists", &Filelists{
 | 
						|
		Xmlns:        "http://linux.duke.edu/metadata/other",
 | 
						|
		PackageCount: len(pfs),
 | 
						|
		Packages:     packages,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml
 | 
						|
func buildOther(pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache) (*repoData, error) { //nolint:dupl
 | 
						|
	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(pv, "other", &Otherdata{
 | 
						|
		Xmlns:        "http://linux.duke.edu/metadata/other",
 | 
						|
		PackageCount: len(pfs),
 | 
						|
		Packages:     packages,
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// 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(pv *packages_model.PackageVersion, filetype string, obj any) (*repoData, error) {
 | 
						|
	content, _ := packages_module.NewHashedBuffer()
 | 
						|
	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(
 | 
						|
		pv,
 | 
						|
		&packages_service.PackageFileCreationInfo{
 | 
						|
			PackageFileInfo: packages_service.PackageFileInfo{
 | 
						|
				Filename: filename,
 | 
						|
			},
 | 
						|
			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
 | 
						|
}
 |