初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package alpine
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
alpine_model "gitea.dev/models/packages/alpine"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/json"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
alpine_module "gitea.dev/modules/packages/alpine"
|
||||
"gitea.dev/modules/util"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
)
|
||||
|
||||
const (
|
||||
IndexFilename = "APKINDEX"
|
||||
IndexArchiveFilename = IndexFilename + ".tar.gz"
|
||||
)
|
||||
|
||||
// GetOrCreateRepositoryVersion gets or creates the internal repository package
|
||||
// The Alpine registry needs multiple index 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.TypeAlpine, alpine_module.RepositoryPackage, alpine_module.RepositoryVersion)
|
||||
}
|
||||
|
||||
// GetOrCreateKeyPair gets or creates the RSA keys used to sign repository files
|
||||
func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
|
||||
priv, err := user_model.GetSetting(ctx, ownerID, alpine_module.SettingKeyPrivate)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
pub, err := user_model.GetSetting(ctx, ownerID, alpine_module.SettingKeyPublic)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if priv == "" || pub == "" {
|
||||
priv, pub, err = util.GenerateKeyPair(4096)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if err := user_model.SetUserSetting(ctx, ownerID, alpine_module.SettingKeyPrivate, priv); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if err := user_model.SetUserSetting(ctx, ownerID, alpine_module.SettingKeyPublic, pub); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
return priv, pub, nil
|
||||
}
|
||||
|
||||
// BuildAllRepositoryFiles (re)builds all repository files for every available branches, repositories and architectures
|
||||
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
|
||||
branches, err := alpine_model.GetBranches(ctx, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, branch := range branches {
|
||||
repositories, err := alpine_model.GetRepositories(ctx, ownerID, branch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, repository := range repositories {
|
||||
architectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, architecture := range architectures {
|
||||
if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
|
||||
return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", branch, repository, architecture, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildSpecificRepositoryFiles builds index files for the repository
|
||||
func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) error {
|
||||
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
architectures := container.SetOf(architecture)
|
||||
if architecture == alpine_module.NoArch {
|
||||
// Update all other architectures too when updating the noarch index
|
||||
additionalArchitectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
architectures.AddMultiple(additionalArchitectures...)
|
||||
}
|
||||
|
||||
for architecture := range architectures {
|
||||
if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type packageData struct {
|
||||
Package *packages_model.Package
|
||||
Version *packages_model.PackageVersion
|
||||
Blob *packages_model.PackageBlob
|
||||
VersionMetadata *alpine_module.VersionMetadata
|
||||
FileMetadata *alpine_module.FileMetadata
|
||||
}
|
||||
|
||||
type packageCache = map[*packages_model.PackageFile]*packageData
|
||||
|
||||
func searchPackageFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) ([]*packages_model.PackageFile, error) {
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
OwnerID: ownerID,
|
||||
PackageType: packages_model.TypeAlpine,
|
||||
Query: "%.apk",
|
||||
Properties: map[string]string{
|
||||
alpine_module.PropertyBranch: branch,
|
||||
alpine_module.PropertyRepository: repository,
|
||||
alpine_module.PropertyArchitecture: architecture,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pfs, nil
|
||||
}
|
||||
|
||||
// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
|
||||
func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
|
||||
pfs, err := searchPackageFiles(ctx, ownerID, branch, repository, architecture)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if architecture != alpine_module.NoArch {
|
||||
// Add all noarch packages too
|
||||
noarchFiles, err := searchPackageFiles(ctx, ownerID, branch, repository, alpine_module.NoArch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pfs = append(pfs, noarchFiles...)
|
||||
}
|
||||
|
||||
// Delete the package indices if there are no packages
|
||||
if len(pfs) == 0 {
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
} else if pf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return packages_service.DeletePackageFile(ctx, pf)
|
||||
}
|
||||
|
||||
// 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, alpine_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
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for _, pf := range pfs {
|
||||
pd := cache[pf]
|
||||
|
||||
fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum)
|
||||
fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name)
|
||||
fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version)
|
||||
fmt.Fprintf(&buf, "A:%s\n", architecture)
|
||||
if pd.VersionMetadata.Description != "" {
|
||||
fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description)
|
||||
}
|
||||
if pd.VersionMetadata.ProjectURL != "" {
|
||||
fmt.Fprintf(&buf, "U:%s\n", pd.VersionMetadata.ProjectURL)
|
||||
}
|
||||
if pd.VersionMetadata.License != "" {
|
||||
fmt.Fprintf(&buf, "L:%s\n", pd.VersionMetadata.License)
|
||||
}
|
||||
fmt.Fprintf(&buf, "S:%d\n", pd.Blob.Size)
|
||||
fmt.Fprintf(&buf, "I:%d\n", pd.FileMetadata.Size)
|
||||
fmt.Fprintf(&buf, "o:%s\n", pd.FileMetadata.Origin)
|
||||
fmt.Fprintf(&buf, "m:%s\n", pd.VersionMetadata.Maintainer)
|
||||
fmt.Fprintf(&buf, "t:%d\n", pd.FileMetadata.BuildDate)
|
||||
if pd.FileMetadata.CommitHash != "" {
|
||||
fmt.Fprintf(&buf, "c:%s\n", pd.FileMetadata.CommitHash)
|
||||
}
|
||||
if len(pd.FileMetadata.Dependencies) > 0 {
|
||||
fmt.Fprintf(&buf, "D:%s\n", strings.Join(pd.FileMetadata.Dependencies, " "))
|
||||
}
|
||||
if len(pd.FileMetadata.Provides) > 0 {
|
||||
fmt.Fprintf(&buf, "p:%s\n", strings.Join(pd.FileMetadata.Provides, " "))
|
||||
}
|
||||
if pd.FileMetadata.InstallIf != "" {
|
||||
fmt.Fprintf(&buf, "i:%s\n", pd.FileMetadata.InstallIf)
|
||||
}
|
||||
if pd.FileMetadata.ProviderPriority > 0 {
|
||||
fmt.Fprintf(&buf, "k:%d\n", pd.FileMetadata.ProviderPriority)
|
||||
}
|
||||
fmt.Fprint(&buf, "\n")
|
||||
}
|
||||
|
||||
unsignedIndexContent, _ := packages_module.NewHashedBuffer()
|
||||
defer unsignedIndexContent.Close()
|
||||
|
||||
h := sha1.New()
|
||||
|
||||
if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), IndexFilename, buf.Bytes(), true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privPem, _ := pem.Decode([]byte(priv))
|
||||
if privPem == nil {
|
||||
return errors.New("failed to decode private key pem")
|
||||
}
|
||||
|
||||
privKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sign, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA1, h.Sum(nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
owner, err := user_model.GetUserByID(ctx, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fingerprint, err := util.CreatePublicKeyFingerprint(&privKey.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedIndexContent, _ := packages_module.NewHashedBuffer()
|
||||
defer signedIndexContent.Close()
|
||||
|
||||
if err := writeGzipStream(
|
||||
signedIndexContent,
|
||||
fmt.Sprintf(".SIGN.RSA.%s@%s.rsa.pub", owner.LowerName, hex.EncodeToString(fingerprint)),
|
||||
sign,
|
||||
false,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(signedIndexContent, unsignedIndexContent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = packages_service.AddFileToPackageVersionInternal(
|
||||
ctx,
|
||||
repoVersion,
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: IndexArchiveFilename,
|
||||
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
|
||||
},
|
||||
Creator: user_model.NewGhostUser(),
|
||||
Data: signedIndexContent,
|
||||
IsLead: false,
|
||||
OverwriteExisting: true,
|
||||
Properties: map[string]string{
|
||||
alpine_module.PropertyBranch: branch,
|
||||
alpine_module.PropertyRepository: repository,
|
||||
alpine_module.PropertyArchitecture: architecture,
|
||||
},
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func writeGzipStream(w io.Writer, filename string, content []byte, addTarEnd bool) error {
|
||||
zw := gzip.NewWriter(w)
|
||||
defer zw.Close()
|
||||
|
||||
tw := tar.NewWriter(zw)
|
||||
if addTarEnd {
|
||||
defer tw.Close()
|
||||
}
|
||||
hdr := &tar.Header{
|
||||
Name: filename,
|
||||
Mode: 0o600,
|
||||
Size: int64(len(content)),
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tw.Write(content); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
arch_model "gitea.dev/models/packages/arch"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/globallock"
|
||||
"gitea.dev/modules/json"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
arch_module "gitea.dev/modules/packages/arch"
|
||||
"gitea.dev/modules/util"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
const (
|
||||
IndexArchiveFilename = "packages.db"
|
||||
)
|
||||
|
||||
func AcquireRegistryLock(ctx context.Context, ownerID int64) (globallock.ReleaseFunc, error) {
|
||||
return globallock.Lock(ctx, fmt.Sprintf("packages_arch_%d", ownerID))
|
||||
}
|
||||
|
||||
// GetOrCreateRepositoryVersion gets or creates the internal repository package
|
||||
// The Arch registry needs multiple index 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.TypeArch, arch_module.RepositoryPackage, arch_module.RepositoryVersion)
|
||||
}
|
||||
|
||||
// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository files
|
||||
func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
|
||||
priv, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPrivate)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
pub, err := user_model.GetSetting(ctx, ownerID, arch_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, arch_module.SettingKeyPrivate, priv); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPublic, pub); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
return priv, pub, nil
|
||||
}
|
||||
|
||||
func generateKeypair() (string, string, error) {
|
||||
e, err := openpgp.NewEntity("", "Arch 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
|
||||
}
|
||||
|
||||
func SignData(ctx context.Context, ownerID int64, r io.Reader) ([]byte, error) {
|
||||
priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := armor.Decode(strings.NewReader(priv))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := openpgp.DetachSign(buf, e, r, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// BuildAllRepositoryFiles (re)builds all repository files for every available repositories and architectures
|
||||
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
|
||||
repositories, err := arch_model.GetRepositories(ctx, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, repository := range repositories {
|
||||
architectures, err := arch_model.GetArchitectures(ctx, ownerID, repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, architecture := range architectures {
|
||||
if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil {
|
||||
return fmt.Errorf("failed to build repository files [%s/%s]: %w", repository, architecture, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildSpecificRepositoryFiles builds index files for the repository
|
||||
func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, repository, architecture string) error {
|
||||
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
architectures := container.SetOf(architecture)
|
||||
if architecture == arch_module.AnyArch {
|
||||
// Update all other architectures too when updating the any index
|
||||
additionalArchitectures, err := arch_model.GetArchitectures(ctx, ownerID, repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
architectures.AddMultiple(additionalArchitectures...)
|
||||
}
|
||||
|
||||
for architecture := range architectures {
|
||||
if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchPackageFiles(ctx context.Context, ownerID int64, repository, architecture string) ([]*packages_model.PackageFile, error) {
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
OwnerID: ownerID,
|
||||
PackageType: packages_model.TypeArch,
|
||||
Query: "%.pkg.tar.%",
|
||||
Properties: map[string]string{
|
||||
arch_module.PropertyRepository: repository,
|
||||
arch_module.PropertyArchitecture: architecture,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pfs, nil
|
||||
}
|
||||
|
||||
func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, repository, architecture string) error {
|
||||
pfs, err := searchPackageFiles(ctx, ownerID, repository, architecture)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if architecture != arch_module.AnyArch {
|
||||
// Add all any packages too
|
||||
anyarchFiles, err := searchPackageFiles(ctx, ownerID, repository, arch_module.AnyArch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pfs = append(pfs, anyarchFiles...)
|
||||
}
|
||||
|
||||
// Delete the package indices if there are no packages
|
||||
if len(pfs) == 0 {
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s", repository, architecture))
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
} else if pf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return packages_service.DeletePackageFile(ctx, pf)
|
||||
}
|
||||
|
||||
vpfs := make(map[int64]*entryOptions)
|
||||
for _, pf := range pfs {
|
||||
current := &entryOptions{
|
||||
File: pf,
|
||||
}
|
||||
current.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// here we compare the versions but not using SearchLatestVersions because we shouldn't allow "downgrading" to a older version by "latest" one.
|
||||
// https://wiki.archlinux.org/title/Downgrading_packages : randomly downgrading can mess up dependencies:
|
||||
// If a downgrade involves a soname change, all dependencies may need downgrading or rebuilding too.
|
||||
if old, ok := vpfs[current.Version.PackageID]; ok {
|
||||
if compareVersions(old.Version.Version, current.Version.Version) == -1 {
|
||||
vpfs[current.Version.PackageID] = current
|
||||
}
|
||||
} else {
|
||||
vpfs[current.Version.PackageID] = current
|
||||
}
|
||||
}
|
||||
|
||||
indexContent, _ := packages_module.NewHashedBuffer()
|
||||
defer indexContent.Close()
|
||||
|
||||
gw := gzip.NewWriter(indexContent)
|
||||
tw := tar.NewWriter(gw)
|
||||
|
||||
cache := make(map[int64]*packages_model.Package)
|
||||
|
||||
for _, opts := range vpfs {
|
||||
if err := json.Unmarshal([]byte(opts.Version.MetadataJSON), &opts.VersionMetadata); err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Package = cache[opts.Version.PackageID]
|
||||
if opts.Package == nil {
|
||||
opts.Package, err = packages_model.GetPackageByID(ctx, opts.Version.PackageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cache[opts.Package.ID] = opts.Package
|
||||
}
|
||||
opts.Blob, err = packages_model.GetBlobByID(ctx, opts.File.BlobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, opts.File.ID, arch_module.PropertySignature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(sig) == 0 {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
opts.Signature = sig[0].Value
|
||||
|
||||
meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, opts.File.ID, arch_module.PropertyMetadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(meta) == 0 {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
if err := json.Unmarshal([]byte(meta[0].Value), &opts.FileMetadata); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writeFiles(tw, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeDescription(tw, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tw.Close()
|
||||
gw.Close()
|
||||
|
||||
signature, err := SignData(ctx, ownerID, indexContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := indexContent.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = packages_service.AddFileToPackageVersionInternal(
|
||||
ctx,
|
||||
repoVersion,
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: IndexArchiveFilename,
|
||||
CompositeKey: fmt.Sprintf("%s|%s", repository, architecture),
|
||||
},
|
||||
Creator: user_model.NewGhostUser(),
|
||||
Data: indexContent,
|
||||
IsLead: false,
|
||||
OverwriteExisting: true,
|
||||
Properties: map[string]string{
|
||||
arch_module.PropertyRepository: repository,
|
||||
arch_module.PropertyArchitecture: architecture,
|
||||
arch_module.PropertySignature: base64.StdEncoding.EncodeToString(signature),
|
||||
},
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
type entryOptions struct {
|
||||
Package *packages_model.Package
|
||||
Version *packages_model.PackageVersion
|
||||
VersionMetadata *arch_module.VersionMetadata
|
||||
File *packages_model.PackageFile
|
||||
FileMetadata *arch_module.FileMetadata
|
||||
Blob *packages_model.PackageBlob
|
||||
Signature string
|
||||
}
|
||||
|
||||
type keyValue struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func writeFiles(tw *tar.Writer, opts *entryOptions) error {
|
||||
return writeFields(tw, fmt.Sprintf("%s-%s/files", opts.Package.Name, opts.Version.Version), []keyValue{
|
||||
{"FILES", strings.Join(opts.FileMetadata.Files, "\n")},
|
||||
})
|
||||
}
|
||||
|
||||
// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_sync.c#L562
|
||||
func writeDescription(tw *tar.Writer, opts *entryOptions) error {
|
||||
return writeFields(tw, fmt.Sprintf("%s-%s/desc", opts.Package.Name, opts.Version.Version), []keyValue{
|
||||
{"FILENAME", opts.File.Name},
|
||||
{"MD5SUM", opts.Blob.HashMD5},
|
||||
{"SHA256SUM", opts.Blob.HashSHA256},
|
||||
{"PGPSIG", opts.Signature},
|
||||
{"CSIZE", strconv.FormatInt(opts.Blob.Size, 10)},
|
||||
{"ISIZE", strconv.FormatInt(opts.FileMetadata.InstalledSize, 10)},
|
||||
{"NAME", opts.Package.Name},
|
||||
{"BASE", opts.FileMetadata.Base},
|
||||
{"ARCH", opts.FileMetadata.Architecture},
|
||||
{"VERSION", opts.Version.Version},
|
||||
{"DESC", opts.VersionMetadata.Description},
|
||||
{"URL", opts.VersionMetadata.ProjectURL},
|
||||
{"LICENSE", strings.Join(opts.VersionMetadata.Licenses, "\n")},
|
||||
{"GROUPS", strings.Join(opts.FileMetadata.Groups, "\n")},
|
||||
{"BUILDDATE", strconv.FormatInt(opts.FileMetadata.BuildDate, 10)},
|
||||
{"PACKAGER", opts.FileMetadata.Packager},
|
||||
{"PROVIDES", strings.Join(opts.FileMetadata.Provides, "\n")},
|
||||
{"REPLACES", strings.Join(opts.FileMetadata.Replaces, "\n")},
|
||||
{"CONFLICTS", strings.Join(opts.FileMetadata.Conflicts, "\n")},
|
||||
{"DEPENDS", strings.Join(opts.FileMetadata.Depends, "\n")},
|
||||
{"OPTDEPENDS", strings.Join(opts.FileMetadata.OptDepends, "\n")},
|
||||
{"MAKEDEPENDS", strings.Join(opts.FileMetadata.MakeDepends, "\n")},
|
||||
{"CHECKDEPENDS", strings.Join(opts.FileMetadata.CheckDepends, "\n")},
|
||||
})
|
||||
}
|
||||
|
||||
func writeFields(tw *tar.Writer, filename string, fields []keyValue) error {
|
||||
buf := &bytes.Buffer{}
|
||||
for _, kv := range fields {
|
||||
if kv.Value == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(buf, "%%%s%%\n%s\n\n", kv.Key, kv.Value)
|
||||
}
|
||||
|
||||
if err := tw.WriteHeader(&tar.Header{
|
||||
Name: filename,
|
||||
Size: int64(buf.Len()),
|
||||
Mode: int64(os.ModePerm),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := io.Copy(tw, buf)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// https://gitlab.archlinux.org/pacman/pacman/-/blob/d55b47e5512808b67bc944feb20c2bcc6c1a4c45/lib/libalpm/version.c
|
||||
|
||||
func parseEVR(evr string) (epoch, version, release string) {
|
||||
if before, after, f := strings.Cut(evr, ":"); f {
|
||||
epoch = before
|
||||
evr = after
|
||||
} else {
|
||||
epoch = "0"
|
||||
}
|
||||
|
||||
if before, after, f := strings.Cut(evr, "-"); f {
|
||||
version = before
|
||||
release = after
|
||||
} else {
|
||||
version = evr
|
||||
release = "1"
|
||||
}
|
||||
return epoch, version, release
|
||||
}
|
||||
|
||||
func compareSegments(a, b []string) int {
|
||||
lenA, lenB := len(a), len(b)
|
||||
l := min(lenA, lenB)
|
||||
for i := range l {
|
||||
if r := compare(a[i], b[i]); r != 0 {
|
||||
return r
|
||||
}
|
||||
}
|
||||
if lenA == lenB {
|
||||
return 0
|
||||
} else if l == lenA {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func compare(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
|
||||
aNumeric := isNumeric(a)
|
||||
bNumeric := isNumeric(b)
|
||||
|
||||
if aNumeric && bNumeric {
|
||||
aInt, _ := strconv.Atoi(a)
|
||||
bInt, _ := strconv.Atoi(b)
|
||||
switch {
|
||||
case aInt < bInt:
|
||||
return -1
|
||||
case aInt > bInt:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
if aNumeric {
|
||||
return 1
|
||||
}
|
||||
if bNumeric {
|
||||
return -1
|
||||
}
|
||||
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
func isNumeric(s string) bool {
|
||||
for _, c := range s {
|
||||
if !unicode.IsDigit(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func compareVersions(a, b string) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
|
||||
epochA, versionA, releaseA := parseEVR(a)
|
||||
epochB, versionB, releaseB := parseEVR(b)
|
||||
|
||||
if res := compareSegments([]string{epochA}, []string{epochB}); res != 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
if res := compareSegments(strings.Split(versionA, "."), strings.Split(versionB, ".")); res != 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
return compareSegments([]string{releaseA}, []string{releaseB})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
// https://man.archlinux.org/man/vercmp.8.en
|
||||
checks := [][]string{
|
||||
{"1.0a", "1.0b", "1.0beta", "1.0p", "1.0pre", "1.0rc", "1.0", "1.0.a", "1.0.1"},
|
||||
{"1", "1.0", "1.1", "1.1.1", "1.2", "2.0", "3.0.0"},
|
||||
}
|
||||
for _, check := range checks {
|
||||
for i := 0; i < len(check)-1; i++ {
|
||||
require.Equal(t, -1, compareVersions(check[i], check[i+1]))
|
||||
require.Equal(t, 1, compareVersions(check[i+1], check[i]))
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, compareVersions("1.0-2", "1.0"))
|
||||
require.Equal(t, 0, compareVersions("0:1.0-1", "1.0"))
|
||||
require.Equal(t, 1, compareVersions("1:1.0-1", "2.0"))
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type packageClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
PackageMeta
|
||||
}
|
||||
type PackageMeta struct {
|
||||
UserID int64
|
||||
Scope auth_model.AccessTokenScope
|
||||
ActionsUserTaskID int64
|
||||
}
|
||||
|
||||
func CreateAuthorizationToken(u *user_model.User, packageScope auth_model.AccessTokenScope) (string, error) {
|
||||
now := time.Now()
|
||||
|
||||
actionsUserTaskID, _ := user_model.GetActionsUserTaskID(u)
|
||||
claims := packageClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
},
|
||||
PackageMeta: PackageMeta{
|
||||
UserID: u.ID,
|
||||
Scope: packageScope,
|
||||
ActionsUserTaskID: actionsUserTaskID,
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func ParseAuthorizationRequest(req *http.Request) (*PackageMeta, error) {
|
||||
h := req.Header.Get("Authorization")
|
||||
if h == "" {
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
parts := strings.SplitN(h, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
log.Error("split token failed: %s", h)
|
||||
return nil, errors.New("split token failed")
|
||||
}
|
||||
|
||||
return ParseAuthorizationToken(parts[1])
|
||||
}
|
||||
|
||||
func ParseAuthorizationToken(tokenStr string) (*PackageMeta, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &packageClaims{}, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return setting.GetGeneralTokenSigningSecret(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, ok := token.Claims.(*packageClaims)
|
||||
if !token.Valid || !ok {
|
||||
return nil, errors.New("invalid token claim")
|
||||
}
|
||||
|
||||
return &c.PackageMeta, nil
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cargo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/json"
|
||||
cargo_module "gitea.dev/modules/packages/cargo"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
files_service "gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
const (
|
||||
IndexRepositoryName = "_cargo-index"
|
||||
ConfigFileName = "config.json"
|
||||
)
|
||||
|
||||
// https://doc.rust-lang.org/cargo/reference/registries.html#index-format
|
||||
|
||||
func BuildPackagePath(name string) string {
|
||||
switch len(name) {
|
||||
case 0:
|
||||
panic("Cargo package name can not be empty")
|
||||
case 1:
|
||||
return path.Join("1", name)
|
||||
case 2:
|
||||
return path.Join("2", name)
|
||||
case 3:
|
||||
return path.Join("3", string(name[0]), name)
|
||||
default:
|
||||
return path.Join(name[0:2], name[2:4], name)
|
||||
}
|
||||
}
|
||||
|
||||
func InitializeIndexRepository(ctx context.Context, doer, owner *user_model.User) error {
|
||||
repo, err := getOrCreateIndexRepository(ctx, doer, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := createOrUpdateConfigFile(ctx, repo, doer, owner); err != nil {
|
||||
return fmt.Errorf("createOrUpdateConfigFile: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RebuildIndex(ctx context.Context, doer, owner *user_model.User) error {
|
||||
repo, err := getOrCreateIndexRepository(ctx, doer, owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeCargo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetPackagesByType: %w", err)
|
||||
}
|
||||
|
||||
return alterRepositoryContent(
|
||||
ctx,
|
||||
doer,
|
||||
repo,
|
||||
"Rebuild Cargo Index",
|
||||
func(t *files_service.TemporaryUploadRepository) error {
|
||||
// Remove all existing content but the Cargo config
|
||||
files, err := t.LsFiles(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, file := range files {
|
||||
if file == ConfigFileName {
|
||||
files[i] = files[len(files)-1]
|
||||
files = files[:len(files)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := t.RemoveFilesFromIndex(ctx, files...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add all packages
|
||||
for _, p := range ps {
|
||||
if err := addOrUpdatePackageIndex(ctx, t, p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func UpdatePackageIndexIfExists(ctx context.Context, doer, owner *user_model.User, packageID int64) error {
|
||||
// We do not want to force the creation of the repo here
|
||||
// cargo http index does not rely on the repo itself,
|
||||
// so if the repo does not exist, we just do nothing.
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
|
||||
}
|
||||
|
||||
p, err := packages_model.GetPackageByID(ctx, packageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetPackageByID[%d]: %w", packageID, err)
|
||||
}
|
||||
|
||||
return alterRepositoryContent(
|
||||
ctx,
|
||||
doer,
|
||||
repo,
|
||||
"Update "+p.Name,
|
||||
func(t *files_service.TemporaryUploadRepository) error {
|
||||
return addOrUpdatePackageIndex(ctx, t, p)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type IndexVersionEntry struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"vers"`
|
||||
Dependencies []*cargo_module.Dependency `json:"deps"`
|
||||
FileChecksum string `json:"cksum"`
|
||||
Features map[string][]string `json:"features"`
|
||||
Yanked bool `json:"yanked"`
|
||||
Links string `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
func BuildPackageIndex(ctx context.Context, p *packages_model.Package) (*bytes.Buffer, error) {
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: p.ID,
|
||||
Sort: packages_model.SortVersionAsc,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SearchVersions[%s]: %w", p.Name, err)
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the package has no versions
|
||||
}
|
||||
|
||||
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetPackageDescriptors[%s]: %w", p.Name, err)
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
for _, pd := range pds {
|
||||
metadata := pd.Metadata.(*cargo_module.Metadata)
|
||||
|
||||
dependencies := metadata.Dependencies
|
||||
if dependencies == nil {
|
||||
dependencies = make([]*cargo_module.Dependency, 0)
|
||||
}
|
||||
|
||||
features := metadata.Features
|
||||
if features == nil {
|
||||
features = make(map[string][]string)
|
||||
}
|
||||
|
||||
yanked, _ := strconv.ParseBool(pd.VersionProperties.GetByName(cargo_module.PropertyYanked))
|
||||
entry, err := json.Marshal(&IndexVersionEntry{
|
||||
Name: pd.Package.Name,
|
||||
Version: pd.Version.Version,
|
||||
Dependencies: dependencies,
|
||||
FileChecksum: pd.Files[0].Blob.HashSHA256,
|
||||
Features: features,
|
||||
Yanked: yanked,
|
||||
Links: metadata.Links,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.Write(entry)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func addOrUpdatePackageIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, p *packages_model.Package) error {
|
||||
b, err := BuildPackageIndex(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return writeObjectToIndex(ctx, t, BuildPackagePath(p.LowerName), b)
|
||||
}
|
||||
|
||||
func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.User) (*repo_model.Repository, error) {
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
repo, err = repo_service.CreateRepositoryDirectly(ctx, doer, owner, repo_service.CreateRepoOptions{
|
||||
Name: IndexRepositoryName,
|
||||
}, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateRepository: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("GetRepositoryByOwnerAndName: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DownloadURL string `json:"dl"`
|
||||
APIURL string `json:"api"`
|
||||
AuthRequired bool `json:"auth-required"`
|
||||
}
|
||||
|
||||
func BuildConfig(owner *user_model.User, isPrivate bool) *Config {
|
||||
return &Config{
|
||||
DownloadURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo/api/v1/crates",
|
||||
APIURL: setting.AppURL + "api/packages/" + owner.Name + "/cargo",
|
||||
AuthRequired: isPrivate,
|
||||
}
|
||||
}
|
||||
|
||||
func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository, doer, owner *user_model.User) error {
|
||||
return alterRepositoryContent(
|
||||
ctx,
|
||||
doer,
|
||||
repo,
|
||||
"Initialize Cargo Config",
|
||||
func(t *files_service.TemporaryUploadRepository) error {
|
||||
var b bytes.Buffer
|
||||
err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeObjectToIndex(ctx, t, ConfigFileName, &b)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// This is a shorter version of CreateOrUpdateRepoFile which allows to perform multiple actions on a git repository
|
||||
func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitMessage string, fn func(*files_service.TemporaryUploadRepository) error) error {
|
||||
t, err := files_service.NewTemporaryUploadRepository(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer t.Close()
|
||||
|
||||
var lastCommitID string
|
||||
if err := t.Clone(ctx, repo.DefaultBranch, true); err != nil {
|
||||
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
|
||||
return err
|
||||
}
|
||||
if err := t.Init(ctx, repo.ObjectFormatName); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := t.SetDefaultIndex(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commit, err := t.GetBranchCommit(repo.DefaultBranch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lastCommitID = commit.ID.String()
|
||||
}
|
||||
|
||||
if err := fn(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
treeHash, err := t.WriteTree(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commitOpts := &files_service.CommitTreeUserOptions{
|
||||
ParentCommitID: lastCommitID,
|
||||
TreeHash: treeHash,
|
||||
CommitMessage: commitMessage,
|
||||
DoerUser: doer,
|
||||
}
|
||||
commitHash, err := t.CommitTree(ctx, commitOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return t.Push(ctx, doer, commitHash, repo.DefaultBranch, false)
|
||||
}
|
||||
|
||||
func writeObjectToIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, path string, r io.Reader) error {
|
||||
hash, err := t.HashObjectAndWrite(ctx, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return t.AddObjectToIndex(ctx, "100644", hash, path)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
alpine_service "gitea.dev/services/packages/alpine"
|
||||
arch_service "gitea.dev/services/packages/arch"
|
||||
cargo_service "gitea.dev/services/packages/cargo"
|
||||
container_service "gitea.dev/services/packages/container"
|
||||
debian_service "gitea.dev/services/packages/debian"
|
||||
rpm_service "gitea.dev/services/packages/rpm"
|
||||
)
|
||||
|
||||
// CleanupTask executes cleanup rules and cleanup expired package data
|
||||
func CleanupTask(ctx context.Context, olderThan time.Duration) error {
|
||||
if err := ExecuteCleanupRules(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return CleanupExpiredData(ctx, olderThan)
|
||||
}
|
||||
|
||||
func executeCleanupOneRulePackage(ctx context.Context, pcr *packages_model.PackageCleanupRule, p *packages_model.Package) (versionDeleted bool, err error) {
|
||||
olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays)
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: p.ID,
|
||||
IsInternal: optional.Some(false),
|
||||
Sort: packages_model.SortCreatedDesc,
|
||||
})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err)
|
||||
}
|
||||
if pcr.KeepCount > 0 {
|
||||
if pcr.KeepCount < len(pvs) {
|
||||
pvs = pvs[pcr.KeepCount:]
|
||||
} else {
|
||||
pvs = nil
|
||||
}
|
||||
}
|
||||
for _, pv := range pvs {
|
||||
if pcr.Type == packages_model.TypeContainer {
|
||||
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
|
||||
return false, fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err)
|
||||
} else if skip {
|
||||
log.Debug("Rule[%d]: keep '%s/%s' (container)", pcr.ID, p.Name, pv.Version)
|
||||
continue
|
||||
}
|
||||
}
|
||||
toMatch := pv.LowerVersion
|
||||
if pcr.MatchFullName {
|
||||
toMatch = p.LowerName + "/" + pv.LowerVersion
|
||||
}
|
||||
if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) {
|
||||
log.Debug("Rule[%d]: keep '%s/%s' (keep pattern)", pcr.ID, p.Name, pv.Version)
|
||||
continue
|
||||
}
|
||||
if pv.CreatedUnix.AsLocalTime().After(olderThan) {
|
||||
log.Debug("Rule[%d]: keep '%s/%s' (remove days) %v", pcr.ID, p.Name, pv.Version, pv.CreatedUnix.FormatDate())
|
||||
continue
|
||||
}
|
||||
if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) {
|
||||
log.Debug("Rule[%d]: keep '%s/%s' (remove pattern)", pcr.ID, p.Name, pv.Version)
|
||||
continue
|
||||
}
|
||||
log.Debug("Rule[%d]: remove '%s/%s'", pcr.ID, p.Name, pv.Version)
|
||||
if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
log.Error("CleanupRule [%d]: DeletePackageVersionAndReferences failed: %v", pcr.ID, err)
|
||||
continue
|
||||
}
|
||||
versionDeleted = true
|
||||
}
|
||||
return versionDeleted, nil
|
||||
}
|
||||
|
||||
func executeCleanupOneRule(ctx context.Context, pcr *packages_model.PackageCleanupRule) error {
|
||||
if err := pcr.CompiledPattern(); err != nil {
|
||||
return fmt.Errorf("CleanupRule [%d]: CompilePattern failed: %w", pcr.ID, err)
|
||||
}
|
||||
|
||||
packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CleanupRule [%d]: GetPackagesByType failed: %w", pcr.ID, err)
|
||||
}
|
||||
|
||||
anyVersionDeleted := false
|
||||
for _, p := range packages {
|
||||
versionDeleted := false
|
||||
err = db.WithTx(ctx, func(ctx context.Context) (err error) {
|
||||
versionDeleted, err = executeCleanupOneRulePackage(ctx, pcr, p)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("CleanupRule [%d]: executeCleanupOneRulePackage(%d) failed: %v", pcr.ID, p.ID, err)
|
||||
continue
|
||||
}
|
||||
anyVersionDeleted = anyVersionDeleted || versionDeleted
|
||||
if versionDeleted {
|
||||
if pcr.Type == packages_model.TypeCargo {
|
||||
owner, err := user_model.GetUserByID(ctx, pcr.OwnerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetUserByID failed: %w", err)
|
||||
}
|
||||
if err := cargo_service.UpdatePackageIndexIfExists(ctx, owner, owner, p.ID); err != nil {
|
||||
return fmt.Errorf("CleanupRule [%d]: cargo.UpdatePackageIndexIfExists failed: %w", pcr.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if anyVersionDeleted {
|
||||
switch pcr.Type {
|
||||
case packages_model.TypeDebian:
|
||||
if err := debian_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
|
||||
return fmt.Errorf("CleanupRule [%d]: debian.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
|
||||
}
|
||||
case packages_model.TypeAlpine:
|
||||
if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
|
||||
return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
|
||||
}
|
||||
case packages_model.TypeRpm:
|
||||
if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
|
||||
return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
|
||||
}
|
||||
case packages_model.TypeArch:
|
||||
release, err := arch_service.AcquireRegistryLock(ctx, pcr.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer release()
|
||||
|
||||
if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
|
||||
return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExecuteCleanupRules(ctx context.Context) error {
|
||||
return packages_model.IterateEnabledCleanupRules(ctx, func(ctx context.Context, pcr *packages_model.PackageCleanupRule) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return db.ErrCancelledf("While processing package cleanup rules")
|
||||
default:
|
||||
}
|
||||
|
||||
err := executeCleanupOneRule(ctx, pcr)
|
||||
if err != nil {
|
||||
log.Error("CleanupRule [%d]: executeCleanupOneRule failed: %v", pcr.ID, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CleanupExpiredData(ctx context.Context, olderThan time.Duration) error {
|
||||
pbs := make([]*packages_model.PackageBlob, 0, 100)
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := container_service.Cleanup(ctx, olderThan); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ps, err := packages_model.FindUnreferencedPackages(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range ps {
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// HINT: PACKAGE-DEFER-STORAGE-DELETE: Handle blob deletion for package storage
|
||||
pbs, err = packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pb := range pbs {
|
||||
if err := packages_model.DeleteBlobByID(ctx, pb.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contentStore := packages_module.NewContentStore()
|
||||
for _, pb := range pbs {
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob [%v]: %v", pb.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/tempdir"
|
||||
)
|
||||
|
||||
var (
|
||||
// errWriteAfterRead occurs if Write is called after a read operation
|
||||
errWriteAfterRead = errors.New("write is unsupported after a read operation")
|
||||
// errOffsetMismatch occurs if the file offset is different than the model
|
||||
errOffsetMismatch = errors.New("offset mismatch between file and model")
|
||||
)
|
||||
|
||||
// BlobUploader handles chunked blob uploads
|
||||
type BlobUploader struct {
|
||||
*packages_model.PackageBlobUpload
|
||||
*packages_module.MultiHasher
|
||||
file *os.File
|
||||
reading bool
|
||||
}
|
||||
|
||||
func uploadPathTempDir() *tempdir.TempDir {
|
||||
return setting.AppDataTempDir("package-upload")
|
||||
}
|
||||
|
||||
func buildFilePath(uploadPath *tempdir.TempDir, id string) string {
|
||||
return uploadPath.JoinPath(id)
|
||||
}
|
||||
|
||||
// NewBlobUploader creates a new blob uploader for the given id
|
||||
func NewBlobUploader(ctx context.Context, id string) (*BlobUploader, error) {
|
||||
model, err := packages_model.GetBlobUploadByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash := packages_module.NewMultiHasher()
|
||||
if len(model.HashStateBytes) != 0 {
|
||||
if err := hash.UnmarshalBinary(model.HashStateBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
uploadPath := uploadPathTempDir()
|
||||
_, err = uploadPath.MkdirAllSub("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := os.OpenFile(buildFilePath(uploadPath, model.ID), os.O_RDWR|os.O_CREATE, 0o666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BlobUploader{
|
||||
PackageBlobUpload: model,
|
||||
MultiHasher: hash,
|
||||
file: f,
|
||||
reading: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close implements io.Closer
|
||||
func (u *BlobUploader) Close() error {
|
||||
return u.file.Close()
|
||||
}
|
||||
|
||||
// Append appends a chunk of data and updates the model
|
||||
func (u *BlobUploader) Append(ctx context.Context, r io.Reader) error {
|
||||
if u.reading {
|
||||
return errWriteAfterRead
|
||||
}
|
||||
|
||||
offset, err := u.file.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if offset != u.BytesReceived {
|
||||
return errOffsetMismatch
|
||||
}
|
||||
|
||||
n, err := io.Copy(io.MultiWriter(u.file, u.MultiHasher), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// fast path if nothing was written
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.BytesReceived += n
|
||||
|
||||
u.HashStateBytes, err = u.MultiHasher.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return packages_model.UpdateBlobUpload(ctx, u.PackageBlobUpload)
|
||||
}
|
||||
|
||||
func (u *BlobUploader) Size() int64 {
|
||||
return u.BytesReceived
|
||||
}
|
||||
|
||||
// Read implements io.Reader
|
||||
func (u *BlobUploader) Read(p []byte) (int, error) {
|
||||
if !u.reading {
|
||||
_, err := u.file.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
u.reading = true
|
||||
}
|
||||
|
||||
return u.file.Read(p)
|
||||
}
|
||||
|
||||
// RemoveBlobUploadByID Remove deletes the data and the model of a blob upload
|
||||
func RemoveBlobUploadByID(ctx context.Context, id string) error {
|
||||
if err := packages_model.DeleteBlobUploadByID(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := os.Remove(buildFilePath(uploadPathTempDir(), id))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
container_model "gitea.dev/models/packages/container"
|
||||
"gitea.dev/modules/optional"
|
||||
container_module "gitea.dev/modules/packages/container"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// Cleanup removes expired container data
|
||||
func Cleanup(ctx context.Context, olderThan time.Duration) error {
|
||||
if err := cleanupExpiredBlobUploads(ctx, olderThan); err != nil {
|
||||
return err
|
||||
}
|
||||
return cleanupExpiredUploadedBlobs(ctx, olderThan)
|
||||
}
|
||||
|
||||
// cleanupExpiredBlobUploads removes expired blob uploads
|
||||
func cleanupExpiredBlobUploads(ctx context.Context, olderThan time.Duration) error {
|
||||
pbus, err := packages_model.FindExpiredBlobUploads(ctx, olderThan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pbu := range pbus {
|
||||
if err := RemoveBlobUploadByID(ctx, pbu.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupExpiredUploadedBlobs removes expired uploaded blobs not referenced by a manifest
|
||||
func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) error {
|
||||
pfs, err := container_model.SearchExpiredUploadedBlobs(ctx, olderThan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pf := range pfs {
|
||||
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
Type: packages_model.TypeContainer,
|
||||
Version: packages_model.SearchValue{
|
||||
ExactMatch: true,
|
||||
Value: container_module.UploadVersion,
|
||||
},
|
||||
IsInternal: optional.Some(true),
|
||||
HasFiles: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pv := range pvs {
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := packages_model.DeleteVersionByID(ctx, pv.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ShouldBeSkipped(ctx context.Context, pcr *packages_model.PackageCleanupRule, p *packages_model.Package, pv *packages_model.PackageVersion) (bool, error) {
|
||||
// Always skip the "latest" tag
|
||||
if pv.LowerVersion == "latest" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check if the version is a digest (or untagged)
|
||||
if digest.Digest(pv.LowerVersion).Validate() == nil {
|
||||
// Check if there is another manifest referencing this version
|
||||
has, err := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: p.ID,
|
||||
Properties: map[string]string{
|
||||
container_module.PropertyManifestReference: pv.LowerVersion,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Skip it if the version is referenced
|
||||
if has {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
container_service "gitea.dev/models/packages/container"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/packages"
|
||||
container_module "gitea.dev/modules/packages/container"
|
||||
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// UpdateRepositoryNames updates the repository name property for all packages of the specific owner
|
||||
func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwnerName string) error {
|
||||
ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeContainer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newOwnerName = strings.ToLower(newOwnerName)
|
||||
|
||||
for _, p := range ps {
|
||||
if err := packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, newOwnerName+"/"+p.LowerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseManifestMetadata(ctx context.Context, rd io.Reader, ownerID int64, imageName string) (*v1.Manifest, *packages_model.PackageFileDescriptor, *container_module.Metadata, error) {
|
||||
var manifest v1.Manifest
|
||||
if err := json.NewDecoder(rd).Decode(&manifest); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
configDescriptor, err := container_service.GetContainerBlob(ctx, &container_service.BlobSearchOptions{
|
||||
OwnerID: ownerID,
|
||||
Image: imageName,
|
||||
Digest: manifest.Config.Digest.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
configReader, err := packages.NewContentStore().OpenBlob(packages.BlobHash256Key(configDescriptor.Blob.HashSHA256))
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
defer configReader.Close()
|
||||
metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader)
|
||||
return &manifest, configDescriptor, metadata, err
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package debian
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
debian_model "gitea.dev/models/packages/debian"
|
||||
user_model "gitea.dev/models/user"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
debian_module "gitea.dev/modules/packages/debian"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/ulikunitz/xz"
|
||||
)
|
||||
|
||||
// GetOrCreateRepositoryVersion gets or creates the internal repository package
|
||||
// The Debian registry needs multiple index 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.TypeDebian, debian_module.RepositoryPackage, debian_module.RepositoryVersion)
|
||||
}
|
||||
|
||||
// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository files
|
||||
func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
|
||||
priv, err := user_model.GetSetting(ctx, ownerID, debian_module.SettingKeyPrivate)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
pub, err := user_model.GetSetting(ctx, ownerID, debian_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, debian_module.SettingKeyPrivate, priv); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if err := user_model.SetUserSetting(ctx, ownerID, debian_module.SettingKeyPublic, pub); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
return priv, pub, nil
|
||||
}
|
||||
|
||||
func generateKeypair() (string, string, error) {
|
||||
// Repository signing keys are long-lived and there is currently no rotation mechanism, choose stronger algorithms
|
||||
cfg := &packet.Config{
|
||||
RSABits: 4096,
|
||||
DefaultHash: crypto.SHA256,
|
||||
DefaultCipher: packet.CipherAES256,
|
||||
}
|
||||
|
||||
e, err := openpgp.NewEntity("", "Automatically generated Debian Registry Key; created "+time.Now().UTC().Format(time.RFC3339), "", cfg)
|
||||
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 distributions, components and architectures
|
||||
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
|
||||
distributions, err := debian_model.GetDistributions(ctx, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, distribution := range distributions {
|
||||
components, err := debian_model.GetComponents(ctx, ownerID, distribution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
architectures, err := debian_model.GetArchitectures(ctx, ownerID, distribution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, component := range components {
|
||||
for _, architecture := range architectures {
|
||||
if err := buildRepositoryFiles(ctx, ownerID, pv, distribution, component, architecture); err != nil {
|
||||
return fmt.Errorf("failed to build repository files [%s/%s/%s]: %w", distribution, component, architecture, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildSpecificRepositoryFiles builds index files for the repository
|
||||
func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, distribution, component, architecture string) error {
|
||||
pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return buildRepositoryFiles(ctx, ownerID, pv, distribution, component, architecture)
|
||||
}
|
||||
|
||||
func buildRepositoryFiles(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution, component, architecture string) error {
|
||||
if err := buildPackagesIndices(ctx, ownerID, repoVersion, distribution, component, architecture); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return buildReleaseFiles(ctx, ownerID, repoVersion, distribution)
|
||||
}
|
||||
|
||||
// https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices
|
||||
func buildPackagesIndices(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution, component, architecture string) error {
|
||||
opts := &debian_model.PackageSearchOptions{
|
||||
OwnerID: ownerID,
|
||||
Distribution: distribution,
|
||||
Component: component,
|
||||
Architecture: architecture,
|
||||
}
|
||||
|
||||
// Delete the package indices if there are no packages
|
||||
if has, err := debian_model.ExistPackages(ctx, opts); err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
key := fmt.Sprintf("%s|%s|%s", distribution, component, architecture)
|
||||
for _, filename := range []string{"Packages", "Packages.gz", "Packages.xz"} {
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, key)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
} else if pf == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
packagesContent, _ := packages_module.NewHashedBuffer()
|
||||
defer packagesContent.Close()
|
||||
|
||||
packagesGzipContent, _ := packages_module.NewHashedBuffer()
|
||||
defer packagesGzipContent.Close()
|
||||
|
||||
gzw := gzip.NewWriter(packagesGzipContent)
|
||||
|
||||
packagesXzContent, _ := packages_module.NewHashedBuffer()
|
||||
defer packagesXzContent.Close()
|
||||
|
||||
xzw, _ := xz.NewWriter(packagesXzContent)
|
||||
|
||||
w := io.MultiWriter(packagesContent, gzw, xzw)
|
||||
|
||||
addSeparator := false
|
||||
pfds, err := debian_model.SearchPackages(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, pfd := range pfds {
|
||||
if addSeparator {
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
addSeparator = true
|
||||
|
||||
fmt.Fprintf(w, "%s\n", strings.TrimSpace(pfd.Properties.GetByName(debian_module.PropertyControl)))
|
||||
|
||||
fmt.Fprintf(w, "Filename: pool/%s/%s/%s\n", distribution, component, pfd.File.Name)
|
||||
fmt.Fprintf(w, "Size: %d\n", pfd.Blob.Size)
|
||||
fmt.Fprintf(w, "MD5sum: %s\n", pfd.Blob.HashMD5)
|
||||
fmt.Fprintf(w, "SHA1: %s\n", pfd.Blob.HashSHA1)
|
||||
fmt.Fprintf(w, "SHA256: %s\n", pfd.Blob.HashSHA256)
|
||||
fmt.Fprintf(w, "SHA512: %s\n", pfd.Blob.HashSHA512)
|
||||
}
|
||||
gzw.Close()
|
||||
xzw.Close()
|
||||
|
||||
for _, file := range []struct {
|
||||
Name string
|
||||
Data packages_module.HashedSizeReader
|
||||
}{
|
||||
{"Packages", packagesContent},
|
||||
{"Packages.gz", packagesGzipContent},
|
||||
{"Packages.xz", packagesXzContent},
|
||||
} {
|
||||
_, err := packages_service.AddFileToPackageVersionInternal(
|
||||
ctx,
|
||||
repoVersion,
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: file.Name,
|
||||
CompositeKey: fmt.Sprintf("%s|%s|%s", distribution, component, architecture),
|
||||
},
|
||||
Creator: user_model.NewGhostUser(),
|
||||
Data: file.Data,
|
||||
IsLead: false,
|
||||
OverwriteExisting: true,
|
||||
Properties: map[string]string{
|
||||
debian_module.PropertyRepositoryIncludeInRelease: "",
|
||||
debian_module.PropertyDistribution: distribution,
|
||||
debian_module.PropertyComponent: component,
|
||||
debian_module.PropertyArchitecture: architecture,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
|
||||
func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, distribution string) error {
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
VersionID: repoVersion.ID,
|
||||
Properties: map[string]string{
|
||||
debian_module.PropertyRepositoryIncludeInRelease: "",
|
||||
debian_module.PropertyDistribution: distribution,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the release files if there are no packages
|
||||
if len(pfs) == 0 {
|
||||
for _, filename := range []string{"Release", "Release.gpg", "InRelease"} {
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, filename, distribution)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
} else if pf == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
components, err := debian_model.GetComponents(ctx, ownerID, distribution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(components)
|
||||
|
||||
architectures, err := debian_model.GetArchitectures(ctx, ownerID, distribution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(architectures)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
inReleaseContent, _ := packages_module.NewHashedBuffer()
|
||||
defer inReleaseContent.Close()
|
||||
|
||||
sw, err := clearsign.Encode(inReleaseContent, e.PrivateKey, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
w := io.MultiWriter(sw, &buf)
|
||||
|
||||
fmt.Fprintf(w, "Origin: %s\n", setting.AppName)
|
||||
fmt.Fprintf(w, "Label: %s\n", setting.AppName)
|
||||
fmt.Fprintf(w, "Suite: %s\n", distribution)
|
||||
fmt.Fprintf(w, "Codename: %s\n", distribution)
|
||||
fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " "))
|
||||
fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " "))
|
||||
fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123))
|
||||
fmt.Fprint(w, "Acquire-By-Hash: yes\n")
|
||||
|
||||
pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var md5, sha1, sha256, sha512 strings.Builder
|
||||
for _, pfd := range pfds {
|
||||
path := fmt.Sprintf("%s/binary-%s/%s", pfd.Properties.GetByName(debian_module.PropertyComponent), pfd.Properties.GetByName(debian_module.PropertyArchitecture), pfd.File.Name)
|
||||
fmt.Fprintf(&md5, " %s %d %s\n", pfd.Blob.HashMD5, pfd.Blob.Size, path)
|
||||
fmt.Fprintf(&sha1, " %s %d %s\n", pfd.Blob.HashSHA1, pfd.Blob.Size, path)
|
||||
fmt.Fprintf(&sha256, " %s %d %s\n", pfd.Blob.HashSHA256, pfd.Blob.Size, path)
|
||||
fmt.Fprintf(&sha512, " %s %d %s\n", pfd.Blob.HashSHA512, pfd.Blob.Size, path)
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "MD5Sum:")
|
||||
fmt.Fprint(w, md5.String())
|
||||
fmt.Fprintln(w, "SHA1:")
|
||||
fmt.Fprint(w, sha1.String())
|
||||
fmt.Fprintln(w, "SHA256:")
|
||||
fmt.Fprint(w, sha256.String())
|
||||
fmt.Fprintln(w, "SHA512:")
|
||||
fmt.Fprint(w, sha512.String())
|
||||
|
||||
sw.Close()
|
||||
|
||||
releaseGpgContent, _ := packages_module.NewHashedBuffer()
|
||||
defer releaseGpgContent.Close()
|
||||
|
||||
if err := openpgp.ArmoredDetachSign(releaseGpgContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releaseContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
|
||||
defer releaseContent.Close()
|
||||
|
||||
for _, file := range []struct {
|
||||
Name string
|
||||
Data packages_module.HashedSizeReader
|
||||
}{
|
||||
{"Release", releaseContent},
|
||||
{"Release.gpg", releaseGpgContent},
|
||||
{"InRelease", inReleaseContent},
|
||||
} {
|
||||
_, err = packages_service.AddFileToPackageVersionInternal(
|
||||
ctx,
|
||||
repoVersion,
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: file.Name,
|
||||
CompositeKey: distribution,
|
||||
},
|
||||
Creator: user_model.NewGhostUser(),
|
||||
Data: file.Data,
|
||||
IsLead: false,
|
||||
OverwriteExisting: true,
|
||||
Properties: map[string]string{
|
||||
debian_module.PropertyDistribution: distribution,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
org_model "gitea.dev/models/organization"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
func LinkToRepository(ctx context.Context, pkg *packages_model.Package, repo *repo_model.Repository, doer *user_model.User) error {
|
||||
if pkg.OwnerID != repo.OwnerID {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
if pkg.RepoID > 0 {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
perms, err := access_model.GetDoerRepoPermission(ctx, repo, doer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting permissions for user %d on repository %d: %w", doer.ID, repo.ID, err)
|
||||
}
|
||||
if !perms.CanWrite(unit.TypePackages) {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if err := packages_model.SetRepositoryLink(ctx, pkg.ID, repo.ID); err != nil {
|
||||
return fmt.Errorf("error while linking package '%v' to repo '%v' : %w", pkg.Name, repo.FullName(), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnlinkFromRepository(ctx context.Context, pkg *packages_model.Package, doer *user_model.User) error {
|
||||
if pkg.RepoID == 0 {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, pkg.RepoID)
|
||||
if err != nil && !repo_model.IsErrRepoNotExist(err) {
|
||||
return fmt.Errorf("error getting repository %d: %w", pkg.RepoID, err)
|
||||
}
|
||||
if err == nil {
|
||||
perms, err := access_model.GetDoerRepoPermission(ctx, repo, doer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting permissions for user %d on repository %d: %w", doer.ID, repo.ID, err)
|
||||
}
|
||||
if !perms.CanWrite(unit.TypePackages) {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
user, err := user_model.GetUserByID(ctx, pkg.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !doer.IsAdmin {
|
||||
if !user.IsOrganization() {
|
||||
if doer.ID != pkg.OwnerID {
|
||||
return fmt.Errorf("no permission to unlink package '%v' from its repository, or packages are disabled", pkg.Name)
|
||||
}
|
||||
} else {
|
||||
isOrgAdmin, err := org_model.OrgFromUser(user).IsOrgAdmin(ctx, doer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !isOrgAdmin {
|
||||
return fmt.Errorf("no permission to unlink package '%v' from its repository, or packages are disabled", pkg.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return packages_model.UnlinkRepository(ctx, pkg.ID)
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/globallock"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
notify_service "gitea.dev/services/notify"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrQuotaTypeSize = errors.New("maximum allowed package type size exceeded")
|
||||
ErrQuotaTotalSize = errors.New("maximum allowed package storage quota exceeded")
|
||||
ErrQuotaTotalCount = errors.New("maximum allowed package count exceeded")
|
||||
)
|
||||
|
||||
type Specialization interface {
|
||||
OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error
|
||||
OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error
|
||||
GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error)
|
||||
}
|
||||
|
||||
// PackageInfo describes a package
|
||||
type PackageInfo struct {
|
||||
Owner *user_model.User
|
||||
PackageType packages_model.Type
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
// PackageCreationInfo describes a package to create
|
||||
type PackageCreationInfo struct {
|
||||
PackageInfo
|
||||
SemverCompatible bool
|
||||
Creator *user_model.User
|
||||
Metadata any
|
||||
PackageProperties map[string]string
|
||||
VersionProperties map[string]string
|
||||
}
|
||||
|
||||
// PackageFileInfo describes a package file
|
||||
type PackageFileInfo struct {
|
||||
Filename string
|
||||
CompositeKey string
|
||||
}
|
||||
|
||||
// PackageFileCreationInfo describes a package file to create
|
||||
type PackageFileCreationInfo struct {
|
||||
PackageFileInfo
|
||||
Creator *user_model.User
|
||||
Data packages_module.HashedSizeReader
|
||||
IsLead bool
|
||||
Properties map[string]string
|
||||
OverwriteExisting bool
|
||||
}
|
||||
|
||||
// CreatePackageAndAddFile creates a package with a file. If the same package exists already, ErrDuplicatePackageVersion is returned
|
||||
func CreatePackageAndAddFile(ctx context.Context, pvci *PackageCreationInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
|
||||
return createPackageAndAddFile(ctx, pvci, pfci, false)
|
||||
}
|
||||
|
||||
// CreatePackageOrAddFileToExisting creates a package with a file or adds the file if the package exists already
|
||||
func CreatePackageOrAddFileToExisting(ctx context.Context, pvci *PackageCreationInfo, pfci *PackageFileCreationInfo) (pv *packages_model.PackageVersion, pf *packages_model.PackageFile, err error) {
|
||||
lockKey := fmt.Sprintf("pkg-upsert-%v-%v-%v", pvci.PackageType, pvci.Name, pvci.Version)
|
||||
err = globallock.LockAndDo(ctx, lockKey, func(ctx context.Context) error {
|
||||
pv, pf, err = createPackageAndAddFile(ctx, pvci, pfci, true)
|
||||
return err
|
||||
})
|
||||
return pv, pf, err
|
||||
}
|
||||
|
||||
func createPackageAndAddFile(ctx context.Context, pvci *PackageCreationInfo, pfci *PackageFileCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
|
||||
dbCtx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
pv, created, err := createPackageAndVersion(dbCtx, pvci, allowDuplicate)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pf, pb, blobCreated, err := addFileToPackageVersion(dbCtx, pv, &pvci.PackageInfo, pfci)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if blobCreated && removeBlob {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
removeBlob = true
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
removeBlob = true
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if created {
|
||||
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
notify_service.PackageCreate(ctx, pvci.Creator, pd)
|
||||
}
|
||||
|
||||
return pv, pf, nil
|
||||
}
|
||||
|
||||
func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, bool, error) {
|
||||
log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.PackageProperties, pvci.VersionProperties, allowDuplicate)
|
||||
|
||||
packageCreated := true
|
||||
p := &packages_model.Package{
|
||||
OwnerID: pvci.Owner.ID,
|
||||
Type: pvci.PackageType,
|
||||
Name: pvci.Name,
|
||||
LowerName: strings.ToLower(pvci.Name),
|
||||
SemverCompatible: pvci.SemverCompatible,
|
||||
}
|
||||
var err error
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackage) {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, false, err
|
||||
}
|
||||
packageCreated = false
|
||||
}
|
||||
|
||||
if packageCreated {
|
||||
for name, value := range pvci.PackageProperties {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, name, value); err != nil {
|
||||
log.Error("Error setting package property: %v", err)
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadataJSON, err := json.Marshal(pvci.Metadata)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
versionCreated := true
|
||||
pv := &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: pvci.Creator.ID,
|
||||
Version: pvci.Version,
|
||||
LowerVersion: strings.ToLower(pvci.Version),
|
||||
MetadataJSON: string(metadataJSON),
|
||||
}
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
|
||||
if errors.Is(err, packages_model.ErrDuplicatePackageVersion) && allowDuplicate {
|
||||
versionCreated = false
|
||||
} else {
|
||||
log.Error("Error inserting package: %v", err) // other error, or disallowing duplicates
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
if versionCreated {
|
||||
if err := CheckCountQuotaExceeded(ctx, pvci.Creator, pvci.Owner); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
for name, value := range pvci.VersionProperties {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, name, value); err != nil {
|
||||
log.Error("Error setting package version property: %v", err)
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pv, versionCreated, nil
|
||||
}
|
||||
|
||||
// AddFileToExistingPackage adds a file to an existing package. If the package does not exist, ErrPackageNotExist is returned
|
||||
func AddFileToExistingPackage(ctx context.Context, pvi *PackageInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, error) {
|
||||
return addFileToPackageWrapper(ctx, func(ctx context.Context) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
return addFileToPackageVersion(ctx, pv, pvi, pfci)
|
||||
})
|
||||
}
|
||||
|
||||
// AddFileToPackageVersionInternal adds a file to the package
|
||||
// This method skips quota checks and should only be used for system-managed packages.
|
||||
func AddFileToPackageVersionInternal(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, error) {
|
||||
return addFileToPackageWrapper(ctx, func(ctx context.Context) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
|
||||
return addFileToPackageVersionUnchecked(ctx, pv, pfci)
|
||||
})
|
||||
}
|
||||
|
||||
func addFileToPackageWrapper(ctx context.Context, fn func(ctx context.Context) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error)) (*packages_model.PackageFile, error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
pf, pb, blobCreated, err := fn(ctx)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if removeBlob {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
removeBlob = blobCreated
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
removeBlob = blobCreated
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pf, nil
|
||||
}
|
||||
|
||||
// NewPackageBlob creates a package blob instance
|
||||
func NewPackageBlob(hsr packages_module.HashedSizeReader) *packages_model.PackageBlob {
|
||||
hashMD5, hashSHA1, hashSHA256, hashSHA512 := hsr.Sums()
|
||||
|
||||
return &packages_model.PackageBlob{
|
||||
Size: hsr.Size(),
|
||||
HashMD5: hex.EncodeToString(hashMD5),
|
||||
HashSHA1: hex.EncodeToString(hashSHA1),
|
||||
HashSHA256: hex.EncodeToString(hashSHA256),
|
||||
HashSHA512: hex.EncodeToString(hashSHA512),
|
||||
}
|
||||
}
|
||||
|
||||
func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pvi *PackageInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
|
||||
if err := CheckSizeQuotaExceeded(ctx, pfci.Creator, pvi.Owner, pvi.PackageType, pfci.Data.Size()); err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
return addFileToPackageVersionUnchecked(ctx, pv, pfci)
|
||||
}
|
||||
|
||||
func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
|
||||
log.Trace("Adding package file: %v, %s", pv.ID, pfci.Filename)
|
||||
|
||||
pb, exists, err := packages_model.GetOrInsertBlob(ctx, NewPackageBlob(pfci.Data))
|
||||
if err != nil {
|
||||
log.Error("Error inserting package blob: %v", err)
|
||||
return nil, nil, false, err
|
||||
}
|
||||
if !exists {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), pfci.Data, pfci.Data.Size()); err != nil {
|
||||
log.Error("Error saving package blob in content store: %v", err)
|
||||
return nil, nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
if pfci.OverwriteExisting {
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, pfci.Filename, pfci.CompositeKey)
|
||||
if err != nil && err != packages_model.ErrPackageFileNotExist {
|
||||
return nil, pb, !exists, err
|
||||
}
|
||||
if pf != nil {
|
||||
// Short circuit if blob is the same
|
||||
if pf.BlobID == pb.ID {
|
||||
return pf, pb, !exists, nil
|
||||
}
|
||||
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
||||
return nil, pb, !exists, err
|
||||
}
|
||||
if err := packages_model.DeleteFileByID(ctx, pf.ID); err != nil {
|
||||
return nil, pb, !exists, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pf := &packages_model.PackageFile{
|
||||
VersionID: pv.ID,
|
||||
BlobID: pb.ID,
|
||||
Name: pfci.Filename,
|
||||
LowerName: strings.ToLower(pfci.Filename),
|
||||
CompositeKey: pfci.CompositeKey,
|
||||
IsLead: pfci.IsLead,
|
||||
}
|
||||
if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackageFile {
|
||||
log.Error("Error inserting package file: %v", err)
|
||||
}
|
||||
return nil, pb, !exists, err
|
||||
}
|
||||
|
||||
for name, value := range pfci.Properties {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil {
|
||||
log.Error("Error setting package file property: %v", err)
|
||||
return pf, pb, !exists, err
|
||||
}
|
||||
}
|
||||
|
||||
return pf, pb, !exists, nil
|
||||
}
|
||||
|
||||
// CheckCountQuotaExceeded checks if the owner has more than the allowed packages
|
||||
// The check is skipped if the doer is an admin.
|
||||
func CheckCountQuotaExceeded(ctx context.Context, doer, owner *user_model.User) error {
|
||||
if doer.IsAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
if setting.Packages.LimitTotalOwnerCount > -1 {
|
||||
totalCount, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: owner.ID,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("CountVersions failed: %v", err)
|
||||
return err
|
||||
}
|
||||
if totalCount > setting.Packages.LimitTotalOwnerCount {
|
||||
return ErrQuotaTotalCount
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckSizeQuotaExceeded checks if the upload size is bigger than the allowed size
|
||||
// The check is skipped if the doer is an admin.
|
||||
func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, packageType packages_model.Type, uploadSize int64) error {
|
||||
if doer.IsAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
var typeSpecificSize int64
|
||||
switch packageType {
|
||||
case packages_model.TypeAlpine:
|
||||
typeSpecificSize = setting.Packages.LimitSizeAlpine
|
||||
case packages_model.TypeArch:
|
||||
typeSpecificSize = setting.Packages.LimitSizeArch
|
||||
case packages_model.TypeCargo:
|
||||
typeSpecificSize = setting.Packages.LimitSizeCargo
|
||||
case packages_model.TypeChef:
|
||||
typeSpecificSize = setting.Packages.LimitSizeChef
|
||||
case packages_model.TypeComposer:
|
||||
typeSpecificSize = setting.Packages.LimitSizeComposer
|
||||
case packages_model.TypeConan:
|
||||
typeSpecificSize = setting.Packages.LimitSizeConan
|
||||
case packages_model.TypeConda:
|
||||
typeSpecificSize = setting.Packages.LimitSizeConda
|
||||
case packages_model.TypeContainer:
|
||||
typeSpecificSize = setting.Packages.LimitSizeContainer
|
||||
case packages_model.TypeCran:
|
||||
typeSpecificSize = setting.Packages.LimitSizeCran
|
||||
case packages_model.TypeDebian:
|
||||
typeSpecificSize = setting.Packages.LimitSizeDebian
|
||||
case packages_model.TypeGeneric:
|
||||
typeSpecificSize = setting.Packages.LimitSizeGeneric
|
||||
case packages_model.TypeGo:
|
||||
typeSpecificSize = setting.Packages.LimitSizeGo
|
||||
case packages_model.TypeHelm:
|
||||
typeSpecificSize = setting.Packages.LimitSizeHelm
|
||||
case packages_model.TypeMaven:
|
||||
typeSpecificSize = setting.Packages.LimitSizeMaven
|
||||
case packages_model.TypeNpm:
|
||||
typeSpecificSize = setting.Packages.LimitSizeNpm
|
||||
case packages_model.TypeNuGet:
|
||||
typeSpecificSize = setting.Packages.LimitSizeNuGet
|
||||
case packages_model.TypePub:
|
||||
typeSpecificSize = setting.Packages.LimitSizePub
|
||||
case packages_model.TypePyPI:
|
||||
typeSpecificSize = setting.Packages.LimitSizePyPI
|
||||
case packages_model.TypeRpm:
|
||||
typeSpecificSize = setting.Packages.LimitSizeRpm
|
||||
case packages_model.TypeRubyGems:
|
||||
typeSpecificSize = setting.Packages.LimitSizeRubyGems
|
||||
case packages_model.TypeSwift:
|
||||
typeSpecificSize = setting.Packages.LimitSizeSwift
|
||||
case packages_model.TypeTerraformState:
|
||||
typeSpecificSize = setting.Packages.LimitSizeTerraformState
|
||||
case packages_model.TypeVagrant:
|
||||
typeSpecificSize = setting.Packages.LimitSizeVagrant
|
||||
}
|
||||
if typeSpecificSize > -1 && typeSpecificSize < uploadSize {
|
||||
return ErrQuotaTypeSize
|
||||
}
|
||||
|
||||
if setting.Packages.LimitTotalOwnerSize > -1 {
|
||||
totalSize, err := packages_model.CalculateFileSize(ctx, &packages_model.PackageFileSearchOptions{
|
||||
OwnerID: owner.ID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("CalculateFileSize failed: %v", err)
|
||||
return err
|
||||
}
|
||||
if totalSize+uploadSize > setting.Packages.LimitTotalOwnerSize {
|
||||
return ErrQuotaTotalSize
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrCreateInternalPackageVersion gets or creates an internal package
|
||||
// Some package types need such internal packages for housekeeping.
|
||||
func GetOrCreateInternalPackageVersion(ctx context.Context, ownerID int64, packageType packages_model.Type, name, version string) (*packages_model.PackageVersion, error) {
|
||||
var pv *packages_model.PackageVersion
|
||||
|
||||
return pv, db.WithTx(ctx, func(ctx context.Context) error {
|
||||
p := &packages_model.Package{
|
||||
OwnerID: ownerID,
|
||||
Type: packageType,
|
||||
Name: name,
|
||||
LowerName: name,
|
||||
IsInternal: true,
|
||||
}
|
||||
var err error
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackage) {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pv = &packages_model.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: ownerID,
|
||||
Version: version,
|
||||
LowerVersion: version,
|
||||
IsInternal: true,
|
||||
MetadataJSON: "null",
|
||||
}
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
|
||||
if err != packages_model.ErrDuplicatePackageVersion {
|
||||
log.Error("Error inserting package version: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RemovePackageVersionByNameAndVersion deletes a package version and all associated files
|
||||
func RemovePackageVersionByNameAndVersion(ctx context.Context, doer *user_model.User, pvi *PackageInfo) error {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return RemovePackageVersion(ctx, doer, pv)
|
||||
}
|
||||
|
||||
// RemovePackageVersion deletes the package version and all associated files
|
||||
func RemovePackageVersion(ctx context.Context, doer *user_model.User, pv *packages_model.PackageVersion) error {
|
||||
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := GetSpecManager().Get(pd.Package.Type).OnBeforeRemovePackageVersion(ctx, doer, pd); err != nil {
|
||||
return err
|
||||
}
|
||||
// HINT: PACKAGE-DEFER-STORAGE-DELETE: Blobs are not deleted immediately, instead they are deleted by the cleanup_packages cron task.
|
||||
// If there are no more versions for the package, the same task removes that as well.
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
log.Trace("Deleting package: %v", pv.ID)
|
||||
return DeletePackageVersionAndReferences(ctx, pv)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notify_service.PackageDelete(ctx, doer, pd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePackageFileAndVersionIfUnreferenced deletes the package file and the version if there are no referenced files afterwards
|
||||
func RemovePackageFileAndVersionIfUnreferenced(ctx context.Context, doer *user_model.User, pf *packages_model.PackageFile) error {
|
||||
var pd *packages_model.PackageDescriptor
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := DeletePackageFile(ctx, pf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
has, err := packages_model.HasVersionFileReferences(ctx, pf.VersionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pd, err = packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pd != nil {
|
||||
notify_service.PackageDelete(ctx, doer, pd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePackageVersionAndReferences deletes the package version and its properties and files
|
||||
func DeletePackageVersionAndReferences(ctx context.Context, pv *packages_model.PackageVersion) error {
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeVersion, pv.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := packages_model.DeleteFilePropertiesByVersionID(ctx, pv.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := packages_model.DeleteFilesByVersionID(ctx, pv.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return packages_model.DeleteVersionByID(ctx, pv.ID)
|
||||
}
|
||||
|
||||
// DeletePackageFile deletes the package file and its properties
|
||||
func DeletePackageFile(ctx context.Context, pf *packages_model.PackageFile) error {
|
||||
if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypeFile, pf.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return packages_model.DeleteFileByID(ctx, pf.ID)
|
||||
}
|
||||
|
||||
// OpenFileForDownloadByPackageNameAndVersion returns the content of the specific package file and increases the download counter.
|
||||
func OpenFileForDownloadByPackageNameAndVersion(ctx context.Context, pvi *PackageInfo, pfi *PackageFileInfo, method string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
|
||||
log.Trace("Getting package file stream: %v, %v, %s, %s, %s, %s", pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version, pfi.Filename, pfi.CompositeKey)
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageNotExist {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
log.Error("Error getting package: %v", err)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return OpenFileForDownloadByPackageVersion(ctx, pv, pfi, method)
|
||||
}
|
||||
|
||||
// OpenFileForDownloadByPackageVersion returns the content of the specific package file and increases the download counter.
|
||||
func OpenFileForDownloadByPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfi *PackageFileInfo, method string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, pfi.Filename, pfi.CompositeKey)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return OpenFileForDownload(ctx, pf, method)
|
||||
}
|
||||
|
||||
// OpenFileForDownload returns the content of the specific package file and increases the download counter.
|
||||
func OpenFileForDownload(ctx context.Context, pf *packages_model.PackageFile, method string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
|
||||
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return OpenBlobForDownload(ctx, pf, pb, method, nil)
|
||||
}
|
||||
|
||||
func OpenBlobStream(pb *packages_model.PackageBlob) (io.ReadSeekCloser, error) {
|
||||
cs := packages_module.NewContentStore()
|
||||
key := packages_module.BlobHash256Key(pb.HashSHA256)
|
||||
return cs.OpenBlob(key)
|
||||
}
|
||||
|
||||
// OpenBlobForDownload returns the content of the specific package blob and increases the download counter.
|
||||
// If the storage supports direct serving and it's enabled, only the direct serving url is returned.
|
||||
func OpenBlobForDownload(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, method string, serveDirectReqParams *storage.ServeDirectOptions) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
|
||||
key := packages_module.BlobHash256Key(pb.HashSHA256)
|
||||
|
||||
cs := packages_module.NewContentStore()
|
||||
|
||||
var s io.ReadSeekCloser
|
||||
var u *url.URL
|
||||
var err error
|
||||
|
||||
if cs.ShouldServeDirect() {
|
||||
u, err = cs.GetServeDirectURL(key, pf.Name, method, serveDirectReqParams)
|
||||
if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
|
||||
log.Error("Error getting serve direct url (fallback to local reader): %v", err)
|
||||
}
|
||||
}
|
||||
if u == nil {
|
||||
s, err = cs.OpenBlob(key)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
if pf.IsLead && method == http.MethodGet {
|
||||
if err := packages_model.IncrementDownloadCounter(ctx, pf.VersionID); err != nil {
|
||||
log.Error("Error incrementing download counter: %v", err)
|
||||
}
|
||||
}
|
||||
return s, u, pf, nil
|
||||
}
|
||||
|
||||
// RemovePackage deletes the package and all its versions
|
||||
func RemovePackage(ctx context.Context, doer *user_model.User, p *packages_model.Package) error {
|
||||
pds, err := packages_model.GetAllPackageDescriptors(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := GetSpecManager().Get(p.Type).OnBeforeRemovePackageAll(ctx, doer, p, pds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// HINT: PACKAGE-DEFER-STORAGE-DELETE: Blobs are not deleted immediately, instead they are deleted by cleanup_packages cron task.
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
err := packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypePackage, p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypeFile, p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = packages_model.DeletePropertiesByPackageID(ctx, packages_model.PropertyTypeVersion, p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = packages_model.DeleteFilesByPackageID(ctx, p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = packages_model.DeleteVersionsByPackageID(ctx, p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return packages_model.DeletePackageByID(ctx, p.ID)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, pd := range pds {
|
||||
notify_service.PackageDelete(ctx, doer, pd)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAllPackages for User
|
||||
func RemoveAllPackages(ctx context.Context, userID int64) (int, error) {
|
||||
count := 0
|
||||
for {
|
||||
pkgVersions, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
Paginator: &db.ListOptions{
|
||||
PageSize: repo_model.RepositoryListDefaultPageSize,
|
||||
Page: 1,
|
||||
},
|
||||
OwnerID: userID,
|
||||
IsInternal: optional.None[bool](),
|
||||
})
|
||||
if err != nil {
|
||||
return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err)
|
||||
}
|
||||
if len(pkgVersions) == 0 {
|
||||
break
|
||||
}
|
||||
for _, pv := range pkgVersions {
|
||||
if err := DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
return count, fmt.Errorf("unable to delete package %d:%s[%d]. Error: %w", pv.PackageID, pv.Version, pv.ID, err)
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pkgspec
|
||||
|
||||
import (
|
||||
packages_model "gitea.dev/models/packages"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
"gitea.dev/services/packages/terraform"
|
||||
)
|
||||
|
||||
func InitManager() error {
|
||||
mgr := packages_service.GetSpecManager()
|
||||
mgr.Add(packages_model.TypeTerraformState, &terraform.Specialization{})
|
||||
// TODO: add more in the future, refactor the existing code to use this approach
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
// 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 "gitea.dev/models/packages"
|
||||
rpm_model "gitea.dev/models/packages/rpm"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
rpm_module "gitea.dev/modules/packages/rpm"
|
||||
"gitea.dev/modules/util"
|
||||
packages_service "gitea.dev/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
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package rpm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/sassoftware/go-rpmutils"
|
||||
)
|
||||
|
||||
func SignPackage(buf *packages_module.HashedBuffer, privateKey string) (*packages_module.HashedBuffer, error) {
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(privateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h, err := rpmutils.SignRpmStream(buf, keyring[0].PrivateKey, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signBlob, err := h.DumpSignatureHeader(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(int64(h.OriginalSignatureHeaderSize()), io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create new buf with signature prefix
|
||||
return packages_module.CreateHashedBufferFromReader(io.MultiReader(bytes.NewReader(signBlob), buf))
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
user_model "gitea.dev/models/user"
|
||||
)
|
||||
|
||||
type nop struct{}
|
||||
|
||||
func (n *nop) GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) {
|
||||
return nil, nil //nolint:nilnil // no data, no error
|
||||
}
|
||||
|
||||
func (n *nop) OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nop) OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ Specialization = (*nop)(nil)
|
||||
|
||||
type SpecManagerType struct {
|
||||
specMap map[packages_model.Type]Specialization
|
||||
}
|
||||
|
||||
func (m *SpecManagerType) Add(t packages_model.Type, spec Specialization) {
|
||||
m.specMap[t] = spec
|
||||
}
|
||||
|
||||
func (m *SpecManagerType) Get(t packages_model.Type) Specialization {
|
||||
if len(m.specMap) == 0 {
|
||||
panic("specialization not initialized")
|
||||
}
|
||||
spec := m.specMap[t]
|
||||
if spec == nil {
|
||||
return &nop{}
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
||||
var GetSpecManager = sync.OnceValue(func() *SpecManagerType {
|
||||
return &SpecManagerType{specMap: make(map[packages_model.Type]Specialization)}
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/optional"
|
||||
terraform_module "gitea.dev/modules/packages/terraform"
|
||||
"gitea.dev/modules/util"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
)
|
||||
|
||||
type Specialization struct{}
|
||||
|
||||
var _ packages_service.Specialization = (*Specialization)(nil)
|
||||
|
||||
func (s Specialization) GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) {
|
||||
var ret struct {
|
||||
IsLatestVersion bool
|
||||
TerraformLock *terraform_module.LockInfo
|
||||
}
|
||||
latestPvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: pd.Package.ID,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
isLatest := len(latestPvs) > 0 && latestPvs[0].ID == pd.Version.ID
|
||||
ret.IsLatestVersion = isLatest
|
||||
|
||||
if isLatest {
|
||||
lockInfo, err := terraform_module.GetLock(ctx, pd.Package.ID)
|
||||
if err != nil {
|
||||
return ret, nil
|
||||
}
|
||||
if lockInfo.IsLocked() {
|
||||
ret.TerraformLock = &lockInfo
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s Specialization) OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error {
|
||||
locked, err := IsLocked(ctx, pkg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if locked {
|
||||
return util.ErrorWrapTranslatable(
|
||||
util.ErrorWrap(util.ErrUnprocessableContent, "terraform state is locked and cannot be deleted"),
|
||||
"packages.terraform.delete.locked",
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Specialization) OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error {
|
||||
locked, err := IsLocked(ctx, pd.Package)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if locked {
|
||||
return util.ErrorWrapTranslatable(
|
||||
util.ErrorWrap(util.ErrUnprocessableContent, "terraform state is locked and cannot be deleted"),
|
||||
"packages.terraform.delete.locked",
|
||||
)
|
||||
}
|
||||
|
||||
latest, err := IsLatest(ctx, pd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if latest {
|
||||
return util.ErrorWrapTranslatable(
|
||||
util.ErrorWrap(util.ErrUnprocessableContent, "the latest version of a Terraform state cannot be deleted"),
|
||||
"packages.terraform.delete.latest",
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
"gitea.dev/modules/optional"
|
||||
terraform_module "gitea.dev/modules/packages/terraform"
|
||||
)
|
||||
|
||||
// IsLocked is a helper function to check if the terraform state is locked
|
||||
func IsLocked(ctx context.Context, pkg *packages_model.Package) (bool, error) {
|
||||
// Non terraform state packages aren't handled here
|
||||
if pkg.Type == packages_model.TypeTerraformState {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
lock, err := terraform_module.GetLock(ctx, pkg.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return lock.IsLocked(), nil
|
||||
}
|
||||
|
||||
// IsLatest is a helper function to check if the terraform state is the latest version
|
||||
func IsLatest(ctx context.Context, pd *packages_model.PackageDescriptor) (bool, error) {
|
||||
if pd.Package.Type == packages_model.TypeTerraformState {
|
||||
return false, nil
|
||||
}
|
||||
latestPvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: pd.Package.ID,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(latestPvs) > 0 && latestPvs[0].ID == pd.Version.ID {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user